Skip to content

Commit

Permalink
Refactoring handling of Verifactu extensions and scenarios, plus othe…
Browse files Browse the repository at this point in the history
…r simplifications
  • Loading branch information
samlown committed Dec 4, 2024
1 parent 2c7873a commit 6aa13a0
Show file tree
Hide file tree
Showing 23 changed files with 769 additions and 250 deletions.
163 changes: 163 additions & 0 deletions addons/es/verifactu/bill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package verifactu

import (
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/regimes/es"
"github.com/invopop/gobl/tax"
"github.com/invopop/validation"
)

var invoiceCorrectionDefinitions = tax.CorrectionSet{
{
Schema: bill.ShortSchemaInvoice,
Extensions: []cbc.Key{
ExtKeyDocType,
},
},
}

func normalizeInvoice(inv *bill.Invoice) {
// Try to move any preceding choices to the document level
for _, row := range inv.Preceding {
if len(row.Ext) == 0 {
continue
}
found := false
if row.Ext.Has(ExtKeyDocType) {
if inv.Tax == nil || !found {
inv.Tax = inv.Tax.MergeExtensions(tax.Extensions{
ExtKeyDocType: row.Ext[ExtKeyDocType],
})
found = true // only assign first one
}
delete(row.Ext, ExtKeyDocType)
}
}

// Try to normalize the correction type, which is especially complex for
// Verifactu implying that scenarios cannot be used.
switch inv.Type {
case bill.InvoiceTypeCreditNote, bill.InvoiceTypeDebitNote:
inv.Tax = inv.Tax.MergeExtensions(tax.Extensions{
ExtKeyCorrectionType: "I",
})
case bill.InvoiceTypeCorrective:
if inv.Tax == nil || inv.Tax.Ext.Get(ExtKeyDocType) != "F3" {
inv.Tax = inv.Tax.MergeExtensions(tax.Extensions{
ExtKeyCorrectionType: "S",
})
} else {
// Substitutions of simplified invoices cannot have a correction type
delete(inv.Tax.Ext, ExtKeyCorrectionType)
}
}
}

func validateInvoice(inv *bill.Invoice) error {
return validation.ValidateStruct(inv,
validation.Field(&inv.Customer,
validation.By(validateInvoiceCustomer),
validation.Skip,
),
validation.Field(&inv.Preceding,
validation.When(
inv.Type.In(es.InvoiceCorrectionTypes...),
validation.Required,
),
validation.Each(
validation.By(validateInvoicePreceding),
),
validation.Skip,
),
validation.Field(&inv.Tax,
validation.Required,
validation.By(validateInvoiceTax(inv.Type)),
validation.Skip,
),
validation.Field(&inv.Notes,
cbc.ValidateNotesHasKey(cbc.NoteKeyGeneral),
validation.Skip,
),
)
}

var docTypesStandard = []tax.ExtValue{ // Standard invoices
"F1", "F2",
}
var docTypesCreditDebit = []tax.ExtValue{ // Credit or Debit notes
"R1", "R2", "R3", "R4", "R5",
}
var docTypesCorrective = []tax.ExtValue{ // Substitutions
"F3", "R1", "R2", "R3", "R4", "R5",
}

func validateInvoiceTax(it cbc.Key) validation.RuleFunc {
return func(val any) error {
obj := val.(*bill.Tax)
return validation.ValidateStruct(obj,
validation.Field(&obj.Ext,
tax.ExtensionsRequires(ExtKeyDocType),
validation.When(
it.In(bill.InvoiceTypeStandard),
tax.ExtensionsHasValues(
ExtKeyDocType,
docTypesStandard...,
),
),
validation.When(
it.In(bill.InvoiceTypeCreditNote, bill.InvoiceTypeDebitNote),
tax.ExtensionsHasValues(
ExtKeyDocType,
docTypesCreditDebit...,
),
),
validation.When(
it.In(bill.InvoiceTypeCorrective),
tax.ExtensionsHasValues(
ExtKeyDocType,
docTypesCorrective...,
),
),
validation.When(
obj.Ext.Get(ExtKeyDocType).In(docTypesCreditDebit...),
tax.ExtensionsRequires(ExtKeyCorrectionType),
),
validation.Skip,
),
)
}
}

