2025-10-13 17:41:21 +00:00
|
|
|
|
import { TaxCatalogProvider, TaxItemType } from "@erp/core";
|
|
|
|
|
|
import { toDinero, toNum } from "./calculate-utils";
|
2025-10-12 18:36:33 +00:00
|
|
|
|
|
|
|
|
|
|
export interface InvoiceItemCalcInput {
|
|
|
|
|
|
quantity?: string; // p.ej. "3.5"
|
|
|
|
|
|
unit_amount?: string; // p.ej. "125.75"
|
|
|
|
|
|
discount_percentage?: string; // p.ej. "10" (=> 10%)
|
2025-10-13 17:41:21 +00:00
|
|
|
|
header_discount_percentage?: string; // p.ej. "5" (=> 5%)
|
2025-10-12 18:36:33 +00:00
|
|
|
|
tax_codes: string[]; // ["iva_21", ...]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-13 17:41:21 +00:00
|
|
|
|
export type InvoiceItemTaxSummary = TaxItemType & {
|
|
|
|
|
|
taxable_amount: number;
|
|
|
|
|
|
taxes_amount: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-12 18:36:33 +00:00
|
|
|
|
export interface InvoiceItemCalcResult {
|
|
|
|
|
|
subtotal_amount: number;
|
|
|
|
|
|
discount_amount: number;
|
2025-10-13 17:41:21 +00:00
|
|
|
|
header_discount_amount: number;
|
2025-10-12 18:36:33 +00:00
|
|
|
|
taxable_amount: number;
|
|
|
|
|
|
taxes_amount: number;
|
2025-10-13 17:41:21 +00:00
|
|
|
|
taxes_summary: InvoiceItemTaxSummary[];
|
2025-10-12 18:36:33 +00:00
|
|
|
|
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 {
|
2025-10-13 17:41:21 +00:00
|
|
|
|
const defaultScale = 4;
|
|
|
|
|
|
const taxesSummary: InvoiceItemTaxSummary[] = [];
|
2025-10-12 18:36:33 +00:00
|
|
|
|
|
|
|
|
|
|
const qty = Number.parseFloat(item.quantity || "0") || 0;
|
|
|
|
|
|
const unit = Number.parseFloat(item.unit_amount || "0") || 0;
|
2025-10-13 17:41:21 +00:00
|
|
|
|
const iten_pct = Number.parseFloat(item.discount_percentage || "0") || 0;
|
|
|
|
|
|
const header_pct = Number.parseFloat(item.header_discount_percentage || "0") || 0;
|
2025-10-12 18:36:33 +00:00
|
|
|
|
|
|
|
|
|
|
// Subtotal = cantidad × precio unitario
|
2025-10-13 17:41:21 +00:00
|
|
|
|
const subtotal_amount = toDinero(unit, defaultScale, currency).multiply(qty);
|
|
|
|
|
|
|
|
|
|
|
|
// Descuento = subtotal × (item_pct / 100)
|
|
|
|
|
|
const discount_amount = subtotal_amount.percentage(iten_pct);
|
|
|
|
|
|
const subtotal_w_discount_amount = subtotal_amount.subtract(discount_amount);
|
2025-10-12 18:36:33 +00:00
|
|
|
|
|
2025-10-13 17:41:21 +00:00
|
|
|
|
// Descuento de la cabecera = subtotal con dto de línea × (header_pct / 100)
|
|
|
|
|
|
const header_discount = subtotal_w_discount_amount.percentage(header_pct);
|
2025-10-12 18:36:33 +00:00
|
|
|
|
|
|
|
|
|
|
// Base imponible
|
2025-10-13 17:41:21 +00:00
|
|
|
|
const taxable_amount = subtotal_w_discount_amount.subtract(header_discount);
|
2025-10-12 18:36:33 +00:00
|
|
|
|
|
2025-10-13 17:41:21 +00:00
|
|
|
|
// Impuestos acumulados con signo
|
|
|
|
|
|
let taxes_amount = toDinero(0, defaultScale, currency);
|
2025-10-12 18:36:33 +00:00
|
|
|
|
for (const code of item.tax_codes ?? []) {
|
|
|
|
|
|
const tax = taxCatalog.findByCode(code);
|
2025-10-13 17:41:21 +00:00
|
|
|
|
if (tax.isNone()) continue;
|
|
|
|
|
|
|
2025-10-12 18:36:33 +00:00
|
|
|
|
tax.map((taxItem) => {
|
2025-10-13 17:41:21 +00:00
|
|
|
|
const tax_pct_value =
|
|
|
|
|
|
Number.parseFloat(taxItem.value) / 10 ** Number.parseInt(taxItem.scale, 10);
|
|
|
|
|
|
const item_taxables_amount = taxable_amount.percentage(tax_pct_value);
|
|
|
|
|
|
|
|
|
|
|
|
// Sumar o restar según grupo
|
|
|
|
|
|
switch (taxItem.group.toLowerCase()) {
|
|
|
|
|
|
case "retención":
|
|
|
|
|
|
taxes_amount = taxes_amount.subtract(item_taxables_amount);
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
taxes_amount = taxes_amount.add(item_taxables_amount);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
taxesSummary.push({
|
|
|
|
|
|
...taxItem,
|
|
|
|
|
|
taxable_amount: toNum(taxable_amount),
|
|
|
|
|
|
taxes_amount: toNum(item_taxables_amount),
|
|
|
|
|
|
});
|
2025-10-12 18:36:33 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-13 17:41:21 +00:00
|
|
|
|
const total = taxable_amount.add(taxes_amount);
|
2025-10-12 18:36:33 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
2025-10-13 17:41:21 +00:00
|
|
|
|
subtotal_amount: toNum(subtotal_amount),
|
|
|
|
|
|
discount_amount: toNum(discount_amount),
|
|
|
|
|
|
header_discount_amount: toNum(header_discount),
|
|
|
|
|
|
taxable_amount: toNum(taxable_amount),
|
|
|
|
|
|
taxes_amount: toNum(taxes_amount),
|
|
|
|
|
|
taxes_summary: taxesSummary,
|
2025-10-12 18:36:33 +00:00
|
|
|
|
total_amount: toNum(total),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|