From eacfaa58901cc5ee6bd01a2af0f58dbfb8e93a7e Mon Sep 17 00:00:00 2001 From: apardods Date: Mon, 4 Nov 2024 16:03:50 +0000 Subject: [PATCH 01/25] Initial Extensions and Validations --- addons/addons.go | 1 + addons/es/verifactu/extensions.go | 127 ++++++++++++++++++++++++++++++ addons/es/verifactu/invoice.go | 115 +++++++++++++++++++++++++++ addons/es/verifactu/verifactu.go | 43 ++++++++++ data/schemas/bill/invoice.json | 4 + 5 files changed, 290 insertions(+) create mode 100644 addons/es/verifactu/extensions.go create mode 100644 addons/es/verifactu/invoice.go create mode 100644 addons/es/verifactu/verifactu.go diff --git a/addons/addons.go b/addons/addons.go index c632e9b3..1e09e457 100644 --- a/addons/addons.go +++ b/addons/addons.go @@ -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" diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go new file mode 100644 index 00000000..9080308b --- /dev/null +++ b/addons/es/verifactu/extensions.go @@ -0,0 +1,127 @@ +package verifactu + +import ( + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/pkg/here" +) + +// Extension keys for TicketBAI +const ( + ExtKeyExemption cbc.Key = "es-verifactu-exemption" + ExtKeyInvoiceType cbc.Key = "es-verifactu-invoice-type" + ExtKeyCorrection cbc.Key = "es-verifactu-correction" +) + +var extensions = []*cbc.KeyDefinition{ + { + Key: ExtKeyExemption, + Name: i18n.String{ + i18n.EN: "Verifactu Exemption code", + i18n.ES: "Código de Exención de Verifactu", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Codes used by TicketBAI for both "exempt", "not-subject", and reverse + charge transactions. In the TicketBAI format these are separated, + but in order to simplify GOBL and be more closely aligned with + other countries we've combined them into one. + `), + }, + Values: []*cbc.ValueDefinition{ + { + Value: "E1", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Article 20 of the Foral VAT Law", + i18n.ES: "Exenta: por el artículo 20 de la Norma Foral del IVA", + }, + }, + { + Value: "E2", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Article 21 of the Foral VAT Law", + i18n.ES: "Exenta: por el artículo 21 de la Norma Foral del IVA", + }, + }, + { + Value: "E3", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Article 22 of the Foral VAT Law", + i18n.ES: "Exenta: por el artículo 22 de la Norma Foral del IVA", + }, + }, + { + Value: "E4", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Articles 23 and 24 of the Foral VAT Law", + i18n.ES: "Exenta: por el artículos 23 y 24 de la Norma Foral del IVA", + }, + }, + { + Value: "E5", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Article 25 of the Foral VAT law", + i18n.ES: "Exenta: por el artículo 25 de la Norma Foral del IVA", + }, + }, + { + Value: "E6", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to other reasons", + i18n.ES: "Exenta: por otra causa", + }, + }, + }, + }, + { + Key: ExtKeyCorrection, + Name: i18n.String{ + i18n.EN: "Verifactu Rectification Type Code", + i18n.ES: "Verifactu Código de Factura Rectificativa", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Corrected or rectified invoices that need to be sent in the Verifactu format + require a specific type code to be defined alongside the preceding invoice + data. + `), + }, + Values: []*cbc.ValueDefinition{ + { + Value: "R1", + Name: i18n.String{ + i18n.EN: "Rectified invoice: error based on law and Article 80 One, Two and Six LIVA", + i18n.ES: "Factura rectificativa: error fundado en derecho y Art. 80 Uno, Dos y Seis LIVA", + }, + }, + { + Value: "R2", + Name: i18n.String{ + i18n.ES: "Factura rectificativa: artículo 80.3", + i18n.EN: "Rectified invoice: error based on law and Article 80.3", + }, + }, + { + Value: "R3", + Name: i18n.String{ + i18n.ES: "Factura rectificativa: artículo 80.4", + i18n.EN: "Rectified invoice: error based on law and Article 80.4", + }, + }, + { + Value: "R4", + Name: i18n.String{ + i18n.ES: "Factura rectificativa: Resto", + i18n.EN: "Rectified invoice: Other", + }, + }, + { + Value: "R5", + Name: i18n.String{ + i18n.ES: "Factura rectificativa: facturas simplificadas", + i18n.EN: "Rectified invoice: simplified invoices", + }, + }, + }, + }, +} diff --git a/addons/es/verifactu/invoice.go b/addons/es/verifactu/invoice.go new file mode 100644 index 00000000..920b51f1 --- /dev/null +++ b/addons/es/verifactu/invoice.go @@ -0,0 +1,115 @@ +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{ + ExtKeyCorrection, + }, + }, +} + +func validateInvoice(inv *bill.Invoice) error { + return validation.ValidateStruct(inv, + validation.Field(&inv.Series, validation.Required), + validation.Field(&inv.Customer, + validation.By(validateInvoiceCustomer), + validation.Skip, + ), + validation.Field(&inv.Preceding, + validation.When( + inv.Type.In(es.InvoiceCorrectionTypes...), + validation.Required, + ), + validation.By(validateInvoicePreceding), + validation.Skip, + ), + validation.Field(&inv.Lines, + validation.Each( + validation.By(validateInvoiceLine), + validation.Skip, + ), + validation.Skip, + ), + validation.Field(&inv.Notes, + cbc.ValidateNotesHasKey(cbc.NoteKeyGeneral), + validation.Skip, + ), + ) +} + +func validateInvoiceCustomer(val any) error { + obj, _ := val.(*org.Party) + if 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 { + return nil + } + return validation.ValidateStruct(p, + validation.Field(&p.IssueDate, validation.Required), + validation.Field(&p.Series, validation.Required), + validation.Field(&p.Ext, + tax.ExtensionsRequires(ExtKeyCorrection), + validation.Skip, + ), + ) +} + +func validateInvoiceLine(value any) error { + obj, _ := value.(*bill.Line) + if obj == nil { + return nil + } + return validation.ValidateStruct(obj, + validation.Field(&obj.Taxes, + validation.Each( + validation.By(validateInvoiceLineTax), + validation.Skip, + ), + validation.Skip, + ), + ) +} + +func validateInvoiceLineTax(value any) error { + obj, ok := value.(*tax.Combo) + if obj == nil || !ok { + return nil + } + return validation.ValidateStruct(obj, + validation.Field(&obj.Ext, + validation.When( + obj.Rate == tax.RateExempt, + tax.ExtensionsRequires(ExtKeyExemption), + ), + validation.Skip, + ), + ) +} diff --git a/addons/es/verifactu/verifactu.go b/addons/es/verifactu/verifactu.go new file mode 100644 index 00000000..032a65c2 --- /dev/null +++ b/addons/es/verifactu/verifactu.go @@ -0,0 +1,43 @@ +// Package verifactu provides the Verifactu addon +package verifactu + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/tax" +) + +const ( + // V1 for Verifactu versions 1.x + V1 cbc.Key = "es-verifactu-v1" +) + +func init() { + tax.RegisterAddonDef(newAddon()) +} + +func newAddon() *tax.AddonDef { + return &tax.AddonDef{ + Key: V1, + Name: i18n.String{ + i18n.EN: "Spain Verifactu", + }, + Extensions: extensions, + Validator: validate, + Normalizer: normalize, + Corrections: invoiceCorrectionDefinitions, + } +} + +func normalize(_ any) { + // nothing to normalize yet +} + +func validate(doc any) error { + switch obj := doc.(type) { + case *bill.Invoice: + return validateInvoice(obj) + } + return nil +} diff --git a/data/schemas/bill/invoice.json b/data/schemas/bill/invoice.json index 54023948..bbb9bff1 100644 --- a/data/schemas/bill/invoice.json +++ b/data/schemas/bill/invoice.json @@ -385,6 +385,10 @@ "const": "es-tbai-v1", "title": "Spain TicketBAI" }, + { + "const": "es-verifactu-v1", + "title": "Spain Verifactu" + }, { "const": "eu-en16931-v2017", "title": "EN 16931-1:2017" From 0cb518bcd807e708907f6ac7c8b1427fda79a05e Mon Sep 17 00:00:00 2001 From: apardods Date: Fri, 8 Nov 2024 11:24:48 +0000 Subject: [PATCH 02/25] Exemption Extensions --- addons/es/verifactu/extensions.go | 47 ++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index 9080308b..9fde6125 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -9,8 +9,8 @@ import ( // Extension keys for TicketBAI const ( ExtKeyExemption cbc.Key = "es-verifactu-exemption" - ExtKeyInvoiceType cbc.Key = "es-verifactu-invoice-type" ExtKeyCorrection cbc.Key = "es-verifactu-correction" + ExtKeyTaxCategory cbc.Key = "es-verifactu-tax-category" ) var extensions = []*cbc.KeyDefinition{ @@ -124,4 +124,49 @@ var extensions = []*cbc.KeyDefinition{ }, }, }, + { + Key: ExtKeyTaxCategory, + Name: i18n.String{ + i18n.EN: "Verifactu Tax Category Code", + i18n.ES: "Verifactu Impuesto de Aplicacion", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Tax category code used to identify the type of tax being applied to the invoice. + The code must be one of the predefined values that correspond to the main Spanish + tax regimes: IVA (Value Added Tax), IPSI (Tax on Production, Services and Imports), + IGIC (Canary Islands General Indirect Tax), or Other. + `), + }, + Values: []*cbc.ValueDefinition{ + { + Value: "01", + Name: i18n.String{ + i18n.EN: "IVA", + i18n.ES: "IVA", + }, + }, + { + Value: "02", + Name: i18n.String{ + i18n.EN: "IPSI", + i18n.ES: "IPSI", + }, + }, + { + Value: "03", + Name: i18n.String{ + i18n.EN: "IGIC", + i18n.ES: "IGIC", + }, + }, + { + Value: "04", + Name: i18n.String{ + i18n.EN: "Other", + i18n.ES: "Otro", + }, + }, + }, + }, } From 098c3510bf4f4c8c0f86f19b910ffa1a35a1dfd4 Mon Sep 17 00:00:00 2001 From: apardods Date: Fri, 8 Nov 2024 11:32:54 +0000 Subject: [PATCH 03/25] Add Invoice Tests --- addons/es/verifactu/invoice_test.go | 102 ++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 addons/es/verifactu/invoice_test.go diff --git a/addons/es/verifactu/invoice_test.go b/addons/es/verifactu/invoice_test.go new file mode 100644 index 00000000..a618f950 --- /dev/null +++ b/addons/es/verifactu/invoice_test.go @@ -0,0 +1,102 @@ +package verifactu_test + +import ( + "testing" + + "github.com/invopop/gobl/addons/es/verifactu" + "github.com/invopop/gobl/bill" + "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()) + }) + t.Run("missing customer tax ID", func(t *testing.T) { + inv := testInvoiceStandard(t) + inv.Customer.TaxID = nil + require.NoError(t, inv.Calculate()) + err := inv.Validate() + assert.ErrorContains(t, err, "customer: (tax_id: cannot be blank.)") + }) + + t.Run("with exemption reason", func(t *testing.T) { + inv := testInvoiceStandard(t) + inv.Lines[0].Taxes[0].Ext = nil + assertValidationError(t, inv, "es-verifactu-exemption: required") + }) + + t.Run("without series", func(t *testing.T) { + inv := testInvoiceStandard(t) + inv.Series = "" + assertValidationError(t, inv, "series: cannot be blank") + }) + + t.Run("without notes", func(t *testing.T) { + inv := testInvoiceStandard(t) + inv.Notes = nil + assertValidationError(t, inv, "notes: with key 'general' missing") + }) +} + +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), + Series: "ABC", + 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: "exempt", + Ext: tax.Extensions{ + verifactu.ExtKeyExemption: "E1", + }, + }, + }, + }, + }, + Notes: []*cbc.Note{ + { + Key: cbc.NoteKeyGeneral, + Text: "This is a test invoice", + }, + }, + } +} From 8f0cee7161583fbc9b88ca123e3f254c226f85e5 Mon Sep 17 00:00:00 2001 From: apardods Date: Mon, 11 Nov 2024 15:08:45 +0000 Subject: [PATCH 04/25] Added QR Stamp --- addons/es/verifactu/verifactu.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/addons/es/verifactu/verifactu.go b/addons/es/verifactu/verifactu.go index 032a65c2..9b83d1f5 100644 --- a/addons/es/verifactu/verifactu.go +++ b/addons/es/verifactu/verifactu.go @@ -13,6 +13,12 @@ const ( V1 cbc.Key = "es-verifactu-v1" ) +// Official stamps or codes validated by government agencies +const ( + // StampQR contains the URL included in the QR code. + StampQR cbc.Key = "verifactu-qr" +) + func init() { tax.RegisterAddonDef(newAddon()) } From 0ec4e55345b3d9dc0f87917241a797155dc66684 Mon Sep 17 00:00:00 2001 From: apardods Date: Tue, 12 Nov 2024 12:30:01 +0000 Subject: [PATCH 05/25] Added Scenarios and Extensions --- addons/es/verifactu/extensions.go | 69 ++++++++++++++++++++----------- addons/es/verifactu/invoice.go | 4 +- addons/es/verifactu/scenarios.go | 66 +++++++++++++++++++++++++++++ addons/es/verifactu/verifactu.go | 1 + 4 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 addons/es/verifactu/scenarios.go diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index 9fde6125..361907fe 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -6,10 +6,10 @@ import ( "github.com/invopop/gobl/pkg/here" ) -// Extension keys for TicketBAI +// Extension keys for Verifactu const ( + ExtKeyDocType cbc.Key = "es-verifactu-doc-type" ExtKeyExemption cbc.Key = "es-verifactu-exemption" - ExtKeyCorrection cbc.Key = "es-verifactu-correction" ExtKeyTaxCategory cbc.Key = "es-verifactu-tax-category" ) @@ -17,13 +17,13 @@ var extensions = []*cbc.KeyDefinition{ { Key: ExtKeyExemption, Name: i18n.String{ - i18n.EN: "Verifactu Exemption code", - i18n.ES: "Código de Exención de Verifactu", + i18n.EN: "Verifactu Exemption code - L10", + i18n.ES: "Código de Exención de Verifactu - L10", }, Desc: i18n.String{ i18n.EN: here.Doc(` - Codes used by TicketBAI for both "exempt", "not-subject", and reverse - charge transactions. In the TicketBAI format these are separated, + Codes used by Verifactu for both "exempt", "not-subject", and reverse + charge transactions. In the Verifactu format these are separated, but in order to simplify GOBL and be more closely aligned with other countries we've combined them into one. `), @@ -32,36 +32,36 @@ var extensions = []*cbc.KeyDefinition{ { Value: "E1", Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Article 20 of the Foral VAT Law", - i18n.ES: "Exenta: por el artículo 20 de la Norma Foral del IVA", + i18n.EN: "Exempt: pursuant to Article 20. Exemptions in internal operations.", + i18n.ES: "Exenta: por el artículo 20. Exenciones en operaciones interiores.", }, }, { Value: "E2", Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Article 21 of the Foral VAT Law", - i18n.ES: "Exenta: por el artículo 21 de la Norma Foral del IVA", + i18n.EN: "Exempt: pursuant to Article 21. Exemptions in exports of goods.", + i18n.ES: "Exenta: por el artículo 21. Exenciones en las exportaciones de bienes.", }, }, { Value: "E3", Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Article 22 of the Foral VAT Law", - i18n.ES: "Exenta: por el artículo 22 de la Norma Foral del IVA", + i18n.EN: "Exempt: pursuant to Article 22. Exemptions in operations asimilated to exports.", + i18n.ES: "Exenta: por el artículo 22. Exenciones en las operaciones asimiladas a las exportaciones.", }, }, { Value: "E4", Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Articles 23 and 24 of the Foral VAT Law", - i18n.ES: "Exenta: por el artículos 23 y 24 de la Norma Foral del IVA", + i18n.EN: "Exempt: pursuant to Articles 23 and 24. Exemptions related to temporary deposit, customs and fiscal regimes, and other situations.", + i18n.ES: "Exenta: por el artículos 23 y 24. Exenciones relativas a las situaciones de depósito temporal, regímenes aduaneros y fiscales, y otras situaciones.", }, }, { Value: "E5", Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Article 25 of the Foral VAT law", - i18n.ES: "Exenta: por el artículo 25 de la Norma Foral del IVA", + i18n.EN: "Exempt: pursuant to Article 25. Exemptions in the delivery of goods destined to another Member State.", + i18n.ES: "Exenta: por el artículo 25. Exenciones en las entregas de bienes destinados a otro Estado miembro.", }, }, { @@ -74,19 +74,38 @@ var extensions = []*cbc.KeyDefinition{ }, }, { - Key: ExtKeyCorrection, + Key: ExtKeyDocType, Name: i18n.String{ - i18n.EN: "Verifactu Rectification Type Code", - i18n.ES: "Verifactu Código de Factura Rectificativa", + i18n.EN: "Verifactu Invoice Type Code - L2", + i18n.ES: "Código de Tipo de Factura de Verifactu - L2", }, Desc: i18n.String{ i18n.EN: here.Doc(` - Corrected or rectified invoices that need to be sent in the Verifactu format - require a specific type code to be defined alongside the preceding invoice - data. + Invoice type code used to identify the type of invoice being sent. `), }, Values: []*cbc.ValueDefinition{ + { + Value: "F1", + Name: i18n.String{ + i18n.EN: "Invoice (Article 6, 7.2 and 7.3 of RD 1619/2012)", + i18n.ES: "Factura (Art. 6, 7.2 y 7.3 del RD 1619/2012)", + }, + }, + { + Value: "F2", + Name: i18n.String{ + i18n.EN: "Simplified invoice (Article 6.1.d) of RD 1619/2012)", + i18n.ES: "Factura Simplificada (Art. 6.1.d) del RD 1619/2012)", + }, + }, + { + Value: "F3", + Name: i18n.String{ + i18n.EN: "Invoice in substitution of simplified invoices.", + i18n.ES: "Factura emitida en sustitución de facturas simplificadas facturadas y declaradas.", + }, + }, { Value: "R1", Name: i18n.String{ @@ -127,8 +146,8 @@ var extensions = []*cbc.KeyDefinition{ { Key: ExtKeyTaxCategory, Name: i18n.String{ - i18n.EN: "Verifactu Tax Category Code", - i18n.ES: "Verifactu Impuesto de Aplicacion", + i18n.EN: "Verifactu Tax Category Code - L1", + i18n.ES: "Código de Tipo de Impuesto de Verifactu - L1", }, Desc: i18n.String{ i18n.EN: here.Doc(` @@ -161,7 +180,7 @@ var extensions = []*cbc.KeyDefinition{ }, }, { - Value: "04", + Value: "05", Name: i18n.String{ i18n.EN: "Other", i18n.ES: "Otro", diff --git a/addons/es/verifactu/invoice.go b/addons/es/verifactu/invoice.go index 920b51f1..3bf55468 100644 --- a/addons/es/verifactu/invoice.go +++ b/addons/es/verifactu/invoice.go @@ -13,7 +13,7 @@ var invoiceCorrectionDefinitions = tax.CorrectionSet{ { Schema: bill.ShortSchemaInvoice, Extensions: []cbc.Key{ - ExtKeyCorrection, + ExtKeyDocType, }, }, } @@ -76,7 +76,7 @@ func validateInvoicePreceding(val any) error { validation.Field(&p.IssueDate, validation.Required), validation.Field(&p.Series, validation.Required), validation.Field(&p.Ext, - tax.ExtensionsRequires(ExtKeyCorrection), + tax.ExtensionsRequires(ExtKeyDocType), validation.Skip, ), ) diff --git a/addons/es/verifactu/scenarios.go b/addons/es/verifactu/scenarios.go new file mode 100644 index 00000000..6eeec26c --- /dev/null +++ b/addons/es/verifactu/scenarios.go @@ -0,0 +1,66 @@ +package verifactu + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/regimes/es" + "github.com/invopop/gobl/tax" +) + +const ( + TagSubstitution = "substitution" +) + +var scenarios = []*tax.ScenarioSet{ + { + Schema: bill.ShortSchemaInvoice, + List: []*tax.Scenario{ + // ** Invoice Document Types ** + { + Types: []cbc.Key{ + bill.InvoiceTypeStandard, + }, + Ext: tax.Extensions{ + ExtKeyDocType: "F1", + }, + }, + { + Types: []cbc.Key{ + bill.InvoiceTypeStandard, + }, + Tags: []cbc.Key{ + tax.TagSimplified, + }, + Ext: tax.Extensions{ + ExtKeyDocType: "F2", + }, + }, + { + Types: []cbc.Key{ + bill.InvoiceTypeStandard, + }, + Tags: []cbc.Key{ + TagSubstitution, + }, + Ext: tax.Extensions{ + ExtKeyDocType: "F3", + }, + }, + { + Types: es.InvoiceCorrectionTypes, + Ext: tax.Extensions{ + ExtKeyDocType: "R1", + }, + }, + { + Types: es.InvoiceCorrectionTypes, + Tags: []cbc.Key{ + tax.TagSimplified, + }, + Ext: tax.Extensions{ + ExtKeyDocType: "R5", + }, + }, + }, + }, +} diff --git a/addons/es/verifactu/verifactu.go b/addons/es/verifactu/verifactu.go index 9b83d1f5..8d0ebe25 100644 --- a/addons/es/verifactu/verifactu.go +++ b/addons/es/verifactu/verifactu.go @@ -31,6 +31,7 @@ func newAddon() *tax.AddonDef { }, Extensions: extensions, Validator: validate, + Scenarios: scenarios, Normalizer: normalize, Corrections: invoiceCorrectionDefinitions, } From 033c0ce0cd344a9fb3fb44f771e24d8eba3d6034 Mon Sep 17 00:00:00 2001 From: apardods Date: Tue, 12 Nov 2024 15:15:14 +0000 Subject: [PATCH 06/25] Added All Lists as Extensions --- addons/es/verifactu/extensions.go | 338 +++++++++++++++++++++++++----- addons/es/verifactu/scenarios.go | 1 + 2 files changed, 287 insertions(+), 52 deletions(-) diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index 361907fe..3681075d 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -8,67 +8,56 @@ import ( // Extension keys for Verifactu const ( - ExtKeyDocType cbc.Key = "es-verifactu-doc-type" - ExtKeyExemption cbc.Key = "es-verifactu-exemption" - ExtKeyTaxCategory cbc.Key = "es-verifactu-tax-category" + ExtKeyDocType cbc.Key = "es-verifactu-doc-type" + ExtKeyIdentity cbc.Key = "es-verifactu-identity" + ExtKeyExemption cbc.Key = "es-verifactu-exemption" + ExtKeyTaxCategory cbc.Key = "es-verifactu-tax-category" + ExtKeyTaxClassification cbc.Key = "es-verifactu-tax-classification" + ExtKeyTaxRegime cbc.Key = "es-verifactu-tax-regime" ) var extensions = []*cbc.KeyDefinition{ { - Key: ExtKeyExemption, + Key: ExtKeyTaxCategory, Name: i18n.String{ - i18n.EN: "Verifactu Exemption code - L10", - i18n.ES: "Código de Exención de Verifactu - L10", + i18n.EN: "Verifactu Tax Category Code - L1", + i18n.ES: "Código de Tipo de Impuesto de Verifactu - L1", }, Desc: i18n.String{ i18n.EN: here.Doc(` - Codes used by Verifactu for both "exempt", "not-subject", and reverse - charge transactions. In the Verifactu format these are separated, - but in order to simplify GOBL and be more closely aligned with - other countries we've combined them into one. + Tax category code used to identify the type of tax being applied to the invoice. + The code must be one of the predefined values that correspond to the main Spanish + tax regimes: IVA (Value Added Tax), IPSI (Tax on Production, Services and Imports), + IGIC (Canary Islands General Indirect Tax), or Other. `), }, Values: []*cbc.ValueDefinition{ { - Value: "E1", - Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Article 20. Exemptions in internal operations.", - i18n.ES: "Exenta: por el artículo 20. Exenciones en operaciones interiores.", - }, - }, - { - Value: "E2", - Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Article 21. Exemptions in exports of goods.", - i18n.ES: "Exenta: por el artículo 21. Exenciones en las exportaciones de bienes.", - }, - }, - { - Value: "E3", + Value: "01", Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Article 22. Exemptions in operations asimilated to exports.", - i18n.ES: "Exenta: por el artículo 22. Exenciones en las operaciones asimiladas a las exportaciones.", + i18n.EN: "IVA", + i18n.ES: "IVA", }, }, { - Value: "E4", + Value: "02", Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Articles 23 and 24. Exemptions related to temporary deposit, customs and fiscal regimes, and other situations.", - i18n.ES: "Exenta: por el artículos 23 y 24. Exenciones relativas a las situaciones de depósito temporal, regímenes aduaneros y fiscales, y otras situaciones.", + i18n.EN: "IPSI", + i18n.ES: "IPSI", }, }, { - Value: "E5", + Value: "03", Name: i18n.String{ - i18n.EN: "Exempt: pursuant to Article 25. Exemptions in the delivery of goods destined to another Member State.", - i18n.ES: "Exenta: por el artículo 25. Exenciones en las entregas de bienes destinados a otro Estado miembro.", + i18n.EN: "IGIC", + i18n.ES: "IGIC", }, }, { - Value: "E6", + Value: "05", Name: i18n.String{ - i18n.EN: "Exempt: pursuant to other reasons", - i18n.ES: "Exenta: por otra causa", + i18n.EN: "Other", + i18n.ES: "Otro", }, }, }, @@ -81,7 +70,7 @@ var extensions = []*cbc.KeyDefinition{ }, Desc: i18n.String{ i18n.EN: here.Doc(` - Invoice type code used to identify the type of invoice being sent. + Invoice type code used to identify the type of invoice being sent. `), }, Values: []*cbc.ValueDefinition{ @@ -144,46 +133,291 @@ var extensions = []*cbc.KeyDefinition{ }, }, { - Key: ExtKeyTaxCategory, + Key: ExtKeyIdentity, Name: i18n.String{ - i18n.EN: "Verifactu Tax Category Code - L1", - i18n.ES: "Código de Tipo de Impuesto de Verifactu - L1", + i18n.EN: "Verifactu Identity Type Code - L7", + i18n.ES: "Código de Tipo de Identificación de Verifactu - L7", }, Desc: i18n.String{ i18n.EN: here.Doc(` - Tax category code used to identify the type of tax being applied to the invoice. - The code must be one of the predefined values that correspond to the main Spanish - tax regimes: IVA (Value Added Tax), IPSI (Tax on Production, Services and Imports), - IGIC (Canary Islands General Indirect Tax), or Other. + Identity type code used to identify the type of identity being used. + `), + }, + Values: []*cbc.ValueDefinition{ + { + Value: "02", + Name: i18n.String{ + i18n.EN: "NIF-VAT", + i18n.ES: "NIF-IVA", + }, + }, + { + Value: "03", + Name: i18n.String{ + i18n.EN: "Passport", + i18n.ES: "Pasaporte", + }, + }, + { + Value: "04", + Name: i18n.String{ + i18n.EN: "Official identification document issued by the country or territory of residence", + i18n.ES: "Documento oficial de identificación expedido por el país o territorio de residencia", + }, + }, + { + Value: "05", + Name: i18n.String{ + i18n.EN: "Certificate of residence", + i18n.ES: "Certificado de residencia", + }, + }, + { + Value: "06", + Name: i18n.String{ + i18n.EN: "Other supporting document", + i18n.ES: "Otro documento probatorio", + }, + }, + { + Value: "07", + Name: i18n.String{ + i18n.EN: "Not registered", + i18n.ES: "No censado", + }, + }, + }, + }, + { + Key: ExtKeyTaxRegime, + Name: i18n.String{ + i18n.EN: "Verifactu Tax Regime Code - L8", + i18n.ES: "Código de Régimen de Impuesto de Verifactu - L8", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Tax regime code used to identify the type of tax regime being applied to the invoice. `), }, Values: []*cbc.ValueDefinition{ { Value: "01", Name: i18n.String{ - i18n.EN: "IVA", - i18n.ES: "IVA", + i18n.EN: "General regime operation", + i18n.ES: "Operación de régimen general", }, }, { Value: "02", Name: i18n.String{ - i18n.EN: "IPSI", - i18n.ES: "IPSI", + i18n.EN: "Export", + i18n.ES: "Exportación", }, }, { Value: "03", Name: i18n.String{ - i18n.EN: "IGIC", - i18n.ES: "IGIC", + i18n.EN: "Special regime for used goods, art objects, antiques and collectibles", + i18n.ES: "Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección", + }, + }, + { + Value: "04", + Name: i18n.String{ + i18n.EN: "Special regime for investment gold", + i18n.ES: "Régimen especial del oro de inversión", }, }, { Value: "05", Name: i18n.String{ - i18n.EN: "Other", - i18n.ES: "Otro", + i18n.EN: "Special regime for travel agencies", + i18n.ES: "Régimen especial de las agencias de viajes", + }, + }, + { + Value: "06", + Name: i18n.String{ + i18n.EN: "Special VAT regime for group entities (Advanced Level)", + i18n.ES: "Régimen especial grupo de entidades en IVA (Nivel Avanzado)", + }, + }, + { + Value: "07", + Name: i18n.String{ + i18n.EN: "Special cash accounting regime", + i18n.ES: "Régimen especial del criterio de caja", + }, + }, + { + Value: "08", + Name: i18n.String{ + i18n.EN: "Operations subject to IPSI/IGIC", + i18n.ES: "Operaciones sujetas al IPSI/IGIC", + }, + }, + { + Value: "09", + Name: i18n.String{ + i18n.EN: "Billing of travel agency services acting as intermediaries in name and on behalf of others", + i18n.ES: "Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena", + }, + }, + { + Value: "10", + Name: i18n.String{ + i18n.EN: "Collection of professional fees or industrial property rights on behalf of third parties", + i18n.ES: "Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial", + }, + }, + { + Value: "11", + Name: i18n.String{ + i18n.EN: "Business premises rental operations", + i18n.ES: "Operaciones de arrendamiento de local de negocio", + }, + }, + { + Value: "14", + Name: i18n.String{ + i18n.EN: "Invoice with pending VAT accrual in work certifications for Public Administration", + i18n.ES: "Factura con IVA pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública", + }, + }, + { + Value: "15", + Name: i18n.String{ + i18n.EN: "Invoice with pending VAT accrual in successive tract operations", + i18n.ES: "Factura con IVA pendiente de devengo en operaciones de tracto sucesivo", + }, + }, + { + Value: "17", + Name: i18n.String{ + i18n.EN: "Operation under OSS and IOSS regimes", + i18n.ES: "Operación acogida a alguno de los regímenes previstos en el capítulo XI del título IX (OSS e IOSS)", + }, + }, + { + Value: "18", + Name: i18n.String{ + i18n.EN: "Equivalence surcharge", + i18n.ES: "Recargo de equivalencia", + }, + }, + { + Value: "19", + Name: i18n.String{ + i18n.EN: "Operations included in the Special Regime for Agriculture, Livestock and Fisheries", + i18n.ES: "Operaciones de actividades incluidas en el Régimen Especial de Agricultura, Ganadería y Pesca (REAGYP)", + }, + }, + { + Value: "20", + Name: i18n.String{ + i18n.EN: "Simplified regime", + i18n.ES: "Régimen simplificado", + }, + }, + }, + }, + { + Key: ExtKeyTaxClassification, + Name: i18n.String{ + i18n.EN: "Verifactu Tax Classification Code - L9", + i18n.ES: "Código de Clasificación de Impuesto de Verifactu - L9", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Tax classification code used to identify the type of tax being applied to the invoice. + `), + }, + Values: []*cbc.ValueDefinition{ + { + Value: "S1", + Name: i18n.String{ + i18n.EN: "Subject and Not Exempt - Without reverse charge", + i18n.ES: "Operación Sujeta y No exenta - Sin inversión del sujeto pasivo", + }, + }, + { + Value: "S2", + Name: i18n.String{ + i18n.EN: "Subject and Not Exempt - With reverse charge", + i18n.ES: "Operación Sujeta y No exenta - Con Inversión del sujeto pasivo", + }, + }, + { + Value: "N1", + Name: i18n.String{ + i18n.EN: "Not Subject - Articles 7, 14, others", + i18n.ES: "Operación No Sujeta artículo 7, 14, otros", + }, + }, + { + Value: "N2", + Name: i18n.String{ + i18n.EN: "Not Subject - Due to location rules", + i18n.ES: "Operación No Sujeta por Reglas de localización", + }, + }, + }, + }, + { + Key: ExtKeyExemption, + Name: i18n.String{ + i18n.EN: "Verifactu Exemption code - L10", + i18n.ES: "Código de Exención de Verifactu - L10", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Codes used by Verifactu for both "exempt", "not-subject", and reverse + charge transactions. In the Verifactu format these are separated, + but in order to simplify GOBL and be more closely aligned with + other countries we've combined them into one. + `), + }, + Values: []*cbc.ValueDefinition{ + { + Value: "E1", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Article 20. Exemptions in internal operations.", + i18n.ES: "Exenta: por el artículo 20. Exenciones en operaciones interiores.", + }, + }, + { + Value: "E2", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Article 21. Exemptions in exports of goods.", + i18n.ES: "Exenta: por el artículo 21. Exenciones en las exportaciones de bienes.", + }, + }, + { + Value: "E3", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Article 22. Exemptions in operations asimilated to exports.", + i18n.ES: "Exenta: por el artículo 22. Exenciones en las operaciones asimiladas a las exportaciones.", + }, + }, + { + Value: "E4", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Articles 23 and 24. Exemptions related to temporary deposit, customs and fiscal regimes, and other situations.", + i18n.ES: "Exenta: por el artículos 23 y 24. Exenciones relativas a las situaciones de depósito temporal, regímenes aduaneros y fiscales, y otras situaciones.", + }, + }, + { + Value: "E5", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to Article 25. Exemptions in the delivery of goods destined to another Member State.", + i18n.ES: "Exenta: por el artículo 25. Exenciones en las entregas de bienes destinados a otro Estado miembro.", + }, + }, + { + Value: "E6", + Name: i18n.String{ + i18n.EN: "Exempt: pursuant to other reasons", + i18n.ES: "Exenta: por otra causa", }, }, }, diff --git a/addons/es/verifactu/scenarios.go b/addons/es/verifactu/scenarios.go index 6eeec26c..0d50d080 100644 --- a/addons/es/verifactu/scenarios.go +++ b/addons/es/verifactu/scenarios.go @@ -8,6 +8,7 @@ import ( ) const ( + // TagSubstitution is used to identify substitution invoices. TagSubstitution = "substitution" ) From 214df6f740539ca3b2f33388fc88ede6ec1b7401 Mon Sep 17 00:00:00 2001 From: apardods Date: Tue, 19 Nov 2024 16:40:00 +0000 Subject: [PATCH 07/25] Remove some unnecessary extensions --- addons/es/verifactu/extensions.go | 132 ++-------------------------- addons/es/verifactu/invoice.go | 5 +- addons/es/verifactu/invoice_test.go | 2 +- 3 files changed, 8 insertions(+), 131 deletions(-) diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index 3681075d..3a2f3c9c 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -9,59 +9,11 @@ import ( // Extension keys for Verifactu const ( ExtKeyDocType cbc.Key = "es-verifactu-doc-type" - ExtKeyIdentity cbc.Key = "es-verifactu-identity" - ExtKeyExemption cbc.Key = "es-verifactu-exemption" - ExtKeyTaxCategory cbc.Key = "es-verifactu-tax-category" ExtKeyTaxClassification cbc.Key = "es-verifactu-tax-classification" ExtKeyTaxRegime cbc.Key = "es-verifactu-tax-regime" ) var extensions = []*cbc.KeyDefinition{ - { - Key: ExtKeyTaxCategory, - Name: i18n.String{ - i18n.EN: "Verifactu Tax Category Code - L1", - i18n.ES: "Código de Tipo de Impuesto de Verifactu - L1", - }, - Desc: i18n.String{ - i18n.EN: here.Doc(` - Tax category code used to identify the type of tax being applied to the invoice. - The code must be one of the predefined values that correspond to the main Spanish - tax regimes: IVA (Value Added Tax), IPSI (Tax on Production, Services and Imports), - IGIC (Canary Islands General Indirect Tax), or Other. - `), - }, - Values: []*cbc.ValueDefinition{ - { - Value: "01", - Name: i18n.String{ - i18n.EN: "IVA", - i18n.ES: "IVA", - }, - }, - { - Value: "02", - Name: i18n.String{ - i18n.EN: "IPSI", - i18n.ES: "IPSI", - }, - }, - { - Value: "03", - Name: i18n.String{ - i18n.EN: "IGIC", - i18n.ES: "IGIC", - }, - }, - { - Value: "05", - Name: i18n.String{ - i18n.EN: "Other", - i18n.ES: "Otro", - }, - }, - }, - }, { Key: ExtKeyDocType, Name: i18n.String{ @@ -132,67 +84,11 @@ var extensions = []*cbc.KeyDefinition{ }, }, }, - { - Key: ExtKeyIdentity, - Name: i18n.String{ - i18n.EN: "Verifactu Identity Type Code - L7", - i18n.ES: "Código de Tipo de Identificación de Verifactu - L7", - }, - Desc: i18n.String{ - i18n.EN: here.Doc(` - Identity type code used to identify the type of identity being used. - `), - }, - Values: []*cbc.ValueDefinition{ - { - Value: "02", - Name: i18n.String{ - i18n.EN: "NIF-VAT", - i18n.ES: "NIF-IVA", - }, - }, - { - Value: "03", - Name: i18n.String{ - i18n.EN: "Passport", - i18n.ES: "Pasaporte", - }, - }, - { - Value: "04", - Name: i18n.String{ - i18n.EN: "Official identification document issued by the country or territory of residence", - i18n.ES: "Documento oficial de identificación expedido por el país o territorio de residencia", - }, - }, - { - Value: "05", - Name: i18n.String{ - i18n.EN: "Certificate of residence", - i18n.ES: "Certificado de residencia", - }, - }, - { - Value: "06", - Name: i18n.String{ - i18n.EN: "Other supporting document", - i18n.ES: "Otro documento probatorio", - }, - }, - { - Value: "07", - Name: i18n.String{ - i18n.EN: "Not registered", - i18n.ES: "No censado", - }, - }, - }, - }, { Key: ExtKeyTaxRegime, Name: i18n.String{ - i18n.EN: "Verifactu Tax Regime Code - L8", - i18n.ES: "Código de Régimen de Impuesto de Verifactu - L8", + i18n.EN: "Verifactu Tax Regime Code - L8A", + i18n.ES: "Código de Régimen de Impuesto de Verifactu - L8A", }, Desc: i18n.String{ i18n.EN: here.Doc(` @@ -324,12 +220,13 @@ var extensions = []*cbc.KeyDefinition{ { Key: ExtKeyTaxClassification, Name: i18n.String{ - i18n.EN: "Verifactu Tax Classification Code - L9", - i18n.ES: "Código de Clasificación de Impuesto de Verifactu - L9", + i18n.EN: "Verifactu Tax Classification/Exemption Code - L9/10", + i18n.ES: "Código de Clasificación/Exención de Impuesto de Verifactu - L9/10", }, Desc: i18n.String{ i18n.EN: here.Doc(` - Tax classification code used to identify the type of tax being applied to the invoice. + Tax classification code used to identify the type of tax being applied to the invoice. In Verifactu these + lists are separate but here they are combined. `), }, Values: []*cbc.ValueDefinition{ @@ -361,23 +258,6 @@ var extensions = []*cbc.KeyDefinition{ i18n.ES: "Operación No Sujeta por Reglas de localización", }, }, - }, - }, - { - Key: ExtKeyExemption, - Name: i18n.String{ - i18n.EN: "Verifactu Exemption code - L10", - i18n.ES: "Código de Exención de Verifactu - L10", - }, - Desc: i18n.String{ - i18n.EN: here.Doc(` - Codes used by Verifactu for both "exempt", "not-subject", and reverse - charge transactions. In the Verifactu format these are separated, - but in order to simplify GOBL and be more closely aligned with - other countries we've combined them into one. - `), - }, - Values: []*cbc.ValueDefinition{ { Value: "E1", Name: i18n.String{ diff --git a/addons/es/verifactu/invoice.go b/addons/es/verifactu/invoice.go index 3bf55468..5ad74a0f 100644 --- a/addons/es/verifactu/invoice.go +++ b/addons/es/verifactu/invoice.go @@ -105,10 +105,7 @@ func validateInvoiceLineTax(value any) error { } return validation.ValidateStruct(obj, validation.Field(&obj.Ext, - validation.When( - obj.Rate == tax.RateExempt, - tax.ExtensionsRequires(ExtKeyExemption), - ), + tax.ExtensionsRequires(ExtKeyTaxClassification), validation.Skip, ), ) diff --git a/addons/es/verifactu/invoice_test.go b/addons/es/verifactu/invoice_test.go index a618f950..c55b334c 100644 --- a/addons/es/verifactu/invoice_test.go +++ b/addons/es/verifactu/invoice_test.go @@ -86,7 +86,7 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { Category: "VAT", Rate: "exempt", Ext: tax.Extensions{ - verifactu.ExtKeyExemption: "E1", + verifactu.ExtKeyTaxClassification: "E1", }, }, }, From 1b04b703647df37e6289f2297fcc1e41a16990e6 Mon Sep 17 00:00:00 2001 From: apardods Date: Tue, 19 Nov 2024 18:08:55 +0000 Subject: [PATCH 08/25] Remove Extension & Fix Test --- addons/es/verifactu/extensions.go | 134 ---------------------------- addons/es/verifactu/invoice_test.go | 2 +- 2 files changed, 1 insertion(+), 135 deletions(-) diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index 3a2f3c9c..fd40d2fb 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -10,7 +10,6 @@ import ( const ( ExtKeyDocType cbc.Key = "es-verifactu-doc-type" ExtKeyTaxClassification cbc.Key = "es-verifactu-tax-classification" - ExtKeyTaxRegime cbc.Key = "es-verifactu-tax-regime" ) var extensions = []*cbc.KeyDefinition{ @@ -84,139 +83,6 @@ var extensions = []*cbc.KeyDefinition{ }, }, }, - { - Key: ExtKeyTaxRegime, - Name: i18n.String{ - i18n.EN: "Verifactu Tax Regime Code - L8A", - i18n.ES: "Código de Régimen de Impuesto de Verifactu - L8A", - }, - Desc: i18n.String{ - i18n.EN: here.Doc(` - Tax regime code used to identify the type of tax regime being applied to the invoice. - `), - }, - Values: []*cbc.ValueDefinition{ - { - Value: "01", - Name: i18n.String{ - i18n.EN: "General regime operation", - i18n.ES: "Operación de régimen general", - }, - }, - { - Value: "02", - Name: i18n.String{ - i18n.EN: "Export", - i18n.ES: "Exportación", - }, - }, - { - Value: "03", - Name: i18n.String{ - i18n.EN: "Special regime for used goods, art objects, antiques and collectibles", - i18n.ES: "Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección", - }, - }, - { - Value: "04", - Name: i18n.String{ - i18n.EN: "Special regime for investment gold", - i18n.ES: "Régimen especial del oro de inversión", - }, - }, - { - Value: "05", - Name: i18n.String{ - i18n.EN: "Special regime for travel agencies", - i18n.ES: "Régimen especial de las agencias de viajes", - }, - }, - { - Value: "06", - Name: i18n.String{ - i18n.EN: "Special VAT regime for group entities (Advanced Level)", - i18n.ES: "Régimen especial grupo de entidades en IVA (Nivel Avanzado)", - }, - }, - { - Value: "07", - Name: i18n.String{ - i18n.EN: "Special cash accounting regime", - i18n.ES: "Régimen especial del criterio de caja", - }, - }, - { - Value: "08", - Name: i18n.String{ - i18n.EN: "Operations subject to IPSI/IGIC", - i18n.ES: "Operaciones sujetas al IPSI/IGIC", - }, - }, - { - Value: "09", - Name: i18n.String{ - i18n.EN: "Billing of travel agency services acting as intermediaries in name and on behalf of others", - i18n.ES: "Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena", - }, - }, - { - Value: "10", - Name: i18n.String{ - i18n.EN: "Collection of professional fees or industrial property rights on behalf of third parties", - i18n.ES: "Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial", - }, - }, - { - Value: "11", - Name: i18n.String{ - i18n.EN: "Business premises rental operations", - i18n.ES: "Operaciones de arrendamiento de local de negocio", - }, - }, - { - Value: "14", - Name: i18n.String{ - i18n.EN: "Invoice with pending VAT accrual in work certifications for Public Administration", - i18n.ES: "Factura con IVA pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública", - }, - }, - { - Value: "15", - Name: i18n.String{ - i18n.EN: "Invoice with pending VAT accrual in successive tract operations", - i18n.ES: "Factura con IVA pendiente de devengo en operaciones de tracto sucesivo", - }, - }, - { - Value: "17", - Name: i18n.String{ - i18n.EN: "Operation under OSS and IOSS regimes", - i18n.ES: "Operación acogida a alguno de los regímenes previstos en el capítulo XI del título IX (OSS e IOSS)", - }, - }, - { - Value: "18", - Name: i18n.String{ - i18n.EN: "Equivalence surcharge", - i18n.ES: "Recargo de equivalencia", - }, - }, - { - Value: "19", - Name: i18n.String{ - i18n.EN: "Operations included in the Special Regime for Agriculture, Livestock and Fisheries", - i18n.ES: "Operaciones de actividades incluidas en el Régimen Especial de Agricultura, Ganadería y Pesca (REAGYP)", - }, - }, - { - Value: "20", - Name: i18n.String{ - i18n.EN: "Simplified regime", - i18n.ES: "Régimen simplificado", - }, - }, - }, - }, { Key: ExtKeyTaxClassification, Name: i18n.String{ diff --git a/addons/es/verifactu/invoice_test.go b/addons/es/verifactu/invoice_test.go index c55b334c..105d16cd 100644 --- a/addons/es/verifactu/invoice_test.go +++ b/addons/es/verifactu/invoice_test.go @@ -31,7 +31,7 @@ func TestInvoiceValidation(t *testing.T) { t.Run("with exemption reason", func(t *testing.T) { inv := testInvoiceStandard(t) inv.Lines[0].Taxes[0].Ext = nil - assertValidationError(t, inv, "es-verifactu-exemption: required") + assertValidationError(t, inv, "es-verifactu-tax-classification: required") }) t.Run("without series", func(t *testing.T) { From 52b07e5161c88bc049c87e1ec95baf880f58b55d Mon Sep 17 00:00:00 2001 From: apardods Date: Fri, 22 Nov 2024 16:42:19 +0000 Subject: [PATCH 09/25] Added Scenario Tests --- addons/es/verifactu/extensions.go | 4 +- addons/es/verifactu/invoice.go | 19 ++- addons/es/verifactu/invoice_test.go | 48 ++++-- addons/es/verifactu/scenarios_test.go | 55 +++++++ data/addons/es-verifactu-v1.json | 227 ++++++++++++++++++++++++++ 5 files changed, 339 insertions(+), 14 deletions(-) create mode 100644 addons/es/verifactu/scenarios_test.go create mode 100644 data/addons/es-verifactu-v1.json diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index fd40d2fb..13bc4120 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -91,8 +91,8 @@ var extensions = []*cbc.KeyDefinition{ }, Desc: i18n.String{ i18n.EN: here.Doc(` - Tax classification code used to identify the type of tax being applied to the invoice. In Verifactu these - lists are separate but here they are combined. + Tax classification code used to identify the type of tax being applied to the line. It includes both exemption reasons and tax scenarios. + These lists are separate in Verifactu but combined here for convenience. `), }, Values: []*cbc.ValueDefinition{ diff --git a/addons/es/verifactu/invoice.go b/addons/es/verifactu/invoice.go index 5ad74a0f..54d6446e 100644 --- a/addons/es/verifactu/invoice.go +++ b/addons/es/verifactu/invoice.go @@ -20,7 +20,6 @@ var invoiceCorrectionDefinitions = tax.CorrectionSet{ func validateInvoice(inv *bill.Invoice) error { return validation.ValidateStruct(inv, - validation.Field(&inv.Series, validation.Required), validation.Field(&inv.Customer, validation.By(validateInvoiceCustomer), validation.Skip, @@ -33,6 +32,11 @@ func validateInvoice(inv *bill.Invoice) error { validation.By(validateInvoicePreceding), validation.Skip, ), + validation.Field(&inv.Tax, + validation.Required, + validation.By(validateInvoiceTax), + validation.Skip, + ), validation.Field(&inv.Lines, validation.Each( validation.By(validateInvoiceLine), @@ -47,6 +51,19 @@ func validateInvoice(inv *bill.Invoice) error { ) } +func validateInvoiceTax(val any) error { + obj, ok := val.(*bill.Tax) + if obj == nil || !ok { + return nil + } + return validation.ValidateStruct(obj, + validation.Field(&obj.Ext, + tax.ExtensionsRequires(ExtKeyDocType), + validation.Skip, + ), + ) +} + func validateInvoiceCustomer(val any) error { obj, _ := val.(*org.Party) if obj == nil { diff --git a/addons/es/verifactu/invoice_test.go b/addons/es/verifactu/invoice_test.go index 105d16cd..cef4dd06 100644 --- a/addons/es/verifactu/invoice_test.go +++ b/addons/es/verifactu/invoice_test.go @@ -10,7 +10,6 @@ import ( "github.com/invopop/gobl/org" "github.com/invopop/gobl/tax" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,9 +22,7 @@ func TestInvoiceValidation(t *testing.T) { t.Run("missing customer tax ID", func(t *testing.T) { inv := testInvoiceStandard(t) inv.Customer.TaxID = nil - require.NoError(t, inv.Calculate()) - err := inv.Validate() - assert.ErrorContains(t, err, "customer: (tax_id: cannot be blank.)") + assertValidationError(t, inv, "customer: (tax_id: cannot be blank.)") }) t.Run("with exemption reason", func(t *testing.T) { @@ -34,17 +31,42 @@ func TestInvoiceValidation(t *testing.T) { assertValidationError(t, inv, "es-verifactu-tax-classification: required") }) - t.Run("without series", func(t *testing.T) { - inv := testInvoiceStandard(t) - inv.Series = "" - assertValidationError(t, inv, "series: cannot be blank") - }) - 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("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 with preceding", func(t *testing.T) { + inv := testInvoiceStandard(t) + inv.Type = bill.InvoiceTypeCreditNote + inv.Tax.Ext[verifactu.ExtKeyDocType] = "R1" + inv.Preceding = []*org.DocumentRef{ + { + Series: "ABC", + Code: "122", + Ext: tax.Extensions{ + verifactu.ExtKeyDocType: "F1", + }, + }, + } + require.NoError(t, inv.Calculate()) + require.NoError(t, inv.Validate()) + }) } func assertValidationError(t *testing.T, inv *bill.Invoice, expected string) { @@ -57,7 +79,6 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { t.Helper() return &bill.Invoice{ Addons: tax.WithAddons(verifactu.V1), - Series: "ABC", Code: "123", Supplier: &org.Party{ Name: "Test Supplier", @@ -98,5 +119,10 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { Text: "This is a test invoice", }, }, + Tax: &bill.Tax{ + Ext: tax.Extensions{ + verifactu.ExtKeyDocType: "F1", + }, + }, } } diff --git a/addons/es/verifactu/scenarios_test.go b/addons/es/verifactu/scenarios_test.go new file mode 100644 index 00000000..c6dc33d2 --- /dev/null +++ b/addons/es/verifactu/scenarios_test.go @@ -0,0 +1,55 @@ +package verifactu + +import ( + "testing" +) + +func TestScenarios(t *testing.T) { + tests := []struct { + name string + scenario string + tags []string + want bool + }{ + { + name: "standard invoice", + scenario: "standard", + tags: []string{}, + want: true, + }, + { + name: "simplified invoice", + scenario: "simplified", + tags: []string{"simplified"}, + want: true, + }, + { + name: "corrective invoice", + scenario: "corrective", + tags: []string{"corrective"}, + want: true, + }, + { + name: "invalid scenario", + scenario: "invalid", + tags: []string{}, + want: false, + }, + { + name: "simplified with wrong tags", + scenario: "simplified", + tags: []string{"corrective"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ValidateScenario(tt.scenario, tt.tags) + if got != tt.want { + t.Errorf("ValidateScenario(%v, %v) = %v, want %v", + tt.scenario, tt.tags, got, tt.want) + } + }) + } +} diff --git a/data/addons/es-verifactu-v1.json b/data/addons/es-verifactu-v1.json new file mode 100644 index 00000000..fa243aa5 --- /dev/null +++ b/data/addons/es-verifactu-v1.json @@ -0,0 +1,227 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/addon-def", + "key": "es-verifactu-v1", + "name": { + "en": "Spain Verifactu" + }, + "extensions": [ + { + "key": "es-verifactu-doc-type", + "name": { + "en": "Verifactu Invoice Type Code - L2", + "es": "Código de Tipo de Factura de Verifactu - L2" + }, + "desc": { + "en": "Invoice type code used to identify the type of invoice being sent." + }, + "values": [ + { + "value": "F1", + "name": { + "en": "Invoice (Article 6, 7.2 and 7.3 of RD 1619/2012)", + "es": "Factura (Art. 6, 7.2 y 7.3 del RD 1619/2012)" + } + }, + { + "value": "F2", + "name": { + "en": "Simplified invoice (Article 6.1.d) of RD 1619/2012)", + "es": "Factura Simplificada (Art. 6.1.d) del RD 1619/2012)" + } + }, + { + "value": "F3", + "name": { + "en": "Invoice in substitution of simplified invoices.", + "es": "Factura emitida en sustitución de facturas simplificadas facturadas y declaradas." + } + }, + { + "value": "R1", + "name": { + "en": "Rectified invoice: error based on law and Article 80 One, Two and Six LIVA", + "es": "Factura rectificativa: error fundado en derecho y Art. 80 Uno, Dos y Seis LIVA" + } + }, + { + "value": "R2", + "name": { + "en": "Rectified invoice: error based on law and Article 80.3", + "es": "Factura rectificativa: artículo 80.3" + } + }, + { + "value": "R3", + "name": { + "en": "Rectified invoice: error based on law and Article 80.4", + "es": "Factura rectificativa: artículo 80.4" + } + }, + { + "value": "R4", + "name": { + "en": "Rectified invoice: Other", + "es": "Factura rectificativa: Resto" + } + }, + { + "value": "R5", + "name": { + "en": "Rectified invoice: simplified invoices", + "es": "Factura rectificativa: facturas simplificadas" + } + } + ] + }, + { + "key": "es-verifactu-tax-classification", + "name": { + "en": "Verifactu Tax Classification/Exemption Code - L9/10", + "es": "Código de Clasificación/Exención de Impuesto de Verifactu - L9/10" + }, + "desc": { + "en": "Tax classification code used to identify the type of tax being applied to the invoice. In Verifactu these\nlists are separate but here they are combined." + }, + "values": [ + { + "value": "S1", + "name": { + "en": "Subject and Not Exempt - Without reverse charge", + "es": "Operación Sujeta y No exenta - Sin inversión del sujeto pasivo" + } + }, + { + "value": "S2", + "name": { + "en": "Subject and Not Exempt - With reverse charge", + "es": "Operación Sujeta y No exenta - Con Inversión del sujeto pasivo" + } + }, + { + "value": "N1", + "name": { + "en": "Not Subject - Articles 7, 14, others", + "es": "Operación No Sujeta artículo 7, 14, otros" + } + }, + { + "value": "N2", + "name": { + "en": "Not Subject - Due to location rules", + "es": "Operación No Sujeta por Reglas de localización" + } + }, + { + "value": "E1", + "name": { + "en": "Exempt: pursuant to Article 20. Exemptions in internal operations.", + "es": "Exenta: por el artículo 20. Exenciones en operaciones interiores." + } + }, + { + "value": "E2", + "name": { + "en": "Exempt: pursuant to Article 21. Exemptions in exports of goods.", + "es": "Exenta: por el artículo 21. Exenciones en las exportaciones de bienes." + } + }, + { + "value": "E3", + "name": { + "en": "Exempt: pursuant to Article 22. Exemptions in operations asimilated to exports.", + "es": "Exenta: por el artículo 22. Exenciones en las operaciones asimiladas a las exportaciones." + } + }, + { + "value": "E4", + "name": { + "en": "Exempt: pursuant to Articles 23 and 24. Exemptions related to temporary deposit, customs and fiscal regimes, and other situations.", + "es": "Exenta: por el artículos 23 y 24. Exenciones relativas a las situaciones de depósito temporal, regímenes aduaneros y fiscales, y otras situaciones." + } + }, + { + "value": "E5", + "name": { + "en": "Exempt: pursuant to Article 25. Exemptions in the delivery of goods destined to another Member State.", + "es": "Exenta: por el artículo 25. Exenciones en las entregas de bienes destinados a otro Estado miembro." + } + }, + { + "value": "E6", + "name": { + "en": "Exempt: pursuant to other reasons", + "es": "Exenta: por otra causa" + } + } + ] + } + ], + "scenarios": [ + { + "schema": "bill/invoice", + "list": [ + { + "type": [ + "standard" + ], + "ext": { + "es-verifactu-doc-type": "F1" + } + }, + { + "type": [ + "standard" + ], + "tags": [ + "simplified" + ], + "ext": { + "es-verifactu-doc-type": "F2" + } + }, + { + "type": [ + "standard" + ], + "tags": [ + "substitution" + ], + "ext": { + "es-verifactu-doc-type": "F3" + } + }, + { + "type": [ + "credit-note", + "corrective", + "debit-note" + ], + "ext": { + "es-verifactu-doc-type": "R1" + } + }, + { + "type": [ + "credit-note", + "corrective", + "debit-note" + ], + "tags": [ + "simplified" + ], + "ext": { + "es-verifactu-doc-type": "R5" + } + } + ] + } + ], + "corrections": [ + { + "schema": "bill/invoice", + "extensions": [ + "es-verifactu-doc-type" + ] + } + ] +} \ No newline at end of file From b47d4f6d36dc373317414c29d1a54fecf7bd2bb6 Mon Sep 17 00:00:00 2001 From: apardods Date: Fri, 22 Nov 2024 17:15:30 +0000 Subject: [PATCH 10/25] Fix Tests --- addons/es/verifactu/invoice_test.go | 13 ++-- addons/es/verifactu/scenarios_test.go | 101 ++++++++++++++------------ 2 files changed, 59 insertions(+), 55 deletions(-) diff --git a/addons/es/verifactu/invoice_test.go b/addons/es/verifactu/invoice_test.go index cef4dd06..4d2c0676 100644 --- a/addons/es/verifactu/invoice_test.go +++ b/addons/es/verifactu/invoice_test.go @@ -1,9 +1,8 @@ -package verifactu_test +package verifactu import ( "testing" - "github.com/invopop/gobl/addons/es/verifactu" "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/num" @@ -54,13 +53,13 @@ func TestInvoiceValidation(t *testing.T) { t.Run("correction invoice with preceding", func(t *testing.T) { inv := testInvoiceStandard(t) inv.Type = bill.InvoiceTypeCreditNote - inv.Tax.Ext[verifactu.ExtKeyDocType] = "R1" + inv.Tax.Ext[ExtKeyDocType] = "R1" inv.Preceding = []*org.DocumentRef{ { Series: "ABC", Code: "122", Ext: tax.Extensions{ - verifactu.ExtKeyDocType: "F1", + ExtKeyDocType: "F1", }, }, } @@ -78,7 +77,7 @@ func assertValidationError(t *testing.T, inv *bill.Invoice, expected string) { func testInvoiceStandard(t *testing.T) *bill.Invoice { t.Helper() return &bill.Invoice{ - Addons: tax.WithAddons(verifactu.V1), + Addons: tax.WithAddons(V1), Code: "123", Supplier: &org.Party{ Name: "Test Supplier", @@ -107,7 +106,7 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { Category: "VAT", Rate: "exempt", Ext: tax.Extensions{ - verifactu.ExtKeyTaxClassification: "E1", + ExtKeyTaxClassification: "E1", }, }, }, @@ -121,7 +120,7 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { }, Tax: &bill.Tax{ Ext: tax.Extensions{ - verifactu.ExtKeyDocType: "F1", + ExtKeyDocType: "F1", }, }, } diff --git a/addons/es/verifactu/scenarios_test.go b/addons/es/verifactu/scenarios_test.go index c6dc33d2..53fdd9d2 100644 --- a/addons/es/verifactu/scenarios_test.go +++ b/addons/es/verifactu/scenarios_test.go @@ -2,54 +2,59 @@ package verifactu import ( "testing" + + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestScenarios(t *testing.T) { - tests := []struct { - name string - scenario string - tags []string - want bool - }{ - { - name: "standard invoice", - scenario: "standard", - tags: []string{}, - want: true, - }, - { - name: "simplified invoice", - scenario: "simplified", - tags: []string{"simplified"}, - want: true, - }, - { - name: "corrective invoice", - scenario: "corrective", - tags: []string{"corrective"}, - want: true, - }, - { - name: "invalid scenario", - scenario: "invalid", - tags: []string{}, - want: false, - }, - { - name: "simplified with wrong tags", - scenario: "simplified", - tags: []string{"corrective"}, - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := ValidateScenario(tt.scenario, tt.tags) - if got != tt.want { - t.Errorf("ValidateScenario(%v, %v) = %v, want %v", - tt.scenario, tt.tags, got, tt.want) - } - }) - } +func TestInvoiceDocumentScenarios(t *testing.T) { + + t.Run("with addon", func(t *testing.T) { + i := testInvoiceStandard(t) + require.NoError(t, i.Calculate()) + assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "F1") + }) + + t.Run("simplified invoice", func(t *testing.T) { + i := testInvoiceStandard(t) + i.SetTags(tax.TagSimplified) + require.NoError(t, i.Calculate()) + assert.Len(t, i.Notes, 1) + assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "F2") + }) + + t.Run("substitution invoice", func(t *testing.T) { + i := testInvoiceStandard(t) + i.SetTags(TagSubstitution) + require.NoError(t, i.Calculate()) + assert.Len(t, i.Notes, 1) + assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "F3") + }) + + t.Run("credit note", func(t *testing.T) { + i := testInvoiceStandard(t) + i.Type = bill.InvoiceTypeCreditNote + require.NoError(t, i.Calculate()) + assert.Len(t, i.Notes, 1) + assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "R1") + }) + + t.Run("corrective", func(t *testing.T) { + i := testInvoiceStandard(t) + i.Type = bill.InvoiceTypeCorrective + require.NoError(t, i.Calculate()) + assert.Len(t, i.Notes, 1) + assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "R1") + }) + + t.Run("simplified credit note", func(t *testing.T) { + i := testInvoiceStandard(t) + i.Type = bill.InvoiceTypeCreditNote + i.SetTags(tax.TagSimplified) + require.NoError(t, i.Calculate()) + assert.Len(t, i.Notes, 1) + assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "R5") + }) } From 496b504ace0c83ea5efff6671715afe7acf45cf6 Mon Sep 17 00:00:00 2001 From: apardods Date: Tue, 26 Nov 2024 10:21:52 +0000 Subject: [PATCH 11/25] Added Invoice Tests and Updated Validation --- addons/es/verifactu/invoice.go | 7 ++----- addons/es/verifactu/invoice_test.go | 28 ++++++++++++++++------------ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/addons/es/verifactu/invoice.go b/addons/es/verifactu/invoice.go index 54d6446e..27aab433 100644 --- a/addons/es/verifactu/invoice.go +++ b/addons/es/verifactu/invoice.go @@ -90,11 +90,8 @@ func validateInvoicePreceding(val any) error { return nil } return validation.ValidateStruct(p, - validation.Field(&p.IssueDate, validation.Required), - validation.Field(&p.Series, validation.Required), - validation.Field(&p.Ext, - tax.ExtensionsRequires(ExtKeyDocType), - validation.Skip, + validation.Field(&p.IssueDate, + validation.Required, ), ) } diff --git a/addons/es/verifactu/invoice_test.go b/addons/es/verifactu/invoice_test.go index 4d2c0676..971ce9d7 100644 --- a/addons/es/verifactu/invoice_test.go +++ b/addons/es/verifactu/invoice_test.go @@ -1,14 +1,15 @@ package verifactu import ( + "fmt" "testing" "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/require" ) @@ -44,6 +45,13 @@ func TestInvoiceValidation(t *testing.T) { require.ErrorContains(t, err, "es-verifactu-doc-type: required") }) + t.Run("no customer", func(t *testing.T) { + inv := testInvoiceStandard(t) + inv.Customer = nil + require.NoError(t, inv.Calculate()) + require.NoError(t, inv.Validate()) + }) + t.Run("correction invoice requires preceding", func(t *testing.T) { inv := testInvoiceStandard(t) inv.Type = bill.InvoiceTypeCreditNote @@ -53,14 +61,12 @@ func TestInvoiceValidation(t *testing.T) { t.Run("correction invoice with preceding", func(t *testing.T) { inv := testInvoiceStandard(t) inv.Type = bill.InvoiceTypeCreditNote - inv.Tax.Ext[ExtKeyDocType] = "R1" + d := cal.MakeDate(2024, 1, 1) inv.Preceding = []*org.DocumentRef{ { - Series: "ABC", - Code: "122", - Ext: tax.Extensions{ - ExtKeyDocType: "F1", - }, + Series: "ABC", + Code: "122", + IssueDate: &d, }, } require.NoError(t, inv.Calculate()) @@ -71,6 +77,9 @@ func TestInvoiceValidation(t *testing.T) { func assertValidationError(t *testing.T, inv *bill.Invoice, expected string) { require.NoError(t, inv.Calculate()) err := inv.Validate() + if inv.Preceding != nil { + fmt.Println(inv.Preceding[0].IssueDate) + } require.ErrorContains(t, err, expected) } @@ -118,10 +127,5 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { Text: "This is a test invoice", }, }, - Tax: &bill.Tax{ - Ext: tax.Extensions{ - ExtKeyDocType: "F1", - }, - }, } } From d0055be53b83e4ecac5285adb6929e61bbaa83e1 Mon Sep 17 00:00:00 2001 From: apardods Date: Tue, 26 Nov 2024 10:27:36 +0000 Subject: [PATCH 12/25] Cover Preceding --- addons/es/verifactu/invoice.go | 14 ++++++++------ addons/es/verifactu/invoice_test.go | 11 +++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/addons/es/verifactu/invoice.go b/addons/es/verifactu/invoice.go index 27aab433..ca6f0820 100644 --- a/addons/es/verifactu/invoice.go +++ b/addons/es/verifactu/invoice.go @@ -29,7 +29,9 @@ func validateInvoice(inv *bill.Invoice) error { inv.Type.In(es.InvoiceCorrectionTypes...), validation.Required, ), - validation.By(validateInvoicePreceding), + validation.Each( + validation.By(validateInvoicePreceding), + ), validation.Skip, ), validation.Field(&inv.Tax, @@ -65,8 +67,8 @@ func validateInvoiceTax(val any) error { } func validateInvoiceCustomer(val any) error { - obj, _ := val.(*org.Party) - if obj == nil { + obj, ok := val.(*org.Party) + if !ok || obj == nil { return nil } // Customers must have a tax ID to at least set the country, @@ -86,7 +88,7 @@ func validateInvoiceCustomer(val any) error { func validateInvoicePreceding(val any) error { p, ok := val.(*org.DocumentRef) - if !ok { + if !ok || p == nil { return nil } return validation.ValidateStruct(p, @@ -97,8 +99,8 @@ func validateInvoicePreceding(val any) error { } func validateInvoiceLine(value any) error { - obj, _ := value.(*bill.Line) - if obj == nil { + obj, ok := value.(*bill.Line) + if !ok || obj == nil { return nil } return validation.ValidateStruct(obj, diff --git a/addons/es/verifactu/invoice_test.go b/addons/es/verifactu/invoice_test.go index 971ce9d7..20d17286 100644 --- a/addons/es/verifactu/invoice_test.go +++ b/addons/es/verifactu/invoice_test.go @@ -58,6 +58,17 @@ func TestInvoiceValidation(t *testing.T) { 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 From c57dc1cd8e0437238df12dcf5abc32d510046ce9 Mon Sep 17 00:00:00 2001 From: apardods Date: Tue, 26 Nov 2024 10:42:05 +0000 Subject: [PATCH 13/25] Run go generate --- data/addons/es-verifactu-v1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/addons/es-verifactu-v1.json b/data/addons/es-verifactu-v1.json index fa243aa5..22a11dc3 100644 --- a/data/addons/es-verifactu-v1.json +++ b/data/addons/es-verifactu-v1.json @@ -80,7 +80,7 @@ "es": "Código de Clasificación/Exención de Impuesto de Verifactu - L9/10" }, "desc": { - "en": "Tax classification code used to identify the type of tax being applied to the invoice. In Verifactu these\nlists are separate but here they are combined." + "en": "Tax classification code used to identify the type of tax being applied to the line. It includes both exemption reasons and tax scenarios.\nThese lists are separate in Verifactu but combined here for convenience." }, "values": [ { From bff32564b1640c19beb42f22ca3c10d34de94f16 Mon Sep 17 00:00:00 2001 From: apardods Date: Tue, 26 Nov 2024 10:44:32 +0000 Subject: [PATCH 14/25] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 559558eb..7a8075a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] +### Added + +- `es-verifactu-v1`: added initial Spain VeriFactu addon. + ## [v0.206.0] - 2024-11-26 ### Added From 056b02fb77f14e5f80893191059f65a7c8eb7ef9 Mon Sep 17 00:00:00 2001 From: apardods Date: Tue, 26 Nov 2024 10:47:36 +0000 Subject: [PATCH 15/25] Added sources --- addons/es/verifactu/extensions.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index 13bc4120..6190289b 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -21,7 +21,9 @@ var extensions = []*cbc.KeyDefinition{ }, Desc: i18n.String{ i18n.EN: here.Doc(` - Invoice type code used to identify the type of invoice being sent. + Invoice type code used to identify the type of invoice being sent. + Source: VeriFactu Ministerial Order: + * https://www.boe.es/diario_boe/txt.php?id=BOE-A-2024-22138 `), }, Values: []*cbc.ValueDefinition{ @@ -93,6 +95,8 @@ var extensions = []*cbc.KeyDefinition{ i18n.EN: here.Doc(` Tax classification code used to identify the type of tax being applied to the line. It includes both exemption reasons and tax scenarios. These lists are separate in Verifactu but combined here for convenience. + Source: VeriFactu Ministerial Order: + * https://www.boe.es/diario_boe/txt.php?id=BOE-A-2024-22138 `), }, Values: []*cbc.ValueDefinition{ From 8cb512e1145c4142600a814a54c7d8156f82e832 Mon Sep 17 00:00:00 2001 From: apardods Date: Wed, 27 Nov 2024 17:01:24 +0000 Subject: [PATCH 16/25] Adding tag --- addons/es/verifactu/invoice_test.go | 38 ++++++++++++++++++++------- addons/es/verifactu/scenarios.go | 20 ++++++++++++-- addons/es/verifactu/scenarios_test.go | 17 ++++++------ addons/es/verifactu/verifactu.go | 7 +++-- data/addons/es-verifactu-v1.json | 18 +++++++++++-- 5 files changed, 76 insertions(+), 24 deletions(-) diff --git a/addons/es/verifactu/invoice_test.go b/addons/es/verifactu/invoice_test.go index 20d17286..fffc2365 100644 --- a/addons/es/verifactu/invoice_test.go +++ b/addons/es/verifactu/invoice_test.go @@ -1,15 +1,16 @@ -package verifactu +package verifactu_test import ( - "fmt" "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" ) @@ -18,6 +19,7 @@ func TestInvoiceValidation(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) @@ -25,7 +27,7 @@ func TestInvoiceValidation(t *testing.T) { assertValidationError(t, inv, "customer: (tax_id: cannot be blank.)") }) - t.Run("with exemption reason", func(t *testing.T) { + t.Run("without exemption reason", func(t *testing.T) { inv := testInvoiceStandard(t) inv.Lines[0].Taxes[0].Ext = nil assertValidationError(t, inv, "es-verifactu-tax-classification: required") @@ -45,11 +47,13 @@ func TestInvoiceValidation(t *testing.T) { require.ErrorContains(t, err, "es-verifactu-doc-type: required") }) - t.Run("no customer", func(t *testing.T) { + 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("correction invoice requires preceding", func(t *testing.T) { @@ -82,22 +86,36 @@ func TestInvoiceValidation(t *testing.T) { } require.NoError(t, inv.Calculate()) require.NoError(t, inv.Validate()) + assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "R1") + }) + + t.Run("substitution invoice", func(t *testing.T) { + inv := testInvoiceStandard(t) + inv.Tags = tax.Tags{List: []cbc.Key{verifactu.TagSubstitution}} + d := cal.MakeDate(2024, 1, 1) + inv.Preceding = []*org.DocumentRef{ + { + Series: "ABC", + Code: "122", + IssueDate: &d, + }, + } + require.NoError(t, inv.Calculate()) + require.NoError(t, inv.Validate()) + assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "F3") }) } func assertValidationError(t *testing.T, inv *bill.Invoice, expected string) { require.NoError(t, inv.Calculate()) err := inv.Validate() - if inv.Preceding != nil { - fmt.Println(inv.Preceding[0].IssueDate) - } require.ErrorContains(t, err, expected) } func testInvoiceStandard(t *testing.T) *bill.Invoice { t.Helper() return &bill.Invoice{ - Addons: tax.WithAddons(V1), + Addons: tax.WithAddons(verifactu.V1), Code: "123", Supplier: &org.Party{ Name: "Test Supplier", @@ -124,9 +142,9 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { Taxes: tax.Set{ { Category: "VAT", - Rate: "exempt", + Rate: "standard", Ext: tax.Extensions{ - ExtKeyTaxClassification: "E1", + "es-verifactu-tax-classification": "S1", }, }, }, diff --git a/addons/es/verifactu/scenarios.go b/addons/es/verifactu/scenarios.go index 0d50d080..2951c9c7 100644 --- a/addons/es/verifactu/scenarios.go +++ b/addons/es/verifactu/scenarios.go @@ -3,15 +3,31 @@ package verifactu import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/regimes/es" "github.com/invopop/gobl/tax" ) const ( - // TagSubstitution is used to identify substitution invoices. - TagSubstitution = "substitution" + // TagSubstitution is used to identify the case for: + // Factura emitida en sustitución de facturas simplificadas facturadas y declaradas. + // To be used when a simplified invoice has been issued and declared. + TagSubstitution cbc.Key = "substitution" ) +var invoiceTags = &tax.TagSet{ + Schema: bill.ShortSchemaInvoice, + List: []*cbc.KeyDefinition{ + { + Key: TagSubstitution, + Name: i18n.String{ + i18n.EN: "Invoice issued in substitution of simplified invoices issued and declared", + i18n.ES: "Factura emitida en sustitución de facturas simplificadas facturadas y declaradas", + }, + }, + }, +} + var scenarios = []*tax.ScenarioSet{ { Schema: bill.ShortSchemaInvoice, diff --git a/addons/es/verifactu/scenarios_test.go b/addons/es/verifactu/scenarios_test.go index 53fdd9d2..9352132a 100644 --- a/addons/es/verifactu/scenarios_test.go +++ b/addons/es/verifactu/scenarios_test.go @@ -1,8 +1,9 @@ -package verifactu +package verifactu_test import ( "testing" + "github.com/invopop/gobl/addons/es/verifactu" "github.com/invopop/gobl/bill" "github.com/invopop/gobl/tax" "github.com/stretchr/testify/assert" @@ -14,7 +15,7 @@ func TestInvoiceDocumentScenarios(t *testing.T) { t.Run("with addon", func(t *testing.T) { i := testInvoiceStandard(t) require.NoError(t, i.Calculate()) - assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "F1") + assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "F1") }) t.Run("simplified invoice", func(t *testing.T) { @@ -22,15 +23,15 @@ func TestInvoiceDocumentScenarios(t *testing.T) { i.SetTags(tax.TagSimplified) require.NoError(t, i.Calculate()) assert.Len(t, i.Notes, 1) - assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "F2") + assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "F2") }) t.Run("substitution invoice", func(t *testing.T) { i := testInvoiceStandard(t) - i.SetTags(TagSubstitution) + i.SetTags(verifactu.TagSubstitution) require.NoError(t, i.Calculate()) assert.Len(t, i.Notes, 1) - assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "F3") + assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "F3") }) t.Run("credit note", func(t *testing.T) { @@ -38,7 +39,7 @@ func TestInvoiceDocumentScenarios(t *testing.T) { i.Type = bill.InvoiceTypeCreditNote require.NoError(t, i.Calculate()) assert.Len(t, i.Notes, 1) - assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "R1") + assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "R1") }) t.Run("corrective", func(t *testing.T) { @@ -46,7 +47,7 @@ func TestInvoiceDocumentScenarios(t *testing.T) { i.Type = bill.InvoiceTypeCorrective require.NoError(t, i.Calculate()) assert.Len(t, i.Notes, 1) - assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "R1") + assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "R1") }) t.Run("simplified credit note", func(t *testing.T) { @@ -55,6 +56,6 @@ func TestInvoiceDocumentScenarios(t *testing.T) { i.SetTags(tax.TagSimplified) require.NoError(t, i.Calculate()) assert.Len(t, i.Notes, 1) - assert.Equal(t, i.Tax.Ext[ExtKeyDocType].String(), "R5") + assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "R5") }) } diff --git a/addons/es/verifactu/verifactu.go b/addons/es/verifactu/verifactu.go index 8d0ebe25..7c6a971d 100644 --- a/addons/es/verifactu/verifactu.go +++ b/addons/es/verifactu/verifactu.go @@ -29,8 +29,11 @@ func newAddon() *tax.AddonDef { Name: i18n.String{ i18n.EN: "Spain Verifactu", }, - Extensions: extensions, - Validator: validate, + Extensions: extensions, + Validator: validate, + Tags: []*tax.TagSet{ + invoiceTags, + }, Scenarios: scenarios, Normalizer: normalize, Corrections: invoiceCorrectionDefinitions, diff --git a/data/addons/es-verifactu-v1.json b/data/addons/es-verifactu-v1.json index 22a11dc3..a2496964 100644 --- a/data/addons/es-verifactu-v1.json +++ b/data/addons/es-verifactu-v1.json @@ -12,7 +12,7 @@ "es": "Código de Tipo de Factura de Verifactu - L2" }, "desc": { - "en": "Invoice type code used to identify the type of invoice being sent." + "en": "Invoice type code used to identify the type of invoice being sent.\nSource: VeriFactu Ministerial Order:\n * https://www.boe.es/diario_boe/txt.php?id=BOE-A-2024-22138" }, "values": [ { @@ -80,7 +80,7 @@ "es": "Código de Clasificación/Exención de Impuesto de Verifactu - L9/10" }, "desc": { - "en": "Tax classification code used to identify the type of tax being applied to the line. It includes both exemption reasons and tax scenarios.\nThese lists are separate in Verifactu but combined here for convenience." + "en": "Tax classification code used to identify the type of tax being applied to the line. It includes both exemption reasons and tax scenarios.\nThese lists are separate in Verifactu but combined here for convenience.\nSource: VeriFactu Ministerial Order:\n * https://www.boe.es/diario_boe/txt.php?id=BOE-A-2024-22138" }, "values": [ { @@ -156,6 +156,20 @@ ] } ], + "tags": [ + { + "schema": "bill/invoice", + "list": [ + { + "key": "substitution", + "name": { + "en": "Invoice issued in substitution of simplified invoices issued and declared", + "es": "Factura emitida en sustitución de facturas simplificadas facturadas y declaradas" + } + } + ] + } + ], "scenarios": [ { "schema": "bill/invoice", From 6aa13a0578c8c19afd8baada1cbf1cc1b95dc160 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 4 Dec 2024 17:25:20 +0000 Subject: [PATCH 17/25] Refactoring handling of Verifactu extensions and scenarios, plus other simplifications --- addons/es/verifactu/bill.go | 163 ++++++++++++++++++ .../{invoice_test.go => bill_test.go} | 40 +++-- addons/es/verifactu/extensions.go | 64 ++++++- addons/es/verifactu/invoice.go | 128 -------------- addons/es/verifactu/scenarios.go | 48 ------ addons/es/verifactu/scenarios_test.go | 29 ++-- addons/es/verifactu/tax.go | 55 ++++++ addons/es/verifactu/tax_test.go | 49 ++++++ addons/es/verifactu/verifactu.go | 20 ++- bill/invoice.go | 1 + bill/invoice_correct.go | 7 +- bill/invoice_correct_test.go | 2 +- bill/invoice_scenarios.go | 22 +-- bill/tax.go | 13 ++ bill/tax_test.go | 40 +++++ cbc/key.go | 14 ++ cbc/key_test.go | 7 + examples/es/invoice-es-es-verifactu.yaml | 53 ++++++ examples/es/out/invoice-es-es-verifactu.json | 149 ++++++++++++++++ tax/constants.go | 4 +- tax/corrections.go | 3 +- tax/extensions.go | 42 +++++ tax/extensions_test.go | 66 +++++++ 23 files changed, 769 insertions(+), 250 deletions(-) create mode 100644 addons/es/verifactu/bill.go rename addons/es/verifactu/{invoice_test.go => bill_test.go} (86%) delete mode 100644 addons/es/verifactu/invoice.go create mode 100644 addons/es/verifactu/tax.go create mode 100644 addons/es/verifactu/tax_test.go create mode 100644 examples/es/invoice-es-es-verifactu.yaml create mode 100644 examples/es/out/invoice-es-es-verifactu.json diff --git a/addons/es/verifactu/bill.go b/addons/es/verifactu/bill.go new file mode 100644 index 00000000..c05608d3 --- /dev/null +++ b/addons/es/verifactu/bill.go @@ -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 + } + return validation.ValidateStruct(p, + validation.Field(&p.IssueDate, + validation.Required, + ), + ) +} diff --git a/addons/es/verifactu/invoice_test.go b/addons/es/verifactu/bill_test.go similarity index 86% rename from addons/es/verifactu/invoice_test.go rename to addons/es/verifactu/bill_test.go index fffc2365..a6c3ec57 100644 --- a/addons/es/verifactu/invoice_test.go +++ b/addons/es/verifactu/bill_test.go @@ -1,6 +1,7 @@ package verifactu_test import ( + "encoding/json" "testing" "github.com/invopop/gobl/addons/es/verifactu" @@ -29,8 +30,10 @@ func TestInvoiceValidation(t *testing.T) { 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-tax-classification: required") + assertValidationError(t, inv, "es-verifactu-op-class: required") }) t.Run("without notes", func(t *testing.T) { @@ -56,6 +59,17 @@ func TestInvoiceValidation(t *testing.T) { 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()) + + 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 @@ -82,28 +96,19 @@ func TestInvoiceValidation(t *testing.T) { 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) }) - t.Run("substitution invoice", func(t *testing.T) { - inv := testInvoiceStandard(t) - inv.Tags = tax.Tags{List: []cbc.Key{verifactu.TagSubstitution}} - d := cal.MakeDate(2024, 1, 1) - inv.Preceding = []*org.DocumentRef{ - { - Series: "ABC", - Code: "122", - IssueDate: &d, - }, - } - require.NoError(t, inv.Calculate()) - require.NoError(t, inv.Validate()) - assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "F3") - }) } func assertValidationError(t *testing.T, inv *bill.Invoice, expected string) { @@ -143,9 +148,6 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { { Category: "VAT", Rate: "standard", - Ext: tax.Extensions{ - "es-verifactu-tax-classification": "S1", - }, }, }, }, diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index 6190289b..d57ac7c8 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -8,8 +8,10 @@ import ( // Extension keys for Verifactu const ( - ExtKeyDocType cbc.Key = "es-verifactu-doc-type" - ExtKeyTaxClassification cbc.Key = "es-verifactu-tax-classification" + ExtKeyDocType cbc.Key = "es-verifactu-doc-type" + ExtKeyOpClass cbc.Key = "es-verifactu-op-class" + ExtKeyCorrectionType cbc.Key = "es-verifactu-correction-type" + ExtKeyExempt cbc.Key = "es-verifactu-exempt" ) var extensions = []*cbc.KeyDefinition{ @@ -44,7 +46,7 @@ var extensions = []*cbc.KeyDefinition{ { Value: "F3", Name: i18n.String{ - i18n.EN: "Invoice in substitution of simplified invoices.", + i18n.EN: "Invoice issued as a replacement for simplified invoices that have been billed and declared.", i18n.ES: "Factura emitida en sustitución de facturas simplificadas facturadas y declaradas.", }, }, @@ -86,17 +88,49 @@ var extensions = []*cbc.KeyDefinition{ }, }, { - Key: ExtKeyTaxClassification, + Key: ExtKeyCorrectionType, Name: i18n.String{ - i18n.EN: "Verifactu Tax Classification/Exemption Code - L9/10", - i18n.ES: "Código de Clasificación/Exención de Impuesto de Verifactu - L9/10", + i18n.EN: "Verifactu Correction Type Code - L3", + i18n.ES: "Código de Tipo de Corrección de Verifactu - L3", }, Desc: i18n.String{ i18n.EN: here.Doc(` - Tax classification code used to identify the type of tax being applied to the line. It includes both exemption reasons and tax scenarios. - These lists are separate in Verifactu but combined here for convenience. + Correction type code used to identify the type of correction being made. + This value will be determined automatically according to the invoice type. + Corrective invoices will be marked as "S", while credit and debit notes as "I". + `), + }, + Values: []*cbc.ValueDefinition{ + { + Value: "S", + Name: i18n.String{ + i18n.EN: "Substitution", + i18n.ES: "Por Sustitución", + }, + }, + { + Value: "I", + Name: i18n.String{ + i18n.EN: "Differences", + i18n.ES: "Por Diferencias", + }, + }, + }, + }, + { + Key: ExtKeyOpClass, + Name: i18n.String{ + i18n.EN: "Verifactu Operation Classification/Exemption Code - L9", + i18n.ES: "Código de Clasificación/Exención de Impuesto de Verifactu - L9", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Operation classification code used to identify if taxes should be applied to the line. Source: VeriFactu Ministerial Order: * https://www.boe.es/diario_boe/txt.php?id=BOE-A-2024-22138 + For details on how best to use and apply these and other codes, see the + AEAT FAQ: + * https://sede.agenciatributaria.gob.es/Sede/impuestos-tasas/iva/iva-libros-registro-iva-traves-aeat/preguntas-frecuentes/3-libro-registro-facturas-expedidas.html?faqId=b5556c3d02bc9510VgnVCM100000dc381e0aRCRD `), }, Values: []*cbc.ValueDefinition{ @@ -128,6 +162,20 @@ var extensions = []*cbc.KeyDefinition{ i18n.ES: "Operación No Sujeta por Reglas de localización", }, }, + }, + }, + { + Key: ExtKeyExempt, + Name: i18n.String{ + i18n.EN: "Verifactu Exemption Code - L10", + i18n.ES: "Código de Exención de Impuesto de Verifactu - L10", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Exemption code used to explain why the operation is exempt from taxes. + `), + }, + Values: []*cbc.ValueDefinition{ { Value: "E1", Name: i18n.String{ diff --git a/addons/es/verifactu/invoice.go b/addons/es/verifactu/invoice.go deleted file mode 100644 index ca6f0820..00000000 --- a/addons/es/verifactu/invoice.go +++ /dev/null @@ -1,128 +0,0 @@ -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 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), - validation.Skip, - ), - validation.Field(&inv.Lines, - validation.Each( - validation.By(validateInvoiceLine), - validation.Skip, - ), - validation.Skip, - ), - validation.Field(&inv.Notes, - cbc.ValidateNotesHasKey(cbc.NoteKeyGeneral), - validation.Skip, - ), - ) -} - -func validateInvoiceTax(val any) error { - obj, ok := val.(*bill.Tax) - if obj == nil || !ok { - return nil - } - return validation.ValidateStruct(obj, - validation.Field(&obj.Ext, - tax.ExtensionsRequires(ExtKeyDocType), - 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 - } - return validation.ValidateStruct(p, - validation.Field(&p.IssueDate, - validation.Required, - ), - ) -} - -func validateInvoiceLine(value any) error { - obj, ok := value.(*bill.Line) - if !ok || obj == nil { - return nil - } - return validation.ValidateStruct(obj, - validation.Field(&obj.Taxes, - validation.Each( - validation.By(validateInvoiceLineTax), - validation.Skip, - ), - validation.Skip, - ), - ) -} - -func validateInvoiceLineTax(value any) error { - obj, ok := value.(*tax.Combo) - if obj == nil || !ok { - return nil - } - return validation.ValidateStruct(obj, - validation.Field(&obj.Ext, - tax.ExtensionsRequires(ExtKeyTaxClassification), - validation.Skip, - ), - ) -} diff --git a/addons/es/verifactu/scenarios.go b/addons/es/verifactu/scenarios.go index 2951c9c7..509364ff 100644 --- a/addons/es/verifactu/scenarios.go +++ b/addons/es/verifactu/scenarios.go @@ -3,31 +3,9 @@ package verifactu import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/i18n" - "github.com/invopop/gobl/regimes/es" "github.com/invopop/gobl/tax" ) -const ( - // TagSubstitution is used to identify the case for: - // Factura emitida en sustitución de facturas simplificadas facturadas y declaradas. - // To be used when a simplified invoice has been issued and declared. - TagSubstitution cbc.Key = "substitution" -) - -var invoiceTags = &tax.TagSet{ - Schema: bill.ShortSchemaInvoice, - List: []*cbc.KeyDefinition{ - { - Key: TagSubstitution, - Name: i18n.String{ - i18n.EN: "Invoice issued in substitution of simplified invoices issued and declared", - i18n.ES: "Factura emitida en sustitución de facturas simplificadas facturadas y declaradas", - }, - }, - }, -} - var scenarios = []*tax.ScenarioSet{ { Schema: bill.ShortSchemaInvoice, @@ -52,32 +30,6 @@ var scenarios = []*tax.ScenarioSet{ ExtKeyDocType: "F2", }, }, - { - Types: []cbc.Key{ - bill.InvoiceTypeStandard, - }, - Tags: []cbc.Key{ - TagSubstitution, - }, - Ext: tax.Extensions{ - ExtKeyDocType: "F3", - }, - }, - { - Types: es.InvoiceCorrectionTypes, - Ext: tax.Extensions{ - ExtKeyDocType: "R1", - }, - }, - { - Types: es.InvoiceCorrectionTypes, - Tags: []cbc.Key{ - tax.TagSimplified, - }, - Ext: tax.Extensions{ - ExtKeyDocType: "R5", - }, - }, }, }, } diff --git a/addons/es/verifactu/scenarios_test.go b/addons/es/verifactu/scenarios_test.go index 9352132a..46b85766 100644 --- a/addons/es/verifactu/scenarios_test.go +++ b/addons/es/verifactu/scenarios_test.go @@ -11,7 +11,6 @@ import ( ) func TestInvoiceDocumentScenarios(t *testing.T) { - t.Run("with addon", func(t *testing.T) { i := testInvoiceStandard(t) require.NoError(t, i.Calculate()) @@ -26,36 +25,32 @@ func TestInvoiceDocumentScenarios(t *testing.T) { assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "F2") }) - t.Run("substitution invoice", func(t *testing.T) { - i := testInvoiceStandard(t) - i.SetTags(verifactu.TagSubstitution) - require.NoError(t, i.Calculate()) - assert.Len(t, i.Notes, 1) - assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "F3") - }) - t.Run("credit note", func(t *testing.T) { i := testInvoiceStandard(t) - i.Type = bill.InvoiceTypeCreditNote - require.NoError(t, i.Calculate()) + require.NoError(t, i.Correct(bill.Credit, bill.WithExtension(verifactu.ExtKeyDocType, "R1"))) + // require.NoError(t, i.Calculate()) assert.Len(t, i.Notes, 1) assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "R1") + assert.Equal(t, i.Tax.Ext.Get(verifactu.ExtKeyCorrectionType).String(), "I") }) t.Run("corrective", func(t *testing.T) { i := testInvoiceStandard(t) - i.Type = bill.InvoiceTypeCorrective - require.NoError(t, i.Calculate()) + require.NoError(t, i.Correct(bill.Corrective, bill.WithExtension(verifactu.ExtKeyDocType, "R2"))) assert.Len(t, i.Notes, 1) - assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "R1") + assert.Equal(t, i.Tax.Ext.Get(verifactu.ExtKeyDocType).String(), "R2") + assert.Equal(t, i.Tax.Ext.Get(verifactu.ExtKeyCorrectionType).String(), "S") }) - t.Run("simplified credit note", func(t *testing.T) { + t.Run("simplified corrective", func(t *testing.T) { i := testInvoiceStandard(t) - i.Type = bill.InvoiceTypeCreditNote i.SetTags(tax.TagSimplified) require.NoError(t, i.Calculate()) + + require.NoError(t, i.Correct(bill.Corrective, bill.WithExtension(verifactu.ExtKeyDocType, "F3"))) + assert.Len(t, i.Notes, 1) - assert.Equal(t, i.Tax.Ext[verifactu.ExtKeyDocType].String(), "R5") + assert.Equal(t, "F3", i.Tax.Ext.Get(verifactu.ExtKeyDocType).String()) + assert.Equal(t, "", i.Tax.Ext.Get(verifactu.ExtKeyCorrectionType).String()) }) } diff --git a/addons/es/verifactu/tax.go b/addons/es/verifactu/tax.go new file mode 100644 index 00000000..f45476db --- /dev/null +++ b/addons/es/verifactu/tax.go @@ -0,0 +1,55 @@ +package verifactu + +import ( + "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/regimes/es" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +// Simple map of the tax rates to operation classes. These only apply to tax combos +// in Spain and only for the most basic of situations. +var taxCategoryOpClassMap = tax.Extensions{ + tax.RateStandard: "S1", + tax.RateReduced: "S1", + tax.RateSuperReduced: "S1", + tax.RateZero: "S1", +} + +func normalizeTaxCombo(tc *tax.Combo) { + if tc.Country != "" && tc.Country != l10n.ES.Tax() { + return + } + switch tc.Category { + case tax.CategoryVAT, es.TaxCategoryIGIC: + if tc.Rate.IsEmpty() { + return + } + v := taxCategoryOpClassMap.Get(tc.Rate) + if v == "" { + return + } + tc.Ext = tc.Ext.Merge( + tax.Extensions{ExtKeyOpClass: v}, + ) + } +} + +func validateTaxCombo(tc *tax.Combo) error { + if !tc.Category.In(tax.CategoryVAT, es.TaxCategoryIGIC) { + return nil + } + return validation.ValidateStruct(tc, + validation.Field(&tc.Ext, + validation.When( + tc.Percent != nil, // Taxed + tax.ExtensionsRequires(ExtKeyOpClass), + ), + validation.When( + tc.Percent == nil && !tc.Ext.Has(ExtKeyOpClass), + tax.ExtensionsRequires(ExtKeyExempt), + ), + validation.Skip, + ), + ) +} diff --git a/addons/es/verifactu/tax_test.go b/addons/es/verifactu/tax_test.go new file mode 100644 index 00000000..51a27be9 --- /dev/null +++ b/addons/es/verifactu/tax_test.go @@ -0,0 +1,49 @@ +package verifactu + +import ( + "testing" + + "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeTaxCombo(t *testing.T) { + t.Run("valid", func(t *testing.T) { + tc := &tax.Combo{ + Category: tax.CategoryVAT, + Rate: tax.RateStandard, + } + normalizeTaxCombo(tc) + assert.Equal(t, "S1", tc.Ext.Get(ExtKeyOpClass).String()) + }) + + t.Run("valid with country", func(t *testing.T) { + tc := &tax.Combo{ + Country: l10n.ES.Tax(), + Category: tax.CategoryVAT, + Rate: tax.RateSuperReduced, + } + normalizeTaxCombo(tc) + assert.Equal(t, "S1", tc.Ext.Get(ExtKeyOpClass).String()) + }) + + t.Run("undefined rate", func(t *testing.T) { + tc := &tax.Combo{ + Category: tax.CategoryVAT, + Rate: tax.RateExempt, + } + normalizeTaxCombo(tc) + assert.Empty(t, tc.Ext.Get(ExtKeyOpClass).String()) + }) + + t.Run("foreign country", func(t *testing.T) { + tc := &tax.Combo{ + Country: "FR", + Category: tax.CategoryVAT, + Rate: tax.RateStandard, + } + normalizeTaxCombo(tc) + assert.Empty(t, tc.Ext.Get(ExtKeyOpClass).String()) + }) +} diff --git a/addons/es/verifactu/verifactu.go b/addons/es/verifactu/verifactu.go index 7c6a971d..4ead855c 100644 --- a/addons/es/verifactu/verifactu.go +++ b/addons/es/verifactu/verifactu.go @@ -27,27 +27,31 @@ func newAddon() *tax.AddonDef { return &tax.AddonDef{ Key: V1, Name: i18n.String{ - i18n.EN: "Spain Verifactu", - }, - Extensions: extensions, - Validator: validate, - Tags: []*tax.TagSet{ - invoiceTags, + i18n.EN: "Spain Verifactu V1", }, + Extensions: extensions, + Validator: validate, Scenarios: scenarios, Normalizer: normalize, Corrections: invoiceCorrectionDefinitions, } } -func normalize(_ any) { - // nothing to normalize yet +func normalize(doc any) { + switch obj := doc.(type) { + case *bill.Invoice: + normalizeInvoice(obj) + case *tax.Combo: + normalizeTaxCombo(obj) + } } func validate(doc any) error { switch obj := doc.(type) { case *bill.Invoice: return validateInvoice(obj) + case *tax.Combo: + return validateTaxCombo(obj) } return nil } diff --git a/bill/invoice.go b/bill/invoice.go index b52c1dc5..77731c83 100644 --- a/bill/invoice.go +++ b/bill/invoice.go @@ -267,6 +267,7 @@ func (inv *Invoice) Calculate() error { if err := inv.calculate(); err != nil { return err } + if err := inv.prepareScenarios(); err != nil { return err } diff --git a/bill/invoice_correct.go b/bill/invoice_correct.go index 82cece22..2cdc2f0a 100644 --- a/bill/invoice_correct.go +++ b/bill/invoice_correct.go @@ -33,7 +33,8 @@ type CorrectionOptions struct { Stamps []*head.Stamp `json:"stamps,omitempty" jsonschema:"title=Stamps"` // Human readable reason for the corrective operation. Reason string `json:"reason,omitempty" jsonschema:"title=Reason"` - // Extensions for region specific requirements. + // Extensions for region specific requirements that may be added in the preceding + // or at the document level, according to the local rules. Ext tax.Extensions `json:"ext,omitempty" jsonschema:"title=Extensions"` // In case we want to use a raw json object as a source of the options. @@ -303,7 +304,7 @@ func (inv *Invoice) Correct(opts ...schema.Option) error { } // correctionDef tries to determine a final correction definition -// by merge potentially multiple sources. The results include +// by merging potentially multiple sources. The results include // a key that can be used to identify the definition. func (inv *Invoice) correctionDef() *tax.CorrectionDefinition { cd := &tax.CorrectionDefinition{ @@ -363,7 +364,7 @@ func (inv *Invoice) validatePrecedingData(o *CorrectionOptions, cd *tax.Correcti pre.Stamps = append(pre.Stamps, s) } - if !o.Type.In(cd.Types...) { + if len(cd.Types) > 0 && !o.Type.In(cd.Types...) { return fmt.Errorf("invalid correction type: %v", o.Type.String()) } diff --git a/bill/invoice_correct_test.go b/bill/invoice_correct_test.go index 8e8f6da4..53800e1c 100644 --- a/bill/invoice_correct_test.go +++ b/bill/invoice_correct_test.go @@ -20,7 +20,6 @@ import ( ) func TestInvoiceCorrect(t *testing.T) { - i := testInvoicePTForCorrection(t) err := i.Correct(bill.Corrective) require.Error(t, err) @@ -263,6 +262,7 @@ func testInvoiceESForCorrection(t *testing.T) *bill.Invoice { func testInvoicePTForCorrection(t *testing.T) *bill.Invoice { t.Helper() i := &bill.Invoice{ + Regime: tax.WithRegime("PT"), Series: "TEST", Code: "123", Tax: &bill.Tax{ diff --git a/bill/invoice_scenarios.go b/bill/invoice_scenarios.go index 9458d5d7..3e59e99f 100644 --- a/bill/invoice_scenarios.go +++ b/bill/invoice_scenarios.go @@ -49,16 +49,11 @@ func (inv *Invoice) scenarioSummary() *tax.ScenarioSummary { ss.Merge(a.Scenarios) } - inv.removePreviousScenarios(ss) + inv.removePreviousScenarioNotes(ss) return ss.SummaryFor(inv) } -func (inv *Invoice) removePreviousScenarios(ss *tax.ScenarioSet) { - if inv.Tax != nil && len(inv.Tax.Ext) > 0 { - for _, ek := range ss.ExtensionKeys() { - delete(inv.Tax.Ext, ek) - } - } +func (inv *Invoice) removePreviousScenarioNotes(ss *tax.ScenarioSet) { for _, n := range ss.Notes() { for i, n2 := range inv.Notes { if n.SameAs(n2) { @@ -75,6 +70,7 @@ func (inv *Invoice) prepareScenarios() error { if ss == nil { return nil } + for _, n := range ss.Notes { // make sure we don't already have the same note in the invoice for _, n2 := range inv.Notes { @@ -87,16 +83,10 @@ func (inv *Invoice) prepareScenarios() error { inv.Notes = append(inv.Notes, n) } } + // Apply extensions at the document level - for k, v := range ss.Ext { - if inv.Tax == nil { - inv.Tax = new(Tax) - } - if inv.Tax.Ext == nil { - inv.Tax.Ext = make(tax.Extensions) - } - // Always override - inv.Tax.Ext[k] = v + if len(ss.Ext) > 0 { + inv.Tax = inv.Tax.MergeExtensions(ss.Ext) } return nil diff --git a/bill/tax.go b/bill/tax.go index 1b8c3cd6..781a0aff 100644 --- a/bill/tax.go +++ b/bill/tax.go @@ -27,6 +27,19 @@ type Tax struct { tags []cbc.Key } +// MergeExtensions makes it easier to add extensions to the tax object +// by automatically handling nil data, and replying a new updated instance. +func (t *Tax) MergeExtensions(ext tax.Extensions) *Tax { + if len(ext) == 0 { + return t + } + if t == nil { + t = new(Tax) + } + t.Ext = t.Ext.Merge(ext) + return t +} + // Normalize performs normalization on the tax and embedded objects using the // provided list of normalizers. func (t *Tax) Normalize(normalizers tax.Normalizers) { diff --git a/bill/tax_test.go b/bill/tax_test.go index 91a4f0a3..e1ae7cfc 100644 --- a/bill/tax_test.go +++ b/bill/tax_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/tax" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -118,3 +119,42 @@ func TestInvoiceTaxTagsMigration(t *testing.T) { assert.Equal(t, "simplified", inv.GetTags()[0].String()) } + +func TestTaxMergeExtensions(t *testing.T) { + t.Run("nil tax", func(t *testing.T) { + var tx *bill.Tax + ext := tax.Extensions{ + "vat-cat": "standard", + } + tx = tx.MergeExtensions(ext) + assert.Equal(t, "standard", tx.Ext["vat-cat"].String()) + }) + t.Run("nil extensions", func(t *testing.T) { + tx := &bill.Tax{} + tx = tx.MergeExtensions(nil) + assert.Nil(t, tx.Ext) + }) + t.Run("with extensions", func(t *testing.T) { + tx := &bill.Tax{ + Ext: tax.Extensions{ + "vat-cat": "standard", + }, + } + tx = tx.MergeExtensions(tax.Extensions{ + "vat-cat": "reduced", + }) + assert.Equal(t, "reduced", tx.Ext["vat-cat"].String()) + }) + t.Run("new extensions", func(t *testing.T) { + tx := &bill.Tax{ + Ext: tax.Extensions{ + "vat-test": "bar", + }, + } + tx = tx.MergeExtensions(tax.Extensions{ + "vat-cat": "reduced", + }) + assert.Equal(t, "reduced", tx.Ext["vat-cat"].String()) + assert.Equal(t, "bar", tx.Ext["vat-test"].String()) + }) +} diff --git a/cbc/key.go b/cbc/key.go index ef571bd4..6193929d 100644 --- a/cbc/key.go +++ b/cbc/key.go @@ -75,6 +75,20 @@ func (k Key) Has(ke Key) bool { return false } +// Pop removes the last key from a list and returns the remaining base, +// or an empty key if there is nothing left. +// +// Example: +// +// Key("a+b+c").Pop() => Key("a+b") +func (k Key) Pop() Key { + ks := strings.Split(k.String(), KeySeparator) + if len(ks) == 0 { + return KeyEmpty + } + return Key(strings.Join(ks[:len(ks)-1], KeySeparator)) +} + // HasPrefix checks to see if the key starts with the provided key. // As per `Has`, only the complete key between `+` symbols are // matched. diff --git a/cbc/key_test.go b/cbc/key_test.go index 928a2a09..a3dd3b4d 100644 --- a/cbc/key_test.go +++ b/cbc/key_test.go @@ -66,6 +66,13 @@ func TestKeyIn(t *testing.T) { assert.False(t, c.In("pro", "reduced")) } +func TestKeyPop(t *testing.T) { + k := cbc.Key("a+b+c") + assert.Equal(t, cbc.Key("a+b"), k.Pop()) + assert.Equal(t, cbc.Key("a"), k.Pop().Pop()) + assert.Equal(t, cbc.KeyEmpty, k.Pop().Pop().Pop()) +} + func TestAppendUniqueKeys(t *testing.T) { keys := []cbc.Key{"a", "b", "c"} keys = cbc.AppendUniqueKeys(keys, "b", "d") diff --git a/examples/es/invoice-es-es-verifactu.yaml b/examples/es/invoice-es-es-verifactu.yaml new file mode 100644 index 00000000..6a383445 --- /dev/null +++ b/examples/es/invoice-es-es-verifactu.yaml @@ -0,0 +1,53 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +$addons: ["es-verifactu-v1"] +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" +currency: "EUR" +issue_date: "2024-12-04" +series: "SAMPLE" +code: "004" + +supplier: + tax_id: + country: "ES" + code: "B98602642" # random + name: "Provide One S.L." + emails: + - addr: "billing@example.com" + addresses: + - num: "42" + street: "Calle Pradillo" + locality: "Madrid" + region: "Madrid" + code: "28002" + country: "ES" + +customer: + tax_id: + country: "ES" + code: "54387763P" + name: "Sample Consumer" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + discounts: + - percent: "10%" + reason: "Special discount" + - amount: "0.00" + taxes: + - cat: VAT + rate: standard + - quantity: 1 + item: + name: "Financial service" + price: "10.00" + taxes: + - cat: VAT + rate: zero + +notes: + - key: "general" + text: "Random invoice" diff --git a/examples/es/out/invoice-es-es-verifactu.json b/examples/es/out/invoice-es-es-verifactu.json new file mode 100644 index 00000000..266974d1 --- /dev/null +++ b/examples/es/out/invoice-es-es-verifactu.json @@ -0,0 +1,149 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "3c92526c4aadfca341185a20891dd65f24a1d8e69df6aa3bb2f54f7dfbcfe758" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "ES", + "$addons": [ + "es-verifactu-v1" + ], + "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", + "type": "standard", + "series": "SAMPLE", + "code": "004", + "issue_date": "2024-12-04", + "currency": "EUR", + "tax": { + "ext": { + "es-verifactu-doc-type": "F1" + } + }, + "supplier": { + "name": "Provide One S.L.", + "tax_id": { + "country": "ES", + "code": "B98602642" + }, + "addresses": [ + { + "num": "42", + "street": "Calle Pradillo", + "locality": "Madrid", + "region": "Madrid", + "code": "28002", + "country": "ES" + } + ], + "emails": [ + { + "addr": "billing@example.com" + } + ] + }, + "customer": { + "name": "Sample Consumer", + "tax_id": { + "country": "ES", + "code": "54387763P" + } + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "price": "90.00", + "unit": "h" + }, + "sum": "1800.00", + "discounts": [ + { + "reason": "Special discount", + "percent": "10%", + "amount": "180.00" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "21.0%", + "ext": { + "es-verifactu-op-class": "S1" + } + } + ], + "total": "1620.00" + }, + { + "i": 2, + "quantity": "1", + "item": { + "name": "Financial service", + "price": "10.00" + }, + "sum": "10.00", + "taxes": [ + { + "cat": "VAT", + "rate": "zero", + "percent": "0.0%", + "ext": { + "es-verifactu-op-class": "S1" + } + } + ], + "total": "10.00" + } + ], + "totals": { + "sum": "1630.00", + "total": "1630.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "ext": { + "es-verifactu-op-class": "S1" + }, + "base": "1620.00", + "percent": "21.0%", + "amount": "340.20" + }, + { + "key": "zero", + "ext": { + "es-verifactu-op-class": "S1" + }, + "base": "10.00", + "percent": "0.0%", + "amount": "0.00" + } + ], + "amount": "340.20" + } + ], + "sum": "340.20" + }, + "tax": "340.20", + "total_with_tax": "1970.20", + "payable": "1970.20" + }, + "notes": [ + { + "key": "general", + "text": "Random invoice" + } + ] + } +} \ No newline at end of file diff --git a/tax/constants.go b/tax/constants.go index 88c8984d..6ffccff8 100644 --- a/tax/constants.go +++ b/tax/constants.go @@ -31,5 +31,7 @@ const ( TagPartial cbc.Key = "partial" TagB2G cbc.Key = "b2g" TagExport cbc.Key = "export" - TagEEA cbc.Key = "eea" // European Economic Area, used with exports + + // European Economic Area, used with exports + TagEEA cbc.Key = "eea" ) diff --git a/tax/corrections.go b/tax/corrections.go index a1f08b0e..87fe2da0 100644 --- a/tax/corrections.go +++ b/tax/corrections.go @@ -50,13 +50,14 @@ func (cd *CorrectionDefinition) Merge(other *CorrectionDefinition) *CorrectionDe if cd.Schema != other.Schema { return cd } - return &CorrectionDefinition{ + cd = &CorrectionDefinition{ Schema: cd.Schema, Types: append(cd.Types, other.Types...), Extensions: append(cd.Extensions, other.Extensions...), ReasonRequired: cd.ReasonRequired || other.ReasonRequired, Stamps: append(cd.Stamps, other.Stamps...), } + return cd } // HasType returns true if the correction definition has a type that matches the one provided. diff --git a/tax/extensions.go b/tax/extensions.go index 3a2b4350..aba56bee 100644 --- a/tax/extensions.go +++ b/tax/extensions.go @@ -110,6 +110,24 @@ func (em Extensions) Validate() error { return nil } +// Get returns the value for the provided key or an empty string if not found +// or the extensions map is nil. If the key is composed of sub-keys and +// no precise match is found, the key will be split until one of the sub +// components is found. +func (em Extensions) Get(k cbc.Key) ExtValue { + if len(em) == 0 { + return "" + } + // while k is not empty, pop the last key and check if it exists + for k != cbc.KeyEmpty { + if v, ok := em[k]; ok { + return v + } + k = k.Pop() + } + return "" +} + // Has returns true if the code map has values for all the provided keys. func (em Extensions) Has(keys ...cbc.Key) bool { for _, k := range keys { @@ -216,9 +234,17 @@ func ExtensionsRequires(keys ...cbc.Key) validation.Rule { } } +func ExtensionsExclude(keys ...cbc.Key) validation.Rule { + return validateExtCodeMap{ + exclude: true, + keys: keys, + } +} + type validateExtCodeMap struct { keys []cbc.Key required bool + exclude bool } func (v validateExtCodeMap) Validate(value interface{}) error { @@ -234,6 +260,12 @@ func (v validateExtCodeMap) Validate(value interface{}) error { err[k.String()] = errors.New("required") } } + } else if v.exclude { + for _, k := range v.keys { + if _, ok := em[k]; ok { + err[k.String()] = errors.New("must be blank") + } + } } else { for k := range em { if !k.In(v.keys...) { @@ -304,6 +336,16 @@ func (ev ExtValue) String() string { return string(ev) } +// In returns true if the value is in the provided list. +func (ev ExtValue) In(values ...ExtValue) bool { + for _, v := range values { + if ev == v { + return true + } + } + return false +} + // Key returns the key value or empty if the value is a Code. func (ev ExtValue) Key() cbc.Key { k := cbc.Key(ev) diff --git a/tax/extensions_test.go b/tax/extensions_test.go index a7a718c9..f0bd1ab6 100644 --- a/tax/extensions_test.go +++ b/tax/extensions_test.go @@ -229,6 +229,41 @@ func TestExtensionsRequiresValidation(t *testing.T) { }) } +func TestExtensionsExcludeValidation(t *testing.T) { + t.Run("nil", func(t *testing.T) { + err := validation.Validate(nil, + tax.ExtensionsExclude(untdid.ExtKeyDocumentType), + ) + assert.NoError(t, err) + }) + t.Run("empty", func(t *testing.T) { + em := tax.Extensions{} + err := validation.Validate(em, + tax.ExtensionsExclude(untdid.ExtKeyDocumentType), + ) + assert.NoError(t, err) + }) + t.Run("correct", func(t *testing.T) { + em := tax.Extensions{ + untdid.ExtKeyDocumentType: "326", + } + err := validation.Validate(em, + tax.ExtensionsExclude(untdid.ExtKeyDocumentType), + ) + assert.ErrorContains(t, err, "untdid-document-type: must be blank") + }) + t.Run("correct with extras", func(t *testing.T) { + em := tax.Extensions{ + untdid.ExtKeyDocumentType: "326", + iso.ExtKeySchemeID: "1234", + } + err := validation.Validate(em, + tax.ExtensionsExclude(untdid.ExtKeyCharge), + ) + assert.NoError(t, err) + }) +} + func TestExtensionsHasValues(t *testing.T) { t.Run("nil", func(t *testing.T) { err := validation.Validate(nil, @@ -448,3 +483,34 @@ func TestExtensionLookup(t *testing.T) { assert.Equal(t, cbc.Key("key2"), em.Lookup("bar")) assert.Equal(t, cbc.KeyEmpty, em.Lookup("missing")) } + +func TestExtensionGet(t *testing.T) { + t.Run("empty", func(t *testing.T) { + var em tax.Extensions + assert.Equal(t, "", em.Get("key").String()) + }) + t.Run("with value", func(t *testing.T) { + em := tax.Extensions{ + "key": "value", + } + assert.Equal(t, "value", em.Get("key").String()) + }) + t.Run("missing", func(t *testing.T) { + em := tax.Extensions{ + "key": "value", + } + assert.Equal(t, "", em.Get("missing").String()) + }) + t.Run("with sub-keys", func(t *testing.T) { + em := tax.Extensions{ + "key": "value", + } + assert.Equal(t, "value", em.Get("key+foo").String()) + }) +} + +func TestExtValueIn(t *testing.T) { + ev := tax.ExtValue("IT") + assert.True(t, ev.In("IT", "ES")) + assert.False(t, ev.In("ES", "FR")) +} From 58bd731059772dc575242867346f85a350399e94 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 4 Dec 2024 17:32:18 +0000 Subject: [PATCH 18/25] Fixing linting issues --- addons/es/verifactu/bill_test.go | 2 +- tax/extensions.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/es/verifactu/bill_test.go b/addons/es/verifactu/bill_test.go index a6c3ec57..c5035d01 100644 --- a/addons/es/verifactu/bill_test.go +++ b/addons/es/verifactu/bill_test.go @@ -64,7 +64,7 @@ func TestInvoiceValidation(t *testing.T) { inv.SetTags(tax.TagSimplified) require.NoError(t, inv.Calculate()) - inv.Correct(bill.Corrective, bill.WithExtension(verifactu.ExtKeyDocType, "F3")) + 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]) diff --git a/tax/extensions.go b/tax/extensions.go index aba56bee..cd498009 100644 --- a/tax/extensions.go +++ b/tax/extensions.go @@ -234,6 +234,8 @@ func ExtensionsRequires(keys ...cbc.Key) validation.Rule { } } +// ExtensionsExclude returns a validation rule that ensures that +// an extensions map does **not** include the provided keys. func ExtensionsExclude(keys ...cbc.Key) validation.Rule { return validateExtCodeMap{ exclude: true, From 59350caae8d5c7241fb12a0fb00d394b9af03157 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 4 Dec 2024 17:42:43 +0000 Subject: [PATCH 19/25] Improving cbc.Key test coverage --- cbc/key.go | 6 ------ cbc/key_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/cbc/key.go b/cbc/key.go index 6193929d..a20f1543 100644 --- a/cbc/key.go +++ b/cbc/key.go @@ -83,9 +83,6 @@ func (k Key) Has(ke Key) bool { // Key("a+b+c").Pop() => Key("a+b") func (k Key) Pop() Key { ks := strings.Split(k.String(), KeySeparator) - if len(ks) == 0 { - return KeyEmpty - } return Key(strings.Join(ks[:len(ks)-1], KeySeparator)) } @@ -94,9 +91,6 @@ func (k Key) Pop() Key { // matched. func (k Key) HasPrefix(ke Key) bool { ks := strings.SplitN(k.String(), KeySeparator, 2) - if len(ks) == 0 { - return false - } return ks[0] == ke.String() } diff --git a/cbc/key_test.go b/cbc/key_test.go index a3dd3b4d..0d054e61 100644 --- a/cbc/key_test.go +++ b/cbc/key_test.go @@ -1,10 +1,13 @@ package cbc_test import ( + "encoding/json" "testing" "github.com/invopop/gobl/cbc" + "github.com/invopop/validation" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestKey(t *testing.T) { @@ -50,6 +53,11 @@ func TestKeyHas(t *testing.T) { assert.True(t, k.Has("pro")) } +func TestKeyStrings(t *testing.T) { + keys := []cbc.Key{"a", "b", "c"} + assert.Equal(t, []string{"a", "b", "c"}, cbc.KeyStrings(keys)) +} + func TestKeyHasPrefix(t *testing.T) { k := cbc.Key("standard") assert.True(t, k.HasPrefix("standard")) @@ -57,6 +65,13 @@ func TestKeyHasPrefix(t *testing.T) { k = k.With("pro") assert.True(t, k.HasPrefix("standard")) assert.False(t, k.HasPrefix("pro")) + k = cbc.KeyEmpty + assert.False(t, k.HasPrefix("foo")) +} + +func TestKeyIsEmpty(t *testing.T) { + assert.True(t, cbc.KeyEmpty.IsEmpty()) + assert.False(t, cbc.Key("foo").IsEmpty()) } func TestKeyIn(t *testing.T) { @@ -78,3 +93,25 @@ func TestAppendUniqueKeys(t *testing.T) { keys = cbc.AppendUniqueKeys(keys, "b", "d") assert.Equal(t, []cbc.Key{"a", "b", "c", "d"}, keys) } + +func TestHasValidKeyIn(t *testing.T) { + k := cbc.Key("standard") + err := validation.Validate(k, cbc.HasValidKeyIn("pro", "reduced+eqs")) + assert.ErrorContains(t, err, "must be or start with a valid ke") + + err = validation.Validate(k, cbc.HasValidKeyIn("pro", "reduced+eqs", "standard")) + assert.NoError(t, err) + + k = cbc.KeyEmpty + err = validation.Validate(k, cbc.HasValidKeyIn("pro", "reduced+eqs", "standard")) + assert.NoError(t, err) +} + +func TestKeyJSONSchema(t *testing.T) { + data := []byte(`{"description":"Text identifier to be used instead of a code for a more verbose but readable identifier.", "maxLength":64, "minLength":1, "pattern":"^(?:[a-z]|[a-z0-9][a-z0-9-+]*[a-z0-9])$", "title":"Key", "type":"string"}`) + k := cbc.Key("standard") + schema := k.JSONSchema() + out, err := json.Marshal(schema) + require.NoError(t, err) + assert.JSONEq(t, string(data), string(out)) +} From 5cd2148fe7b719985ea23f81985a205b77162504 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 5 Dec 2024 08:41:34 +0000 Subject: [PATCH 20/25] Updating CHANGELOG --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17398118..03174fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### 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 From 5b5a7b7eca3b11b48c266f591e51973375ef759c Mon Sep 17 00:00:00 2001 From: apardods Date: Thu, 5 Dec 2024 10:13:15 +0000 Subject: [PATCH 21/25] Add Tax Regime Extension --- addons/es/verifactu/extensions.go | 134 +++++++++++++++++++ addons/es/verifactu/tax.go | 10 ++ addons/es/verifactu/tax_test.go | 14 ++ examples/es/out/invoice-es-es-verifactu.json | 14 +- 4 files changed, 167 insertions(+), 5 deletions(-) diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index d57ac7c8..0e83d669 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -12,6 +12,7 @@ const ( ExtKeyOpClass cbc.Key = "es-verifactu-op-class" ExtKeyCorrectionType cbc.Key = "es-verifactu-correction-type" ExtKeyExempt cbc.Key = "es-verifactu-exempt" + ExtKeyTaxRegime cbc.Key = "es-verifactu-tax-regime" ) var extensions = []*cbc.KeyDefinition{ @@ -220,4 +221,137 @@ var extensions = []*cbc.KeyDefinition{ }, }, }, + { + Key: ExtKeyTaxRegime, + Name: i18n.String{ + i18n.EN: "Tax Regime Code - L8A/B", + i18n.ES: "Código de Régimen de IVA/IGIC - L8A/B", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + VAT regime code used to identify the type of VAT regime applied to the operation. This list combines lists 8A, which include values for IVA and 8B, which include values for IGIC. + `), + }, + Values: []*cbc.ValueDefinition{ + { + Value: "01", + Name: i18n.String{ + i18n.EN: "General regime operation", + i18n.ES: "Operación de régimen general", + }, + }, + { + Value: "02", + Name: i18n.String{ + i18n.EN: "Export", + i18n.ES: "Exportación", + }, + }, + { + Value: "03", + Name: i18n.String{ + i18n.EN: "Special regime for used goods, art objects, antiques and collectibles", + i18n.ES: "Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección", + }, + }, + { + Value: "04", + Name: i18n.String{ + i18n.EN: "Special regime for investment gold", + i18n.ES: "Régimen especial del oro de inversión", + }, + }, + { + Value: "05", + Name: i18n.String{ + i18n.EN: "Special regime for travel agencies", + i18n.ES: "Régimen especial de las agencias de viajes", + }, + }, + { + Value: "06", + Name: i18n.String{ + i18n.EN: "Special regime for IVA/IGIC groups (Advanced Level)", + i18n.ES: "Régimen especial grupo de entidades en IVA/IGIC (Nivel Avanzado)", + }, + }, + { + Value: "07", + Name: i18n.String{ + i18n.EN: "Special cash accounting regime", + i18n.ES: "Régimen especial del criterio de caja", + }, + }, + { + Value: "08", + Name: i18n.String{ + i18n.EN: "Operations subject to a different regime", + i18n.ES: "Operaciones sujetas a un régimen diferente", + }, + }, + { + Value: "09", + Name: i18n.String{ + i18n.EN: "Billing of travel agency services acting as mediators in name and on behalf of others", + i18n.ES: "Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena", + }, + }, + { + Value: "10", + Name: i18n.String{ + i18n.EN: "Collection of professional fees or rights on behalf of third parties", + i18n.ES: "Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial", + }, + }, + { + Value: "11", + Name: i18n.String{ + i18n.EN: "Business premises rental operations", + i18n.ES: "Operaciones de arrendamiento de local de negocio", + }, + }, + { + Value: "14", + Name: i18n.String{ + i18n.EN: "Invoice with pending VAT accrual in work certifications for Public Administration", + i18n.ES: "Factura con IVA pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública", + }, + }, + { + Value: "15", + Name: i18n.String{ + i18n.EN: "Invoice with pending VAT accrual in successive tract operations", + i18n.ES: "Factura con IVA pendiente de devengo en operaciones de tracto sucesivo", + }, + }, + { + Value: "17", + Name: i18n.String{ + i18n.EN: "Operation under OSS and IOSS regimes", + i18n.ES: "Operación acogida a alguno de los regímenes previstos en el capítulo XI del título IX (OSS e IOSS)", + }, + }, + { + Value: "18", + Name: i18n.String{ + i18n.EN: "Equivalence surcharge (IVA) / Special regime for small traders or retailers (IGIC)", + i18n.ES: "Recargo de equivalencia (IVA) / Régimen especial del pequeño comerciante o minorista (IGIC)", + }, + }, + { + Value: "19", + Name: i18n.String{ + i18n.EN: "Operations included in the Special Regime for Agriculture, Livestock and Fisheries", + i18n.ES: "Operaciones de actividades incluidas en el Régimen Especial de Agricultura, Ganadería y Pesca (REAGYP)", + }, + }, + { + Value: "20", + Name: i18n.String{ + i18n.EN: "Simplified regime (IVA)", + i18n.ES: "Régimen simplificado (IVA)", + }, + }, + }, + }, } diff --git a/addons/es/verifactu/tax.go b/addons/es/verifactu/tax.go index f45476db..c04f3d7f 100644 --- a/addons/es/verifactu/tax.go +++ b/addons/es/verifactu/tax.go @@ -32,6 +32,12 @@ func normalizeTaxCombo(tc *tax.Combo) { tc.Ext = tc.Ext.Merge( tax.Extensions{ExtKeyOpClass: v}, ) + // Set default tax regime to "01" (General regime operation) if not specified + if !tc.Ext.Has(ExtKeyTaxRegime) { + tc.Ext = tc.Ext.Merge( + tax.Extensions{ExtKeyTaxRegime: "01"}, + ) + } } } @@ -49,6 +55,10 @@ func validateTaxCombo(tc *tax.Combo) error { tc.Percent == nil && !tc.Ext.Has(ExtKeyOpClass), tax.ExtensionsRequires(ExtKeyExempt), ), + validation.When( + tc.Category.In(tax.CategoryVAT, es.TaxCategoryIGIC), + tax.ExtensionsRequires(ExtKeyTaxRegime), + ), validation.Skip, ), ) diff --git a/addons/es/verifactu/tax_test.go b/addons/es/verifactu/tax_test.go index 51a27be9..a77df394 100644 --- a/addons/es/verifactu/tax_test.go +++ b/addons/es/verifactu/tax_test.go @@ -16,6 +16,7 @@ func TestNormalizeTaxCombo(t *testing.T) { } normalizeTaxCombo(tc) assert.Equal(t, "S1", tc.Ext.Get(ExtKeyOpClass).String()) + assert.Equal(t, "01", tc.Ext.Get(ExtKeyTaxRegime).String()) }) t.Run("valid with country", func(t *testing.T) { @@ -26,6 +27,7 @@ func TestNormalizeTaxCombo(t *testing.T) { } normalizeTaxCombo(tc) assert.Equal(t, "S1", tc.Ext.Get(ExtKeyOpClass).String()) + assert.Equal(t, "01", tc.Ext.Get(ExtKeyTaxRegime).String()) }) t.Run("undefined rate", func(t *testing.T) { @@ -46,4 +48,16 @@ func TestNormalizeTaxCombo(t *testing.T) { normalizeTaxCombo(tc) assert.Empty(t, tc.Ext.Get(ExtKeyOpClass).String()) }) + + t.Run("with tax regime", func(t *testing.T) { + tc := &tax.Combo{ + Category: tax.CategoryVAT, + Rate: tax.RateStandard, + Ext: tax.Extensions{ + ExtKeyTaxRegime: "03", + }, + } + normalizeTaxCombo(tc) + assert.Equal(t, "03", tc.Ext.Get(ExtKeyTaxRegime).String()) + }) } diff --git a/examples/es/out/invoice-es-es-verifactu.json b/examples/es/out/invoice-es-es-verifactu.json index 266974d1..ae3c7961 100644 --- a/examples/es/out/invoice-es-es-verifactu.json +++ b/examples/es/out/invoice-es-es-verifactu.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "3c92526c4aadfca341185a20891dd65f24a1d8e69df6aa3bb2f54f7dfbcfe758" + "val": "07dcb67d687ac2faa064bdd3af359159f01b3b19c136157f9aa01518ad5c4757" } }, "doc": { @@ -76,7 +76,8 @@ "rate": "standard", "percent": "21.0%", "ext": { - "es-verifactu-op-class": "S1" + "es-verifactu-op-class": "S1", + "es-verifactu-tax-regime": "01" } } ], @@ -96,7 +97,8 @@ "rate": "zero", "percent": "0.0%", "ext": { - "es-verifactu-op-class": "S1" + "es-verifactu-op-class": "S1", + "es-verifactu-tax-regime": "01" } } ], @@ -114,7 +116,8 @@ { "key": "standard", "ext": { - "es-verifactu-op-class": "S1" + "es-verifactu-op-class": "S1", + "es-verifactu-tax-regime": "01" }, "base": "1620.00", "percent": "21.0%", @@ -123,7 +126,8 @@ { "key": "zero", "ext": { - "es-verifactu-op-class": "S1" + "es-verifactu-op-class": "S1", + "es-verifactu-tax-regime": "01" }, "base": "10.00", "percent": "0.0%", From d80ef4148a1de053cdcd9b6cc280f4be80cfb141 Mon Sep 17 00:00:00 2001 From: apardods Date: Thu, 5 Dec 2024 10:18:50 +0000 Subject: [PATCH 22/25] Remove redundant check --- addons/es/verifactu/tax.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/addons/es/verifactu/tax.go b/addons/es/verifactu/tax.go index c04f3d7f..b4efc369 100644 --- a/addons/es/verifactu/tax.go +++ b/addons/es/verifactu/tax.go @@ -55,10 +55,7 @@ func validateTaxCombo(tc *tax.Combo) error { tc.Percent == nil && !tc.Ext.Has(ExtKeyOpClass), tax.ExtensionsRequires(ExtKeyExempt), ), - validation.When( - tc.Category.In(tax.CategoryVAT, es.TaxCategoryIGIC), - tax.ExtensionsRequires(ExtKeyTaxRegime), - ), + tax.ExtensionsRequires(ExtKeyTaxRegime), validation.Skip, ), ) From 92b3d2aef4dd3aa8f9274bd734b558dd905adb26 Mon Sep 17 00:00:00 2001 From: apardods Date: Thu, 5 Dec 2024 10:33:11 +0000 Subject: [PATCH 23/25] Name Change --- addons/es/verifactu/extensions.go | 8 ++++---- addons/es/verifactu/tax.go | 6 +++--- addons/es/verifactu/tax_test.go | 8 ++++---- examples/es/out/invoice-es-es-verifactu.json | 18 +++++++++--------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index 0e83d669..010736c3 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -12,7 +12,7 @@ const ( ExtKeyOpClass cbc.Key = "es-verifactu-op-class" ExtKeyCorrectionType cbc.Key = "es-verifactu-correction-type" ExtKeyExempt cbc.Key = "es-verifactu-exempt" - ExtKeyTaxRegime cbc.Key = "es-verifactu-tax-regime" + ExtKeyIVAIGICRegime cbc.Key = "es-verifactu-iva-igic-regime" ) var extensions = []*cbc.KeyDefinition{ @@ -222,14 +222,14 @@ var extensions = []*cbc.KeyDefinition{ }, }, { - Key: ExtKeyTaxRegime, + Key: ExtKeyIVAIGICRegime, Name: i18n.String{ - i18n.EN: "Tax Regime Code - L8A/B", + i18n.EN: "IVA/IGIC Regime Code - L8A/B", i18n.ES: "Código de Régimen de IVA/IGIC - L8A/B", }, Desc: i18n.String{ i18n.EN: here.Doc(` - VAT regime code used to identify the type of VAT regime applied to the operation. This list combines lists 8A, which include values for IVA and 8B, which include values for IGIC. + Code list used to identify the type of VAT or IGIC regime applied to the operation. This list combines lists 8A, which include values for IVA and 8B, which include values for IGIC. `), }, Values: []*cbc.ValueDefinition{ diff --git a/addons/es/verifactu/tax.go b/addons/es/verifactu/tax.go index b4efc369..c0b750f9 100644 --- a/addons/es/verifactu/tax.go +++ b/addons/es/verifactu/tax.go @@ -33,9 +33,9 @@ func normalizeTaxCombo(tc *tax.Combo) { tax.Extensions{ExtKeyOpClass: v}, ) // Set default tax regime to "01" (General regime operation) if not specified - if !tc.Ext.Has(ExtKeyTaxRegime) { + if !tc.Ext.Has(ExtKeyIVAIGICRegime) { tc.Ext = tc.Ext.Merge( - tax.Extensions{ExtKeyTaxRegime: "01"}, + tax.Extensions{ExtKeyIVAIGICRegime: "01"}, ) } } @@ -55,7 +55,7 @@ func validateTaxCombo(tc *tax.Combo) error { tc.Percent == nil && !tc.Ext.Has(ExtKeyOpClass), tax.ExtensionsRequires(ExtKeyExempt), ), - tax.ExtensionsRequires(ExtKeyTaxRegime), + tax.ExtensionsRequires(ExtKeyIVAIGICRegime), validation.Skip, ), ) diff --git a/addons/es/verifactu/tax_test.go b/addons/es/verifactu/tax_test.go index a77df394..dd8c48ea 100644 --- a/addons/es/verifactu/tax_test.go +++ b/addons/es/verifactu/tax_test.go @@ -16,7 +16,7 @@ func TestNormalizeTaxCombo(t *testing.T) { } normalizeTaxCombo(tc) assert.Equal(t, "S1", tc.Ext.Get(ExtKeyOpClass).String()) - assert.Equal(t, "01", tc.Ext.Get(ExtKeyTaxRegime).String()) + assert.Equal(t, "01", tc.Ext.Get(ExtKeyIVAIGICRegime).String()) }) t.Run("valid with country", func(t *testing.T) { @@ -27,7 +27,7 @@ func TestNormalizeTaxCombo(t *testing.T) { } normalizeTaxCombo(tc) assert.Equal(t, "S1", tc.Ext.Get(ExtKeyOpClass).String()) - assert.Equal(t, "01", tc.Ext.Get(ExtKeyTaxRegime).String()) + assert.Equal(t, "01", tc.Ext.Get(ExtKeyIVAIGICRegime).String()) }) t.Run("undefined rate", func(t *testing.T) { @@ -54,10 +54,10 @@ func TestNormalizeTaxCombo(t *testing.T) { Category: tax.CategoryVAT, Rate: tax.RateStandard, Ext: tax.Extensions{ - ExtKeyTaxRegime: "03", + ExtKeyIVAIGICRegime: "03", }, } normalizeTaxCombo(tc) - assert.Equal(t, "03", tc.Ext.Get(ExtKeyTaxRegime).String()) + assert.Equal(t, "03", tc.Ext.Get(ExtKeyIVAIGICRegime).String()) }) } diff --git a/examples/es/out/invoice-es-es-verifactu.json b/examples/es/out/invoice-es-es-verifactu.json index ae3c7961..e4065a9b 100644 --- a/examples/es/out/invoice-es-es-verifactu.json +++ b/examples/es/out/invoice-es-es-verifactu.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "07dcb67d687ac2faa064bdd3af359159f01b3b19c136157f9aa01518ad5c4757" + "val": "63c9a59c1ba0c5ecc7447760b081ce154779a0bc6bc1bdb71079d15e2356ee51" } }, "doc": { @@ -76,8 +76,8 @@ "rate": "standard", "percent": "21.0%", "ext": { - "es-verifactu-op-class": "S1", - "es-verifactu-tax-regime": "01" + "es-verifactu-iva-igic-regime": "01", + "es-verifactu-op-class": "S1" } } ], @@ -97,8 +97,8 @@ "rate": "zero", "percent": "0.0%", "ext": { - "es-verifactu-op-class": "S1", - "es-verifactu-tax-regime": "01" + "es-verifactu-iva-igic-regime": "01", + "es-verifactu-op-class": "S1" } } ], @@ -116,8 +116,8 @@ { "key": "standard", "ext": { - "es-verifactu-op-class": "S1", - "es-verifactu-tax-regime": "01" + "es-verifactu-iva-igic-regime": "01", + "es-verifactu-op-class": "S1" }, "base": "1620.00", "percent": "21.0%", @@ -126,8 +126,8 @@ { "key": "zero", "ext": { - "es-verifactu-op-class": "S1", - "es-verifactu-tax-regime": "01" + "es-verifactu-iva-igic-regime": "01", + "es-verifactu-op-class": "S1" }, "base": "10.00", "percent": "0.0%", From 5019e8604cc1f4047713131785ddc312a7fe1262 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 5 Dec 2024 10:49:41 +0000 Subject: [PATCH 24/25] Renaming Verifactu regime --- addons/es/verifactu/extensions.go | 28 +-- addons/es/verifactu/tax.go | 28 +-- addons/es/verifactu/tax_test.go | 33 ++- data/addons/es-verifactu-v1.json | 219 ++++++++++++++----- data/schemas/bill/correction-options.json | 2 +- data/schemas/bill/invoice.json | 2 +- examples/es/out/invoice-es-es-verifactu.json | 18 +- 7 files changed, 238 insertions(+), 92 deletions(-) diff --git a/addons/es/verifactu/extensions.go b/addons/es/verifactu/extensions.go index 010736c3..5b0c114a 100644 --- a/addons/es/verifactu/extensions.go +++ b/addons/es/verifactu/extensions.go @@ -12,7 +12,7 @@ const ( ExtKeyOpClass cbc.Key = "es-verifactu-op-class" ExtKeyCorrectionType cbc.Key = "es-verifactu-correction-type" ExtKeyExempt cbc.Key = "es-verifactu-exempt" - ExtKeyIVAIGICRegime cbc.Key = "es-verifactu-iva-igic-regime" + ExtKeyRegime cbc.Key = "es-verifactu-regime" ) var extensions = []*cbc.KeyDefinition{ @@ -222,14 +222,14 @@ var extensions = []*cbc.KeyDefinition{ }, }, { - Key: ExtKeyIVAIGICRegime, + Key: ExtKeyRegime, Name: i18n.String{ - i18n.EN: "IVA/IGIC Regime Code - L8A/B", + i18n.EN: "VAT/IGIC Regime Code - L8A/B", i18n.ES: "Código de Régimen de IVA/IGIC - L8A/B", }, Desc: i18n.String{ i18n.EN: here.Doc(` - Code list used to identify the type of VAT or IGIC regime applied to the operation. This list combines lists 8A, which include values for IVA and 8B, which include values for IGIC. + Identify the type of VAT or IGIC regime applied to the operation. This list combines lists L8A which include values for VAT, and L8B for IGIC. `), }, Values: []*cbc.ValueDefinition{ @@ -271,7 +271,7 @@ var extensions = []*cbc.KeyDefinition{ { Value: "06", Name: i18n.String{ - i18n.EN: "Special regime for IVA/IGIC groups (Advanced Level)", + i18n.EN: "Special regime for VAT/IGIC groups (Advanced Level)", i18n.ES: "Régimen especial grupo de entidades en IVA/IGIC (Nivel Avanzado)", }, }, @@ -313,28 +313,28 @@ var extensions = []*cbc.KeyDefinition{ { Value: "14", Name: i18n.String{ - i18n.EN: "Invoice with pending VAT accrual in work certifications for Public Administration", - i18n.ES: "Factura con IVA pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública", + i18n.EN: "Invoice with pending VAT/IGIC accrual in work certifications for Public Administration", + i18n.ES: "Factura con IVA/IGIC pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública", }, }, { Value: "15", Name: i18n.String{ - i18n.EN: "Invoice with pending VAT accrual in successive tract operations", - i18n.ES: "Factura con IVA pendiente de devengo en operaciones de tracto sucesivo", + i18n.EN: "Invoice with pending VAT/IGIC accrual in successive tract operations", + i18n.ES: "Factura con IVA/IGIC pendiente de devengo en operaciones de tracto sucesivo", }, }, { Value: "17", Name: i18n.String{ - i18n.EN: "Operation under OSS and IOSS regimes", - i18n.ES: "Operación acogida a alguno de los regímenes previstos en el capítulo XI del título IX (OSS e IOSS)", + i18n.EN: "Operation under OSS and IOSS regimes (VAT) / Special regime for retail traders. (IGIC)", + i18n.ES: "Operación acogida a alguno de los regímenes previstos en el capítulo XI del título IX (OSS e IOSS, IVA) / Régimen especial de comerciante minorista. (IGIC)", }, }, { Value: "18", Name: i18n.String{ - i18n.EN: "Equivalence surcharge (IVA) / Special regime for small traders or retailers (IGIC)", + i18n.EN: "Equivalence surcharge (VAT) / Special regime for small traders or retailers (IGIC)", i18n.ES: "Recargo de equivalencia (IVA) / Régimen especial del pequeño comerciante o minorista (IGIC)", }, }, @@ -348,8 +348,8 @@ var extensions = []*cbc.KeyDefinition{ { Value: "20", Name: i18n.String{ - i18n.EN: "Simplified regime (IVA)", - i18n.ES: "Régimen simplificado (IVA)", + i18n.EN: "Simplified regime (VAT only)", + i18n.ES: "Régimen simplificado (IVA only)", }, }, }, diff --git a/addons/es/verifactu/tax.go b/addons/es/verifactu/tax.go index c0b750f9..549b0f56 100644 --- a/addons/es/verifactu/tax.go +++ b/addons/es/verifactu/tax.go @@ -22,21 +22,21 @@ func normalizeTaxCombo(tc *tax.Combo) { } switch tc.Category { case tax.CategoryVAT, es.TaxCategoryIGIC: - if tc.Rate.IsEmpty() { - return + ext := make(tax.Extensions) + + // Set default tax regime to "01" (General regime operation) if not already specified + if !tc.Ext.Has(ExtKeyRegime) { + ext[ExtKeyRegime] = "01" } - v := taxCategoryOpClassMap.Get(tc.Rate) - if v == "" { - return + + if !tc.Rate.IsEmpty() { + if v := taxCategoryOpClassMap.Get(tc.Rate); v != "" { + ext[ExtKeyOpClass] = v + } } - tc.Ext = tc.Ext.Merge( - tax.Extensions{ExtKeyOpClass: v}, - ) - // Set default tax regime to "01" (General regime operation) if not specified - if !tc.Ext.Has(ExtKeyIVAIGICRegime) { - tc.Ext = tc.Ext.Merge( - tax.Extensions{ExtKeyIVAIGICRegime: "01"}, - ) + + if len(ext) > 0 { + tc.Ext = tc.Ext.Merge(ext) } } } @@ -55,7 +55,7 @@ func validateTaxCombo(tc *tax.Combo) error { tc.Percent == nil && !tc.Ext.Has(ExtKeyOpClass), tax.ExtensionsRequires(ExtKeyExempt), ), - tax.ExtensionsRequires(ExtKeyIVAIGICRegime), + tax.ExtensionsRequires(ExtKeyRegime), validation.Skip, ), ) diff --git a/addons/es/verifactu/tax_test.go b/addons/es/verifactu/tax_test.go index dd8c48ea..703cfe10 100644 --- a/addons/es/verifactu/tax_test.go +++ b/addons/es/verifactu/tax_test.go @@ -16,7 +16,7 @@ func TestNormalizeTaxCombo(t *testing.T) { } normalizeTaxCombo(tc) assert.Equal(t, "S1", tc.Ext.Get(ExtKeyOpClass).String()) - assert.Equal(t, "01", tc.Ext.Get(ExtKeyIVAIGICRegime).String()) + assert.Equal(t, "01", tc.Ext.Get(ExtKeyRegime).String()) }) t.Run("valid with country", func(t *testing.T) { @@ -27,7 +27,7 @@ func TestNormalizeTaxCombo(t *testing.T) { } normalizeTaxCombo(tc) assert.Equal(t, "S1", tc.Ext.Get(ExtKeyOpClass).String()) - assert.Equal(t, "01", tc.Ext.Get(ExtKeyIVAIGICRegime).String()) + assert.Equal(t, "01", tc.Ext.Get(ExtKeyRegime).String()) }) t.Run("undefined rate", func(t *testing.T) { @@ -54,10 +54,35 @@ func TestNormalizeTaxCombo(t *testing.T) { Category: tax.CategoryVAT, Rate: tax.RateStandard, Ext: tax.Extensions{ - ExtKeyIVAIGICRegime: "03", + ExtKeyRegime: "03", }, } normalizeTaxCombo(tc) - assert.Equal(t, "03", tc.Ext.Get(ExtKeyIVAIGICRegime).String()) + assert.Equal(t, "03", tc.Ext.Get(ExtKeyRegime).String()) }) } + +func TestValidateTaxCombo(t *testing.T) { + t.Run("valid", func(t *testing.T) { + tc := &tax.Combo{ + Category: tax.CategoryVAT, + Rate: tax.RateStandard, + Ext: tax.Extensions{ + ExtKeyOpClass: "S1", + ExtKeyRegime: "01", + }, + } + err := validateTaxCombo(tc) + assert.NoError(t, err) + }) + + t.Run("not in category", func(t *testing.T) { + tc := &tax.Combo{ + Category: tax.CategoryGST, + Rate: tax.RateStandard, + } + err := validateTaxCombo(tc) + assert.NoError(t, err) + }) + +} diff --git a/data/addons/es-verifactu-v1.json b/data/addons/es-verifactu-v1.json index a2496964..ff004412 100644 --- a/data/addons/es-verifactu-v1.json +++ b/data/addons/es-verifactu-v1.json @@ -2,7 +2,7 @@ "$schema": "https://gobl.org/draft-0/tax/addon-def", "key": "es-verifactu-v1", "name": { - "en": "Spain Verifactu" + "en": "Spain Verifactu V1" }, "extensions": [ { @@ -32,7 +32,7 @@ { "value": "F3", "name": { - "en": "Invoice in substitution of simplified invoices.", + "en": "Invoice issued as a replacement for simplified invoices that have been billed and declared.", "es": "Factura emitida en sustitución de facturas simplificadas facturadas y declaradas." } }, @@ -74,13 +74,39 @@ ] }, { - "key": "es-verifactu-tax-classification", + "key": "es-verifactu-correction-type", "name": { - "en": "Verifactu Tax Classification/Exemption Code - L9/10", - "es": "Código de Clasificación/Exención de Impuesto de Verifactu - L9/10" + "en": "Verifactu Correction Type Code - L3", + "es": "Código de Tipo de Corrección de Verifactu - L3" }, "desc": { - "en": "Tax classification code used to identify the type of tax being applied to the line. It includes both exemption reasons and tax scenarios.\nThese lists are separate in Verifactu but combined here for convenience.\nSource: VeriFactu Ministerial Order:\n * https://www.boe.es/diario_boe/txt.php?id=BOE-A-2024-22138" + "en": "Correction type code used to identify the type of correction being made.\nThis value will be determined automatically according to the invoice type.\nCorrective invoices will be marked as \"S\", while credit and debit notes as \"I\"." + }, + "values": [ + { + "value": "S", + "name": { + "en": "Substitution", + "es": "Por Sustitución" + } + }, + { + "value": "I", + "name": { + "en": "Differences", + "es": "Por Diferencias" + } + } + ] + }, + { + "key": "es-verifactu-op-class", + "name": { + "en": "Verifactu Operation Classification/Exemption Code - L9", + "es": "Código de Clasificación/Exención de Impuesto de Verifactu - L9" + }, + "desc": { + "en": "Operation classification code used to identify if taxes should be applied to the line.\nSource: VeriFactu Ministerial Order:\n * https://www.boe.es/diario_boe/txt.php?id=BOE-A-2024-22138\nFor details on how best to use and apply these and other codes, see the\nAEAT FAQ:\n * https://sede.agenciatributaria.gob.es/Sede/impuestos-tasas/iva/iva-libros-registro-iva-traves-aeat/preguntas-frecuentes/3-libro-registro-facturas-expedidas.html?faqId=b5556c3d02bc9510VgnVCM100000dc381e0aRCRD" }, "values": [ { @@ -110,7 +136,19 @@ "en": "Not Subject - Due to location rules", "es": "Operación No Sujeta por Reglas de localización" } - }, + } + ] + }, + { + "key": "es-verifactu-exempt", + "name": { + "en": "Verifactu Exemption Code - L10", + "es": "Código de Exención de Impuesto de Verifactu - L10" + }, + "desc": { + "en": "Exemption code used to explain why the operation is exempt from taxes." + }, + "values": [ { "value": "E1", "name": { @@ -154,17 +192,134 @@ } } ] - } - ], - "tags": [ + }, { - "schema": "bill/invoice", - "list": [ + "key": "es-verifactu-regime", + "name": { + "en": "VAT/IGIC Regime Code - L8A/B", + "es": "Código de Régimen de IVA/IGIC - L8A/B" + }, + "desc": { + "en": "Identify the type of VAT or IGIC regime applied to the operation. This list combines lists L8A which include values for VAT, and L8B for IGIC." + }, + "values": [ + { + "value": "01", + "name": { + "en": "General regime operation", + "es": "Operación de régimen general" + } + }, + { + "value": "02", + "name": { + "en": "Export", + "es": "Exportación" + } + }, + { + "value": "03", + "name": { + "en": "Special regime for used goods, art objects, antiques and collectibles", + "es": "Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección" + } + }, + { + "value": "04", + "name": { + "en": "Special regime for investment gold", + "es": "Régimen especial del oro de inversión" + } + }, + { + "value": "05", + "name": { + "en": "Special regime for travel agencies", + "es": "Régimen especial de las agencias de viajes" + } + }, + { + "value": "06", + "name": { + "en": "Special regime for VAT/IGIC groups (Advanced Level)", + "es": "Régimen especial grupo de entidades en IVA/IGIC (Nivel Avanzado)" + } + }, + { + "value": "07", + "name": { + "en": "Special cash accounting regime", + "es": "Régimen especial del criterio de caja" + } + }, + { + "value": "08", + "name": { + "en": "Operations subject to a different regime", + "es": "Operaciones sujetas a un régimen diferente" + } + }, + { + "value": "09", + "name": { + "en": "Billing of travel agency services acting as mediators in name and on behalf of others", + "es": "Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena" + } + }, + { + "value": "10", + "name": { + "en": "Collection of professional fees or rights on behalf of third parties", + "es": "Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial" + } + }, + { + "value": "11", + "name": { + "en": "Business premises rental operations", + "es": "Operaciones de arrendamiento de local de negocio" + } + }, + { + "value": "14", + "name": { + "en": "Invoice with pending VAT/IGIC accrual in work certifications for Public Administration", + "es": "Factura con IVA/IGIC pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública" + } + }, + { + "value": "15", + "name": { + "en": "Invoice with pending VAT/IGIC accrual in successive tract operations", + "es": "Factura con IVA/IGIC pendiente de devengo en operaciones de tracto sucesivo" + } + }, + { + "value": "17", + "name": { + "en": "Operation under OSS and IOSS regimes (VAT) / Special regime for retail traders. (IGIC)", + "es": "Operación acogida a alguno de los regímenes previstos en el capítulo XI del título IX (OSS e IOSS, IVA) / Régimen especial de comerciante minorista. (IGIC)" + } + }, + { + "value": "18", + "name": { + "en": "Equivalence surcharge (VAT) / Special regime for small traders or retailers (IGIC)", + "es": "Recargo de equivalencia (IVA) / Régimen especial del pequeño comerciante o minorista (IGIC)" + } + }, + { + "value": "19", + "name": { + "en": "Operations included in the Special Regime for Agriculture, Livestock and Fisheries", + "es": "Operaciones de actividades incluidas en el Régimen Especial de Agricultura, Ganadería y Pesca (REAGYP)" + } + }, { - "key": "substitution", + "value": "20", "name": { - "en": "Invoice issued in substitution of simplified invoices issued and declared", - "es": "Factura emitida en sustitución de facturas simplificadas facturadas y declaradas" + "en": "Simplified regime (VAT only)", + "es": "Régimen simplificado (IVA only)" } } ] @@ -192,40 +347,6 @@ "ext": { "es-verifactu-doc-type": "F2" } - }, - { - "type": [ - "standard" - ], - "tags": [ - "substitution" - ], - "ext": { - "es-verifactu-doc-type": "F3" - } - }, - { - "type": [ - "credit-note", - "corrective", - "debit-note" - ], - "ext": { - "es-verifactu-doc-type": "R1" - } - }, - { - "type": [ - "credit-note", - "corrective", - "debit-note" - ], - "tags": [ - "simplified" - ], - "ext": { - "es-verifactu-doc-type": "R5" - } } ] } diff --git a/data/schemas/bill/correction-options.json b/data/schemas/bill/correction-options.json index d77c02a0..a19120de 100644 --- a/data/schemas/bill/correction-options.json +++ b/data/schemas/bill/correction-options.json @@ -36,7 +36,7 @@ "ext": { "$ref": "https://gobl.org/draft-0/tax/extensions", "title": "Extensions", - "description": "Extensions for region specific requirements." + "description": "Extensions for region specific requirements that may be added in the preceding\nor at the document level, according to the local rules." } }, "type": "object", diff --git a/data/schemas/bill/invoice.json b/data/schemas/bill/invoice.json index 3b711e86..236fb15f 100644 --- a/data/schemas/bill/invoice.json +++ b/data/schemas/bill/invoice.json @@ -391,7 +391,7 @@ }, { "const": "es-verifactu-v1", - "title": "Spain Verifactu" + "title": "Spain Verifactu V1" }, { "const": "eu-en16931-v2017", diff --git a/examples/es/out/invoice-es-es-verifactu.json b/examples/es/out/invoice-es-es-verifactu.json index e4065a9b..f2aa930c 100644 --- a/examples/es/out/invoice-es-es-verifactu.json +++ b/examples/es/out/invoice-es-es-verifactu.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "63c9a59c1ba0c5ecc7447760b081ce154779a0bc6bc1bdb71079d15e2356ee51" + "val": "76bad772eedf169425dc7434038175fca40fe38b6028e2683490f7623b103827" } }, "doc": { @@ -76,8 +76,8 @@ "rate": "standard", "percent": "21.0%", "ext": { - "es-verifactu-iva-igic-regime": "01", - "es-verifactu-op-class": "S1" + "es-verifactu-op-class": "S1", + "es-verifactu-regime": "01" } } ], @@ -97,8 +97,8 @@ "rate": "zero", "percent": "0.0%", "ext": { - "es-verifactu-iva-igic-regime": "01", - "es-verifactu-op-class": "S1" + "es-verifactu-op-class": "S1", + "es-verifactu-regime": "01" } } ], @@ -116,8 +116,8 @@ { "key": "standard", "ext": { - "es-verifactu-iva-igic-regime": "01", - "es-verifactu-op-class": "S1" + "es-verifactu-op-class": "S1", + "es-verifactu-regime": "01" }, "base": "1620.00", "percent": "21.0%", @@ -126,8 +126,8 @@ { "key": "zero", "ext": { - "es-verifactu-iva-igic-regime": "01", - "es-verifactu-op-class": "S1" + "es-verifactu-op-class": "S1", + "es-verifactu-regime": "01" }, "base": "10.00", "percent": "0.0%", From 633fce761013afd555e25c7691db041ddf71e550 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 5 Dec 2024 10:54:48 +0000 Subject: [PATCH 25/25] Test fix --- bill/invoice_correct_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bill/invoice_correct_test.go b/bill/invoice_correct_test.go index 53800e1c..ceeb7953 100644 --- a/bill/invoice_correct_test.go +++ b/bill/invoice_correct_test.go @@ -181,7 +181,7 @@ func TestCorrectionOptionsSchema(t *testing.T) { } // Sorry, this is copied and pasted from the test output! - exp := `{"properties":{"type":{"$ref":"https://gobl.org/draft-0/cbc/key","oneOf":[{"const":"credit-note","title":"Credit Note","description":"Reflects a refund either partial or complete of the preceding document. A \ncredit note effectively *extends* the previous document."},{"const":"corrective","title":"Corrective","description":"Corrected invoice that completely *replaces* the preceding document."},{"const":"debit-note","title":"Debit Note","description":"An additional set of charges to be added to the preceding document."}],"title":"Type","description":"The type of corrective invoice to produce.","default":"credit-note"},"issue_date":{"$ref":"https://gobl.org/draft-0/cal/date","title":"Issue Date","description":"When the new corrective invoice's issue date should be set to."},"series":{"$ref":"https://gobl.org/draft-0/cbc/code","title":"Series","description":"Series to assign to the new corrective invoice.","default":"TEST"},"stamps":{"items":{"$ref":"https://gobl.org/draft-0/head/stamp"},"type":"array","title":"Stamps","description":"Stamps of the previous document to include in the preceding data."},"reason":{"type":"string","title":"Reason","description":"Human readable reason for the corrective operation."},"ext":{"properties":{"es-facturae-correction":{"oneOf":[{"const":"01","title":"Invoice code"},{"const":"02","title":"Invoice series"},{"const":"03","title":"Issue date"},{"const":"04","title":"Name and surnames/Corporate name - Issuer (Sender)"},{"const":"05","title":"Name and surnames/Corporate name - Receiver"},{"const":"06","title":"Issuer's Tax Identification Number"},{"const":"07","title":"Receiver's Tax Identification Number"},{"const":"08","title":"Supplier's address"},{"const":"09","title":"Customer's address"},{"const":"10","title":"Item line"},{"const":"11","title":"Applicable Tax Rate"},{"const":"12","title":"Applicable Tax Amount"},{"const":"13","title":"Applicable Date/Period"},{"const":"14","title":"Invoice Class"},{"const":"15","title":"Legal literals"},{"const":"16","title":"Taxable Base"},{"const":"80","title":"Calculation of tax outputs"},{"const":"81","title":"Calculation of tax inputs"},{"const":"82","title":"Taxable Base modified due to return of packages and packaging materials"},{"const":"83","title":"Taxable Base modified due to discounts and rebates"},{"const":"84","title":"Taxable Base modified due to firm court ruling or administrative decision"},{"const":"85","title":"Taxable Base modified due to unpaid outputs where there is a judgement opening insolvency proceedings"}],"type":"string","title":"FacturaE Change","description":"FacturaE requires a specific and single code that explains why the previous invoice is being corrected."}},"type":"object","title":"Extensions","description":"Extensions for region specific requirements.","recommended":["es-facturae-correction"]}},"type":"object","required":["type"],"description":"CorrectionOptions defines a structure used to pass configuration options to correct a previous invoice.","recommended":["series","ext"]}` + exp := `{"properties":{"type":{"$ref":"https://gobl.org/draft-0/cbc/key","oneOf":[{"const":"credit-note","title":"Credit Note","description":"Reflects a refund either partial or complete of the preceding document. A \ncredit note effectively *extends* the previous document."},{"const":"corrective","title":"Corrective","description":"Corrected invoice that completely *replaces* the preceding document."},{"const":"debit-note","title":"Debit Note","description":"An additional set of charges to be added to the preceding document."}],"title":"Type","description":"The type of corrective invoice to produce.","default":"credit-note"},"issue_date":{"$ref":"https://gobl.org/draft-0/cal/date","title":"Issue Date","description":"When the new corrective invoice's issue date should be set to."},"series":{"$ref":"https://gobl.org/draft-0/cbc/code","title":"Series","description":"Series to assign to the new corrective invoice.","default":"TEST"},"stamps":{"items":{"$ref":"https://gobl.org/draft-0/head/stamp"},"type":"array","title":"Stamps","description":"Stamps of the previous document to include in the preceding data."},"reason":{"type":"string","title":"Reason","description":"Human readable reason for the corrective operation."},"ext":{"properties":{"es-facturae-correction":{"oneOf":[{"const":"01","title":"Invoice code"},{"const":"02","title":"Invoice series"},{"const":"03","title":"Issue date"},{"const":"04","title":"Name and surnames/Corporate name - Issuer (Sender)"},{"const":"05","title":"Name and surnames/Corporate name - Receiver"},{"const":"06","title":"Issuer's Tax Identification Number"},{"const":"07","title":"Receiver's Tax Identification Number"},{"const":"08","title":"Supplier's address"},{"const":"09","title":"Customer's address"},{"const":"10","title":"Item line"},{"const":"11","title":"Applicable Tax Rate"},{"const":"12","title":"Applicable Tax Amount"},{"const":"13","title":"Applicable Date/Period"},{"const":"14","title":"Invoice Class"},{"const":"15","title":"Legal literals"},{"const":"16","title":"Taxable Base"},{"const":"80","title":"Calculation of tax outputs"},{"const":"81","title":"Calculation of tax inputs"},{"const":"82","title":"Taxable Base modified due to return of packages and packaging materials"},{"const":"83","title":"Taxable Base modified due to discounts and rebates"},{"const":"84","title":"Taxable Base modified due to firm court ruling or administrative decision"},{"const":"85","title":"Taxable Base modified due to unpaid outputs where there is a judgement opening insolvency proceedings"}],"type":"string","title":"FacturaE Change","description":"FacturaE requires a specific and single code that explains why the previous invoice is being corrected."}},"type":"object","title":"Extensions","description":"Extensions for region specific requirements that may be added in the preceding\nor at the document level, according to the local rules.","recommended":["es-facturae-correction"]}},"type":"object","required":["type"],"description":"CorrectionOptions defines a structure used to pass configuration options to correct a previous invoice.","recommended":["series","ext"]}` data, err := json.Marshal(cos) require.NoError(t, err) if !assert.JSONEq(t, exp, string(data)) {