From 885e0768b061b1a0e2d92b2ee93b6a2bf80cde08 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 15 Nov 2024 14:21:34 +0000 Subject: [PATCH] Allow empty lines with discounts or charges, fix empty check --- CHANGELOG.md | 8 ++++++++ bill/invoice.go | 5 ++++- bill/invoice_scenarios.go | 8 +++++--- bill/invoice_scenarios_test.go | 30 ++++++++++++++++++++---------- bill/invoice_test.go | 20 ++++++++++++++++++++ 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0ab145..0c3b6348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `org`: `Address` includes `LineOne()`, `LineTwo()`, `CompleteNumber()` methods to help with conversion to other formats with some regional formatting. +### Changes + +- `bill`: `Invoice` can now have empty lines if discounts or charges present. + +### Fixes + +- `bill`: `Invoice` `GetExtensions` method now works correctly if missing totals [Issue #424](https://github.com/invopop/gobl/issues/424). + ## [v0.205.0] ### Added diff --git a/bill/invoice.go b/bill/invoice.go index 9a0bff9f..b52c1dc5 100644 --- a/bill/invoice.go +++ b/bill/invoice.go @@ -151,7 +151,10 @@ func (inv *Invoice) ValidateWithContext(ctx context.Context) error { validation.By(validateInvoiceCustomer), ), validation.Field(&inv.Lines, - validation.Required, + validation.When( + len(inv.Discounts) == 0 && len(inv.Charges) == 0, + validation.Required.Error("cannot be empty without discounts or charges"), + ), ), validation.Field(&inv.Discounts), validation.Field(&inv.Charges), diff --git a/bill/invoice_scenarios.go b/bill/invoice_scenarios.go index 32b1116c..9458d5d7 100644 --- a/bill/invoice_scenarios.go +++ b/bill/invoice_scenarios.go @@ -19,9 +19,11 @@ func (inv *Invoice) GetExtensions() []tax.Extensions { exts = append(exts, inv.Tax.Ext) } } - for _, cat := range inv.Totals.Taxes.Categories { - for _, rate := range cat.Rates { - exts = append(exts, rate.Ext) + if inv.Totals != nil && inv.Totals.Taxes != nil { + for _, cat := range inv.Totals.Taxes.Categories { + for _, rate := range cat.Rates { + exts = append(exts, rate.Ext) + } } } return exts diff --git a/bill/invoice_scenarios_test.go b/bill/invoice_scenarios_test.go index 9ead9192..13bef80d 100644 --- a/bill/invoice_scenarios_test.go +++ b/bill/invoice_scenarios_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/invopop/gobl/addons/it/sdi" + "github.com/invopop/gobl/addons/pt/saft" "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/tax" @@ -105,14 +106,23 @@ func TestScenarios(t *testing.T) { } func TestInvoiceGetExtensions(t *testing.T) { - inv := baseInvoiceWithLines(t) - inv.Addons = tax.WithAddons(sdi.V1) - inv.Supplier.TaxID = &tax.Identity{ - Country: "IT", - Code: "12345678903", - } - require.NoError(t, inv.Calculate()) - ext := inv.GetExtensions() - assert.Len(t, ext, 2) - assert.Equal(t, "FPR12", ext[0][sdi.ExtKeyFormat].String()) + t.Run("with lines", func(t *testing.T) { + inv := baseInvoiceWithLines(t) + inv.Addons = tax.WithAddons(sdi.V1) + inv.Supplier.TaxID = &tax.Identity{ + Country: "IT", + Code: "12345678903", + } + require.NoError(t, inv.Calculate()) + ext := inv.GetExtensions() + assert.Len(t, ext, 2) + assert.Equal(t, "FPR12", ext[0][sdi.ExtKeyFormat].String()) + }) + t.Run("missing lines", func(t *testing.T) { + inv := baseInvoice(t) + inv.Addons = tax.WithAddons(saft.V1) + require.NoError(t, inv.Calculate()) + ext := inv.GetExtensions() + assert.Len(t, ext, 1) + }) } diff --git a/bill/invoice_test.go b/bill/invoice_test.go index a963843d..dd0ff694 100644 --- a/bill/invoice_test.go +++ b/bill/invoice_test.go @@ -1135,6 +1135,26 @@ func TestValidation(t *testing.T) { err := inv.Validate() assert.ErrorContains(t, err, "customer: (name: cannot be blank.).") }) + + t.Run("missing lines", func(t *testing.T) { + inv := baseInvoice(t) + require.NoError(t, inv.Calculate()) + err := inv.Validate() + assert.ErrorContains(t, err, "lines: cannot be empty without discounts or charges.") + }) + + t.Run("missing lines with charge", func(t *testing.T) { + inv := baseInvoice(t) + inv.Charges = []*bill.Charge{ + { + Reason: "Testing", + Amount: num.MakeAmount(1000, 2), + }, + } + require.NoError(t, inv.Calculate()) + err := inv.Validate() + assert.NoError(t, err) + }) } func TestInvoiceTagsValidation(t *testing.T) {