Uecko_ERP/modules/customer-invoices/src/web/hooks/calcs/use-invoice-auto-recalc.ts
2025-10-11 18:44:38 +02:00

179 lines
5.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 { 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,
]);
}