Uecko_ERP/modules/customer-invoices/src/web/domain/calculate-invoice-item-amounts.ts

98 lines
3.2 KiB
TypeScript
Raw Normal View History

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),
};
}