Totales
El SII compara cada monto de un documento con los montos que calcula desde las líneas, así que los números deben seguir sus reglas al peso. calculateLineAmount calcula una línea, calculateTotals calcula la sección Totales de un documento. createDocument llama a ambas por ti; tú las llamas para totalizar un carrito antes de que exista un documento, o para comprobar una cifra.
Ambas viven en la entrada dte-sdk. Son funciones puras sobre datos planos: sin reloj, sin red, sin secretos.
Uso#
import { calculateLineAmount, calculateTotals } from 'dte-sdk'
// Una línea: 3 unidades a $15.000 con 10 % de descuento.
const line = calculateLineAmount({ quantity: 3, unitPrice: 15000, discountPercent: 10 })
// { base: 45000, discountAmount: 4500, surchargeAmount: 0, amount: 40500 }
// Los totales de una factura con dos líneas. De cada línea solo se leen `amount` y `exemptIndicator`.
const totals = calculateTotals([{ amount: 100000 }, { amount: line.amount }], { type: 33 })
// { netAmount: 140500, vatRate: 19, vatAmount: 26695, totalAmount: 167195 }No necesitas llamarlas cuando usas createDocument: ejecuta el mismo código y guarda el resultado en document.header.totals.
Montos de línea#
calculateLineAmount({ quantity, unitPrice, discountPercent, surchargePercent }) devuelve pesos enteros:
| Campo | Significado | Se escribe como |
|---|---|---|
base | round(quantity × unitPrice) | (no se escribe) |
discountAmount | round(base × discountPercent / 100), 0 sin descuento | DescuentoMonto |
surchargeAmount | round(base × surchargePercent / 100), 0 sin recargo | RecargoMonto |
amount | base - discountAmount + surchargeAmount | MontoItem |
Las entradas tienen por defecto quantity 1 y unitPrice 0, así que calculateLineAmount({}) es todo ceros y calculateLineAmount({ unitPrice: 999 }) tiene amount: 999.
El redondeo es por línea, nunca sobre la suma: el SII compara MontoItem con QtyItem × PrcItem en cada línea. Es el Math.round de JavaScript, así que una mitad sube: 1.5 × 333 = 499.5 queda en 500.
| Entrada | Resultado |
|---|---|
{ 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 |
Cuando una línea tiene descuento y recargo a la vez, ambos porcentajes se aplican sobre la misma base: { quantity: 3, unitPrice: 15000, discountPercent: 10, surchargePercent: 5 } da 4500 menos y 2250 más, amount 42750.
En createDocument, una línea recibe discountAmount y surchargeAmount solo cuando son mayores que 0, y amount es el resultado de arriba salvo que le entregues a la línea un amount explícito.
Totales del documento#
calculateTotals(items, options) recibe las líneas (cualquier objeto con amount y, opcionalmente, exemptIndicator) y devuelve un Totals:
| Opción | Significado |
|---|---|
type (obligatorio) | El tipo de documento. Decide la forma de las boletas y la retención por defecto. |
pricesIncludeVat | Los montos de línea incluyen IVA. |
vatRate | Tasa de IVA en porcentaje. Por defecto 19 (VAT_RATE). |
globalAdjustments | Descuentos y recargos globales (GlobalAdjustment[], como en un documento creado). |
withholdVat | Retener todo el IVA. Por defecto true en el tipo 46 y false en los demás. |
Los campos de Totals, todos en pesos enteros:
| Campo | Etiqueta SII | Significado |
|---|---|---|
netAmount | MntNeto | Monto neto: las líneas afectas antes del IVA. Se omite cuando es 0. |
exemptAmount | MntExe | Monto exento (líneas con IndExe 1). Se omite cuando es 0. |
vatRate | TasaIVA | Tasa de IVA en porcentaje. Nunca en boletas. |
vatAmount | IVA | Monto del IVA. |
ownVatAmount | IVAProp | IVA propio (liquidaciones factura). No se calcula. |
thirdPartyVatAmount | IVATerc | IVA de terceros (liquidaciones factura). No se calcula. |
withholdings | ImptoReten | Retenciones: { taxCode, rate, amount } (TipoImp, TasaImp, MontoImp). |
commissions | Comisiones | Totales de comisiones de una liquidación factura (netValue, exemptValue, vatValue). No se calcula. |
totalAmount | MntTotal | El total: neto + IVA + exento. Sin el IVA cuando está retenido. |
nonBillableAmount | MontoNF | Monto no facturable (líneas con IndExe 2 a 6). Se omite cuando es 0. |
periodAmount | MontoPeriodo | Monto del período. No se calcula. |
previousBalance | SaldoAnterior | Saldo anterior. No se calcula. |
amountDue | VlrPagar | Valor a pagar. No se calcula. |
calculateTotals calcula netAmount, exemptAmount, vatRate, vatAmount, withholdings, totalAmount y nonBillableAmount. Los campos marcados "No se calcula" existen en el tipo para que puedas entregarlos en unos totals explícitos.
Las claves salen en el orden del esquema y las secciones en cero se omiten, tal como espera el SII. Las reglas, en el orden en que el código las aplica:
- Cada línea va a una base según su
exemptIndicator: ninguno es afecto, 1 es exento, 2 a 6 son no facturable. Una base es la suma de sus líneas (una línea negativa, comoIndExe6, resta). - Los ajustes globales se aplican sobre las bases (mira Descuentos y recargos globales).
- Cuando el neto es mayor que 0, el IVA es
round(net × vatRate / 100). ConpricesIncludeVat, la base afecta es el bruto:net = round(gross / (1 + vatRate / 100))yvat = gross - net, así que los dos siempre suman el bruto. - El total es
net + vat + exempt. Cuando el IVA está retenido esnet + exempt, y una entrada dewithholdingscon código 15 (IVA retenido total) lleva el IVA, con elvatRatecomo tasa.
El IVA se calcula una sola vez sobre el neto, nunca línea por línea.
Precios netos#
calculateTotals([{ amount: 1680 }], { type: 33 })
// { netAmount: 1680, vatRate: 19, vatAmount: 319, totalAmount: 1999 }1680 × 19 % es 319,2, que se redondea a 319.
Precios con IVA incluido#
Con pricesIncludeVat: true los montos de las líneas afectas son brutos (MntBruto). El neto se deriva de ellos y el IVA es lo que queda:
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 es 1680,67, así que el neto es 1681 y el IVA 319. La parte afecta del total siempre es el bruto que entregaste.
En createDocument, pricesIncludeVat: true además escribe MntBruto=1 en el encabezado en todos los tipos salvo boletas. Las líneas exentas no se ven afectadas: nunca se dividen.
Líneas exentas y no facturables#
Una línea con exemptIndicator: 1 (o exempt: true) es exenta: va a exemptAmount y queda fuera del neto, así que no paga IVA. Las líneas con indicador de 2 a 6 son no facturables: van a nonBillableAmount y quedan fuera del total.
// Una línea afecta y una exenta, precios con IVA incluido.
calculateTotals([{ amount: 2000 }, { amount: 500, exemptIndicator: 1 }], {
type: 33,
pricesIncludeVat: true,
})
// { netAmount: 1681, exemptAmount: 500, vatRate: 19, vatAmount: 319, totalAmount: 2500 }
// Solo líneas exentas: sin neto y sin IVA.
calculateTotals([{ amount: 500, exemptIndicator: 1 }], { type: 33 })
// { exemptAmount: 500, totalAmount: 500 }
// Una línea no facturable y una negativa (IndExe 6) se compensan en nonBillableAmount.
calculateTotals([{ amount: 1000 }, { amount: 300, exemptIndicator: 2 }, { amount: -50, exemptIndicator: 6 }], {
type: 33,
})
// { netAmount: 1000, vatRate: 19, vatAmount: 190, totalAmount: 1190, nonBillableAmount: 250 }Los tipos exentos (34 y 41) dan IndExe=1 a cada línea cuando construyes el documento con createDocument, así que sus totales solo tienen exemptAmount y totalAmount. calculateTotals por sí sola lee únicamente los indicadores de las líneas que le pasas; no aplica el default del tipo. Los indicadores son EXEMPT_INDICATORS: exempt 1, nonBillable 2, containerDeposit 3, notASale 4, deduction 5, nonBillableNegative 6.
Descuentos y recargos globales#
Un ajuste global es un descuento o un recargo sobre una base completa, como porcentaje o como monto. Su exemptIndicator elige la base: ninguno para el monto afecto, 1 para el exento, 2 para el no facturable. Los ajustes se aplican uno tras otro, en orden, y un ajuste porcentual trabaja sobre la base tal como queda después de los anteriores:
// 10 % de descuento sobre la base afecta.
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 }
// Recargo de $100 sobre la base exenta.
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 % de descuento sobre la base no facturable: sigue fuera del 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 }Dos descuentos de 10 % sobre 1000 dan 810, no 800: el segundo toma el 10 % de 900. Un porcentaje es round(base × value / 100); un monto es round(value).
Con createDocument escribes los ajustes en la entrada (globalAdjustments, mira Documentos) y el SDK se los pasa a esta función.
Facturas de compra y retención#
En una factura de compra (tipo 46) el comprador retiene el IVA, así que withholdVat es true por defecto. El IVA se sigue informando (vatAmount), una entrada de withholdings lo lleva con el código de impuesto 15, y el total lo deja fuera:
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 }En cualquier otro tipo, pasa withholdVat: true para retener (una nota de crédito o de débito que afecta una factura de compra lo hace). Con createDocument es el campo withholdVat de la entrada. No se escribe ninguna entrada de withholdings cuando el IVA es 0.
Boletas#
Las boletas (39 y 41) no llevan TasaIVA: los totales tienen netAmount, vatAmount y totalAmount, nunca vatRate. Normalmente se emiten con precios con IVA incluido:
calculateTotals([{ amount: 3000 }], { type: 39, pricesIncludeVat: true })
// { netAmount: 2521, vatAmount: 479, totalAmount: 3000 }Sin pricesIncludeVat los montos son netos y el IVA se suma encima: calculateTotals([{ amount: 3000 }], { type: 39 }) da netAmount 3000, vatAmount 570 y totalAmount 3570.
Documentos vacíos y casos especiales#
Cuando no hay monto afecto ni exento ni no facturable, el resultado es solo el total en cero, con la tasa en todos los tipos salvo boletas. Es el caso de una guía de despacho de un traslado sin valores:
calculateTotals([{ amount: 0 }], { type: 52 }) // { vatRate: 19, totalAmount: 0 }
calculateTotals([], { type: 39 }) // { totalAmount: 0 }Un documento con solo líneas no facturables tiene totalAmount: 0 y un nonBillableAmount, y no lleva vatRate: calculateTotals([{ amount: 300, exemptIndicator: 2 }], { type: 33 }) da { totalAmount: 0, nonBillableAmount: 300 }.
Otra tasa de IVA#
vatRate es 19 por defecto, la tasa vigente (VAT_RATE). calculateTotals([{ amount: 1000 }], { type: 33, vatRate: 10 }) da vatRate 10, vatAmount 100 y totalAmount 1100. createDocument siempre usa 19; para emitir un documento con otra tasa, calcula tú los totales y entrégalos como totals.
Totales explícitos#
createDocument calcula los totales salvo que la entrada tenga un objeto totals, que entonces reemplaza al calculado:
const document = createDocument({
...input,
totals: { netAmount: 1000, vatRate: 19, vatAmount: 190, totalAmount: 1190 },
})Úsalo para lo que calculateTotals no calcula (totales de comisiones, IVAProp, IVATerc, MontoPeriodo, SaldoAnterior, VlrPagar) o para una tasa de IVA distinta de 19. El SDK confía en el objeto y no lo compara con las líneas, así que un error aquí llega al SII. La bandera pricesIncludeVat sigue decidiendo si se escribe MntBruto=1, y solo cuando totals.netAmount es mayor que 0.
El totalAmount también termina en el timbre electrónico (MNT), así que tiene que estar correcto antes de timbrar: al timbrar el documento se firma el monto que tenga.
Siguientes pasos#
- Documentos: el modelo de entrada que alimenta estas funciones.
- Timbre:
MNTen el TED es eltotalAmount. - Libros: las líneas de los libros se derivan de documentos emitidos.