Skip to content

Commit

Permalink
Merge branch 'main' into it-scontrino
Browse files Browse the repository at this point in the history
  • Loading branch information
alvarolivie committed Mar 10, 2025
2 parents 7aea03e + 2015886 commit b70ab84
Show file tree
Hide file tree
Showing 50 changed files with 4,005 additions and 1,429 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Added

- `it-ticket-v1`: implemented addon for AdE e-receipt format
- `bill`: line discount and charge `base` property, to use instead of the line sum in order to comply with EN16931.
- `bill`: line Charge support for Quantity and Rate special cases for charges like tariffs that result in a fixed amount base on a rate, like, 1 cent for every 100g of sugar.

### Changed

- `bill`: line totals will be rounded to currency precision for presentation only
- `bill`: document Discount and Charge base and amounts always rounded to currency's precision
- `bill`: line Discount and Charge base and amounts always rounded to currency's precision
- `tax`: renamed rounding rules `sum-then-round` to `precise`, and `round-then-sum` to `currency`, to more accurately reflect their objectives.
- `bill`: `currency` rounding rule implies that line totals will be calculated with the currency's precisions, bringing closer alliance with EN16931 requirements.

## [v0.211.0] - 2025-02-28

Expand Down
1 change: 1 addition & 0 deletions bill/bill.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func init() {
Order{},
Payment{},
CorrectionOptions{},
Line{},
)
}

Expand Down
48 changes: 27 additions & 21 deletions bill/calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,31 @@ func calculate(doc billable) error {
}
doc.setCurrency(r.Currency)
}
cur := doc.getCurrency()

t := doc.getTotals()
// Prepare the totals we'll need with amounts based on currency
if t == nil {
t = new(Totals)
}
zero := doc.getCurrency().Def().Zero()
zero := cur.Def().Zero()
t.reset(zero)

// Figure out rounding rules and if prices include tax early
var pit cbc.Code
var rr cbc.Key
if tx := doc.getTax(); tx != nil {
if tx.PricesInclude != "" {
pit = tx.PricesInclude
}
if tx.Rounding != "" {
rr = tx.Rounding
}
}
if rr == "" {
rr = r.GetRoundingRule()
}

