Calculation Logic¶
How InvoicePDFs computes invoice totals with precision.
Overview¶
All calculations use decimal arithmetic (no floating point) with half-up rounding to 2 decimal places. This ensures accurate financial math even with complex tax and discount combinations.
Formula¶
Calculation Order¶
Two passes, because a document-level discount is not known until every line has been read, but it has to affect the tax on every line.
Pass one — reduce each line to a taxable value
a. gross = quantity × unit_price
b. subtract the per-line discount
c. extract inclusive tax, if any
→ what remains is the line's taxable value; the sum is the subtotal
Between the passes
d. compute the document-level discount from the subtotal
e. apportion it back across the lines, pro-rata by taxable value
Pass two — tax what is left
f. apply each line's rates to (taxable value − its share of the discount),
plus its share of the freight when shipping.taxable is set
g. total = subtotal − document_discount + tax_total + shipping_total
Both kinds of discount reduce the tax
Where a discount is written does not change what is owed. 10% off a line and 10% off the invoice produce the same total, because the document-level discount is apportioned back into the taxable value before tax is applied.
This is not a preference. A discount granted at the time of supply is excluded from the taxable amount — EU VAT Directive Art. 79, CGST s.15(3)(a) — and a tax invoice must show the taxable value net of it (CGST Rule 46).
Worked Examples¶
Simple Invoice¶
| Step | Amount |
|---|---|
| Gross | 2 × $50.00 = $100.00 |
| Subtotal | $100.00 |
| Total | $100.00 |
With Exclusive Tax¶
{
"line_items": [
{
"name": "Service",
"quantity": "1",
"unit_price": "49.00",
"taxes": [{ "name": "Sales Tax", "rate": "8.375", "inclusive": false }]
}
]
}
| Step | Amount |
|---|---|
| Gross | $49.00 |
| Tax | $49.00 × 8.375% = $4.10 (rounded) |
| Subtotal | $49.00 |
| Tax total | $4.10 |
| Total | $53.10 |
With Inclusive Tax (VAT)¶
{
"line_items": [
{
"name": "Product",
"quantity": "1",
"unit_price": "120.00",
"taxes": [{ "name": "VAT", "rate": "20", "inclusive": true }]
}
]
}
| Step | Amount |
|---|---|
| Gross | $120.00 |
| Inclusive VAT | $120.00 × (20 / 120) = $20.00 |
| Net (subtotal) | $120.00 - $20.00 = $100.00 |
| Tax total | $20.00 |
| Total | $120.00 |
With Per-Line Discount + Tax¶
{
"line_items": [
{
"name": "Service",
"quantity": "1",
"unit_price": "100.00",
"discount": { "type": "percentage", "value": "20" },
"taxes": [{ "name": "Tax", "rate": "10", "inclusive": false }]
}
]
}
| Step | Amount |
|---|---|
| Gross | $100.00 |
| Line discount | $100.00 × 20% = $20.00 |
| Discounted gross | $80.00 |
| Tax | $80.00 × 10% = $8.00 |
| Subtotal | $80.00 |
| Discount total | $20.00 |
| Tax total | $8.00 |
| Total | $88.00 |
Key insight
Per-line discounts are applied before tax calculation. This means tax is computed on the discounted amount, not the original price.
Full Example: Discounts + Tax + Shipping¶
{
"line_items": [
{
"name": "Product A",
"quantity": "2",
"unit_price": "100.00",
"discount": { "type": "percentage", "value": "10" },
"taxes": [{ "name": "Tax", "rate": "10", "inclusive": false }]
}
],
"discounts": [
{ "type": "fixed", "value": "10.00" }
],
"shipping": { "amount": "15.00" }
}
| Step | Amount |
|---|---|
| Gross | 2 × $100.00 = $200.00 |
| Line discount | $200.00 × 10% = $20.00 |
| Subtotal (taxable value) | $180.00 |
| Document discount | $10.00 |
| Taxed on | $180.00 − $10.00 = $170.00 |
| Tax | $170.00 × 10% = $17.00 |
| Shipping | $15.00 |
| Discount total | $20.00 + $10.00 = $30.00 |
| Total | $180.00 − $10.00 + $17.00 + $15.00 = $202.00 |
This example changed
Tax here is $17.00, not $18.00, and the total $202.00, not $203.00. The document-level discount used to be subtracted after tax was computed, so the buyer was charged tax on $10.00 they were never billed for. The rate row on the printed invoice now cites $170.00 as the taxable value — the amount actually taxed.
Rounding¶
- All monetary calculations round to 2 decimal places using half-up rounding
- Rounding is applied per line item to avoid surprising totals
- Tax is rounded per-line, then summed — not computed on the aggregate
Validation¶
Input that cannot be computed is refused with 422 unprocessable_entity, and
the message names the field:
| Rejected | Why |
|---|---|
"Infinity", "NaN" in any numeric field |
Not finite; the arithmetic cannot produce a number |
quantity ≤ 0, negative unit_price, negative rate |
Not a quantity, price or rate |
A percentage discount above 100 |
Cannot discount more than the whole |
| A fixed line discount above its line amount | Would produce negative tax |
| Document discounts above the subtotal | Same |
{
"error": {
"status": 422,
"code": "unprocessable_entity",
"message": "line_items[0].quantity must be > 0"
}
}
POST /api/v1/documents/validate runs the full calculation and discards the
result, so it answers whether the payload can actually become a document — not
just whether its fields look plausible. Anything it accepts, calculate and
render will accept too.
Response Fields¶
| Field | Description |
|---|---|
gross_subtotal |
Line amounts at list price, before any discount. What the Amount column on the printed invoice sums to |
subtotal |
The taxable value: net of per-line discounts and of any tax bundled into the price |
discount_total |
Every discount — per-line and document-level. Reported for information |
document_discount_total |
Only the document-level discounts. This is the one the total formula subtracts |
tax_total |
All tax, inclusive and exclusive |
shipping_total |
Shipping amount (0 if none) |
total |
subtotal − document_discount_total + tax_total + shipping_total |
discount_total is not what the total subtracts
subtotal is already net of per-line discounts, so subtracting
discount_total from it removes them a second time. Use
document_discount_total for arithmetic, discount_total for display.
Both of these identities hold, and they are the two ways an invoice adds up:
A rendered document also carries tax_lines: one row per rate, each with the
taxable value it applies to and the tax it produced. Under GST a rate-wise
breakup is part of what makes a document a tax invoice.
The POST /api/v1/documents/calculate endpoint wraps the breakdown under
data.calculation:
{
"data": {
"calculation": {
"subtotal": { "amount": "180.00", "currency": "USD" },
"discount_total": { "amount": "30.00", "currency": "USD" },
"document_discount_total": { "amount": "10.00", "currency": "USD" },
"tax_total": { "amount": "17.00", "currency": "USD" },
"shipping_total": { "amount": "15.00", "currency": "USD" },
"total": { "amount": "202.00", "currency": "USD" }
}
}
}