Totals
The SII compares every amount in a document with the amounts it computes from the lines, so the numbers have to follow its rules to the peso. calculateLineAmount computes one line, calculateTotals computes the Totales section of a document. createDocument calls both for you; you call them yourself to total a cart before a document exists, or to check a figure.
Both live in the dte-sdk entry point. They are pure functions over plain data: no clock, no network, no secrets.
Usage#
import { calculateLineAmount, calculateTotals } from 'dte-sdk'
// One line: 3 units at $15,000 with a 10 % discount.
const line = calculateLineAmount({ quantity: 3, unitPrice: 15000, discountPercent: 10 })
// { base: 45000, discountAmount: 4500, surchargeAmount: 0, amount: 40500 }
// The totals of an invoice with two lines. Only `amount` and `exemptIndicator` are read from a line.
const totals = calculateTotals([{ amount: 100000 }, { amount: line.amount }], { type: 33 })
// { netAmount: 140500, vatRate: 19, vatAmount: 26695, totalAmount: 167195 }You do not need to call them when you use createDocument: it runs the same code and stores the result in document.header.totals.
Line amounts#
calculateLineAmount({ quantity, unitPrice, discountPercent, surchargePercent }) returns whole pesos:
| Field | Meaning | Written as |
|---|---|---|
base | round(quantity × unitPrice) | (not written) |
discountAmount | round(base × discountPercent / 100), 0 without a discount | DescuentoMonto |
surchargeAmount | round(base × surchargePercent / 100), 0 without a surcharge | RecargoMonto |
amount | base - discountAmount + surchargeAmount | MontoItem |
The inputs default to quantity 1 and unitPrice 0, so calculateLineAmount({}) is all zeros and calculateLineAmount({ unitPrice: 999 }) has amount: 999.
Rounding is per line, never over the sum: the SII compares MontoItem against QtyItem × PrcItem for each line. It is JavaScript Math.round, so a half rounds up: 1.5 × 333 = 499.5 becomes 500.
| Input | Result |
|---|---|
{ quantity: 2, unitPrice: 1000 } | base 2000, amount 2000 |
{ quantity: 1.5, unitPrice: 333 } | base 500, amount 500 |
{ quantity: 3, unitPrice: 1000, discountPercent: 10 } | base 3000, discountAmount 300, amount 2700 |
{ quantity: 1, unitPrice: 1000, surchargePercent: 5 } | base 1000, surchargeAmount 50, amount 1050 |
When a line has both a discount and a surcharge, both percentages apply to the same base: { quantity: 3, unitPrice: 15000, discountPercent: 10, surchargePercent: 5 } gives 4500 off and 2250 on, amount 42750.
In createDocument, a line gets discountAmount and surchargeAmount only when they are greater than 0, and amount is the result above unless you give the line an explicit amount.
Document totals#
calculateTotals(items, options) takes the lines (anything with amount and, optionally, exemptIndicator) and returns a Totals:
| Option | Meaning |
|---|---|
type (required) | The document type. It decides the receipt shape and the default withholding. |
pricesIncludeVat | The line amounts include VAT. |
vatRate | VAT rate in percent. Default 19 (VAT_RATE). |
globalAdjustments | Global discounts and surcharges (GlobalAdjustment[], as in a created document). |
withholdVat | Withhold all the VAT. Default true for type 46 and false for every other type. |
The Totals fields, all in whole pesos:
| Field | SII tag | Meaning |
|---|---|---|
netAmount | MntNeto | Net amount: the affected lines before VAT. Omitted when it is 0. |
exemptAmount | MntExe | Exempt amount (lines with IndExe 1). Omitted when it is 0. |
vatRate | TasaIVA | VAT rate in percent. Never on receipts. |
vatAmount | IVA | VAT amount. |
ownVatAmount | IVAProp | Own VAT (settlement invoices). Not computed. |
thirdPartyVatAmount | IVATerc | Third-party VAT (settlement invoices). Not computed. |
withholdings | ImptoReten | Withholdings: { taxCode, rate, amount } (TipoImp, TasaImp, MontoImp). |
commissions | Comisiones | Commission totals of a settlement invoice (netValue, exemptValue, vatValue). Not computed. |
totalAmount | MntTotal | The total: net + VAT + exempt. Without the VAT when it is withheld. |
nonBillableAmount | MontoNF | Non-billable amount (lines with IndExe 2 to 6). Omitted when it is 0. |
periodAmount | MontoPeriodo | Period amount. Not computed. |
previousBalance | SaldoAnterior | Previous balance. Not computed. |
amountDue | VlrPagar | Amount due. Not computed. |
calculateTotals computes netAmount, exemptAmount, vatRate, vatAmount, withholdings, totalAmount and nonBillableAmount. The fields marked "not computed" exist in the type so you can pass them in an explicit totals.
The keys come out in schema order and the zero sections are left out, exactly as the SII expects. The rules, in the order the code applies them:
- Each line goes to a base by its
exemptIndicator: none means affected, 1 means exempt, 2 to 6 mean non-billable. A base is the sum of its lines (a negative line, such asIndExe6, subtracts). - The global adjustments are applied to the bases (see Global discounts and surcharges).
- When the net is greater than 0, VAT is
round(net × vatRate / 100). WithpricesIncludeVat, the affected base is the gross:net = round(gross / (1 + vatRate / 100))andvat = gross - net, so the two always add back up to the gross. - The total is
net + vat + exempt. When the VAT is withheld it isnet + exempt, and awithholdingsentry of 15 (total VAT withholding) carries the VAT, with thevatRateas its rate.
VAT is computed once over the net, never line by line.
Net prices#
calculateTotals([{ amount: 1680 }], { type: 33 })
// { netAmount: 1680, vatRate: 19, vatAmount: 319, totalAmount: 1999 }1680 × 19 % is 319.2, rounded to 319.
VAT-included prices#
With pricesIncludeVat: true the amounts of the affected lines are gross (MntBruto). The net is derived from them and the VAT is what is left:
calculateTotals([{ amount: 2000 }], { type: 33, pricesIncludeVat: true })
// { netAmount: 1681, vatRate: 19, vatAmount: 319, totalAmount: 2000 }
calculateTotals([{ amount: 1018 }], { type: 33, pricesIncludeVat: true })
// { netAmount: 855, vatRate: 19, vatAmount: 163, totalAmount: 1018 }
calculateTotals([{ amount: 1005 }], { type: 33, pricesIncludeVat: true })
// { netAmount: 845, vatRate: 19, vatAmount: 160, totalAmount: 1005 }2000 / 1.19 is 1680.67, so the net is 1681 and the VAT is 319. The affected part of the total is always the gross you gave.
In createDocument, pricesIncludeVat: true also writes MntBruto=1 in the header on every type but receipts. Exempt lines are not affected by it: they are never divided.
Exempt and non-billable lines#
A line with exemptIndicator: 1 (or exempt: true) is exempt: it goes to exemptAmount and stays out of the net, so it pays no VAT. Lines with an indicator from 2 to 6 are non-billable: they go to nonBillableAmount and stay out of the total.
// An affected line and an exempt one, VAT-included prices.
calculateTotals([{ amount: 2000 }, { amount: 500, exemptIndicator: 1 }], {
type: 33,
pricesIncludeVat: true,
})
// { netAmount: 1681, exemptAmount: 500, vatRate: 19, vatAmount: 319, totalAmount: 2500 }
// Only exempt lines: no net, no VAT.
calculateTotals([{ amount: 500, exemptIndicator: 1 }], { type: 33 })
// { exemptAmount: 500, totalAmount: 500 }
// A non-billable line and a negative one (IndExe 6) net each other out in nonBillableAmount.
calculateTotals([{ amount: 1000 }, { amount: 300, exemptIndicator: 2 }, { amount: -50, exemptIndicator: 6 }], {
type: 33,
})
// { netAmount: 1000, vatRate: 19, vatAmount: 190, totalAmount: 1190, nonBillableAmount: 250 }The exempt types (34 and 41) give every line IndExe=1 when you build the document with createDocument, so their totals only have exemptAmount and totalAmount. calculateTotals itself reads only the indicators on the lines you pass; it does not apply the type's default. Indicators are EXEMPT_INDICATORS: exempt 1, nonBillable 2, containerDeposit 3, notASale 4, deduction 5, nonBillableNegative 6.
Global discounts and surcharges#
A global adjustment is a discount or a surcharge over a whole base, as a percent or as an amount. Its exemptIndicator picks the base: none for the affected amount, 1 for the exempt amount, 2 for the non-billable amount. Adjustments are applied one after the other, in order, and a percent adjustment works on the base as it stands after the previous ones:
// 10 % off the affected base.
calculateTotals([{ amount: 1000 }, { amount: 500, exemptIndicator: 1 }], {
type: 33,
globalAdjustments: [{ lineNumber: 1, kind: 'discount', valueType: 'percent', value: 10 }],
})
// { netAmount: 900, exemptAmount: 500, vatRate: 19, vatAmount: 171, totalAmount: 1571 }
// $100 surcharge on the exempt base.
calculateTotals([{ amount: 1000 }, { amount: 500, exemptIndicator: 1 }], {
type: 33,
globalAdjustments: [
{ lineNumber: 1, kind: 'surcharge', valueType: 'amount', value: 100, exemptIndicator: 1 },
],
})
// { netAmount: 1000, exemptAmount: 600, vatRate: 19, vatAmount: 190, totalAmount: 1790 }
// 50 % off the non-billable base: it stays out of the total.
calculateTotals([{ amount: 1000 }, { amount: 300, exemptIndicator: 2 }], {
type: 33,
globalAdjustments: [
{ lineNumber: 1, kind: 'discount', valueType: 'percent', value: 50, exemptIndicator: 2 },
],
})
// { netAmount: 1000, vatRate: 19, vatAmount: 190, totalAmount: 1190, nonBillableAmount: 150 }Two 10 % discounts on 1000 give 810, not 800: the second one takes 10 % of 900. A percent is round(base × value / 100); an amount is round(value).
With createDocument you write the adjustments in the input (globalAdjustments, see Documents) and the SDK passes them here.
Purchase invoices and withholding#
On a purchase invoice (type 46) the buyer withholds the VAT, so withholdVat defaults to true. The VAT is still reported (vatAmount), a withholdings entry carries it with tax code 15, and the total leaves it out:
calculateTotals([{ amount: 1000 }], { type: 46 })
// {
// netAmount: 1000,
// vatRate: 19,
// vatAmount: 190,
// withholdings: [{ taxCode: 15, rate: 19, amount: 190 }],
// totalAmount: 1000,
// }
calculateTotals([{ amount: 1000 }], { type: 46, withholdVat: false })
// { netAmount: 1000, vatRate: 19, vatAmount: 190, totalAmount: 1190 }On any other type, pass withholdVat: true to withhold (a credit or debit note that affects a purchase invoice does this). Through createDocument it is the withholdVat field of the input. No withholdings entry is written when the VAT is 0.
Receipts#
Receipts (39 and 41) carry no TasaIVA: the totals have netAmount, vatAmount and totalAmount, never vatRate. They are normally issued with VAT-included prices:
calculateTotals([{ amount: 3000 }], { type: 39, pricesIncludeVat: true })
// { netAmount: 2521, vatAmount: 479, totalAmount: 3000 }Without pricesIncludeVat the amounts are net and the VAT is added on top: calculateTotals([{ amount: 3000 }], { type: 39 }) gives netAmount 3000, vatAmount 570 and totalAmount 3570.
Empty and unusual documents#
When there is no affected or exempt amount and no non-billable amount, the result is just the total at zero, with the rate on everything but receipts. That is the case of a dispatch guide for a transfer without values:
calculateTotals([{ amount: 0 }], { type: 52 }) // { vatRate: 19, totalAmount: 0 }
calculateTotals([], { type: 39 }) // { totalAmount: 0 }A document with only non-billable lines has totalAmount: 0 and a nonBillableAmount, and no vatRate: calculateTotals([{ amount: 300, exemptIndicator: 2 }], { type: 33 }) gives { totalAmount: 0, nonBillableAmount: 300 }.
A different VAT rate#
vatRate defaults to 19, the rate in force (VAT_RATE). calculateTotals([{ amount: 1000 }], { type: 33, vatRate: 10 }) gives vatRate 10, vatAmount 100 and totalAmount 1100. createDocument always uses 19; to issue a document with another rate, calculate the totals yourself and pass them as totals.
Explicit totals#
createDocument computes the totals unless the input has a totals object, which then replaces the computed one:
const document = createDocument({
...input,
totals: { netAmount: 1000, vatRate: 19, vatAmount: 190, totalAmount: 1190 },
})Use it for what calculateTotals does not compute (commission totals, IVAProp, IVATerc, MontoPeriodo, SaldoAnterior, VlrPagar) or for a VAT rate other than 19. The SDK trusts the object and does not compare it with the lines, so a mistake here reaches the SII. The pricesIncludeVat flag still decides whether MntBruto=1 is written, and only when totals.netAmount is greater than 0.
The totalAmount also ends up in the electronic stamp (MNT), so it has to be right before you stamp: stamping the document signs whatever amount it holds.