Skip to content
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

X-Rechnung Add-on #393

Merged
merged 26 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Changed

- `mx` – deprecated the `mx-cfdi-post-code` extension in favor of the customer address post code.
- New "tax catalogues" used for defining extensions for specific standards.
- `tax`: New "tax catalogues" used for defining extensions for specific standards.
- `iso`: catalogue created with `iso-schema-id` extensions.
- `untdid`: catalogue created with extensions: `untdid-document-type`, `untdid-payment-means`, `untdid-tax-category`, `untdid-allowance`, and `untdid-charge`.
- `eu-en16931-v2017`: addon for underlying support of the EN16931 semantic specifications.
- `de-xrechnung-v3`: addon with extra normalization for XRechnung specification in Germany.
- `pay`: Added `sepa` payment means key extension in main definition to be used with Credit Transfers and Direct Debit.
- `org`: `Identity` and `Inbox` support for extensions.
- `tax`: tags for `export` and `eea` (european economic area) for use with rates.
- `bill`: support for extensions in `Discount`, `Charge`, `LineDiscount`, and `LineCharge`.
- `bill`: specifically defined keys for Discounts and Charges.

### Changed

- `tax`: rate keys can now be extended, so `exempt+reverse-charge` will be accepted and may be used by addons to included additional codes.
- `tax`: Addons can now depend on other addons, whose keys will be automatically added during normalization.
- `cbc`: Code now allows `:` separator.

### Removed

- `pay`: UNTDID 4461 mappings from payment means table, now provided by catalogues
- `bill`: `Outlay` has been removed in favour of Charges, we've also not seen any evidence this field has been used.
- `bill`: `ref` field from discounts and charges in favour of `code`.
- `tax`: Regime `ChargeKeys` removed. Keys now provided in `bill` package.
- `it`: Charge keys no longer defined, no migration required, already supported.

## [v0.203.0]

Expand Down
2 changes: 2 additions & 0 deletions addons/addons.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
// Import all the addons to ensure they're ready to use.
_ "github.com/invopop/gobl/addons/br/nfse"
_ "github.com/invopop/gobl/addons/co/dian"
_ "github.com/invopop/gobl/addons/de/xrechnung"
_ "github.com/invopop/gobl/addons/es/facturae"
_ "github.com/invopop/gobl/addons/es/tbai"
_ "github.com/invopop/gobl/addons/eu/en16931"
_ "github.com/invopop/gobl/addons/gr/mydata"
_ "github.com/invopop/gobl/addons/it/sdi"
_ "github.com/invopop/gobl/addons/mx/cfdi"
Expand Down
4 changes: 0 additions & 4 deletions addons/co/dian/invoices.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ func validateInvoice(inv *bill.Invoice) error {
validation.Each(validation.By(validateInvoicePreceding(inv.Type))),
validation.Skip,
),
validation.Field(&inv.Outlays,
validation.Empty,
validation.Skip,
),
)
}

Expand Down
79 changes: 79 additions & 0 deletions addons/de/xrechnung/instructions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package xrechnung

import (
"github.com/invopop/gobl/pay"
"github.com/invopop/validation"
)

// ValidatePaymentInstructions validates the payment instructions according to the XRechnung standard
func validatePaymentInstructions(value interface{}) error {
instr, ok := value.(*pay.Instructions)
if !ok || instr == nil {
return nil
}

Check warning on line 13 in addons/de/xrechnung/instructions.go

View check run for this annotation

Codecov / codecov/patch

addons/de/xrechnung/instructions.go#L12-L13

Added lines #L12 - L13 were not covered by tests
return validation.ValidateStruct(instr,
// BR-DE-23
validation.Field(&instr.CreditTransfer,
validation.When(
instr.Key.Has(pay.MeansKeyCreditTransfer),
validation.Required,
validation.Each(validation.By(validateCreditTransfer)),
),
validation.Skip,
),
// BR-DE-24
validation.Field(&instr.Card,
validation.When(
instr.Key.Has(pay.MeansKeyCard),
validation.Required,
),
validation.Skip,
),
// BR-DE-25
validation.Field(&instr.DirectDebit,
validation.When(
instr.Key.Has(pay.MeansKeyDirectDebit),
validation.Required,
validation.By(validateInstructionsDirectDebit),
validation.Skip,
),
),
)
}

func validateInstructionsDirectDebit(value interface{}) error {
dd, ok := value.(*pay.DirectDebit)
if !ok || dd == nil {
return nil
}

Check warning on line 48 in addons/de/xrechnung/instructions.go

View check run for this annotation

Codecov / codecov/patch

addons/de/xrechnung/instructions.go#L47-L48

Added lines #L47 - L48 were not covered by tests
return validation.ValidateStruct(dd,
// BR-DE-29 - Changed to Peppol-EN16931-R061
validation.Field(&dd.Ref,
validation.Required,
),
// BR-DE-30
validation.Field(&dd.Creditor,
validation.Required,
),
// BR-DE-31
validation.Field(&dd.Account,
validation.Required,
),
)
}

