73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
|
|
import { TaxCatalogProvider } from "@erp/core";
|
|||
|
|
import Dinero, { Currency } from "dinero.js";
|
|||
|
|
|
|||
|
|
export interface InvoiceItemCalcInput {
|
|||
|
|
quantity?: string; // p.ej. "3.5"
|
|||
|
|
unit_amount?: string; // p.ej. "125.75"
|
|||
|
|
discount_percentage?: string; // p.ej. "10" (=> 10%)
|
|||
|
|
tax_codes: string[]; // ["iva_21", ...]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface InvoiceItemCalcResult {
|
|||
|
|
subtotal_amount: number;
|
|||
|
|
discount_amount: number;
|
|||
|
|
taxable_amount: number;
|
|||
|
|
taxes_amount: number;
|
|||
|
|
total_amount: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Cálculo financiero exacto por línea de factura.
|
|||
|
|
* Usa Dinero.js (base 10^2 y round half up) → resultados seguros en céntimos.
|
|||
|
|
*/
|
|||
|
|
export function calculateInvoiceItemAmounts(
|
|||
|
|
item: InvoiceItemCalcInput,
|
|||
|
|
currency: string,
|
|||
|
|
taxCatalog: TaxCatalogProvider
|
|||
|
|
): InvoiceItemCalcResult {
|
|||
|
|
const scale = 4;
|
|||
|
|
const toDinero = (n: number) =>
|
|||
|
|
Dinero({
|
|||
|
|
amount: n === 0 ? 0 : Math.round(n * 10 ** scale),
|
|||
|
|
precision: scale,
|
|||
|
|
currency: currency as Currency,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const qty = Number.parseFloat(item.quantity || "0") || 0;
|
|||
|
|
const unit = Number.parseFloat(item.unit_amount || "0") || 0;
|
|||
|
|
const pct = Number.parseFloat(item.discount_percentage || "0") || 0;
|
|||
|
|
|
|||
|
|
// Subtotal = cantidad × precio unitario
|
|||
|
|
const subtotal = toDinero(qty * unit);
|
|||
|
|
|
|||
|
|
// Descuento = subtotal × (pct / 100)
|
|||
|
|
const discount = subtotal.percentage(pct);
|
|||
|
|
|
|||
|
|
// Base imponible
|
|||
|
|
const taxable = subtotal.subtract(discount);
|
|||
|
|
|
|||
|
|
// Impuestos acumulados
|
|||
|
|
let taxes = toDinero(0);
|
|||
|
|
for (const code of item.tax_codes ?? []) {
|
|||
|
|
const tax = taxCatalog.findByCode(code);
|
|||
|
|
tax.map((taxItem) => {
|
|||
|
|
const pctValue = Number.parseFloat(taxItem.value) / 10 ** Number.parseInt(taxItem.scale, 10);
|
|||
|
|
const taxAmount = taxable.percentage(pctValue);
|
|||
|
|
taxes = taxes.add(taxAmount);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const total = taxable.add(taxes);
|
|||
|
|
|
|||
|
|
// Devuelve valores desescalados (número con 2 decimales exactos)
|
|||
|
|
const toNum = (d: Dinero.Dinero) => d.toUnit();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
subtotal_amount: toNum(subtotal),
|
|||
|
|
discount_amount: toNum(discount),
|
|||
|
|
taxable_amount: toNum(taxable),
|
|||
|
|
taxes_amount: toNum(taxes),
|
|||
|
|
total_amount: toNum(total),
|
|||
|
|
};
|
|||
|
|
}
|