func validateInvoiceCustomer(val any) error {
obj, ok := val.(*org.Party)
if !ok || obj == nil {
return nil
}
// Customers must have a tax ID to at least set the country,
// and Spanish ones should also have an ID. There are more complex
// rules for exports.
return validation.ValidateStruct(obj,
validation.Field(&obj.TaxID,
validation.Required,
validation.When(
obj.TaxID != nil && obj.TaxID.Country.In("ES"),
tax.RequireIdentityCode,
),
validation.Skip,
),
)
}

func validateInvoicePreceding(val any) error {
p, ok := val.(*org.DocumentRef)
if !ok || p == nil {
return nil
}

Check warning on line 157 in addons/es/verifactu/bill.go

View check run for this annotation

Codecov / codecov/patch

addons/es/verifactu/bill.go#L156-L157

Added lines #L156 - L157 were not covered by tests
return validation.ValidateStruct(p,
validation.Field(&p.IssueDate,
validation.Required,
),
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package verifactu_test

import (
"encoding/json"
"testing"

"github.com/invopop/gobl/addons/es/verifactu"
Expand Down Expand Up @@ -29,8 +30,10 @@ func TestInvoiceValidation(t *testing.T) {

t.Run("without exemption reason", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Lines[0].Taxes[0].Rate = ""
inv.Lines[0].Taxes[0].Percent = num.NewPercentage(21, 2)
inv.Lines[0].Taxes[0].Ext = nil
assertValidationError(t, inv, "es-verifactu-tax-classification: required")
assertValidationError(t, inv, "es-verifactu-op-class: required")
})

t.Run("without notes", func(t *testing.T) {
Expand All @@ -56,6 +59,17 @@ func TestInvoiceValidation(t *testing.T) {
assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "F2")
})

t.Run("simplified substitution", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.SetTags(tax.TagSimplified)
require.NoError(t, inv.Calculate())

inv.Correct(bill.Corrective, bill.WithExtension(verifactu.ExtKeyDocType, "F3"))

Check failure on line 67 in addons/es/verifactu/bill_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `inv.Correct` is not checked (errcheck)
require.NoError(t, inv.Validate())
assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "F3")
assert.Empty(t, inv.Tax.Ext[verifactu.ExtKeyCorrectionType])
})

t.Run("correction invoice requires preceding", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Type = bill.InvoiceTypeCreditNote
Expand All @@ -82,28 +96,19 @@ func TestInvoiceValidation(t *testing.T) {
Series: "ABC",
Code: "122",
IssueDate: &d,
Ext: tax.Extensions{
verifactu.ExtKeyDocType: "R1",
},
},
}
require.NoError(t, inv.Calculate())
data, _ := json.MarshalIndent(inv, "", " ")
t.Log(string(data))
require.NoError(t, inv.Validate())
assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "R1")
assert.Empty(t, inv.Preceding[0].Ext)
})

t.Run("substitution invoice", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Tags = tax.Tags{List: []cbc.Key{verifactu.TagSubstitution}}
d := cal.MakeDate(2024, 1, 1)
inv.Preceding = []*org.DocumentRef{
{
Series: "ABC",
Code: "122",
IssueDate: &d,
},
}
require.NoError(t, inv.Calculate())
require.NoError(t, inv.Validate())
assert.Equal(t, inv.Tax.Ext[verifactu.ExtKeyDocType].String(), "F3")
})
}

