From 95bfc8fc231afe07811c2eef5a2776a50cc1441e Mon Sep 17 00:00:00 2001 From: Jose Rilla Date: Wed, 5 Aug 2026 08:20:14 +0000 Subject: [PATCH 1/3] Map every AltriDatiGestionali reference to a single attribute --- items.go | 18 +++++++++----- items_parse.go | 38 +++++++++++++---------------- items_parse_internal_test.go | 25 ++++++++++---------- items_test.go | 46 ++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 39 deletions(-) diff --git a/items.go b/items.go index c9ca6df..00aa87c 100644 --- a/items.go +++ b/items.go @@ -130,9 +130,11 @@ func generateLineDetails(inv *bill.Invoice) []*LineDetail { } // attributesToOtherData maps an item's attributes to AltriDatiGestionali blocks. -// The attribute's type (or key as a fallback) becomes the TipoDato, and its value -// goes to the reference field that matches: text and code to RiferimentoTesto, -// amounts to RiferimentoNumero, and dates to RiferimentoData. +// The attribute's type (or key as a fallback) becomes the TipoDato, and each +// value fills the reference field that matches: text and code to +// RiferimentoTesto, amounts to RiferimentoNumero, and dates to RiferimentoData. +// A block may carry several references at once, so an attribute holding more +// than one value still maps to a single block. func attributesToOtherData(attrs []*org.Attribute) []*OtherData { var out []*OtherData for _, a := range attrs { @@ -147,16 +149,20 @@ func attributesToOtherData(attrs []*org.Attribute) []*OtherData { continue } od := &OtherData{DataType: name} + // Text and code share RiferimentoTesto, so only one of them fits. switch { case a.Text != "": od.TextReference = a.Text case a.Code != "": od.TextReference = a.Code.String() - case a.Amount != nil: + } + if a.Amount != nil { od.NumReference = formatAmount8(a.Amount) - case a.Date != nil: + } + if a.Date != nil { od.DateReference = a.Date.String() - default: + } + if od.TextReference == "" && od.NumReference == "" && od.DateReference == "" { // No value to map, so skip instead of emitting an empty block. continue } diff --git a/items_parse.go b/items_parse.go index fd8610d..81d4a88 100644 --- a/items_parse.go +++ b/items_parse.go @@ -154,9 +154,9 @@ func goblBillInvoiceAddLineDetails(inv *bill.Invoice, lineDetails []*LineDetail, } // otherDataToAttribute converts an AltriDatiGestionali block into a GOBL item -// attribute. TipoDato becomes the attribute type, and one of the reference -// fields becomes the value. A block may carry more than one reference, but an -// attribute holds a single value, so date wins over number, which wins over text. +// attribute. TipoDato becomes the attribute type, and each reference field +// becomes the value that matches it. A block may carry several references at +// once, and they all end up in the same attribute. func otherDataToAttribute(od *OtherData) *org.Attribute { if od == nil { return nil @@ -167,34 +167,30 @@ func otherDataToAttribute(od *OtherData) *org.Attribute { if dataType == "" { return nil } - text := strings.TrimSpace(od.TextReference) number := strings.TrimSpace(od.NumReference) dateRef := strings.TrimSpace(od.DateReference) - a := &org.Attribute{Type: cbc.Code(dataType)} - if dateRef != "" { - if date, err := parseDate(dateRef); err == nil { - a.Date = &date - return a - } + a := &org.Attribute{ + Type: cbc.Code(dataType), + Text: strings.TrimSpace(od.TextReference), } + // A reference that doesn't parse is kept as text rather than dropped, but + // only while the text is still free. if number != "" { if amount, err := parseAmount(number); err == nil { a.Amount = &amount - return a + } else if a.Text == "" { + a.Text = number } } - if text != "" { - a.Text = text - return a + if dateRef != "" { + if date, err := parseDate(dateRef); err == nil { + a.Date = &date + } else if a.Text == "" { + a.Text = dateRef + } } - // Nothing parsed cleanly, so keep the raw value instead of dropping it. - switch { - case dateRef != "": - a.Text = dateRef - case number != "": - a.Text = number - default: + if a.Text == "" && a.Amount == nil && a.Date == nil { return nil } return a diff --git a/items_parse_internal_test.go b/items_parse_internal_test.go index 6bb4649..38740ec 100644 --- a/items_parse_internal_test.go +++ b/items_parse_internal_test.go @@ -31,7 +31,7 @@ func TestOtherDataToAttribute(t *testing.T) { assert.Equal(t, "not-a-number", a.Text) }) - t.Run("prefers the date over the number and the text", func(t *testing.T) { + t.Run("keeps every reference of a block in one attribute", func(t *testing.T) { a := otherDataToAttribute(&OtherData{ DataType: "SCADENZA", TextReference: "ABC-123", @@ -39,25 +39,26 @@ func TestOtherDataToAttribute(t *testing.T) { DateReference: "2024-03-15", }) require.NotNil(t, a) + assert.Equal(t, "ABC-123", a.Text) + require.NotNil(t, a.Amount) + assert.Equal(t, "12.50", a.Amount.String()) require.NotNil(t, a.Date) assert.Equal(t, "2024-03-15", a.Date.String()) - assert.Nil(t, a.Amount) - assert.Empty(t, a.Text) }) - t.Run("prefers the number over the text", func(t *testing.T) { + t.Run("keeps both the text and the number", func(t *testing.T) { a := otherDataToAttribute(&OtherData{ - DataType: "PESO", - TextReference: "ABC-123", - NumReference: "12.50", + DataType: "CASSA-PREV", + TextReference: "ENASARCO TC07", + NumReference: "80.00", }) require.NotNil(t, a) + assert.Equal(t, "ENASARCO TC07", a.Text) require.NotNil(t, a.Amount) - assert.Equal(t, "12.50", a.Amount.String()) - assert.Empty(t, a.Text) + assert.Equal(t, "80.00", a.Amount.String()) }) - t.Run("falls back to the next reference when the preferred one is invalid", func(t *testing.T) { + t.Run("keeps the other references when the date is invalid", func(t *testing.T) { a := otherDataToAttribute(&OtherData{ DataType: "SCADENZA", TextReference: "ABC-123", @@ -68,10 +69,10 @@ func TestOtherDataToAttribute(t *testing.T) { assert.Nil(t, a.Date) require.NotNil(t, a.Amount) assert.Equal(t, "12.50", a.Amount.String()) - assert.Empty(t, a.Text) + assert.Equal(t, "ABC-123", a.Text) }) - t.Run("falls back to the text when the number is also invalid", func(t *testing.T) { + t.Run("drops an unparseable reference when the text is already taken", func(t *testing.T) { a := otherDataToAttribute(&OtherData{ DataType: "SCADENZA", TextReference: "ABC-123", diff --git a/items_test.go b/items_test.go index 8af928b..900e666 100644 --- a/items_test.go +++ b/items_test.go @@ -134,6 +134,52 @@ func TestAltriDatiGestionaliAttributes(t *testing.T) { assert.Equal(t, "2024-03-15", od[3].DateReference) }) + t.Run("should map every value of an attribute into a single block", func(t *testing.T) { + env := test.LoadTestFile("invoice-simple.json", test.PathGOBLFatturaPA) + date := cal.MakeDate(2025, 3, 10) + amount := num.MakeAmount(8000, 2) + test.ModifyInvoice(env, func(inv *bill.Invoice) { + inv.Lines[0].Item.Attributes = []*org.Attribute{ + {Type: "INTENTO", Text: "08060120341234567-000001", Date: &date}, + {Type: "CASSA-PREV", Text: "ENASARCO TC07", Amount: &amount}, + } + }) + + doc, err := test.ConvertFromGOBL(env) + require.NoError(t, err) + + od := doc.Body[0].GoodsServices.LineDetails[0].OtherData + require.Len(t, od, 2) + + // Declaration of intent: protocol number and the AdE receipt date. + assert.Equal(t, "INTENTO", od[0].DataType) + assert.Equal(t, "08060120341234567-000001", od[0].TextReference) + assert.Equal(t, "2025-03-10", od[0].DateReference) + assert.Empty(t, od[0].NumReference) + + // Pension fund: which fund and how much was contributed. + assert.Equal(t, "CASSA-PREV", od[1].DataType) + assert.Equal(t, "ENASARCO TC07", od[1].TextReference) + assert.Equal(t, "80.00", od[1].NumReference) + assert.Empty(t, od[1].DateReference) + }) + + t.Run("should prefer the text over the code, as both share RiferimentoTesto", func(t *testing.T) { + env := test.LoadTestFile("invoice-simple.json", test.PathGOBLFatturaPA) + test.ModifyInvoice(env, func(inv *bill.Invoice) { + inv.Lines[0].Item.Attributes = []*org.Attribute{ + {Type: "COLORE", Text: "rosso", Code: "RAL5010"}, + } + }) + + doc, err := test.ConvertFromGOBL(env) + require.NoError(t, err) + + od := doc.Body[0].GoodsServices.LineDetails[0].OtherData + require.Len(t, od, 1) + assert.Equal(t, "rosso", od[0].TextReference) + }) + t.Run("should fall back to the attribute key when no type is set", func(t *testing.T) { env := test.LoadTestFile("invoice-simple.json", test.PathGOBLFatturaPA) test.ModifyInvoice(env, func(inv *bill.Invoice) { From 564d47927782d094db8357341e0e2766da5cc539 Mon Sep 17 00:00:00 2001 From: Jose Rilla Date: Thu, 6 Aug 2026 16:03:48 +0000 Subject: [PATCH 2/3] Bump gobl to v0.504.0 --- go.mod | 2 +- go.sum | 2 ++ payments.go | 8 ++++---- payments_parse.go | 4 ++-- payments_parse_test.go | 5 +++-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index cbaa6b0..62a9200 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/invopop/gobl.fatturapa go 1.25.0 require ( - github.com/invopop/gobl v0.503.0 + github.com/invopop/gobl v0.504.0 github.com/invopop/xmlctx v0.13.0 github.com/invopop/xmldsig v0.10.0 github.com/lestrrat-go/libxml2 v0.0.0-20240905100032-c934e3fcb9d3 diff --git a/go.sum b/go.sum index e5d82db..4165ab0 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/gobl v0.503.0 h1:i15dqw6OlHdEWZzCgvM7MquMKKzBq7K8VMF8MQUJhGo= github.com/invopop/gobl v0.503.0/go.mod h1:HmiEdQreTSQYyNbhs81VKTmI7BAJKYC/6enh9RDwnE0= +github.com/invopop/gobl v0.504.0 h1:RLmZmeAk9RL4rkMUc7xQfC70/RGiBeekRwVgMsMthQA= +github.com/invopop/gobl v0.504.0/go.mod h1:HmiEdQreTSQYyNbhs81VKTmI7BAJKYC/6enh9RDwnE0= github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= github.com/invopop/xmlctx v0.13.0 h1:ZNRMC0O/A5h8InoLVSA7tIjjrhJn/NDBYfByBUpSb+g= diff --git a/payments.go b/payments.go index a8ea542..513d8ec 100644 --- a/payments.go +++ b/payments.go @@ -78,8 +78,8 @@ func prepareAdvancePaymentDetails(inv *bill.Invoice) []*PaymentDetailRow { row.Date = advance.Date.String() } if advance.CreditTransfer != nil { - row.IBAN = advance.CreditTransfer.IBAN - row.BIC = advance.CreditTransfer.BIC + row.IBAN = advance.CreditTransfer.IBAN.String() + row.BIC = advance.CreditTransfer.BIC.String() } dp = append(dp, row) } @@ -96,8 +96,8 @@ func preparePaymentDetails(inv *bill.Invoice) []*PaymentDetailRow { } if len(payment.Instructions.CreditTransfer) > 0 { ct1 := payment.Instructions.CreditTransfer[0] - br.IBAN = ct1.IBAN - br.BIC = ct1.BIC + br.IBAN = ct1.IBAN.String() + br.BIC = ct1.BIC.String() br.FinancialInstitution = ct1.Name } diff --git a/payments_parse.go b/payments_parse.go index 5214976..a61f9c2 100644 --- a/payments_parse.go +++ b/payments_parse.go @@ -181,8 +181,8 @@ func goblBillPaymentAddPaymentInstructions(payment *bill.PaymentDetails, payment // Add credit transfer if IBAN or BIC is available if paymentDetail.IBAN != "" || paymentDetail.BIC != "" { creditTransfer := pay.CreditTransfer{ - IBAN: paymentDetail.IBAN, - BIC: paymentDetail.BIC, + IBAN: cbc.Code(paymentDetail.IBAN), + BIC: cbc.Code(paymentDetail.BIC), } if paymentDetail.FinancialInstitution != "" { creditTransfer.Name = paymentDetail.FinancialInstitution diff --git a/payments_parse_test.go b/payments_parse_test.go index d1362fc..f40e55a 100644 --- a/payments_parse_test.go +++ b/payments_parse_test.go @@ -7,6 +7,7 @@ import ( "github.com/invopop/gobl.fatturapa/test" "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/num" "github.com/invopop/gobl/pay" "github.com/stretchr/testify/assert" @@ -70,8 +71,8 @@ func TestPaymentsInConversion(t *testing.T) { // Check credit transfer require.NotEmpty(t, invoice.Payment.Instructions.CreditTransfer) creditTransfer := invoice.Payment.Instructions.CreditTransfer[0] - assert.Equal(t, "IT60X0542811101000000123456", creditTransfer.IBAN) - assert.Equal(t, "BCITITMM", creditTransfer.BIC) + assert.Equal(t, cbc.Code("IT60X0542811101000000123456"), creditTransfer.IBAN) + assert.Equal(t, cbc.Code("BCITITMM"), creditTransfer.BIC) }) t.Run("should convert multiple due dates correctly", func(t *testing.T) { From 8b965ca4d969a47667cb097be69d157e8c878ea9 Mon Sep 17 00:00:00 2001 From: Jose Rilla Date: Thu, 6 Aug 2026 16:03:48 +0000 Subject: [PATCH 3/3] Show a multi-value block in the despatch example --- test/data/gobl.fatturapa/invoice-despatch.json | 5 +++-- test/data/gobl.fatturapa/out/invoice-despatch.xml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/data/gobl.fatturapa/invoice-despatch.json b/test/data/gobl.fatturapa/invoice-despatch.json index 0056b26..18fb3fa 100644 --- a/test/data/gobl.fatturapa/invoice-despatch.json +++ b/test/data/gobl.fatturapa/invoice-despatch.json @@ -4,7 +4,7 @@ "uuid": "679a2f25-7483-11ec-9722-7ea2cb436ff6", "dig": { "alg": "sha256", - "val": "2837c4f5ee910dd4e583548ba5ab67173ff09bcf4716037badb7e3e602c2463f" + "val": "69753172e2d48a904682d139f4536fa8128ad532268cec822d28d435ca00279f" } }, "doc": { @@ -113,7 +113,8 @@ "attributes": [ { "type": "LOTTO", - "text": "CEM-2025-07" + "text": "CEM-2025-07", + "date": "2027-07-01" }, { "type": "PESO", diff --git a/test/data/gobl.fatturapa/out/invoice-despatch.xml b/test/data/gobl.fatturapa/out/invoice-despatch.xml index 9bc0c6f..69bda90 100644 --- a/test/data/gobl.fatturapa/out/invoice-despatch.xml +++ b/test/data/gobl.fatturapa/out/invoice-despatch.xml @@ -102,6 +102,7 @@ LOTTO CEM-2025-07 + 2027-07-01 PESO @@ -124,7 +125,7 @@ - GW4pWcB8xBGrMF/XRtEJNqF8RcEVrn0ASbvWmDZUNajnCn9otIqzDVlS6eD7xzfSPCZCqMdpePYm02Igtybh6w== + /NREZsygXpAhAclOfhjNpF1lsdT6029lLYC5fhThcgbFdrxULgxBf4GNqZtclOv3djrAqLrLyeLhyyTnoyo71g== @@ -135,7 +136,7 @@ 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