Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Veri*Factu Addon #410

Merged
merged 32 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
eacfaa5
Initial Extensions and Validations
Nov 4, 2024
0cb518b
Exemption Extensions
apardods Nov 8, 2024
098c351
Add Invoice Tests
apardods Nov 8, 2024
a1846a3
Merge branch 'main' into addon-verifactu
apardods Nov 8, 2024
8f0cee7
Added QR Stamp
apardods Nov 11, 2024
580b5e4
Merge branch 'main' into addon-verifactu
apardods Nov 11, 2024
0ec4e55
Added Scenarios and Extensions
apardods Nov 12, 2024
033c0ce
Added All Lists as Extensions
apardods Nov 12, 2024
80fdfb3
Merge branch 'main' into addon-verifactu
apardods Nov 15, 2024
214df6f
Remove some unnecessary extensions
apardods Nov 19, 2024
c3526d8
Merge branch 'main' into addon-verifactu
apardods Nov 19, 2024
1b04b70
Remove Extension & Fix Test
apardods Nov 19, 2024
52b07e5
Added Scenario Tests
apardods Nov 22, 2024
b47d4f6
Fix Tests
apardods Nov 22, 2024
496b504
Added Invoice Tests and Updated Validation
apardods Nov 26, 2024
d0055be
Cover Preceding
apardods Nov 26, 2024
c57dc1c
Run go generate
apardods Nov 26, 2024
1b166f3
Merge branch 'main' into addon-verifactu
apardods Nov 26, 2024
bff3256
Update changelog
apardods Nov 26, 2024
056b02f
Added sources
apardods Nov 26, 2024
8cb512e
Adding tag
apardods Nov 27, 2024
2c7873a
Merge branch 'main' into addon-verifactu
apardods Dec 3, 2024
6aa13a0
Refactoring handling of Verifactu extensions and scenarios, plus othe…
samlown Dec 4, 2024
58bd731
Fixing linting issues
samlown Dec 4, 2024
59350ca
Improving cbc.Key test coverage
samlown Dec 4, 2024
5cd2148
Updating CHANGELOG
samlown Dec 5, 2024
5b5a7b7
Add Tax Regime Extension
apardods Dec 5, 2024
a504971
Merge branch 'addon-verifactu' of github.com:invopop/gobl into addon-…
apardods Dec 5, 2024
d80ef41
Remove redundant check
apardods Dec 5, 2024
92b3d2a
Name Change
apardods Dec 5, 2024
5019e86
Renaming Verifactu regime
samlown Dec 5, 2024
633fce7
Test fix
samlown Dec 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

## [Unreleased]

### Added

- `es-verifactu-v1`: added initial Spain VeriFactu addon.
- `tax`: Extensions `Get` convenience method that helps when using extensions using sub-keys.
- `tax`: `ExtensionsExclude` validator for checking that extensions do **not** include certain keys.
- `tax`: `ExtValue.In` for comparing extension values.
- `bill`: `Tax.MergeExtensions` convenience method for adding extensions to tax objects and avoid nil panics.
- `cbc`: `Key.Pop` method for splitting keys with sub-keys, e.g. `cbc.Key("a+b").Pop() == cbc.Key("a")`.

### Fixed

- `bill`: corrected issues around correction definitions and merging types.

## [v0.206.1] - 2024-11-28

### Fixed
Expand Down
1 change: 1 addition & 0 deletions addons/addons.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
_ "github.com/invopop/gobl/addons/de/xrechnung"
_ "github.com/invopop/gobl/addons/es/facturae"
_ "github.com/invopop/gobl/addons/es/tbai"
_ "github.com/invopop/gobl/addons/es/verifactu"
_ "github.com/invopop/gobl/addons/eu/en16931"
_ "github.com/invopop/gobl/addons/gr/mydata"
_ "github.com/invopop/gobl/addons/it/sdi"
Expand Down
163 changes: 163 additions & 0 deletions addons/es/verifactu/bill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package verifactu

import (
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/regimes/es"
"github.com/invopop/gobl/tax"
"github.com/invopop/validation"
)

var invoiceCorrectionDefinitions = tax.CorrectionSet{
{
Schema: bill.ShortSchemaInvoice,
Extensions: []cbc.Key{
ExtKeyDocType,
},
},
}