func assertValidationError(t *testing.T, inv *bill.Invoice, expected string) {
Expand Down Expand Up @@ -143,9 +148,6 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice {
{
Category: "VAT",
Rate: "standard",
Ext: tax.Extensions{
"es-verifactu-tax-classification": "S1",
},
},
},
},
Expand Down
64 changes: 56 additions & 8 deletions addons/es/verifactu/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (

// Extension keys for Verifactu
const (
ExtKeyDocType cbc.Key = "es-verifactu-doc-type"
ExtKeyTaxClassification cbc.Key = "es-verifactu-tax-classification"
ExtKeyDocType cbc.Key = "es-verifactu-doc-type"
ExtKeyOpClass cbc.Key = "es-verifactu-op-class"
ExtKeyCorrectionType cbc.Key = "es-verifactu-correction-type"
ExtKeyExempt cbc.Key = "es-verifactu-exempt"
)

var extensions = []*cbc.KeyDefinition{
Expand Down Expand Up @@ -44,7 +46,7 @@ var extensions = []*cbc.KeyDefinition{
{
Value: "F3",
Name: i18n.String{
i18n.EN: "Invoice in substitution of simplified invoices.",
i18n.EN: "Invoice issued as a replacement for simplified invoices that have been billed and declared.",
i18n.ES: "Factura emitida en sustitución de facturas simplificadas facturadas y declaradas.",
},
},
Expand Down Expand Up @@ -86,17 +88,49 @@ var extensions = []*cbc.KeyDefinition{
},
},
{
Key: ExtKeyTaxClassification,
Key: ExtKeyCorrectionType,
Name: i18n.String{
i18n.EN: "Verifactu Tax Classification/Exemption Code - L9/10",
i18n.ES: "Código de Clasificación/Exención de Impuesto de Verifactu - L9/10",
i18n.EN: "Verifactu Correction Type Code - L3",
i18n.ES: "Código de Tipo de Corrección de Verifactu - L3",
},
Desc: i18n.String{
i18n.EN: here.Doc(`
Tax classification code used to identify the type of tax being applied to the line. It includes both exemption reasons and tax scenarios.
These lists are separate in Verifactu but combined here for convenience.
Correction type code used to identify the type of correction being made.
This value will be determined automatically according to the invoice type.
Corrective invoices will be marked as "S", while credit and debit notes as "I".
`),
},
Values: []*cbc.ValueDefinition{
{
Value: "S",
Name: i18n.String{
i18n.EN: "Substitution",
i18n.ES: "Por Sustitución",
},
},
{
Value: "I",
Name: i18n.String{
i18n.EN: "Differences",
i18n.ES: "Por Diferencias",
},
},
},
},
{
Key: ExtKeyOpClass,
Name: i18n.String{
i18n.EN: "Verifactu Operation Classification/Exemption Code - L9",
i18n.ES: "Código de Clasificación/Exención de Impuesto de Verifactu - L9",
},
Desc: i18n.String{
i18n.EN: here.Doc(`
Operation classification code used to identify if taxes should be applied to the line.
Source: VeriFactu Ministerial Order:
* https://www.boe.es/diario_boe/txt.php?id=BOE-A-2024-22138
For details on how best to use and apply these and other codes, see the
AEAT FAQ:
* https://sede.agenciatributaria.gob.es/Sede/impuestos-tasas/iva/iva-libros-registro-iva-traves-aeat/preguntas-frecuentes/3-libro-registro-facturas-expedidas.html?faqId=b5556c3d02bc9510VgnVCM100000dc381e0aRCRD
`),
},
Values: []*cbc.ValueDefinition{
Expand Down Expand Up @@ -128,6 +162,20 @@ var extensions = []*cbc.KeyDefinition{
i18n.ES: "Operación No Sujeta por Reglas de localización",
},
},
},
},
{
Key: ExtKeyExempt,
Name: i18n.String{
i18n.EN: "Verifactu Exemption Code - L10",
i18n.ES: "Código de Exención de Impuesto de Verifactu - L10",
},
Desc: i18n.String{
i18n.EN: here.Doc(`
Exemption code used to explain why the operation is exempt from taxes.
`),
},
Values: []*cbc.ValueDefinition{
{
Value: "E1",
Name: i18n.String{
Expand Down
Loading

0 comments on commit 6aa13a0

Please sign in to comment.