Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

## [Unreleased]

### Fixed

- `bill`: removing taxes included in prices from a document using the `currency` rounding rule now switches it to `precise`, so that the resulting tax bases and amounts match those of the original document. Before, the tax-exclusive line totals were rounded to the currency's precision, which drifted from the original tax amounts by an amount that grew with the number of lines and left the difference in the document's `rounding` total.

## [v0.504.0]

### Added
Expand Down
11 changes: 11 additions & 0 deletions bill/calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,17 @@ func removeIncludedTaxes(doc billable) error {
tx := doc.getTax()
tx.PricesInclude = ""

// Tax-exclusive prices need more decimal places than the currency to
// represent the original amounts, so the currency rounding rule can no
// longer be applied without losing the tax totals.
rr := tx.Rounding
if rr == "" {
rr = doc.RegimeDef().GetRoundingRule()
}
if rr == tax.RoundingRuleCurrency {
tx.Rounding = tax.RoundingRulePrecise
}

if err := calculate(doc); err != nil {
return err
}
Expand Down
38 changes: 38 additions & 0 deletions bill/calculator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,44 @@ func TestRemoveIncludedTaxes(t *testing.T) {
assert.Equal(t, "1000.00", inv.Totals.Payable.String())
})

t.Run("with currency rounding rule", func(t *testing.T) {
lines := make([]*bill.Line, 12)
for i := range lines {
lines[i] = &bill.Line{
Quantity: num.MakeAmount(1, 0),
Item: &org.Item{
Name: "Room rate",
Price: num.NewAmount(12500, 2),
},
Taxes: tax.Set{
{
Category: tax.CategoryVAT,
Percent: num.NewPercentage(6, 2),
},
},
}
}
inv := baseInvoice(t, lines...)
inv.Tax.Rounding = tax.RoundingRuleCurrency
require.NoError(t, inv.Calculate())
require.NoError(t, inv.RemoveIncludedTaxes())

// The document can no longer use the currency's precision for the
// line prices, so it switches to the precise rounding rule in order
// to maintain the original tax amounts.
assert.Equal(t, tax.RoundingRulePrecise, inv.Tax.Rounding)
assert.Equal(t, "117.9245", inv.Lines[0].Item.Price.String())
assert.Equal(t, "1415.09", inv.Totals.Sum.String())
assert.Equal(t, "1415.09", inv.Totals.Total.String())
assert.Equal(t, "84.91", inv.Totals.Tax.String())
assert.Equal(t, "1500.00", inv.Totals.TotalWithTax.String())
assert.Equal(t, "1500.00", inv.Totals.Payable.String())
assert.Nil(t, inv.Totals.Rounding, "no rounding adjustment needed")
rt := inv.Totals.Taxes.Categories[0].Rates[0]
assert.Equal(t, "1415.09", rt.Base.String())
assert.Equal(t, "84.91", rt.Amount.String())
})

t.Run("from discounts", func(t *testing.T) {
inv := baseInvoiceWithLines(t)
inv.Discounts = []*bill.Discount{
Expand Down
4 changes: 4 additions & 0 deletions bill/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ func (inv *Invoice) supportedTags() []cbc.Key {
// If after removing taxes the totals don't match, a rounding error will be added to the
// invoice totals. In most scenarios this shouldn't be more than a cent or two.
//
// Documents using the `currency` rounding rule are switched to `precise`, as the
// tax-exclusive prices need more decimal places than the currency to reproduce the
// original tax amounts.
//
// This method will replace the invoice contents in place, or return an error.
func (inv *Invoice) RemoveIncludedTaxes() error {
return removeIncludedTaxes(inv)
Expand Down
Loading