From 81059a946da807f63d8d9ab2a7ad71057d800bf5 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 26 Jul 2024 12:42:33 +0000 Subject: [PATCH 1/5] Advanced scenarios supported with additional notes for exempt reasons --- bill/invoice.go | 37 ++++- cbc/meta.go | 13 ++ cbc/meta_test.go | 21 +++ cbc/notes.go | 16 ++ data/regimes/it.json | 170 ++++++++++---------- data/regimes/pl.json | 52 +++--- data/regimes/pt.json | 72 ++++++++- data/schemas/tax/regime.json | 18 ++- regimes/pt/examples/invoice-exempt.yaml | 29 ++++ regimes/pt/examples/out/invoice-exempt.json | 92 +++++++++++ regimes/pt/scenarios.go | 73 ++++++++- tax/scenario.go | 58 ++++++- 12 files changed, 516 insertions(+), 135 deletions(-) create mode 100644 regimes/pt/examples/invoice-exempt.yaml create mode 100644 regimes/pt/examples/out/invoice-exempt.json diff --git a/bill/invoice.go b/bill/invoice.go index ca54d6ba..4f37d199 100644 --- a/bill/invoice.go +++ b/bill/invoice.go @@ -302,7 +302,7 @@ func (inv *Invoice) Calculate() error { } } - if err := inv.prepareTagsAndScenarios(); err != nil { + if err := inv.prepareTags(); err != nil { return err } @@ -321,7 +321,15 @@ func (inv *Invoice) Calculate() error { return err } - return inv.calculateWithRegime(r) + if err := inv.calculateWithRegime(r); err != nil { + return err + } + + if err := inv.prepareScenarios(); err != nil { + return err + } + + return nil } // RemoveIncludedTaxes is a special function that will go through all prices which may include @@ -404,14 +412,25 @@ func (inv *Invoice) scenarioSummary(r *tax.Regime) *tax.ScenarioSummary { if ss == nil { return nil } + exts := make([]tax.Extensions, 0) tags := []cbc.Key{} + if inv.Tax != nil { tags = inv.Tax.Tags + if len(inv.Tax.Ext) > 0 { + exts = append(exts, inv.Tax.Ext) + } } - return ss.SummaryFor(inv.Type, tags) + for _, cat := range inv.Totals.Taxes.Categories { + for _, rate := range cat.Rates { + exts = append(exts, rate.Ext) + } + } + + return ss.SummaryFor(inv.Type, tags, exts) } -func (inv *Invoice) prepareTagsAndScenarios() error { +func (inv *Invoice) prepareTags() error { r := inv.TaxRegime() if r == nil { return nil @@ -426,6 +445,14 @@ func (inv *Invoice) prepareTagsAndScenarios() error { return fmt.Errorf("invalid document tag: %v", k) } } + return nil +} + +func (inv *Invoice) prepareScenarios() error { + r := inv.TaxRegime() + if r == nil { + return nil + } // Use the scenario summary to add any notes to the invoice ss := inv.scenarioSummary(r) @@ -436,7 +463,7 @@ func (inv *Invoice) prepareTagsAndScenarios() error { // make sure we don't already have the same note in the invoice var en *cbc.Note for _, n2 := range inv.Notes { - if n.Src == n2.Src { + if n.Src == n2.Src && n.Code == n2.Code { en = n break } diff --git a/cbc/meta.go b/cbc/meta.go index 06311564..b1e7b7ca 100644 --- a/cbc/meta.go +++ b/cbc/meta.go @@ -32,6 +32,19 @@ func (m Meta) Validate() error { return err } +// Equals checks if the meta data is the same. +func (m Meta) Equals(m2 Meta) bool { + if len(m) != len(m2) { + return false + } + for k, v := range m { + if m2[k] != v { + return false + } + } + return true +} + // JSONSchemaExtend ensures the meta keys are valid. func (Meta) JSONSchemaExtend(schema *jsonschema.Schema) { prop := schema.AdditionalProperties diff --git a/cbc/meta_test.go b/cbc/meta_test.go index e8090d81..3bc835e4 100644 --- a/cbc/meta_test.go +++ b/cbc/meta_test.go @@ -33,3 +33,24 @@ func TestMeta(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "Meta: (bad_key: must be in a valid format.)") } + +func TestMetaEquals(t *testing.T) { + m1 := cbc.Meta{ + cbc.Key("test"): "bar", + } + m2 := cbc.Meta{ + cbc.Key("test"): "bar", + } + assert.True(t, m1.Equals(m2)) + + m2 = cbc.Meta{ + cbc.Key("test"): "foo", + } + assert.False(t, m1.Equals(m2)) + + m2 = cbc.Meta{ + cbc.Key("test"): "bar", + cbc.Key("foo"): "bar", + } + assert.False(t, m1.Equals(m2)) +} diff --git a/cbc/notes.go b/cbc/notes.go index 2ea7bd95..5a0e1aa1 100644 --- a/cbc/notes.go +++ b/cbc/notes.go @@ -285,6 +285,22 @@ func (n *Note) WithSrc(src Key) *Note { return &nw } +// WithCode provides a new copy of the note with the code set. +func (n *Note) WithCode(code string) *Note { + nw := *n // copy + nw.Code = code + return &nw +} + +// Equals returns true if the provided note is the same as the current one. +func (n *Note) Equals(n2 *Note) bool { + return n.Key == n2.Key && + n.Code == n2.Code && + n.Src == n2.Src && + n.Text == n2.Text && + n.Meta.Equals(n2.Meta) +} + // JSONSchemaExtend adds the list of definitions for the notes. func (Note) JSONSchemaExtend(schema *jsonschema.Schema) { ks, _ := schema.Properties.Get("key") diff --git a/data/regimes/it.json b/data/regimes/it.json index 0acbd568..23e2dc93 100644 --- a/data/regimes/it.json +++ b/data/regimes/it.json @@ -991,72 +991,76 @@ "schema": "bill/invoice", "list": [ { - "type": [ - "standard" - ], "name": { "en": "Regular Invoice", "it": "Fattura" }, + "type": [ + "standard" + ], "codes": { "fatturapa-tipo-documento": "TD01" } }, { + "name": { + "en": "Advance or down payment on invoice", + "it": "Acconto / anticipo su fattura" + }, "type": [ "standard" ], "tags": [ "partial" ], - "name": { - "en": "Advance or down payment on invoice", - "it": "Acconto / anticipo su fattura" - }, "codes": { "fatturapa-tipo-documento": "TD02" } }, { - "type": [ - "credit-note" - ], "name": { "en": "Credit Note", "it": "Nota di credito" }, + "type": [ + "credit-note" + ], "codes": { "fatturapa-tipo-documento": "TD04" } }, { - "type": [ - "debit-note" - ], "name": { "en": "Debit Note", "it": "Nota di debito" }, + "type": [ + "debit-note" + ], "codes": { "fatturapa-tipo-documento": "TD05" } }, { + "name": { + "en": "Freelancer invoice with retained taxes", + "it": "Parcella" + }, "type": [ "standard" ], "tags": [ "freelance" ], - "name": { - "en": "Freelancer invoice with retained taxes", - "it": "Parcella" - }, "codes": { "fatturapa-tipo-documento": "TD06" } }, { + "name": { + "en": "Advance or down payment on freelance invoice", + "it": "Acconto / anticipo su parcella" + }, "type": [ "standard" ], @@ -1064,75 +1068,75 @@ "partial", "freelance" ], - "name": { - "en": "Advance or down payment on freelance invoice", - "it": "Acconto / anticipo su parcella" - }, "codes": { "fatturapa-tipo-documento": "TD03" } }, { + "name": { + "en": "Simplified Invoice", + "it": "Fattura Semplificata" + }, "type": [ "standard" ], "tags": [ "simplified" ], - "name": { - "en": "Simplified Invoice", - "it": "Fattura Semplificata" - }, "codes": { "fatturapa-tipo-documento": "TD07" } }, { + "name": { + "en": "Simplified Credit Note", + "it": "Nota di credito semplificata" + }, "type": [ "credit-note" ], "tags": [ "simplified" ], - "name": { - "en": "Simplified Credit Note", - "it": "Nota di credito semplificata" - }, "codes": { "fatturapa-tipo-documento": "TD08" } }, { + "name": { + "en": "Simplified Debit Note", + "it": "Nota di debito semplificata" + }, "type": [ "debit-note" ], "tags": [ "simplified" ], - "name": { - "en": "Simplified Debit Note", - "it": "Nota di debito semplificata" - }, "codes": { "fatturapa-tipo-documento": "TD09" } }, { + "name": { + "en": "Self-billed for self consumption or for free transfer without recourse", + "it": "Fattura per autoconsumo o per cessioni gratuite senza rivalsa" + }, "type": [ "standard" ], "tags": [ "self-billed" ], - "name": { - "en": "Self-billed for self consumption or for free transfer without recourse", - "it": "Fattura per autoconsumo o per cessioni gratuite senza rivalsa" - }, "codes": { "fatturapa-tipo-documento": "TD27" } }, { + "name": { + "en": "Reverse charge", + "it": "Integrazione fattura reverse charge interno" + }, "type": [ "standard" ], @@ -1140,15 +1144,15 @@ "self-billed", "reverse-charge" ], - "name": { - "en": "Reverse charge", - "it": "Integrazione fattura reverse charge interno" - }, "codes": { "fatturapa-tipo-documento": "TD16" } }, { + "name": { + "en": "Self-billed Import", + "it": "Integrazione/autofattura per acquisto servizi da estero" + }, "type": [ "standard" ], @@ -1156,15 +1160,15 @@ "self-billed", "import" ], - "name": { - "en": "Self-billed Import", - "it": "Integrazione/autofattura per acquisto servizi da estero" - }, "codes": { "fatturapa-tipo-documento": "TD17" } }, { + "name": { + "en": "Self-billed EU Goods Import", + "it": "Integrazione per acquisto beni intracomunitari" + }, "type": [ "standard" ], @@ -1173,15 +1177,15 @@ "import", "goods-eu" ], - "name": { - "en": "Self-billed EU Goods Import", - "it": "Integrazione per acquisto beni intracomunitari" - }, "codes": { "fatturapa-tipo-documento": "TD18" } }, { + "name": { + "en": "Self-billed Goods Import", + "it": "Integrazione/autofattura per acquisto beni ex art.17 c.2 DPR 633/72" + }, "type": [ "standard" ], @@ -1190,15 +1194,15 @@ "import", "goods" ], - "name": { - "en": "Self-billed Goods Import", - "it": "Integrazione/autofattura per acquisto beni ex art.17 c.2 DPR 633/72" - }, "codes": { "fatturapa-tipo-documento": "TD19" } }, { + "name": { + "en": "Self-billed Regularization", + "it": "Autofattura per regolarizzazione e integrazione delle fatture - art.6 c.8 d.lgs.471/97 o art.46 c.5 D.L.331/93" + }, "type": [ "standard" ], @@ -1206,15 +1210,15 @@ "self-billed", "regularization" ], - "name": { - "en": "Self-billed Regularization", - "it": "Autofattura per regolarizzazione e integrazione delle fatture - art.6 c.8 d.lgs.471/97 o art.46 c.5 D.L.331/93" - }, "codes": { "fatturapa-tipo-documento": "TD20" } }, { + "name": { + "en": "Self-billed invoice when ceiling exceeded", + "it": "Autofattura per splafonamento" + }, "type": [ "standard" ], @@ -1222,15 +1226,15 @@ "self-billed", "ceiling-exceeded" ], - "name": { - "en": "Self-billed invoice when ceiling exceeded", - "it": "Autofattura per splafonamento" - }, "codes": { "fatturapa-tipo-documento": "TD21" } }, { + "name": { + "en": "Self-billed for goods extracted from VAT warehouse", + "it": "Estrazione beni da Deposito IVA" + }, "type": [ "standard" ], @@ -1238,15 +1242,15 @@ "self-billed", "goods-extracted" ], - "name": { - "en": "Self-billed for goods extracted from VAT warehouse", - "it": "Estrazione beni da Deposito IVA" - }, "codes": { "fatturapa-tipo-documento": "TD22" } }, { + "name": { + "en": "Self-billed for goods extracted from VAT warehouse with VAT payment", + "it": "Estrazione beni da Deposito IVA con versamento IVA" + }, "type": [ "standard" ], @@ -1254,30 +1258,30 @@ "self-billed", "goods-with-tax" ], - "name": { - "en": "Self-billed for goods extracted from VAT warehouse with VAT payment", - "it": "Estrazione beni da Deposito IVA con versamento IVA" - }, "codes": { "fatturapa-tipo-documento": "TD23" } }, { + "name": { + "en": "Deferred invoice ex art.21, c.4, lett. a) DPR 633/72", + "it": "Fattura differita - art.21 c.4 lett. a" + }, "type": [ "standard" ], "tags": [ "deferred" ], - "name": { - "en": "Deferred invoice ex art.21, c.4, lett. a) DPR 633/72", - "it": "Fattura differita - art.21 c.4 lett. a" - }, "codes": { "fatturapa-tipo-documento": "TD24" } }, { + "name": { + "en": "Deferred invoice ex art.21, c.4, third period lett. b) DPR 633/72", + "it": "Fattura differita - art.21 c.4 terzo periodo lett. b" + }, "type": [ "standard" ], @@ -1285,30 +1289,30 @@ "deferred", "third-period" ], - "name": { - "en": "Deferred invoice ex art.21, c.4, third period lett. b) DPR 633/72", - "it": "Fattura differita - art.21 c.4 terzo periodo lett. b" - }, "codes": { "fatturapa-tipo-documento": "TD25" } }, { + "name": { + "en": "Sale of depreciable assets and for internal transfers (ex art.36 DPR 633/72", + "it": "Cessione di beni ammortizzabili e per passaggi interni - art.36 DPR 633/72" + }, "type": [ "standard" ], "tags": [ "depreciable-assets" ], - "name": { - "en": "Sale of depreciable assets and for internal transfers (ex art.36 DPR 633/72", - "it": "Cessione di beni ammortizzabili e per passaggi interni - art.36 DPR 633/72" - }, "codes": { "fatturapa-tipo-documento": "TD26" } }, { + "name": { + "en": "Purchases from San Marino with VAT (paper invoice)", + "it": "Acquisti da San Marino con IVA (fattura cartacea)" + }, "type": [ "standard" ], @@ -1316,10 +1320,6 @@ "self-billed", "san-marino-paper" ], - "name": { - "en": "Purchases from San Marino with VAT (paper invoice)", - "it": "Acquisti da San Marino con IVA (fattura cartacea)" - }, "codes": { "fatturapa-tipo-documento": "TD28" } diff --git a/data/regimes/pl.json b/data/regimes/pl.json index e7323d65..5d1a0deb 100644 --- a/data/regimes/pl.json +++ b/data/regimes/pl.json @@ -237,100 +237,100 @@ "schema": "bill/invoice", "list": [ { - "type": [ - "standard" - ], "name": { "en": "Regular Invoice", "pl": "Faktura Podstawowa" }, + "type": [ + "standard" + ], "codes": { "favat-rodzaj-faktury": "VAT" } }, { + "name": { + "en": "Prepayment Invoice", + "pl": "Faktura Zaliczkowa" + }, "type": [ "standard" ], "tags": [ "partial" ], - "name": { - "en": "Prepayment Invoice", - "pl": "Faktura Zaliczkowa" - }, "codes": { "favat-rodzaj-faktury": "ZAL" } }, { + "name": { + "en": "Settlement Invoice", + "pl": "Faktura Rozliczeniowa" + }, "type": [ "standard" ], "tags": [ "settlement" ], - "name": { - "en": "Settlement Invoice", - "pl": "Faktura Rozliczeniowa" - }, "codes": { "favat-rodzaj-faktury": "ROZ" } }, { + "name": { + "en": "Simplified Invoice", + "pl": "Faktura Uproszczona" + }, "type": [ "standard" ], "tags": [ "simplified" ], - "name": { - "en": "Simplified Invoice", - "pl": "Faktura Uproszczona" - }, "codes": { "favat-rodzaj-faktury": "UPR" } }, { - "type": [ - "credit-note" - ], "name": { "en": "Credit note", "pl": "Faktura korygująca" }, + "type": [ + "credit-note" + ], "codes": { "favat-rodzaj-faktury": "KOR" } }, { + "name": { + "en": "Prepayment credit note", + "pl": "Faktura korygująca fakturę zaliczkową" + }, "type": [ "credit-note" ], "tags": [ "partial" ], - "name": { - "en": "Prepayment credit note", - "pl": "Faktura korygująca fakturę zaliczkową" - }, "codes": { "favat-rodzaj-faktury": "KOR_ZAL" } }, { + "name": { + "en": "Settlement credit note", + "pl": "Faktura korygująca fakturę rozliczeniową" + }, "type": [ "credit-note" ], "tags": [ "settlement" ], - "name": { - "en": "Settlement credit note", - "pl": "Faktura korygująca fakturę rozliczeniową" - }, "codes": { "favat-rodzaj-faktury": "KOR_ROZ" } diff --git a/data/regimes/pt.json b/data/regimes/pt.json index 02e84bc4..64adb4e6 100644 --- a/data/regimes/pt.json +++ b/data/regimes/pt.json @@ -348,13 +348,75 @@ } }, { - "tags": [ - "reverse-charge" - ], + "ext_key": "pt-exemption-code", + "ext_value": "M30", + "note": { + "key": "legal", + "src": "pt-exemption-code", + "text": "Reverse charge / autoliquidação - Artigo 2.° n.° 1 alínea i) do Código do IVA" + } + }, + { + "ext_key": "pt-exemption-code", + "ext_value": "M31", + "note": { + "key": "legal", + "src": "pt-exemption-code", + "text": "Reverse charge / autoliquidação - Artigo 2.° n.° 1 alínea j) do Código do IVA" + } + }, + { + "ext_key": "pt-exemption-code", + "ext_value": "M32", + "note": { + "key": "legal", + "src": "pt-exemption-code", + "text": "Reverse charge / autoliquidação - Artigo 2.° n.° 1 alínea I) do Código do IVA" + } + }, + { + "ext_key": "pt-exemption-code", + "ext_value": "M33", + "note": { + "key": "legal", + "src": "pt-exemption-code", + "text": "Reverse charge / autoliquidação - Artigo 2.° n.° 1 alínea m) do Código do IVA" + } + }, + { + "ext_key": "pt-exemption-code", + "ext_value": "M40", + "note": { + "key": "legal", + "src": "pt-exemption-code", + "text": "Reverse charge / Autoliquidação - Artigo 6.º n.º 6 alínea a) do Código do IVA, a contrário" + } + }, + { + "ext_key": "pt-exemption-code", + "ext_value": "M41", + "note": { + "key": "legal", + "src": "pt-exemption-code", + "text": "Reverse charge / Autoliquidação - Artigo 8.º n.º 3 do RITI" + } + }, + { + "ext_key": "pt-exemption-code", + "ext_value": "M42", + "note": { + "key": "legal", + "src": "pt-exemption-code", + "text": "Reverse charge / Autoliquidação - Decreto-Lei n.º 21/2007, de 29 de janeiro" + } + }, + { + "ext_key": "pt-exemption-code", + "ext_value": "M43", "note": { "key": "legal", - "src": "reverse-charge", - "text": "Reverse charge / Autoliquidação - Artigo 2.º n.º 1 alínea j) do Código do IVA" + "src": "pt-exemption-code", + "text": "Reverse charge / Autoliquidação - Decreto-Lei n.° 362/99, de 16 de setembro" } } ] diff --git a/data/schemas/tax/regime.json b/data/schemas/tax/regime.json index 84309dbf..29b02b8e 100644 --- a/data/schemas/tax/regime.json +++ b/data/schemas/tax/regime.json @@ -342,6 +342,11 @@ }, "Scenario": { "properties": { + "name": { + "$ref": "https://gobl.org/draft-0/i18n/string", + "title": "Name", + "description": "Name of the scenario for further information." + }, "type": { "items": { "$ref": "https://gobl.org/draft-0/cbc/key" @@ -358,10 +363,15 @@ "title": "Tag", "description": "Tag that was applied to the document" }, - "name": { - "$ref": "https://gobl.org/draft-0/i18n/string", - "title": "Name", - "description": "Name of the scenario for further information." + "ext_key": { + "$ref": "https://gobl.org/draft-0/cbc/key", + "title": "Extension Key", + "description": "Extension key that must be present in the document." + }, + "ext_value": { + "type": "string", + "title": "Extension Value", + "description": "Extension value that along side the key must be present for a match\nto happen. This cannot be used without an `ExtKey`. The value will\nbe copied to the note code if needed." }, "note": { "$ref": "https://gobl.org/draft-0/cbc/note", diff --git a/regimes/pt/examples/invoice-exempt.yaml b/regimes/pt/examples/invoice-exempt.yaml new file mode 100644 index 00000000..19d42647 --- /dev/null +++ b/regimes/pt/examples/invoice-exempt.yaml @@ -0,0 +1,29 @@ +$schema: https://gobl.org/draft-0/bill/invoice +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" +type: standard +currency: EUR +issue_date: "2023-01-30" +supplier: + uuid: 9de7584f-ea5c-42a7-b159-5e4c6a280a5c + tax_id: + country: PT + code: "545259045" + name: Hotelzinho + addresses: + - street: Rua do Hotelzinho + code: 1000-000 + locality: Lisboa +customer: + name: Maria Santos Silva +lines: + - i: 1 + quantity: 1 + item: + name: Noite em quarto duplo + price: 100.00 + sum: 100.00 + taxes: + - cat: VAT + rate: exempt + ext: + pt-exemption-code: "M40" diff --git a/regimes/pt/examples/out/invoice-exempt.json b/regimes/pt/examples/out/invoice-exempt.json new file mode 100644 index 00000000..752f9df5 --- /dev/null +++ b/regimes/pt/examples/out/invoice-exempt.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "3855300fedceccd979f3505677edbd4125e771bec7e4ea9bee5f0b1953a866bf" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", + "type": "standard", + "code": "", + "issue_date": "2023-01-30", + "currency": "EUR", + "supplier": { + "uuid": "9de7584f-ea5c-42a7-b159-5e4c6a280a5c", + "name": "Hotelzinho", + "tax_id": { + "country": "PT", + "code": "545259045" + }, + "addresses": [ + { + "street": "Rua do Hotelzinho", + "locality": "Lisboa", + "code": "1000-000" + } + ] + }, + "customer": { + "name": "Maria Santos Silva" + }, + "lines": [ + { + "i": 1, + "quantity": "1", + "item": { + "name": "Noite em quarto duplo", + "price": "100.00" + }, + "sum": "100.00", + "taxes": [ + { + "cat": "VAT", + "rate": "exempt", + "ext": { + "pt-exemption-code": "M40" + } + } + ], + "total": "100.00" + } + ], + "totals": { + "sum": "100.00", + "total": "100.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "exempt", + "ext": { + "pt-exemption-code": "M40" + }, + "base": "100.00", + "amount": "0.00" + } + ], + "amount": "0.00" + } + ], + "sum": "0.00" + }, + "tax": "0.00", + "total_with_tax": "100.00", + "payable": "100.00" + }, + "notes": [ + { + "key": "legal", + "code": "M40", + "src": "pt-exemption-code", + "text": "Reverse charge / Autoliquidação - Artigo 6.º n.º 6 alínea a) do Código do IVA, a contrário" + } + ] + } +} \ No newline at end of file diff --git a/regimes/pt/scenarios.go b/regimes/pt/scenarios.go index bedb592f..5fa16bbd 100644 --- a/regimes/pt/scenarios.go +++ b/regimes/pt/scenarios.go @@ -63,13 +63,78 @@ var invoiceScenarios = &tax.ScenarioSet{ }, }, - // Reverse Charges + // Extension texts + + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M30", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Reverse charge / autoliquidação - Artigo 2.° n.° 1 alínea i) do Código do IVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M31", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Reverse charge / autoliquidação - Artigo 2.° n.° 1 alínea j) do Código do IVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M32", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Reverse charge / autoliquidação - Artigo 2.° n.° 1 alínea I) do Código do IVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M33", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Reverse charge / autoliquidação - Artigo 2.° n.° 1 alínea m) do Código do IVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M40", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Reverse charge / Autoliquidação - Artigo 6.º n.º 6 alínea a) do Código do IVA, a contrário", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M41", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Reverse charge / Autoliquidação - Artigo 8.º n.º 3 do RITI", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M42", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Reverse charge / Autoliquidação - Decreto-Lei n.º 21/2007, de 29 de janeiro", + }, + }, { - Tags: []cbc.Key{tax.TagReverseCharge}, + ExtKey: ExtKeyExemptionCode, + ExtValue: "M43", Note: &cbc.Note{ Key: cbc.NoteKeyLegal, - Src: tax.TagReverseCharge, - Text: "Reverse charge / Autoliquidação - Artigo 2.º n.º 1 alínea j) do Código do IVA", + Src: ExtKeyExemptionCode, + Text: "Reverse charge / Autoliquidação - Decreto-Lei n.° 362/99, de 16 de setembro", }, }, }, diff --git a/tax/scenario.go b/tax/scenario.go index ff47048b..2cae2262 100644 --- a/tax/scenario.go +++ b/tax/scenario.go @@ -19,15 +19,31 @@ type ScenarioSet struct { // Scenario is used to describe a tax scenario of a document based on the combination // of document type and tag used. +// +// There are effectively two parts to a scenario, the filters that are used to determine +// if the scenario is applicable to a document and the output that is applied or data to +// be used by conversion processes. type Scenario struct { + // Name of the scenario for further information. + Name i18n.String `json:"name,omitempty" jsonschema:"title=Name"` + + /* Filters */ + // Type of document, if present. Types []cbc.Key `json:"type,omitempty" jsonschema:"title=Type"` // Tag that was applied to the document Tags []cbc.Key `json:"tags,omitempty" jsonschema:"title=Tag"` - // Name of the scenario for further information. - Name i18n.String `json:"name,omitempty" jsonschema:"title=Name"` + // Extension key that must be present in the document. + ExtKey cbc.Key `json:"ext_key,omitempty" jsonschema:"title=Extension Key"` + + // Extension value that along side the key must be present for a match + // to happen. This cannot be used without an `ExtKey`. The value will + // be copied to the note code if needed. + ExtValue ExtValue `json:"ext_value,omitempty" jsonschema:"title=Extension Value"` + + /* Outputs */ // A note to be added to the document if the scenario is applied. Note *cbc.Note `json:"note,omitempty" jsonschema:"title=Note"` @@ -60,16 +76,16 @@ func (ss *ScenarioSet) ValidateWithContext(ctx context.Context) error { // SummaryFor returns a summary by applying the scenarios to the // supplied document. -func (ss *ScenarioSet) SummaryFor(docType cbc.Key, docTags []cbc.Key) *ScenarioSummary { +func (ss *ScenarioSet) SummaryFor(docType cbc.Key, docTags []cbc.Key, docExt []Extensions) *ScenarioSummary { summary := &ScenarioSummary{ Notes: make([]*cbc.Note, 0), Codes: make(cbc.CodeMap), Meta: make(cbc.Meta), } for _, s := range ss.List { - if s.match(docType, docTags) { + if s.match(docType, docTags, docExt) { if s.Note != nil { - summary.Notes = append(summary.Notes, s.Note) + summary.addNote(s.Note.WithCode(s.ExtValue.String())) } for k, v := range s.Codes { summary.Codes[k] = v @@ -82,9 +98,20 @@ func (ss *ScenarioSet) SummaryFor(docType cbc.Key, docTags []cbc.Key) *ScenarioS return summary } +func (ss *ScenarioSummary) addNote(note *cbc.Note) { + for _, n := range ss.Notes { + if n.Equals(note) { + return // already added + } + } + ss.Notes = append(ss.Notes, note) +} + // match checks if the scenario has a matching doc type or set of tags. // Empty types or tags in the scenario implies that all values are valid. -func (s *Scenario) match(docType cbc.Key, docTags []cbc.Key) bool { +// The list of extensions can contain duplicate extension maps to make recompilation +// of the array easier. +func (s *Scenario) match(docType cbc.Key, docTags []cbc.Key, docExt []Extensions) bool { if len(s.Types) > 0 { if !s.hasType(docType) { return false @@ -95,6 +122,25 @@ func (s *Scenario) match(docType cbc.Key, docTags []cbc.Key) bool { return false } } + if s.ExtKey != cbc.KeyEmpty { + // For extensions we need to find a complete match + // and reject if none found. We intentionally don't try + // to combine extensions from the document. + for _, ext := range docExt { + v, ok := ext[s.ExtKey] + if !ok { + continue // try next extension + } + if s.ExtValue != "" { + if v == s.ExtValue { + return true + } + } else { + return true + } + } + return false + } return true } From 9c0c3c5f715d3efac41bbffe59c93cfb190fbd70 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 26 Jul 2024 13:05:33 +0000 Subject: [PATCH 2/5] Covering all PT exemption codes --- regimes/pt/scenarios.go | 172 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 1 deletion(-) diff --git a/regimes/pt/scenarios.go b/regimes/pt/scenarios.go index 5fa16bbd..d798a46d 100644 --- a/regimes/pt/scenarios.go +++ b/regimes/pt/scenarios.go @@ -64,7 +64,168 @@ var invoiceScenarios = &tax.ScenarioSet{ }, // Extension texts - + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M01", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Artigo 16.°, n.° 6 do CIVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M02", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Artigo 6.° do Decreto-Lei n.° 198/90, de 19 de junho", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M04", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Exempt / Isento artigo 13.° do CIVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M05", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Exempt / Isento artigo 14.° do CIVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M06", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Exempt / Isento artigo 15.° do CIVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M07", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Exempt / Isento artigo 9.° do CIVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M09", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Não confere direito a dedução / Artigo 62.° alínea b) do CIVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M10", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Regime de isenção / Artigo 57.° do CIVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M11", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Regime particular do tabaco / Decreto-Lei n.° 346/85, de 23 de agosto", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M12", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Regime da margem de lucro - Agências de viagens / Decreto-Lei n.° 221/85, de 3 de julho", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M13", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Regime da margem de lucro - Bens em segunda mão / Decreto-Lei n.° 199/96, de 18 de outubro", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M14", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Regime da margem de lucro - Objetos de arte / Decreto-Lei n.° 199/96, de 18 de outubro", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M15", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Regime da margem de lucro - Objetos de coleção e antiguidades / Decreto-Lei n.° 199/96, de 18 de outubro", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M16", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Exempt / Isento artigo 14.° do RITI", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M19", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Outras isenções - Isenções temporárias determinadas em diploma próprio", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M20", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Regime forfetário / Artigo 59.°-D n.°2 do CIVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M21", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Não confere direito à dedução (ou expressão similar) - Artigo 72.° n.° 4 do CIVA", + }, + }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M25", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Mercadorias à consignação - Artigo 38.° n.° 1 alínea a) do CIVA", + }, + }, { ExtKey: ExtKeyExemptionCode, ExtValue: "M30", @@ -137,5 +298,14 @@ var invoiceScenarios = &tax.ScenarioSet{ Text: "Reverse charge / Autoliquidação - Decreto-Lei n.° 362/99, de 16 de setembro", }, }, + { + ExtKey: ExtKeyExemptionCode, + ExtValue: "M99", + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: ExtKeyExemptionCode, + Text: "Não sujeito ou não tributado", + }, + }, }, } From 858fb2f5d26ae364f281c6a396ae7d1c0203723e Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 26 Jul 2024 13:09:55 +0000 Subject: [PATCH 3/5] Updating the Changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b96bd1..ec3d803b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] +### Added + +- `tax`: Scenarios now handle extension key and value for filtering. +- PT: exemption text handling moved to scenarios. + ## [v0.111.1] - 2024-07-25 ### Added From 9a4a34f24d355dd27c3260ae6a081f0378b1c8ac Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 26 Jul 2024 14:59:56 +0000 Subject: [PATCH 4/5] Updating example --- regimes/pt/examples/invoice-exempt.yaml | 13 +++++++-- regimes/pt/examples/out/invoice-exempt.json | 31 +++++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/regimes/pt/examples/invoice-exempt.yaml b/regimes/pt/examples/invoice-exempt.yaml index 19d42647..a3cc4f11 100644 --- a/regimes/pt/examples/invoice-exempt.yaml +++ b/regimes/pt/examples/invoice-exempt.yaml @@ -16,12 +16,19 @@ supplier: customer: name: Maria Santos Silva lines: - - i: 1 - quantity: 1 + - quantity: 1 item: name: Noite em quarto duplo price: 100.00 - sum: 100.00 + taxes: + - cat: VAT + rate: exempt + ext: + pt-exemption-code: "M40" + - quantity: 2 + item: + name: Noite em quarto triplo + price: 120.00 taxes: - cat: VAT rate: exempt diff --git a/regimes/pt/examples/out/invoice-exempt.json b/regimes/pt/examples/out/invoice-exempt.json index 752f9df5..47bb0485 100644 --- a/regimes/pt/examples/out/invoice-exempt.json +++ b/regimes/pt/examples/out/invoice-exempt.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "3855300fedceccd979f3505677edbd4125e771bec7e4ea9bee5f0b1953a866bf" + "val": "4edbfc19235a0f031273f35014dafe6055a63f62006b320474274516ea927500" }, "draft": true }, @@ -52,11 +52,30 @@ } ], "total": "100.00" + }, + { + "i": 2, + "quantity": "2", + "item": { + "name": "Noite em quarto triplo", + "price": "120.00" + }, + "sum": "240.00", + "taxes": [ + { + "cat": "VAT", + "rate": "exempt", + "ext": { + "pt-exemption-code": "M40" + } + } + ], + "total": "240.00" } ], "totals": { - "sum": "100.00", - "total": "100.00", + "sum": "340.00", + "total": "340.00", "taxes": { "categories": [ { @@ -67,7 +86,7 @@ "ext": { "pt-exemption-code": "M40" }, - "base": "100.00", + "base": "340.00", "amount": "0.00" } ], @@ -77,8 +96,8 @@ "sum": "0.00" }, "tax": "0.00", - "total_with_tax": "100.00", - "payable": "100.00" + "total_with_tax": "340.00", + "payable": "340.00" }, "notes": [ { From d3afe15e02fefd2f301da255b1d0542ec1fd000b Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 26 Jul 2024 15:02:24 +0000 Subject: [PATCH 5/5] Including version bump --- CHANGELOG.md | 2 ++ version.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec3d803b..7efbee45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] +## [v0.112.0] - 2024-07-26 + ### Added - `tax`: Scenarios now handle extension key and value for filtering. diff --git a/version.go b/version.go index 1f0f6d30..d93b5cee 100644 --- a/version.go +++ b/version.go @@ -8,7 +8,7 @@ import ( type Version string // VERSION is the current version of the GOBL library. -const VERSION Version = "v0.111.1" +const VERSION Version = "v0.112.0" // Semver parses and returns semver func (v Version) Semver() *semver.Version {