From b9f8e461ed1444d304e14c93d3e6bb382c3c389c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Fri, 25 Oct 2024 09:49:15 +0000 Subject: [PATCH] Add initial Brazil NFSe addon --- CHANGELOG.md | 6 ++ addons/addons.go | 1 + addons/br/nfse/extensions.go | 32 +++++++++ addons/br/nfse/item.go | 21 ++++++ addons/br/nfse/item_test.go | 62 +++++++++++++++++ addons/br/nfse/line.go | 18 +++++ addons/br/nfse/line_test.go | 67 +++++++++++++++++++ addons/br/nfse/nfse.go | 41 ++++++++++++ data/addons/br-nfse-v1.json | 21 ++++++ data/schemas/bill/invoice.json | 4 ++ ...voice-br-br.yaml => invoice-services.yaml} | 2 + ...voice-br-br.json => invoice-services.json} | 7 +- 12 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 addons/br/nfse/extensions.go create mode 100644 addons/br/nfse/item.go create mode 100644 addons/br/nfse/item_test.go create mode 100644 addons/br/nfse/line.go create mode 100644 addons/br/nfse/line_test.go create mode 100644 addons/br/nfse/nfse.go create mode 100644 data/addons/br-nfse-v1.json rename regimes/br/examples/{invoice-br-br.yaml => invoice-services.yaml} (94%) rename regimes/br/examples/out/{invoice-br-br.json => invoice-services.json} (91%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 597cb9c5..b8b6da98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to GOBL will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). See also the [GOBL versions](https://docs.gobl.org/overview/versions) documentation site for more details. +## [Unreleased] + +### Added + +- `br-nfse-v1`: added initial Brazil NFS-e addon + ## [v0.203.0] ### Added diff --git a/addons/addons.go b/addons/addons.go index 8f8cc110..7eeac335 100644 --- a/addons/addons.go +++ b/addons/addons.go @@ -9,6 +9,7 @@ package addons import ( // Import all the addons to ensure they're ready to use. + _ "github.com/invopop/gobl/addons/br/nfse" _ "github.com/invopop/gobl/addons/co/dian" _ "github.com/invopop/gobl/addons/es/facturae" _ "github.com/invopop/gobl/addons/es/tbai" diff --git a/addons/br/nfse/extensions.go b/addons/br/nfse/extensions.go new file mode 100644 index 00000000..9bcc34cd --- /dev/null +++ b/addons/br/nfse/extensions.go @@ -0,0 +1,32 @@ +package nfse + +import ( + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/pkg/here" +) + +// Brazilian extension keys required to issue NFS-e documents. +const ( + ExtKeyService = "br-nfse-service" +) + +var extensions = []*cbc.KeyDefinition{ + { + Key: ExtKeyService, + Name: i18n.String{ + i18n.EN: "Service Code", + i18n.PT: "Código Item Lista Serviço", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + The service code as defined by the municipality. Typically, one of the codes listed + in the Lei Complementar 116/2003, but municipalities can make their own changes. + + For further details on the list of possible codes, see: + + * https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp116.htm + `), + }, + }, +} diff --git a/addons/br/nfse/item.go b/addons/br/nfse/item.go new file mode 100644 index 00000000..d84d7ce7 --- /dev/null +++ b/addons/br/nfse/item.go @@ -0,0 +1,21 @@ +package nfse + +import ( + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +func validateItem(value any) error { + item, _ := value.(*org.Item) + if item == nil { + return nil + } + + return validation.ValidateStruct(item, + validation.Field(&item.Ext, + tax.ExtensionsRequires(ExtKeyService), + validation.Skip, + ), + ) +} diff --git a/addons/br/nfse/item_test.go b/addons/br/nfse/item_test.go new file mode 100644 index 00000000..953f0e38 --- /dev/null +++ b/addons/br/nfse/item_test.go @@ -0,0 +1,62 @@ +package nfse_test + +import ( + "testing" + + "github.com/invopop/gobl/addons/br/nfse" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestItemValidation(t *testing.T) { + tests := []struct { + name string + item *org.Item + err string + }{ + { + name: "valid item", + item: &org.Item{ + Ext: tax.Extensions{ + nfse.ExtKeyService: "12345678", + }, + }, + }, + { + name: "missing extensions", + item: &org.Item{}, + err: "ext: (br-nfse-service: required.)", + }, + { + name: "empty extensions", + item: &org.Item{ + Ext: tax.Extensions{}, + }, + err: "ext: (br-nfse-service: required.)", + }, + { + name: "missing extension", + item: &org.Item{ + Ext: tax.Extensions{ + "random": "12345678", + }, + }, + err: "ext: (br-nfse-service: required.).", + }, + } + + addon := tax.AddonForKey(nfse.V1) + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + err := addon.Validator(ts.item) + if ts.err == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), ts.err) + } + } + }) + } +} diff --git a/addons/br/nfse/line.go b/addons/br/nfse/line.go new file mode 100644 index 00000000..92ae8d28 --- /dev/null +++ b/addons/br/nfse/line.go @@ -0,0 +1,18 @@ +package nfse + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/regimes/br" + "github.com/invopop/validation" +) + +func validateLine(value any) error { + line, _ := value.(*bill.Line) + if line == nil { + return nil + } + + return validation.Validate(line, + bill.RequireLineTaxCategory(br.TaxCategoryISS), + ) +} diff --git a/addons/br/nfse/line_test.go b/addons/br/nfse/line_test.go new file mode 100644 index 00000000..5173aacd --- /dev/null +++ b/addons/br/nfse/line_test.go @@ -0,0 +1,67 @@ +package nfse_test + +import ( + "testing" + + "github.com/invopop/gobl/addons/br/nfse" + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/regimes/br" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestLineValidation(t *testing.T) { + tests := []struct { + name string + line *bill.Line + err string + }{ + { + name: "valid line", + line: &bill.Line{ + Taxes: tax.Set{ + { + Category: br.TaxCategoryISS, + }, + }, + }, + }, + { + name: "missing taxes", + line: &bill.Line{}, + err: "taxes: missing category ISS.", + }, + { + name: "empty taxes", + line: &bill.Line{ + Taxes: tax.Set{}, + }, + err: "taxes: missing category ISS.", + }, + { + name: "missing ISS tax", + line: &bill.Line{ + Taxes: tax.Set{ + { + Category: br.TaxCategoryPIS, + }, + }, + }, + err: "taxes: missing category ISS.", + }, + } + + addon := tax.AddonForKey(nfse.V1) + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + err := addon.Validator(ts.line) + if ts.err == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), ts.err) + } + } + }) + } +} diff --git a/addons/br/nfse/nfse.go b/addons/br/nfse/nfse.go new file mode 100644 index 00000000..07310b34 --- /dev/null +++ b/addons/br/nfse/nfse.go @@ -0,0 +1,41 @@ +// Package nfse handles extensions and validation rules to issue NFS-e in +// Brazil. +package nfse + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" +) + +const ( + // V1 identifies the NFS-e addon version + V1 cbc.Key = "br-nfse-v1" +) + +func init() { + tax.RegisterAddonDef(newAddon()) +} + +func newAddon() *tax.AddonDef { + return &tax.AddonDef{ + Key: V1, + Name: i18n.String{ + i18n.EN: "Brazil NFS-e 1.X", + }, + Extensions: extensions, + Validator: validate, + } +} + +func validate(doc any) error { + switch obj := doc.(type) { + case *bill.Line: + return validateLine(obj) + case *org.Item: + return validateItem(obj) + } + return nil +} diff --git a/data/addons/br-nfse-v1.json b/data/addons/br-nfse-v1.json new file mode 100644 index 00000000..c310e792 --- /dev/null +++ b/data/addons/br-nfse-v1.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/addon-def", + "key": "br-nfse-v1", + "name": { + "en": "Brazil NFS-e 1.X" + }, + "extensions": [ + { + "key": "br-nfse-service", + "name": { + "en": "Service Code", + "pt": "Código Item Lista Serviço" + }, + "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" + } + } + ], + "scenarios": null, + "corrections": null +} \ No newline at end of file diff --git a/data/schemas/bill/invoice.json b/data/schemas/bill/invoice.json index 50122c1e..3aafad4c 100644 --- a/data/schemas/bill/invoice.json +++ b/data/schemas/bill/invoice.json @@ -248,6 +248,10 @@ "items": { "$ref": "https://gobl.org/draft-0/cbc/key", "oneOf": [ + { + "const": "br-nfse-v1", + "title": "Brazil NFS-e 1.X" + }, { "const": "co-dian-v2", "title": "Colombia DIAN UBL 2.X" diff --git a/regimes/br/examples/invoice-br-br.yaml b/regimes/br/examples/invoice-services.yaml similarity index 94% rename from regimes/br/examples/invoice-br-br.yaml rename to regimes/br/examples/invoice-services.yaml index ec799707..626b237e 100644 --- a/regimes/br/examples/invoice-br-br.yaml +++ b/regimes/br/examples/invoice-services.yaml @@ -30,6 +30,8 @@ lines: name: "Development services" price: "90.00" unit: "h" + ext: + br-nfse-service: "1.01" discounts: - percent: "10%" reason: "Special discount" diff --git a/regimes/br/examples/out/invoice-br-br.json b/regimes/br/examples/out/invoice-services.json similarity index 91% rename from regimes/br/examples/out/invoice-br-br.json rename to regimes/br/examples/out/invoice-services.json index eebde428..4045a3de 100644 --- a/regimes/br/examples/out/invoice-br-br.json +++ b/regimes/br/examples/out/invoice-services.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "5b7bb418d378de68d73da3f12f211648c5238e8ba916f4f5f00b2712f24d2476" + "val": "edfb9ac9ce7e8ceecb839cbfba109d8350ecf7408038e6eddd008cb294949456" } }, "doc": { @@ -52,7 +52,10 @@ "item": { "name": "Development services", "price": "90.00", - "unit": "h" + "unit": "h", + "ext": { + "br-nfse-service": "1.01" + } }, "sum": "1800.00", "discounts": [