// Do we need to deal with the customer-rates tag?
if doc.HasTags(tax.TagCustomerRates) {
applyCustomerRates(doc)
Expand All @@ -82,25 +98,25 @@ func calculate(doc billable) error {
}

// Preceding
calculateOrgDocumentRefs(doc.getPreceding(), doc.getCurrency(), r.GetRoundingRule())
calculateOrgDocumentRefs(doc.getPreceding(), cur, rr)

// Lines
if err := calculateLines(doc.getLines(), doc.getCurrency(), doc.getExchangeRates()); err != nil {
if err := calculateLines(doc.getLines(), cur, doc.getExchangeRates(), rr); err != nil {
return validation.Errors{"lines": err}
}
t.Sum = calculateLineSum(doc.getLines(), doc.getCurrency())
t.Sum = calculateLineSum(doc.getLines(), cur)
t.Total = t.Sum

// Discount Lines
calculateDiscounts(doc.getDiscounts(), t.Sum, zero)
if discounts := calculateDiscountSum(doc.getDiscounts(), zero); discounts != nil {
calculateDiscounts(doc.getDiscounts(), cur, t.Sum, rr)
if discounts := calculateDiscountSum(doc.getDiscounts(), cur); discounts != nil {
t.Discount = discounts
t.Total = t.Total.Subtract(*discounts)
}

// Charge Lines
calculateCharges(doc.getCharges(), t.Sum, zero)
if charges := calculateChargeSum(doc.getCharges(), zero); charges != nil {
calculateCharges(doc.getCharges(), cur, t.Sum, rr)
if charges := calculateChargeSum(doc.getCharges(), cur); charges != nil {
t.Charge = charges
t.Total = t.Total.Add(*charges)
}
Expand All @@ -127,19 +143,6 @@ func calculate(doc billable) error {
}

// Now figure out the tax totals
var pit cbc.Code
var rr cbc.Key
if tx := doc.getTax(); tx != nil {
if tx.PricesInclude != "" {
pit = tx.PricesInclude
}
if tx.Rounding != "" {
rr = tx.Rounding
}
}
if rr == "" {
rr = r.GetRoundingRule()
}
t.Taxes = new(tax.Total)
tc := &tax.TotalCalculator{
Currency: doc.getCurrency(),
Expand Down Expand Up @@ -189,6 +192,9 @@ func calculate(doc billable) error {
pd.Terms.CalculateDues(zero, t.Payable)
}

roundLines(doc.getLines(), cur)
roundDiscounts(doc.getDiscounts(), cur)
roundCharges(doc.getCharges(), cur)
t.round(zero)
doc.setTotals(t)

Expand Down
4 changes: 2 additions & 2 deletions bill/calculator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ func TestCalculate(t *testing.T) {
},
})
inv.Tax.PricesInclude = ""
inv.Tax.Rounding = tax.RoundingRuleRoundThenSum
inv.Tax.Rounding = tax.RoundingRuleCurrency
require.NoError(t, inv.Calculate())
assert.Equal(t, "3.48", inv.Totals.Tax.String())

inv.Tax.Rounding = tax.RoundingRuleSumThenRound
inv.Tax.Rounding = tax.RoundingRulePrecise
require.NoError(t, inv.Calculate())
assert.Equal(t, "3.49", inv.Totals.Tax.String())
})
Expand Down
37 changes: 21 additions & 16 deletions bill/charges.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/currency"
"github.com/invopop/gobl/i18n"
"github.com/invopop/gobl/num"
"github.com/invopop/gobl/tax"
Expand Down Expand Up @@ -102,9 +103,6 @@ type Charge struct {
Ext tax.Extensions `json:"ext,omitempty" jsonschema:"title=Extensions"`
// Additional semi-structured information.
Meta cbc.Meta `json:"meta,omitempty" jsonschema:"title=Meta"`

// internal amount for calculations
amount num.Amount
}

// Normalize performs normalization on the line and embedded objects using the
Expand Down Expand Up @@ -163,42 +161,49 @@ func (Charge) JSONSchemaExtend(schema *jsonschema.Schema) {
extendJSONSchemaWithChargeKey(schema)
}

func calculateCharges(lines []*Charge, sum, zero num.Amount) {
func calculateCharges(lines []*Charge, cur currency.Code, sum num.Amount, rr cbc.Key) {
// COPIED FROM discount.go
zero := cur.Def().Zero()
if len(lines) == 0 {
return
}
for i, l := range lines {
l.Index = i + 1
if l.Percent != nil && !l.Percent.IsZero() {
base := sum
exp := zero.Exp()
if l.Base != nil {
base = l.Base.RescaleUp(exp)
exp = base.Exp()
base = l.Base.RescaleUp(zero.Exp() + linePrecisionExtra)
base = tax.ApplyRoundingRule(rr, cur, base)
}
l.Amount = l.Percent.Of(base)
l.amount = l.Amount
l.Amount = l.Amount.Rescale(exp)
} else {
l.Amount = l.Amount.MatchPrecision(zero)
l.amount = l.Amount
}
l.Amount = tax.ApplyRoundingRule(rr, cur, l.Amount)
}
}

func calculateChargeSum(charges []*Charge, zero num.Amount) *num.Amount {
func calculateChargeSum(charges []*Charge, cur currency.Code) *num.Amount {
if len(charges) == 0 {
return nil
}
total := zero
total := cur.Def().Zero()
for _, l := range charges {
total = total.MatchPrecision(l.amount)
total = total.Add(l.amount)
total = total.MatchPrecision(l.Amount)
total = total.Add(l.Amount)
}
return &total
}

func (m *Charge) round(cur currency.Code) {
cd := cur.Def()
m.Amount = cd.Rescale(m.Amount)
}

func roundCharges(lines []*Charge, cur currency.Code) {
for _, l := range lines {
l.round(cur)
}
}

func extendJSONSchemaWithChargeKey(schema *jsonschema.Schema) {
prop, ok := schema.Properties.Get("key")
if !ok {
Expand Down
Loading

0 comments on commit b70ab84

Please sign in to comment.