From 950fbfe5de16f164308431db3d842b2324e9b2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Wed, 27 Dec 2023 17:10:41 +0000 Subject: [PATCH] Revamp to use TBAI extensions --- README.md | 25 +- go.mod | 4 +- go.sum | 15 +- internal/doc/breakdown.go | 390 +++++++----------- internal/doc/breakdown_test.go | 109 ++++- internal/doc/invoice.go | 10 +- internal/doc/invoice_test.go | 2 +- .../data/invoice-es-nl-tbai-b2c-explicit.json | 111 +++++ .../data/invoice-es-nl-tbai-b2c-explicit.yaml | 46 +++ test/data/invoice-es-nl-tbai-b2c.json | 174 ++++++++ test/data/invoice-es-nl-tbai-b2c.yaml | 68 +++ test/data/invoice-es-nl-tbai-exempt.json | 112 +++++ test/data/invoice-es-nl-tbai-exempt.yaml | 47 +++ .../out/invoice-es-nl-tbai-b2c-explicit.xml | 161 ++++++++ test/data/out/invoice-es-nl-tbai-b2c.xml | 183 ++++++++ test/data/out/invoice-es-nl-tbai-exempt.xml | 163 ++++++++ test/data/sample-invoice-export.json | 25 +- test/data/sample-invoice-export.yaml | 21 +- 18 files changed, 1341 insertions(+), 325 deletions(-) create mode 100644 test/data/invoice-es-nl-tbai-b2c-explicit.json create mode 100644 test/data/invoice-es-nl-tbai-b2c-explicit.yaml create mode 100644 test/data/invoice-es-nl-tbai-b2c.json create mode 100644 test/data/invoice-es-nl-tbai-b2c.yaml create mode 100644 test/data/invoice-es-nl-tbai-exempt.json create mode 100644 test/data/invoice-es-nl-tbai-exempt.yaml create mode 100644 test/data/out/invoice-es-nl-tbai-b2c-explicit.xml create mode 100644 test/data/out/invoice-es-nl-tbai-b2c.xml create mode 100644 test/data/out/invoice-es-nl-tbai-exempt.xml diff --git a/README.md b/README.md index e9b3af8..9efba22 100644 --- a/README.md +++ b/README.md @@ -143,25 +143,26 @@ Invoice tax tags can be added to invoice documents in order to reflect a special - `reverse-charge` - B2B services or goods sold to a tax registered EU member who will pay VAT on the suppliers behalf. Implies that all items will be classified under the `TipoNoExenta` value of `S2`. - `customer-rates` - B2C services, specifically for the EU digital goods act (2015) which imply local taxes will be applied. All items will specify the `DetalleNoSujeta` cause of `RL`. -### Item Keys - -Each of the line items in your invoice may require a special key to correctly group them in the final TicketBAI report. The currently supported invoice line item keys are: - -- `services` - indicates that the product being sold is a service (as opposed to a physical good). By default, all items are considered services. -- `goods` - indicates that the product being sold is a physical good. -- `resale` - indicates that a line item is sold without modification from a provider under the Equalisation Charge scheme. (This implies that the `OperacionEnRecargoDeEquivalenciaORegimenSimplificado` tag will be set to `S`). - ## Tax Extensions The following extension can be applied to each line tax: -- `exempt` - identifies the specific TicketBAI exemption reason code as to why taxes should not be applied to the line according to the whole set of exemptions defined in the law. It has to be set along with the tax rate value of `exempt`. These are the valid values: - - `E1` – Exenta por el artículo 20 de la Norma Foral del IVA +- `es-tbai-product` – allows to correctly group the invoice's lines taxes in the TicketBAI breakdowns (a.k.a. desgloses). These are the valid values: + - `services` - indicates that the product being sold is a service (as opposed to a physical good). Services are accounted in the `DesgloseTipoOperacion > PrestacionServicios` breakdown of invoices to foreign customers. By default, all items are considered services. + - `goods` - indicates that the product being sold is a physical good. Products are accounted in the `DesgloseTipoOperacion > Entrega` breakdown of invoices to foreign customers. + - `resale` - indicates that a line item is sold without modification from a provider under the Equalisation Charge scheme. (This implies that the `OperacionEnRecargoDeEquivalenciaORegimenSimplificado` tag will be set to `S`). + +- `es-tbai-exemption` - identifies the specific TicketBAI reason code as to why taxes should not be applied to the line according to the whole set of exemptions or not-subject scenarios defined in the law. It has to be set along with the tax rate value of `exempt`. These are the valid values: + - `E1` – Exenta por el artículo 20 de la Norma Foral del IVA - `E2` – Exenta por el artículo 21 de la Norma Foral del IVA - `E3` – Exenta por el artículo 22 de la Norma Foral del IVA - `E4` – Exenta por el artículo 23 y 24 de la Norma Foral del IVA - `E5` – Exenta por el artículo 25 de la Norma Foral del IVA - `E6` – Exenta por otra causa + - `OT` – No sujeto por el artículo 7 de la Norma Foral de IVA / Otros supuestos + - `RL` – No sujeto por reglas de localización (*) + +_(*) As noted elsewhere, `RL` will be set automatically set in invoices using the `customer-rates` tax tag. It can also be set explicitly using the `es-tbai-exemption` extension in invoices not using that tag._ ### Use-Cases @@ -169,9 +170,9 @@ Under what situations should the TicketBAI system be expected to function: - B2B & B2C: regular national invoice with VAT. Operation with minimal data. - B2B Provider to Retailer: Include equalisation surcharge VAT rates -- B2B Retailer: Same as regular invoice, except with invoice lines that include `item.key = resale` when the goods being provided are being sold without modification (recargo de equivalencia), very much related to the next point. +- B2B Retailer: Same as regular invoice, except with invoice lines that include `ext[es-tbai-product] = resale` when the goods being provided are being sold without modification (recargo de equivalencia), very much related to the next point. - B2B Retailer Simplified: Include the simplified scheme key. (This implies that the `OperacionEnRecargoDeEquivalenciaORegimenSimplificado` tag will be set to `S`). -- EU B2B: Reverse charge EU export, scheme: reverse-charge taxes calculated, but not applied to totals. By default all line items assumed to be services. Individual rows can use the `item.key = goods` value to identify when the line is a physical good. Operations like this are normally assigned the TipoNoExenta value of S2. If however the service or goods are exempt of tax, each line's tax `ext[exempt]` field can be used to identify a reason. +- EU B2B: Reverse charge EU export, scheme: reverse-charge taxes calculated, but not applied to totals. By default all line items assumed to be services. Individual lines can use the `ext[es-tbai-product] = goods` value to identify when the line is a physical good. Operations like this are normally assigned the TipoNoExenta value of S2. If however the service or goods are exempt of tax, each line's tax `ext[exempt]` field can be used to identify a reason. - EU B2C Digital Goods: use tax tag `customer-rates`, that applies VAT according to customer location. In TicketBAI, these cases are "not subject" to tax, and thus should have the cause RL (por reglas de localización). ## Test Data diff --git a/go.mod b/go.mod index f047789..42aecaa 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/go-resty/resty/v2 v2.10.0 - github.com/invopop/gobl v0.64.0 + github.com/invopop/gobl v0.65.0 github.com/invopop/xmldsig v0.9.0 github.com/lestrrat-go/libxml2 v0.0.0-20231124114421-99c71026c2f5 github.com/magefile/mage v1.15.0 @@ -32,7 +32,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - golang.org/x/crypto v0.16.0 // indirect + golang.org/x/crypto v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect software.sslmate.com/src/go-pkcs12 v0.4.0 // indirect diff --git a/go.sum b/go.sum index f269bf9..8b7cb5f 100644 --- a/go.sum +++ b/go.sum @@ -15,26 +15,20 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/gobl v0.64.0 h1:eaSLjGyTKRSHGMfN6tJyc1Xh2j6oqaJjnccXv/h2tfI= -github.com/invopop/gobl v0.64.0/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= +github.com/invopop/gobl v0.65.0 h1:VdoV2C3JMRi15ZpvGMYrijqMoxNmGOpowIe64SODJUM= +github.com/invopop/gobl v0.65.0/go.mod h1:Jau+ajdfUCBPVH9VMor6aeYq3S9o7HuSNm07QxxxomE= github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/invopop/validation v0.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw= github.com/invopop/validation v0.3.0/go.mod h1:qIBG6APYLp2Wu3/96p3idYjP8ffTKVmQBfKiZbw0Hts= -github.com/invopop/xmldsig v0.7.0 h1:cnUE5SOW4TanHKCnF5Va/PfsdK0LgEoRE/+P6It5LuY= -github.com/invopop/xmldsig v0.7.0/go.mod h1:dc3+2BYNw0vzauyZiOobTltp1t3BbvWSq7ae/F2gdk0= -github.com/invopop/xmldsig v0.8.0 h1:W/yRh/HcMSlZrkaVtIeycxmBLssXIfy437yNCvx4gD4= -github.com/invopop/xmldsig v0.8.0/go.mod h1:dc3+2BYNw0vzauyZiOobTltp1t3BbvWSq7ae/F2gdk0= github.com/invopop/xmldsig v0.9.0 h1:fOmPSGzXgbZEJu2ZkqXYUPJ9w1SkVuMSslb8lB7VNog= github.com/invopop/xmldsig v0.9.0/go.mod h1:dc3+2BYNw0vzauyZiOobTltp1t3BbvWSq7ae/F2gdk0= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= @@ -74,8 +68,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -120,7 +114,6 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/xmlpath.v1 v1.0.0-20140413065638-a146725ea6e7 h1:zibSPXbkfB1Dwl76rJgLa68xcdHu42qmFTe6vAnU4wA= diff --git a/internal/doc/breakdown.go b/internal/doc/breakdown.go index 3dac200..755c293 100644 --- a/internal/doc/breakdown.go +++ b/internal/doc/breakdown.go @@ -1,10 +1,10 @@ package doc import ( - "sort" "strings" "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/num" "github.com/invopop/gobl/regimes/es" @@ -27,20 +27,8 @@ type DesgloseFactura struct { // DesgloseTipoOperacion contains taxes breakdown if customer is from abroad type DesgloseTipoOperacion struct { - PrestacionServicios *PrestacionServicios - Entrega *Entrega -} - -// PrestacionServicios means that there is no exchange of goods -type PrestacionServicios struct { - Sujeta *Sujeta - NoSujeta *NoSujeta -} - -// Entrega means that there is an exchange of goods -type Entrega struct { - Sujeta *Sujeta - NoSujeta *NoSujeta + PrestacionServicios *DesgloseFactura + Entrega *DesgloseFactura } // Sujeta means that some amount is liable to taxes (VAT, equivalence surcharge) @@ -52,7 +40,7 @@ type Sujeta struct { // Exenta contains the info of the amounts that even being liable to taxes, are // under 0% rate due to different reasons type Exenta struct { - DetalleExenta []DetalleExenta + DetalleExenta []*DetalleExenta } // DetalleExenta details about 0% taxed amounts @@ -63,7 +51,7 @@ type DetalleExenta struct { // NoExenta list the amounts that a liable to taxes (VAT, equivalence surcharge) other than 0% type NoExenta struct { - DetalleNoExenta []DetalleNoExenta + DetalleNoExenta []*DetalleNoExenta } // DetalleNoExenta details about non 0% taxes amounts @@ -74,7 +62,7 @@ type DetalleNoExenta struct { // DesgloseIVA list of VAT details type DesgloseIVA struct { - DetalleIVA []DetalleIVA + DetalleIVA []*DetalleIVA } // DetalleIVA contains details of VAT / equivalence surcharge taxes @@ -89,13 +77,13 @@ type DetalleIVA struct { // NoSujeta means that some part of the invoice is not liable to VAT type NoSujeta struct { - DetalleNoSujeta []DetalleNoSujeta + DetalleNoSujeta []*DetalleNoSujeta } // DetalleNoSujeta contails details about the not liable amount type DetalleNoSujeta struct { Causa string - Importe string + Importe num.Amount } type taxInfo struct { @@ -105,259 +93,144 @@ type taxInfo struct { } func newTipoDesglose(gobl *bill.Invoice) *TipoDesglose { - desglose := &TipoDesglose{} - - taxInfo := taxInfo{} - if gobl.Tax != nil { - for _, scheme := range gobl.Tax.Tags { - switch scheme { - case es.TagSimplifiedScheme: - taxInfo.simplifiedRegime = true - case tax.TagReverseCharge: - taxInfo.reverseCharge = true - case tax.TagCustomerRates: - taxInfo.customerRates = true - } - } + catTotal := gobl.Totals.Taxes.Category(tax.CategoryVAT) + if catTotal == nil { + return nil } + taxInfo := newTaxInfo(gobl) + + desglose := &TipoDesglose{} if gobl.Customer == nil || gobl.Customer.TaxID.Country == l10n.ES { - desglose.DesgloseFactura = &DesgloseFactura{ - NoSujeta: newNoSujeta(gobl.Lines, taxInfo), - Sujeta: newSujeta(gobl.Lines, taxInfo), - } + desglose.DesgloseFactura = newDesgloseFactura(taxInfo, catTotal.Rates) } else { - desglose.DesgloseTipoOperacion = &DesgloseTipoOperacion{} - - goodsLines := filterGoodsLines(gobl) - if len(goodsLines) > 0 { - desglose.DesgloseTipoOperacion.Entrega = &Entrega{ - NoSujeta: newNoSujeta(goodsLines, taxInfo), - Sujeta: newSujeta(goodsLines, taxInfo), - } - } + goods, services := splitByTBAIProduct(catTotal.Rates) - serviceLines := filterServiceLines(gobl) - if len(serviceLines) > 0 { - desglose.DesgloseTipoOperacion.PrestacionServicios = &PrestacionServicios{ - NoSujeta: newNoSujeta(serviceLines, taxInfo), - Sujeta: newSujeta(serviceLines, taxInfo), - } + desglose.DesgloseTipoOperacion = &DesgloseTipoOperacion{ + Entrega: newDesgloseFactura(taxInfo, goods), + PrestacionServicios: newDesgloseFactura(taxInfo, services), } } return desglose } -func filterGoodsLines(gobl *bill.Invoice) []*bill.Line { - lines := []*bill.Line{} +func newDesgloseFactura(taxInfo taxInfo, rates []*tax.RateTotal) *DesgloseFactura { + if len(rates) == 0 { + return nil + } + + df := &DesgloseFactura{ + NoSujeta: &NoSujeta{}, + Sujeta: &Sujeta{ + Exenta: &Exenta{}, + NoExenta: &NoExenta{}, + }, + } - for _, line := range gobl.Lines { - if line.Item.Key == es.ItemGoods { - lines = append(lines, line) + for _, rate := range rates { + if taxInfo.isNoSujeta(rate) { + df.NoSujeta.appendDetalle(&DetalleNoSujeta{ + Causa: taxInfo.causaNoSujeta(rate), + Importe: rate.Base, + }) + } else if taxInfo.isExenta(rate) { + df.Sujeta.Exenta.appendDetalle(&DetalleExenta{ + CausaExencion: rate.Ext[es.ExtKeyTBAIExemption].String(), + BaseImponible: rate.Base.Rescale(2).String(), + }) + } else { + dne := df.Sujeta.NoExenta.appendDetalle(&DetalleNoExenta{ + TipoNoExenta: taxInfo.nonExemptedType(), + DesgloseIVA: &DesgloseIVA{}, + }) + + diva := newDetalleIVA(taxInfo, rate) + + dne.DesgloseIVA.appendDetalle(diva) } } - return lines + return df.prune() } -func filterServiceLines(gobl *bill.Invoice) []*bill.Line { - lines := []*bill.Line{} - - for _, line := range gobl.Lines { - if line.Item.Key != es.ItemGoods { - lines = append(lines, line) +func splitByTBAIProduct(rates []*tax.RateTotal) (goods, services []*tax.RateTotal) { + for _, rate := range rates { + if rate.Ext[es.ExtKeyTBAIProduct] == "goods" { + goods = append(goods, rate) + } else { + services = append(services, rate) } } - return lines + return goods, services } -func newNoSujeta(lines []*bill.Line, taxInfo taxInfo) *NoSujeta { - sum := sumNoSujetaAmount(lines) - - if sum.IsZero() { - return nil +func (df *DesgloseFactura) prune() *DesgloseFactura { + if len(df.NoSujeta.DetalleNoSujeta) == 0 { + df.NoSujeta = nil } - - cause := "OT" - if taxInfo.customerRates { - cause = "RL" + if len(df.Sujeta.Exenta.DetalleExenta) == 0 { + df.Sujeta.Exenta = nil } - - return &NoSujeta{ - DetalleNoSujeta: []DetalleNoSujeta{ - { - Causa: cause, - Importe: sum.Rescale(2).String(), - }, - }, + if len(df.Sujeta.NoExenta.DetalleNoExenta) == 0 { + df.Sujeta.NoExenta = nil } -} - -func sumNoSujetaAmount(lines []*bill.Line) num.Amount { - sum := num.MakeAmount(0, 2) - - for _, line := range lines { - withoutTaxes := true - for _, tax := range line.Taxes { - if tax.Category != es.TaxCategoryIRPF { - withoutTaxes = false - } - } - - if withoutTaxes { - sum = sum.Add(line.Total) - } + if df.Sujeta.Exenta == nil && df.Sujeta.NoExenta == nil { + df.Sujeta = nil } - return sum + return df } -func newSujeta(lines []*bill.Line, taxInfo taxInfo) *Sujeta { - details, detailsExempted := buildDetails(lines, taxInfo) - - if len(details) == 0 && len(detailsExempted) == 0 { - return nil - } - - var noExenta *NoExenta - if len(details) > 0 { - noExenta = &NoExenta{ - DetalleNoExenta: []DetalleNoExenta{ - { - TipoNoExenta: nonExemptedType(taxInfo), - DesgloseIVA: &DesgloseIVA{DetalleIVA: details}, - }, - }, - } - } - - var exenta *Exenta - if len(detailsExempted) > 0 { - exenta = &Exenta{ - DetalleExenta: detailsExempted, +func (ns *NoSujeta) appendDetalle(d *DetalleNoSujeta) *DetalleNoSujeta { + for _, e := range ns.DetalleNoSujeta { + if e.Causa == d.Causa { + e.Importe = e.Importe.Add(d.Importe) + return e } } - - return &Sujeta{ - NoExenta: noExenta, - Exenta: exenta, - } + ns.DetalleNoSujeta = append(ns.DetalleNoSujeta, d) + return d } -func nonExemptedType(taxInfo taxInfo) string { - if taxInfo.reverseCharge { - return "S2" - } - - return "S1" +func (e *Exenta) appendDetalle(d *DetalleExenta) *DetalleExenta { + e.DetalleExenta = append(e.DetalleExenta, d) + return d } -func buildDetails(lines []*bill.Line, taxInfo taxInfo) ([]DetalleIVA, []DetalleExenta) { - exempted, nonExempted, surcharged := sumAmountsPerType(lines) - - exemptedList := []DetalleExenta{} - eachSumDetail(exempted, func(cause string, sum sumDetail) { - exemptedList = append(exemptedList, DetalleExenta{ - CausaExencion: cause, - BaseImponible: sum.amount.Rescale(2).String(), - }) - }) - - detailList := []DetalleIVA{} - eachSumDetail(nonExempted, func(_ string, sum sumDetail) { - detail := DetalleIVA{ - BaseImponible: sum.amount.Rescale(2).String(), - TipoImpositivo: formatPercent(sum.percent), - CuotaImpuesto: sum.percent.Of(sum.amount).Rescale(2).String(), +func (ne *NoExenta) appendDetalle(d *DetalleNoExenta) *DetalleNoExenta { + for _, e := range ne.DetalleNoExenta { + if e.TipoNoExenta == d.TipoNoExenta { + return e } - - if !sum.surcharge.IsZero() { - detail.TipoRecargoEquivalencia = formatPercent(sum.surcharge) - detail.CuotaRecargoEquivalencia = sum.surcharge.Of(sum.amount).Rescale(2).String() - } - - if taxInfo.simplifiedRegime { - detail.OperacionEnRecargoDeEquivalenciaORegimenSimplificado = "S" - } - - detailList = append(detailList, detail) - }) - - eachSumDetail(surcharged, func(_ string, sum sumDetail) { - detailList = append(detailList, DetalleIVA{ - BaseImponible: sum.amount.Rescale(2).String(), - TipoImpositivo: formatPercent(sum.percent), - CuotaImpuesto: sum.percent.Of(sum.amount).Rescale(2).String(), - OperacionEnRecargoDeEquivalenciaORegimenSimplificado: "S", - }) - }) - - return detailList, exemptedList + } + ne.DetalleNoExenta = append(ne.DetalleNoExenta, d) + return d } -type sumDetail struct { - amount num.Amount - percent num.Percentage - surcharge num.Percentage +func (di *DesgloseIVA) appendDetalle(d *DetalleIVA) *DetalleIVA { + di.DetalleIVA = append(di.DetalleIVA, d) + return d } -func sumAmountsPerType(lines []*bill.Line) (map[string]sumDetail, map[string]sumDetail, map[string]sumDetail) { - exempted := make(map[string]sumDetail) - nonExempted := make(map[string]sumDetail) - surcharged := make(map[string]sumDetail) - - for _, line := range lines { - discount := calculateDiscounts(line) - // TODO: Handle charges - taxableAmount := line.Item.Price.Multiply(line.Quantity).Subtract(discount) - lineSurcharged := line.Item.Key == es.ItemResale - - for _, t := range line.Taxes { - if t.Category == tax.CategoryVAT && t.Rate == tax.RateExempt { - exempted = updateAmount( - exempted, - t.Ext[es.ExtKeyTBAIExemption].String(), - taxableAmount, - num.MakePercentage(0, 0), - surcharge(t), - ) - } else if t.Category == tax.CategoryVAT && lineSurcharged { - surcharged = updateAmount( - surcharged, - taxKey(t), - taxableAmount, - *t.Percent, - surcharge(t), - ) - } else if t.Category == tax.CategoryVAT { - nonExempted = updateAmount( - nonExempted, - taxKey(t), - taxableAmount, - *t.Percent, - surcharge(t), - ) - } - } +func newDetalleIVA(taxInfo taxInfo, rate *tax.RateTotal) *DetalleIVA { + diva := &DetalleIVA{ + BaseImponible: rate.Base.Rescale(2).String(), + TipoImpositivo: formatPercent(*rate.Percent), + CuotaImpuesto: rate.Amount.Rescale(2).String(), } - return exempted, nonExempted, surcharged -} - -// eachSumDetail iterates over a map of sumDetail sorting it by the keys first. -// This is needed to get a deterministic output -func eachSumDetail(sums map[string]sumDetail, f func(string, sumDetail)) { - keys := make([]string, 0, len(sums)) - for k := range sums { - keys = append(keys, k) + if rate.Surcharge != nil { + diva.TipoRecargoEquivalencia = formatPercent(rate.Surcharge.Percent) + diva.CuotaRecargoEquivalencia = rate.Surcharge.Amount.Rescale(2).String() } - sort.Strings(keys) - for _, key := range keys { - f(key, sums[key]) + if taxInfo.simplifiedRegime || rate.Ext[es.ExtKeyTBAIProduct] == "resale" { + diva.OperacionEnRecargoDeEquivalenciaORegimenSimplificado = "S" } + + return diva } func formatPercent(percent num.Percentage) string { @@ -369,37 +242,50 @@ func formatPercent(percent num.Percentage) string { return maybeNegative } -func taxKey(tax *tax.Combo) string { - key := tax.Percent.String() - if tax.Surcharge != nil { - key = key + "+" + tax.Surcharge.String() +func newTaxInfo(gobl *bill.Invoice) taxInfo { + taxInfo := taxInfo{} + if gobl.Tax != nil { + for _, scheme := range gobl.Tax.Tags { + switch scheme { + case es.TagSimplifiedScheme: + taxInfo.simplifiedRegime = true + case tax.TagReverseCharge: + taxInfo.reverseCharge = true + case tax.TagCustomerRates: + taxInfo.customerRates = true + } + } + } + + return taxInfo +} + +func (t taxInfo) nonExemptedType() string { + if t.reverseCharge { + return "S2" } - return key + return "S1" } -func updateAmount( - totals map[string]sumDetail, - key string, - taxableAmount num.Amount, - percentage num.Percentage, - surcharge num.Percentage, -) map[string]sumDetail { - total, found := totals[key] - if !found { - totals[key] = sumDetail{percent: percentage, amount: taxableAmount, surcharge: surcharge} - } else { - total.amount = total.amount.Add(taxableAmount) - totals[key] = total +var notSubjectExemptionCodes = []cbc.Code{"OT", "RL"} + +func (t taxInfo) isNoSujeta(r *tax.RateTotal) bool { + if t.customerRates { + return true } - return totals + return r.Key == tax.RateExempt && r.Ext[es.ExtKeyTBAIExemption].Code().In(notSubjectExemptionCodes...) } -func surcharge(tax *tax.Combo) num.Percentage { - var surcharge num.Percentage - if tax.Surcharge != nil { - surcharge = *tax.Surcharge +func (t taxInfo) causaNoSujeta(r *tax.RateTotal) string { + if t.customerRates { + return "RL" } - return surcharge + + return r.Ext[es.ExtKeyTBAIExemption].String() +} + +func (taxInfo) isExenta(r *tax.RateTotal) bool { + return r.Key == tax.RateExempt && !r.Ext[es.ExtKeyTBAIExemption].Code().In(notSubjectExemptionCodes...) } diff --git a/internal/doc/breakdown_test.go b/internal/doc/breakdown_test.go index 0a78864..15365d4 100644 --- a/internal/doc/breakdown_test.go +++ b/internal/doc/breakdown_test.go @@ -56,7 +56,8 @@ func TestDesgloseConversion(t *testing.T) { t.Run("should distinguish goods from services when customer from other country", func(t *testing.T) { goblInvoice := invoiceFromCountry("GB") - goblInvoice.Lines[0].Item.Key = es.ItemGoods + goblInvoice.Lines[0].Taxes.Get(tax.CategoryVAT).Ext = tax.ExtMap{es.ExtKeyTBAIProduct: "goods"} + _ = goblInvoice.Calculate() invoice, _ := doc.NewTicketBAI(goblInvoice, ts, role) @@ -79,11 +80,18 @@ func TestDesgloseConversion(t *testing.T) { t.Run("should divide details between services and goods", func(t *testing.T) { goblInvoice := invoiceFromCountry("GB") + goblInvoice.Tax = &bill.Tax{Tags: []cbc.Key{tax.TagCustomerRates}} goblInvoice.Lines = []*bill.Line{ { Index: 1, Quantity: num.MakeAmount(1, 0), Item: &org.Item{Name: "A", Price: num.MakeAmount(10, 0)}, + Taxes: tax.Set{ + &tax.Combo{ + Category: "VAT", + Rate: "standard", + }, + }, }, { Index: 2, @@ -91,7 +99,32 @@ func TestDesgloseConversion(t *testing.T) { Item: &org.Item{ Name: "A", Price: num.MakeAmount(20, 0), - Key: es.ItemGoods, + }, + Taxes: tax.Set{ + &tax.Combo{ + Category: "VAT", + Rate: "standard", + Ext: tax.ExtMap{ + es.ExtKeyTBAIProduct: "goods", + }, + }, + }, + }, + { + Index: 3, + Quantity: num.MakeAmount(1, 0), + Item: &org.Item{ + Name: "A", + Price: num.MakeAmount(10, 0), + }, + Taxes: tax.Set{ + &tax.Combo{ + Category: "VAT", + Rate: "reduced", + Ext: tax.ExtMap{ + es.ExtKeyTBAIProduct: "goods", + }, + }, }, }, } @@ -100,8 +133,10 @@ func TestDesgloseConversion(t *testing.T) { invoice, _ := doc.NewTicketBAI(goblInvoice, ts, role) details := invoice.Factura.TipoDesglose.DesgloseTipoOperacion - assert.Equal(t, "20.00", details.Entrega.NoSujeta.DetalleNoSujeta[0].Importe) - assert.Equal(t, "10.00", details.PrestacionServicios.NoSujeta.DetalleNoSujeta[0].Importe) + assert.Equal(t, 1, len(details.Entrega.NoSujeta.DetalleNoSujeta)) + assert.Equal(t, "30.00", details.Entrega.NoSujeta.DetalleNoSujeta[0].Importe.String()) + assert.Equal(t, 1, len(details.PrestacionServicios.NoSujeta.DetalleNoSujeta)) + assert.Equal(t, "10.00", details.PrestacionServicios.NoSujeta.DetalleNoSujeta[0].Importe.String()) }) t.Run("should divide details between services and goods when taxes exist", @@ -120,9 +155,14 @@ func TestDesgloseConversion(t *testing.T) { Item: &org.Item{ Name: "A", Price: num.MakeAmount(20, 0), - Key: es.ItemGoods, }, - Taxes: tax.Set{&tax.Combo{Category: "VAT", Rate: "standard"}}, + Taxes: tax.Set{ + &tax.Combo{ + Category: "VAT", + Rate: "standard", + Ext: tax.ExtMap{es.ExtKeyTBAIProduct: "goods"}, + }, + }, }, } _ = goblInvoice.Calculate() @@ -143,24 +183,37 @@ func TestDesgloseConversion(t *testing.T) { Quantity: num.MakeAmount(100, 0), Item: &org.Item{Name: "A", Price: num.MakeAmount(10, 0)}, Discounts: []*bill.LineDiscount{DiscountOf(100)}, - Taxes: tax.Set{&tax.Combo{Category: "IRPF", Rate: "pro"}}, + Taxes: tax.Set{ + &tax.Combo{Category: "IRPF", Rate: "pro"}, + &tax.Combo{ + Category: "VAT", + Rate: "exempt", + Ext: tax.ExtMap{es.ExtKeyTBAIExemption: "OT"}, + }, + }, }} _ = goblInvoice.Calculate() invoice, _ := doc.NewTicketBAI(goblInvoice, ts, role) desglose := invoice.Factura.TipoDesglose.DesgloseFactura - assert.Equal(t, "900.00", desglose.NoSujeta.DetalleNoSujeta[0].Importe) + assert.Equal(t, "900.00", desglose.NoSujeta.DetalleNoSujeta[0].Importe.String()) assert.Equal(t, "OT", desglose.NoSujeta.DetalleNoSujeta[0].Causa) }) t.Run("should change No Sujeta cause when taxes are paid in other EU country", func(t *testing.T) { goblInvoice := invoiceFromCountry("ES") - goblInvoice.Tax = &bill.Tax{Tags: []cbc.Key{tax.TagCustomerRates}} goblInvoice.Lines = []*bill.Line{{ Index: 1, Quantity: num.MakeAmount(100, 0), Item: &org.Item{Name: "A", Price: num.MakeAmount(10, 0)}, + Taxes: tax.Set{ + &tax.Combo{ + Category: "VAT", + Rate: "exempt", + Ext: tax.ExtMap{es.ExtKeyTBAIExemption: "RL"}, + }, + }, }} _ = goblInvoice.Calculate() @@ -177,13 +230,20 @@ func TestDesgloseConversion(t *testing.T) { Quantity: num.MakeAmount(100, 0), Item: &org.Item{Name: "A", Price: num.MakeAmount(10, 0)}, Discounts: []*bill.LineDiscount{DiscountOf(100)}, + Taxes: tax.Set{ + &tax.Combo{ + Category: "VAT", + Rate: "exempt", + Ext: tax.ExtMap{es.ExtKeyTBAIExemption: "RL"}, + }, + }, }} _ = goblInvoice.Calculate() invoice, _ := doc.NewTicketBAI(goblInvoice, ts, role) desglose := invoice.Factura.TipoDesglose.DesgloseTipoOperacion - assert.Equal(t, "900.00", desglose.PrestacionServicios.NoSujeta.DetalleNoSujeta[0].Importe) + assert.Equal(t, "900.00", desglose.PrestacionServicios.NoSujeta.DetalleNoSujeta[0].Importe.String()) }) t.Run("should add VAT detail on national invoices", func(t *testing.T) { @@ -267,9 +327,14 @@ func TestDesgloseConversion(t *testing.T) { Item: &org.Item{ Name: "A", Price: num.MakeAmount(10, 0), - Key: es.ItemResale, }, - Taxes: tax.Set{&tax.Combo{Category: "VAT", Rate: "standard"}}, + Taxes: tax.Set{ + &tax.Combo{ + Category: "VAT", + Rate: "standard", + Ext: tax.ExtMap{es.ExtKeyTBAIProduct: "resale"}, + }, + }, }, } _ = goblInvoice.Calculate() @@ -290,7 +355,13 @@ func TestDesgloseConversion(t *testing.T) { Index: 1, Quantity: num.MakeAmount(100, 0), Item: &org.Item{Name: "A", Price: num.MakeAmount(10, 0)}, - Taxes: tax.Set{&tax.Combo{Category: tax.CategoryVAT, Rate: tax.RateExempt}}, + Taxes: tax.Set{ + &tax.Combo{ + Category: tax.CategoryVAT, + Rate: tax.RateExempt, + Ext: tax.ExtMap{es.ExtKeyTBAIExemption: "E1"}, + }, + }, }} _ = goblInvoice.Calculate() @@ -314,7 +385,7 @@ func TestDesgloseConversion(t *testing.T) { &tax.Combo{ Category: tax.CategoryVAT, Rate: tax.RateExempt, - Ext: cbc.CodeMap{ + Ext: tax.ExtMap{ es.ExtKeyTBAIExemption: "E1", }, }, @@ -331,7 +402,7 @@ func TestDesgloseConversion(t *testing.T) { &tax.Combo{ Category: tax.CategoryVAT, Rate: tax.RateExempt, - Ext: cbc.CodeMap{ + Ext: tax.ExtMap{ es.ExtKeyTBAIExemption: "E2", }, }, @@ -393,20 +464,20 @@ func invoiceFromCountry(countryCode l10n.CountryCode) *bill.Invoice { return goblInvoice } -func detalleIVA(desgloseIVA *doc.DesgloseIVA, rate string) doc.DetalleIVA { +func detalleIVA(desgloseIVA *doc.DesgloseIVA, rate string) *doc.DetalleIVA { for _, detail := range desgloseIVA.DetalleIVA { if detail.TipoImpositivo == rate { return detail } } - return doc.DetalleIVA{} + return &doc.DetalleIVA{} } -func findExemption(exemptions []doc.DetalleExenta, cause string) *doc.DetalleExenta { +func findExemption(exemptions []*doc.DetalleExenta, cause string) *doc.DetalleExenta { for _, exemption := range exemptions { if exemption.CausaExencion == cause { - return &exemption + return exemption } } diff --git a/internal/doc/invoice.go b/internal/doc/invoice.go index 1f5a430..2e8a7ce 100644 --- a/internal/doc/invoice.go +++ b/internal/doc/invoice.go @@ -162,11 +162,17 @@ func newClaves(inv *bill.Invoice) []IDClave { } func hasSurchargedLines(inv *bill.Invoice) bool { - for _, line := range inv.Lines { - if line.Item.Key == es.ItemResale { + vat := inv.Totals.Taxes.Category(tax.CategoryVAT) + if vat == nil { + return false + } + + for _, rate := range vat.Rates { + if rate.Ext[es.ExtKeyTBAIProduct] == "resale" { return true } } + return false } diff --git a/internal/doc/invoice_test.go b/internal/doc/invoice_test.go index 75f6263..05343c1 100644 --- a/internal/doc/invoice_test.go +++ b/internal/doc/invoice_test.go @@ -176,12 +176,12 @@ func TestFacturaConversion(t *testing.T) { Item: &org.Item{ Name: "A", Price: num.MakeAmount(10, 0), - Key: es.ItemResale, }, Taxes: tax.Set{ &tax.Combo{ Category: "VAT", Rate: "standard", + Ext: tax.ExtMap{es.ExtKeyTBAIProduct: "resale"}, }, }, }} diff --git a/test/data/invoice-es-nl-tbai-b2c-explicit.json b/test/data/invoice-es-nl-tbai-b2c-explicit.json new file mode 100644 index 0000000..2a00175 --- /dev/null +++ b/test/data/invoice-es-nl-tbai-b2c-explicit.json @@ -0,0 +1,111 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "328f20c8-a0ac-11ee-bb1d-e6a7901137ed", + "dig": { + "alg": "sha256", + "val": "a87bf8dbc6316cbbad363a13383ff35dd6ae651352ad7d1758484e0c2fed701f" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", + "series": "EXPORT-X", + "code": "0002", + "issue_date": "2023-12-18", + "currency": "EUR", + "supplier": { + "name": "Provide One S.L.", + "tax_id": { + "country": "ES", + "zone": "BI", + "code": "B98602642" + }, + "addresses": [ + { + "num": "42", + "street": "San Frantzisko", + "locality": "Bilbo", + "region": "Bizkaia", + "code": "48003", + "country": "ES" + } + ], + "emails": [ + { + "addr": "billing@example.com" + } + ] + }, + "customer": { + "name": "Sample End-Consumer", + "tax_id": { + "country": "NL" + } + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "price": "90.00", + "unit": "h" + }, + "sum": "1800.00", + "discounts": [ + { + "percent": "10%", + "amount": "180.00", + "reason": "Special discount" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "exempt", + "ext": { + "es-tbai-exemption": "RL", + "es-tbai-product": "services" + } + } + ], + "total": "1620.00" + } + ], + "totals": { + "sum": "1620.00", + "total": "1620.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "exempt", + "ext": { + "es-tbai-exemption": "RL", + "es-tbai-product": "services" + }, + "base": "1620.00", + "amount": "0.00" + } + ], + "amount": "0.00" + } + ], + "sum": "0.00" + }, + "tax": "0.00", + "total_with_tax": "1620.00", + "payable": "1620.00" + }, + "notes": [ + { + "key": "general", + "text": "Some random description" + } + ] + } +} diff --git a/test/data/invoice-es-nl-tbai-b2c-explicit.yaml b/test/data/invoice-es-nl-tbai-b2c-explicit.yaml new file mode 100644 index 0000000..7f8287c --- /dev/null +++ b/test/data/invoice-es-nl-tbai-b2c-explicit.yaml @@ -0,0 +1,46 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +currency: "EUR" +issue_date: "2023-12-18" +series: "EXPORT-X" +code: "0002" + +supplier: + tax_id: + country: "ES" + zone: "BI" + code: "B98602642" # random + name: "Provide One S.L." + emails: + - addr: "billing@example.com" + addresses: + - num: "42" + street: "San Frantzisko" + locality: "Bilbo" + region: "Bizkaia" + code: "48003" + country: "ES" + +customer: + tax_id: + country: "NL" + name: "Sample End-Consumer" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + discounts: + - percent: "10%" + reason: "Special discount" + taxes: + - cat: VAT + rate: exempt + ext: + es-tbai-product: "services" + es-tbai-exemption: "RL" + +notes: + - key: "general" + text: "Some random description" diff --git a/test/data/invoice-es-nl-tbai-b2c.json b/test/data/invoice-es-nl-tbai-b2c.json new file mode 100644 index 0000000..9b02110 --- /dev/null +++ b/test/data/invoice-es-nl-tbai-b2c.json @@ -0,0 +1,174 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "7fe11346-a0ce-11ee-b8f0-e6a7901137ed", + "dig": { + "alg": "sha256", + "val": "afc7524dc6207f037c138c6d3c5a953eeb84c78a0afb61c87b33c97ce4dc55af" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", + "series": "EXPORT-X", + "code": "0002", + "issue_date": "2023-12-18", + "currency": "EUR", + "tax": { + "tags": [ + "customer-rates" + ] + }, + "supplier": { + "name": "Provide One S.L.", + "tax_id": { + "country": "ES", + "zone": "BI", + "code": "B98602642" + }, + "addresses": [ + { + "num": "42", + "street": "San Frantzisko", + "locality": "Bilbo", + "region": "Bizkaia", + "code": "48003", + "country": "ES" + } + ], + "emails": [ + { + "addr": "billing@example.com" + } + ] + }, + "customer": { + "name": "Sample End-Consumer", + "tax_id": { + "country": "NL" + } + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "price": "90.00", + "unit": "h" + }, + "sum": "1800.00", + "discounts": [ + { + "percent": "10%", + "amount": "180.00", + "reason": "Special discount" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "21.0%", + "ext": { + "es-tbai-product": "services" + } + } + ], + "total": "1620.00" + }, + { + "i": 2, + "quantity": "1", + "item": { + "name": "Some merch", + "price": "90.00" + }, + "sum": "90.00", + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "21.0%", + "ext": { + "es-tbai-product": "goods" + } + } + ], + "total": "90.00" + }, + { + "i": 3, + "quantity": "1", + "item": { + "name": "Some essential needs merch", + "price": "30.00" + }, + "sum": "30.00", + "taxes": [ + { + "cat": "VAT", + "rate": "reduced", + "percent": "9.0%", + "ext": { + "es-tbai-product": "goods" + } + } + ], + "total": "30.00" + } + ], + "totals": { + "sum": "1740.00", + "total": "1740.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "ext": { + "es-tbai-product": "services" + }, + "base": "1620.00", + "percent": "21.0%", + "amount": "340.20" + }, + { + "key": "standard", + "ext": { + "es-tbai-product": "goods" + }, + "base": "90.00", + "percent": "21.0%", + "amount": "18.90" + }, + { + "key": "reduced", + "ext": { + "es-tbai-product": "goods" + }, + "base": "30.00", + "percent": "9.0%", + "amount": "2.70" + } + ], + "amount": "361.80" + } + ], + "sum": "361.80" + }, + "tax": "361.80", + "total_with_tax": "2101.80", + "payable": "2101.80" + }, + "notes": [ + { + "key": "general", + "text": "Some random description" + } + ] + } +} diff --git a/test/data/invoice-es-nl-tbai-b2c.yaml b/test/data/invoice-es-nl-tbai-b2c.yaml new file mode 100644 index 0000000..2a169ab --- /dev/null +++ b/test/data/invoice-es-nl-tbai-b2c.yaml @@ -0,0 +1,68 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +currency: "EUR" +issue_date: "2023-12-18" +series: "EXPORT-X" +code: "0002" +tax: + tags: + - customer-rates + +supplier: + tax_id: + country: "ES" + zone: "BI" + code: "B98602642" # random + name: "Provide One S.L." + emails: + - addr: "billing@example.com" + addresses: + - num: "42" + street: "San Frantzisko" + locality: "Bilbo" + region: "Bizkaia" + code: "48003" + country: "ES" + +customer: + tax_id: + country: "NL" + name: "Sample End-Consumer" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + discounts: + - percent: "10%" + reason: "Special discount" + taxes: + - cat: VAT + rate: standard + ext: + es-tbai-product: "services" + # es-tbai-not-subject: "RL" # set automatically + - quantity: 1 + item: + name: "Some merch" + price: "90.00" + taxes: + - cat: VAT + rate: standard + ext: + es-tbai-product: "goods" + # es-tbai-not-subject: "RL" # set automatically + - quantity: 1 + item: + name: "Some essential needs merch" + price: "30.00" + taxes: + - cat: VAT + rate: reduced + ext: + es-tbai-product: "goods" + # es-tbai-not-subject: "RL" # set automatically +notes: + - key: "general" + text: "Some random description" diff --git a/test/data/invoice-es-nl-tbai-exempt.json b/test/data/invoice-es-nl-tbai-exempt.json new file mode 100644 index 0000000..867f5d6 --- /dev/null +++ b/test/data/invoice-es-nl-tbai-exempt.json @@ -0,0 +1,112 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "3b445a46-a0ac-11ee-a4ee-e6a7901137ed", + "dig": { + "alg": "sha256", + "val": "ff52d9205014653784555bb33c9f4bb82a1e576d438f2be9a5dcad4a98530554" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", + "series": "EXPORT", + "code": "0002", + "issue_date": "2022-02-01", + "currency": "EUR", + "supplier": { + "name": "Provide One S.L.", + "tax_id": { + "country": "ES", + "zone": "BI", + "code": "B98602642" + }, + "addresses": [ + { + "num": "42", + "street": "San Frantzisko", + "locality": "Bilbo", + "region": "Bizkaia", + "code": "48003", + "country": "ES" + } + ], + "emails": [ + { + "addr": "billing@example.com" + } + ] + }, + "customer": { + "name": "Sample Consumer", + "tax_id": { + "country": "NL", + "code": "000099995B57" + } + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "price": "90.00", + "unit": "h" + }, + "sum": "1800.00", + "discounts": [ + { + "percent": "10%", + "amount": "180.00", + "reason": "Special discount" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "exempt", + "ext": { + "es-tbai-exemption": "E1", + "es-tbai-product": "services" + } + } + ], + "total": "1620.00" + } + ], + "totals": { + "sum": "1620.00", + "total": "1620.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "exempt", + "ext": { + "es-tbai-exemption": "E1", + "es-tbai-product": "services" + }, + "base": "1620.00", + "amount": "0.00" + } + ], + "amount": "0.00" + } + ], + "sum": "0.00" + }, + "tax": "0.00", + "total_with_tax": "1620.00", + "payable": "1620.00" + }, + "notes": [ + { + "key": "general", + "text": "Some random description" + } + ] + } +} diff --git a/test/data/invoice-es-nl-tbai-exempt.yaml b/test/data/invoice-es-nl-tbai-exempt.yaml new file mode 100644 index 0000000..10abb2a --- /dev/null +++ b/test/data/invoice-es-nl-tbai-exempt.yaml @@ -0,0 +1,47 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +currency: "EUR" +issue_date: "2022-02-01" +series: "EXPORT" +code: "0002" + +supplier: + tax_id: + country: "ES" + zone: "BI" + code: "B98602642" # random + name: "Provide One S.L." + emails: + - addr: "billing@example.com" + addresses: + - num: "42" + street: "San Frantzisko" + locality: "Bilbo" + region: "Bizkaia" + code: "48003" + country: "ES" + +customer: + tax_id: + country: "NL" + code: "000099995B57" + name: "Sample Consumer" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + discounts: + - percent: "10%" + reason: "Special discount" + taxes: + - cat: VAT + rate: exempt + ext: + es-tbai-product: "services" + es-tbai-exemption: "E1" + +notes: + - key: "general" + text: "Some random description" diff --git a/test/data/out/invoice-es-nl-tbai-b2c-explicit.xml b/test/data/out/invoice-es-nl-tbai-b2c-explicit.xml new file mode 100644 index 0000000..eee0171 --- /dev/null +++ b/test/data/out/invoice-es-nl-tbai-b2c-explicit.xml @@ -0,0 +1,161 @@ + + + + 1.2 + + + + B98602642 + Provide One S.L. + + + + + NL + 02 + + + Sample End-Consumer + + + T + + + + 0002 + 01-02-2022 + 05:00:00 + N + + + 18-12-2023 + Some random description + + + Development services + 20 + 90.00 + 180.00 + 1620.00 + + + 1620.00 + 0.00 + + + 02 + + + + + + + + + RL + 1620.00 + + + + + + + + + + 1234567890 + 01-01-2021 + 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 + + + My License + + 12345678A + + My Software + 1.0 + + + + + + + + + + + + xb3/LjBi2EGWXZPznBFDxY7oOqYmCsXTlWXlB+KQEUfB7KdAgAxymor/G6f+71cUwiPLeWRNGrAsSk5vb4k2DQ== + + + + U6fg4EciDwQOUTEKqxjU1bv/VsZWnou+YdIWW6XJdIPoU6j/dQO0ONsEPvMI+P6+PQBdeG4C+IZtHN76iWPh5A== + + + + BCDSY/nwp7odGq+sxFyNHnOrCrqObH8g8sCJnH4vQ8cB934KpPaZo9y2wp2j7Rxkfw7mQV2NeU5fiOpC4pgE7A== + + + P8Bp0V2bSsnrlGqTD+kJfkOqS68RC38NkP9mVwb2wCb1MDKVlnI+3vwcNOEKDhlhJTTSLztwmwMfq7DCAv1WOKgJ55GRc+GZzsk3eUyJp2pO7w4KYqHSaoXoW9L7vsX1J79mYVBhHuYnyVdCNiUl2bAdemmPWMTJpB6yEgQUR40D6hfBZsPxzxhiESg4NcpnE3tdwOli6d0903Mk6YGAbtTGxYsZDrbyBv/WglEjoAxefVbChK8CJPQ2T0J+mWKayD66CP+U8+Mc8eDt9BRgTKUztu09L9e6fKG3ipigWeOG20+nlAnuyXXz0iY7qt2aXVDHM9IB0AD27s0SJuC3bQ== + + + MIIJtzCCB5+gAwIBAgICG3YwDQYJKoZIhvcNAQELBQAwgZcxCzAJBgNVBAYTAkVTMRQwEgYDVQQKDAtJWkVOUEUgUy5BLjE6MDgGA1UECwwxTlpaIFppdXJ0YWdpcmkgcHVibGlrb2EgLSBDZXJ0aWZpY2FkbyBwdWJsaWNvIFNDSTE2MDQGA1UEAwwtQ0EgZGUgQ2l1ZGFkYW5vcyB5IEVudGlkYWRlcyAoNCkgLSBERVNBUlJPTExPMB4XDTIxMDMxNTEwMDQzN1oXDTI1MDMxNTEwMDQzN1owggFtMQswCQYDVQQGEwJFUzFvMG0GA1UEDQxmUmVnOkdhc3RlaXogL0hvamE6Qi0xNjY0NzIgL1RvbW86MzczOTMgL1NlY2Npb246OCAvTGlicm86MSAvRm9saW86MjAgL0ZlY2hhOjEwLTAyLTE5NzQgL0luc2NyaXBjacOzbjoxMRgwFgYDVQRhDA9WQVRFUy1TNzgzNjEwN0gxPTA7BgNVBAsMNE9yZGV6a2FyaSB6aXVydGFnaXJpYSAtIENlcnRpZmljYWRvIGRlIHJlcHJlc2VudGFudGUxFDASBgNVBAoMC0laRU5QRSBTLkEuMRIwEAYDVQQFEwk5OTk5OTk3M0sxGDAWBgNVBAQMD0ZJQ1RJQ0lPIEFDVElWTzEWMBQGA1UEKgwNUkVQUkVTRU5UQU5URTE4MDYGA1UEAwwvOTk5OTk5NzNLIFJFUFJFU0VOVEFOVEUgRklDVElDSU8gKFI6IFM3ODM2MTA3SCkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsI8cbiOSsEMrK+lr6Vn7xeDlI1UJIVWM4kMTyXoFcU9F7LkdbSv5jS1D1g3/c8YId1nFjPrHXjpBbjv4Am9QEvHKPa9djI9lTKS3gut5DDU1ePRAagnSCAr2Y6m4isbMF54S5tp0/Ng+myx5c2E+hMmgNw6uZ9KvdwaYY1gQW/N/7qS0KlA1eB0CSHyzZeVRgbYAXI6AMCtuYCRVNLbnzJBvSN0J4SuZeiM/KK0I0oj/8THajszp8hg3v2cfOMAGu5cM3yuBAPTBPBZCkGofwZqMn2ioMZXwRYuXJv4UEncs+d9qZbVGFpc9y0vwbQuPhZU2omSGTWedL4DlnbXDHAgMBAAGjggQyMIIELjCBxwYDVR0SBIG/MIG8hhVodHRwOi8vd3d3Lml6ZW5wZS5jb22BD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAxMCBWaXRvcmlhLUdhc3RlaXowDgYDVR0PAQH/BAQDAgXgMB8GA1UdJQQYMBYGCCsGAQUFBwMCBgorBgEEAYI3CgMMMB0GA1UdDgQWBBQRR3q3tcdVPr8U7NS+zkf7EBt32jAfBgNVHSMEGDAWgBRv//0N8gxjDyZxXRrgb2VkZjhrkzCCATQGA1UdIASCASswggEnMIIBDQYJKwYBBAHzOWYMMIH/MCUGCCsGAQUFBwIBFhlodHRwOi8vd3d3Lml6ZW5wZS5jb20vY3BzMIHVBggrBgEFBQcCAjCByAyBxUtvbnRzdWx0YSB3d3cuaXplbnBlLmNvbS1lbiBiYWxkaW50emFrIGV0YSBrb25kaXppb2FrIHppdXJ0YWdpcmlhbiBmaWRhdHUgZWRvIGVyYWJpbGkgYXVycmV0aWsgLSBDb25zdWx0ZSBlbiB3d3cuaXplbnBlLmNvbSBsb3MgdMOpcm1pbm9zIHkgY29uZGljaW9uZXMgYW50ZXMgZGUgdXRpbGl6YXIgbyBjb25maWFyIGVuIGVsIGNlcnRpZmljYWRvMAkGBwQAi+xAAQIwCQYHYIVUAQMFCDCBogYIKwYBBQUHAQEEgZUwgZIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwZGVzLml6ZW5wZS5jb20waQYIKwYBBQUHMAKGXWh0dHA6Ly93d3cuaXplbnBlLmNvbS9jb250ZW5pZG9zL2luZm9ybWFjaW9uL2Nhc19pemVucGUvZXNfY2FzL2FkanVudG9zL0NDRUVSX2NlcnRfc2hhMjU2LmNydDCB2wYIKwYBBQUHAQMEgc4wgcswCAYGBACORgEBMAgGBgQAjkYBBDALBgYEAI5GAQMCAQ8wfAYGBACORgEFMHIwJBYeaHR0cHM6Ly93d3cuaXplbnBlLmNvbS9wZHMvZW4vEwJlbjAkFh5odHRwczovL3d3dy5pemVucGUuY29tL3Bkcy9ldS8TAmV1MCQWHmh0dHBzOi8vd3d3Lml6ZW5wZS5jb20vcGRzL2VzLxMCZXMwEwYGBACORgEGMAkGBwQAjkYBBgEwFQYIKwYBBQUHCwIwCQYHBACL7EkBAjA2BgNVHR8ELzAtMCugKaAnhiVodHRwOi8vY3JsZGVzLml6ZW5wZS5jb20vY2dpLWJpbi9jcmwyMA0GCSqGSIb3DQEBCwUAA4ICAQCJaRL+xyX6HFu/6AX7N0j/r1ZB3OAY8t4S3KBvxQBs/PGeQmHFr8cnFXxb2cfkZ/5IzDDMElicChXq86BgaXn6xxtw4q30/qsuwz8iwF+mLKENFGIYTdxmaGCuSBwhENWrv03uUeskL4gIMQIu5fhpdZRj2aW4ccsb9QdfLCHRtjxrgZMoL7tpXRYQcpKgMKroAg3PuIhwdg3eLhVZvihtUJ0oNGtEBz+04eRCZjXx8dWVrmYxfTEqHFaYc8Nxu6AuQvPQdBAC2DoxYLUpihUBvKo8aUrU/QhUCue76Sq9hnzi6TXCofNeixMZBB+ODwvcInWTX0N/m7zC/9wRcQ1vE6gO6lbNd6JnwiLHclf+oLAsRLWwN9dEeDEER2IvtlmIapKhxDrPQ9zSAiibaVSAVxvWC5bal2CgfgijlamDQ8lPD+/Fv1O6s8hGTEfMiDNErCC7IWn3ckjg+Ipz11DQO2hRI/VmarFDFtreavVsSgwseVJIQxavgOzMJVFRx1TQCBZFvW9RnM32QDygq2vrs234fD3ak8DtdmKd0aPLNYSN3zaiaPOzfhK6Z5m36Zltqzhzjg0YkoPcbmPGUO5tq7X7SwFeb0Kx7gEPzZHavbcnNYtuPgqFJklNk2+3zztodccRgZGtLRQJay+xwSNh8YiU+NmIgx2ad24qugodzg== + + + + rCPHG4jkrBDKyvpa+lZ+8Xg5SNVCSFVjOJDE8l6BXFPRey5HW0r+Y0tQ9YN/3PGCHdZxYz6x146QW47+AJvUBLxyj2vXYyPZUykt4LreQw1NXj0QGoJ0ggK9mOpuIrGzBeeEubadPzYPpsseXNhPoTJoDcOrmfSr3cGmGNYEFvzf+6ktCpQNXgdAkh8s2XlUYG2AFyOgDArbmAkVTS258yQb0jdCeErmXojPyitCNKI//Ex2o7M6fIYN79nHzjABruXDN8rgQD0wTwWQpBqH8GajJ9oqDGV8EWLlyb+FBJ3LPnfamW1RhaXPctL8G0Lj4WVNqJkhk1nnS+A5Z21wxw== + AQAB + + + + + + + + 2022-02-01T04:00:00+00:00 + + + + + 2GrtUTk1CEDaqlG+Cq/RIrbT29BNG+spDJ8FVfBsMoyCSuzhnp/MSqbKXz1n6veO0QIXxMk2FfWtIG5R5Tclkw== + + + CN=CA de Ciudadanos y Entidades (4) - DESARROLLO,OU=NZZ Ziurtagiri publikoa - Certificado publico SCI,O=IZENPE S.A.,C=ES + 7030 + + + + + + + https://www.batuz.eus/fitxategiak/batuz/ticketbai/sinadura_elektronikoaren_zehaztapenak_especificaciones_de_la_firma_electronica_v1_0.pdf + + + + + Quzn98x3PMbSHwbUzaj5f5KOpiH0u8bvmwbbbNkO9Es= + + + + + + Thirdparty + + + + + + + + urn:oid:1.2.840.10003.5.109.10 + + + text/xml + + + + + + + + \ No newline at end of file diff --git a/test/data/out/invoice-es-nl-tbai-b2c.xml b/test/data/out/invoice-es-nl-tbai-b2c.xml new file mode 100644 index 0000000..694e096 --- /dev/null +++ b/test/data/out/invoice-es-nl-tbai-b2c.xml @@ -0,0 +1,183 @@ + + + + 1.2 + + + + B98602642 + Provide One S.L. + + + + + NL + 02 + + + Sample End-Consumer + + + T + + + + 0002 + 01-02-2022 + 05:00:00 + N + + + 18-12-2023 + Some random description + + + Development services + 20 + 90.00 + 180.00 + 1960.00 + + + Some merch + 1 + 90.00 + 0.00 + 109.00 + + + Some essential needs merch + 1 + 30.00 + 0.00 + 33.00 + + + 2101.80 + 0.00 + + + 02 + + + + + + + + + RL + 1620.00 + + + + + + + RL + 120.00 + + + + + + + + + + 1234567890 + 01-01-2021 + 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 + + + My License + + 12345678A + + My Software + 1.0 + + + + + + + + + + + + feNN9wvG8ZuslzX+ZqpWK+BtVZi13LNSbYQmEkNjNkeYfRAIsSbVY4UlO6Eg6FK1ZUF5pWKyizvlLzCB0y75vw== + + + + tOWvm1vX5pH0Y1uemsda83AfAZ4YDQp9xQpymUpTm8M14gPkDzpmHLC1b6p6ZdlCPuaG7HYKGOKHMq1SjGRyPg== + + + + rGJ18IKfu3f1kUgtYC63q1BZ7Pqz4EbuA3jc3vWp34mGJEXwXBxDl48BP29nQ7VE3mC6tb+jZLtD3dd+9CY+Kw== + + + inQf2j9R8B/1GXB1quHpaRJAgO4ZVEnourM0SGLh0ArZbAekHXRnHNiqWx2hPi09j4C8ZJjZ4E4L/eVrOrsPcx247VV6ZIaPN19kEsDj7p8HrG7iSnSCNBMGiL87JE9dJ2aGrzvXOSenKa3YT8Lg8v5fizcppzcxMeT/9ZwY04WI/3cl6CBjALH/Sx8cb4NDrA/Ygk0ZX10DputXGAspqR9x0VHqVHWktsotQQRz5oDpYIGIeSbNoW0CNgIPV36BW0jln6FHY9B/ZcAIhE0hXFQSHceFu0sFaECjnEs3UlkyQ7bhkzcbAmlgtusdaNOI7H060d4z4hvBbFZE/SOYtg== + + + MIIJtzCCB5+gAwIBAgICG3YwDQYJKoZIhvcNAQELBQAwgZcxCzAJBgNVBAYTAkVTMRQwEgYDVQQKDAtJWkVOUEUgUy5BLjE6MDgGA1UECwwxTlpaIFppdXJ0YWdpcmkgcHVibGlrb2EgLSBDZXJ0aWZpY2FkbyBwdWJsaWNvIFNDSTE2MDQGA1UEAwwtQ0EgZGUgQ2l1ZGFkYW5vcyB5IEVudGlkYWRlcyAoNCkgLSBERVNBUlJPTExPMB4XDTIxMDMxNTEwMDQzN1oXDTI1MDMxNTEwMDQzN1owggFtMQswCQYDVQQGEwJFUzFvMG0GA1UEDQxmUmVnOkdhc3RlaXogL0hvamE6Qi0xNjY0NzIgL1RvbW86MzczOTMgL1NlY2Npb246OCAvTGlicm86MSAvRm9saW86MjAgL0ZlY2hhOjEwLTAyLTE5NzQgL0luc2NyaXBjacOzbjoxMRgwFgYDVQRhDA9WQVRFUy1TNzgzNjEwN0gxPTA7BgNVBAsMNE9yZGV6a2FyaSB6aXVydGFnaXJpYSAtIENlcnRpZmljYWRvIGRlIHJlcHJlc2VudGFudGUxFDASBgNVBAoMC0laRU5QRSBTLkEuMRIwEAYDVQQFEwk5OTk5OTk3M0sxGDAWBgNVBAQMD0ZJQ1RJQ0lPIEFDVElWTzEWMBQGA1UEKgwNUkVQUkVTRU5UQU5URTE4MDYGA1UEAwwvOTk5OTk5NzNLIFJFUFJFU0VOVEFOVEUgRklDVElDSU8gKFI6IFM3ODM2MTA3SCkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsI8cbiOSsEMrK+lr6Vn7xeDlI1UJIVWM4kMTyXoFcU9F7LkdbSv5jS1D1g3/c8YId1nFjPrHXjpBbjv4Am9QEvHKPa9djI9lTKS3gut5DDU1ePRAagnSCAr2Y6m4isbMF54S5tp0/Ng+myx5c2E+hMmgNw6uZ9KvdwaYY1gQW/N/7qS0KlA1eB0CSHyzZeVRgbYAXI6AMCtuYCRVNLbnzJBvSN0J4SuZeiM/KK0I0oj/8THajszp8hg3v2cfOMAGu5cM3yuBAPTBPBZCkGofwZqMn2ioMZXwRYuXJv4UEncs+d9qZbVGFpc9y0vwbQuPhZU2omSGTWedL4DlnbXDHAgMBAAGjggQyMIIELjCBxwYDVR0SBIG/MIG8hhVodHRwOi8vd3d3Lml6ZW5wZS5jb22BD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAxMCBWaXRvcmlhLUdhc3RlaXowDgYDVR0PAQH/BAQDAgXgMB8GA1UdJQQYMBYGCCsGAQUFBwMCBgorBgEEAYI3CgMMMB0GA1UdDgQWBBQRR3q3tcdVPr8U7NS+zkf7EBt32jAfBgNVHSMEGDAWgBRv//0N8gxjDyZxXRrgb2VkZjhrkzCCATQGA1UdIASCASswggEnMIIBDQYJKwYBBAHzOWYMMIH/MCUGCCsGAQUFBwIBFhlodHRwOi8vd3d3Lml6ZW5wZS5jb20vY3BzMIHVBggrBgEFBQcCAjCByAyBxUtvbnRzdWx0YSB3d3cuaXplbnBlLmNvbS1lbiBiYWxkaW50emFrIGV0YSBrb25kaXppb2FrIHppdXJ0YWdpcmlhbiBmaWRhdHUgZWRvIGVyYWJpbGkgYXVycmV0aWsgLSBDb25zdWx0ZSBlbiB3d3cuaXplbnBlLmNvbSBsb3MgdMOpcm1pbm9zIHkgY29uZGljaW9uZXMgYW50ZXMgZGUgdXRpbGl6YXIgbyBjb25maWFyIGVuIGVsIGNlcnRpZmljYWRvMAkGBwQAi+xAAQIwCQYHYIVUAQMFCDCBogYIKwYBBQUHAQEEgZUwgZIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwZGVzLml6ZW5wZS5jb20waQYIKwYBBQUHMAKGXWh0dHA6Ly93d3cuaXplbnBlLmNvbS9jb250ZW5pZG9zL2luZm9ybWFjaW9uL2Nhc19pemVucGUvZXNfY2FzL2FkanVudG9zL0NDRUVSX2NlcnRfc2hhMjU2LmNydDCB2wYIKwYBBQUHAQMEgc4wgcswCAYGBACORgEBMAgGBgQAjkYBBDALBgYEAI5GAQMCAQ8wfAYGBACORgEFMHIwJBYeaHR0cHM6Ly93d3cuaXplbnBlLmNvbS9wZHMvZW4vEwJlbjAkFh5odHRwczovL3d3dy5pemVucGUuY29tL3Bkcy9ldS8TAmV1MCQWHmh0dHBzOi8vd3d3Lml6ZW5wZS5jb20vcGRzL2VzLxMCZXMwEwYGBACORgEGMAkGBwQAjkYBBgEwFQYIKwYBBQUHCwIwCQYHBACL7EkBAjA2BgNVHR8ELzAtMCugKaAnhiVodHRwOi8vY3JsZGVzLml6ZW5wZS5jb20vY2dpLWJpbi9jcmwyMA0GCSqGSIb3DQEBCwUAA4ICAQCJaRL+xyX6HFu/6AX7N0j/r1ZB3OAY8t4S3KBvxQBs/PGeQmHFr8cnFXxb2cfkZ/5IzDDMElicChXq86BgaXn6xxtw4q30/qsuwz8iwF+mLKENFGIYTdxmaGCuSBwhENWrv03uUeskL4gIMQIu5fhpdZRj2aW4ccsb9QdfLCHRtjxrgZMoL7tpXRYQcpKgMKroAg3PuIhwdg3eLhVZvihtUJ0oNGtEBz+04eRCZjXx8dWVrmYxfTEqHFaYc8Nxu6AuQvPQdBAC2DoxYLUpihUBvKo8aUrU/QhUCue76Sq9hnzi6TXCofNeixMZBB+ODwvcInWTX0N/m7zC/9wRcQ1vE6gO6lbNd6JnwiLHclf+oLAsRLWwN9dEeDEER2IvtlmIapKhxDrPQ9zSAiibaVSAVxvWC5bal2CgfgijlamDQ8lPD+/Fv1O6s8hGTEfMiDNErCC7IWn3ckjg+Ipz11DQO2hRI/VmarFDFtreavVsSgwseVJIQxavgOzMJVFRx1TQCBZFvW9RnM32QDygq2vrs234fD3ak8DtdmKd0aPLNYSN3zaiaPOzfhK6Z5m36Zltqzhzjg0YkoPcbmPGUO5tq7X7SwFeb0Kx7gEPzZHavbcnNYtuPgqFJklNk2+3zztodccRgZGtLRQJay+xwSNh8YiU+NmIgx2ad24qugodzg== + + + + rCPHG4jkrBDKyvpa+lZ+8Xg5SNVCSFVjOJDE8l6BXFPRey5HW0r+Y0tQ9YN/3PGCHdZxYz6x146QW47+AJvUBLxyj2vXYyPZUykt4LreQw1NXj0QGoJ0ggK9mOpuIrGzBeeEubadPzYPpsseXNhPoTJoDcOrmfSr3cGmGNYEFvzf+6ktCpQNXgdAkh8s2XlUYG2AFyOgDArbmAkVTS258yQb0jdCeErmXojPyitCNKI//Ex2o7M6fIYN79nHzjABruXDN8rgQD0wTwWQpBqH8GajJ9oqDGV8EWLlyb+FBJ3LPnfamW1RhaXPctL8G0Lj4WVNqJkhk1nnS+A5Z21wxw== + AQAB + + + + + + + + 2022-02-01T04:00:00+00:00 + + + + + 2GrtUTk1CEDaqlG+Cq/RIrbT29BNG+spDJ8FVfBsMoyCSuzhnp/MSqbKXz1n6veO0QIXxMk2FfWtIG5R5Tclkw== + + + CN=CA de Ciudadanos y Entidades (4) - DESARROLLO,OU=NZZ Ziurtagiri publikoa - Certificado publico SCI,O=IZENPE S.A.,C=ES + 7030 + + + + + + + https://www.batuz.eus/fitxategiak/batuz/ticketbai/sinadura_elektronikoaren_zehaztapenak_especificaciones_de_la_firma_electronica_v1_0.pdf + + + + + Quzn98x3PMbSHwbUzaj5f5KOpiH0u8bvmwbbbNkO9Es= + + + + + + Thirdparty + + + + + + + + urn:oid:1.2.840.10003.5.109.10 + + + text/xml + + + + + + + + \ No newline at end of file diff --git a/test/data/out/invoice-es-nl-tbai-exempt.xml b/test/data/out/invoice-es-nl-tbai-exempt.xml new file mode 100644 index 0000000..04b82a2 --- /dev/null +++ b/test/data/out/invoice-es-nl-tbai-exempt.xml @@ -0,0 +1,163 @@ + + + + 1.2 + + + + B98602642 + Provide One S.L. + + + + + NL + 02 + 000099995B57 + + Sample Consumer + + + T + + + + 0002 + 01-02-2022 + 05:00:00 + N + + + 01-02-2022 + Some random description + + + Development services + 20 + 90.00 + 180.00 + 1620.00 + + + 1620.00 + 0.00 + + + 02 + + + + + + + + + + E1 + 1620.00 + + + + + + + + + + + 1234567890 + 01-01-2021 + 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 + + + My License + + 12345678A + + My Software + 1.0 + + + + + + + + + + + + T6wmbQq6CTbSBsl9iMIwTec2TkumtK5yxq5CRp25ddzpL6ym+TlFWkjWzJSZV6itokz+a6ubanHnUZxRwhXYPg== + + + + HfGrpOtdYsBll3THrwguU7RCRej5wkclfM+uLsVMuuOdMPC9ThhPrt+kbOzMQ3MRV8Ua58GmyxDuTXsN4zbJpA== + + + + 3g96M1rxPgP4xSbE+UUe7zZ1s4mEQrjgo8Ee1ti+A6Y3s/HmigwCw1aJhIK42hSdU5U+Dmt52KVM9ZKguB07Ig== + + + Yi3HtylFzIP+wuX03jY0f1fwRAy9pWR0pG+MV9HawJsfNSCrVZf51/gbkzthdlthHhGPj/9CLsuxvopnQej7HTgY6I3/2KCPpGnQgULSiXXdT6ChUeq3h0R9fKTg2fBO6aVMmFuOUH6f87jZt0OkEmYmz8Sa5gRjhniti8tRIpjmoD0ztMfcBvraNf/y+ENPwBkhNTUIaLIoRe2Se1gm6tV9RCS6aq666Chp1nrfPIBr7Hstt16opGxtDg4LuVS4/aQhnsGVcI/iq8Ev81bHD16OKInri5jxkLh5qDmrGJPYmj5YY1La2uBnC/ceNoDdnZecSJYXBATvM7mS1E6ghA== + + + MIIJtzCCB5+gAwIBAgICG3YwDQYJKoZIhvcNAQELBQAwgZcxCzAJBgNVBAYTAkVTMRQwEgYDVQQKDAtJWkVOUEUgUy5BLjE6MDgGA1UECwwxTlpaIFppdXJ0YWdpcmkgcHVibGlrb2EgLSBDZXJ0aWZpY2FkbyBwdWJsaWNvIFNDSTE2MDQGA1UEAwwtQ0EgZGUgQ2l1ZGFkYW5vcyB5IEVudGlkYWRlcyAoNCkgLSBERVNBUlJPTExPMB4XDTIxMDMxNTEwMDQzN1oXDTI1MDMxNTEwMDQzN1owggFtMQswCQYDVQQGEwJFUzFvMG0GA1UEDQxmUmVnOkdhc3RlaXogL0hvamE6Qi0xNjY0NzIgL1RvbW86MzczOTMgL1NlY2Npb246OCAvTGlicm86MSAvRm9saW86MjAgL0ZlY2hhOjEwLTAyLTE5NzQgL0luc2NyaXBjacOzbjoxMRgwFgYDVQRhDA9WQVRFUy1TNzgzNjEwN0gxPTA7BgNVBAsMNE9yZGV6a2FyaSB6aXVydGFnaXJpYSAtIENlcnRpZmljYWRvIGRlIHJlcHJlc2VudGFudGUxFDASBgNVBAoMC0laRU5QRSBTLkEuMRIwEAYDVQQFEwk5OTk5OTk3M0sxGDAWBgNVBAQMD0ZJQ1RJQ0lPIEFDVElWTzEWMBQGA1UEKgwNUkVQUkVTRU5UQU5URTE4MDYGA1UEAwwvOTk5OTk5NzNLIFJFUFJFU0VOVEFOVEUgRklDVElDSU8gKFI6IFM3ODM2MTA3SCkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsI8cbiOSsEMrK+lr6Vn7xeDlI1UJIVWM4kMTyXoFcU9F7LkdbSv5jS1D1g3/c8YId1nFjPrHXjpBbjv4Am9QEvHKPa9djI9lTKS3gut5DDU1ePRAagnSCAr2Y6m4isbMF54S5tp0/Ng+myx5c2E+hMmgNw6uZ9KvdwaYY1gQW/N/7qS0KlA1eB0CSHyzZeVRgbYAXI6AMCtuYCRVNLbnzJBvSN0J4SuZeiM/KK0I0oj/8THajszp8hg3v2cfOMAGu5cM3yuBAPTBPBZCkGofwZqMn2ioMZXwRYuXJv4UEncs+d9qZbVGFpc9y0vwbQuPhZU2omSGTWedL4DlnbXDHAgMBAAGjggQyMIIELjCBxwYDVR0SBIG/MIG8hhVodHRwOi8vd3d3Lml6ZW5wZS5jb22BD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAxMCBWaXRvcmlhLUdhc3RlaXowDgYDVR0PAQH/BAQDAgXgMB8GA1UdJQQYMBYGCCsGAQUFBwMCBgorBgEEAYI3CgMMMB0GA1UdDgQWBBQRR3q3tcdVPr8U7NS+zkf7EBt32jAfBgNVHSMEGDAWgBRv//0N8gxjDyZxXRrgb2VkZjhrkzCCATQGA1UdIASCASswggEnMIIBDQYJKwYBBAHzOWYMMIH/MCUGCCsGAQUFBwIBFhlodHRwOi8vd3d3Lml6ZW5wZS5jb20vY3BzMIHVBggrBgEFBQcCAjCByAyBxUtvbnRzdWx0YSB3d3cuaXplbnBlLmNvbS1lbiBiYWxkaW50emFrIGV0YSBrb25kaXppb2FrIHppdXJ0YWdpcmlhbiBmaWRhdHUgZWRvIGVyYWJpbGkgYXVycmV0aWsgLSBDb25zdWx0ZSBlbiB3d3cuaXplbnBlLmNvbSBsb3MgdMOpcm1pbm9zIHkgY29uZGljaW9uZXMgYW50ZXMgZGUgdXRpbGl6YXIgbyBjb25maWFyIGVuIGVsIGNlcnRpZmljYWRvMAkGBwQAi+xAAQIwCQYHYIVUAQMFCDCBogYIKwYBBQUHAQEEgZUwgZIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwZGVzLml6ZW5wZS5jb20waQYIKwYBBQUHMAKGXWh0dHA6Ly93d3cuaXplbnBlLmNvbS9jb250ZW5pZG9zL2luZm9ybWFjaW9uL2Nhc19pemVucGUvZXNfY2FzL2FkanVudG9zL0NDRUVSX2NlcnRfc2hhMjU2LmNydDCB2wYIKwYBBQUHAQMEgc4wgcswCAYGBACORgEBMAgGBgQAjkYBBDALBgYEAI5GAQMCAQ8wfAYGBACORgEFMHIwJBYeaHR0cHM6Ly93d3cuaXplbnBlLmNvbS9wZHMvZW4vEwJlbjAkFh5odHRwczovL3d3dy5pemVucGUuY29tL3Bkcy9ldS8TAmV1MCQWHmh0dHBzOi8vd3d3Lml6ZW5wZS5jb20vcGRzL2VzLxMCZXMwEwYGBACORgEGMAkGBwQAjkYBBgEwFQYIKwYBBQUHCwIwCQYHBACL7EkBAjA2BgNVHR8ELzAtMCugKaAnhiVodHRwOi8vY3JsZGVzLml6ZW5wZS5jb20vY2dpLWJpbi9jcmwyMA0GCSqGSIb3DQEBCwUAA4ICAQCJaRL+xyX6HFu/6AX7N0j/r1ZB3OAY8t4S3KBvxQBs/PGeQmHFr8cnFXxb2cfkZ/5IzDDMElicChXq86BgaXn6xxtw4q30/qsuwz8iwF+mLKENFGIYTdxmaGCuSBwhENWrv03uUeskL4gIMQIu5fhpdZRj2aW4ccsb9QdfLCHRtjxrgZMoL7tpXRYQcpKgMKroAg3PuIhwdg3eLhVZvihtUJ0oNGtEBz+04eRCZjXx8dWVrmYxfTEqHFaYc8Nxu6AuQvPQdBAC2DoxYLUpihUBvKo8aUrU/QhUCue76Sq9hnzi6TXCofNeixMZBB+ODwvcInWTX0N/m7zC/9wRcQ1vE6gO6lbNd6JnwiLHclf+oLAsRLWwN9dEeDEER2IvtlmIapKhxDrPQ9zSAiibaVSAVxvWC5bal2CgfgijlamDQ8lPD+/Fv1O6s8hGTEfMiDNErCC7IWn3ckjg+Ipz11DQO2hRI/VmarFDFtreavVsSgwseVJIQxavgOzMJVFRx1TQCBZFvW9RnM32QDygq2vrs234fD3ak8DtdmKd0aPLNYSN3zaiaPOzfhK6Z5m36Zltqzhzjg0YkoPcbmPGUO5tq7X7SwFeb0Kx7gEPzZHavbcnNYtuPgqFJklNk2+3zztodccRgZGtLRQJay+xwSNh8YiU+NmIgx2ad24qugodzg== + + + + rCPHG4jkrBDKyvpa+lZ+8Xg5SNVCSFVjOJDE8l6BXFPRey5HW0r+Y0tQ9YN/3PGCHdZxYz6x146QW47+AJvUBLxyj2vXYyPZUykt4LreQw1NXj0QGoJ0ggK9mOpuIrGzBeeEubadPzYPpsseXNhPoTJoDcOrmfSr3cGmGNYEFvzf+6ktCpQNXgdAkh8s2XlUYG2AFyOgDArbmAkVTS258yQb0jdCeErmXojPyitCNKI//Ex2o7M6fIYN79nHzjABruXDN8rgQD0wTwWQpBqH8GajJ9oqDGV8EWLlyb+FBJ3LPnfamW1RhaXPctL8G0Lj4WVNqJkhk1nnS+A5Z21wxw== + AQAB + + + + + + + + 2022-02-01T04:00:00+00:00 + + + + + 2GrtUTk1CEDaqlG+Cq/RIrbT29BNG+spDJ8FVfBsMoyCSuzhnp/MSqbKXz1n6veO0QIXxMk2FfWtIG5R5Tclkw== + + + CN=CA de Ciudadanos y Entidades (4) - DESARROLLO,OU=NZZ Ziurtagiri publikoa - Certificado publico SCI,O=IZENPE S.A.,C=ES + 7030 + + + + + + + https://www.batuz.eus/fitxategiak/batuz/ticketbai/sinadura_elektronikoaren_zehaztapenak_especificaciones_de_la_firma_electronica_v1_0.pdf + + + + + Quzn98x3PMbSHwbUzaj5f5KOpiH0u8bvmwbbbNkO9Es= + + + + + + Thirdparty + + + + + + + + urn:oid:1.2.840.10003.5.109.10 + + + text/xml + + + + + + + + \ No newline at end of file diff --git a/test/data/sample-invoice-export.json b/test/data/sample-invoice-export.json index d88427c..a7dcbd6 100644 --- a/test/data/sample-invoice-export.json +++ b/test/data/sample-invoice-export.json @@ -4,7 +4,7 @@ "uuid": "90e8a582-92ae-11ee-8a77-e6a7901137ed", "dig": { "alg": "sha256", - "val": "8aa806c8439f75826a67d190804e08c5ce9afb7868687618005cc9055219e8cf" + "val": "40e741970fefffb416ecd8983308ff10cf49e84a730aa1a31d43ced71815fc78" }, "draft": true }, @@ -63,7 +63,6 @@ "i": 1, "quantity": "6", "item": { - "key": "goods", "name": "Item being purchased", "price": "100.00" }, @@ -73,7 +72,8 @@ "cat": "VAT", "rate": "exempt", "ext": { - "es-tbai-exemption": "E1" + "es-tbai-exemption": "E1", + "es-tbai-product": "goods" } } ], @@ -83,7 +83,6 @@ "i": 2, "quantity": "5", "item": { - "key": "goods", "name": "Another item being purchased", "price": "20.00" }, @@ -93,7 +92,8 @@ "cat": "VAT", "rate": "exempt", "ext": { - "es-tbai-exemption": "E2" + "es-tbai-exemption": "E2", + "es-tbai-product": "goods" } } ], @@ -103,7 +103,6 @@ "i": 3, "quantity": "2", "item": { - "key": "service", "name": "Service being paid", "price": "150.00" }, @@ -112,7 +111,10 @@ { "cat": "VAT", "rate": "standard", - "percent": "21.0%" + "percent": "21.0%", + "ext": { + "es-tbai-product": "services" + } }, { "cat": "IRPF", @@ -134,7 +136,8 @@ { "key": "exempt", "ext": { - "es-tbai-exemption": "E1" + "es-tbai-exemption": "E1", + "es-tbai-product": "goods" }, "base": "600.00", "amount": "0.00" @@ -142,13 +145,17 @@ { "key": "exempt", "ext": { - "es-tbai-exemption": "E2" + "es-tbai-exemption": "E2", + "es-tbai-product": "goods" }, "base": "100.00", "amount": "0.00" }, { "key": "standard", + "ext": { + "es-tbai-product": "services" + }, "base": "300.00", "percent": "21.0%", "amount": "63.00" diff --git a/test/data/sample-invoice-export.yaml b/test/data/sample-invoice-export.yaml index 01360d4..909a9bb 100644 --- a/test/data/sample-invoice-export.yaml +++ b/test/data/sample-invoice-export.yaml @@ -32,50 +32,37 @@ customer: - code: "42501" locality: "Actopan" country: "MX" -# customer: -# tax_id: -# country: "ES" -# code: "93542762Z" -# name: "Pere Sancho Latorre" -# emails: -# - addr: "mzkrme5v@blu.it" -# addresses: -# - num: "58" -# street: "Praza Real" -# locality: "Caudete de las Fuentes" -# region: "Valencia" -# code: "46701" -# country: "ES" lines: - quantity: 6 item: name: "Item being purchased" price: "100.00" - key: "goods" taxes: - cat: "VAT" rate: "exempt" ext: es-tbai-exemption: "E1" + es-tbai-product: "goods" - quantity: 5 item: name: "Another item being purchased" price: "20.00" - key: "goods" taxes: - cat: "VAT" rate: "exempt" ext: es-tbai-exemption: "E2" + es-tbai-product: "goods" - quantity: 2 item: name: "Service being paid" price: "150.00" - key: "service" taxes: - cat: "VAT" rate: "standard" + ext: + es-tbai-product: "services" - cat: "IRPF" rate: "pro"