Skip to content

Commit

Permalink
Refining extension checks with ES TBAI validation
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Dec 20, 2023
1 parent 4a3f16b commit a410e45
Show file tree
Hide file tree
Showing 15 changed files with 264 additions and 113 deletions.
2 changes: 1 addition & 1 deletion org/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (i *Item) ValidateWithContext(ctx context.Context) error {
validation.Field(&i.Price, validation.Required),
validation.Field(&i.Unit),
validation.Field(&i.Origin),
validation.Field(&i.Ext, tax.InRegimeExtensions),
validation.Field(&i.Ext),
validation.Field(&i.Meta),
)
}
2 changes: 1 addition & 1 deletion org/party.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (p *Party) ValidateWithContext(ctx context.Context) error {
validation.Field(&p.Telephones),
validation.Field(&p.Registration),
validation.Field(&p.Logos),
validation.Field(&p.Ext, tax.InRegimeExtensions),
validation.Field(&p.Ext),
validation.Field(&p.Meta),
)
}
66 changes: 38 additions & 28 deletions regimes/es/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package es

import (
"github.com/invopop/gobl/i18n"
"github.com/invopop/gobl/pkg/here"
"github.com/invopop/gobl/tax"
)

// Spanish regime extension codes for local electronic formats.
const (
ExtKeyTBAIExemption = "es-tbai-exemption"
ExtKeyTBAINotSubject = "es-tbai-not-subject"
ExtKeyTBAIProduct = "es-tbai-product"
ExtKeyTBAIExemption = "es-tbai-exemption"
ExtKeyTBAIProduct = "es-tbai-product"
)

