diff --git a/README.md b/README.md index d702368b..9f3852b2 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ The complexity around invoicing and in particular electronic invoicing can quick - [Documentation](https://docs.gobl.org) contains details on how to use GOBL, and the schema. - [Builder](https://build.gobl.org) helps try out GOBL and quickly figure out what is possible, all from your browser. -- [Blog](https://gobl.org/posts/) for news and general updates about what is being worked on. - [Issues](https://github.com/invopop/gobl/issues) if you have a specific problem with GOBL related to code or usage. - [Discussions](https://github.com/invopop/gobl/discussions) for open discussions about the future of GOBL, complications with a specific country, or any open ended issues. - [Pull Requests](https://github.com/invopop/gobl/pulls) are very welcome, especially if you'd like to see a new local country or features. diff --git a/bill/invoice_correct.go b/bill/invoice_correct.go index 4d1b0449..6e62d938 100644 --- a/bill/invoice_correct.go +++ b/bill/invoice_correct.go @@ -9,6 +9,7 @@ import ( "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/head" "github.com/invopop/gobl/schema" + "github.com/invopop/gobl/tax" ) // CorrectionOptions defines a structure used to pass configuration options @@ -28,9 +29,9 @@ type CorrectionOptions struct { // Human readable reason for the corrective operation. Reason string `json:"reason,omitempty" jsonschema:"title=Reason"` // Correction method as defined by the tax regime. - CorrectionMethod cbc.Key `json:"correction_method,omitempty" jsonschema:"title=Correction Method"` - // Correction keys that describe the specific changes according to the tax regime. - Corrections []cbc.Key `json:"corrections,omitempty" jsonschema:"title=Corrections"` + Method cbc.Key `json:"method,omitempty" jsonschema:"title=Method"` + // Changes keys that describe the specific changes according to the tax regime. + Changes []cbc.Key `json:"changes,omitempty" jsonschema:"title=Changes"` // In case we want to use a raw json object as a source of the options. data json.RawMessage `json:"-"` @@ -74,20 +75,20 @@ func WithReason(reason string) schema.Option { } } -// WithCorrectionMethod defines the method used to correct the previous invoice. -func WithCorrectionMethod(method cbc.Key) schema.Option { +// WithMethod defines the method used to correct the previous invoice. +func WithMethod(method cbc.Key) schema.Option { return func(o interface{}) { opts := o.(*CorrectionOptions) - opts.CorrectionMethod = method + opts.Method = method } } -// WithCorrection adds a single correction key to the invoice preceding data, -// use multiple times for multiple entries. -func WithCorrection(correction cbc.Key) schema.Option { +// WithChanges adds the set of change keys to the invoice's preceding data, +// can be called multiple times. +func WithChanges(changes ...cbc.Key) schema.Option { return func(o interface{}) { opts := o.(*CorrectionOptions) - opts.Corrections = append(opts.Corrections, correction) + opts.Changes = append(opts.Changes, changes...) } } @@ -121,24 +122,8 @@ var Debit schema.Option = func(o interface{}) { // most use cases this will prevent looping over the same invoice. func (inv *Invoice) Correct(opts ...schema.Option) error { o := new(CorrectionOptions) - for _, row := range opts { - row(o) - } - - // Copy over the stamps from the previous header - if o.Head != nil && len(o.Head.Stamps) > 0 { - o.Stamps = append(o.Stamps, o.Head.Stamps...) - } - - // If we have a raw json object, this will override any of the other options - if len(o.data) > 0 { - if err := json.Unmarshal(o.data, o); err != nil { - return fmt.Errorf("failed to unmarshal correction options: %w", err) - } - } - - if o.Credit && o.Debit { - return errors.New("cannot use both credit and debit options") + if err := prepareCorrectionOptions(o, opts...); err != nil { + return err } if inv.Code == "" { return errors.New("cannot correct an invoice without a code") @@ -156,8 +141,8 @@ func (inv *Invoice) Correct(opts ...schema.Option) error { Code: inv.Code, IssueDate: inv.IssueDate.Clone(), Reason: o.Reason, - Corrections: o.Corrections, - CorrectionMethod: o.CorrectionMethod, + CorrectionMethod: o.Method, + Changes: o.Changes, } inv.UUID = nil inv.Series = "" @@ -168,12 +153,55 @@ func (inv *Invoice) Correct(opts ...schema.Option) error { inv.IssueDate = cal.Today() } + cd := r.CorrectionDefinitionFor(ShortSchemaInvoice) + + if err := inv.prepareCorrectionType(o, cd); err != nil { + return err + } + + if err := inv.validatePrecedingData(o, cd, pre); err != nil { + return err + } + + // Replace all previous preceding data + inv.Preceding = []*Preceding{pre} + + // Running a Calculate feels a bit out of place, but not performing + // this operation on the corrected invoice results in potentially + // conflicting or incomplete data. + return inv.Calculate() +} + +func prepareCorrectionOptions(o *CorrectionOptions, opts ...schema.Option) error { + for _, row := range opts { + row(o) + } + + // Copy over the stamps from the previous header + if o.Head != nil && len(o.Head.Stamps) > 0 { + o.Stamps = append(o.Stamps, o.Head.Stamps...) + } + + // If we have a raw json object, this will override any of the other options + if len(o.data) > 0 { + if err := json.Unmarshal(o.data, o); err != nil { + return fmt.Errorf("failed to unmarshal correction options: %w", err) + } + } + + if o.Credit && o.Debit { + return errors.New("cannot use both credit and debit options") + } + return nil +} + +func (inv *Invoice) prepareCorrectionType(o *CorrectionOptions, cd *tax.CorrectionDefinition) error { // Take the regime def to figure out what needs to be copied if o.Credit { - if r.Preceding.HasType(InvoiceTypeCreditNote) { + if cd.HasType(InvoiceTypeCreditNote) { // regular credit note inv.Type = InvoiceTypeCreditNote - } else if r.Preceding.HasType(InvoiceTypeCorrective) { + } else if cd.HasType(InvoiceTypeCorrective) { // corrective invoice with negative values inv.Type = InvoiceTypeCorrective inv.Invert() @@ -182,7 +210,7 @@ func (inv *Invoice) Correct(opts ...schema.Option) error { } inv.Payment.ResetAdvances() } else if o.Debit { - if r.Preceding.HasType(InvoiceTypeDebitNote) { + if cd.HasType(InvoiceTypeDebitNote) { // regular debit note, implies no rows as new ones // will be added inv.Type = InvoiceTypeDebitNote @@ -191,35 +219,56 @@ func (inv *Invoice) Correct(opts ...schema.Option) error { return errors.New("debit note not supported by regime") } } else { - if r.Preceding.HasType(InvoiceTypeCorrective) { + if cd.HasType(InvoiceTypeCorrective) { inv.Type = InvoiceTypeCorrective } else { return fmt.Errorf("corrective invoice type not supported by regime, try credit or debit") } } + return nil +} - // Make sure the stamps are there too - if r.Preceding != nil { - for _, k := range r.Preceding.Stamps { - var s *head.Stamp - for _, row := range o.Stamps { - if row.Provider == k { - s = row - break - } +func (inv *Invoice) validatePrecedingData(o *CorrectionOptions, cd *tax.CorrectionDefinition, pre *Preceding) error { + if cd == nil { + return nil + } + for _, k := range cd.Stamps { + var s *head.Stamp + for _, row := range o.Stamps { + if row.Provider == k { + s = row + break } - if s == nil { - return fmt.Errorf("missing stamp: %v", k) + } + if s == nil { + return fmt.Errorf("missing stamp: %v", k) + } + pre.Stamps = append(pre.Stamps, s) + } + + if len(cd.Methods) > 0 { + if pre.CorrectionMethod == cbc.KeyEmpty { + return errors.New("missing correction method") + } + if !cd.HasMethod(pre.CorrectionMethod) { + return fmt.Errorf("invalid correction method: %v", pre.CorrectionMethod) + } + } + + if len(cd.Keys) > 0 { + if len(pre.Changes) == 0 { + return errors.New("missing changes") + } + for _, k := range pre.Changes { + if !cd.HasKey(k) { + return fmt.Errorf("invalid change key: '%v'", k) } - pre.Stamps = append(pre.Stamps, s) } } - // Replace all previous preceding data - inv.Preceding = []*Preceding{pre} + if cd.ReasonRequired && pre.Reason == "" { + return errors.New("missing corrective reason") + } - // Running a Calculate feels a bit out of place, but not performing - // this operation on the corrected invoice results in potentially - // conflicting or incomplete data. - return inv.Calculate() + return nil } diff --git a/bill/invoice_correct_test.go b/bill/invoice_correct_test.go index 35d46dd5..a426e763 100644 --- a/bill/invoice_correct_test.go +++ b/bill/invoice_correct_test.go @@ -5,12 +5,14 @@ import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/head" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/num" "github.com/invopop/gobl/org" "github.com/invopop/gobl/regimes/co" "github.com/invopop/gobl/regimes/common" + "github.com/invopop/gobl/regimes/es" "github.com/invopop/gobl/tax" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,7 +26,11 @@ func TestInvoiceCorrect(t *testing.T) { assert.Contains(t, err.Error(), "cannot use both credit and debit options") i = testInvoiceESForCorrection(t) - err = i.Correct(bill.Credit, bill.WithReason("test refund")) + err = i.Correct(bill.Credit, + bill.WithReason("test refund"), + bill.WithMethod(es.CorrectionMethodKeyComplete), + bill.WithChanges(es.CorrectionKeyLine), + ) require.NoError(t, err) assert.Equal(t, bill.InvoiceTypeCorrective, i.Type) assert.Equal(t, i.Lines[0].Quantity.String(), "-10") @@ -47,14 +53,21 @@ func TestInvoiceCorrect(t *testing.T) { assert.Contains(t, err.Error(), "debit note not supported by regime") i = testInvoiceESForCorrection(t) - err = i.Correct() + err = i.Correct( + bill.WithMethod(es.CorrectionMethodKeyComplete), + bill.WithChanges(es.CorrectionKeyLine), + ) require.NoError(t, err) assert.Equal(t, i.Type, bill.InvoiceTypeCorrective) // With preset date i = testInvoiceESForCorrection(t) d := cal.MakeDate(2023, 6, 13) - err = i.Correct(bill.WithIssueDate(d)) + err = i.Correct( + bill.WithIssueDate(d), + bill.WithMethod(es.CorrectionMethodKeyComplete), + bill.WithChanges(es.CorrectionKeyLine), + ) require.NoError(t, err) assert.Equal(t, i.IssueDate, d) @@ -93,7 +106,12 @@ func TestInvoiceCorrect(t *testing.T) { assert.Contains(t, err.Error(), "corrective invoice type not supported by regime, try credit or debit") i = testInvoiceCOForCorrection(t) - err = i.Correct(bill.Credit, bill.WithStamps(stamps), bill.WithCorrectionMethod(co.CorrectionMethodKeyRevoked)) + err = i.Correct( + bill.Credit, + bill.WithStamps(stamps), + bill.WithMethod(co.CorrectionMethodKeyRevoked), + bill.WithReason("test refund"), + ) require.NoError(t, err) assert.Equal(t, i.Type, bill.InvoiceTypeCreditNote) pre = i.Preceding[0] @@ -105,8 +123,10 @@ func TestInvoiceCorrect(t *testing.T) { func TestCorrectWithOptions(t *testing.T) { i := testInvoiceESForCorrection(t) opts := &bill.CorrectionOptions{ - Credit: true, - Reason: "test refund", + Credit: true, + Reason: "test refund", + Method: es.CorrectionMethodKeyComplete, + Changes: []cbc.Key{es.CorrectionKeyLine}, } err := i.Correct(bill.WithOptions(opts)) require.NoError(t, err) @@ -125,7 +145,11 @@ func TestCorrectWithData(t *testing.T) { i := testInvoiceESForCorrection(t) data := []byte(`{"credit":true,"reason":"test refund"}`) - err := i.Correct(bill.WithData(data)) + err := i.Correct( + bill.WithData(data), + bill.WithMethod(es.CorrectionMethodKeyComplete), + bill.WithChanges(es.CorrectionKeyLine), + ) assert.NoError(t, err) assert.Equal(t, i.Lines[0].Quantity.String(), "-10") // implies credit was made diff --git a/bill/preceding.go b/bill/preceding.go index 17644bb0..93a9bde4 100644 --- a/bill/preceding.go +++ b/bill/preceding.go @@ -1,6 +1,8 @@ package bill import ( + "encoding/json" + "github.com/invopop/gobl/cal" "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/head" @@ -24,16 +26,34 @@ type Preceding struct { Reason string `json:"reason,omitempty" jsonschema:"title=Reason"` // Seals of approval from other organisations that may need to be listed. Stamps []*head.Stamp `json:"stamps,omitempty" jsonschema:"title=Stamps"` - // Tax regime specific keys reflecting why the preceding invoice is being replaced. - Corrections []cbc.Key `json:"corrections,omitempty" jsonschema:"title=Corrections"` - // Tax regime specific keys reflecting the method used to correct the preceding invoice. + // Tax regime specific key reflecting the method used to correct the preceding invoice. CorrectionMethod cbc.Key `json:"correction_method,omitempty" jsonschema:"title=Correction Method"` + // Tax regime specific keys reflecting what has been changed from the previous invoice. + Changes []cbc.Key `json:"changes,omitempty" jsonschema:"title=Changes"` // Tax period in which the previous invoice had an effect required by some tax regimes and formats. Period *cal.Period `json:"period,omitempty" jsonschema:"title=Period"` // Additional semi-structured data that may be useful in specific regions Meta cbc.Meta `json:"meta,omitempty" jsonschema:"title=Meta"` } +// UnmarshalJSON is used to handle the refactor away from "corrections" to "changes" +func (p *Preceding) UnmarshalJSON(data []byte) error { + type Alias Preceding + aux := &struct { + Corrections []cbc.Key `json:"corrections,omitempty"` + *Alias + }{ + Alias: (*Alias)(p), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + if len(aux.Corrections) != 0 { + p.Changes = aux.Corrections + } + return nil +} + // Validate ensures the preceding details look okay func (p *Preceding) Validate() error { return validation.ValidateStruct(p, @@ -42,8 +62,8 @@ func (p *Preceding) Validate() error { validation.Field(&p.Code, validation.Required), validation.Field(&p.IssueDate, cal.DateNotZero()), validation.Field(&p.Stamps), - validation.Field(&p.Corrections), validation.Field(&p.CorrectionMethod), + validation.Field(&p.Changes), validation.Field(&p.Period), validation.Field(&p.Meta), ) diff --git a/bill/preceding_test.go b/bill/preceding_test.go index 74471d03..1317da93 100644 --- a/bill/preceding_test.go +++ b/bill/preceding_test.go @@ -16,3 +16,12 @@ func TestPrecedingValidation(t *testing.T) { err := p.Validate() assert.NoError(t, err) } + +func TestPrecedingJSONMigration(t *testing.T) { + data := []byte(`{"correction_method":"foo","corrections":["bar"]}`) + p := new(bill.Preceding) + err := p.UnmarshalJSON(data) + assert.NoError(t, err) + assert.Equal(t, "foo", p.CorrectionMethod.String()) + assert.Equal(t, "bar", p.Changes[0].String()) +} diff --git a/build/regimes/co.json b/build/regimes/co.json index 05046c3a..69cead8a 100644 --- a/build/regimes/co.json +++ b/build/regimes/co.json @@ -4,6 +4,9 @@ "en": "Colombia", "es": "Colombia" }, + "description": { + "en": "The Colombian tax regime is based on the DIAN (Dirección de Impuestos y Aduanas Nacionales)\nspecifications for electronic invoicing." + }, "time_zone": "America/Bogota", "country": "CO", "zones": [ @@ -14716,66 +14719,75 @@ } } ], - "preceding": { - "types": [ - "credit-note" - ], - "stamps": [ - "dian-cude" - ], - "correction_methods": [ - { - "key": "partial", - "desc": { - "en": "Partial refund of part of the goods or services.", - "es": "Devolución de parte de los bienes; no aceptación de partes del servicio." - }, - "map": { - "dian": "1" - } - }, - { - "key": "revoked", - "desc": { - "en": "Previous document has been completely cancelled.", - "es": "Anulación de la factura anterior." + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note" + ], + "methods": [ + { + "key": "partial", + "name": null, + "desc": { + "en": "Partial refund of part of the goods or services.", + "es": "Devolución de parte de los bienes; no aceptación de partes del servicio." + }, + "map": { + "dian": "1" + } }, - "map": { - "dian": "2" - } - }, - { - "key": "discount", - "desc": { - "en": "Partial or total discount.", - "es": "Rebaja o descuento parcial o total." + { + "key": "revoked", + "name": null, + "desc": { + "en": "Previous document has been completely cancelled.", + "es": "Anulación de la factura anterior." + }, + "map": { + "dian": "2" + } }, - "map": { - "dian": "3" - } - }, - { - "key": "price-adjustment", - "desc": { - "en": "Ajuste de precio.", - "es": "Price adjustment." + { + "key": "discount", + "name": null, + "desc": { + "en": "Partial or total discount.", + "es": "Rebaja o descuento parcial o total." + }, + "map": { + "dian": "3" + } }, - "map": { - "dian": "4" - } - }, - { - "key": "other", - "desc": { - "en": "Otros.", - "es": "Other." + { + "key": "price-adjustment", + "name": null, + "desc": { + "en": "Ajuste de precio.", + "es": "Price adjustment." + }, + "map": { + "dian": "4" + } }, - "map": { - "dian": "5" + { + "key": "other", + "name": null, + "desc": { + "en": "Otros.", + "es": "Other." + }, + "map": { + "dian": "5" + } } - } - ] - }, + ], + "reason_required": true, + "stamps": [ + "dian-cude" + ] + } + ], "categories": [ { "code": "VAT", @@ -14783,7 +14795,7 @@ "en": "VAT", "es": "IVA" }, - "desc": { + "title": { "en": "Value Added Tax", "es": "Impuesto al Valor Agregado" }, @@ -14837,7 +14849,7 @@ "name": { "es": "IC" }, - "desc": { + "title": { "en": "Consumption Tax", "es": "Impuesto sobre Consumo" } @@ -14847,7 +14859,7 @@ "name": { "es": "ICA" }, - "desc": { + "title": { "en": "Industry and Commerce Tax", "es": "Impuesto de Industria y Comercio" } @@ -14857,7 +14869,7 @@ "name": { "es": "ReteIVA" }, - "desc": { + "title": { "es": "Retención en la fuente por el Impuesto al Valor Agregado" }, "retained": true @@ -14867,7 +14879,7 @@ "name": { "es": "ReteICA" }, - "desc": { + "title": { "es": "Retención en la fuente por el Impuesto de Industria y Comercio" }, "retained": true @@ -14877,7 +14889,7 @@ "name": { "es": "Retefuente" }, - "desc": { + "title": { "es": "Retención en la fuente por el Impuesto de la Renta" }, "retained": true diff --git a/build/regimes/es.json b/build/regimes/es.json index 5cbb6abe..cb7f523b 100644 --- a/build/regimes/es.json +++ b/build/regimes/es.json @@ -888,275 +888,278 @@ ] } ], - "preceding": { - "types": [ - "corrective" - ], - "corrections": [ - { - "key": "code", - "desc": { - "en": "Invoice code", - "es": "Número de la factura" - }, - "map": { - "facturae": "01" - } - }, - { - "key": "series", - "desc": { - "en": "Invoice series", - "es": "Serie de la factura" - }, - "map": { - "facturae": "02" - } - }, - { - "key": "issue-date", - "desc": { - "en": "Issue date", - "es": "Fecha expedición" + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "corrective" + ], + "keys": [ + { + "key": "code", + "name": { + "en": "Invoice code", + "es": "Número de la factura" + }, + "map": { + "facturae": "01" + } }, - "map": { - "facturae": "03" - } - }, - { - "key": "supplier-name", - "desc": { - "en": "Name and surnames/Corporate name – Issuer (Sender)", - "es": "Nombre y apellidos/Razón Social-Emisor" + { + "key": "series", + "name": { + "en": "Invoice series", + "es": "Serie de la factura" + }, + "map": { + "facturae": "02" + } }, - "map": { - "facturae": "04" - } - }, - { - "key": "customer-name", - "desc": { - "en": "Name and surnames/Corporate name - Receiver", - "es": "Nombre y apellidos/Razón Social-Receptor" + { + "key": "issue-date", + "name": { + "en": "Issue date", + "es": "Fecha expedición" + }, + "map": { + "facturae": "03" + } }, - "map": { - "facturae": "05" - } - }, - { - "key": "supplier-tax-id", - "desc": { - "en": "Issuer's Tax Identification Number", - "es": "Identificación fiscal Emisor/obligado" + { + "key": "supplier-name", + "name": { + "en": "Name and surnames/Corporate name - Issuer (Sender)", + "es": "Nombre y apellidos/Razón Social-Emisor" + }, + "map": { + "facturae": "04" + } }, - "map": { - "facturae": "06" - } - }, - { - "key": "customer-tax-id", - "desc": { - "en": "Receiver's Tax Identification Number", - "es": "Identificación fiscal Receptor" + { + "key": "customer-name", + "name": { + "en": "Name and surnames/Corporate name - Receiver", + "es": "Nombre y apellidos/Razón Social-Receptor" + }, + "map": { + "facturae": "05" + } }, - "map": { - "facturae": "07" - } - }, - { - "key": "supplier-addr", - "desc": { - "en": "Issuer's address", - "es": "Domicilio Emisor/Obligado" + { + "key": "supplier-tax-id", + "name": { + "en": "Issuer's Tax Identification Number", + "es": "Identificación fiscal Emisor/obligado" + }, + "map": { + "facturae": "06" + } }, - "map": { - "facturae": "08" - } - }, - { - "key": "customer-addr", - "desc": { - "en": "Receiver's address", - "es": "Domicilio Receptor" + { + "key": "customer-tax-id", + "name": { + "en": "Receiver's Tax Identification Number", + "es": "Identificación fiscal Receptor" + }, + "map": { + "facturae": "07" + } }, - "map": { - "facturae": "09" - } - }, - { - "key": "line", - "desc": { - "en": "Item line", - "es": "Detalle Operación" + { + "key": "supplier-addr", + "name": { + "en": "Issuer's address", + "es": "Domicilio Emisor/Obligado" + }, + "map": { + "facturae": "08" + } }, - "map": { - "facturae": "10" - } - }, - { - "key": "tax-rate", - "desc": { - "en": "Applicable Tax Rate", - "es": "Porcentaje impositivo a aplicar" + { + "key": "customer-addr", + "name": { + "en": "Receiver's address", + "es": "Domicilio Receptor" + }, + "map": { + "facturae": "09" + } }, - "map": { - "facturae": "11" - } - }, - { - "key": "tax-amount", - "desc": { - "en": "Applicable Tax Amount", - "es": "Cuota tributaria a aplicar" + { + "key": "line", + "name": { + "en": "Item line", + "es": "Detalle Operación" + }, + "map": { + "facturae": "10" + } }, - "map": { - "facturae": "12" - } - }, - { - "key": "period", - "desc": { - "en": "Applicable Date/Period", - "es": "Fecha/Periodo a aplicar" + { + "key": "tax-rate", + "name": { + "en": "Applicable Tax Rate", + "es": "Porcentaje impositivo a aplicar" + }, + "map": { + "facturae": "11" + } }, - "map": { - "facturae": "13" - } - }, - { - "key": "type", - "desc": { - "en": "Invoice Class", - "es": "Clase de factura" + { + "key": "tax-amount", + "name": { + "en": "Applicable Tax Amount", + "es": "Cuota tributaria a aplicar" + }, + "map": { + "facturae": "12" + } }, - "map": { - "facturae": "14" - } - }, - { - "key": "legal-details", - "desc": { - "en": "Legal literals", - "es": "Literales legales" + { + "key": "period", + "name": { + "en": "Applicable Date/Period", + "es": "Fecha/Periodo a aplicar" + }, + "map": { + "facturae": "13" + } }, - "map": { - "facturae": "15" - } - }, - { - "key": "tax-base", - "desc": { - "en": "Taxable Base", - "es": "Base imponible" + { + "key": "type", + "name": { + "en": "Invoice Class", + "es": "Clase de factura" + }, + "map": { + "facturae": "14" + } }, - "map": { - "facturae": "16" - } - }, - { - "key": "tax", - "desc": { - "en": "Calculation of tax outputs", - "es": "Cálculo de cuotas repercutidas" + { + "key": "legal-details", + "name": { + "en": "Legal literals", + "es": "Literales legales" + }, + "map": { + "facturae": "15" + } }, - "map": { - "facturae": "80" - } - }, - { - "key": "tax-retained", - "desc": { - "en": "Calculation of tax inputs", - "es": "Cálculo de cuotas retenidas" + { + "key": "tax-base", + "name": { + "en": "Taxable Base", + "es": "Base imponible" + }, + "map": { + "facturae": "16" + } }, - "map": { - "facturae": "81" - } - }, - { - "key": "refund", - "desc": { - "en": "Taxable Base modified due to return of packages and packaging materials", - "es": "Base imponible modificada por devolución de envases / embalajes" + { + "key": "tax", + "name": { + "en": "Calculation of tax outputs", + "es": "Cálculo de cuotas repercutidas" + }, + "map": { + "facturae": "80" + } }, - "map": { - "facturae": "82" - } - }, - { - "key": "discount", - "desc": { - "en": "Taxable Base modified due to discounts and rebates", - "es": "Base imponible modificada por descuentos y bonificaciones" + { + "key": "tax-retained", + "name": { + "en": "Calculation of tax inputs", + "es": "Cálculo de cuotas retenidas" + }, + "map": { + "facturae": "81" + } }, - "map": { - "facturae": "83" - } - }, - { - "key": "judicial", - "desc": { - "en": "Taxable Base modified due to firm court ruling or administrative decision", - "es": "Base imponible modificada por resolución firme, judicial o administrativa" + { + "key": "refund", + "name": { + "en": "Taxable Base modified due to return of packages and packaging materials", + "es": "Base imponible modificada por devolución de envases / embalajes" + }, + "map": { + "facturae": "82" + } }, - "map": { - "facturae": "84" - } - }, - { - "key": "insolvency", - "desc": { - "en": "Taxable Base modified due to unpaid outputs where there is a judgement opening insolvency proceedings", - "es": "Base imponible modificada cuotas repercutidas no satisfechas. Auto de declaración de concurso" + { + "key": "discount", + "name": { + "en": "Taxable Base modified due to discounts and rebates", + "es": "Base imponible modificada por descuentos y bonificaciones" + }, + "map": { + "facturae": "83" + } }, - "map": { - "facturae": "85" - } - } - ], - "correction_methods": [ - { - "key": "complete", - "desc": { - "en": "Complete", - "es": "Rectificaticón íntegra" + { + "key": "judicial", + "name": { + "en": "Taxable Base modified due to firm court ruling or administrative decision", + "es": "Base imponible modificada por resolución firme, judicial o administrativa" + }, + "map": { + "facturae": "84" + } }, - "map": { - "facturae": "01" + { + "key": "insolvency", + "name": { + "en": "Taxable Base modified due to unpaid outputs where there is a judgement opening insolvency proceedings", + "es": "Base imponible modificada cuotas repercutidas no satisfechas. Auto de declaración de concurso" + }, + "map": { + "facturae": "85" + } } - }, - { - "key": "partial", - "desc": { - "en": "Corrected items only", - "es": "Rectificación por diferencias" + ], + "methods": [ + { + "key": "complete", + "name": { + "en": "Complete", + "es": "Rectificaticón íntegra" + }, + "map": { + "facturae": "01" + } }, - "map": { - "facturae": "02" - } - }, - { - "key": "discount", - "desc": { - "en": "Bulk deal in a given period", - "es": "Rectificación por descuento por volumen de operaciones durante un periodo" + { + "key": "partial", + "name": { + "en": "Corrected items only", + "es": "Rectificación por diferencias" + }, + "map": { + "facturae": "02" + } }, - "map": { - "facturae": "03" - } - }, - { - "key": "authorized", - "desc": { - "en": "Authorized by the Tax Agency", - "es": "Autorizadas por la Agencia Tributaria" + { + "key": "discount", + "name": { + "en": "Bulk deal in a given period", + "es": "Rectificación por descuento por volumen de operaciones durante un periodo" + }, + "map": { + "facturae": "03" + } }, - "map": { - "facturae": "04" + { + "key": "authorized", + "name": { + "en": "Authorized by the Tax Agency", + "es": "Autorizadas por la Agencia Tributaria" + }, + "map": { + "facturae": "04" + } } - } - ] - }, + ] + } + ], "categories": [ { "code": "VAT", @@ -1164,10 +1167,13 @@ "en": "VAT", "es": "IVA" }, - "desc": { + "title": { "en": "Value Added Tax", "es": "Impuesto sobre el Valor Añadido" }, + "desc": { + "en": "Known in Spanish as \"Impuesto sobre el Valor Añadido\" (IVA), is a consumption tax\napplied to the purchase of goods and services. It's a tax on the value added at\neach stage of production or distribution. Spain, as a member of the European Union,\nfollows the EU's VAT Directive, but with specific rates and exemptions tailored\nto its local needs." + }, "rates": [ { "key": "zero", @@ -1175,6 +1181,9 @@ "en": "Zero Rate", "es": "Tipo Cero" }, + "desc": { + "en": "May be applied to exports and intra-community supplies." + }, "values": [ { "percent": "0.0%" @@ -1322,7 +1331,7 @@ "en": "IGIC", "es": "IGIC" }, - "desc": { + "title": { "en": "Canary Island General Indirect Tax", "es": "Impuesto General Indirecto Canario" }, @@ -1374,7 +1383,7 @@ "en": "IPSI", "es": "IPSI" }, - "desc": { + "title": { "en": "Production, Services, and Import Tax", "es": "Impuesto sobre la Producción, los Servicios y la Importación" }, @@ -1388,7 +1397,7 @@ "en": "IRPF", "es": "IRPF" }, - "desc": { + "title": { "en": "Personal income tax.", "es": "Impuesto sobre la renta de las personas físicas." }, diff --git a/build/regimes/fr.json b/build/regimes/fr.json index ab80e787..478fa235 100644 --- a/build/regimes/fr.json +++ b/build/regimes/fr.json @@ -4,6 +4,9 @@ "en": "France", "fr": "La France" }, + "description": { + "en": "The French tax regime covers the basics." + }, "time_zone": "Europe/Paris", "country": "FR", "currency": "EUR", @@ -33,12 +36,15 @@ ] } ], - "preceding": { - "types": [ - "corrective", - "credit-note" - ] - }, + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "corrective", + "credit-note" + ] + } + ], "categories": [ { "code": "VAT", @@ -46,7 +52,7 @@ "en": "VAT", "fr": "TVA" }, - "desc": { + "title": { "en": "Value Added Tax", "fr": "Taxe sur la Valeur Ajoutée" }, diff --git a/build/regimes/gb.json b/build/regimes/gb.json index a5b91777..b09a695e 100644 --- a/build/regimes/gb.json +++ b/build/regimes/gb.json @@ -12,7 +12,7 @@ "name": { "en": "VAT" }, - "desc": { + "title": { "en": "Value Added Tax" }, "rates": [ diff --git a/build/regimes/it.json b/build/regimes/it.json index 8194c9ef..215945d4 100644 --- a/build/regimes/it.json +++ b/build/regimes/it.json @@ -1920,7 +1920,7 @@ "en": "VAT", "it": "IVA" }, - "desc": { + "title": { "en": "Value Added Tax", "it": "Imposta sul Valore Aggiunto" }, @@ -2004,7 +2004,7 @@ "en": "IRPEF", "it": "IRPEF" }, - "desc": { + "title": { "en": "Personal Income Tax", "it": "Imposta sul Reddito delle Persone Fisiche" }, @@ -2022,7 +2022,7 @@ "en": "IRES", "it": "IRES" }, - "desc": { + "title": { "en": "Corporate Income Tax", "it": "Imposta sul Reddito delle Società" }, @@ -2040,7 +2040,7 @@ "en": "INPS Contribution", "it": "Contributo INPS" }, - "desc": { + "title": { "en": "Contribution to the National Social Security Institute", "it": "Contributo Istituto Nazionale della Previdenza Sociale" }, @@ -2058,7 +2058,7 @@ "en": "ENASARCO Contribution", "it": "Contributo ENASARCO" }, - "desc": { + "title": { "en": "Contribution to the National Welfare Board for Sales Agents and Representatives", "it": "Contributo Ente Nazionale Assistenza Agenti e Rappresentanti di Commercio" }, @@ -2076,7 +2076,7 @@ "en": "ENPAM Contribution", "it": "Contributo ENPAM" }, - "desc": { + "title": { "en": "Contribution to the National Pension and Welfare Board for Doctors", "it": "Contributo - Ente Nazionale Previdenza e Assistenza Medici" }, diff --git a/build/regimes/mx.json b/build/regimes/mx.json index 02b0c769..46429766 100644 --- a/build/regimes/mx.json +++ b/build/regimes/mx.json @@ -574,14 +574,17 @@ ] } ], - "preceding": { - "types": [ - "credit-note" - ], - "stamps": [ - "sat-uuid" - ] - }, + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note" + ], + "stamps": [ + "sat-uuid" + ] + } + ], "categories": [ { "code": "VAT", @@ -589,7 +592,7 @@ "en": "VAT", "es": "IVA" }, - "desc": { + "title": { "en": "Value Added Tax", "es": "Impuesto al Valor Agregado" }, diff --git a/build/regimes/nl.json b/build/regimes/nl.json index d39ec37f..5271a69d 100644 --- a/build/regimes/nl.json +++ b/build/regimes/nl.json @@ -14,7 +14,7 @@ "en": "VAT", "nl": "BTW" }, - "desc": { + "title": { "en": "Value Added Tax", "nl": "Belasting Toegevoegde Waarde" }, diff --git a/build/regimes/pt.json b/build/regimes/pt.json index 140ce817..c8bf25ba 100644 --- a/build/regimes/pt.json +++ b/build/regimes/pt.json @@ -458,11 +458,14 @@ ] } ], - "preceding": { - "types": [ - "credit-note" - ] - }, + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note" + ] + } + ], "categories": [ { "code": "VAT", @@ -470,7 +473,7 @@ "en": "VAT", "pt": "IVA" }, - "desc": { + "title": { "en": "Value Added Tax", "pt": "Imposto sobre o Valor Acrescentado" }, diff --git a/build/regimes/us.json b/build/regimes/us.json index 13be13fc..d93c0c35 100644 --- a/build/regimes/us.json +++ b/build/regimes/us.json @@ -10,9 +10,9 @@ { "code": "ST", "name": { - "en": "Sales Tax" + "en": "ST" }, - "desc": { + "title": { "en": "Sales Tax" } } diff --git a/envelope_test.go b/envelope_test.go index e5c89005..79056242 100644 --- a/envelope_test.go +++ b/envelope_test.go @@ -3,7 +3,6 @@ package gobl_test import ( "encoding/json" "fmt" - "io/ioutil" "os" "testing" @@ -18,6 +17,7 @@ import ( "github.com/invopop/gobl/dsig" "github.com/invopop/gobl/head" "github.com/invopop/gobl/note" + "github.com/invopop/gobl/regimes/es" "github.com/invopop/gobl/schema" "github.com/invopop/gobl/uuid" ) @@ -293,19 +293,25 @@ func TestEnvelopeCorrect(t *testing.T) { t.Run("correct invoice", func(t *testing.T) { env := gobl.NewEnvelope() - data, err := ioutil.ReadFile("./regimes/es/examples/invoice-es-es.env.yaml") + data, err := os.ReadFile("./regimes/es/examples/invoice-es-es.env.yaml") require.NoError(t, err) err = yaml.Unmarshal(data, env) require.NoError(t, err) require.NoError(t, env.Calculate()) - _, err = env.Correct() + _, err = env.Correct( + bill.WithChanges(es.CorrectionKeyLine), + bill.WithMethod(es.CorrectionMethodKeyComplete), + ) require.NoError(t, err) doc := env.Extract().(*bill.Invoice) assert.Equal(t, doc.Type, bill.InvoiceTypeStandard, "no change") - e2, err := env.Correct() + e2, err := env.Correct( + bill.WithMethod(es.CorrectionMethodKeyComplete), + bill.WithChanges(es.CorrectionKeyLine), + ) require.NoError(t, err) doc = e2.Extract().(*bill.Invoice) assert.Equal(t, doc.Type, bill.InvoiceTypeCorrective, "corrected") diff --git a/regimes/co/co.go b/regimes/co/co.go index 7a180247..88dc2d7e 100644 --- a/regimes/co/co.go +++ b/regimes/co/co.go @@ -7,6 +7,7 @@ import ( "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/org" + "github.com/invopop/gobl/pkg/here" "github.com/invopop/gobl/tax" ) @@ -36,19 +37,29 @@ func New() *tax.Regime { i18n.EN: "Colombia", i18n.ES: "Colombia", }, + Description: i18n.String{ + i18n.EN: here.Doc(` + The Colombian tax regime is based on the DIAN (Dirección de Impuestos y Aduanas Nacionales) + specifications for electronic invoicing. + `), + }, TimeZone: "America/Bogota", Validator: Validate, Calculator: Calculate, IdentityTypeKeys: taxIdentityTypeDefs, // see tax_identity.go Zones: zones, // see zones.go - Preceding: &tax.PrecedingDefinitions{ // see preceding.go - Types: []cbc.Key{ - bill.InvoiceTypeCreditNote, - }, - Stamps: []cbc.Key{ - StampProviderDIANCUDE, + Corrections: []*tax.CorrectionDefinition{ // see preceding.go + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCreditNote, + }, + ReasonRequired: true, + Stamps: []cbc.Key{ + StampProviderDIANCUDE, + }, + Methods: correctionMethodList, }, - CorrectionMethods: correctionMethodList, }, Categories: taxCategories, } diff --git a/regimes/co/invoices_test.go b/regimes/co/invoices_test.go index 8e8f01fa..8d50bad5 100644 --- a/regimes/co/invoices_test.go +++ b/regimes/co/invoices_test.go @@ -161,7 +161,7 @@ func TestBasicCreditNoteValidation(t *testing.T) { inv.Preceding[0].CorrectionMethod = "fooo" err = inv.Validate() if assert.Error(t, err) { - assert.Contains(t, err.Error(), "correction_method: must be a valid value") + assert.Contains(t, err.Error(), "method: must be a valid value") } } diff --git a/regimes/co/tax_categories.go b/regimes/co/tax_categories.go index d227586f..65fe0e64 100644 --- a/regimes/co/tax_categories.go +++ b/regimes/co/tax_categories.go @@ -29,7 +29,7 @@ var taxCategories = []*tax.Category{ i18n.EN: "VAT", i18n.ES: "IVA", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Value Added Tax", i18n.ES: "Impuesto al Valor Agregado", }, @@ -87,7 +87,7 @@ var taxCategories = []*tax.Category{ Name: i18n.String{ i18n.ES: "IC", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Consumption Tax", i18n.ES: "Impuesto sobre Consumo", }, @@ -102,7 +102,7 @@ var taxCategories = []*tax.Category{ Name: i18n.String{ i18n.ES: "ICA", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Industry and Commerce Tax", i18n.ES: "Impuesto de Industria y Comercio", }, @@ -117,7 +117,7 @@ var taxCategories = []*tax.Category{ Name: i18n.String{ i18n.ES: "ReteIVA", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.ES: "Retención en la fuente por el Impuesto al Valor Agregado", }, Retained: true, @@ -131,7 +131,7 @@ var taxCategories = []*tax.Category{ Name: i18n.String{ i18n.ES: "ReteICA", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.ES: "Retención en la fuente por el Impuesto de Industria y Comercio", }, Retained: true, @@ -145,7 +145,7 @@ var taxCategories = []*tax.Category{ Name: i18n.String{ i18n.ES: "Retefuente", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.ES: "Retención en la fuente por el Impuesto de la Renta", }, Retained: true, diff --git a/regimes/es/preceding.go b/regimes/es/corrections.go similarity index 92% rename from regimes/es/preceding.go rename to regimes/es/corrections.go index a01bb775..f8d909af 100644 --- a/regimes/es/preceding.go +++ b/regimes/es/corrections.go @@ -48,7 +48,7 @@ const ( var correctionList = []*tax.KeyDefinition{ { Key: CorrectionKeyCode, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Invoice code", i18n.ES: "Número de la factura", }, @@ -56,7 +56,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeySeries, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Invoice series", i18n.ES: "Serie de la factura", }, @@ -64,7 +64,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyIssueDate, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Issue date", i18n.ES: "Fecha expedición", }, @@ -72,15 +72,15 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeySupplierName, - Desc: i18n.String{ - i18n.EN: "Name and surnames/Corporate name – Issuer (Sender)", + Name: i18n.String{ + i18n.EN: "Name and surnames/Corporate name - Issuer (Sender)", i18n.ES: "Nombre y apellidos/Razón Social-Emisor", }, Map: cbc.CodeMap{KeyFacturaE: "04"}, }, { Key: CorrectionKeyCustomerName, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Name and surnames/Corporate name - Receiver", i18n.ES: "Nombre y apellidos/Razón Social-Receptor", }, @@ -88,7 +88,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeySupplierTaxID, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Issuer's Tax Identification Number", i18n.ES: "Identificación fiscal Emisor/obligado", }, @@ -96,7 +96,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyCustomerTaxID, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Receiver's Tax Identification Number", i18n.ES: "Identificación fiscal Receptor", }, @@ -104,7 +104,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeySupplierAddress, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Issuer's address", i18n.ES: "Domicilio Emisor/Obligado", }, @@ -112,7 +112,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyCustomerAddress, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Receiver's address", i18n.ES: "Domicilio Receptor", }, @@ -120,7 +120,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyLine, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Item line", i18n.ES: "Detalle Operación", }, @@ -128,7 +128,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyTaxRate, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Applicable Tax Rate", i18n.ES: "Porcentaje impositivo a aplicar", }, @@ -136,7 +136,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyTaxAmount, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Applicable Tax Amount", i18n.ES: "Cuota tributaria a aplicar", }, @@ -144,7 +144,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyPeriod, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Applicable Date/Period", i18n.ES: "Fecha/Periodo a aplicar", }, @@ -152,7 +152,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyType, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Invoice Class", i18n.ES: "Clase de factura", }, @@ -160,7 +160,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyLegalDetails, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Legal literals", i18n.ES: "Literales legales", }, @@ -168,7 +168,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyTaxBase, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Taxable Base", i18n.ES: "Base imponible", }, @@ -176,7 +176,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyTax, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Calculation of tax outputs", i18n.ES: "Cálculo de cuotas repercutidas", }, @@ -184,7 +184,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyTaxRetained, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Calculation of tax inputs", i18n.ES: "Cálculo de cuotas retenidas", }, @@ -192,7 +192,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyRefund, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Taxable Base modified due to return of packages and packaging materials", i18n.ES: "Base imponible modificada por devolución de envases / embalajes", }, @@ -200,7 +200,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyDiscount, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Taxable Base modified due to discounts and rebates", i18n.ES: "Base imponible modificada por descuentos y bonificaciones", }, @@ -208,7 +208,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyJudicial, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Taxable Base modified due to firm court ruling or administrative decision", i18n.ES: "Base imponible modificada por resolución firme, judicial o administrativa", }, @@ -216,7 +216,7 @@ var correctionList = []*tax.KeyDefinition{ }, { Key: CorrectionKeyInsolvency, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Taxable Base modified due to unpaid outputs where there is a judgement opening insolvency proceedings", i18n.ES: "Base imponible modificada cuotas repercutidas no satisfechas. Auto de declaración de concurso", }, @@ -227,7 +227,7 @@ var correctionList = []*tax.KeyDefinition{ var correctionMethodList = []*tax.KeyDefinition{ { Key: CorrectionMethodKeyComplete, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Complete", i18n.ES: "Rectificaticón íntegra", }, @@ -235,7 +235,7 @@ var correctionMethodList = []*tax.KeyDefinition{ }, { Key: CorrectionMethodKeyPartial, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Corrected items only", i18n.ES: "Rectificación por diferencias", }, @@ -243,7 +243,7 @@ var correctionMethodList = []*tax.KeyDefinition{ }, { Key: CorrectionMethodKeyDiscount, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Bulk deal in a given period", i18n.ES: "Rectificación por descuento por volumen de operaciones durante un periodo", }, @@ -251,7 +251,7 @@ var correctionMethodList = []*tax.KeyDefinition{ }, { Key: CorrectionMethodKeyAuthorized, - Desc: i18n.String{ + Name: i18n.String{ i18n.EN: "Authorized by the Tax Agency", i18n.ES: "Autorizadas por la Agencia Tributaria", }, diff --git a/regimes/es/es.go b/regimes/es/es.go index e9da59c1..497c72ff 100644 --- a/regimes/es/es.go +++ b/regimes/es/es.go @@ -85,12 +85,15 @@ func New() *tax.Regime { Scenarios: []*tax.ScenarioSet{ invoiceScenarios, }, - Preceding: &tax.PrecedingDefinitions{ - Types: []cbc.Key{ - bill.InvoiceTypeCorrective, + Corrections: []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCorrective, + }, + Keys: correctionList, + Methods: correctionMethodList, }, - Corrections: correctionList, - CorrectionMethods: correctionMethodList, }, } } diff --git a/regimes/es/invoices.go b/regimes/es/invoices.go index 291ec20b..7206c185 100644 --- a/regimes/es/invoices.go +++ b/regimes/es/invoices.go @@ -87,7 +87,7 @@ func (v *invoiceValidator) preceding(value interface{}) error { return nil } return validation.ValidateStruct(obj, - validation.Field(&obj.Corrections, + validation.Field(&obj.Changes, validation.Required, validation.Each(isValidCorrectionKey), ), diff --git a/regimes/es/tax_categories.go b/regimes/es/tax_categories.go index 57bcfd73..673d356f 100644 --- a/regimes/es/tax_categories.go +++ b/regimes/es/tax_categories.go @@ -5,6 +5,7 @@ import ( "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/num" + "github.com/invopop/gobl/pkg/here" "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -20,10 +21,19 @@ var taxCategories = []*tax.Category{ i18n.EN: "VAT", i18n.ES: "IVA", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Value Added Tax", i18n.ES: "Impuesto sobre el Valor Añadido", }, + Description: &i18n.String{ + i18n.EN: here.Doc(` + Known in Spanish as "Impuesto sobre el Valor Añadido" (IVA), is a consumption tax + applied to the purchase of goods and services. It's a tax on the value added at + each stage of production or distribution. Spain, as a member of the European Union, + follows the EU's VAT Directive, but with specific rates and exemptions tailored + to its local needs. + `), + }, Map: cbc.CodeMap{ KeyFacturaETaxTypeCode: "01", }, @@ -34,6 +44,9 @@ var taxCategories = []*tax.Category{ i18n.EN: "Zero Rate", i18n.ES: "Tipo Cero", }, + Description: i18n.String{ + i18n.EN: "May be applied to exports and intra-community supplies.", + }, Values: []*tax.RateValue{ { Percent: num.MakePercentage(0, 3), @@ -184,7 +197,7 @@ var taxCategories = []*tax.Category{ Map: cbc.CodeMap{ KeyFacturaETaxTypeCode: "03", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Canary Island General Indirect Tax", i18n.ES: "Impuesto General Indirecto Canario", }, @@ -242,7 +255,7 @@ var taxCategories = []*tax.Category{ Map: cbc.CodeMap{ KeyFacturaETaxTypeCode: "02", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Production, Services, and Import Tax", i18n.ES: "Impuesto sobre la Producción, los Servicios y la Importación", }, @@ -265,7 +278,7 @@ var taxCategories = []*tax.Category{ Map: cbc.CodeMap{ KeyFacturaETaxTypeCode: "04", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Personal income tax.", i18n.ES: "Impuesto sobre la renta de las personas físicas.", }, diff --git a/regimes/fr/fr.go b/regimes/fr/fr.go index 9c660fa4..40397b64 100644 --- a/regimes/fr/fr.go +++ b/regimes/fr/fr.go @@ -6,6 +6,7 @@ import ( "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/pkg/here" "github.com/invopop/gobl/tax" ) @@ -32,16 +33,24 @@ func New() *tax.Regime { i18n.EN: "France", i18n.FR: "La France", }, + Description: i18n.String{ + i18n.EN: here.Doc(` + The French tax regime covers the basics. + `), + }, TimeZone: "Europe/Paris", Tags: invoiceTags, Scenarios: []*tax.ScenarioSet{ invoiceScenarios, }, - Preceding: &tax.PrecedingDefinitions{ - // France supports both corrective methods - Types: []cbc.Key{ - bill.InvoiceTypeCorrective, // Code 384 - bill.InvoiceTypeCreditNote, // Code 381 + Corrections: []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + // France supports both corrective methods + Types: []cbc.Key{ + bill.InvoiceTypeCorrective, // Code 384 + bill.InvoiceTypeCreditNote, // Code 381 + }, }, }, Validator: Validate, diff --git a/regimes/fr/tax_categories.go b/regimes/fr/tax_categories.go index 1c3a1783..617d55d2 100644 --- a/regimes/fr/tax_categories.go +++ b/regimes/fr/tax_categories.go @@ -18,7 +18,7 @@ var taxCategories = []*tax.Category{ i18n.EN: "VAT", i18n.FR: "TVA", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Value Added Tax", i18n.FR: "Taxe sur la Valeur Ajoutée", }, @@ -50,7 +50,7 @@ var taxCategories = []*tax.Category{ i18n.EN: "Standard rate", i18n.FR: "Taux normal", }, - Desc: i18n.String{ + Description: i18n.String{ i18n.EN: "For the majority of sales of goods and services: it applies to all products or services for which no other rate is expressly provided.", i18n.FR: "Pour la majorité des ventes de biens et des prestations de services : il s'applique à tous les produits ou services pour lesquels aucun autre taux n'est expressément prévu.", }, @@ -67,7 +67,7 @@ var taxCategories = []*tax.Category{ i18n.EN: "Intermediate rate", i18n.FR: "Taux intermédiaire", }, - Desc: i18n.String{ + Description: i18n.String{ i18n.EN: "Applicable in particular to unprocessed agricultural products, firewood, housing improvement works which do not benefit from the 5.5% rate, to certain accommodation and camping services, to fairs and exhibitions, fairground games and rides, to entrance fees to museums, zoos, monuments, to passenger transport, to the processing of waste, restoration.", i18n.FR: "Notamment applicable aux produits agricoles non transformés, au bois de chauffage, aux travaux d'amélioration du logement qui ne bénéficient pas du taux de 5,5%, à certaines prestations de logement et de camping, aux foires et salons, jeux et manèges forains, aux droits d'entrée des musées, zoo, monuments, aux transports de voyageurs, au traitement des déchets, à la restauration.", }, @@ -84,7 +84,7 @@ var taxCategories = []*tax.Category{ i18n.EN: "Reduced rate", i18n.FR: "Taux réduit", }, - Desc: i18n.String{ + Description: i18n.String{ i18n.EN: "Concerns most food products, feminine hygiene protection products, equipment and services for the disabled, books on any medium, gas and electricity subscriptions, supply of heat from renewable energies, supply of meals in school canteens, ticketing for live shows and cinemas, certain imports and deliveries of works of art, improvement works the energy quality of housing, social or emergency housing, home ownership.", i18n.FR: "Concerne l'essentiel des produits alimentaires, les produits de protection hygiénique féminine, équipements et services pour handicapés, livres sur tout support, abonnements gaz et électricité, fourniture de chaleur issue d’énergies renouvelables, fourniture de repas dans les cantines scolaires, billeterie de spectacle vivant et de cinéma, certaines importations et livraisons d'œuvres d'art, travaux d’amélioration de la qualité énergétique des logements, logements sociaux ou d'urgence, accession à la propriété.", }, @@ -101,7 +101,7 @@ var taxCategories = []*tax.Category{ i18n.EN: "Special rate", i18n.FR: "Taux particulier", }, - Desc: i18n.String{ + Description: i18n.String{ i18n.EN: "Reserved for medicines reimbursable by social security, sales of live animals for slaughter and charcuterie to non-taxable persons, the television license fee, certain shows and press publications registered with the Joint Commission for Publications and Press Agencies.", i18n.FR: "Réservé aux médicaments remboursables par la sécurité sociale, aux ventes d’animaux vivants de boucherie et de charcuterie à des non assujettis, à la redevance télévision, à certains spectacles et aux publications de presse inscrites à la Commission paritaire des publications et agences de presse.", }, diff --git a/regimes/gb/tax_categories.go b/regimes/gb/tax_categories.go index dedcbc04..26640dca 100644 --- a/regimes/gb/tax_categories.go +++ b/regimes/gb/tax_categories.go @@ -17,7 +17,7 @@ var taxCategories = []*tax.Category{ Name: i18n.String{ i18n.EN: "VAT", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Value Added Tax", }, Retained: false, diff --git a/regimes/it/categories.go b/regimes/it/categories.go index 58d3b105..32ed5fe9 100644 --- a/regimes/it/categories.go +++ b/regimes/it/categories.go @@ -28,7 +28,7 @@ var categories = []*tax.Category{ i18n.EN: "VAT", i18n.IT: "IVA", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Value Added Tax", i18n.IT: "Imposta sul Valore Aggiunto", }, @@ -115,7 +115,7 @@ var categories = []*tax.Category{ i18n.EN: "IRPEF", i18n.IT: "IRPEF", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Personal Income Tax", i18n.IT: "Imposta sul Reddito delle Persone Fisiche", }, @@ -131,7 +131,7 @@ var categories = []*tax.Category{ i18n.EN: "IRES", i18n.IT: "IRES", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Corporate Income Tax", i18n.IT: "Imposta sul Reddito delle Società", }, @@ -147,7 +147,7 @@ var categories = []*tax.Category{ i18n.EN: "INPS Contribution", i18n.IT: "Contributo INPS", // nolint:misspell }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Contribution to the National Social Security Institute", i18n.IT: "Contributo Istituto Nazionale della Previdenza Sociale", // nolint:misspell }, @@ -163,7 +163,7 @@ var categories = []*tax.Category{ i18n.EN: "ENASARCO Contribution", i18n.IT: "Contributo ENASARCO", // nolint:misspell }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Contribution to the National Welfare Board for Sales Agents and Representatives", i18n.IT: "Contributo Ente Nazionale Assistenza Agenti e Rappresentanti di Commercio", // nolint:misspell }, @@ -179,7 +179,7 @@ var categories = []*tax.Category{ i18n.EN: "ENPAM Contribution", i18n.IT: "Contributo ENPAM", // nolint:misspell }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Contribution to the National Pension and Welfare Board for Doctors", i18n.IT: "Contributo - Ente Nazionale Previdenza e Assistenza Medici", // nolint:misspell }, diff --git a/regimes/mx/corrections.go b/regimes/mx/corrections.go new file mode 100644 index 00000000..54283f79 --- /dev/null +++ b/regimes/mx/corrections.go @@ -0,0 +1,19 @@ +package mx + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/tax" +) + +var correctionDefinitions = []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCreditNote, + }, + Stamps: []cbc.Key{ + StampProviderSATUUID, + }, + }, +} diff --git a/regimes/mx/mx.go b/regimes/mx/mx.go index c6d4678c..e65f0ed3 100644 --- a/regimes/mx/mx.go +++ b/regimes/mx/mx.go @@ -44,7 +44,7 @@ func New() *tax.Regime { Extensions: extensionKeys, // extensions.go Scenarios: scenarios, // scenarios.go Categories: taxCategories, // categories.go - Preceding: precedingDefinitions, // preceding.go + Corrections: correctionDefinitions, // corrections.go } } diff --git a/regimes/mx/preceding.go b/regimes/mx/preceding.go deleted file mode 100644 index 18b87ac7..00000000 --- a/regimes/mx/preceding.go +++ /dev/null @@ -1,16 +0,0 @@ -package mx - -import ( - "github.com/invopop/gobl/bill" - "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/tax" -) - -var precedingDefinitions = &tax.PrecedingDefinitions{ - Types: []cbc.Key{ - bill.InvoiceTypeCreditNote, - }, - Stamps: []cbc.Key{ - StampProviderSATUUID, - }, -} diff --git a/regimes/mx/tax_categories.go b/regimes/mx/tax_categories.go index 3bd03baf..aade705f 100644 --- a/regimes/mx/tax_categories.go +++ b/regimes/mx/tax_categories.go @@ -20,7 +20,7 @@ var taxCategories = []*tax.Category{ i18n.EN: "VAT", i18n.ES: "IVA", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Value Added Tax", i18n.ES: "Impuesto al Valor Agregado", }, diff --git a/regimes/nl/nl.go b/regimes/nl/nl.go index 51029dab..bcaf4509 100644 --- a/regimes/nl/nl.go +++ b/regimes/nl/nl.go @@ -36,7 +36,7 @@ func New() *tax.Regime { i18n.EN: "VAT", i18n.NL: "BTW", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Value Added Tax", i18n.NL: "Belasting Toegevoegde Waarde", }, diff --git a/regimes/pt/pt.go b/regimes/pt/pt.go index 4b0aaadc..e6107748 100644 --- a/regimes/pt/pt.go +++ b/regimes/pt/pt.go @@ -69,9 +69,12 @@ func New() *tax.Regime { Scenarios: scenarios, Validator: Validate, Calculator: Calculate, - Preceding: &tax.PrecedingDefinitions{ - Types: []cbc.Key{ - bill.InvoiceTypeCreditNote, + Corrections: []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCreditNote, + }, }, }, Categories: taxCategories, diff --git a/regimes/pt/tax_categories.go b/regimes/pt/tax_categories.go index 9ef709a8..64887a9a 100644 --- a/regimes/pt/tax_categories.go +++ b/regimes/pt/tax_categories.go @@ -27,7 +27,7 @@ var taxCategories = []*tax.Category{ i18n.EN: "VAT", i18n.PT: "IVA", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Value Added Tax", i18n.PT: "Imposto sobre o Valor Acrescentado", }, diff --git a/regimes/us/us.go b/regimes/us/us.go index 7a3e928c..61859dc5 100644 --- a/regimes/us/us.go +++ b/regimes/us/us.go @@ -37,9 +37,9 @@ func New() *tax.Regime { { Code: common.TaxCategoryST, Name: i18n.String{ - i18n.EN: "Sales Tax", + i18n.EN: "ST", }, - Desc: i18n.String{ + Title: i18n.String{ i18n.EN: "Sales Tax", }, Retained: false, diff --git a/tax/regime.go b/tax/regime.go index 0b1b60ee..f4b0d4b2 100644 --- a/tax/regime.go +++ b/tax/regime.go @@ -28,6 +28,9 @@ type Regime struct { // Name of the country Name i18n.String `json:"name" jsonschema:"title=Name"` + // Introductory details about the regime. + Description i18n.String `json:"description,omitempty" jsonschema:"title=Description"` + // Location name for the country's central time zone. Accepted // values from IANA Time Zone Database (https://iana.org/time-zones). TimeZone string `json:"time_zone" jsonschema:"title=Time Zone"` @@ -72,8 +75,8 @@ type Regime struct { // Sets of scenario definitions for the regime. Scenarios []*ScenarioSet `json:"scenarios,omitempty" jsonschema:"title=Scenarios"` - // Configuration details for preceding options. - Preceding *PrecedingDefinitions `json:"preceding,omitempty" jsonschema:"title=Preceding"` + // Configuration details for corrections to be used with correction options. + Corrections []*CorrectionDefinition `json:"corrections,omitempty" jsonschema:"title=Corrections"` // List of tax categories. Categories []*Category `json:"categories" jsonschema:"title=Categories"` @@ -108,9 +111,17 @@ type Zone struct { // Category contains the definition of a general type of tax inside a region. type Category struct { - Code cbc.Code `json:"code" jsonschema:"title=Code"` + // Code to be used in documents + Code cbc.Code `json:"code" jsonschema:"title=Code"` + + // Short name of the category to be used instead of code in output Name i18n.String `json:"name" jsonschema:"title=Name"` - Desc i18n.String `json:"desc,omitempty" jsonschema:"title=Description"` + + // Human name for the code to use for titles + Title i18n.String `json:"title,omitempty" jsonschema:"title=Title"` + + // Useful description of the category. + Description *i18n.String `json:"desc,omitempty" jsonschema:"title=Description"` // Retained when true implies that the tax amount will be retained // by the buyer on behalf of the supplier, and thus subtracted from @@ -157,7 +168,7 @@ type Rate struct { // Human name of the rate Name i18n.String `json:"name" jsonschema:"title=Name"` // Useful description of the rate. - Desc i18n.String `json:"desc,omitempty" jsonschema:"title=Description"` + Description i18n.String `json:"desc,omitempty" jsonschema:"title=Description"` // Exempt when true implies that the rate when used in a tax Combo should // not define a percent value. @@ -199,25 +210,28 @@ type RateValue struct { Disabled bool `json:"disabled,omitempty" jsonschema:"title=Disabled"` } -// PrecedingDefinitions contains details about what can be defined in Invoice -// preceding document data. -type PrecedingDefinitions struct { +// CorrectionDefinition contains details about what can be defined in . +type CorrectionDefinition struct { + // Partial or complete schema URL for the document type supported by correction. + Schema string `json:"schema" jsonschema:"title=Schema"` // The types of sub-documents supported by the regime Types []cbc.Key `json:"types,omitempty" jsonschema:"title=Types"` + // List of all the keys that can be used to identify a correction. + Keys []*KeyDefinition `json:"keys,omitempty" jsonschema:"title=Keys"` + // Methods describe the methods used to correct an invoice. + Methods []*KeyDefinition `json:"methods,omitempty" jsonschema:"title=Methods"` + // ReasonRequired when true implies that a reason must be provided + ReasonRequired bool `json:"reason_required,omitempty" jsonschema:"title=Reason Required"` // Stamps that must be copied from the preceding document. Stamps []cbc.Key `json:"stamps,omitempty" jsonschema:"title=Stamps"` - // Corrections contains a list of all the keys that can be used to identify a correction. - Corrections []*KeyDefinition `json:"corrections,omitempty" jsonschema:"title=Corrections"` - // CorrectionMethods describe the methods used to correct an invoice. - CorrectionMethods []*KeyDefinition `json:"correction_methods,omitempty" jsonschema:"title=Correction Methods"` } // KeyDefinition defines properties of a key that is specific for a regime. type KeyDefinition struct { // Actual key value. Key cbc.Key `json:"key" jsonschema:"title=Key"` - // Short name for the key, if relevant. - Name i18n.String `json:"name,omitempty" jsonschema:"title=Name"` + // Short name for the key. + Name i18n.String `json:"name" jsonschema:"title=Name"` // Description offering more details about when the key should be used. Desc i18n.String `json:"desc,omitempty" jsonschema:"title=Description"` // Codes describes the list of codes that can be used alongside the Key, @@ -278,6 +292,16 @@ func (r *Regime) ScenarioSet(schema string) *ScenarioSet { return nil } +// CorrectionDefinitionFor provides the correction definition for the matching schema. +func (r *Regime) CorrectionDefinitionFor(schema string) *CorrectionDefinition { + for _, c := range r.Corrections { + if strings.HasSuffix(schema, c.Schema) { + return c + } + } + return nil +} + // Validate enures the region definition is valid, including all // subsequent categories. func (r *Regime) Validate() error { @@ -288,6 +312,7 @@ func (r *Regime) Validate() error { validation.Field(&r.Scenarios), validation.Field(&r.Categories, validation.Required), validation.Field(&r.Zones), + validation.Field(&r.Corrections), ) return err } @@ -456,7 +481,8 @@ func (c *Category) Validate() error { err := validation.ValidateStruct(c, validation.Field(&c.Code, validation.Required), validation.Field(&c.Name, validation.Required), - validation.Field(&c.Desc), + validation.Field(&c.Title, validation.Required), + validation.Field(&c.Description), validation.Field(&c.Sources), validation.Field(&c.Rates), validation.Field(&c.Extensions, @@ -612,12 +638,51 @@ func (rv *RateValue) HasZone(zone l10n.Code) bool { return false } -// HasType returns true if the preceding definitions has a type that matches the one provided. -func (pd *PrecedingDefinitions) HasType(t cbc.Key) bool { - if pd == nil { +// HasType returns true if the correction definition has a type that matches the one provided. +func (cd *CorrectionDefinition) HasType(t cbc.Key) bool { + if cd == nil { return false // no preceding definitions } - return t.In(pd.Types...) + return t.In(cd.Types...) +} + +// HasKey returns true if the correction definition has the keys provided. +func (cd *CorrectionDefinition) HasKey(key cbc.Key) bool { + if cd == nil { + return false // no correction definitions + } + + for _, kd := range cd.Keys { + if kd.Key == key { + return true + } + } + return false +} + +// HasMethod returns true if the correction definition has the method provided. +func (cd *CorrectionDefinition) HasMethod(key cbc.Key) bool { + if cd == nil { + return false // no correction definitions + } + for _, kd := range cd.Methods { + if kd.Key == key { + return true + } + } + return false +} + +// Validate ensures the key definition looks correct in the context of the regime. +func (cd *CorrectionDefinition) Validate() error { + err := validation.ValidateStruct(cd, + validation.Field(&cd.Schema, validation.Required), + validation.Field(&cd.Types), + validation.Field(&cd.Stamps), + validation.Field(&cd.Keys), + validation.Field(&cd.Methods), + ) + return err } // Validate ensures the key definition looks correct in the context of the regime.