98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import { TaxCatalogProvider, TaxItemType } from "@erp/core";
|
||
import { toDinero, toNum } from "./calculate-utils";
|
||
|
||
export interface InvoiceItemCalcInput {
|
||
quantity?: string; // p.ej. "3.5"
|
||
unit_amount?: string; // p.ej. "125.75"
|
||
discount_percentage?: string; // p.ej. "10" (=> 10%)
|
||
header_discount_percentage?: string; // p.ej. "5" (=> 5%)
|
||
tax_codes: string[]; // ["iva_21", ...]
|
||
}
|
||
|
||
export type InvoiceItemTaxSummary = TaxItemType & {
|
||
taxable_amount: number;
|
||
taxes_amount: number;
|
||
};
|
||
|
||
export interface InvoiceItemCalcResult {
|
||
subtotal_amount: number;
|
||
discount_amount: number;
|
||
header_discount_amount: number;
|
||
taxable_amount: number;
|
||
taxes_amount: number;
|
||
taxes_summary: InvoiceItemTaxSummary[];
|
||
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 defaultScale = 4;
|
||
const taxesSummary: InvoiceItemTaxSummary[] = [];
|
||
|
||
const qty = Number.parseFloat(item.quantity || "0") || 0;
|
||
const unit = Number.parseFloat(item.unit_amount || "0") || 0;
|
||
const iten_pct = Number.parseFloat(item.discount_percentage || "0") || 0;
|
||
const header_pct = Number.parseFloat(item.header_discount_percentage || "0") || 0;
|
||
|
||
// Subtotal = cantidad × precio unitario
|
||
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);
|
||
|
||
// Descuento de la cabecera = subtotal con dto de línea × (header_pct / 100)
|
||
const header_discount = subtotal_w_discount_amount.percentage(header_pct);
|
||
|
||
// Base imponible
|
||
const taxable_amount = subtotal_w_discount_amount.subtract(header_discount);
|
||
|
||
// Impuestos acumulados con signo
|
||
let taxes_amount = toDinero(0, defaultScale, currency);
|
||
for (const code of item.tax_codes ?? []) {
|
||
const tax = taxCatalog.findByCode(code);
|
||
if (tax.isNone()) continue;
|
||
|
||
tax.map((taxItem) => {
|
||
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),
|
||
});
|
||
});
|
||
}
|
||
|
||
const total = taxable_amount.add(taxes_amount);
|
||
|
||
return {
|
||
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,
|
||
total_amount: toNum(total),
|
||
};
|
||
}
|