// BR-DE-19
func validateCreditTransfer(value interface{}) error {
ct, ok := value.(*pay.CreditTransfer)
if ct == nil || !ok {
return nil
}

Check warning on line 70 in addons/de/xrechnung/instructions.go

View check run for this annotation

Codecov / codecov/patch

addons/de/xrechnung/instructions.go#L69-L70

Added lines #L69 - L70 were not covered by tests
return validation.ValidateStruct(ct,
validation.Field(&ct.Number,
validation.When(
ct.IBAN == "",
validation.Required,
),
),
)
}
110 changes: 110 additions & 0 deletions addons/de/xrechnung/instructions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package xrechnung_test

import (
"testing"

"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/pay"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

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: "credit-transfer+sepa",
CreditTransfer: []*pay.CreditTransfer{
{
IBAN: "DE89370400440532013000",
BIC: "DEUTDEFF",
},
},
},
}
require.NoError(t, inv.Calculate())
assert.NoError(t, inv.Validate())
})

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: pay.MeansKeyCreditTransfer.With(pay.MeansKeySEPA),
CreditTransfer: []*pay.CreditTransfer{
{
BIC: "DEUTDEFF",
},
},
},
}
require.NoError(t, inv.Calculate())
err := inv.Validate()
assert.ErrorContains(t, err, "payment: (instructions: (credit_transfer: (0: (number: cannot be blank.).).).)")
})

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{},
},
}
require.NoError(t, inv.Calculate())
assert.NoError(t, inv.Validate())
})

t.Run("valid invoice with SEPA direct debit", func(t *testing.T) {
inv := invoiceTemplate(t)
inv.Payment = &bill.Payment{
Instructions: &pay.Instructions{
Key: "direct-debit+sepa",
DirectDebit: &pay.DirectDebit{
Ref: "MANDATE123",
Creditor: "DE98ZZZ09999999999",
Account: "DE89370400440532013000",
},
},
}
require.NoError(t, inv.Calculate())
assert.NoError(t, inv.Validate())
})

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: "direct-debit+sepa",
DirectDebit: &pay.DirectDebit{
Creditor: "DE98ZZZ09999999999",
Account: "DE89370400440532013000",
},
},
}
require.NoError(t, inv.Calculate())
err := inv.Validate()
assert.ErrorContains(t, err, "payment: (instructions: (direct_debit: (ref: cannot be blank.).).)")
})

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"),
},
}
require.NoError(t, inv.Calculate())
err := inv.Validate()
assert.ErrorContains(t, err, "payment: (instructions: (key: must be or start with a valid key.).)")
})
}
54 changes: 54 additions & 0 deletions addons/de/xrechnung/invoices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package xrechnung

import (
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/catalogues/untdid"
"github.com/invopop/gobl/tax"
"github.com/invopop/validation"
)

// BR-DE-17 - restricted subset of UNTDID document type codes
var validInvoiceUNTDIDDocumentTypeValues = []tax.ExtValue{
"326", // Partial
"380", // Commercial
"384", // Corrected
"389", // Self-billed
"381", // Credit note
"875", // Partial construction invoice
"876", // Partial Final construction invoice
"877", // Final construction invoice
}

// validateInvoice validates the invoice according to the XRechnung standard
func validateInvoice(inv *bill.Invoice) error {
return validation.ValidateStruct(inv,
// BR-DE-17
validation.Field(&inv.Tax,
validation.By(validateInvoiceTax),
validation.Skip,
),
validation.Field(&inv.Preceding,
validation.When(
inv.Type.In(
bill.InvoiceTypeCorrective,
bill.InvoiceTypeCreditNote,
),
validation.Required,
),
validation.Skip,
),
)
}

func validateInvoiceTax(value any) error {
tx, ok := value.(*bill.Tax)
if !ok || tx == nil {
return nil
}

Check warning on line 47 in addons/de/xrechnung/invoices.go

View check run for this annotation

Codecov / codecov/patch

addons/de/xrechnung/invoices.go#L46-L47

Added lines #L46 - L47 were not covered by tests
return validation.ValidateStruct(tx,
validation.Field(&tx.Ext,
tax.ExtensionsHasValues(untdid.ExtKeyTaxCategory, validInvoiceUNTDIDDocumentTypeValues...),
validation.Skip,
),
)
}
Loading
Loading