From 3847418a78c200740869649a5e37ec8f95676f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Fri, 15 Nov 2024 06:42:40 +0000 Subject: [PATCH 1/8] Add extensions and identities for BR suppliers --- CHANGELOG.md | 1 + addons/br/nfse/extensions.go | 121 ++++++++++++++++++++++++++++++++++- addons/br/nfse/identities.go | 29 +++++++++ addons/br/nfse/nfse.go | 1 + 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 addons/br/nfse/identities.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c3b6348..3bbff72f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added - `org`: `Address` includes `LineOne()`, `LineTwo()`, `CompleteNumber()` methods to help with conversion to other formats with some regional formatting. +- `br`: new extensions and identities for suppliers ### Changes diff --git a/addons/br/nfse/extensions.go b/addons/br/nfse/extensions.go index 9bcc34cd..efbb2bc9 100644 --- a/addons/br/nfse/extensions.go +++ b/addons/br/nfse/extensions.go @@ -8,10 +8,55 @@ import ( // Brazilian extension keys required to issue NFS-e documents. const ( - ExtKeyService = "br-nfse-service" + ExtKeyFiscalIncentive = "br-nfse-fiscal-incentive" + ExtKeyMunicipality = "br-nfse-municipality" + ExtKeyService = "br-nfse-service" + ExtKeySimplesNacional = "br-nfse-simples-nacional" + ExtKeySpecialRegime = "br-nfse-special-regime" ) var extensions = []*cbc.KeyDefinition{ + { + Key: ExtKeyFiscalIncentive, + Name: i18n.String{ + i18n.EN: "Fiscal Incentive", + i18n.PT: "Incentivo Fiscal", + }, + Values: []*cbc.ValueDefinition{ + { + Value: "1", + Name: i18n.String{ + i18n.EN: "Has incentive", + i18n.PT: "Possui incentivo", + }, + }, + { + Value: "2", + Name: i18n.String{ + i18n.EN: "Does not have incentive", + i18n.PT: "Não possui incentivo", + }, + }, + }, + }, + { + Key: ExtKeyMunicipality, + Name: i18n.String{ + i18n.EN: "IGBE Municipality Code", + i18n.PT: "Código do Município do IBGE", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + The municipality code as defined by the IGBE (Brazilian Institute of Geography and + Statistics). + + For further details on the list of possible codes, see: + + * https://www.ibge.gov.br/explica/codigos-dos-municipios.php + `), + }, + Pattern: `^\d{7}$`, + }, { Key: ExtKeyService, Name: i18n.String{ @@ -29,4 +74,78 @@ var extensions = []*cbc.KeyDefinition{ `), }, }, + { + Key: ExtKeySimplesNacional, + Name: i18n.String{ + i18n.EN: "Opting for “Simples Nacional”", + i18n.PT: "Optante pelo Simples Nacional", + }, + Values: []*cbc.ValueDefinition{ + { + Value: "1", + Name: i18n.String{ + i18n.EN: "Opt-in", + i18n.PT: "Optante", + }, + }, + { + Value: "2", + Name: i18n.String{ + i18n.EN: "Opt-out", + i18n.PT: "Não optante", + }, + }, + }, + }, + { + Key: ExtKeySpecialRegime, + Name: i18n.String{ + i18n.EN: "Special Tax Regime", + i18n.PT: "Regime Especial de Tributação", + }, + Values: []*cbc.ValueDefinition{ + { + Value: "1", + Name: i18n.String{ + i18n.EN: "Municipal micro-enterprise", + i18n.PT: "Microempresa municipal", + }, + }, + { + Value: "2", + Name: i18n.String{ + i18n.EN: "Estimated", + i18n.PT: "Estimativa", + }, + }, + { + Value: "3", + Name: i18n.String{ + i18n.EN: "Professional Society", + i18n.PT: "Sociedade de profissionais", + }, + }, + { + Value: "4", + Name: i18n.String{ + i18n.EN: "Cooperative", + i18n.PT: "Cooperativa", + }, + }, + { + Value: "5", + Name: i18n.String{ + i18n.EN: "Single micro-entrepreneur (MEI)", + i18n.PT: "Microempreendedor individual (MEI)", + }, + }, + { + Value: "6", + Name: i18n.String{ + i18n.EN: "Micro-enterprise or Small Business (ME EPP)", + i18n.PT: "Microempresa ou Empresa de Pequeno Porte (ME EPP).", + }, + }, + }, + }, } diff --git a/addons/br/nfse/identities.go b/addons/br/nfse/identities.go new file mode 100644 index 00000000..215ce60f --- /dev/null +++ b/addons/br/nfse/identities.go @@ -0,0 +1,29 @@ +package nfse + +import ( + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" +) + +// Brazilian identity keys required to issue NFS-e documents. +const ( + IdentityKeyMunicipalReg = "br-nfse-municipal-reg" + IdentityKeyNationalReg = "br-nfse-national-reg" +) + +var identities = []*cbc.KeyDefinition{ + { + Key: IdentityKeyMunicipalReg, + Name: i18n.String{ + i18n.EN: "Company Municipal Registration", + i18n.PT: "Inscrição Municipal da Empresa", + }, + }, + { + Key: IdentityKeyNationalReg, + Name: i18n.String{ + i18n.EN: "Company National Registration", + i18n.PT: "Inscrição Nacional da Empresa", + }, + }, +} diff --git a/addons/br/nfse/nfse.go b/addons/br/nfse/nfse.go index 5cd373de..cb94c9ac 100644 --- a/addons/br/nfse/nfse.go +++ b/addons/br/nfse/nfse.go @@ -26,6 +26,7 @@ func newAddon() *tax.AddonDef { i18n.EN: "Brazil NFS-e 1.X", }, Extensions: extensions, + Identities: identities, Validator: validate, } } From c8c21d9050a539bacf45ad5ec39d57eec5c3f235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Mon, 18 Nov 2024 06:07:54 +0000 Subject: [PATCH 2/8] Add validations for BR suppliers --- CHANGELOG.md | 2 +- addons/br/nfse/invoices.go | 71 ++++++++++++++++++++++ addons/br/nfse/invoices_test.go | 103 ++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bbff72f..57f3df39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added - `org`: `Address` includes `LineOne()`, `LineTwo()`, `CompleteNumber()` methods to help with conversion to other formats with some regional formatting. -- `br`: new extensions and identities for suppliers +- `br`: supplier extensions, validations & identities ### Changes diff --git a/addons/br/nfse/invoices.go b/addons/br/nfse/invoices.go index 1d39e7a3..2e523511 100644 --- a/addons/br/nfse/invoices.go +++ b/addons/br/nfse/invoices.go @@ -1,16 +1,35 @@ package nfse import ( + "regexp" + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" "github.com/invopop/validation" ) +var ( + validStates = []cbc.Code{ + "AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", + "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", + "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO", + } + + validAddressCode = regexp.MustCompile(`^(?:\D*\d){8}\D*$`) +) + func validateInvoice(inv *bill.Invoice) error { if inv == nil { return nil } return validation.ValidateStruct(inv, + validation.Field(&inv.Supplier, + validation.By(validateSupplier), + validation.Skip, + ), validation.Field(&inv.Charges, validation.Empty.Error("not supported by nfse"), validation.Skip, @@ -21,3 +40,55 @@ func validateInvoice(inv *bill.Invoice) error { ), ) } + +func validateSupplier(value interface{}) error { + obj, _ := value.(*org.Party) + if obj == nil { + return nil + } + + return validation.ValidateStruct(obj, + validation.Field(&obj.TaxID, + validation.Required, + tax.RequireIdentityCode, + ), + validation.Field(&obj.Identities, + org.RequireIdentityKey(IdentityKeyMunicipalReg), + ), + validation.Field(&obj.Name, validation.Required), + validation.Field(&obj.Addresses, + validation.Required, + validation.Each( + validation.Required, + validation.By(validateSupplierAddress), + ), + ), + validation.Field(&obj.Ext, + tax.ExtensionsRequires( + ExtKeySimplesNacional, + ExtKeyMunicipality, + ), + ), + ) +} + +func validateSupplierAddress(value interface{}) error { + obj, _ := value.(*org.Address) + if obj == nil { + return nil + } + + return validation.ValidateStruct(obj, + validation.Field(&obj.Street, validation.Required), + validation.Field(&obj.Number, validation.Required), + validation.Field(&obj.Locality, validation.Required), + validation.Field(&obj.State, + validation.Required, + validation.In(validStates...), + ), + validation.Field(&obj.Code, + validation.Required, + validation.Match(validAddressCode), + ), + ) +} diff --git a/addons/br/nfse/invoices_test.go b/addons/br/nfse/invoices_test.go index 45f52add..c9eb3a58 100644 --- a/addons/br/nfse/invoices_test.go +++ b/addons/br/nfse/invoices_test.go @@ -6,6 +6,7 @@ import ( "github.com/invopop/gobl/addons/br/nfse" "github.com/invopop/gobl/bill" "github.com/invopop/gobl/num" + "github.com/invopop/gobl/org" "github.com/invopop/gobl/tax" "github.com/stretchr/testify/assert" ) @@ -62,3 +63,105 @@ func TestInvoicesValidation(t *testing.T) { }) } } + +func TestSuppliersValidation(t *testing.T) { + addon := tax.AddonForKey(nfse.V1) + + t.Run("validates supplier", func(t *testing.T) { + sup := new(org.Party) + inv := &bill.Invoice{ + Supplier: sup, + } + err := addon.Validator(inv) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "name: cannot be blank") + } + + sup.Name = "Test" + err = addon.Validator(inv) + if assert.Error(t, err) { + assert.NotContains(t, err.Error(), "name: cannot be blank") + } + }) + + t.Run("validates tax ID", func(t *testing.T) { + sup := new(org.Party) + inv := &bill.Invoice{ + Supplier: sup, + } + err := addon.Validator(inv) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "tax_id: cannot be blank") + } + + sup.TaxID = new(tax.Identity) + err = addon.Validator(inv) + if assert.Error(t, err) { + assert.NotContains(t, err.Error(), "tax_id: cannot be blank") + assert.Contains(t, err.Error(), "tax_id: (code: cannot be blank") + } + + sup.TaxID.Code = "123" + err = addon.Validator(inv) + if assert.Error(t, err) { + assert.NotContains(t, err.Error(), "tax_id: (code: cannot be blank") + } + }) + + t.Run("validates identities", func(t *testing.T) { + sup := new(org.Party) + inv := &bill.Invoice{ + Supplier: sup, + } + err := addon.Validator(inv) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "identities: missing key br-nfse-municipal-reg;") + } + + sup.Identities = append(sup.Identities, &org.Identity{ + Key: nfse.IdentityKeyMunicipalReg, + Code: "12345678", + }) + err = addon.Validator(inv) + if assert.Error(t, err) { + assert.NotContains(t, err.Error(), "identities: missing key br-nfse-municipal-reg;") + } + }) + + t.Run("validates addresses", func(t *testing.T) { + sup := new(org.Party) + inv := &bill.Invoice{ + Supplier: sup, + } + err := addon.Validator(inv) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "addresses: cannot be blank") + } + + sup.Addresses = []*org.Address{nil} + err = addon.Validator(inv) + if assert.Error(t, err) { + assert.NotContains(t, err.Error(), "addresses: cannot be blank") + assert.Contains(t, err.Error(), "addresses: (0: cannot be blank.)") + } + + sup.Addresses[0] = new(org.Address) + err = addon.Validator(inv) + if assert.Error(t, err) { + assert.NotContains(t, err.Error(), "addresses: (0: cannot be blank.)") + assert.Contains(t, err.Error(), "addresses: (0: (code: cannot be blank; locality: cannot be blank; num: cannot be blank; state: cannot be blank; street: cannot be blank.).)") + } + + sup.Addresses[0] = &org.Address{ + Code: "12345678", + Locality: "Test", + Number: "123", + State: "RJ", + Street: "Test", + } + err = addon.Validator(inv) + if assert.Error(t, err) { + assert.NotContains(t, err.Error(), "addresses: (0:") + } + }) +} From da8dc2ee5772e92a209a25ae2e4f477f723aed40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Mon, 18 Nov 2024 06:17:12 +0000 Subject: [PATCH 3/8] Improve BR regime examples and docs --- addons/br/nfse/extensions.go | 27 +++++ data/addons/br-nfse-v1.json | 133 +++++++++++++++++++++++ examples/br/invoice-services.json | 86 +++++++++++++++ examples/br/invoice-services.yaml | 40 ------- examples/br/out/invoice-services.json | 100 ++++++++++------- regimes/br/README.md | 148 +++++++++++++++++++++++++- 6 files changed, 454 insertions(+), 80 deletions(-) create mode 100644 examples/br/invoice-services.json delete mode 100644 examples/br/invoice-services.yaml diff --git a/addons/br/nfse/extensions.go b/addons/br/nfse/extensions.go index efbb2bc9..9e40e4f0 100644 --- a/addons/br/nfse/extensions.go +++ b/addons/br/nfse/extensions.go @@ -38,6 +38,15 @@ var extensions = []*cbc.KeyDefinition{ }, }, }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Indicates whether a party benefits from a fiscal incentive. + + List of codes taken from the national NFSe standard: + https://abrasf.org.br/biblioteca/arquivos-publicos/nfs-e-manual-de-orientacao-do-contribuinte-2-04/download + (Section 10.2, Field B-68) + `), + }, }, { Key: ExtKeyMunicipality, @@ -96,6 +105,15 @@ var extensions = []*cbc.KeyDefinition{ }, }, }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Indicates whether a party is opting for the “Simples Nacional” tax regime. + + List of codes taken from the national NFSe standard: + https://abrasf.org.br/biblioteca/arquivos-publicos/nfs-e-manual-de-orientacao-do-contribuinte-2-04/download + (Section 10.2, Field B-67) + `), + }, }, { Key: ExtKeySpecialRegime, @@ -147,5 +165,14 @@ var extensions = []*cbc.KeyDefinition{ }, }, }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Indicates a special tax regime that the party is subject to. + + List of codes taken from the national NFSe standard: + https://abrasf.org.br/biblioteca/arquivos-publicos/nfs-e-manual-de-orientacao-do-contribuinte-2-04/download + (Section 10.2, Field B-66) + `), + }, }, } diff --git a/data/addons/br-nfse-v1.json b/data/addons/br-nfse-v1.json index c310e792..062f8285 100644 --- a/data/addons/br-nfse-v1.json +++ b/data/addons/br-nfse-v1.json @@ -5,6 +5,43 @@ "en": "Brazil NFS-e 1.X" }, "extensions": [ + { + "key": "br-nfse-fiscal-incentive", + "name": { + "en": "Fiscal Incentive", + "pt": "Incentivo Fiscal" + }, + "desc": { + "en": "Indicates whether a party benefits from a fiscal incentive.\n\nList of codes taken from the national NFSe standard:\nhttps://abrasf.org.br/biblioteca/arquivos-publicos/nfs-e-manual-de-orientacao-do-contribuinte-2-04/download\n(Section 10.2, Field B-68)" + }, + "values": [ + { + "value": "1", + "name": { + "en": "Has incentive", + "pt": "Possui incentivo" + } + }, + { + "value": "2", + "name": { + "en": "Does not have incentive", + "pt": "Não possui incentivo" + } + } + ] + }, + { + "key": "br-nfse-municipality", + "name": { + "en": "IGBE Municipality Code", + "pt": "Código do Município do IBGE" + }, + "desc": { + "en": "The municipality code as defined by the IGBE (Brazilian Institute of Geography and\nStatistics).\n\nFor further details on the list of possible codes, see:\n\n* https://www.ibge.gov.br/explica/codigos-dos-municipios.php" + }, + "pattern": "^\\d{7}$" + }, { "key": "br-nfse-service", "name": { @@ -14,8 +51,104 @@ "desc": { "en": "The service code as defined by the municipality. Typically, one of the codes listed\nin the Lei Complementar 116/2003, but municipalities can make their own changes.\n\nFor further details on the list of possible codes, see:\n\n* https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp116.htm" } + }, + { + "key": "br-nfse-simples-nacional", + "name": { + "en": "Opting for “Simples Nacional”", + "pt": "Optante pelo Simples Nacional" + }, + "desc": { + "en": "Indicates whether a party is opting for the “Simples Nacional” tax regime.\n\nList of codes taken from the national NFSe standard:\nhttps://abrasf.org.br/biblioteca/arquivos-publicos/nfs-e-manual-de-orientacao-do-contribuinte-2-04/download\n(Section 10.2, Field B-67)" + }, + "values": [ + { + "value": "1", + "name": { + "en": "Opt-in", + "pt": "Optante" + } + }, + { + "value": "2", + "name": { + "en": "Opt-out", + "pt": "Não optante" + } + } + ] + }, + { + "key": "br-nfse-special-regime", + "name": { + "en": "Special Tax Regime", + "pt": "Regime Especial de Tributação" + }, + "desc": { + "en": "Indicates a special tax regime that the party is subject to.\n\nList of codes taken from the national NFSe standard:\nhttps://abrasf.org.br/biblioteca/arquivos-publicos/nfs-e-manual-de-orientacao-do-contribuinte-2-04/download\n(Section 10.2, Field B-66)" + }, + "values": [ + { + "value": "1", + "name": { + "en": "Municipal micro-enterprise", + "pt": "Microempresa municipal" + } + }, + { + "value": "2", + "name": { + "en": "Estimated", + "pt": "Estimativa" + } + }, + { + "value": "3", + "name": { + "en": "Professional Society", + "pt": "Sociedade de profissionais" + } + }, + { + "value": "4", + "name": { + "en": "Cooperative", + "pt": "Cooperativa" + } + }, + { + "value": "5", + "name": { + "en": "Single micro-entrepreneur (MEI)", + "pt": "Microempreendedor individual (MEI)" + } + }, + { + "value": "6", + "name": { + "en": "Micro-enterprise or Small Business (ME EPP)", + "pt": "Microempresa ou Empresa de Pequeno Porte (ME EPP)." + } + } + ] } ], "scenarios": null, + "identities": [ + { + "key": "br-nfse-municipal-reg", + "name": { + "en": "Company Municipal Registration", + "pt": "Inscrição Municipal da Empresa" + } + }, + { + "key": "br-nfse-national-reg", + "name": { + "en": "Company National Registration", + "pt": "Inscrição Nacional da Empresa" + } + } + ], "corrections": null } \ No newline at end of file diff --git a/examples/br/invoice-services.json b/examples/br/invoice-services.json new file mode 100644 index 00000000..b99630d0 --- /dev/null +++ b/examples/br/invoice-services.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://gobl.org/draft-0/bill/invoice", + "uuid": "0193305c-a7be-730d-b552-2093f0df2df7", + "series": "SAMPLE", + "code": "1", + "issue_date": "2024-11-15", + "currency": "BRL", + "supplier": { + "name": "SaúdeMais Serviços Médicos Ltda.", + "tax_id": { + "country": "BR", + "code": "55263640000186" + }, + "identities": [ + { + "key": "br-nfse-municipal-reg", + "code": "45678901234567" + }, + { + "key": "br-nfse-national-reg", + "code": "12345012345678" + } + ], + "addresses": [ + { + "num": "200", + "street": "Rua Primeiro de Março", + "street_extra": "Torre A", + "locality": "Centro", + "region": "Rio de Janeiro", + "state": "RJ", + "code": "20010-000", + "country": "BR" + } + ], + "emails": [ + { + "addr": "saudemais@example.com" + } + ], + "ext": { + "br-nfse-municipality": "3304557", + "br-nfse-fiscal-incentive": "1", + "br-nfse-simples-nacional": "1", + "br-nfse-special-regime": "4" + } + }, + "customer": { + "name": "Construforte Engenharia Ltda.", + "tax_id": { + "country": "BR", + "code": "46602178000103" + }, + "addresses": [ + { + "num": "75", + "street": "Avenida Sete de Setembro", + "street_extra": "Bloco C", + "locality": "Centro", + "region": "Salvador", + "state": "BA", + "code": "40060-000", + "country": "BR" + } + ], + }, + "lines": [ + { + "i": 1, + "quantity": "15", + "item": { + "name": "Consultancy Services", + "price": "100.00", + "ext": { + "br-nfse-service": "10.5" + } + }, + "taxes": [ + { + "cat": "ISS", + "percent": "15%" + } + ] + } + ] +} diff --git a/examples/br/invoice-services.yaml b/examples/br/invoice-services.yaml deleted file mode 100644 index 626b237e..00000000 --- a/examples/br/invoice-services.yaml +++ /dev/null @@ -1,40 +0,0 @@ -$schema: "https://gobl.org/draft-0/bill/invoice" -uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" -currency: "BRL" -issue_date: "2023-04-21" -series: "SAMPLE" -code: "001" - -supplier: - tax_id: - country: "BR" - name: "TechSolutions Brasil Ltda." - emails: - - addr: "supplier_br@example.com" - addresses: - - num: "595" - street: "Rua Haddock Lobo" - locality: "São Paulo" - region: "SP" - code: "01311-000" - country: "BR" - -customer: - name: "Sample Consumer" - emails: - - addr: "customer_br@example.com" - -lines: - - quantity: 20 - item: - name: "Development services" - price: "90.00" - unit: "h" - ext: - br-nfse-service: "1.01" - discounts: - - percent: "10%" - reason: "Special discount" - taxes: - - cat: ISS - percent: "15%" diff --git a/examples/br/out/invoice-services.json b/examples/br/out/invoice-services.json index 0a94da5b..a8252b84 100644 --- a/examples/br/out/invoice-services.json +++ b/examples/br/out/invoice-services.json @@ -4,98 +4,120 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "edfb9ac9ce7e8ceecb839cbfba109d8350ecf7408038e6eddd008cb294949456" + "val": "b1fc7b37b41dc9df2148365e52dd97cdc3a3847de92d5b7560b3cf40cda10f77" } }, "doc": { "$schema": "https://gobl.org/draft-0/bill/invoice", "$regime": "BR", - "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", + "uuid": "0193305c-a7be-730d-b552-2093f0df2df7", "type": "standard", "series": "SAMPLE", - "code": "001", - "issue_date": "2023-04-21", + "code": "1", + "issue_date": "2024-11-15", "currency": "BRL", "supplier": { - "name": "TechSolutions Brasil Ltda.", + "name": "SaúdeMais Serviços Médicos Ltda.", "tax_id": { - "country": "BR" + "country": "BR", + "code": "55263640000186" }, + "identities": [ + { + "key": "br-nfse-municipal-reg", + "code": "45678901234567" + }, + { + "key": "br-nfse-national-reg", + "code": "12345012345678" + } + ], "addresses": [ { - "num": "595", - "street": "Rua Haddock Lobo", - "locality": "São Paulo", - "region": "SP", - "code": "01311-000", + "num": "200", + "street": "Rua Primeiro de Março", + "street_extra": "Torre A", + "locality": "Centro", + "region": "Rio de Janeiro", + "state": "RJ", + "code": "20010-000", "country": "BR" } ], "emails": [ { - "addr": "supplier_br@example.com" + "addr": "saudemais@example.com" } - ] + ], + "ext": { + "br-nfse-fiscal-incentive": "1", + "br-nfse-municipality": "3304557", + "br-nfse-simples-nacional": "1", + "br-nfse-special-regime": "4" + } }, "customer": { - "name": "Sample Consumer", - "emails": [ + "name": "Construforte Engenharia Ltda.", + "tax_id": { + "country": "BR", + "code": "46602178000103" + }, + "addresses": [ { - "addr": "customer_br@example.com" + "num": "75", + "street": "Avenida Sete de Setembro", + "street_extra": "Bloco C", + "locality": "Centro", + "region": "Salvador", + "state": "BA", + "code": "40060-000", + "country": "BR" } ] }, "lines": [ { "i": 1, - "quantity": "20", + "quantity": "15", "item": { - "name": "Development services", - "price": "90.00", - "unit": "h", + "name": "Consultancy Services", + "price": "100.00", "ext": { - "br-nfse-service": "1.01" + "br-nfse-service": "10.5" } }, - "sum": "1800.00", - "discounts": [ - { - "reason": "Special discount", - "percent": "10%", - "amount": "180.00" - } - ], + "sum": "1500.00", "taxes": [ { "cat": "ISS", "percent": "15%" } ], - "total": "1620.00" + "total": "1500.00" } ], "totals": { - "sum": "1620.00", - "total": "1620.00", + "sum": "1500.00", + "total": "1500.00", "taxes": { "categories": [ { "code": "ISS", "rates": [ { - "base": "1620.00", + "base": "1500.00", "percent": "15%", - "amount": "243.00" + "amount": "225.00" } ], - "amount": "243.00" + "amount": "225.00" } ], - "sum": "243.00" + "sum": "225.00" }, - "tax": "243.00", - "total_with_tax": "1863.00", - "payable": "1863.00" + "tax": "225.00", + "total_with_tax": "1725.00", + "payable": "1725.00" } } } \ No newline at end of file diff --git a/regimes/br/README.md b/regimes/br/README.md index 2b9b0b1f..7543e630 100644 --- a/regimes/br/README.md +++ b/regimes/br/README.md @@ -1,3 +1,149 @@ # 🇧🇷 GOBL Brazil Tax Regime -Example BR GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Brazil uses _Notas Fiscais Eletrônicas_ (like NFSe, NFe or NFCe) for reporting tax information to municipality, state or federal authorities. + +Find example BR GOBL files in the [`examples`](../../examples/br) (uncalculated documents) and [`examples/out`](../../examples/br/out) (calculated envelopes) subdirectories. + +## Public Documentation + +* [NFS-e Technical Documentation at ABRASAF](https://abrasf.org.br/biblioteca/arquivos-publicos/nfs-e) +* [IBGE Municipality Codes](https://www.ibge.gov.br/explica/codigos-dos-municipios.php) + +## Brazil-specific Requirements + +### Addresses + +Brazilian addresses have 3 subdivisions relevant for tax purposes: _bairro_ (neighbourhood), _municipio_ (municipality) and _estado_ (state). Specify them in GOBL addresses like this: + +| GOBL Field | Maps to | Example | +| ---------- | ------------------------ | -------- | +| `locality` | Bairro / Neighbourhood | Centro | +| `region` | Município / Municipality | Salvador | +| `state` | Estado / State | BR | + +For example: + +```js +"supplier": { + //... + "addresses": [ + { + "num": "75", + "street": "Avenida Sete de Setembro", + "street_extra": "Bloco C", + "locality": "Centro", // Bairro + "region": "Salvador", // Municipio + "state": "BA", // State + "code": "40060-000", + "country": "BR" + } + ], + //... +``` + +### Service Notes + +Services notes (NFSe, Notas fiscais de servicio eletrônicas) let service providers document and report the taxes (e.g. ISS) related to the services they provision. Municipal governments regulate them. + +Please also see the [NFSe Addon](../../addons/br/nfse) package named `br-nfse-v1`, which you should include in your documents. + +#### Municipality Code + +Set the party's [IBGE municipality code](https://www.ibge.gov.br/explica/codigos-dos-municipios.php) using the `br-nfse-municipality` extension. + +For example: + +```js +"supplier": { + //... + "ext": { + "br-igbe-municipality": "2927408" + }, + //... +``` + +#### National and municipal registration + +Specify the party's municipal and national registration numbers as identities using the `br-nfse-municipal-reg` and `br-nfse-national-reg` keys. + +For example: + +```js +"supplier": { + //... + "identities": [ + { + "key": "br-nfse-municipal-reg", + "code": "45678901234567" + }, + { + "key": "br-nfse-national-reg", + "code": "12345012345678" + } + ], + //... +``` + +#### “Simples Nacional” + +Report whether the party opts in for the _Simples Nacional_ simplified regime using the `br-nfse-simples-nacional` extension set to any of these codes: + +| Code | Description | +| ---- | ----------- | +| `1` | Opt-in | +| `2` | Opt-out | + +For example: + +```js +"supplier": { + //... + "ext": { + "br-nfse-simples-nacional": "1", // Opt-in + }, + //... +``` + +#### Special Tax Regime + +Specify a special tax regime the party is subject to using the `br-nfse-special-regime` set to any of these codes: + +| Code | Description | +| ---- | ------------------------------------------- | +| `1` | Municipal micro-enterprise | +| `2` | Estimated | +| `3` | Professional Society | +| `4` | Cooperative | +| `5` | Single micro-entrepreneur (MEI) | +| `6` | Micro-enterprise or Small Business (ME EPP) | + +For example: + +```js +"supplier": { + //... + "ext": { + "br-nfse-special-regime": "4" // Cooperative + }, + //... +``` + +#### Fiscal Incentive + +Report whether the party benefits from a fiscal incentive using the `br-nfse-fiscal-incentive` extension set to any of these codes: + +| Code | Description | +| ---- | ----------------------- | +| `1` | Has incentive | +| `2` | Does not have incentive | + +For example: + +```js +"supplier": { + //... + "ext": { + "br-nfse-fiscal-incentive": "2" // No tax incentive + }, + //... +``` From 4683cdc1a62af466ea04a4d4e52d434b59849d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Mon, 18 Nov 2024 06:18:42 +0000 Subject: [PATCH 4/8] Harmonize regime READMEs, fix links --- regimes/ca/README.md | 2 +- regimes/co/README.md | 4 +-- regimes/de/README.md | 2 +- regimes/es/README.md | 6 ++-- regimes/fr/README.md | 6 ++-- regimes/gr/README.md | 4 +-- regimes/it/README.md | 11 ++++--- regimes/mx/README.md | 68 ++++++++++++++++++++++---------------------- regimes/nl/README.md | 1 + regimes/pl/README.md | 4 +-- regimes/pt/README.md | 22 +++++++------- regimes/us/README.md | 2 +- 12 files changed, 70 insertions(+), 62 deletions(-) diff --git a/regimes/ca/README.md b/regimes/ca/README.md index 3fd6bf1f..deba21d5 100644 --- a/regimes/ca/README.md +++ b/regimes/ca/README.md @@ -1,3 +1,3 @@ # 🇨🇦 GOBL Canada Tax Regime -Example Canada GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example CA GOBL files in the [`examples`](../../examples/ca) (uncalculated documents) and [`examples/out`](../../examples/ca/out) (calculated envelopes) subdirectories. diff --git a/regimes/co/README.md b/regimes/co/README.md index 6e30e57b..68181c86 100644 --- a/regimes/co/README.md +++ b/regimes/co/README.md @@ -1,8 +1,8 @@ # 🇨🇴 GOBL Colombia Tax Regime -Example CO GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example CO GOBL files in the [`examples`](../../examples/co) (uncalculated documents) and [`examples/out`](../../examples/co/out) (calculated envelopes) subdirectories. -## Colombia specifics +## Colombia-specific Requirements Please also see the [DIAN Addon](../../addons/co/dian) package named `co-dian-v2` which should be included in your documents. diff --git a/regimes/de/README.md b/regimes/de/README.md index ceb844b6..611ed4a3 100644 --- a/regimes/de/README.md +++ b/regimes/de/README.md @@ -1,3 +1,3 @@ # 🇩🇪 GOBL Germany Tax Regime -Example DE GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example DE GOBL files in the [`examples`](../../examples/de) (uncalculated documents) and [`examples/out`](../../examples/de/out) (calculated envelopes) subdirectories. diff --git a/regimes/es/README.md b/regimes/es/README.md index 0b727040..7b48d08e 100644 --- a/regimes/es/README.md +++ b/regimes/es/README.md @@ -1,8 +1,10 @@ # 🇪🇸 GOBL Spain Tax Regime -Example ES GOBL files can be found in the [`examples`](./examples) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example ES GOBL files in the [`examples`](../../examples/es) (uncalculated documents) and [`examples/out`](../../examples/es/out) (calculated envelopes) subdirectories. -## Corrective Invoices +## Spain-specific Requirements + +### Corrective Invoices According to Spanish law on invoicing [Real Decreto 1619/2012, de 30 de noviembre](https://www.boe.es/buscar/act.php?id=BOE-A-2012-14696), only "rectified" invoices are recognized. There are, in fact, no mentions of international credit or debit notes at all. diff --git a/regimes/fr/README.md b/regimes/fr/README.md index 93943345..260aebc0 100644 --- a/regimes/fr/README.md +++ b/regimes/fr/README.md @@ -1,8 +1,10 @@ # 🇫🇷 GOBL France Tax Regime -Example FR GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example FR GOBL files in the [`examples`](../../examples/fr) (uncalculated documents) and [`examples/out`](../../examples/fr/out) (calculated envelopes) subdirectories. -## Tax IDs +## France-specific Requirements + +### Tax IDs France has three main company IDs which are all very closely related and may be included on Invoice documents: diff --git a/regimes/gr/README.md b/regimes/gr/README.md index 15e86c86..931e3818 100644 --- a/regimes/gr/README.md +++ b/regimes/gr/README.md @@ -2,7 +2,7 @@ Greece uses the myDATA and Peppol BIS Billing 3.0 formats for their e-invoicing/tax-reporting system. -Example GR GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example GR GOBL files in the [`examples`](../../examples/gr) (uncalculated documents) and [`examples/out`](../../examples/gr/out) (calculated envelopes) subdirectories. ## Public Documentation @@ -10,7 +10,7 @@ Example GR GOBL files can be found in the [`examples`](./examples) (YAML uncalcu * [Greek Peppol BIS Billing 3.0](https://www.gsis.gr/sites/default/files/eInvoice/Instructions%20to%20B2G%20Suppliers%20and%20certified%20PEPPOL%20Providers%20for%20the%20Greek%20PEPPOL%20BIS-EN-%20v1.0.pdf) * [VAT Rates](https://www.gov.gr/en/sdg/taxes/vat/general/basic-vat-rates) -## Greece specifics +## Greece-specific Requirements ### Invoice Type diff --git a/regimes/it/README.md b/regimes/it/README.md index a3118bca..bde8715c 100644 --- a/regimes/it/README.md +++ b/regimes/it/README.md @@ -2,7 +2,7 @@ Italy uses the FatturaPA format for their e-invoicing system. -Example IT GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example IT GOBL files in the [`examples`](../../examples/it) (uncalculated documents) and [`examples/out`](../../examples/it/out) (calculated envelopes) subdirectories. ## Public Documentation @@ -12,11 +12,14 @@ Example IT GOBL files can be found in the [`examples`](./examples) (YAML uncalcu - [FatturaPA documentation: FatturPA's website](https://www.fatturapa.gov.it/it/norme-e-regole/documentazione-fattura-elettronica/formato-fatturapa/) #### Ordinary invoices (Fattura Ordinaria) + - [Schema V1.2.3 PDF (IT)](https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2.2/RappresentazioneTabellareFattOrdinariav123.pdf) - most up-to-date but in Italian. - [Schema V1.2.1 Spec Table View (EN)](https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2.1/Table-view-B2B-Ordinary-invoice.pdf) - last version of the table translated to English. Since the difference between 1.2.3 and 1.2.1 is minimal, this is perfectly usable. - [XSD V1.2.2](https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2.2/Schema_del_file_xml_FatturaPA_v1.2.2.xsd) - [FatturaPA filling guide](https://www.agenziaentrate.gov.it/portale/documents/20143/451259/Guida_compilazione-FE-Esterometro-V_1.9_2024-03-05.pdf/67fe4c2d-1174-e8de-f1ee-cea77b7f5203) - useful to understand what values to choose within the extensions (e.g. Natura) + ##### Changes from 1.2.1 to 1.2.3 + - Documentation changes: TD25, N1, N6.2, N7 - Addition of TD28: Acquisti da San Marino con IVA (fattura cartacea) - New codes have been introduced for the AltriDatiGestionali block for agricultural producers under the special regime. @@ -24,8 +27,8 @@ Example IT GOBL files can be found in the [`examples`](./examples) (YAML uncalcu -The guidelines for the use of TD28 for transactions to and from entities not established in Italy have been updated. #### Simplified invoices (Fattura Semplificata) -- [Simplified invoice schema – table view (IT)](https://www.agenziaentrate.gov.it/portale/documents/20143/4631413/RappresentazioneTabellareFattSemplificata.xlsx/a7ec4a67-f4cf-b558-1bda-0aaab4f0e552) +- [Simplified invoice schema – table view (IT)](https://www.agenziaentrate.gov.it/portale/documents/20143/4631413/RappresentazioneTabellareFattSemplificata.xlsx/a7ec4a67-f4cf-b558-1bda-0aaab4f0e552) ### Tax Rates @@ -38,7 +41,7 @@ Example IT GOBL files can be found in the [`examples`](./examples) (YAML uncalcu - [VAT Number (Partita IVA)](https://en.wikipedia.org/wiki/VAT_identification_number) - [Agenzia Entrate (Tax Office) IVA Doc](https://www.agenziaentrate.gov.it/portale/web/english/nse/business/vat-in-italy) -## Italy specifics +## Italy-specific Requirements Italy requires all invoices to comply with the [FatturaPA](https://www.fatturapa.gov.it/it/index.html) format which includes support for a specific set of fields unique to Italy. GOBL tries to guess what the best options are so that the conversion process is simple, but some data needs to be added manually. @@ -96,7 +99,7 @@ These can be added to GOBL Invoices as "charges" (`bill.Charge`) defined with th } ``` -See also [examples/stamp-duty.json](./examples/stamp-duty.json). +See also [examples/stamp-duty.json](../../examples/it/stamp-duty.json). ### Numero REA diff --git a/regimes/mx/README.md b/regimes/mx/README.md index 290c4e19..67e38eb4 100644 --- a/regimes/mx/README.md +++ b/regimes/mx/README.md @@ -2,7 +2,7 @@ Mexico uses the CFDI (Comprobante Fiscal Digital por Internet) format for e-invoicing. -Find example MX GOBL files in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example MX GOBL files in the [`examples`](../../examples/mx) (uncalculated documents) and [`examples/out`](../../examples/mx/out) (calculated envelopes) subdirectories. ## Public Documentation @@ -35,7 +35,7 @@ Specify the postal code where the invoice was issued using the `mx-cfdi-issue-pl } ``` -### `RegimenFiscal` - Fiscal Regime +### Fiscal Regime (`RegimenFiscal`) Every Supplier and Customer in a Mexican invoice must be associated with a fiscal regime code. You'll need to ensure this field's value is requested from customers when they require an invoice. @@ -63,7 +63,7 @@ The following example will associate the supplier with the `601` fiscal regime c } ``` -### `DomicilioFiscalReceptor` - Receipt's Tax Address +### Receipt's Tax Address (`DomicilioFiscalReceptor`) In CFDI, `DomicilioFiscalReceptor` is a mandatory field that specifies the postal code of the recepient's tax address. In a GOBL Invoice, you can provide this value setting the customer's address post code. @@ -92,7 +92,7 @@ In CFDI, `DomicilioFiscalReceptor` is a mandatory field that specifies the posta } ``` -### `UsoCFDI` - CFDI Use +### CFDI Use (`UsoCFDI`) The CFDI’s `UsoCFDI` field specifies how the invoice's recipient will use the invoice to deduce taxes for the expenditure made. In a GOBL Invoice, include the `mx-cfdi-use` extension in the customer. @@ -123,7 +123,7 @@ The following GOBL maps to the `G03` (Gastos en general) value of the `UsoCFDI` } ``` -### `MetodoPago` – Payment Method +### Payment Method (`MetodoPago`) The CFDI’s `MetodoPago` field specifies whether the invoice has been fully paid at the moment of issuing the invoice (`PUE` - Pago en una sola exhibición) or whether it will be paid in one or several instalments after that (`PPD` – Pago en parcialidades o diferido). @@ -202,7 +202,7 @@ The following GOBL will map to the `PPD` (Pago en parcialidades o diferido) valu } ``` -### `FormaPago` - Payment Means +### Payment Means (`FormaPago`) The CFDI’s `FormaPago` field specifies an invoice's means of payment. @@ -210,30 +210,30 @@ If the invoice hasn't been fully paid at the time of issuing the invoice (`Metod Otherwise (`MetodoPago = PUE`), the `FormaPago` value will be mapped from the key of the largest payment advance in the GOBL invoice. The following table lists all the supported values and how GOBL will map them: -| Code | Name | GOBL Payment Advance Key | -| ---- | ----------------------------------- | ----------------------------- | -| 01 | Efectivo | `cash` | -| 02 | Cheque nominativo | `cheque` | -| 03 | Transferencia electrónica de fondos | `credit-transfer` | -| 04 | Tarjeta de crédito | `card` | -| 05 | Monedero electrónico | `online+wallet` | -| 06 | Dinero electrónico | `online` | -| 08 | Vales de despensa | `other+grocery-vouchers` | -| 12 | Dación en pago | `other+in-kind` | -| 13 | Pago por subrogación | `other+subrogation` | -| 14 | Pago por consignación | `other+consignment` | -| 15 | Condonación | `other+debt-relief` | -| 17 | Compensación | `netting` | -| 23 | Novación | `other+novation` | -| 24 | Confusión | `other+merger` | -| 25 | Remisión de deuda | `other+remission` | -| 26 | Prescripción o caducidad | `other+expiration` | -| 27 | A satisfacción del acreedor | `other+satisfy-creditor` | -| 28 | Tarjeta de débito | `card+debit` | -| 29 | Tarjeta de servicios | `card+services` | -| 30 | Aplicación de anticipos | `other+advance` | -| 31 | Intermediario pagos | `other+intermediary` | -| 99 | Por definir | `other` | +| Code | Name | GOBL Payment Advance Key | +| ---- | ----------------------------------- | ------------------------ | +| 01 | Efectivo | `cash` | +| 02 | Cheque nominativo | `cheque` | +| 03 | Transferencia electrónica de fondos | `credit-transfer` | +| 04 | Tarjeta de crédito | `card` | +| 05 | Monedero electrónico | `online+wallet` | +| 06 | Dinero electrónico | `online` | +| 08 | Vales de despensa | `other+grocery-vouchers` | +| 12 | Dación en pago | `other+in-kind` | +| 13 | Pago por subrogación | `other+subrogation` | +| 14 | Pago por consignación | `other+consignment` | +| 15 | Condonación | `other+debt-relief` | +| 17 | Compensación | `netting` | +| 23 | Novación | `other+novation` | +| 24 | Confusión | `other+merger` | +| 25 | Remisión de deuda | `other+remission` | +| 26 | Prescripción o caducidad | `other+expiration` | +| 27 | A satisfacción del acreedor | `other+satisfy-creditor` | +| 28 | Tarjeta de débito | `card+debit` | +| 29 | Tarjeta de servicios | `card+services` | +| 30 | Aplicación de anticipos | `other+advance` | +| 31 | Intermediario pagos | `other+intermediary` | +| 99 | Por definir | `other` | #### Example @@ -257,7 +257,7 @@ The following GOBL maps to the `05` (Monedero electrónico) value of the `FormaP } ``` -### `ClaveUnidad` - Unit Code +### Unit Code (`ClaveUnidad`) The CFDI’s `ClaveUnidad` field specifies the unit in which the quantity of an invoice's line is given. These are UNECE codes that GOBL will map directly from the invoice's line item unit. See the [source code](../../org/unit.go) for the full list of supported units with their associated UNECE codes. @@ -285,7 +285,7 @@ The following GOBL maps to the `KGM` (Kilogram) value of the `ClaveUnidad` field } ``` -### `ClaveProdServ` - Product or Service Code +### Product/Service Code (`ClaveProdServ`) The CFDI’s `ClaveProdServ` field specifies the type of an invoice's line item. GOBL uses the line item extension key `mx-cfdi-prod-serv` to map the code directly to the `ClaveProdServ` field. @@ -370,7 +370,7 @@ In Mexico, e-wallet suppliers use this complement to report this information in Learn more about this complement here: * [Schema Documentation](https://docs.gobl.org/draft-0/regimes/mx/fuel_account_balance) -* [Example GOBL document](./examples/out/fuel-account-balance.json) +* [Example GOBL document](../../examples/mx/out/fuel-account-balance.json) #### Food Vouchers @@ -380,4 +380,4 @@ In Mexico, e-wallet suppliers use this complement to report this information in Learn more about this complement here: * [Schema Documentation](https://docs.gobl.org/draft-0/regimes/mx/food_vouchers) -* [Example GOBL document](./examples/out/food-vouchers.json) +* [Example GOBL document](../../examples/mx/out/food-vouchers.json) diff --git a/regimes/nl/README.md b/regimes/nl/README.md index e5a98090..fdb5df7b 100644 --- a/regimes/nl/README.md +++ b/regimes/nl/README.md @@ -1,2 +1,3 @@ # 🇳🇱 GOBL Netherlands Tax Regime +Find example NL GOBL files in the [`examples`](../../examples/nl) (uncalculated documents) and [`examples/out`](../../examples/nl/out) (calculated envelopes) subdirectories. diff --git a/regimes/pl/README.md b/regimes/pl/README.md index bb554f03..6607ab34 100644 --- a/regimes/pl/README.md +++ b/regimes/pl/README.md @@ -2,7 +2,7 @@ Poland uses the FA_VAT format for their e-invoicing system. -Example PL GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example PL GOBL files in the [`examples`](../../examples/pl) (uncalculated documents) and [`examples/out`](../../examples/pl/out) (calculated envelopes) subdirectories. ## Public Documentation @@ -10,7 +10,7 @@ Example PL GOBL files can be found in the [`examples`](./examples) (YAML uncalcu - [Invoice Templates (Wzór faktury) FA(1)](http://crd.gov.pl/wzor/2021/11/29/11089/) - [Invoice Templates (Wzór faktury) FA(2)](http://crd.gov.pl/wzor/2023/06/29/12648/) -## Poland specifics +## Poland-specific Requirements ### `TFormaPlatnosci` - Payment Means diff --git a/regimes/pt/README.md b/regimes/pt/README.md index 758bab43..d1e74a35 100644 --- a/regimes/pt/README.md +++ b/regimes/pt/README.md @@ -2,7 +2,7 @@ Portugal doesn't have an e-invoicing format per se. Tax information is reported electronically to the AT (Autoridade Tributária e Aduaneira) either periodically in batches via a SAF-T (PT) report or individually in real time via a web service. -Example PT GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example PT GOBL files in the [`examples`](../../examples/pt) (uncalculated documents) and [`examples/out`](../../examples/pt/out) (calculated envelopes) subdirectories. ## Public Documentation @@ -11,17 +11,17 @@ Example PT GOBL files can be found in the [`examples`](./examples) (YAML uncalcu - [Portaria n.o 195/2020 – Especificações Técnicas Código QR](https://info.portaldasfinancas.gov.pt/pt/apoio_contribuinte/Novas_regras_faturacao/Documents/Especificacoes_Tecnicas_Codigo_QR.pdf) - [Comunicação dos elementos dos documentos de faturação à AT, por webservice](https://info.portaldasfinancas.gov.pt/pt/apoio_contribuinte/Faturacao/Fatcorews/Documents/Comunicacao_dos_elementos_dos_documentos_de_faturacao.pdf) -## Portugal specifics +## Portugal-specific Requirements ### `InvoiceType` (Tipo de documento) AT's `InvoiceType` (Tipo de documento) specifies the type of a Portuguese tax document. The following table lists all the supported invoice types and how GOBL will map them with a combination of invoice type and tax tags: -| Code | Name | GOBL Type | GOBL Tax Tag | -| ---- | ----------------------------------------------------------------------- | ------------- | ----------------- | +| Code | Name | GOBL Type | GOBL Tax Tag | +| ---- | ------------------------------------------------------------------------ | ------------- | ----------------- | | FT | Fatura, emitida nos termos do artigo 36.o do Código do IVA | `standard` | | | FS | Fatura simplificada, emitida nos termos do artigo 40.o do Código do IVA | `standard` | `simplified` | -| FR | Fatura-recibo | `standard` | `invoice-receipt` | +| FR | Fatura-recibo | `standard` | `invoice-receipt` | | ND | Nota de débito | `credit-note` | | | NC | Nota de crédito | `debit-note` | | @@ -31,17 +31,17 @@ AT's `TaxCountryRegion` (País ou região do imposto) specifies the region of To set the specific a region different to Portugal mainland, the `pt-region` extension of each line's VAT tax should be set to one of the following values: -| Code | Description | -| --- | --- | -| PT | Mainland Portugal (default, no need to be explicit) | -| PT-AC | Açores | -| PT-MA | Madeira | +| Code | Description | +| ----- | --------------------------------------------------- | +| PT | Mainland Portugal (default, no need to be explicit) | +| PT-AC | Açores | +| PT-MA | Madeira | ### VAT Tax Rates The AT `TaxCode` (Código do imposto) is required for invoice items that apply VAT. GOBL helps determine this code using the `rate` field, which in Portuguese invoices is required. The following table lists the supported tax codes and how GOBL will map them: -| Code |  Name | GOBL Tax Rate | +| Code | Name | GOBL Tax Rate | | ---- | --------------- | ------------------------------------- | | NOR | Tipo Geral | `standard` | | INT | Taxa Intermédia | `intermediate` | diff --git a/regimes/us/README.md b/regimes/us/README.md index 71fbb783..7209b00e 100644 --- a/regimes/us/README.md +++ b/regimes/us/README.md @@ -1,3 +1,3 @@ # 🇺🇸 GOBL United States of America Tax Regime -Example US GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. +Find example US GOBL files in the [`examples`](../../examples/us) (uncalculated documents) and [`examples/out`](../../examples/us/out) (calculated envelopes) subdirectories. From d90795468a37f294a46de69f4d43f7fe86b9d9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Tue, 19 Nov 2024 12:28:45 +0000 Subject: [PATCH 5/8] Move BR address validation to regime --- addons/br/nfse/invoices.go | 23 +---- regimes/br/br.go | 3 + regimes/br/party.go | 58 ++++++++++++ regimes/br/party_test.go | 177 +++++++++++++++++++++++++++++++++++++ 4 files changed, 240 insertions(+), 21 deletions(-) create mode 100644 regimes/br/party.go create mode 100644 regimes/br/party_test.go diff --git a/addons/br/nfse/invoices.go b/addons/br/nfse/invoices.go index 2e523511..f737e75e 100644 --- a/addons/br/nfse/invoices.go +++ b/addons/br/nfse/invoices.go @@ -1,25 +1,12 @@ package nfse import ( - "regexp" - "github.com/invopop/gobl/bill" - "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/org" "github.com/invopop/gobl/tax" "github.com/invopop/validation" ) -var ( - validStates = []cbc.Code{ - "AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", - "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", - "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO", - } - - validAddressCode = regexp.MustCompile(`^(?:\D*\d){8}\D*$`) -) - func validateInvoice(inv *bill.Invoice) error { if inv == nil { return nil @@ -82,13 +69,7 @@ func validateSupplierAddress(value interface{}) error { validation.Field(&obj.Street, validation.Required), validation.Field(&obj.Number, validation.Required), validation.Field(&obj.Locality, validation.Required), - validation.Field(&obj.State, - validation.Required, - validation.In(validStates...), - ), - validation.Field(&obj.Code, - validation.Required, - validation.Match(validAddressCode), - ), + validation.Field(&obj.State, validation.Required), + validation.Field(&obj.Code, validation.Required), ) } diff --git a/regimes/br/br.go b/regimes/br/br.go index 7f1addbe..30ce654d 100644 --- a/regimes/br/br.go +++ b/regimes/br/br.go @@ -6,6 +6,7 @@ import ( "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/currency" "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -45,6 +46,8 @@ func Validate(doc interface{}) error { switch obj := doc.(type) { case *bill.Invoice: return validateInvoice(obj) + case *org.Party: + return validateParty(obj) case *tax.Identity: return validateTaxIdentity(obj) } diff --git a/regimes/br/party.go b/regimes/br/party.go new file mode 100644 index 00000000..4f0356a1 --- /dev/null +++ b/regimes/br/party.go @@ -0,0 +1,58 @@ +package br + +import ( + "regexp" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/org" + "github.com/invopop/validation" +) + +var ( + validStates = []cbc.Code{ + "AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", + "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", + "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO", + } + validPostCode = regexp.MustCompile(`^\d{5}-?\d{3}$`) +) + +func validateParty(party *org.Party) error { + if party == nil { + return nil + } + + return validation.ValidateStruct(party, + validation.Field(&party.Addresses, + validation.Each( + validation.By(validateAddress(party)), + ), + ), + ) +} + +func validateAddress(party *org.Party) validation.RuleFunc { + return func(value interface{}) error { + addr, _ := value.(*org.Address) + if addr == nil { + return nil + } + + if !isBrazilianAddress(party, addr) { + return nil + } + + return validation.ValidateStruct(addr, + validation.Field(&addr.State, validation.In(validStates...)), + validation.Field(&addr.Code, validation.Match(validPostCode)), + ) + } +} + +func isBrazilianAddress(party *org.Party, addr *org.Address) bool { + if addr.Country != "" { + return addr.Country == l10n.BR.ISO() + } + return party.TaxID != nil && party.TaxID.Country == l10n.BR.Tax() +} diff --git a/regimes/br/party_test.go b/regimes/br/party_test.go new file mode 100644 index 00000000..b57e38ba --- /dev/null +++ b/regimes/br/party_test.go @@ -0,0 +1,177 @@ +package br_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/br" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestValidateAddresses(t *testing.T) { + tests := []struct { + name string + party *org.Party + err string + }{ + { + name: "nil party", + party: nil, + }, + { + name: "empty party", + party: &org.Party{}, + }, + { + name: "empty address", + party: &org.Party{ + Addresses: []*org.Address{}, + }, + }, + { + name: "empty Brazilian address", + party: &org.Party{ + Addresses: []*org.Address{ + { + Country: "BR", + }, + }, + }, + }, + { + name: "valid Brazilian address", + party: &org.Party{ + Addresses: []*org.Address{ + { + Country: "BR", + Code: "12345-678", + State: "SP", + }, + }, + }, + }, + { + name: "invalid Brazilian post code", + party: &org.Party{ + Addresses: []*org.Address{ + { + Country: "BR", + Code: "12345", + }, + }, + }, + err: "code: must be in a valid format", + }, + { + name: "invalid Brazilian state", + party: &org.Party{ + Addresses: []*org.Address{ + { + Country: "BR", + State: "XX", + }, + }, + }, + err: "state: must be a valid value.", + }, + { + name: "invalid Brazilian address with tax country only", + party: &org.Party{ + TaxID: &tax.Identity{ + Country: "BR", + }, + Addresses: []*org.Address{ + { + Code: "12345", + }, + }, + }, + err: "code: must be in a valid format", + }, + { + name: "non-Brazilian address", + party: &org.Party{ + TaxID: &tax.Identity{ + Country: "BR", + }, + Addresses: []*org.Address{ + { + Country: "US", + Code: "123", + State: "NY", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := br.Validate(tt.party) + if tt.err == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.ErrorContains(t, err, tt.err) + } + }) + } +} + +func TestValidatePostCodes(t *testing.T) { + tests := []struct { + name string + code string + err string + }{ + { + name: "valid", + code: "12345-678", + }, + { + name: "valid without dash", + code: "12345678", + }, + { + name: "too short", + code: "12345", + err: "code: must be in a valid format", + }, + { + name: "too long", + code: "123456789", + err: "code: must be in a valid format", + }, + { + name: "invalid chars", + code: "12345-678a", + err: "code: must be in a valid format", + }, + { + name: "dash in wrong place", + code: "1234-5678", + err: "code: must be in a valid format", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + party := &org.Party{ + Addresses: []*org.Address{ + { + Country: "BR", + Code: cbc.Code(tt.code), + }, + }, + } + err := br.Validate(party) + if tt.err == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.ErrorContains(t, err, tt.err) + } + }) + } +} From 496784dfaa241947c013215b496b2ba0491d661c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Tue, 19 Nov 2024 12:37:18 +0000 Subject: [PATCH 6/8] Add explanatory comment to br-nfse regime --- addons/br/nfse/extensions.go | 6 +++++- addons/br/nfse/identities.go | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/addons/br/nfse/extensions.go b/addons/br/nfse/extensions.go index 9e40e4f0..5edd2598 100644 --- a/addons/br/nfse/extensions.go +++ b/addons/br/nfse/extensions.go @@ -6,7 +6,11 @@ import ( "github.com/invopop/gobl/pkg/here" ) -// Brazilian extension keys required to issue NFS-e documents. +// Brazilian extension keys required to issue NFS-e documents. In an initial +// assessment, these extensions do not seem to apply to documents other than +// NFS-e. However, if when implementing other Fiscal Notes it is found that some +// of these extensions are common, they can be moved to the regime or to a +// shared addon. const ( ExtKeyFiscalIncentive = "br-nfse-fiscal-incentive" ExtKeyMunicipality = "br-nfse-municipality" diff --git a/addons/br/nfse/identities.go b/addons/br/nfse/identities.go index 215ce60f..b7f65512 100644 --- a/addons/br/nfse/identities.go +++ b/addons/br/nfse/identities.go @@ -5,7 +5,11 @@ import ( "github.com/invopop/gobl/i18n" ) -// Brazilian identity keys required to issue NFS-e documents. +// Brazilian identity keys required to issue NFS-e documents. In an initial +// assessment, these identities do not seem to apply to documents other than +// NFS-e. However, if when implementing other Fiscal Notes it is found that some +// of these extensions are common, they can be moved to the regime or to a +// shared addon. const ( IdentityKeyMunicipalReg = "br-nfse-municipal-reg" IdentityKeyNationalReg = "br-nfse-national-reg" From 7d1b4d538fbae09e89d4de60c9a26bb0317326dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Tue, 19 Nov 2024 14:17:05 +0000 Subject: [PATCH 7/8] Add validation skips to BR regime and addon --- addons/br/nfse/invoices.go | 4 ++++ regimes/br/invoices.go | 5 ++++- regimes/br/party.go | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/addons/br/nfse/invoices.go b/addons/br/nfse/invoices.go index f737e75e..aeafb36b 100644 --- a/addons/br/nfse/invoices.go +++ b/addons/br/nfse/invoices.go @@ -38,9 +38,11 @@ func validateSupplier(value interface{}) error { validation.Field(&obj.TaxID, validation.Required, tax.RequireIdentityCode, + validation.Skip, ), validation.Field(&obj.Identities, org.RequireIdentityKey(IdentityKeyMunicipalReg), + validation.Skip, ), validation.Field(&obj.Name, validation.Required), validation.Field(&obj.Addresses, @@ -49,12 +51,14 @@ func validateSupplier(value interface{}) error { validation.Required, validation.By(validateSupplierAddress), ), + validation.Skip, ), validation.Field(&obj.Ext, tax.ExtensionsRequires( ExtKeySimplesNacional, ExtKeyMunicipality, ), + validation.Skip, ), ) } diff --git a/regimes/br/invoices.go b/regimes/br/invoices.go index 6c1464fe..b8b93fb7 100644 --- a/regimes/br/invoices.go +++ b/regimes/br/invoices.go @@ -7,6 +7,9 @@ import ( func validateInvoice(inv *bill.Invoice) error { return validation.ValidateStruct(inv, - validation.Field(&inv.Supplier, validation.Required), + validation.Field(&inv.Supplier, + validation.Required, + validation.Skip, + ), ) } diff --git a/regimes/br/party.go b/regimes/br/party.go index 4f0356a1..52873d1e 100644 --- a/regimes/br/party.go +++ b/regimes/br/party.go @@ -28,6 +28,7 @@ func validateParty(party *org.Party) error { validation.Each( validation.By(validateAddress(party)), ), + validation.Skip, ), ) } From 20f27e666df7bdeba3f7ef8680bf042cbccb36d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Tue, 19 Nov 2024 15:16:25 +0000 Subject: [PATCH 8/8] Make BR fiscal incentive mandatory with a default --- addons/br/nfse/invoices.go | 19 ++++++++++ addons/br/nfse/invoices_test.go | 65 +++++++++++++++++++++++++++++++++ addons/br/nfse/nfse.go | 8 ++++ regimes/br/README.md | 8 ++-- 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/addons/br/nfse/invoices.go b/addons/br/nfse/invoices.go index aeafb36b..ec4946a6 100644 --- a/addons/br/nfse/invoices.go +++ b/addons/br/nfse/invoices.go @@ -7,6 +7,11 @@ import ( "github.com/invopop/validation" ) +const ( + // FiscalIncentiveDefault is the default value for the fiscal incentive extenstion + FiscalIncentiveDefault = "2" // No incentiva +) + func validateInvoice(inv *bill.Invoice) error { if inv == nil { return nil @@ -57,6 +62,7 @@ func validateSupplier(value interface{}) error { tax.ExtensionsRequires( ExtKeySimplesNacional, ExtKeyMunicipality, + ExtKeyFiscalIncentive, ), validation.Skip, ), @@ -77,3 +83,16 @@ func validateSupplierAddress(value interface{}) error { validation.Field(&obj.Code, validation.Required), ) } + +func normalizeSupplier(sup *org.Party) { + if sup == nil { + return + } + + if !sup.Ext.Has(ExtKeyFiscalIncentive) { + if sup.Ext == nil { + sup.Ext = make(tax.Extensions) + } + sup.Ext[ExtKeyFiscalIncentive] = FiscalIncentiveDefault + } +} diff --git a/addons/br/nfse/invoices_test.go b/addons/br/nfse/invoices_test.go index c9eb3a58..222097bd 100644 --- a/addons/br/nfse/invoices_test.go +++ b/addons/br/nfse/invoices_test.go @@ -164,4 +164,69 @@ func TestSuppliersValidation(t *testing.T) { assert.NotContains(t, err.Error(), "addresses: (0:") } }) + + t.Run("validates extensions", func(t *testing.T) { + sup := new(org.Party) + inv := &bill.Invoice{ + Supplier: sup, + } + err := addon.Validator(inv) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "br-nfse-simples-nacional: required") + assert.Contains(t, err.Error(), "br-nfse-municipality: required") + assert.Contains(t, err.Error(), "br-nfse-fiscal-incentive: required") + } + + sup.Ext = tax.Extensions{ + nfse.ExtKeySimplesNacional: "1", + nfse.ExtKeyMunicipality: "12345678", + nfse.ExtKeyFiscalIncentive: "2", + } + err = addon.Validator(inv) + if assert.Error(t, err) { + assert.NotContains(t, err.Error(), "br-nfse-simples-nacional: required") + assert.NotContains(t, err.Error(), "br-nfse-municipality: required") + assert.NotContains(t, err.Error(), "br-nfse-fiscal-incentive: required") + } + }) +} + +func TestSuppliersNormalization(t *testing.T) { + addon := tax.AddonForKey(nfse.V1) + + tests := []struct { + name string + supplier *org.Party + out tax.ExtValue + }{ + { + name: "no supplier", + supplier: nil, + }, + { + name: "sets default fiscal incentive", + supplier: &org.Party{}, + out: "2", + }, + { + name: "does not override fiscal incentive", + supplier: &org.Party{ + Ext: tax.Extensions{ + nfse.ExtKeyFiscalIncentive: "1", + }, + }, + out: "1", + }, + } + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + inv := &bill.Invoice{Supplier: ts.supplier} + addon.Normalizer(inv) + if ts.supplier == nil { + assert.Nil(t, inv.Supplier) + } else { + assert.Equal(t, ts.out, inv.Supplier.Ext[nfse.ExtKeyFiscalIncentive]) + } + }) + } } diff --git a/addons/br/nfse/nfse.go b/addons/br/nfse/nfse.go index cb94c9ac..4497e0bc 100644 --- a/addons/br/nfse/nfse.go +++ b/addons/br/nfse/nfse.go @@ -28,6 +28,7 @@ func newAddon() *tax.AddonDef { Extensions: extensions, Identities: identities, Validator: validate, + Normalizer: normalize, } } @@ -42,3 +43,10 @@ func validate(doc any) error { } return nil } + +func normalize(doc any) { + switch obj := doc.(type) { + case *bill.Invoice: + normalizeSupplier(obj.Supplier) + } +} diff --git a/regimes/br/README.md b/regimes/br/README.md index 7543e630..f9d4e34b 100644 --- a/regimes/br/README.md +++ b/regimes/br/README.md @@ -132,10 +132,10 @@ For example: Report whether the party benefits from a fiscal incentive using the `br-nfse-fiscal-incentive` extension set to any of these codes: -| Code | Description | -| ---- | ----------------------- | -| `1` | Has incentive | -| `2` | Does not have incentive | +| Code | Description | +| ---- | --------------------------------- | +| `1` | Has incentive | +| `2` | Does not have incentive (Default) | For example: