Skip to content

Commit 6a89a85

Browse files
pmenendzclaude
andcommitted
Drop legacy tag/key logic, defer to addon extensions
newClaves now reads ClaveRegimen from rate.Ext (es-tbai-regime); breakdown classifies rates from es-tbai-exemption alone; otherIdentity reads the IDType straight from es-tbai-identity-type. The addon is already required, so the per-rate signal and identity-key mappings duplicated here can go. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b002bbd commit 6a89a85

6 files changed

Lines changed: 50 additions & 122 deletions

File tree

convert/breakdown.go

Lines changed: 20 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"github.com/invopop/gobl/cbc"
99
"github.com/invopop/gobl/l10n"
1010
"github.com/invopop/gobl/num"
11-
"github.com/invopop/gobl/regimes/es"
1211
"github.com/invopop/gobl/tax"
1312
)
1413

@@ -87,11 +86,6 @@ type DetalleNoSujeta struct {
8786
Importe num.Amount
8887
}
8988

90-
type taxInfo struct {
91-
simplifiedRegime bool
92-
customerRates bool
93-
}
94-
9589
func newTipoDesglose(gobl *bill.Invoice) *TipoDesglose {
9690
if gobl.Totals == nil || gobl.Totals.Taxes == nil {
9791
return nil
@@ -100,25 +94,24 @@ func newTipoDesglose(gobl *bill.Invoice) *TipoDesglose {
10094
if catTotal == nil {
10195
return nil
10296
}
103-
taxInfo := newTaxInfo(gobl)
10497

10598
desglose := &TipoDesglose{}
10699

107100
if gobl.Customer == nil || partyCountry(gobl.Customer) == l10n.ES.Tax() {
108-
desglose.DesgloseFactura = newDesgloseFactura(taxInfo, catTotal.Rates)
101+
desglose.DesgloseFactura = newDesgloseFactura(catTotal.Rates)
109102
} else {
110103
goods, services := splitByTBAIProduct(catTotal.Rates)
111104

112105
desglose.DesgloseTipoOperacion = &DesgloseTipoOperacion{
113-
Entrega: newDesgloseFactura(taxInfo, goods),
114-
PrestacionServicios: newDesgloseFactura(taxInfo, services),
106+
Entrega: newDesgloseFactura(goods),
107+
PrestacionServicios: newDesgloseFactura(services),
115108
}
116109
}
117110

118111
return desglose
119112
}
120113

121-
func newDesgloseFactura(taxInfo taxInfo, rates []*tax.RateTotal) *DesgloseFactura {
114+
func newDesgloseFactura(rates []*tax.RateTotal) *DesgloseFactura {
122115
if len(rates) == 0 {
123116
return nil
124117
}
@@ -132,25 +125,25 @@ func newDesgloseFactura(taxInfo taxInfo, rates []*tax.RateTotal) *DesgloseFactur
132125
}
133126

134127
for _, rate := range rates {
135-
if taxInfo.isNoSujeta(rate) {
128+
code := rate.Ext.Get(tbai.ExtKeyExempt)
129+
switch {
130+
case code.In(notSubjectExemptionCodes...):
136131
df.NoSujeta.appendDetalle(&DetalleNoSujeta{
137-
Causa: taxInfo.causaNoSujeta(rate),
132+
Causa: code.String(),
138133
Importe: rate.Base,
139134
})
140-
} else if taxInfo.isExenta(rate) {
135+
case code.In(exemptExemptionCodes...):
141136
df.Sujeta.Exenta.appendDetalle(&DetalleExenta{
142-
CausaExencion: rate.Ext.Get(tbai.ExtKeyExempt).String(),
137+
CausaExencion: code.String(),
143138
BaseImponible: rate.Base.Rescale(2).String(),
144139
})
145-
} else {
140+
default:
146141
dne := df.Sujeta.NoExenta.appendDetalle(&DetalleNoExenta{
147142
TipoNoExenta: nonExemptedType(rate),
148143
DesgloseIVA: &DesgloseIVA{},
149144
})
150145

151-
diva := newDetalleIVA(taxInfo, rate)
152-
153-
dne.DesgloseIVA.appendDetalle(diva)
146+
dne.DesgloseIVA.appendDetalle(newDetalleIVA(rate))
154147
}
155148
}
156149

@@ -217,7 +210,7 @@ func (di *DesgloseIVA) appendDetalle(d *DetalleIVA) *DetalleIVA {
217210
return d
218211
}
219212

220-
func newDetalleIVA(taxInfo taxInfo, rate *tax.RateTotal) *DetalleIVA {
213+
func newDetalleIVA(rate *tax.RateTotal) *DetalleIVA {
221214
percent := num.PercentageZero
222215
if rate.Percent != nil {
223216
percent = *rate.Percent
@@ -233,7 +226,7 @@ func newDetalleIVA(taxInfo taxInfo, rate *tax.RateTotal) *DetalleIVA {
233226
diva.CuotaRecargoEquivalencia = rate.Surcharge.Amount.Rescale(2).String()
234227
}
235228

236-
if taxInfo.simplifiedRegime || rate.Ext.Get(tbai.ExtKeyProduct) == "resale" {
229+
if rate.Ext.Get(tbai.ExtKeyRegime) == "52" || rate.Ext.Get(tbai.ExtKeyProduct) == "resale" {
237230
diva.OperacionEnRecargoDeEquivalenciaORegimenSimplificado = "S"
238231
}
239232

@@ -249,47 +242,19 @@ func formatPercent(percent num.Percentage) string {
249242
return maybeNegative
250243
}
251244

252-
func newTaxInfo(gobl *bill.Invoice) taxInfo {
253-
return taxInfo{
254-
simplifiedRegime: gobl.HasTags(es.TagSimplifiedScheme),
255-
customerRates: gobl.HasTags(tax.TagCustomerRates),
256-
}
257-
}
258-
259-
// notSubjectExemptionCodes lists the es-tbai-exemption codes that map to
260-
// DetalleNoSujeta/Causa.
245+
// es-tbai-exemption codes routed to DetalleNoSujeta.
261246
var notSubjectExemptionCodes = []cbc.Code{"OT", "RL", "VT", "IE"}
262247

263-
// reverseChargeExemptionCodes lists the es-tbai-exemption codes that map to
264-
// DetalleNoExenta/TipoNoExenta = S2.
248+
// es-tbai-exemption codes routed to Sujeta.Exenta.
249+
var exemptExemptionCodes = []cbc.Code{"E1", "E2", "E3", "E4", "E5", "E6"}
250+
251+
// es-tbai-exemption codes routed to DetalleNoExenta with TipoNoExenta=S2.
265252
var reverseChargeExemptionCodes = []cbc.Code{"S2"}
266253

267-
// nonExemptedType returns the TBAI TipoNoExenta value for a subject,
268-
// non-exempt tax rate.
254+
// nonExemptedType returns the TipoNoExenta value for a non-exempt rate.
269255
func nonExemptedType(r *tax.RateTotal) string {
270256
if r.Ext.Get(tbai.ExtKeyExempt).In(reverseChargeExemptionCodes...) {
271257
return "S2"
272258
}
273259
return "S1"
274260
}
275-
276-
func (t taxInfo) isNoSujeta(r *tax.RateTotal) bool {
277-
if t.customerRates {
278-
return true
279-
}
280-
return r.Percent == nil && r.Ext.Get(tbai.ExtKeyExempt).In(notSubjectExemptionCodes...)
281-
}
282-
283-
func (t taxInfo) causaNoSujeta(r *tax.RateTotal) string {
284-
if t.customerRates {
285-
return "RL"
286-
}
287-
return r.Ext.Get(tbai.ExtKeyExempt).String()
288-
}
289-
290-
func (taxInfo) isExenta(r *tax.RateTotal) bool {
291-
code := r.Ext.Get(tbai.ExtKeyExempt)
292-
return r.Percent == nil &&
293-
!code.In(notSubjectExemptionCodes...) &&
294-
!code.In(reverseChargeExemptionCodes...)
295-
}

convert/doc_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ func TestInvoiceConversion(t *testing.T) {
105105
Code: "PP123456S",
106106
},
107107
}
108+
require.NoError(t, goblInvoice.Calculate())
108109
invoice, _ := convert.NewTicketBAI(goblInvoice, ts, role, convert.ZoneBI)
109110

110111
dest := invoice.Sujetos.Destinatarios.IDDestinatario[0]
@@ -119,6 +120,7 @@ func TestInvoiceConversion(t *testing.T) {
119120
goblInvoice.Customer.Identities = []*org.Identity{
120121
{Key: org.IdentityKeyPassport, Code: "PP123456S"},
121122
}
123+
require.NoError(t, goblInvoice.Calculate())
122124
invoice, _ := convert.NewTicketBAI(goblInvoice, ts, role, convert.ZoneBI)
123125

124126
dest := invoice.Sujetos.Destinatarios.IDDestinatario[0]
@@ -134,6 +136,7 @@ func TestInvoiceConversion(t *testing.T) {
134136
goblInvoice.Customer.Identities = []*org.Identity{
135137
{Country: "ES", Key: org.IdentityKeyPassport, Code: "PP123456S"},
136138
}
139+
require.NoError(t, goblInvoice.Calculate())
137140
invoice, _ := convert.NewTicketBAI(goblInvoice, ts, role, convert.ZoneBI)
138141

139142
dest := invoice.Sujetos.Destinatarios.IDDestinatario[0]

convert/invoice.go

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"github.com/invopop/gobl/bill"
66
"github.com/invopop/gobl/num"
77
"github.com/invopop/gobl/org"
8-
"github.com/invopop/gobl/regimes/es"
98
"github.com/invopop/gobl/tax"
109
)
1110

@@ -146,8 +145,8 @@ func newRetencionSoportada(inv *bill.Invoice) string {
146145
return totalRetention.String()
147146
}
148147

149-
// newClaves builds the IDClave list with distinct ClaveRegimen codes derived
150-
// from each VAT rate on the invoice.
148+
// newClaves returns the distinct ClaveRegimen codes from each VAT rate's
149+
// es-tbai-regime extension.
151150
func newClaves(inv *bill.Invoice) []IDClave {
152151
claves := []IDClave{}
153152
seen := make(map[string]bool)
@@ -158,40 +157,19 @@ func newClaves(inv *bill.Invoice) []IDClave {
158157
seen[code] = true
159158
claves = append(claves, IDClave{ClaveRegimenIvaOpTrascendencia: code})
160159
}
161-
simplified := inv.HasTags(es.TagSimplifiedScheme)
162160
if inv.Totals != nil && inv.Totals.Taxes != nil {
163161
if cat := inv.Totals.Taxes.Category(tax.CategoryVAT); cat != nil {
164162
for _, rate := range cat.Rates {
165-
add(claveForRate(rate, simplified))
163+
add(rate.Ext.Get(tbai.ExtKeyRegime).String())
166164
}
167165
}
168166
}
169167
if len(claves) == 0 {
170-
if simplified {
171-
add("52")
172-
} else {
173-
add("01")
174-
}
168+
add("01")
175169
}
176170
return claves
177171
}
178172

179-
// claveForRate returns the ClaveRegimen code for a single VAT rate.
180-
func claveForRate(rate *tax.RateTotal, simplified bool) string {
181-
if rate == nil {
182-
return ""
183-
}
184-
switch {
185-
case rate.Key == tax.KeyExport:
186-
return "02"
187-
case rate.Surcharge != nil:
188-
return "51"
189-
case simplified:
190-
return "52"
191-
}
192-
return "01"
193-
}
194-
195173
func newFacturaRectificativa(inv *bill.Invoice) *FacturaRectificativa {
196174
if len(inv.Preceding) == 0 {
197175
return nil

convert/parties.go

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
11
package convert
22

33
import (
4-
"github.com/invopop/gobl/cbc"
4+
"github.com/invopop/gobl/addons/es/tbai"
55
"github.com/invopop/gobl/l10n"
66
"github.com/invopop/gobl/org"
77
)
88

9-
const (
10-
idTypeCodeTaxID = "02"
11-
)
12-
13-
var idTypeCodeMap = map[cbc.Key]string{
14-
org.IdentityKeyPassport: "03",
15-
org.IdentityKeyForeign: "04",
16-
org.IdentityKeyResident: "05",
17-
org.IdentityKeyOther: "06",
18-
}
19-
209
// Sujetos contains invoice parties info
2110
type Sujetos struct {
2211
Emisor *Emisor
@@ -84,35 +73,28 @@ func newDestinatario(party *org.Party) *IDDestinatario {
8473
}
8574

8675
func otherIdentity(party *org.Party) *IDOtro {
87-
oid := new(IDOtro)
88-
if party.TaxID != nil {
89-
oid.CodigoPais = party.TaxID.Country.String()
90-
}
91-
9276
if party.TaxID != nil && party.TaxID.Code != "" {
93-
oid.IDType = idTypeCodeTaxID
94-
oid.ID = party.TaxID.Code.String()
95-
return oid
96-
}
97-
98-
for _, id := range party.Identities {
99-
if id == nil || id.Code == "" {
100-
continue
77+
return &IDOtro{
78+
CodigoPais: party.TaxID.Country.String(),
79+
IDType: tbai.ExtCodeIdentityTypeVAT.String(),
80+
ID: party.TaxID.Code.String(),
10181
}
102-
it, ok := idTypeCodeMap[id.Key]
103-
if !ok {
104-
continue
105-
}
106-
107-
oid.IDType = it
108-
oid.ID = id.Code.String()
109-
if id.Country != "" {
110-
oid.CodigoPais = id.Country.String()
111-
}
112-
return oid
11382
}
114-
115-
return nil
83+
id := org.IdentityForExtKey(party.Identities, tbai.ExtKeyIdentityType)
84+
if id == nil || id.Code == "" {
85+
return nil
86+
}
87+
oid := &IDOtro{
88+
IDType: id.Ext.Get(tbai.ExtKeyIdentityType).String(),
89+
ID: id.Code.String(),
90+
}
91+
switch {
92+
case id.Country != "":
93+
oid.CodigoPais = id.Country.String()
94+
case party.TaxID != nil:
95+
oid.CodigoPais = party.TaxID.Country.String()
96+
}
97+
return oid
11698
}
11799

118100
func partyCountry(party *org.Party) l10n.TaxCountryCode {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.26.1
44

55
require (
66
github.com/go-resty/resty/v2 v2.13.1
7-
github.com/invopop/gobl v0.403.0
7+
github.com/invopop/gobl v0.403.1-0.20260518201647-1c8899e5fdc4
88
github.com/invopop/xmldsig v0.14.0
99
github.com/joho/godotenv v1.5.1
1010
github.com/lestrrat-go/helium v0.0.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2828
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
2929
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3030
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
31-
github.com/invopop/gobl v0.403.0 h1:jLkqmPbeTxu4gXntjsCtczq3rGg6JnXBwyMF/TkvOhU=
32-
github.com/invopop/gobl v0.403.0/go.mod h1:6jYbcNFgUcBXIsS3PIeZc99rrMLBaGzPsFEg7y2DyrQ=
31+
github.com/invopop/gobl v0.403.1-0.20260518201647-1c8899e5fdc4 h1:uyshjniQjI8r5cZmgUxtXge/Q/XkV7630WeoemiFVBc=
32+
github.com/invopop/gobl v0.403.1-0.20260518201647-1c8899e5fdc4/go.mod h1:6jYbcNFgUcBXIsS3PIeZc99rrMLBaGzPsFEg7y2DyrQ=
3333
github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg=
3434
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
3535
github.com/invopop/xmldsig v0.14.0 h1:ROwf32DZtX2EekrrOSjLLiY0s2kPEcqHZculITlZfAw=

0 commit comments

Comments
 (0)