diff --git a/addons/br/nfse/extensions.go b/addons/br/nfse/extensions.go index 86d7c8b2..607a6f99 100644 --- a/addons/br/nfse/extensions.go +++ b/addons/br/nfse/extensions.go @@ -14,6 +14,7 @@ import ( const ( ExtKeyCNAE = "br-nfse-cnae" ExtKeyFiscalIncentive = "br-nfse-fiscal-incentive" + ExtKeyISSLiability = "br-nfse-iss-liability" ExtKeyMunicipality = "br-nfse-municipality" ExtKeyService = "br-nfse-service" ExtKeySimplesNacional = "br-nfse-simples-nacional" @@ -70,6 +71,74 @@ var extensions = []*cbc.KeyDefinition{ `), }, }, + { + Key: ExtKeyISSLiability, + Name: i18n.String{ + i18n.EN: "ISS Liability", + i18n.PT: "Exigibilidade ISS", + }, + Values: []*cbc.ValueDefinition{ + { + Value: "1", + Name: i18n.String{ + i18n.EN: "Liable", + i18n.PT: "Exigível", + }, + }, + { + Value: "2", + Name: i18n.String{ + i18n.EN: "Not subject", + i18n.PT: "Não incidência", + }, + }, + { + Value: "3", + Name: i18n.String{ + i18n.EN: "Exempt", + i18n.PT: "Isenção", + }, + }, + { + Value: "4", + Name: i18n.String{ + i18n.EN: "Export", + i18n.PT: "Exportação", + }, + }, + { + Value: "5", + Name: i18n.String{ + i18n.EN: "Immune", + i18n.PT: "Imunidade", + }, + }, + { + Value: "6", + Name: i18n.String{ + i18n.EN: "Suspended Judicially", + i18n.PT: "Suspensa por Decisão Judicial", + }, + }, + { + Value: "7", + Name: i18n.String{ + i18n.EN: "Suspended Administratively", + i18n.PT: "Suspensa por Processo Administrativo", + }, + }, + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Indicates the ISS liability status, i.e., whether the ISS tax is due or not and why. + + 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-38) + `), + }, + }, { Key: ExtKeyMunicipality, Name: i18n.String{ diff --git a/addons/br/nfse/nfse.go b/addons/br/nfse/nfse.go index 4497e0bc..fe4c1727 100644 --- a/addons/br/nfse/nfse.go +++ b/addons/br/nfse/nfse.go @@ -40,6 +40,8 @@ func validate(doc any) error { return validateLine(obj) case *org.Item: return validateItem(obj) + case *tax.Combo: + return validateTaxCombo(obj) } return nil } @@ -48,5 +50,7 @@ func normalize(doc any) { switch obj := doc.(type) { case *bill.Invoice: normalizeSupplier(obj.Supplier) + case *tax.Combo: + normalizeTaxCombo(obj) } } diff --git a/addons/br/nfse/tax_combo.go b/addons/br/nfse/tax_combo.go new file mode 100644 index 00000000..2879c08d --- /dev/null +++ b/addons/br/nfse/tax_combo.go @@ -0,0 +1,35 @@ +package nfse + +import ( + "github.com/invopop/gobl/regimes/br" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +const ( + // ISSLiabilityDefault is the default value for the ISS liability extension + ISSLiabilityDefault = "1" // Liable +) + +func validateTaxCombo(tc *tax.Combo) error { + return validation.ValidateStruct(tc, + validation.Field(&tc.Ext, + validation.When(tc.Category == br.TaxCategoryISS, + tax.ExtensionsRequires(ExtKeyISSLiability), + ), + ), + ) +} + +func normalizeTaxCombo(tc *tax.Combo) { + if tc == nil || tc.Category != br.TaxCategoryISS { + return + } + + if !tc.Ext.Has(ExtKeyISSLiability) { + if tc.Ext == nil { + tc.Ext = make(tax.Extensions) + } + tc.Ext[ExtKeyISSLiability] = ISSLiabilityDefault + } +} diff --git a/addons/br/nfse/tax_combo_test.go b/addons/br/nfse/tax_combo_test.go new file mode 100644 index 00000000..86e6b982 --- /dev/null +++ b/addons/br/nfse/tax_combo_test.go @@ -0,0 +1,105 @@ +package nfse_test + +import ( + "testing" + + "github.com/invopop/gobl/addons/br/nfse" + "github.com/invopop/gobl/regimes/br" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestTaxComboValidation(t *testing.T) { + addon := tax.AddonForKey(nfse.V1) + + tests := []struct { + name string + tc *tax.Combo + err string + }{ + { + name: "valid ISS tax combo", + tc: &tax.Combo{ + Category: br.TaxCategoryISS, + Ext: tax.Extensions{ + nfse.ExtKeyISSLiability: "1", + }, + }, + }, + { + name: "valid non-ISS tax combo", + tc: &tax.Combo{ + Category: br.TaxCategoryPIS, + }, + }, + { + name: "missing ISS liability", + tc: &tax.Combo{ + Category: br.TaxCategoryISS, + }, + err: "br-nfse-iss-liability: required", + }, + } + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + err := addon.Validator(ts.tc) + if ts.err == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.ErrorContains(t, err, ts.err) + } + } + }) + } +} + +func TestTaxComboNormalization(t *testing.T) { + addon := tax.AddonForKey(nfse.V1) + + tests := []struct { + name string + tc *tax.Combo + out tax.ExtValue + }{ + { + name: "no tax combo", + tc: nil, + }, + { + name: "sets default ISS liability", + tc: &tax.Combo{ + Category: br.TaxCategoryISS, + }, + out: "1", + }, + { + name: "does not override ISS liability", + tc: &tax.Combo{ + Category: br.TaxCategoryISS, + Ext: tax.Extensions{ + nfse.ExtKeyISSLiability: "2", + }, + }, + out: "2", + }, + { + name: "non-ISS tax combo", + tc: &tax.Combo{ + Category: br.TaxCategoryPIS, + }, + }, + } + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + addon.Normalizer(ts.tc) + if ts.tc == nil { + assert.Nil(t, ts.tc) + } else { + assert.NotNil(t, ts.tc) + assert.Equal(t, ts.out, ts.tc.Ext[nfse.ExtKeyISSLiability]) + } + }) + } + +} diff --git a/data/addons/br-nfse-v1.json b/data/addons/br-nfse-v1.json index 1230978e..7dca06f4 100644 --- a/data/addons/br-nfse-v1.json +++ b/data/addons/br-nfse-v1.json @@ -41,6 +41,67 @@ } ] }, + { + "key": "br-nfse-iss-liability", + "name": { + "en": "ISS Liability", + "pt": "Exigibilidade ISS" + }, + "desc": { + "en": "Indicates the ISS liability status, i.e., whether the ISS tax is due or not and why.\n\nList of codes taken from the national NFSe standard:\n\n* https://abrasf.org.br/biblioteca/arquivos-publicos/nfs-e-manual-de-orientacao-do-contribuinte-2-04/download\n(Section 10.2, Field B-38)" + }, + "values": [ + { + "value": "1", + "name": { + "en": "Liable", + "pt": "Exigível" + } + }, + { + "value": "2", + "name": { + "en": "Not subject", + "pt": "Não incidência" + } + }, + { + "value": "3", + "name": { + "en": "Exempt", + "pt": "Isenção" + } + }, + { + "value": "4", + "name": { + "en": "Export", + "pt": "Exportação" + } + }, + { + "value": "5", + "name": { + "en": "Immune", + "pt": "Imunidade" + } + }, + { + "value": "6", + "name": { + "en": "Suspended Judicially", + "pt": "Suspensa por Decisão Judicial" + } + }, + { + "value": "7", + "name": { + "en": "Suspended Administratively", + "pt": "Suspensa por Processo Administrativo" + } + } + ] + }, { "key": "br-nfse-municipality", "name": { diff --git a/examples/br/invoice-services.json b/examples/br/invoice-services.json index 1c7be41f..9c985ff7 100644 --- a/examples/br/invoice-services.json +++ b/examples/br/invoice-services.json @@ -79,7 +79,10 @@ "taxes": [ { "cat": "ISS", - "percent": "15%" + "percent": "15%", + "ext": { + "br-nfse-iss-liability": "1" + } } ] } diff --git a/examples/br/out/invoice-services.json b/examples/br/out/invoice-services.json index 90fc13be..d93efb63 100644 --- a/examples/br/out/invoice-services.json +++ b/examples/br/out/invoice-services.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "31573fb261adbfa6011c8c21e5e53c4a7546389e6a39fc47124d0e34fb698811" + "val": "6b9933fc6209359b95583878eb64c358ac1b9028727c3e992ec4e78f7a196687" } }, "doc": { @@ -91,7 +91,10 @@ "taxes": [ { "cat": "ISS", - "percent": "15%" + "percent": "15%", + "ext": { + "br-nfse-iss-liability": "1" + } } ], "total": "1500.00" @@ -106,6 +109,9 @@ "code": "ISS", "rates": [ { + "ext": { + "br-nfse-iss-liability": "1" + }, "base": "1500.00", "percent": "15%", "amount": "225.00" diff --git a/regimes/br/README.md b/regimes/br/README.md index 6530e6ea..57a38e53 100644 --- a/regimes/br/README.md +++ b/regimes/br/README.md @@ -181,4 +181,31 @@ For example: //... ``` +#### ISS Liability +Report the ISS liability -i.e. whether the tax is due or not and why– using the `br-nfse-iss-liability` extension at ISS tax level. Find the list of possible codes below: + +| Code | Description | +| ---- | -------------------------- | +| `1` | Liable (Default) | +| `2` | Not subject | +| `3` | Exempt | +| `4` | Export | +| `5` | Immune | +| `6` | Suspended Judicially | +| `7` | Suspended Administratively | + +For example: + +```js +"lines": [ + { +//... + "taxes": [ + { + "cat": "ISS", + "ext": { + "br-nfse-iss-liability": "1" + } +//... +```