var extensionKeys = []*tax.KeyDefinition{
Expand All @@ -19,6 +19,17 @@ var extensionKeys = []*tax.KeyDefinition{
i18n.EN: "TicketBAI Product Key",
i18n.ES: "Clave de Producto TicketBAI",
},
Desc: i18n.String{
i18n.EN: here.Doc(`
Product keys are used by TicketBAI to differentiate between -exported- goods
and services. It may be useful to classify all products regardless of wether
they are exported or not.
There is an additional exception case for goods that are resold without modification
when the supplier is in the simplified tax regime. For must purposes this special
case can be ignored.
`),
},
Keys: []*tax.KeyDefinition{
{
Key: "goods",
Expand Down Expand Up @@ -49,70 +60,69 @@ var extensionKeys = []*tax.KeyDefinition{
i18n.EN: "TicketBAI Exemption code",
i18n.ES: "Código de Exención de TicketBAI",
},
Desc: i18n.String{
i18n.EN: here.Doc(`
Codes used by TicketBAI for both "exempt" and "not-subject"
transactions. In the TicketBAI format these are separated,
but in order to simplify GOBL and be more closely aligned with
other countries we've combined them into one.
`),
},
Codes: []*tax.CodeDefinition{
{
Code: "E1",
Name: i18n.String{
i18n.EN: "Exempt pursuant to Article 20 of the Foral VAT Law",
i18n.ES: "Exenta por el artículo 20 de la Norma Foral del IVA",
i18n.EN: "Exempt: pursuant to Article 20 of the Foral VAT Law",
i18n.ES: "Exenta: por el artículo 20 de la Norma Foral del IVA",
},
},
{
Code: "E2",
Name: i18n.String{
i18n.EN: "Exempt pursuant to Article 21 of the Foral VAT Law",
i18n.ES: "Exenta por el artículo 21 de la Norma Foral del IVA",
i18n.EN: "Exempt: pursuant to Article 21 of the Foral VAT Law",
i18n.ES: "Exenta: por el artículo 21 de la Norma Foral del IVA",
},
},
{
Code: "E3",
Name: i18n.String{
i18n.EN: "Exempt pursuant to Article 22 of the Foral VAT Law",
i18n.ES: "Exenta por el artículo 22 de la Norma Foral del IVA",
i18n.EN: "Exempt: pursuant to Article 22 of the Foral VAT Law",
i18n.ES: "Exenta: por el artículo 22 de la Norma Foral del IVA",
},
},
{
Code: "E4",
Name: i18n.String{
i18n.EN: "Exempt pursuant to Articles 23 and 24 of the Foral VAT Law",
i18n.ES: "Exenta por el artículos 23 y 24 de la Norma Foral del IVA",
i18n.EN: "Exempt: pursuant to Articles 23 and 24 of the Foral VAT Law",
i18n.ES: "Exenta: por el artículos 23 y 24 de la Norma Foral del IVA",
},
},
{
Code: "E5",
Name: i18n.String{
i18n.EN: "Exempt pursuant to Article 25 of the Foral VAT law",
i18n.ES: "Exenta por el artículo 25 de la Norma Foral del IVA",
i18n.EN: "Exempt: pursuant to Article 25 of the Foral VAT law",
i18n.ES: "Exenta: por el artículo 25 de la Norma Foral del IVA",
},
},
{
Code: "E6",
Name: i18n.String{
i18n.EN: "Exempt pursuant to other reasons",
i18n.ES: "Exenta por otra causa",
i18n.EN: "Exempt: pursuant to other reasons",
i18n.ES: "Exenta: por otra causa",
},
},
},
},
{
Key: ExtKeyTBAINotSubject,
Name: i18n.String{
i18n.EN: "TicketBAI Not Subject Cause",
i18n.ES: "Causa no-sujeta de TicketBAI",
},
Codes: []*tax.CodeDefinition{
{
Code: "OT",
Name: i18n.String{
i18n.EN: "Not subject pursuant to Article 7 of the VAT Law. Other cases of non-subject.",
i18n.ES: "No sujeto por el artículo 7 de la Ley del IVA. Otros supuestos de no sujeción.",
i18n.EN: "Not subject: pursuant to Article 7 of the VAT Law. Other cases of non-subject.",
i18n.ES: "No sujeto: por el artículo 7 de la Ley del IVA. Otros supuestos de no sujeción.",
},
},
{
Code: "RL",
Name: i18n.String{
i18n.EN: "Not subject pursuant to localization rules.",
i18n.ES: "No sujeto por reglas de localización.",
i18n.EN: "Not subject: pursuant to localization rules.",
i18n.ES: "No sujeto: por reglas de localización.",
},
},
},
Expand Down
47 changes: 47 additions & 0 deletions regimes/es/invoices.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,23 @@ func (v *invoiceValidator) validate() error {
)),
validation.Field(&inv.Preceding,
validation.Each(validation.By(v.preceding)),
validation.Skip,
),
validation.Field(&inv.Supplier,
validation.Required,
validation.By(v.supplier),
validation.Skip,
),
validation.Field(&inv.Customer,
validation.By(v.commercialCustomer),
validation.Skip,
),
validation.Field(&inv.Lines,
validation.Each(
validation.By(v.validateLine),
validation.Skip,
),
validation.Skip,
),
)
}
Expand Down Expand Up @@ -93,3 +103,40 @@ func (v *invoiceValidator) preceding(value interface{}) error {
),
)
}

func (v *invoiceValidator) validateLine(value interface{}) error {
obj, _ := value.(*bill.Line)
if obj == nil {
return nil
}
return validation.ValidateStruct(obj,
validation.Field(&obj.Taxes,
validation.Each(
validation.By(v.validateLineTax),
validation.Skip,
),
validation.Skip,
),
)
}

func (v *invoiceValidator) validateLineTax(value interface{}) error {
obj, ok := value.(*tax.Combo)
if obj == nil || !ok {
return nil
}
zone := l10n.CodeEmpty
if v.inv.Supplier != nil && v.inv.Supplier.TaxID != nil {
zone = v.inv.Supplier.TaxID.Zone
}
return validation.ValidateStruct(obj,
validation.Field(&obj.Ext,
validation.When(
zone.In(ZonesBasqueCountry...) &&
obj.Rate == tax.RateExempt,
tax.ExtMapRequires(ExtKeyTBAIExemption),
),
validation.Skip,
),
)
}
75 changes: 75 additions & 0 deletions regimes/es/invoices_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package es_test

