Uecko_ERP/modules/customer-invoices/src/web/hooks/use-calculate-item-amounts.ts
2025-10-08 15:11:26 +02:00

128 lines
3.9 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 { MoneyDTO, PercentageDTO, TaxCatalogProvider } from "@erp/core";
import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
import * as React from "react";
import { CustomerInvoiceItem } from "../schemas";
/**
* Recalcula todos los importes de una línea usando los hooks de escala.
*/
type UseCalculateItemAmountsParams = {
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)
};
export function useCalculateItemAmounts(params: UseCalculateItemAmountsParams) {
const { locale, currencyCode, taxCatalog, keepNullWhenEmpty } = params;
const {
add,
sub,
multiply,
percentage: moneyPct,
fromNumber,
toNumber,
isEmptyMoneyDTO,
fallbackCurrency,
} = useMoney();
const { toNumber: qtyToNumber } = useQuantity();
const { toNumber: pctToNumber } = usePercentage();
// 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]
);
const emptyAmountDTO = React.useMemo(
() => ({
value: "",
scale: "4",
currency_code: currencyCode,
}),
[]
);
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);
// Línea “vacía”: mantener null si se pide y no hay datos
const isEmptyLine = qty === 0 && (!unit || toNumber(unit) === 0);
if (isEmptyLine && keepNullWhenEmpty) {
return {
...item,
subtotal_amount: emptyAmountDTO,
discount_amount: emptyAmountDTO,
taxable_amount: emptyAmountDTO,
taxes_amount: emptyAmountDTO,
total_amount: emptyAmountDTO,
};
}
// 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,
]
);
}