func normalizeInvoice(inv *bill.Invoice) {
// Try to move any preceding choices to the document level
for _, row := range inv.Preceding {
if len(row.Ext) == 0 {
continue
}
found := false
if row.Ext.Has(ExtKeyDocType) {
if inv.Tax == nil || !found {
inv.Tax = inv.Tax.MergeExtensions(tax.Extensions{
ExtKeyDocType: row.Ext[ExtKeyDocType],
})
found = true // only assign first one
}
delete(row.Ext, ExtKeyDocType)
}
}

// Try to normalize the correction type, which is especially complex for
// Verifactu implying that scenarios cannot be used.
switch inv.Type {
case bill.InvoiceTypeCreditNote, bill.InvoiceTypeDebitNote:
inv.Tax = inv.Tax.MergeExtensions(tax.Extensions{
ExtKeyCorrectionType: "I",
})
case bill.InvoiceTypeCorrective:
if inv.Tax == nil || inv.Tax.Ext.Get(ExtKeyDocType) != "F3" {
inv.Tax = inv.Tax.MergeExtensions(tax.Extensions{
ExtKeyCorrectionType: "S",
})
} else {
// Substitutions of simplified invoices cannot have a correction type
delete(inv.Tax.Ext, ExtKeyCorrectionType)
}
}
}

func validateInvoice(inv *bill.Invoice) error {
return validation.ValidateStruct(inv,
validation.Field(&inv.Customer,
validation.By(validateInvoiceCustomer),
validation.Skip,
),
validation.Field(&inv.Preceding,
validation.When(
inv.Type.In(es.InvoiceCorrectionTypes...),
validation.Required,
),
validation.Each(
validation.By(validateInvoicePreceding),
),
validation.Skip,
),
validation.Field(&inv.Tax,
validation.Required,
validation.By(validateInvoiceTax(inv.Type)),
validation.Skip,
),
validation.Field(&inv.Notes,
cbc.ValidateNotesHasKey(cbc.NoteKeyGeneral),
validation.Skip,
),
)
}

var docTypesStandard = []tax.ExtValue{ // Standard invoices
"F1", "F2",
}
var docTypesCreditDebit = []tax.ExtValue{ // Credit or Debit notes
"R1", "R2", "R3", "R4", "R5",
}
var docTypesCorrective = []tax.ExtValue{ // Substitutions
"F3", "R1", "R2", "R3", "R4", "R5",
}

func validateInvoiceTax(it cbc.Key) validation.RuleFunc {
return func(val any) error {
obj := val.(*bill.Tax)
return validation.ValidateStruct(obj,
validation.Field(&obj.Ext,
tax.ExtensionsRequires(ExtKeyDocType),
validation.When(
it.In(bill.InvoiceTypeStandard),
tax.ExtensionsHasValues(
ExtKeyDocType,
docTypesStandard...,
),
),
validation.When(
it.In(bill.InvoiceTypeCreditNote, bill.InvoiceTypeDebitNote),
tax.ExtensionsHasValues(
ExtKeyDocType,
docTypesCreditDebit...,
),
),
validation.When(
it.In(bill.InvoiceTypeCorrective),
tax.ExtensionsHasValues(
ExtKeyDocType,
docTypesCorrective...,
),
),
validation.When(
obj.Ext.Get(ExtKeyDocType).In(docTypesCreditDebit...),
tax.ExtensionsRequires(ExtKeyCorrectionType),
),
validation.Skip,
),
)
}
}

func validateInvoiceCustomer(val any) error {
obj, ok := val.(*org.Party)
if !ok || obj == nil {
return nil
}
// Customers must have a tax ID to at least set the country,
// and Spanish ones should also have an ID. There are more complex
// rules for exports.
return validation.ValidateStruct(obj,
validation.Field(&obj.TaxID,
validation.Required,
validation.When(
obj.TaxID != nil && obj.TaxID.Country.In("ES"),
tax.RequireIdentityCode,
),
validation.Skip,
),
)
}

func validateInvoicePreceding(val any) error {
p, ok := val.(*org.DocumentRef)
if !ok || p == nil {
return nil
}

Check warning on line 157 in addons/es/verifactu/bill.go

View check run for this annotation

Codecov / codecov/patch

addons/es/verifactu/bill.go#L156-L157

Added lines #L156 - L157 were not covered by tests
return validation.ValidateStruct(p,
validation.Field(&p.IssueDate,
validation.Required,
),
)
}
162 changes: 162 additions & 0 deletions addons/es/verifactu/bill_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package verifactu_test

