From f082f48949f0d1dde5da0f89c2883456b63b3e75 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Mon, 20 Nov 2023 21:25:49 +0000 Subject: [PATCH] Defining common invoice tags, simplified invoice validation moved up --- bill/invoice.go | 48 +++++++++++++++-------- regimes/co/co.go | 3 +- regimes/co/invoices.go | 23 ----------- regimes/common/invoices.go | 69 +++++++++++++++++++++++++++++++++ regimes/es/invoices.go | 7 +--- regimes/es/scenarios.go | 36 +---------------- regimes/fr/fr.go | 3 +- regimes/fr/scenarios.go | 12 ------ regimes/gb/gb.go | 1 + regimes/gb/invoice_validator.go | 10 ++--- regimes/it/invoice_validator.go | 14 +++++-- regimes/it/scenarios.go | 18 +-------- regimes/mx/invoice_validator.go | 1 - regimes/mx/mx.go | 1 + regimes/nl/invoice_validator.go | 15 +++---- regimes/nl/nl.go | 1 + regimes/pt/invoice_validator.go | 6 --- regimes/pt/scenarios.go | 11 +----- regimes/us/invoice_validator.go | 1 - regimes/us/us.go | 1 + 20 files changed, 136 insertions(+), 145 deletions(-) create mode 100644 regimes/common/invoices.go diff --git a/bill/invoice.go b/bill/invoice.go index 9f2595f8..fff9fdfa 100644 --- a/bill/invoice.go +++ b/bill/invoice.go @@ -114,39 +114,53 @@ func (inv *Invoice) ValidateWithContext(ctx context.Context) error { ctx = r.WithContext(ctx) err := validation.ValidateStructWithContext(ctx, inv, validation.Field(&inv.UUID), - validation.Field(&inv.Type, validation.Required, isValidInvoiceType), + validation.Field(&inv.Type, + validation.Required, + isValidInvoiceType, + ), validation.Field(&inv.Series), validation.Field(&inv.Code, - validation.When(!internal.IsDraft(ctx), validation.Required), + validation.When( + !internal.IsDraft(ctx), + validation.Required, + ), + ), + validation.Field(&inv.IssueDate, + cal.DateNotZero(), ), - validation.Field(&inv.IssueDate, cal.DateNotZero()), validation.Field(&inv.OperationDate), validation.Field(&inv.ValueDate), - validation.Field(&inv.Currency, validation.Required), + validation.Field(&inv.Currency, + validation.Required, + ), validation.Field(&inv.ExchangeRates), - validation.Field(&inv.Preceding), - validation.Field(&inv.Tax), - - validation.Field(&inv.Supplier, validation.Required), - validation.Field(&inv.Customer), - - validation.Field(&inv.Lines, validation.Required), + validation.Field(&inv.Supplier, + validation.Required, + ), + validation.Field(&inv.Customer, + // Customer is not required for simplified invoices. + validation.When( + !inv.Tax.ContainsTag(common.TagSimplified), + validation.Required, + ), + ), + validation.Field(&inv.Lines, + validation.Required, + ), validation.Field(&inv.Discounts), validation.Field(&inv.Charges), validation.Field(&inv.Outlays), - validation.Field(&inv.Ordering), validation.Field(&inv.Payment), validation.Field(&inv.Delivery), - - validation.Field(&inv.Totals, validation.Required), - + validation.Field(&inv.Totals, + validation.Required, + ), validation.Field(&inv.Notes), + validation.Field(&inv.Complements), validation.Field(&inv.Meta), - - validation.Field(&inv.Complements, validation.Each()), ) if err == nil { err = r.ValidateObject(inv) diff --git a/regimes/co/co.go b/regimes/co/co.go index 979251ad..5f31871e 100644 --- a/regimes/co/co.go +++ b/regimes/co/co.go @@ -8,6 +8,7 @@ import ( "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/org" "github.com/invopop/gobl/pkg/here" + "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -44,7 +45,7 @@ func New() *tax.Regime { `), }, TimeZone: "America/Bogota", - Tags: invoiceTags, + Tags: common.InvoiceTags(), Validator: Validate, Calculator: Calculate, IdentityTypeKeys: taxIdentityTypeDefs, // see tax_identity.go diff --git a/regimes/co/invoices.go b/regimes/co/invoices.go index 5d7ab4a0..3622ff29 100644 --- a/regimes/co/invoices.go +++ b/regimes/co/invoices.go @@ -3,30 +3,12 @@ package co import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/currency" - "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/org" - "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" "github.com/invopop/validation" ) -var invoiceTags = []*tax.KeyDefinition{ - // Simplified invoices when a client ID is not available. For conversion to local formats, - // the correct client ID data will need to be provided automatically. - { - Key: common.TagSimplified, - Name: i18n.String{ - i18n.EN: "Simplified Invoice", - i18n.ES: "Factura Simplificada", - }, - Desc: i18n.String{ - i18n.EN: "Used for B2C transactions when the client details are not available, check with local authorities for limits.", - i18n.ES: "Usado para transacciones B2C cuando los detalles del cliente no están disponibles, consulte con las autoridades locales para los límites.", - }, - }, -} - type invoiceValidator struct { inv *bill.Invoice } @@ -48,15 +30,10 @@ func (v *invoiceValidator) validate() error { ), ), validation.Field(&inv.Supplier, - validation.Required, validation.By(v.validParty), validation.By(v.validSupplier), ), validation.Field(&inv.Customer, - validation.When( - !inv.Tax.ContainsTag(common.TagSimplified), - validation.Required, - ), validation.By(v.validParty), ), validation.Field(&inv.Preceding, diff --git a/regimes/common/invoices.go b/regimes/common/invoices.go new file mode 100644 index 00000000..ff8376e4 --- /dev/null +++ b/regimes/common/invoices.go @@ -0,0 +1,69 @@ +package common + +import ( + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/tax" +) + +var invoiceTags = []*tax.KeyDefinition{ + // Simplified invoices are issued when the complete fiscal details of + // a customer are not available. + { + Key: TagSimplified, + Name: i18n.String{ + i18n.EN: "Simplified Invoice", + i18n.ES: "Factura Simplificada", + i18n.IT: "Fattura Semplificata", + }, + Desc: i18n.String{ + i18n.EN: "Used for B2C transactions when the client details are not available, check with local authorities for limits.", + i18n.ES: "Usado para transacciones B2C cuando los detalles del cliente no están disponibles, consulte con las autoridades locales para los límites.", + i18n.IT: "Utilizzato per le transazioni B2C quando i dettagli del cliente non sono disponibili, controllare con le autorità locali per i limiti.", + }, + }, + + // Reverse Charge mechanism is used when the supplier is not + // required to charge VAT on the invoice and the customer is + // responsible for paying the VAT to the tax authorities. + { + Key: TagReverseCharge, + Name: i18n.String{ + i18n.EN: "Reverse Charge", + i18n.ES: "Inversión del Sujeto Pasivo", + i18n.IT: "Inversione del soggetto passivo", + }, + }, + + // Self-billed invoices are issued by the customer instead of the + // supplier. This is usually done when the customer is a large + // company and the supplier is a small company. + { + Key: TagSelfBilled, + Name: i18n.String{ + i18n.EN: "Self-billed", + i18n.ES: "Facturación por el destinatario", + i18n.IT: "Autofattura", + }, + }, + + // Customer rates (mainly for digital goods inside EU) + { + Key: TagCustomerRates, + Name: i18n.String{ + i18n.EN: "Customer rates", + i18n.ES: "Tarifas aplicables al destinatario", + }, + }, +} + +// InvoiceTags returns a list of common invoice tag key +// definitions. +func InvoiceTags() []*tax.KeyDefinition { + return invoiceTags +} + +// InvoiceTagsWith appends the list of provided key definitions +// to the base list of tags and returns a new array. +func InvoiceTagsWith(tags []*tax.KeyDefinition) []*tax.KeyDefinition { + return append(InvoiceTags(), tags...) +} diff --git a/regimes/es/invoices.go b/regimes/es/invoices.go index 4faf957f..94ab9cf5 100644 --- a/regimes/es/invoices.go +++ b/regimes/es/invoices.go @@ -5,7 +5,6 @@ import ( "github.com/invopop/gobl/currency" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/org" - "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" "github.com/invopop/validation" ) @@ -40,11 +39,7 @@ func (v *invoiceValidator) validate() error { validation.By(v.supplier), ), validation.Field(&inv.Customer, - validation.When( - !inv.Tax.ContainsTag(common.TagSimplified), - validation.Required, - validation.By(v.commercialCustomer), - ), + validation.By(v.commercialCustomer), ), ) } diff --git a/regimes/es/scenarios.go b/regimes/es/scenarios.go index 2dc1a30a..b25d1312 100644 --- a/regimes/es/scenarios.go +++ b/regimes/es/scenarios.go @@ -21,39 +21,7 @@ const ( TagCashBasis cbc.Key = "cash-basis" ) -var invoiceTags = []*tax.KeyDefinition{ - // Simplified Invoice - { - Key: common.TagSimplified, - Name: i18n.String{ - i18n.EN: "Simplified Invoice", - i18n.ES: "Factura Simplificada", - }, - }, - // Customer rates (mainly for digital goods inside EU) - { - Key: common.TagCustomerRates, - Name: i18n.String{ - i18n.EN: "Customer rates", - i18n.ES: "Tarifas aplicables al destinatario", - }, - }, - // Reverse Charge Mechanism - { - Key: common.TagReverseCharge, - Name: i18n.String{ - i18n.EN: "Reverse Charge", - i18n.ES: "Inversión del sujeto pasivo", - }, - }, - // Customer issued invoices - { - Key: common.TagSelfBilled, - Name: i18n.String{ - i18n.EN: "Customer issued invoice", - i18n.ES: "Facturación por el destinatario", - }, - }, +var invoiceTags = common.InvoiceTagsWith([]*tax.KeyDefinition{ // Copy of the original document { Key: TagCopy, @@ -119,7 +87,7 @@ var invoiceTags = []*tax.KeyDefinition{ i18n.ES: "Régimen especial del criterio de caja", }, }, -} +}) var invoiceScenarios = &tax.ScenarioSet{ Schema: bill.ShortSchemaInvoice, diff --git a/regimes/fr/fr.go b/regimes/fr/fr.go index 40397b64..79d5c756 100644 --- a/regimes/fr/fr.go +++ b/regimes/fr/fr.go @@ -7,6 +7,7 @@ import ( "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/pkg/here" + "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -39,7 +40,7 @@ func New() *tax.Regime { `), }, TimeZone: "Europe/Paris", - Tags: invoiceTags, + Tags: common.InvoiceTags(), Scenarios: []*tax.ScenarioSet{ invoiceScenarios, }, diff --git a/regimes/fr/scenarios.go b/regimes/fr/scenarios.go index 53d9b67c..cf83b282 100644 --- a/regimes/fr/scenarios.go +++ b/regimes/fr/scenarios.go @@ -3,22 +3,10 @@ package fr import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) -var invoiceTags = []*tax.KeyDefinition{ - // Reverse Charge Mechanism - { - Key: common.TagReverseCharge, - Name: i18n.String{ - i18n.EN: "Reverse Charge", - i18n.ES: "Inversión del sujeto pasivo", - }, - }, -} - var invoiceScenarios = &tax.ScenarioSet{ Schema: bill.ShortSchemaInvoice, List: []*tax.Scenario{ diff --git a/regimes/gb/gb.go b/regimes/gb/gb.go index b9ed943e..f69138e6 100644 --- a/regimes/gb/gb.go +++ b/regimes/gb/gb.go @@ -30,6 +30,7 @@ func New() *tax.Regime { TimeZone: "Europe/London", Validator: Validate, Calculator: Calculate, + Tags: common.InvoiceTags(), Categories: taxCategories, } } diff --git a/regimes/gb/invoice_validator.go b/regimes/gb/invoice_validator.go index 7cab7918..eedf64e0 100644 --- a/regimes/gb/invoice_validator.go +++ b/regimes/gb/invoice_validator.go @@ -3,7 +3,6 @@ package gb import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/currency" - "github.com/invopop/gobl/regimes/common" "github.com/invopop/validation" ) @@ -21,11 +20,8 @@ func validateInvoice(inv *bill.Invoice) error { func (v *invoiceValidator) validate() error { inv := v.inv return validation.ValidateStruct(inv, - validation.Field(&inv.Currency, validation.In(currency.GBP)), - validation.Field(&inv.Supplier, validation.Required), - validation.Field(&inv.Customer, validation.When( - !inv.Tax.ContainsTag(common.TagSimplified), - validation.Required, - )), + validation.Field(&inv.Currency, + validation.In(currency.GBP), + ), ) } diff --git a/regimes/it/invoice_validator.go b/regimes/it/invoice_validator.go index ba83f6d8..32869410 100644 --- a/regimes/it/invoice_validator.go +++ b/regimes/it/invoice_validator.go @@ -29,9 +29,15 @@ func (v *invoiceValidator) validate() error { inv := v.inv return validation.ValidateStruct(inv, - validation.Field(&inv.Currency, validation.In(currency.EUR)), - validation.Field(&inv.Supplier, validation.By(v.supplier)), - validation.Field(&inv.Customer, validation.By(v.customer)), + validation.Field(&inv.Currency, + validation.In(currency.EUR), + ), + validation.Field(&inv.Supplier, + validation.By(v.supplier), + ), + validation.Field(&inv.Customer, + validation.By(v.customer), + ), ) } @@ -59,7 +65,7 @@ func (v *invoiceValidator) supplier(value interface{}) error { func (v *invoiceValidator) customer(value interface{}) error { customer, _ := value.(*org.Party) if customer == nil { - return errors.New("missing customer details") + return nil } // Customers must have a tax ID (PartitaIVA) if they are Italian legal entities diff --git a/regimes/it/scenarios.go b/regimes/it/scenarios.go index 1c397219..81ed626d 100644 --- a/regimes/it/scenarios.go +++ b/regimes/it/scenarios.go @@ -28,7 +28,7 @@ const ( // This is only a partial list of all the potential tags that // could be available for use in Italy. Given the complexity // involved, we've focussed here on the most useful. -var invoiceTags = []*tax.KeyDefinition{ +var invoiceTags = common.InvoiceTagsWith([]*tax.KeyDefinition{ // *** Document Type Tags *** { Key: TagFreelance, @@ -37,20 +37,6 @@ var invoiceTags = []*tax.KeyDefinition{ i18n.IT: "Parcella", }, }, - { - Key: common.TagReverseCharge, - Name: i18n.String{ - i18n.EN: "Reverse Charge", - i18n.IT: "Inversione del soggetto passivo", - }, - }, - { - Key: common.TagSelfBilled, - Name: i18n.String{ - i18n.EN: "Self-billed", - i18n.IT: "Autofattura", - }, - }, { Key: TagCeilingExceeded, Name: i18n.String{ @@ -128,7 +114,7 @@ var invoiceTags = []*tax.KeyDefinition{ i18n.IT: "Beni ammortizzabili", }, }, -} +}) var scenarios = []*tax.ScenarioSet{ invoiceScenarios, diff --git a/regimes/mx/invoice_validator.go b/regimes/mx/invoice_validator.go index 00500726..16c10169 100644 --- a/regimes/mx/invoice_validator.go +++ b/regimes/mx/invoice_validator.go @@ -33,7 +33,6 @@ func (v *invoiceValidator) validate() error { validation.Skip, ), validation.Field(&inv.Customer, - validation.Required, validation.By(v.validCustomer), validation.Skip, ), diff --git a/regimes/mx/mx.go b/regimes/mx/mx.go index 2002a383..e399609b 100644 --- a/regimes/mx/mx.go +++ b/regimes/mx/mx.go @@ -47,6 +47,7 @@ func New() *tax.Regime { TimeZone: "America/Mexico_City", Validator: Validate, Calculator: Calculate, + Tags: common.InvoiceTags(), PaymentMeansKeys: paymentMeansKeyDefinitions, // pay.go Extensions: extensionKeys, // extensions.go Scenarios: scenarios, // scenarios.go diff --git a/regimes/nl/invoice_validator.go b/regimes/nl/invoice_validator.go index 7c22b713..64c90d03 100644 --- a/regimes/nl/invoice_validator.go +++ b/regimes/nl/invoice_validator.go @@ -4,7 +4,6 @@ import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/currency" "github.com/invopop/gobl/org" - "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" "github.com/invopop/validation" ) @@ -23,13 +22,15 @@ func validateInvoice(inv *bill.Invoice) error { func (v *invoiceValidator) validate() error { inv := v.inv return validation.ValidateStruct(inv, - validation.Field(&inv.Currency, validation.In(currency.EUR)), - validation.Field(&inv.Supplier, validation.Required, validation.By(v.supplier)), - validation.Field(&inv.Customer, validation.When( - !inv.Tax.ContainsTag(common.TagSimplified), - validation.Required, + validation.Field(&inv.Currency, + validation.In(currency.EUR), + ), + validation.Field(&inv.Supplier, + validation.By(v.supplier), + ), + validation.Field(&inv.Customer, validation.By(v.customer), - )), + ), ) } diff --git a/regimes/nl/nl.go b/regimes/nl/nl.go index bcaf4509..eb3d89c6 100644 --- a/regimes/nl/nl.go +++ b/regimes/nl/nl.go @@ -26,6 +26,7 @@ func New() *tax.Regime { TimeZone: "Europe/Amsterdam", Validator: Validate, Calculator: Calculate, + Tags: common.InvoiceTags(), Categories: []*tax.Category{ // // VAT diff --git a/regimes/pt/invoice_validator.go b/regimes/pt/invoice_validator.go index df5288a2..0f66398f 100644 --- a/regimes/pt/invoice_validator.go +++ b/regimes/pt/invoice_validator.go @@ -5,7 +5,6 @@ import ( "github.com/invopop/gobl/currency" "github.com/invopop/gobl/num" "github.com/invopop/gobl/org" - "github.com/invopop/gobl/regimes/common" "github.com/invopop/validation" ) @@ -22,11 +21,6 @@ func (v *invoiceValidator) validate() error { inv := v.inv return validation.ValidateStruct(inv, validation.Field(&inv.Currency, validation.In(currency.EUR)), - validation.Field(&inv.Supplier, validation.Required), - validation.Field(&inv.Customer, validation.When( - !inv.Tax.ContainsTag(common.TagSimplified), - validation.Required, - )), validation.Field(&inv.Lines, validation.Each( validation.By(v.validLine), diff --git a/regimes/pt/scenarios.go b/regimes/pt/scenarios.go index d929a33c..19f9957c 100644 --- a/regimes/pt/scenarios.go +++ b/regimes/pt/scenarios.go @@ -17,7 +17,7 @@ var scenarios = []*tax.ScenarioSet{ invoiceScenarios, } -var invoiceTags = []*tax.KeyDefinition{ +var invoiceTags = common.InvoiceTagsWith([]*tax.KeyDefinition{ { Key: TagInvoiceReceipt, Name: i18n.String{ @@ -25,14 +25,7 @@ var invoiceTags = []*tax.KeyDefinition{ i18n.PT: "Fatura-recibo", }, }, - { - Key: common.TagSimplified, - Name: i18n.String{ - i18n.EN: "Simplified invoice", - i18n.PT: "Fatura simplificada", - }, - }, -} +}) var invoiceScenarios = &tax.ScenarioSet{ Schema: bill.ShortSchemaInvoice, diff --git a/regimes/us/invoice_validator.go b/regimes/us/invoice_validator.go index 53f115d3..751a199b 100644 --- a/regimes/us/invoice_validator.go +++ b/regimes/us/invoice_validator.go @@ -22,6 +22,5 @@ func (v *invoiceValidator) validate() error { return validation.ValidateStruct(inv, validation.Field(&inv.Currency, validation.In(currency.USD)), validation.Field(&inv.Supplier, validation.Required), - validation.Field(&inv.Customer), ) } diff --git a/regimes/us/us.go b/regimes/us/us.go index 61859dc5..0fbaa76f 100644 --- a/regimes/us/us.go +++ b/regimes/us/us.go @@ -30,6 +30,7 @@ func New() *tax.Regime { }, TimeZone: "America/Chicago", // Around the middle Validator: Validate, + Tags: common.InvoiceTags(), Categories: []*tax.Category{ // // Sales Tax