Uecko_ERP/modules/customer-invoices/src/web/hooks/calcs/use-invoice-auto-recalc.ts

179 lines
5.9 KiB
TypeScript
Raw Normal View History

2025-10-11 16:44:38 +00:00
import { areMoneyDTOEqual } from "@erp/core";
import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
import * as React from "react";
import { UseFormReturn } from "react-hook-form";
import { CustomerInvoiceFormData, CustomerInvoiceItemFormData } from "../../schemas";
/**
* Hook que recalcula automáticamente los totales de cada línea
* y los totales generales de la factura cuando cambian los valores relevantes.
*/
export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData>) {
const {
watch,
setValue,
getValues,
formState: { isDirty, isLoading, isSubmitting },
} = form;
const moneyHelper = useMoney();
const qtyHelper = useQuantity();
const pctHelper = usePercentage();
// Cálculo de una línea
const calculateItemTotals = React.useCallback(
(item: CustomerInvoiceItemFormData) => {
if (!item) {
const zero = moneyHelper.fromNumber(0);
return {
subtotalDTO: zero,
discountAmountDTO: zero,
taxableBaseDTO: zero,
taxesDTO: zero,
totalDTO: zero,
};
}
// Subtotal = unit_amount × quantity
const subtotalDTO = moneyHelper.multiply(item.unit_amount, qtyHelper.toNumber(item.quantity));
// Descuento = subtotal × (discount_percentage / 100)
const discountDTO = moneyHelper.percentage(
subtotalDTO,
pctHelper.toNumber(item.discount_percentage)
);
// Base imponible = subtotal descuento
const taxableBaseDTO = moneyHelper.sub(subtotalDTO, discountDTO);
// Impuestos (placeholder: se integrará con tax catalog)
const taxesDTO = moneyHelper.fromNumber(0);
// Total = base imponible + impuestos
const totalDTO = moneyHelper.add(taxableBaseDTO, taxesDTO);
return {
subtotalDTO,
discountAmountDTO: discountDTO,
taxableBaseDTO,
taxesDTO,
totalDTO,
};
},
[moneyHelper, qtyHelper, pctHelper]
);
// Cálculo de los totales de la factura a partir de los conceptos
const calculateInvoiceTotals = React.useCallback(
(items: CustomerInvoiceItemFormData[]) => {
let subtotalDTO = moneyHelper.fromNumber(0);
let discountTotalDTO = moneyHelper.fromNumber(0);
let taxableBaseDTO = moneyHelper.fromNumber(0);
let taxesDTO = moneyHelper.fromNumber(0);
let totalDTO = moneyHelper.fromNumber(0);
for (const item of items) {
const t = calculateItemTotals(item);
subtotalDTO = moneyHelper.add(subtotalDTO, t.subtotalDTO);
discountTotalDTO = moneyHelper.add(discountTotalDTO, t.discountAmountDTO);
taxableBaseDTO = moneyHelper.add(taxableBaseDTO, t.taxableBaseDTO);
taxesDTO = moneyHelper.add(taxesDTO, t.taxesDTO);
totalDTO = moneyHelper.add(totalDTO, t.totalDTO);
}
return {
subtotalDTO,
discountTotalDTO,
taxableBaseDTO,
taxesDTO,
totalDTO,
};
},
[moneyHelper, calculateItemTotals]
);
// Suscribirse a cambios del formulario
React.useEffect(() => {
if (!isDirty || isLoading || isSubmitting) {
return;
}
const subscription = watch((formData, { name, type }) => {
if (!formData?.items?.length) return;
// 1. Si cambia una línea completa (add/remove/move)
if (name === "items" && type === "change") {
formData.items.forEach((item, i) => {
if (!item) return;
const typedItem = item as CustomerInvoiceItemFormData;
const totals = calculateItemTotals(typedItem);
const current = getValues(`items.${i}.total_amount`);
if (!areMoneyDTOEqual(current, totals.totalDTO)) {
setValue(`items.${i}.total_amount`, totals.totalDTO, {
shouldDirty: true,
shouldValidate: false,
});
}
});
// Recalcular importes totales de la factura y
// actualizar valores calculados.
const typedItems = formData.items as CustomerInvoiceItemFormData[];
const totalsGlobal = calculateInvoiceTotals(typedItems);
setValue("subtotal_amount", totalsGlobal.subtotalDTO);
setValue("discount_amount", totalsGlobal.discountTotalDTO);
setValue("taxable_amount", totalsGlobal.taxableBaseDTO);
setValue("taxes_amount", totalsGlobal.taxesDTO);
setValue("total_amount", totalsGlobal.totalDTO);
}
// 2. Si cambia un campo dentro de un concepto
if (name?.startsWith("items.") && type === "change") {
const index = Number(name.split(".")[1]);
const fieldName = name.split(".")[2];
if (["quantity", "unit_amount", "discount_percentage"].includes(fieldName)) {
const typedItem = formData.items[index] as CustomerInvoiceItemFormData;
if (!typedItem) return;
// Recalcular línea
const totals = calculateItemTotals(typedItem);
const current = getValues(`items.${index}.total_amount`);
if (!areMoneyDTOEqual(current, totals.totalDTO)) {
setValue(`items.${index}.total_amount`, totals.totalDTO, {
shouldDirty: true,
shouldValidate: false,
});
}
// Recalcular importes totales de la factura y
// actualizar valores calculados.
const typedItems = formData.items as CustomerInvoiceItemFormData[];
const totalsGlobal = calculateInvoiceTotals(typedItems);
setValue("subtotal_amount", totalsGlobal.subtotalDTO);
setValue("discount_amount", totalsGlobal.discountTotalDTO);
setValue("taxable_amount", totalsGlobal.taxableBaseDTO);
setValue("taxes_amount", totalsGlobal.taxesDTO);
setValue("total_amount", totalsGlobal.totalDTO);
}
}
});
return () => subscription.unsubscribe();
}, [
watch,
isDirty,
isLoading,
isSubmitting,
setValue,
getValues,
calculateItemTotals,
calculateInvoiceTotals,
]);
}