From 4cef11ce6cb060f5b647317c403630bdec51e091 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 19 Sep 2023 11:23:33 +0000 Subject: [PATCH] Removing total calculator option, increasing accuracy in line calculations --- bill/invoice.go | 3 - bill/invoice_test.go | 61 +- bill/line.go | 25 +- bill/payment.go | 1 + bill/payment_test.go | 2 + bill/tax.go | 23 - bill/tax_test.go | 11 - pay/terms.go | 2 +- regimes/co/co.go | 1 + regimes/co/examples/out/simplified.json | 85 ++ regimes/co/examples/simplified.json | 41 + regimes/co/invoices.go | 42 +- regimes/es/invoices.go | 1 + tax/set.go | 5 - tax/totals_calculator.go | 150 +-- tax/totals_calculator_line_test.go | 1059 ----------------- ...otal_test.go => totals_calculator_test.go} | 13 +- 17 files changed, 257 insertions(+), 1268 deletions(-) create mode 100644 regimes/co/examples/out/simplified.json create mode 100644 regimes/co/examples/simplified.json delete mode 100644 tax/totals_calculator_line_test.go rename tax/{totals_calculator_total_test.go => totals_calculator_test.go} (99%) diff --git a/bill/invoice.go b/bill/invoice.go index e79a4456..5fae9c4e 100644 --- a/bill/invoice.go +++ b/bill/invoice.go @@ -420,9 +420,6 @@ func (inv *Invoice) calculate(r *tax.Regime, tID *tax.Identity) error { Lines: tls, Includes: pit, } - if inv.Tax != nil { - tc.Calculator = inv.Tax.Calculator - } if err := tc.Calculate(t.Taxes); err != nil { return err } diff --git a/bill/invoice_test.go b/bill/invoice_test.go index f7b94d70..e0872e3d 100644 --- a/bill/invoice_test.go +++ b/bill/invoice_test.go @@ -165,7 +165,6 @@ func TestRemoveIncludedTax2(t *testing.T) { Code: "123TEST", Tax: &bill.Tax{ PricesInclude: common.TaxCategoryVAT, - Calculator: tax.TotalCalculatorLine, }, Supplier: &org.Party{ TaxID: &tax.Identity{ @@ -230,7 +229,6 @@ func TestRemoveIncludedTax3(t *testing.T) { Code: "123TEST", Tax: &bill.Tax{ PricesInclude: common.TaxCategoryVAT, - Calculator: tax.TotalCalculatorLine, }, Supplier: &org.Party{ TaxID: &tax.Identity{ @@ -496,7 +494,6 @@ func TestRemoveIncludedTaxDeep(t *testing.T) { Code: "123TEST", Tax: &bill.Tax{ PricesInclude: common.TaxCategoryVAT, - Calculator: tax.TotalCalculatorLine, }, Supplier: &org.Party{ TaxID: &tax.Identity{ @@ -628,6 +625,63 @@ func TestRemoveIncludedTaxDeep2(t *testing.T) { assert.Equal(t, i.Totals.Payable.String(), i2.Totals.Payable.String()) } +func TestCalculateTotalsWithFractions(t *testing.T) { + i := &bill.Invoice{ + Code: "123TEST", + Supplier: &org.Party{ + TaxID: &tax.Identity{ + Country: l10n.ES, + Code: "B98602642", + }, + }, + Customer: &org.Party{ + TaxID: &tax.Identity{ + Country: l10n.ES, + Code: "54387763P", + }, + }, + IssueDate: cal.MakeDate(2022, 6, 13), + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(2010, 2), + Item: &org.Item{ + Name: "Test Item", + Price: num.MakeAmount(305, 2), + }, + Taxes: tax.Set{ + { + Category: "VAT", + Percent: num.NewPercentage(230, 3), + }, + }, + }, + { + Quantity: num.MakeAmount(2010, 2), + Item: &org.Item{ + Name: "Test Item 2", + Price: num.MakeAmount(305, 2), + }, + Taxes: tax.Set{ + { + Category: "VAT", + Percent: num.NewPercentage(230, 3), + }, + }, + }, + }, + } + + require.NoError(t, i.Calculate()) + + //data, _ := json.MarshalIndent(i, "", " ") + //t.Log(string(data)) + + l0 := i.Lines[0] + assert.Equal(t, "3.05", l0.Item.Price.String()) + assert.Equal(t, "61.31", l0.Sum.String()) + assert.Equal(t, "122.61", i.Totals.Total.String()) +} + func TestCalculate(t *testing.T) { i := &bill.Invoice{ Code: "123TEST", @@ -753,7 +807,6 @@ func baseInvoice(t *testing.T, lines ...*bill.Line) *bill.Invoice { Code: "123TEST", Tax: &bill.Tax{ PricesInclude: common.TaxCategoryVAT, - Calculator: tax.TotalCalculatorLine, }, Supplier: &org.Party{ TaxID: &tax.Identity{ diff --git a/bill/line.go b/bill/line.go index 34aac0c0..6249041b 100644 --- a/bill/line.go +++ b/bill/line.go @@ -35,6 +35,9 @@ type Line struct { // Set of specific notes for this line that may be required for // clarification. Notes []*cbc.Note `json:"notes,omitempty" jsonschema:"title=Notes"` + + // internal amount provided with greater precision + total num.Amount } // GetTaxes responds with the array of tax rates applied to this line. @@ -44,7 +47,7 @@ func (l *Line) GetTaxes() tax.Set { // GetTotal provides the final total for this line, excluding any tax calculations. func (l *Line) GetTotal() num.Amount { - return l.Total + return l.total } // ValidateWithContext ensures the line contains everything required using @@ -79,17 +82,19 @@ func (l *Line) calculate(r *tax.Regime, zero num.Amount) error { // Ensure the Price precision is set correctly according to the currency l.Item.Price = l.Item.Price.MatchPrecision(zero) + price := l.Item.Price.RescaleUp(zero.Exp() + 2) // Calculate the line sum and total - l.Sum = l.Item.Price.Multiply(l.Quantity) - l.Total = l.Sum + l.Sum = price.Multiply(l.Quantity) + l.total = l.Sum for _, d := range l.Discounts { if d.Percent != nil && !d.Percent.IsZero() { d.Amount = d.Percent.Of(l.Sum) // always override } d.Amount = d.Amount.MatchPrecision(zero) - l.Total = l.Total.Subtract(d.Amount) + l.total = l.total.Subtract(d.Amount) + d.Amount = d.Amount.Rescale(l.Item.Price.Exp()) } for _, c := range l.Charges { @@ -97,8 +102,14 @@ func (l *Line) calculate(r *tax.Regime, zero num.Amount) error { c.Amount = c.Percent.Of(l.Sum) // always override } c.Amount = c.Amount.MatchPrecision(zero) - l.Total = l.Total.Add(c.Amount) + l.total = l.total.Add(c.Amount) + c.Amount = c.Amount.Rescale(l.Item.Price.Exp()) } + + // Rescale the final sum and total + l.Sum = l.Sum.Rescale(l.Item.Price.Exp()) + l.Total = l.total.Rescale(l.Item.Price.Exp()) + return nil } @@ -152,8 +163,8 @@ func calculateLines(r *tax.Regime, zero num.Amount, lines []*Line) error { func calculateLineSum(zero num.Amount, lines []*Line) num.Amount { sum := zero for _, l := range lines { - sum = sum.MatchPrecision(l.Total) - sum = sum.Add(l.Total) + sum = sum.MatchPrecision(l.total) + sum = sum.Add(l.total) } return sum } diff --git a/bill/payment.go b/bill/payment.go index 35d6d733..abbbd771 100644 --- a/bill/payment.go +++ b/bill/payment.go @@ -54,6 +54,7 @@ func (p *Payment) totalAdvance(zero num.Amount) *num.Amount { sum := zero for _, a := range p.Advances { sum = sum.Add(a.Amount) + a.Amount = a.Amount.Rescale(zero.Exp()) } return &sum } diff --git a/bill/payment_test.go b/bill/payment_test.go index 6999b3d0..61d340cc 100644 --- a/bill/payment_test.go +++ b/bill/payment_test.go @@ -33,6 +33,8 @@ func TestPaymentCalculations(t *testing.T) { assert.Equal(t, "10", p.Advances[0].Amount.String()) p.calculateAdvances(zero, total) assert.Equal(t, "10.00", p.Advances[0].Amount.String()) + ta := p.totalAdvance(zero) + assert.Equal(t, "10.00", ta.String()) p = &Payment{ Advances: []*pay.Advance{ diff --git a/bill/tax.go b/bill/tax.go index b6bcbecf..7f6cc245 100644 --- a/bill/tax.go +++ b/bill/tax.go @@ -5,9 +5,7 @@ import ( "errors" "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/tax" - "github.com/invopop/jsonschema" "github.com/invopop/validation" ) @@ -25,10 +23,6 @@ type Tax struct { // Special tax tags that apply to this invoice according to local requirements. Tags []cbc.Key `json:"tags,omitempty" jsonschema:"title=Tags"` - // Calculator defines the rule to use when calculating the taxes. - // Currently supported options: `line`, or `total` (default). - Calculator cbc.Key `json:"calculator,omitempty" jsonschema:"title=Calculator"` - // Any additional data that may be required for processing, but should never // be relied upon by recipients. Meta cbc.Meta `json:"meta,omitempty" jsonschema:"title=Meta"` @@ -51,23 +45,6 @@ func (t *Tax) ValidateWithContext(ctx context.Context) error { return validation.ValidateStructWithContext(ctx, t, validation.Field(&t.PricesInclude), validation.Field(&t.Tags, validation.Each(r.InTags())), - validation.Field(&t.Calculator, tax.InKeyDefs(tax.TotalCalculatorDefs)), validation.Field(&t.Meta), ) } - -// JSONSchemaExtend extends the schema with additional property details -func (Tax) JSONSchemaExtend(schema *jsonschema.Schema) { - props := schema.Properties - if val, ok := props.Get("calculator"); ok { - its := val.(*jsonschema.Schema) - its.OneOf = make([]*jsonschema.Schema, len(tax.TotalCalculatorDefs)) - for i, v := range tax.TotalCalculatorDefs { - its.OneOf[i] = &jsonschema.Schema{ - Const: v.Key.String(), - Title: v.Name.String(i18n.EN), - Description: v.Desc.String(i18n.EN), - } - } - } -} diff --git a/bill/tax_test.go b/bill/tax_test.go index 50459e0e..c635743e 100644 --- a/bill/tax_test.go +++ b/bill/tax_test.go @@ -25,15 +25,4 @@ func TestTaxValidation(t *testing.T) { err = tx.ValidateWithContext(ctx) require.Error(t, err) assert.Contains(t, err.Error(), "must be a valid value") - - tx = &bill.Tax{ - Calculator: "line", - } - err = tx.ValidateWithContext(ctx) - require.NoError(t, err) - - tx.Calculator = "invalid" - err = tx.ValidateWithContext(ctx) - require.Error(t, err) - assert.Contains(t, err.Error(), "calculator: must be a valid value") } diff --git a/pay/terms.go b/pay/terms.go index 87f38a56..cc0d0d3f 100644 --- a/pay/terms.go +++ b/pay/terms.go @@ -105,7 +105,7 @@ func (t *Terms) CalculateDues(zero num.Amount, sum num.Amount) { if dd.Percent != nil && !dd.Percent.IsZero() { dd.Amount = dd.Percent.Of(sum) } - dd.Amount = dd.Amount.MatchPrecision(zero) + dd.Amount = dd.Amount.Rescale(zero.Exp()) } } diff --git a/regimes/co/co.go b/regimes/co/co.go index 88dc2d7e..979251ad 100644 --- a/regimes/co/co.go +++ b/regimes/co/co.go @@ -44,6 +44,7 @@ func New() *tax.Regime { `), }, TimeZone: "America/Bogota", + Tags: invoiceTags, Validator: Validate, Calculator: Calculate, IdentityTypeKeys: taxIdentityTypeDefs, // see tax_identity.go diff --git a/regimes/co/examples/out/simplified.json b/regimes/co/examples/out/simplified.json new file mode 100644 index 00000000..1c4273d6 --- /dev/null +++ b/regimes/co/examples/out/simplified.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "2346991aab5ee704bd2b0ad14a06260aaf81fa7baca32c2bcd211cbc177aa6a2" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", + "series": "SETT", + "code": "1234", + "issue_date": "2021-01-01", + "currency": "COP", + "tax": { + "tags": [ + "simplified" + ] + }, + "supplier": { + "name": "EXAMPLE SUPPLIER S.A.S.", + "tax_id": { + "country": "CO", + "zone": "11001", + "type": "tin", + "code": "9014514812" + } + }, + "lines": [ + { + "i": 1, + "quantity": "1", + "item": { + "name": "Useful service", + "price": "200000.00" + }, + "sum": "200000.00", + "taxes": [ + { + "cat": "VAT", + "percent": "19%" + } + ], + "total": "200000.00" + } + ], + "payment": { + "advances": [ + { + "desc": "Prepaid", + "percent": "100%", + "amount": "238000.00" + } + ] + }, + "totals": { + "sum": "200000.00", + "total": "200000.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "base": "200000.00", + "percent": "19%", + "amount": "38000.00" + } + ], + "amount": "38000.00" + } + ], + "sum": "38000.00" + }, + "tax": "38000.00", + "total_with_tax": "238000.00", + "payable": "238000.00", + "advance": "238000.00", + "due": "0.00" + } + } +} \ No newline at end of file diff --git a/regimes/co/examples/simplified.json b/regimes/co/examples/simplified.json new file mode 100644 index 00000000..d06623e6 --- /dev/null +++ b/regimes/co/examples/simplified.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://gobl.org/draft-0/bill/invoice", + "code": "1234", + "series": "SETT", + "currency": "COP", + "issue_date": "2021-01-01", + "tax": { + "tags": ["simplified"] + }, + "supplier": { + "tax_id": { + "country": "CO", + "code": "9014514812", + "zone": "11001" + }, + "name": "EXAMPLE SUPPLIER S.A.S." + }, + "lines": [ + { + "quantity": "1", + "item": { + "name": "Useful service", + "price": "200000.00" + }, + "taxes": [ + { + "cat": "VAT", + "percent": "19%" + } + ] + } + ], + "payment": { + "advances": [ + { + "desc": "Prepaid", + "percent": "100%" + } + ] + } +} diff --git a/regimes/co/invoices.go b/regimes/co/invoices.go index 12f584ed..92c294dd 100644 --- a/regimes/co/invoices.go +++ b/regimes/co/invoices.go @@ -3,12 +3,30 @@ package co import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/currency" + "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" "github.com/invopop/validation" ) +var invoiceTags = []*tax.KeyDefinition{ + // Simplified invoices when a client ID is not available. For conversion to local formats, + // the correct client ID data will need to be provided automatically. + { + Key: common.TagSimplified, + Name: i18n.String{ + i18n.EN: "Simplified Invoice", + i18n.ES: "Factura Simplificada", + }, + Desc: i18n.String{ + i18n.EN: "Used for B2C transactions when the client details are not available, check with local authorities for limits.", + i18n.ES: "Usado para transacciones B2C cuando los detalles del cliente no están disponibles, consulte con las autoridades locales para los límites.", + }, + }, +} + type invoiceValidator struct { inv *bill.Invoice } @@ -23,7 +41,11 @@ func (v *invoiceValidator) validate() error { return validation.ValidateStruct(inv, validation.Field(&inv.Currency, validation.In(currency.COP)), validation.Field(&inv.Type, - validation.In(bill.InvoiceTypeStandard, bill.InvoiceTypeCreditNote), + validation.In( + bill.InvoiceTypeStandard, + bill.InvoiceTypeCreditNote, + bill.InvoiceTypeProforma, + ), ), validation.Field(&inv.Supplier, validation.Required, @@ -31,14 +53,18 @@ func (v *invoiceValidator) validate() error { validation.By(v.validSupplier), ), validation.Field(&inv.Customer, - validation.Required, + validation.When( + !inv.Tax.ContainsTag(common.TagSimplified), + validation.Required, + ), validation.By(v.validParty), ), - validation.Field(&inv.Preceding, validation.When( - inv.Type.In(bill.InvoiceTypeCreditNote), - validation.Required, - validation.Each(validation.By(v.preceding)), - )), + validation.Field(&inv.Preceding, + validation.When( + inv.Type.In(bill.InvoiceTypeCreditNote), + validation.Required, + ), + validation.Each(validation.By(v.preceding))), validation.Field(&inv.Outlays, validation.Empty), ) } @@ -96,7 +122,7 @@ func (v *invoiceValidator) validTaxIdentity(value interface{}) error { func (v *invoiceValidator) preceding(value interface{}) error { obj, ok := value.(*bill.Preceding) - if !ok { + if !ok || obj == nil { return nil } return validation.ValidateStruct(obj, diff --git a/regimes/es/invoices.go b/regimes/es/invoices.go index 7206c185..07b3522c 100644 --- a/regimes/es/invoices.go +++ b/regimes/es/invoices.go @@ -30,6 +30,7 @@ func (v *invoiceValidator) validate() error { validation.Field(&inv.Type, validation.In( bill.InvoiceTypeStandard, bill.InvoiceTypeCorrective, + bill.InvoiceTypeProforma, )), validation.Field(&inv.Preceding, validation.Each(validation.By(v.preceding)), diff --git a/tax/set.go b/tax/set.go index ea90f24b..8b077428 100644 --- a/tax/set.go +++ b/tax/set.go @@ -33,11 +33,6 @@ type Combo struct { // Internal link back to the category object category *Category - - // base and amount used for line-by-line calculations - base num.Amount - amount num.Amount - surcharge *num.Amount } // ValidateWithContext ensures the Combo has the correct details. diff --git a/tax/totals_calculator.go b/tax/totals_calculator.go index 59eef9b1..a3ec7a24 100644 --- a/tax/totals_calculator.go +++ b/tax/totals_calculator.go @@ -1,53 +1,21 @@ package tax import ( - "errors" - "github.com/invopop/gobl/cal" "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/num" ) -// Tax total calculator options -const ( - TotalCalculatorTotal cbc.Key = "total" // default - TotalCalculatorLine cbc.Key = "line" -) - -// TotalCalculatorDefs to use in the schema -var TotalCalculatorDefs = []*KeyDefinition{ - { - Key: TotalCalculatorTotal, - Name: i18n.String{ - i18n.EN: "Total", - }, - Desc: i18n.String{ - i18n.EN: "Calculate the taxes based on the sum of all the line items (default).", - }, - }, - { - Key: TotalCalculatorLine, - Name: i18n.String{ - i18n.EN: "Line", - }, - Desc: i18n.String{ - i18n.EN: "Calculate the taxes based on each line item.", - }, - }, -} - // TotalCalculator defines the base structure with the available // data for calculating tax totals. type TotalCalculator struct { - Regime *Regime - Zone l10n.Code - Zero num.Amount - Date cal.Date - Lines []TaxableLine - Includes cbc.Code // Tax included in price - Calculator cbc.Key // Calculation model to use + Regime *Regime + Zone l10n.Code + Zero num.Amount + Date cal.Date + Lines []TaxableLine + Includes cbc.Code // Tax included in price } // TaxableLine defines what we expect from a line in order to subsequently calculate @@ -62,9 +30,6 @@ func (tc *TotalCalculator) Calculate(t *Total) error { if tc.Regime == nil { return ErrMissingRegime } - if tc.Calculator == cbc.KeyEmpty { - tc.Calculator = TotalCalculatorTotal - } // reset t.Categories = make([]*CategoryTotal, 0) @@ -77,28 +42,12 @@ func (tc *TotalCalculator) Calculate(t *Total) error { return err } - // Pre-Process each line for tax calculations - var err error - switch tc.Calculator { - case TotalCalculatorLine: - err = tc.calculateLineTaxes(taxLines) - case TotalCalculatorTotal: - err = tc.removeIncludedTaxes(taxLines) - default: - err = errors.New("unknown tax calculator type") - } - if err != nil { + // Remove included taxes + if err := tc.removeIncludedTaxes(taxLines); err != nil { return err } - // Go through each line to calculate rate totals - switch tc.Calculator { - case TotalCalculatorLine: - tc.calculateLineRateTotals(taxLines, t) - case TotalCalculatorTotal: - tc.calculateBaseRateTotals(taxLines, t) - } - + tc.calculateBaseRateTotals(taxLines, t) tc.calculateFinalSum(t) tc.round(t) @@ -119,34 +68,6 @@ func (tc *TotalCalculator) prepareLines(taxLines []*taxLine) error { return nil } -func (tc *TotalCalculator) calculateLineTaxes(taxLines []*taxLine) error { - // Go through each line, and figure out the totals for each tax combo - for _, tl := range taxLines { - // prepare included taxes first so we can update the total - if tc.Includes != cbc.CodeEmpty { - if combo := tl.taxes.Get(tc.Includes); combo != nil && combo.Percent != nil { - if combo.category.Retained { - return ErrInvalidPricesInclude.WithMessage("cannot include retained category '%s'", tc.Includes.String()) - } - tl.total = tl.total.Remove(*combo.Percent) - } - } - - // Make calculations - for _, c := range tl.taxes { - c.base = tl.total - if c.Percent != nil { - c.amount = c.Percent.Of(c.base) - } - if c.Surcharge != nil { - sc := c.Surcharge.Of(tl.total) - c.surcharge = &sc - } - } - } - return nil -} - func (tc *TotalCalculator) removeIncludedTaxes(taxLines []*taxLine) error { // If prices include a tax, perform a pre-loop to update all the line prices with // the price minus the defined tax. @@ -168,30 +89,6 @@ func (tc *TotalCalculator) removeIncludedTaxes(taxLines []*taxLine) error { return nil } -// calculateLineRateTotals goes through each line to sum the rate totals. -// This is when the rounding method starts to become important. If we're doing -// post rounding, then then accuracy will be maintained, otherwise each step -// will perform rounding. -func (tc *TotalCalculator) calculateLineRateTotals(taxLines []*taxLine, t *Total) { - for _, tl := range taxLines { - for _, combo := range tl.taxes { - rt := t.rateTotalFor(combo, tc.Zero) - rt.Base = rt.Base.MatchPrecision(combo.base) - rt.Base = rt.Base.Add(combo.base) - if combo.Percent == nil && combo.Rate.IsEmpty() { - continue // not much to do here! - } - - rt.Amount = rt.Amount.MatchPrecision(combo.amount) - rt.Amount = rt.Amount.Add(combo.amount) - if combo.surcharge != nil { - rt.Surcharge.Amount = rt.Surcharge.Amount.MatchPrecision(*combo.surcharge) - rt.Surcharge.Amount = rt.Surcharge.Amount.Add(*combo.surcharge) - } - } - } -} - func (tc *TotalCalculator) calculateBaseRateTotals(taxLines []*taxLine, t *Total) { // Go through each line and add the total to the base of each tax for _, tl := range taxLines { @@ -210,11 +107,7 @@ func (tc *TotalCalculator) calculateFinalSum(t *Total) { // Now go through each category to apply the percentage and calculate the final sums t.Sum = tc.Zero for _, ct := range t.Categories { - if tc.Calculator == TotalCalculatorLine { - tc.calculateLineCategoryTotal(ct) - } else { - tc.calculateBaseCategoryTotal(ct) - } + tc.calculateBaseCategoryTotal(ct) t.Sum = t.Sum.MatchPrecision(ct.Amount) if ct.Retained { @@ -231,29 +124,6 @@ func (tc *TotalCalculator) calculateFinalSum(t *Total) { } } -func (tc *TotalCalculator) calculateLineCategoryTotal(ct *CategoryTotal) { - zero := tc.Zero - ct.Amount = zero - for _, rt := range ct.Rates { - if rt.Percent == nil { - rt.Amount = zero - continue // exempt, nothing else to do - } - ct.Amount = ct.Amount.MatchPrecision(rt.Amount) - ct.Amount = ct.Amount.Add(rt.Amount) - if rt.Surcharge != nil { - if ct.Surcharge == nil { - ct.Surcharge = &zero - } - a := rt.Surcharge.Amount - x := *ct.Surcharge - x = x.MatchPrecision(a) - x = x.Add(a) - ct.Surcharge = &x - } - } -} - func (tc *TotalCalculator) calculateBaseCategoryTotal(ct *CategoryTotal) { zero := tc.Zero ct.Amount = zero diff --git a/tax/totals_calculator_line_test.go b/tax/totals_calculator_line_test.go deleted file mode 100644 index 4c872330..00000000 --- a/tax/totals_calculator_line_test.go +++ /dev/null @@ -1,1059 +0,0 @@ -package tax_test - -import ( - "encoding/json" - "testing" - - "github.com/invopop/gobl/cal" - "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/l10n" - "github.com/invopop/gobl/num" - "github.com/invopop/gobl/regimes/common" - "github.com/invopop/gobl/regimes/es" - "github.com/invopop/gobl/regimes/it" - "github.com/invopop/gobl/regimes/pt" - "github.com/invopop/gobl/tax" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTotalByLineCalculate(t *testing.T) { - spain := es.New() - portugal := pt.New() - italy := it.New() - date := cal.MakeDate(2022, 01, 24) - zero := num.MakeAmount(0, 2) - var tests = []struct { - desc string - regime *tax.Regime // default, spain - zone l10n.Code // default empty - lines []tax.TaxableLine - date *cal.Date - taxIncluded cbc.Code - want *tax.Total - err error - errContent string - }{ - { - desc: "basic no tax", - lines: []tax.TaxableLine{ - &taxableLine{taxes: nil, amount: num.MakeAmount(10000, 2)}, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{}, - Sum: zero, - }, - }, - { - desc: "with VAT", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(2100, 2), - }, - }, - Amount: num.MakeAmount(2100, 2), - }, - }, - Sum: num.MakeAmount(2100, 2), - }, - }, - { - desc: "with exemption", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateExempt, - Ext: cbc.CodeMap{ - es.ExtKeyTBAIExemption: "E1", - }, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateExempt, - Ext: cbc.CodeMap{ - es.ExtKeyTBAIExemption: "E1", - }, - Base: num.MakeAmount(10000, 2), - Percent: nil, - Amount: num.MakeAmount(0, 2), - }, - }, - Amount: num.MakeAmount(0, 2), - }, - }, - Sum: num.MakeAmount(0, 2), - }, - }, - { - desc: "with exemption and empty ext", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateExempt, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateReduced, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateExempt, - Base: num.MakeAmount(10000, 2), - Percent: nil, - Amount: num.MakeAmount(0, 2), - }, - { - Key: common.TaxRateReduced, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(100, 3), - Amount: num.MakeAmount(1000, 2), - }, - }, - Amount: num.MakeAmount(1000, 2), - }, - }, - Sum: num.MakeAmount(1000, 2), - }, - }, - { - desc: "with no percents and matching rate keys", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateExempt, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateExempt, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateExempt, - Base: num.MakeAmount(20000, 2), - Percent: nil, - Amount: num.MakeAmount(0, 2), - }, - }, - Amount: num.MakeAmount(0, 2), - }, - }, - Sum: num.MakeAmount(0, 2), - }, - }, - { - desc: "with VAT in Azores", - regime: portugal, - zone: pt.ZoneAzores, - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(160, 3), - Amount: num.MakeAmount(1600, 2), - }, - }, - Amount: num.MakeAmount(1600, 2), - }, - }, - Sum: num.MakeAmount(1600, 2), - }, - }, - { - desc: "with VAT percents defined", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Percent: num.NewPercentage(210, 3), - }, - }, - amount: num.MakeAmount(100000, 3), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - // Key: common.TaxRateStandard, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(2100, 2), - }, - }, - Amount: num.MakeAmount(2100, 2), - }, - }, - Sum: num.MakeAmount(2100, 2), - }, - }, - - { - desc: "with VAT percents defined, rate override", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - Percent: num.NewPercentage(20, 2), - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(2100, 2), - }, - }, - Amount: num.MakeAmount(2100, 2), - }, - }, - Sum: num.MakeAmount(2100, 2), - }, - }, - { - desc: "with multiline VAT", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - }, - }, - amount: num.MakeAmount(15000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(25000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(5250, 2), - }, - }, - Amount: num.MakeAmount(5250, 2), - }, - }, - Sum: num.MakeAmount(5250, 2), - }, - }, - { - desc: "with multiline VAT and Surcharge", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard.With(es.TaxRateEquivalence), - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard.With(es.TaxRateEquivalence), - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - }, - }, - amount: num.MakeAmount(15000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard.With(es.TaxRateEquivalence), - Base: num.MakeAmount(20000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(4200, 2), - Surcharge: &tax.RateTotalSurcharge{ - Percent: num.MakePercentage(52, 3), - Amount: num.MakeAmount(1040, 2), - }, - }, - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(15000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(3150, 2), - }, - }, - Amount: num.MakeAmount(7350, 2), - Surcharge: num.NewAmount(1040, 2), - }, - }, - Sum: num.MakeAmount(8390, 2), - }, - }, - { - desc: "with multiline VAT as percentages", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Percent: num.NewPercentage(210, 3), - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Percent: num.NewPercentage(2100, 4), // different exp. - }, - }, - amount: num.MakeAmount(15000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Base: num.MakeAmount(25000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(5250, 2), - }, - }, - Amount: num.MakeAmount(5250, 2), - }, - }, - Sum: num.MakeAmount(5250, 2), - }, - }, - { - desc: "with multirate VAT", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateReduced, - }, - }, - amount: num.MakeAmount(15000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(2100, 2), - }, - { - Key: common.TaxRateReduced, - Base: num.MakeAmount(15000, 2), - Percent: num.NewPercentage(100, 3), - Amount: num.MakeAmount(1500, 2), - }, - }, - Amount: num.MakeAmount(3600, 2), - }, - }, - Sum: num.MakeAmount(3600, 2), - }, - }, - { - desc: "with multirate VAT as percentages", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Percent: num.NewPercentage(210, 3), - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Percent: num.NewPercentage(100, 3), - }, - }, - amount: num.MakeAmount(15000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - // Key: common.TaxRateStandard, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(2100, 2), - }, - { - // Key: common.TaxRateReduced, - Base: num.MakeAmount(15000, 2), - Percent: num.NewPercentage(100, 3), - Amount: num.MakeAmount(1500, 2), - }, - }, - Amount: num.MakeAmount(3600, 2), - }, - }, - Sum: num.MakeAmount(3600, 2), - }, - }, - { - desc: "with multirate VAT included in price", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateReduced, - }, - }, - amount: num.MakeAmount(15000, 2), - }, - }, - taxIncluded: common.TaxCategoryVAT, - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(8264, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(1736, 2), - }, - { - Key: common.TaxRateReduced, - Base: num.MakeAmount(13636, 2), - Percent: num.NewPercentage(100, 3), - Amount: num.MakeAmount(1364, 2), - }, - }, - Amount: num.MakeAmount(3099, 2), - }, - }, - Sum: num.MakeAmount(3099, 2), - }, - }, - { - desc: "with multirate VAT as percentages, and included in price", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Percent: num.NewPercentage(21, 2), - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Percent: num.NewPercentage(10, 2), - }, - }, - amount: num.MakeAmount(15000, 2), - }, - }, - taxIncluded: common.TaxCategoryVAT, - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Base: num.MakeAmount(8264, 2), - Percent: num.NewPercentage(21, 2), - Amount: num.MakeAmount(1736, 2), - }, - { - Base: num.MakeAmount(13636, 2), - Percent: num.NewPercentage(10, 2), - Amount: num.MakeAmount(1364, 2), - }, - }, - Amount: num.MakeAmount(3099, 2), - }, - }, - Sum: num.MakeAmount(3099, 2), - }, - }, - { - desc: "with multirate VAT and retained tax", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - }, - { - Category: es.TaxCategoryIRPF, - Rate: es.TaxRatePro, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateReduced, - }, - }, - amount: num.MakeAmount(15000, 2), - }, - }, - taxIncluded: "", - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(2100, 2), - }, - { - Key: common.TaxRateReduced, - Base: num.MakeAmount(15000, 2), - Percent: num.NewPercentage(100, 3), - Amount: num.MakeAmount(1500, 2), - }, - }, - Amount: num.MakeAmount(3600, 2), - }, - { - Code: es.TaxCategoryIRPF, - Retained: true, - Rates: []*tax.RateTotal{ - { - Key: es.TaxRatePro, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(150, 3), - Amount: num.MakeAmount(1500, 2), - }, - }, - Amount: num.MakeAmount(1500, 2), - }, - }, - Sum: num.MakeAmount(2100, 2), - }, - }, - - { - desc: "with multirate VAT included in price plus retained tax", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - }, - { - Category: es.TaxCategoryIRPF, - Rate: es.TaxRatePro, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateReduced, - }, - }, - amount: num.MakeAmount(15000, 2), - }, - }, - taxIncluded: common.TaxCategoryVAT, - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Retained: false, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(8264, 2), - Percent: num.NewPercentage(210, 3), - Amount: num.MakeAmount(1736, 2), - }, - { - Key: common.TaxRateReduced, - Base: num.MakeAmount(13636, 2), - Percent: num.NewPercentage(100, 3), - Amount: num.MakeAmount(1364, 2), - }, - }, - Amount: num.MakeAmount(3099, 2), - }, - { - Code: es.TaxCategoryIRPF, - Retained: true, - Rates: []*tax.RateTotal{ - { - Key: es.TaxRatePro, - Base: num.MakeAmount(8264, 2), - Percent: num.NewPercentage(150, 3), - Amount: num.MakeAmount(1240, 2), - }, - }, - Amount: num.MakeAmount(1240, 2), - }, - }, - Sum: num.MakeAmount(1860, 2), - }, - }, - { - desc: "with invalid category", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: cbc.Code("FOO"), - Rate: common.TaxRateStandard, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - err: tax.ErrInvalidCategory, - errContent: "invalid-category: 'FOO'", - }, - { - desc: "with invalid rate", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: es.TaxCategoryIRPF, - Rate: common.TaxRateStandard, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - err: tax.ErrInvalidRate, - errContent: "invalid-rate: 'standard' rate not defined in category 'IRPF'", - }, - - { - desc: "with invalid rate on date", - date: cal.NewDate(2005, 1, 1), - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: es.TaxCategoryIRPF, - Rate: es.TaxRatePro, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - err: tax.ErrInvalidDate, - errContent: "invalid-date: rate value unavailable for 'pro' in 'IRPF' on '2005-01-01'", - }, - { - desc: "with invalid tax included", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: es.TaxCategoryIRPF, - Rate: es.TaxRatePro, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - taxIncluded: es.TaxCategoryIRPF, - err: tax.ErrInvalidPricesInclude, - errContent: "cannot include retained", - }, - { - desc: "tax included with exempt rate", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateExempt, - Ext: cbc.CodeMap{ - es.ExtKeyTBAIExemption: "E1", - }, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - taxIncluded: common.TaxCategoryVAT, - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateExempt, - Ext: cbc.CodeMap{ - es.ExtKeyTBAIExemption: "E1", - }, - Base: num.MakeAmount(10000, 2), - Amount: num.MakeAmount(0, 2), - }, - }, - Amount: num.MakeAmount(0, 2), - }, - }, - Sum: num.MakeAmount(0, 2), - }, - }, - { - desc: "tax included with regular and exempt rate", - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Percent: num.NewPercentage(21, 2), - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateExempt, - Ext: cbc.CodeMap{ - es.ExtKeyTBAIExemption: "E2", - }, - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - taxIncluded: common.TaxCategoryVAT, - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Rates: []*tax.RateTotal{ - { - Base: num.MakeAmount(8264, 2), - Percent: num.NewPercentage(21, 2), - Amount: num.MakeAmount(1736, 2), - }, - { - Key: common.TaxRateExempt, - Ext: cbc.CodeMap{ - es.ExtKeyTBAIExemption: "E2", - }, - Base: num.MakeAmount(10000, 2), - Amount: num.MakeAmount(0, 2), - }, - }, - Amount: num.MakeAmount(1736, 2), - }, - }, - Sum: num.MakeAmount(1736, 2), - }, - }, - { - desc: "multiple different retained rates", - regime: italy, - lines: []tax.TaxableLine{ - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Rate: common.TaxRateStandard, - Percent: num.NewPercentage(22, 2), - }, - { - Category: it.TaxCategoryIRPEF, - Ext: cbc.CodeMap{ - it.ExtKeySDIRetainedTax: "A", - }, - Percent: num.NewPercentage(20, 2), - }, - }, - amount: num.MakeAmount(10000, 2), - }, - &taxableLine{ - taxes: tax.Set{ - { - Category: common.TaxCategoryVAT, - Percent: num.NewPercentage(22, 2), - }, - { - Category: it.TaxCategoryIRPEF, - Ext: cbc.CodeMap{ - it.ExtKeySDIRetainedTax: "J", // truffles! - }, - Percent: num.NewPercentage(20, 2), - }, - }, - amount: num.MakeAmount(10000, 2), - }, - }, - want: &tax.Total{ - Categories: []*tax.CategoryTotal{ - { - Code: common.TaxCategoryVAT, - Rates: []*tax.RateTotal{ - { - Key: common.TaxRateStandard, - Base: num.MakeAmount(20000, 2), - Percent: num.NewPercentage(220, 3), - Amount: num.MakeAmount(4400, 2), - }, - }, - Amount: num.MakeAmount(4400, 2), - }, - { - Code: it.TaxCategoryIRPEF, - Retained: true, - Rates: []*tax.RateTotal{ - { - Ext: cbc.CodeMap{ - it.ExtKeySDIRetainedTax: "A", - }, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(20, 2), - Amount: num.MakeAmount(2000, 2), - }, - { - Ext: cbc.CodeMap{ - it.ExtKeySDIRetainedTax: "J", - }, - Base: num.MakeAmount(10000, 2), - Percent: num.NewPercentage(20, 2), - Amount: num.MakeAmount(2000, 2), - }, - }, - Amount: num.MakeAmount(4000, 2), - }, - }, - Sum: num.MakeAmount(400, 2), - }, - }, - } - - for _, test := range tests { - t.Run(test.desc, func(t *testing.T) { - d := date - if test.date != nil { - d = *test.date - } - reg := spain - if test.regime != nil { - reg = test.regime - } - zone := l10n.CodeEmpty - if test.zone != l10n.CodeEmpty { - zone = test.zone - } - tc := &tax.TotalCalculator{ - Regime: reg, - Zone: zone, - Zero: zero, - Date: d, - Lines: test.lines, - Includes: test.taxIncluded, - Calculator: tax.TotalCalculatorLine, - } - tot := new(tax.Total) - err := tc.Calculate(tot) - if test.err != nil && assert.Error(t, err) { - assert.ErrorIs(t, err, test.err) - } - if test.errContent != "" && assert.Error(t, err) { - assert.Contains(t, err.Error(), test.errContent) - } - if test.want != nil { - want, err := json.Marshal(test.want) - require.NoError(t, err) - got, err := json.Marshal(tot) - require.NoError(t, err) - if !assert.JSONEq(t, string(want), string(got)) { - data, _ := json.MarshalIndent(tot, "", " ") - t.Logf("data output: %v", string(data)) - } - } - }) - } - -} diff --git a/tax/totals_calculator_total_test.go b/tax/totals_calculator_test.go similarity index 99% rename from tax/totals_calculator_total_test.go rename to tax/totals_calculator_test.go index 7f0daef4..f9fc4fb9 100644 --- a/tax/totals_calculator_total_test.go +++ b/tax/totals_calculator_test.go @@ -1027,13 +1027,12 @@ func TestTotalBySumCalculate(t *testing.T) { zone = test.zone } tc := &tax.TotalCalculator{ - Regime: reg, - Zone: zone, - Zero: zero, - Date: d, - Lines: test.lines, - Includes: test.taxIncluded, - Calculator: tax.TotalCalculatorTotal, + Regime: reg, + Zone: zone, + Zero: zero, + Date: d, + Lines: test.lines, + Includes: test.taxIncluded, } tot := new(tax.Total) err := tc.Calculate(tot)