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
Changes from 1 commit
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
Next Next commit
Initial Draft for DE Addon
apardods authored and samlown committed Oct 31, 2024
commit 4c33511215cd0cef47fd6d7b483ad47481609445
185 changes: 185 additions & 0 deletions addons/de/xrechnung/invoices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package xrechnung

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

func normalizeInvoice(inv *bill.Invoice) {
// Ensure payment instructions are present
if inv.Payment == nil {
inv.Payment = &bill.Payment{}
}
if inv.Payment.Instructions == nil {
inv.Payment.Instructions = &pay.Instructions{}
}

// Ensure invoice type is valid
if !isValidInvoiceType(inv.Type) {
inv.Type = bill.InvoiceTypeStandard
}
}

func isValidInvoiceType(t cbc.Key) bool {
validTypes := []cbc.Key{
bill.InvoiceTypeStandard,
bill.InvoiceTypeCreditNote,
bill.InvoiceTypeCorrective,
invoiceTypeSelfBilled,
invoiceTypePartial,
}
for _, validType := range validTypes {
if t == validType {
return true
}
}
return false
}

func validateInvoice(inv *bill.Invoice) error {
return validation.ValidateStruct(inv,
validation.Field(&inv.Type,
validation.In(bill.InvoiceTypeStandard, bill.InvoiceTypeCreditNote, bill.InvoiceTypeCorrective, invoiceTypeSelfBilled, invoiceTypePartial),
),
validation.Field(&inv.Payment.Instructions,
validation.Required,
),
validation.Field(&inv.Supplier,
validation.By(validateParty),
),
validation.Field(&inv.Customer,
validation.By(validateParty),
),
validation.Field(&inv.Delivery,
validation.When(inv.Delivery != nil,
validation.By(validateDeliveryParty),
),
),
)
}

func validateParty(value interface{}) error {
party, _ := value.(*org.Party)
if party == nil {
return nil
}
return validation.ValidateStruct(party,
validation.Field(&party.Name,
validation.Required,
),
validation.Field(&party.Addresses,
validation.Required,
validation.Length(1, 1),
validation.Each(validation.By(validateAddress)),
),
validation.Field(&party.People,
validation.Required,
validation.Length(1, 1),
),
validation.Field(&party.Telephones,
validation.Required,
validation.Length(1, 1),
),
validation.Field(&party.Emails,
validation.Required,
validation.Length(1, 1),
),
)
}

func validateAddress(value interface{}) error {
addr, _ := value.(*org.Address)
if addr == nil {
return nil
}
return validation.ValidateStruct(addr,
validation.Field(&addr.Locality,
validation.Required,
),
validation.Field(&addr.Code,
validation.Required,
),
)
}

func validateDeliveryParty(value interface{}) error {
party, _ := value.(*org.Party)
if party == nil {
return nil
}
return validation.ValidateStruct(party,
validation.Field(&party.Addresses,
validation.Required,
validation.Length(1, 1),
validation.Each(validation.By(validateGermanAddress)),
),
)
}

func validateGermanAddress(value interface{}) error {
addr, _ := value.(*org.Address)
if addr == nil {
return nil
}
return validation.ValidateStruct(addr,
validation.Field(&addr.Locality,
validation.Required,
),
validation.Field(&addr.Code,
validation.Required,
),
validation.Field(&addr.Country,
validation.In("DE"),
),
)
}

func validateTaxCombo(tc *tax.Combo) error {
if tc == nil {
return nil
}
return validation.ValidateStruct(tc,
validation.Field(&tc.Category,
validation.When(tc.Category == tax.CategoryVAT,
validation.By(validateVATRate),
),
),
)
}

func validateVATRate(value interface{}) error {
rate, _ := value.(cbc.Key)
if rate == "" {
return validation.NewError("required", "VAT category rate is required")
}
return nil
}

func validatePayInstructions(instructions *pay.Instructions) error {
return validation.ValidateStruct(instructions,
validation.Field(&instructions.CreditTransfer,
validation.When(instructions.Key == pay.MeansKeyCreditTransfer,
validation.By(validateCreditTransfer),
),
),
)
}

func validateCreditTransfer(value interface{}) error {
credit, _ := value.(*pay.CreditTransfer)
if credit == nil {
return nil
}
return nil
// return validation.ValidateStruct(credit,
// validation.Field(&credit.IBAN,
// validation.When(credit.Key == pay.MeansKeyCreditTransfer,
// validation.Required,
// ),
// ),
// )
}
69 changes: 69 additions & 0 deletions addons/de/xrechnung/xrechnung.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package xrechnung

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/pay"
"github.com/invopop/gobl/pkg/here"
"github.com/invopop/gobl/tax"
)

const (
V3 cbc.Key = "de-xrechnung-3.0.2"
)

const (
invoiceTypeSelfBilled cbc.Key = "389"
invoiceTypePartial cbc.Key = "326"
)

func init() {
tax.RegisterAddonDef(newAddon())
}

func newAddon() *tax.AddonDef {
return &tax.AddonDef{
Key: V3,
Name: i18n.String{
i18n.EN: "German XRechnung 3.0.2",
},
Description: i18n.String{
i18n.EN: here.Doc(`
Extensions to support the German XRechnung standard version 3.0.2 for electronic invoicing.
XRechnung is based on the European Norm (EN) 16931 and is mandatory for business-to-government
(B2G) invoices in Germany. This addon provides the necessary structures and validations to
ensure compliance with the XRechnung format.
For more information on XRechnung, visit:
https://www.xrechnung.de/
`),
},
// Extensions: extensions,
// Identities: identities,
Normalizer: normalize,
Validator: validate,
}
}

func normalize(doc any) {
switch obj := doc.(type) {
case *bill.Invoice:
normalizeInvoice(obj)
}
}

func validate(doc any) error {
switch obj := doc.(type) {
case *bill.Invoice:
return validateInvoice(obj)
case *pay.Instructions:
return validatePayInstructions(obj)
case *org.Party:
return validateParty(obj)
case *tax.Combo:
return validateTaxCombo(obj)
}
return nil
}