diff --git a/addons/de/xrechnung/extensions.go b/addons/de/xrechnung/extensions.go index 1a3158a5..e742933f 100644 --- a/addons/de/xrechnung/extensions.go +++ b/addons/de/xrechnung/extensions.go @@ -3,6 +3,7 @@ package xrechnung import ( "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/pkg/here" ) // ExtKeyTaxRate is the key for the tax rate extension in XRechnung @@ -18,6 +19,14 @@ var extensions = []*cbc.KeyDefinition{ i18n.EN: "Tax Rate", i18n.DE: "Steuersatz", }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Code used to describe the applicable tax rate. Taken from the UNTDID 5305 code list. + `), + i18n.DE: here.Doc(` + Code verwendet um den anwendbaren Steuersatz zu beschreiben. Entnommen aus der UNTDID 5305 Code-Liste. + `), + }, Values: []*cbc.ValueDefinition{ { Value: "S", @@ -90,6 +99,14 @@ var extensions = []*cbc.KeyDefinition{ i18n.EN: "Document Type", i18n.DE: "Dokumentenart", }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + Code used to describe the type of document. + `), + i18n.DE: here.Doc(` + Code verwendet um die Art des Dokuments zu beschreiben. + `), + }, Values: []*cbc.ValueDefinition{ { Value: "326", diff --git a/addons/de/xrechnung/instructions.go b/addons/de/xrechnung/instructions.go index 13b9bf91..8d3d9c44 100644 --- a/addons/de/xrechnung/instructions.go +++ b/addons/de/xrechnung/instructions.go @@ -24,7 +24,7 @@ var validPaymentKeys = []cbc.Key{ } // ValidatePaymentInstructions validates the payment instructions according to the XRechnung standard -func ValidatePaymentInstructions(value interface{}) error { +func validatePaymentInstructions(value interface{}) error { instr, ok := value.(*pay.Instructions) if !ok || instr == nil { return nil @@ -48,6 +48,7 @@ func ValidatePaymentInstructions(value interface{}) error { validation.When(instr.Key == pay.MeansKeyCard, validation.Required, ), + validation.Skip, ), // BR-DE-25 validation.Field(&instr.DirectDebit, @@ -66,7 +67,7 @@ func validatePaymentKey(value interface{}) error { return validation.NewError("invalid_key", "invalid payment key") } if !t.In(validPaymentKeys...) { - return validation.NewError("invalid", "Invalid payment key") + return validation.NewError("invalid", "invalid payment key") } return nil } @@ -79,15 +80,15 @@ func validateDirectDebit(value interface{}) error { return validation.ValidateStruct(dd, // BR-DE-29 - Changed to Peppol-EN16931-R061 validation.Field(&dd.Ref, - validation.Required.Error("Mandate reference is mandatory for direct debit"), + validation.Required, ), // BR-DE-30 validation.Field(&dd.Creditor, - validation.Required.Error("Creditor identifier is mandatory for direct debit"), + validation.Required, ), // BR-DE-31 validation.Field(&dd.Account, - validation.Required.Error("Debited account identifier is mandatory for direct debit"), + validation.Required, ), ) } @@ -101,7 +102,7 @@ func validateCreditTransfer(value interface{}) error { return validation.ValidateStruct(creditTransfer, validation.Field(&creditTransfer.Number, validation.When(creditTransfer.IBAN == "", - validation.Required.Error("IBAN must be provided for SEPA credit transfer"), + validation.Required, ), ), ) diff --git a/addons/de/xrechnung/instructions_test.go b/addons/de/xrechnung/instructions_test.go index 9d208944..36e8a14f 100644 --- a/addons/de/xrechnung/instructions_test.go +++ b/addons/de/xrechnung/instructions_test.go @@ -4,72 +4,98 @@ import ( "testing" "github.com/invopop/gobl/addons/de/xrechnung" + "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/pay" "github.com/stretchr/testify/assert" ) -func TestPaymentInstructions(t *testing.T) { - t.Run("valid SEPA credit transfer", func(t *testing.T) { - instr := &pay.Instructions{ - Key: xrechnung.KeyPaymentMeansSEPACreditTransfer, - CreditTransfer: []*pay.CreditTransfer{ - { - IBAN: "DE89370400440532013000", - BIC: "DEUTDEFF", +func invoiceTemplate(t *testing.T) *bill.Invoice { + t.Helper() + inv := testInvoiceStandard(t) + inv.Payment = nil + return inv +} + +func TestValidateInvoice(t *testing.T) { + t.Run("valid invoice with SEPA credit transfer", func(t *testing.T) { + inv := invoiceTemplate(t) + inv.Payment = &bill.Payment{ + Instructions: &pay.Instructions{ + Key: cbc.Key("sepa-credit-transfer"), + CreditTransfer: []*pay.CreditTransfer{ + { + IBAN: "DE89370400440532013000", + BIC: "DEUTDEFF", + }, }, }, } - assert.NoError(t, xrechnung.ValidatePaymentInstructions(instr)) + assert.NoError(t, xrechnung.ValidateInvoice(inv)) }) - t.Run("missing IBAN for SEPA credit transfer", func(t *testing.T) { - instr := &pay.Instructions{ - Key: xrechnung.KeyPaymentMeansSEPACreditTransfer, - CreditTransfer: []*pay.CreditTransfer{ - { - BIC: "DEUTDEFF", + t.Run("invalid invoice with missing IBAN for SEPA credit transfer", func(t *testing.T) { + inv := invoiceTemplate(t) + inv.Payment = &bill.Payment{ + Instructions: &pay.Instructions{ + Key: xrechnung.KeyPaymentMeansSEPACreditTransfer, + CreditTransfer: []*pay.CreditTransfer{ + { + BIC: "DEUTDEFF", + }, }, }, } - assert.Error(t, xrechnung.ValidatePaymentInstructions(instr)) + assert.Error(t, xrechnung.ValidateInvoice(inv)) }) - t.Run("valid card payment", func(t *testing.T) { - instr := &pay.Instructions{ - Key: pay.MeansKeyCard, - Card: &pay.Card{}, + t.Run("valid invoice with card payment", func(t *testing.T) { + inv := invoiceTemplate(t) + inv.Payment = &bill.Payment{ + Instructions: &pay.Instructions{ + Key: pay.MeansKeyCard, + Card: &pay.Card{}, + }, } - assert.NoError(t, xrechnung.ValidatePaymentInstructions(instr)) + assert.NoError(t, xrechnung.ValidateInvoice(inv)) }) - t.Run("valid SEPA direct debit", func(t *testing.T) { - instr := &pay.Instructions{ - Key: xrechnung.KeyPaymentMeansSEPADirectDebit, - DirectDebit: &pay.DirectDebit{ - Ref: "MANDATE123", - Creditor: "DE98ZZZ09999999999", - Account: "DE89370400440532013000", + t.Run("valid invoice with SEPA direct debit", func(t *testing.T) { + inv := invoiceTemplate(t) + inv.Payment = &bill.Payment{ + Instructions: &pay.Instructions{ + Key: xrechnung.KeyPaymentMeansSEPADirectDebit, + DirectDebit: &pay.DirectDebit{ + Ref: "MANDATE123", + Creditor: "DE98ZZZ09999999999", + Account: "DE89370400440532013000", + }, }, } - assert.NoError(t, xrechnung.ValidatePaymentInstructions(instr)) + assert.NoError(t, xrechnung.ValidateInvoice(inv)) }) - t.Run("missing mandate reference for direct debit", func(t *testing.T) { - instr := &pay.Instructions{ - Key: xrechnung.KeyPaymentMeansSEPADirectDebit, - DirectDebit: &pay.DirectDebit{ - Creditor: "DE98ZZZ09999999999", - Account: "DE89370400440532013000", + t.Run("invalid invoice with missing mandate reference for direct debit", func(t *testing.T) { + inv := invoiceTemplate(t) + inv.Payment = &bill.Payment{ + Instructions: &pay.Instructions{ + Key: xrechnung.KeyPaymentMeansSEPADirectDebit, + DirectDebit: &pay.DirectDebit{ + Creditor: "DE98ZZZ09999999999", + Account: "DE89370400440532013000", + }, }, } - assert.Error(t, xrechnung.ValidatePaymentInstructions(instr)) + assert.Error(t, xrechnung.ValidateInvoice(inv)) }) - t.Run("invalid payment key", func(t *testing.T) { - instr := &pay.Instructions{ - Key: cbc.Key("invalid-key"), + t.Run("invalid invoice with invalid payment key", func(t *testing.T) { + inv := invoiceTemplate(t) + inv.Payment = &bill.Payment{ + Instructions: &pay.Instructions{ + Key: cbc.Key("invalid-key"), + }, } - assert.Error(t, xrechnung.ValidatePaymentInstructions(instr)) + assert.Error(t, xrechnung.ValidateInvoice(inv)) }) } diff --git a/addons/de/xrechnung/invoices.go b/addons/de/xrechnung/invoices.go index e768d7e0..1b713be3 100644 --- a/addons/de/xrechnung/invoices.go +++ b/addons/de/xrechnung/invoices.go @@ -19,36 +19,19 @@ func ValidateInvoice(inv *bill.Invoice) error { // BR-DE-17 validation.Field(&inv.Type, validation.By(validateInvoiceType), + validation.Skip, ), // BR-DE-01 - validation.Field(&inv.Payment, validation.Required), validation.Field(&inv.Payment, - validation.By(func(value interface{}) error { - payment, ok := value.(*bill.Payment) - if !ok || payment == nil { - return validation.NewError("payment_type", "must be a valid non-empty Payment type") - } - return validation.ValidateStruct(payment, - validation.Field(&payment.Instructions, - validation.Required, - validation.By(ValidatePaymentInstructions), - validation.Skip, - ), - ) - }), + validation.Required, + validation.By(validatePayment), + validation.Skip, ), // BR-DE-15 - validation.Field(&inv.Ordering, validation.Required), validation.Field(&inv.Ordering, - validation.By(func(value interface{}) error { - ordering, ok := value.(*bill.Ordering) - if !ok || ordering == nil { - return validation.NewError("ordering_type", "must be a valid Ordering type") - } - return validation.ValidateStruct(ordering, - validation.Field(&ordering.Code, validation.Required), - ) - }), + validation.Required, + validation.By(validateOrdering), + validation.Skip, ), validation.Field(&inv.Supplier, validation.By(validateSupplier), @@ -75,13 +58,38 @@ func ValidateInvoice(inv *bill.Invoice) error { ) } +func validatePayment(value interface{}) error { + payment, ok := value.(*bill.Payment) + if !ok || payment == nil { + return nil + } + return validation.ValidateStruct(payment, + validation.Field(&payment.Instructions, + validation.Required, + validation.By(validatePaymentInstructions), + ), + ) +} + +func validateOrdering(value interface{}) error { + ordering, ok := value.(*bill.Ordering) + if !ok || ordering == nil { + return nil + } + return validation.ValidateStruct(ordering, + validation.Field(&ordering.Code, + validation.Required, + ), + ) +} + func validateInvoiceType(value interface{}) error { t, ok := value.(cbc.Key) if !ok { - return validation.NewError("type", "Invalid invoice type") + return validation.NewError("type", "invalid invoice type") } if !t.In(validTypes...) { - return validation.NewError("invalid", "Invalid invoice type") + return validation.NewError("invalid", "invalid invoice type") } return nil } @@ -142,10 +150,10 @@ func validateSupplierTaxInfo(value interface{}) error { func validateTaxNumber(value interface{}) error { identities, ok := value.([]*org.Identity) if !ok { - return validation.NewError("invalid_identities", "Identities are invalid") + return validation.NewError("invalid_identities", "identities are invalid") } if org.IdentityForKey(identities, "de-tax-number") == nil { - return validation.NewError("missing_tax_identifier", "German tax identifier (de-tax-number) is required") + return validation.NewError("missing_tax_identifier", "tax identifier (de-tax-number) is required") } return nil } diff --git a/addons/de/xrechnung/scenarios.go b/addons/de/xrechnung/scenarios.go index f1960d7e..204868c8 100644 --- a/addons/de/xrechnung/scenarios.go +++ b/addons/de/xrechnung/scenarios.go @@ -19,11 +19,11 @@ const ( // Invoice type constants const ( - invoiceTypeSelfBilled = "380" + invoiceTypeSelfBilled = "389" invoiceTypePartial = "326" - invoiceTypePartialConstruction = "80" - invoiceTypePartialFinalConstruction = "84" - invoiceTypeFinalConstruction = "389" + invoiceTypePartialConstruction = "875" + invoiceTypePartialFinalConstruction = "876" + invoiceTypeFinalConstruction = "877" ) var invoiceTags = &tax.TagSet{ @@ -150,6 +150,7 @@ var scenarios = []*tax.ScenarioSet{ ExtKeyTaxRate: "AE", }, }, + // TODO: Map Scenarios { Ext: tax.Extensions{ ExtKeyTaxRate: "K", diff --git a/addons/de/xrechnung/xrechnung.go b/addons/de/xrechnung/xrechnung.go index fa927c68..9ed4a02a 100644 --- a/addons/de/xrechnung/xrechnung.go +++ b/addons/de/xrechnung/xrechnung.go @@ -35,6 +35,10 @@ func newAddon() *tax.AddonDef { https://www.xrechnung.de/ `), }, + Tags: []*tax.TagSet{ + invoiceTags, + }, + Scenarios: scenarios, Extensions: extensions, Normalizer: normalize, Validator: validate,