import (
"encoding/json"
"testing"

"github.com/invopop/gobl/addons/es/verifactu"
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cal"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/num"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/tax"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestInvoiceValidation(t *testing.T) {
t.Run("standard invoice", func(t *testing.T) {
inv := testInvoiceStandard(t)
require.NoError(t, inv.Calculate())
require.NoError(t, inv.Validate())
assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "F1")
})
t.Run("missing customer tax ID", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Customer.TaxID = nil
assertValidationError(t, inv, "customer: (tax_id: cannot be blank.)")
})

t.Run("without exemption reason", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Lines[0].Taxes[0].Rate = ""
inv.Lines[0].Taxes[0].Percent = num.NewPercentage(21, 2)
inv.Lines[0].Taxes[0].Ext = nil
assertValidationError(t, inv, "es-verifactu-op-class: required")
})

t.Run("without notes", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Notes = nil
assertValidationError(t, inv, "notes: with key 'general' missing")
})

t.Run("missing doc type", func(t *testing.T) {
inv := testInvoiceStandard(t)
require.NoError(t, inv.Calculate())
inv.Tax.Ext = nil
err := inv.Validate()
require.ErrorContains(t, err, "es-verifactu-doc-type: required")
})

t.Run("simplified invoice", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.SetTags(tax.TagSimplified)
inv.Customer = nil
require.NoError(t, inv.Calculate())
require.NoError(t, inv.Validate())
assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "F2")
})

t.Run("simplified substitution", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.SetTags(tax.TagSimplified)
require.NoError(t, inv.Calculate())

require.NoError(t, inv.Correct(bill.Corrective, bill.WithExtension(verifactu.ExtKeyDocType, "F3")))
require.NoError(t, inv.Validate())
assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "F3")
assert.Empty(t, inv.Tax.Ext[verifactu.ExtKeyCorrectionType])
})

t.Run("correction invoice requires preceding", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Type = bill.InvoiceTypeCreditNote
assertValidationError(t, inv, "preceding: cannot be blank")
})

t.Run("correction invoice preceding requires issue date", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Type = bill.InvoiceTypeCreditNote
inv.Preceding = []*org.DocumentRef{
{
Code: "123",
},
}
assertValidationError(t, inv, "preceding: (0: (issue_date: cannot be blank.).)")
})

t.Run("correction invoice with preceding", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Type = bill.InvoiceTypeCreditNote
d := cal.MakeDate(2024, 1, 1)
inv.Preceding = []*org.DocumentRef{
{
Series: "ABC",
Code: "122",
IssueDate: &d,
Ext: tax.Extensions{
verifactu.ExtKeyDocType: "R1",
},
},
}
require.NoError(t, inv.Calculate())
data, _ := json.MarshalIndent(inv, "", " ")
t.Log(string(data))
require.NoError(t, inv.Validate())
assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "R1")
assert.Empty(t, inv.Preceding[0].Ext)
})

}

func assertValidationError(t *testing.T, inv *bill.Invoice, expected string) {
require.NoError(t, inv.Calculate())
err := inv.Validate()
require.ErrorContains(t, err, expected)
}

func testInvoiceStandard(t *testing.T) *bill.Invoice {
t.Helper()
return &bill.Invoice{
Addons: tax.WithAddons(verifactu.V1),
Code: "123",
Supplier: &org.Party{
Name: "Test Supplier",
TaxID: &tax.Identity{
Country: "ES",
Code: "B98602642",
},
},
Customer: &org.Party{
Name: "Test Customer",
TaxID: &tax.Identity{
Country: "NL",
Code: "000099995B57",
},
},
Lines: []*bill.Line{
{
Quantity: num.MakeAmount(1, 0),
Item: &org.Item{
Name: "bogus",
Price: num.MakeAmount(10000, 2),
Unit: org.UnitPackage,
},
Taxes: tax.Set{
{
Category: "VAT",
Rate: "standard",
},
},
},
},
Notes: []*cbc.Note{
{
Key: cbc.NoteKeyGeneral,
Text: "This is a test invoice",
},
},
}
}
Loading
Loading