Skip to content

Commit

Permalink
Merge pull request #201 from invopop/rounding-simplified
Browse files Browse the repository at this point in the history
Removing total calculator option, increasing accuracy in line calculations
  • Loading branch information
samlown authored Sep 19, 2023
2 parents f3930fb + 4cef11c commit 0a6fcb5
Show file tree
Hide file tree
Showing 17 changed files with 257 additions and 1,268 deletions.
3 changes: 0 additions & 3 deletions bill/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
61 changes: 57 additions & 4 deletions bill/invoice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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{
Expand Down
25 changes: 18 additions & 7 deletions bill/line.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -79,26 +82,34 @@ 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 {
if c.Percent != nil && !c.Percent.IsZero() {
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
}

Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions bill/payment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions bill/payment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
23 changes: 0 additions & 23 deletions bill/tax.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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"`
Expand All @@ -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),
}
}
}
}
11 changes: 0 additions & 11 deletions bill/tax_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
2 changes: 1 addition & 1 deletion pay/terms.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

Expand Down
1 change: 1 addition & 0 deletions regimes/co/co.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func New() *tax.Regime {
`),
},
TimeZone: "America/Bogota",
Tags: invoiceTags,
Validator: Validate,
Calculator: Calculate,
IdentityTypeKeys: taxIdentityTypeDefs, // see tax_identity.go
Expand Down
85 changes: 85 additions & 0 deletions regimes/co/examples/out/simplified.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
41 changes: 41 additions & 0 deletions regimes/co/examples/simplified.json
Original file line number Diff line number Diff line change
@@ -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%"
}
]
}
}
Loading

0 comments on commit 0a6fcb5

Please sign in to comment.