Uecko_ERP/modules/customer-invoices/src/web/hooks/use-calculate-item-amounts.ts

128 lines
3.9 KiB
TypeScript
Raw Normal View History

2025-10-07 16:38:03 +00:00
import { MoneyDTO, PercentageDTO, TaxCatalogProvider } from "@erp/core";
2025-10-06 17:40:37 +00:00
import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
2025-10-07 16:38:03 +00:00
import * as React from "react";
import { CustomerInvoiceItem } from "../schemas";
2025-10-06 17:40:37 +00:00
/**
* Recalcula todos los importes de una línea usando los hooks de escala.
*/
2025-10-08 13:11:26 +00:00
type UseCalculateItemAmountsParams = {
2025-10-07 16:38:03 +00:00
locale: string;
currencyCode: string;
keepNullWhenEmpty?: boolean; // Mantener todos los importes a null cuando la línea está “vacía” (qty+unit vacíos)
taxCatalog: TaxCatalogProvider; // Catálogo de impuestos (inyectable para test)
};
2025-10-06 17:40:37 +00:00
2025-10-08 13:11:26 +00:00
export function useCalculateItemAmounts(params: UseCalculateItemAmountsParams) {
const { locale, currencyCode, taxCatalog, keepNullWhenEmpty } = params;
2025-10-06 17:40:37 +00:00
2025-10-07 16:38:03 +00:00
const {
add,
sub,
multiply,
percentage: moneyPct,
fromNumber,
toNumber,
isEmptyMoneyDTO,
fallbackCurrency,
} = useMoney();
const { toNumber: qtyToNumber } = useQuantity();
const { toNumber: pctToNumber } = usePercentage();
2025-10-06 17:40:37 +00:00
2025-10-07 16:38:03 +00:00
// Crea un MoneyDTO "cero" con la misma divisa/escala que unit_amount
const zeroOf = React.useCallback(
(unit?: MoneyDTO | null): MoneyDTO => {
const cur = unit?.currency_code ?? fallbackCurrency;
const sc = Number(unit?.scale ?? 2);
return fromNumber(0, cur as any, sc);
},
[fromNumber, fallbackCurrency]
);
2025-10-06 17:40:37 +00:00
2025-10-07 16:38:03 +00:00
const emptyAmountDTO = React.useMemo(
() => ({
value: "",
scale: "4",
currency_code: currencyCode,
}),
[]
);
2025-10-06 17:40:37 +00:00
2025-10-07 16:38:03 +00:00
return React.useCallback(
(item: CustomerInvoiceItem): CustomerInvoiceItem => {
const qty = qtyToNumber(item.quantity); // 0 si vacío
const unit = item.unit_amount && !isEmptyMoneyDTO(item.unit_amount) ? item.unit_amount : null;
const zero = zeroOf(unit ?? undefined);
2025-10-06 17:40:37 +00:00
2025-10-07 16:38:03 +00:00
// Línea “vacía”: mantener null si se pide y no hay datos
const isEmptyLine = qty === 0 && (!unit || toNumber(unit) === 0);
if (isEmptyLine && keepNullWhenEmpty) {
2025-10-06 17:40:37 +00:00
return {
2025-10-07 16:38:03 +00:00
...item,
subtotal_amount: emptyAmountDTO,
discount_amount: emptyAmountDTO,
taxable_amount: emptyAmountDTO,
taxes_amount: emptyAmountDTO,
total_amount: emptyAmountDTO,
2025-10-06 17:40:37 +00:00
};
2025-10-07 16:38:03 +00:00
}
// 1) Subtotal = qty × unit
const subtotal = unit ? multiply(unit, qty) : zero;
// 2) Descuento = subtotal × (discount_percentage / 100)
const pctDTO = item.discount_percentage ?? ({ value: "", scale: "" } as PercentageDTO);
const pct = pctToNumber(pctDTO); // 0 si vacío
const discountAmount = pct !== 0 ? moneyPct(subtotal, Math.abs(pct)) : zero;
// 3) Base imponible = subtotal - descuento
const baseAmount = sub(subtotal, discountAmount);
// 4) Impuestos
const taxesBreakdown =
(item.tax_codes
?.map((code) => {
const maybe = taxCatalog.findByCode(code);
if (maybe.isNone()) {
console.warn(`[useCalculateItemAmounts] Tax code not found: "${code}"`);
return null;
}
const tax = maybe.unwrap()!; // { name, value, scale }
const p = pctToNumber({ value: tax.value, scale: tax.scale }); // ej. 21
return moneyPct(baseAmount, p);
})
.filter(Boolean) as MoneyDTO[]) ?? [];
const taxesTotal = taxesBreakdown.length
? taxesBreakdown.reduce((acc, m) => add(acc, m), zero)
: zero;
// 5) Total = base + impuestos
const total = add(baseAmount, taxesTotal);
return {
...item,
subtotal_amount: subtotal,
discount_amount: discountAmount,
taxable_amount: baseAmount,
taxes_amount: taxesTotal,
total_amount: total,
};
},
[
qtyToNumber,
pctToNumber,
multiply,
moneyPct,
add,
sub,
isEmptyMoneyDTO,
zeroOf,
toNumber,
keepNullWhenEmpty,
taxCatalog,
]
);
2025-10-06 17:40:37 +00:00
}