179 lines
5.9 KiB
TypeScript
179 lines
5.9 KiB
TypeScript
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,
|
||
]);
|
||
}
|