-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add initial Brazil NFSe addon #402
Changes from 1 commit
b9f8e46
2b6842c
177eb69
745a7b8
e3f6a2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
`), | ||
}, | ||
}, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
), | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.).", | ||
}, | ||
cavalle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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) | ||
} | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this also need to be repeated for Discount and Charge and lines? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I don't think it makes sense to support either Charges or Discounts: it doesn't seem possible to map them to an NFSe. So, I've added validations to ensure those aren't present in 177eb69 |
||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is is possible to add a
Pattern
flag perhaps to validate the codes?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose not to validate anything since every municipality can define their own list of codes without restrictions on the format. The list in LEI116 is commonly used, apparently, but there's no guarantee those will be the codes in the municipality.