Uecko_ERP/modules/customer-invoices/src/web/domain/calculate-invoice-item-amounts.ts
2025-10-13 19:41:21 +02:00

98 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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