import (
"testing"

"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/l10n"
"github.com/invopop/gobl/num"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/regimes/es"
"github.com/invopop/gobl/tax"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func validBasqueInvoice() *bill.Invoice {
return &bill.Invoice{
Code: "123",
Supplier: &org.Party{
Name: "Test Supplier",
TaxID: &tax.Identity{
Country: l10n.ES,
Zone: es.ZoneBI,
Code: "B98602642",
},
},
Customer: &org.Party{
Name: "Test Customer",
TaxID: &tax.Identity{
Country: l10n.NL,
Code: "000099995B57",
},
},
Lines: []*bill.Line{
{
Quantity: num.MakeAmount(1, 0),
Item: &org.Item{
Name: "bogus",
Price: num.MakeAmount(10000, 2),
Unit: org.UnitPackage,
},
Taxes: tax.Set{
{
Category: "VAT",
Rate: "exempt",
Ext: tax.ExtMap{
es.ExtKeyTBAIExemption: "E1",
},
},
},
},
},
}
}

func TestBasqueLineValidation(t *testing.T) {
inv := validBasqueInvoice()
require.NoError(t, inv.Calculate())
require.NoError(t, inv.Validate())

inv.Lines[0].Taxes[0].Ext[es.ExtKeyTBAIProduct] = "services"
require.NoError(t, inv.Calculate())
require.NoError(t, inv.Validate())

inv.Lines[0].Taxes[0].Ext = nil
assertValidationError(t, inv, "es-tbai-exemption: require")
}

func assertValidationError(t *testing.T, inv *bill.Invoice, expected string) {
require.NoError(t, inv.Calculate())
err := inv.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), expected)
}
1 change: 0 additions & 1 deletion regimes/es/tax_categories.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ var taxCategories = []*tax.Category{
},
Extensions: []cbc.Key{
ExtKeyTBAIProduct,
ExtKeyTBAINotSubject,
},
Rates: []*tax.Rate{
{
Expand Down
9 changes: 9 additions & 0 deletions regimes/es/zones.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,13 @@ const (
ZoneML l10n.Code = "ML" // (52) Melilla
)

// ZonesBasqueCountry is used to identify the Basque Country
// zones that use the TicketBAI system and require additional
// validations.
var ZonesBasqueCountry = []l10n.Code{
ZoneVI,
ZoneBI,
ZoneSS,
}

var zones = tax.NewZoneStore(data.Content, "regimes/es.json")
15 changes: 5 additions & 10 deletions regimes/it/categories.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ var categories = []*tax.Category{
Map: cbc.CodeMap{
KeyFatturaPATipoRitenuta: "RT01",
},
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
ExtensionsRequired: true,
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
},
{
Code: TaxCategoryIRES,
Expand All @@ -138,8 +137,7 @@ var categories = []*tax.Category{
Map: cbc.CodeMap{
KeyFatturaPATipoRitenuta: "RT02",
},
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
ExtensionsRequired: true,
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
},
{
Code: TaxCategoryINPS,
Expand All @@ -152,8 +150,7 @@ var categories = []*tax.Category{
i18n.EN: "Contribution to the National Social Security Institute",
i18n.IT: "Contributo Istituto Nazionale della Previdenza Sociale", // nolint:misspell
},
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
ExtensionsRequired: true,
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
Map: cbc.CodeMap{
KeyFatturaPATipoRitenuta: "RT03",
},
Expand All @@ -169,8 +166,7 @@ var categories = []*tax.Category{
i18n.EN: "Contribution to the National Welfare Board for Sales Agents and Representatives",
i18n.IT: "Contributo Ente Nazionale Assistenza Agenti e Rappresentanti di Commercio", // nolint:misspell
},
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
ExtensionsRequired: true,
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
Map: cbc.CodeMap{
KeyFatturaPATipoRitenuta: "RT04",
},
Expand All @@ -186,8 +182,7 @@ var categories = []*tax.Category{
i18n.EN: "Contribution to the National Pension and Welfare Board for Doctors",
i18n.IT: "Contributo - Ente Nazionale Previdenza e Assistenza Medici", // nolint:misspell
},
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
ExtensionsRequired: true,
Extensions: []cbc.Key{ExtKeySDIRetainedTax},
Map: cbc.CodeMap{
KeyFatturaPATipoRitenuta: "RT05",
},
Expand Down
Loading

0 comments on commit a410e45

Please sign in to comment.