.
This commit is contained in:
parent
a248e8cdc0
commit
3b77fb7cb8
@ -8,24 +8,6 @@
|
||||
"description": "IVA general. Tipo estándar nacional.",
|
||||
"aeat_code": "01"
|
||||
},
|
||||
{
|
||||
"name": "IVA 18%",
|
||||
"code": "iva_18",
|
||||
"value": "1800",
|
||||
"scale": "2",
|
||||
"group": "IVA",
|
||||
"description": "IVA general. Tipo estándar nacional hasta finales de 2011",
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "IVA 16%",
|
||||
"code": "iva_16",
|
||||
"value": "1600",
|
||||
"scale": "2",
|
||||
"group": "IVA",
|
||||
"description": "IVA general. Tipo estándar nacional hasta finales de 2009.",
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "IVA 10%",
|
||||
"code": "iva_10",
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export * from "./payment-method-options.constants";
|
||||
export * from "./retentions-options.constants";
|
||||
export * from "./taxes-options.constants";
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
export type RetentionPercentageOption = 15 | 7;
|
||||
|
||||
export interface ProformaRetentionsDefinition {
|
||||
retentionPercentage: RetentionPercentageOption;
|
||||
retentionLabel: string;
|
||||
}
|
||||
|
||||
export const PROFORMA_RETENTION_DEFINITIONS: Record<
|
||||
RetentionPercentageOption,
|
||||
ProformaRetentionsDefinition
|
||||
> = {
|
||||
15: {
|
||||
retentionPercentage: 15,
|
||||
retentionLabel: "General",
|
||||
},
|
||||
7: {
|
||||
retentionPercentage: 7,
|
||||
retentionLabel: "Reducido",
|
||||
},
|
||||
};
|
||||
|
||||
export const PROFORMA_RETENTION_OPTIONS = [
|
||||
{ value: "15", label: "15%" },
|
||||
{ value: "7", label: "7%" },
|
||||
];
|
||||
@ -0,0 +1,37 @@
|
||||
export type TaxPercentageOption = 21 | 10 | 4 | 0;
|
||||
|
||||
export interface ProformaTaxesDefinition {
|
||||
taxPercentage: TaxPercentageOption;
|
||||
taxLabel: string;
|
||||
recPercentage: number;
|
||||
}
|
||||
|
||||
export const PROFORMA_TAX_DEFINITIONS: Record<TaxPercentageOption, ProformaTaxesDefinition> = {
|
||||
21: {
|
||||
taxPercentage: 21,
|
||||
taxLabel: "General",
|
||||
recPercentage: 5.2,
|
||||
},
|
||||
10: {
|
||||
taxPercentage: 10,
|
||||
taxLabel: "Reducido",
|
||||
recPercentage: 1.4,
|
||||
},
|
||||
4: {
|
||||
taxPercentage: 4,
|
||||
taxLabel: "Superreducido",
|
||||
recPercentage: 0.5,
|
||||
},
|
||||
0: {
|
||||
taxPercentage: 0,
|
||||
taxLabel: "Exento",
|
||||
recPercentage: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const PROFORMA_TAX_OPTIONS = [
|
||||
{ value: "0", label: "0%" },
|
||||
{ value: "4", label: "4%" },
|
||||
{ value: "10", label: "10%" },
|
||||
{ value: "21", label: "21%" },
|
||||
];
|
||||
@ -12,11 +12,11 @@ export interface ProformaTaxSummary {
|
||||
ivaAmount: number;
|
||||
|
||||
recCode: string | null;
|
||||
recPercentage: number;
|
||||
recPercentage: number | null;
|
||||
recAmount: number;
|
||||
|
||||
retentionCode: string | null;
|
||||
retentionPercentage: number;
|
||||
retentionPercentage: number | null;
|
||||
retentionAmount: number;
|
||||
|
||||
taxesAmount: number;
|
||||
|
||||
@ -4,3 +4,4 @@ export * from "./constants";
|
||||
export * from "./entities";
|
||||
export * from "./hooks";
|
||||
export * from "./ui";
|
||||
export * from "./utils";
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { PROFORMA_TAX_DEFINITIONS, type TaxPercentageOption } from "../constants";
|
||||
|
||||
export const getProformaRecPercentage = (taxPercentage: TaxPercentageOption): number => {
|
||||
return PROFORMA_TAX_DEFINITIONS[taxPercentage].recPercentage;
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
import { PROFORMA_RETENTION_OPTIONS } from "../constants";
|
||||
|
||||
export const getProformaRetentionOptions = () => PROFORMA_RETENTION_OPTIONS;
|
||||
@ -0,0 +1,3 @@
|
||||
import { PROFORMA_TAX_OPTIONS } from "../constants";
|
||||
|
||||
export const getProformaTaxOptions = () => PROFORMA_TAX_OPTIONS;
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./get-proforma-rec-percentage";
|
||||
export * from "./get-proforma-retention-options";
|
||||
export * from "./get-proforma-tax-options";
|
||||
@ -1,3 +1,3 @@
|
||||
export * from "./map-proforma-form-to-commercial-document-lines";
|
||||
export * from "./map-proforma-items-form-to-proforma-line-inpus";
|
||||
export * from "./map-proforma-to-proforma-update-form.adapter";
|
||||
export * from "./map-proforma-to-selected-customer.adapter";
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import type { CommercialDocumentLineInput, ProformaUpdateForm } from "../entities";
|
||||
|
||||
export const mapProformaFormToCommercialDocumentLines = (
|
||||
form: ProformaUpdateForm
|
||||
): CommercialDocumentLineInput[] => {
|
||||
return form.items
|
||||
.filter((item) => item.isValued)
|
||||
.map((item) => ({
|
||||
quantity: item.quantity,
|
||||
unitAmount: item.unitAmount,
|
||||
itemDiscountPercentage: item.itemDiscountPercentage,
|
||||
taxPercentage: item.taxPercentage,
|
||||
recPercentage: item.recPercentage,
|
||||
}));
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import type { ProformaItemUpdateForm, ProformaLineInput } from "../entities";
|
||||
|
||||
/**
|
||||
* A partir de las líneas del formulario de actualización de proforma,
|
||||
* devuelve la información de las lineas de la proforma necesarias
|
||||
* para calcular los totales.
|
||||
* @param form
|
||||
* @returns
|
||||
*/
|
||||
|
||||
interface MapProformaItemFormToProformaLineInputsParams {
|
||||
globalDiscountPercentage: number | null;
|
||||
}
|
||||
|
||||
export const mapProformaItemFormToProformaLineInputs = (
|
||||
items: ProformaItemUpdateForm[],
|
||||
params: MapProformaItemFormToProformaLineInputsParams
|
||||
): ProformaLineInput[] => {
|
||||
return items
|
||||
.filter((item) => item.isValued)
|
||||
.map((item) => ({
|
||||
quantity: item.quantity,
|
||||
unitAmount: item.unitAmount,
|
||||
|
||||
itemDiscountPercentage: item.itemDiscountPercentage,
|
||||
globalDiscountPercentage: params.globalDiscountPercentage,
|
||||
|
||||
taxPercentage: item.taxPercentage,
|
||||
recPercentage: item.recPercentage,
|
||||
}));
|
||||
};
|
||||
@ -48,12 +48,17 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
|
||||
|
||||
taxMode,
|
||||
taxRegimeCode: proformaDefaults.taxRegimeCode,
|
||||
defaultTaxPercentage,
|
||||
hasRecPercentage: hasPositivePercentage(defaultRecPercentage),
|
||||
hasRetention: hasPositivePercentage(defaultRetentionPercentage),
|
||||
retentionPercentage: defaultRetentionPercentage,
|
||||
|
||||
paymentMethod: proforma.paymentMethod ?? "",
|
||||
hasTaxPercentage: PercentageHelper.hasPositivePercentage(defaultTaxPercentage),
|
||||
defaultTaxPercentage: defaultTaxPercentage,
|
||||
|
||||
hasRecPercentage: PercentageHelper.hasPositivePercentage(defaultRecPercentage),
|
||||
defaultRecPercentage: defaultRecPercentage,
|
||||
|
||||
hasRetentionPercentage: PercentageHelper.hasPositivePercentage(defaultRetentionPercentage),
|
||||
defaultRetentionPercentage: defaultRetentionPercentage,
|
||||
|
||||
paymentMethodId: proforma.paymentMethod ?? proformaDefaults.paymentMethodId,
|
||||
|
||||
items: proforma.items.map(mapProformaItemsToProformaItemsUpdateForm),
|
||||
};
|
||||
@ -66,11 +71,17 @@ const getFirstTaxableItem = (items: ProformaItem[]): ProformaItem | undefined =>
|
||||
const inferProformaTaxMode = (items: ProformaItem[]): ProformaTaxMode => {
|
||||
const comparableItems = items.filter((item) => item.isValued);
|
||||
|
||||
const sourceItems = comparableItems.length > 0 ? comparableItems : items;
|
||||
if (comparableItems.length === 0) {
|
||||
return "single";
|
||||
}
|
||||
|
||||
const ivaPercentages = uniqueNumbers(sourceItems.map((item) => item.ivaPercentage));
|
||||
const recPercentages = uniqueNumbers(sourceItems.map((item) => item.recPercentage));
|
||||
const retentionPercentages = uniqueNumbers(sourceItems.map((item) => item.retentionPercentage));
|
||||
const sourceItems = comparableItems;
|
||||
|
||||
const ivaPercentages = uniquePercentageValues(sourceItems.map((item) => item.ivaPercentage));
|
||||
const recPercentages = uniquePercentageValues(sourceItems.map((item) => item.recPercentage));
|
||||
const retentionPercentages = uniquePercentageValues(
|
||||
sourceItems.map((item) => item.retentionPercentage)
|
||||
);
|
||||
|
||||
const hasSingleTaxSetup =
|
||||
ivaPercentages.length <= 1 && recPercentages.length <= 1 && retentionPercentages.length <= 1;
|
||||
@ -78,20 +89,12 @@ const inferProformaTaxMode = (items: ProformaItem[]): ProformaTaxMode => {
|
||||
return hasSingleTaxSetup ? "single" : "perLine";
|
||||
};
|
||||
|
||||
const uniqueNumbers = (values: Array<number | null | undefined>): number[] => {
|
||||
const uniquePercentageValues = (values: Array<number | null | undefined>): number[] => {
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.filter((value): value is number => value !== null && value !== undefined)
|
||||
.map((value) => normalizePercentage(value))
|
||||
.map((value) => PercentageHelper.normalizePercentage(value))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const normalizePercentage = (value: number): number => {
|
||||
return Math.round(value * 10000) / 10000;
|
||||
};
|
||||
|
||||
const hasPositivePercentage = (value: number | null | undefined): boolean => {
|
||||
return PercentageHelper.hasPositivePercentage(value);
|
||||
};
|
||||
|
||||
@ -162,6 +162,8 @@ export const useUpdateProformaController = (
|
||||
|
||||
console.log("Enviando actualización con params:", params);
|
||||
|
||||
return;
|
||||
|
||||
try {
|
||||
// Enviamos cambios al servidor
|
||||
const updated = await mutateAsync(params);
|
||||
|
||||
@ -7,10 +7,11 @@ import {
|
||||
useWatch,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { mapProformaItemFormToProformaLineInputs } from "../adapters";
|
||||
import type { ProformaItemUpdateForm, ProformaUpdateForm } from "../entities";
|
||||
import { buildProformaItemUpdateDefault } from "../utils";
|
||||
import { calculateCommercialDocumentLineAmounts } from "../utils/calculations/calculate-commercial-document-line-amounts";
|
||||
import { calculateCommercialDocumentLinesTotals } from "../utils/calculations/calculate-commercial-document-lines-totals";
|
||||
import { calculateProformaLineTotal } from "../utils/calculations/calculate-proforma-line-total";
|
||||
import { calculateProformaLinesTotals } from "../utils/calculations/calculate-proforma-lines-totals";
|
||||
|
||||
export interface ProformaItemAmounts {
|
||||
subtotal: number;
|
||||
@ -25,7 +26,6 @@ export interface ProformaItemsTotals {
|
||||
}
|
||||
|
||||
export type ProformaItemField = FieldArrayWithId<ProformaUpdateForm, "items", "fieldId">;
|
||||
|
||||
export type ProformaItemError = FieldErrors<ProformaItemUpdateForm>;
|
||||
|
||||
export interface UseUpdateProformaItemsControllerResult {
|
||||
@ -56,10 +56,6 @@ interface UseUpdateProformaItemsControllerParams {
|
||||
form: UseFormReturn<ProformaUpdateForm>;
|
||||
}
|
||||
|
||||
const roundCurrency = (value: number): number => {
|
||||
return Math.round(value * 100) / 100;
|
||||
};
|
||||
|
||||
const normalizeItemPositions = (items: ProformaItemUpdateForm[]): ProformaItemUpdateForm[] => {
|
||||
return items.map((item, index) => ({
|
||||
...item,
|
||||
@ -83,11 +79,7 @@ export const useUpdateProformaItemsController = ({
|
||||
keyName: "fieldId",
|
||||
});
|
||||
|
||||
const watchedItems = useWatch({
|
||||
control,
|
||||
name: "items",
|
||||
});
|
||||
|
||||
const watchedItems = useWatch({ control, name: "items" });
|
||||
const items = React.useMemo(() => watchedItems ?? [], [watchedItems]);
|
||||
|
||||
const replaceItems = React.useCallback(
|
||||
@ -113,9 +105,7 @@ export const useUpdateProformaItemsController = ({
|
||||
(index: number) => {
|
||||
const currentItems = getValues("items") ?? [];
|
||||
|
||||
if (index < 0 || index >= currentItems.length) {
|
||||
return;
|
||||
}
|
||||
if (index < 0 || index >= currentItems.length) return;
|
||||
|
||||
replaceItems(currentItems.filter((_, currentIndex) => currentIndex !== index));
|
||||
},
|
||||
@ -127,9 +117,7 @@ export const useUpdateProformaItemsController = ({
|
||||
const currentItems = getValues("items") ?? [];
|
||||
const item = currentItems[index];
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (!item) return;
|
||||
|
||||
const duplicatedItem: ProformaItemUpdateForm = {
|
||||
...item,
|
||||
@ -149,9 +137,7 @@ export const useUpdateProformaItemsController = ({
|
||||
(index: number) => {
|
||||
const currentItems = getValues("items") ?? [];
|
||||
|
||||
if (index <= 0 || index >= currentItems.length) {
|
||||
return;
|
||||
}
|
||||
if (index <= 0 || index >= currentItems.length) return;
|
||||
|
||||
const nextItems = [...currentItems];
|
||||
[nextItems[index - 1], nextItems[index]] = [nextItems[index], nextItems[index - 1]];
|
||||
@ -165,9 +151,7 @@ export const useUpdateProformaItemsController = ({
|
||||
(index: number) => {
|
||||
const currentItems = getValues("items") ?? [];
|
||||
|
||||
if (index < 0 || index >= currentItems.length - 1) {
|
||||
return;
|
||||
}
|
||||
if (index < 0 || index >= currentItems.length - 1) return;
|
||||
|
||||
const nextItems = [...currentItems];
|
||||
[nextItems[index], nextItems[index + 1]] = [nextItems[index + 1], nextItems[index]];
|
||||
@ -179,16 +163,21 @@ export const useUpdateProformaItemsController = ({
|
||||
|
||||
const getItemAmounts = React.useCallback(
|
||||
(index: number): ProformaItemAmounts => {
|
||||
const amounts = calculateCommercialDocumentLineAmounts(items[index]);
|
||||
const line = mapProformaItemFormToProformaLineInputs([items[index]], {
|
||||
globalDiscountPercentage: null,
|
||||
})[0];
|
||||
|
||||
const amounts = calculateProformaLineTotal(line);
|
||||
|
||||
return {
|
||||
subtotal: amounts.grossAmount,
|
||||
subtotal: amounts.subtotalBeforeDiscounts,
|
||||
itemDiscountAmount: amounts.itemDiscountAmount,
|
||||
total: amounts.taxableBaseBeforeGlobalDiscount,
|
||||
total: amounts.subtotalBeforeGlobalDiscount,
|
||||
};
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
const insertItemAt = React.useCallback(
|
||||
(index: number) => {
|
||||
const currentItems = getValues("items") ?? [];
|
||||
@ -221,7 +210,11 @@ export const useUpdateProformaItemsController = ({
|
||||
);
|
||||
|
||||
const totals = React.useMemo<ProformaItemsTotals>(() => {
|
||||
const lineTotals = calculateCommercialDocumentLinesTotals(items);
|
||||
const lineTotals = calculateProformaLinesTotals(
|
||||
mapProformaItemFormToProformaLineInputs(items, {
|
||||
globalDiscountPercentage: null,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
subtotal: lineTotals.subtotalBeforeDiscounts,
|
||||
@ -231,9 +224,7 @@ export const useUpdateProformaItemsController = ({
|
||||
}, [items]);
|
||||
|
||||
const itemErrors = React.useMemo<ProformaItemError[]>(() => {
|
||||
if (!Array.isArray(errors.items)) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(errors.items)) return [];
|
||||
|
||||
return errors.items.map((error) => error ?? {});
|
||||
}, [errors.items]);
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { type UseFormReturn, useWatch } from "react-hook-form";
|
||||
|
||||
import {
|
||||
type RetentionPercentageOption,
|
||||
type TaxPercentageOption,
|
||||
getProformaRecPercentage,
|
||||
} from "../../shared";
|
||||
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
||||
|
||||
interface UseUpdateProformaTaxControllerParams {
|
||||
@ -10,24 +15,35 @@ interface UseUpdateProformaTaxControllerParams {
|
||||
export interface UseUpdateProformaTaxControllerResult {
|
||||
taxMode: ProformaTaxMode;
|
||||
|
||||
hasTaxPercentage: boolean;
|
||||
defaultTaxPercentage: number | null;
|
||||
|
||||
hasRecPercentage: boolean;
|
||||
hasRetention: boolean;
|
||||
retentionPercentage: number | null;
|
||||
defaultRecPercentage: number | null;
|
||||
|
||||
hasRetentionPercentage: boolean;
|
||||
defaultRetentionPercentage: number | null;
|
||||
|
||||
usesSingleTax: boolean;
|
||||
usesPerLineTax: boolean;
|
||||
|
||||
enablePerLineTaxes: () => void;
|
||||
disablePerLineTaxes: () => void;
|
||||
|
||||
updateDefaultTaxPercentage: (newTaxPercentage: RetentionPercentageOption) => void;
|
||||
updateDefaultRecPercentage: (enabled: boolean) => void;
|
||||
updateDefaultRetentionPercentage: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const getRecPercentage = (taxPercentage: number | null | undefined): number | null => {
|
||||
if (taxPercentage === 21) return 5.2;
|
||||
if (taxPercentage === 10) return 1.4;
|
||||
if (taxPercentage === 4) return 0.5;
|
||||
|
||||
const resolveRecPercentage = (
|
||||
enabled: boolean,
|
||||
taxPercentage: RetentionPercentageOption | null
|
||||
): number | null => {
|
||||
if (!enabled || taxPercentage === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getProformaRecPercentage(taxPercentage as TaxPercentageOption);
|
||||
};
|
||||
|
||||
export const useUpdateProformaTaxController = ({
|
||||
@ -36,17 +52,28 @@ export const useUpdateProformaTaxController = ({
|
||||
const { control, getValues, setValue } = form;
|
||||
|
||||
const taxMode = useWatch({ control, name: "taxMode" });
|
||||
const hasRecPercentage = useWatch({ control, name: "hasRecPercentage" }) ?? false;
|
||||
const hasRetention = useWatch({ control, name: "hasRetention" }) ?? false;
|
||||
const retentionPercentage = useWatch({ control, name: "retentionPercentage" }) ?? null;
|
||||
|
||||
const hasTaxPercentage = useWatch({ control, name: "hasTaxPercentage" }) ?? false;
|
||||
const defaultTaxPercentage = useWatch({ control, name: "defaultTaxPercentage" }) ?? null;
|
||||
|
||||
const hasRecPercentage = useWatch({ control, name: "hasRecPercentage" }) ?? false;
|
||||
const defaultRecPercentage = useWatch({ control, name: "defaultRecPercentage" }) ?? null;
|
||||
|
||||
const hasRetentionPercentage = useWatch({ control, name: "hasRetentionPercentage" }) ?? false;
|
||||
const defaultRetentionPercentage =
|
||||
useWatch({ control, name: "defaultRetentionPercentage" }) ?? null;
|
||||
|
||||
const hasMountedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (taxMode !== "single") return;
|
||||
|
||||
const currentItems = getValues("items") ?? [];
|
||||
const recPercentage = hasRecPercentage ? getRecPercentage(defaultTaxPercentage) : null;
|
||||
|
||||
const nextRecPercentage = resolveRecPercentage(
|
||||
hasRecPercentage,
|
||||
defaultTaxPercentage as RetentionPercentageOption | null
|
||||
);
|
||||
|
||||
const shouldMarkDirty = hasMountedRef.current;
|
||||
|
||||
@ -59,8 +86,8 @@ export const useUpdateProformaTaxController = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (item.recPercentage !== recPercentage) {
|
||||
setValue(`items.${index}.recPercentage`, recPercentage, {
|
||||
if (item.recPercentage !== nextRecPercentage) {
|
||||
setValue(`items.${index}.recPercentage`, nextRecPercentage, {
|
||||
shouldDirty: shouldMarkDirty,
|
||||
shouldTouch: false,
|
||||
shouldValidate: true,
|
||||
@ -87,18 +114,79 @@ export const useUpdateProformaTaxController = ({
|
||||
});
|
||||
}, [setValue]);
|
||||
|
||||
const updateDefaultTaxPercentage = useCallback(
|
||||
(newTaxPercentage: RetentionPercentageOption): void => {
|
||||
setValue("defaultTaxPercentage", newTaxPercentage, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
const isRecPercentageEnabled = getValues("hasRecPercentage");
|
||||
|
||||
if (isRecPercentageEnabled) {
|
||||
setValue(
|
||||
"defaultRecPercentage",
|
||||
getProformaRecPercentage(newTaxPercentage as TaxPercentageOption),
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[getValues, setValue]
|
||||
);
|
||||
|
||||
const updateDefaultRecPercentage = useCallback(
|
||||
(enabled: boolean): void => {
|
||||
setValue("hasRecPercentage", enabled, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
const taxPercentage = getValues("defaultTaxPercentage") as RetentionPercentageOption | null;
|
||||
|
||||
setValue("defaultRecPercentage", resolveRecPercentage(enabled, taxPercentage), {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
},
|
||||
[getValues, setValue]
|
||||
);
|
||||
|
||||
const updateDefaultRetentionPercentage = useCallback(
|
||||
(enabled: boolean): void => {
|
||||
setValue("hasRetentionPercentage", enabled, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
return {
|
||||
taxMode,
|
||||
|
||||
hasTaxPercentage,
|
||||
defaultTaxPercentage,
|
||||
|
||||
hasRecPercentage,
|
||||
hasRetention,
|
||||
retentionPercentage,
|
||||
defaultRecPercentage,
|
||||
|
||||
hasRetentionPercentage,
|
||||
defaultRetentionPercentage,
|
||||
|
||||
usesSingleTax: taxMode === "single",
|
||||
usesPerLineTax: taxMode === "perLine",
|
||||
|
||||
enablePerLineTaxes,
|
||||
disablePerLineTaxes,
|
||||
|
||||
updateDefaultTaxPercentage,
|
||||
updateDefaultRecPercentage,
|
||||
updateDefaultRetentionPercentage,
|
||||
};
|
||||
};
|
||||
|
||||
@ -15,22 +15,22 @@ export interface UseUpdateProformaTotalsControllerResult {
|
||||
export const useUpdateProformaTotalsController = ({
|
||||
form,
|
||||
}: UseUpdateProformaTotalsControllerParams): UseUpdateProformaTotalsControllerResult => {
|
||||
const { control, getValues } = form;
|
||||
const { control } = form;
|
||||
|
||||
const items = useWatch({ control, name: "items" });
|
||||
const globalDiscountPercentage = useWatch({ control, name: "globalDiscountPercentage" });
|
||||
const hasRetention = useWatch({ control, name: "hasRetention" });
|
||||
const retentionPercentage = useWatch({ control, name: "retentionPercentage" });
|
||||
const items = useWatch({ control, name: "items" });
|
||||
|
||||
const hasRetentionPercentage = useWatch({ control, name: "hasRetentionPercentage" });
|
||||
const retentionPercentage = useWatch({ control, name: "defaultRetentionPercentage" });
|
||||
|
||||
const totals = useMemo(() => {
|
||||
return calculateProformaTotals({
|
||||
...getValues(),
|
||||
items,
|
||||
globalDiscountPercentage,
|
||||
hasRetention,
|
||||
retentionPercentage,
|
||||
items: items ?? [],
|
||||
hasRetentionPercentage: hasRetentionPercentage ?? false,
|
||||
retentionPercentage: retentionPercentage ?? null,
|
||||
});
|
||||
}, [items, globalDiscountPercentage, hasRetention, retentionPercentage]);
|
||||
}, [globalDiscountPercentage, items, hasRetentionPercentage, retentionPercentage]);
|
||||
|
||||
return { totals };
|
||||
};
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
export interface CommercialDocumentLineInput {
|
||||
quantity: number | null;
|
||||
unitAmount: number | null;
|
||||
itemDiscountPercentage: number | null;
|
||||
taxPercentage: number | null;
|
||||
|
||||
recPercentage: number | null;
|
||||
}
|
||||
|
||||
export interface CommercialDocumentLineAmounts {
|
||||
grossAmount: number;
|
||||
itemDiscountAmount: number;
|
||||
taxableBaseBeforeGlobalDiscount: number;
|
||||
}
|
||||
|
||||
export interface CommercialDocumentTaxBreakdownLine {
|
||||
taxPercentage: number;
|
||||
taxableBase: number;
|
||||
taxAmount: number;
|
||||
}
|
||||
|
||||
export interface CommercialDocumentTotals {
|
||||
subtotalBeforeDiscounts: number;
|
||||
|
||||
lineDiscountTotal: number;
|
||||
globalDiscountPercentage: number;
|
||||
globalDiscountAmount: number;
|
||||
|
||||
taxableBase: number;
|
||||
|
||||
taxBreakdown: CommercialDocumentTaxBreakdownLine[];
|
||||
taxTotal: number;
|
||||
|
||||
total: number;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
export * from "./commercial-document-calculation.entity";
|
||||
export * from "./proforma-calculation.entity";
|
||||
export * from "./proforma-item-update-form.entity";
|
||||
export * from "./proforma-item-update-form.schema";
|
||||
export * from "./proforma-item-update-patch.entity";
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
export interface ProformaLineInput {
|
||||
quantity: number | null;
|
||||
unitAmount: number | null;
|
||||
|
||||
itemDiscountPercentage: number | null;
|
||||
globalDiscountPercentage: number | null;
|
||||
|
||||
taxPercentage: number | null;
|
||||
recPercentage: number | null;
|
||||
}
|
||||
|
||||
export interface ProformaLineTotal {
|
||||
subtotalBeforeDiscounts: number;
|
||||
itemDiscountAmount: number;
|
||||
subtotalBeforeGlobalDiscount: number;
|
||||
globalDiscountAmount: number;
|
||||
totalDiscountAmount: number;
|
||||
}
|
||||
|
||||
export interface ProformaTaxBreakdown {
|
||||
taxPercentage: number;
|
||||
taxableBase: number;
|
||||
taxAmount: number;
|
||||
recPercentage: number | null;
|
||||
recAmount: number;
|
||||
}
|
||||
|
||||
export interface ProformaTotals {
|
||||
subtotalBeforeDiscounts: number;
|
||||
|
||||
lineDiscountTotal: number;
|
||||
|
||||
globalDiscountPercentage: number;
|
||||
globalDiscountAmount: number;
|
||||
totalDiscountAmount: number;
|
||||
|
||||
taxableBase: number;
|
||||
|
||||
taxBreakdown: ProformaTaxBreakdown[];
|
||||
|
||||
taxTotal: number;
|
||||
|
||||
recTotal: number;
|
||||
|
||||
retentionPercentage: number | null;
|
||||
retentionAmount: number;
|
||||
|
||||
total: number;
|
||||
}
|
||||
@ -36,13 +36,17 @@ export interface ProformaUpdateForm {
|
||||
|
||||
taxMode: ProformaTaxMode;
|
||||
taxRegimeCode: string | null;
|
||||
|
||||
hasTaxPercentage: boolean;
|
||||
defaultTaxPercentage: number | null;
|
||||
|
||||
hasRecPercentage: boolean;
|
||||
hasRetention: boolean;
|
||||
retentionPercentage: number | null;
|
||||
defaultRecPercentage: number | null;
|
||||
|
||||
paymentMethod: string;
|
||||
hasRetentionPercentage: boolean;
|
||||
defaultRetentionPercentage: number | null;
|
||||
|
||||
paymentMethodId: string | null;
|
||||
|
||||
items: ProformaItemUpdateForm[];
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { PercentageHelper } from "@repo/rdx-utils";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema";
|
||||
@ -16,7 +17,8 @@ import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema
|
||||
* - sin detalles impuestos por el widget
|
||||
*/
|
||||
|
||||
export const ProformaUpdateFormSchema = z.object({
|
||||
export const ProformaUpdateFormSchema = z
|
||||
.object({
|
||||
series: z.string(),
|
||||
|
||||
invoiceDate: z.string().min(1),
|
||||
@ -35,15 +37,35 @@ export const ProformaUpdateFormSchema = z.object({
|
||||
|
||||
taxMode: z.enum(["single", "perLine"]),
|
||||
taxRegimeCode: z.string(),
|
||||
|
||||
hasTaxPercentage: z.boolean(),
|
||||
defaultTaxPercentage: z.number().nullable(),
|
||||
|
||||
hasRecPercentage: z.boolean(),
|
||||
hasRetention: z.boolean(),
|
||||
retentionPercentage: z.number().nullable(),
|
||||
defaultRecPercentage: z.number().nullable(),
|
||||
|
||||
paymentMethod: z.string(),
|
||||
hasRetentionPercentage: z.boolean(),
|
||||
defaultRetentionPercentage: z.number().nullable(),
|
||||
|
||||
paymentMethodId: z.string().nullable(),
|
||||
|
||||
items: z.array(ProformaItemUpdateFormSchema).min(1),
|
||||
});
|
||||
})
|
||||
.refine(
|
||||
(formValues) => {
|
||||
if (
|
||||
formValues.hasRetentionPercentage &&
|
||||
!PercentageHelper.hasPositivePercentage(formValues.defaultRetentionPercentage)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Retention percentage is required when retention percentage is enabled",
|
||||
path: ["defaultRetentionPercentage"],
|
||||
}
|
||||
);
|
||||
|
||||
export type ProformaUpdateFormSchemaType = z.infer<typeof ProformaUpdateFormSchema>;
|
||||
|
||||
@ -1,27 +1,17 @@
|
||||
import type { ProformaHeaderTotals } from "./proforma-calculation.entity";
|
||||
|
||||
export interface ProformaTaxBreakdownLine {
|
||||
taxPercentage: number;
|
||||
taxableBase: number;
|
||||
taxAmount: number;
|
||||
ivaPercentage: number;
|
||||
ivaAmount: number;
|
||||
recPercentage: number | null;
|
||||
equivalenceSurchargeAmount: number;
|
||||
recAmount: number;
|
||||
}
|
||||
|
||||
export interface ProformaTotals {
|
||||
subtotalBeforeDiscounts: number;
|
||||
|
||||
lineDiscountTotal: number;
|
||||
globalDiscountPercentage: number;
|
||||
globalDiscountAmount: number;
|
||||
|
||||
taxableBase: number;
|
||||
|
||||
taxBreakdown: ProformaTaxBreakdownLine[];
|
||||
taxTotal: number;
|
||||
|
||||
equivalenceSurchargeTotal: number;
|
||||
export interface ProformaTotals extends ProformaHeaderTotals {
|
||||
recPercentage: number | null;
|
||||
recAmount: number;
|
||||
|
||||
retentionPercentage: number | null;
|
||||
retentionAmount: number;
|
||||
|
||||
total: number;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
AmountField,
|
||||
CheckboxField,
|
||||
LineDescriptionField,
|
||||
PercentageField,
|
||||
QuantityField,
|
||||
@ -63,6 +64,12 @@ export const ProformaLineEditor = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns: LineEditorColumn<ProformaItemField>[] = [
|
||||
{
|
||||
id: "isValued",
|
||||
header: t("form_fields.items.description.is_valued", "¿Valorado?"),
|
||||
headClassName: "w-[100px] text-right",
|
||||
cell: ({ index }) => <CheckboxField name={`items.${index}.isValued`} />,
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
header: t("form_fields.items.description.label", "Descripción"),
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { FormSectionCard, FormSectionGrid, PercentageField } from "@repo/rdx-ui/components";
|
||||
import { FormSectionCard, FormSectionGrid } from "@repo/rdx-ui/components";
|
||||
import { MoneyHelper, PercentageHelper } from "@repo/rdx-utils";
|
||||
import { Separator } from "@repo/shadcn-ui/components";
|
||||
import { Card, CardContent, CardHeader, CardTitle, Separator } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { CurrencyIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaTotals } from "../../entities";
|
||||
@ -10,25 +12,22 @@ interface ProformaTotalsSummaryProps {
|
||||
totals: ProformaTotals;
|
||||
currency?: string;
|
||||
|
||||
showEquivalenceSurcharge?: boolean;
|
||||
showRec?: boolean;
|
||||
showRetention?: boolean;
|
||||
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
globalDiscountField?: ReactNode;
|
||||
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ProformaTotalsSummary = ({
|
||||
totals,
|
||||
currency = "EUR",
|
||||
|
||||
showEquivalenceSurcharge = false,
|
||||
showRec = false,
|
||||
showRetention = false,
|
||||
|
||||
globalDiscountField,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
|
||||
className,
|
||||
}: ProformaTotalsSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
@ -41,6 +40,13 @@ export const ProformaTotalsSummary = ({
|
||||
return PercentageHelper.formatPercent(value);
|
||||
};
|
||||
|
||||
const retentionLabel =
|
||||
totals.retentionPercentage === null
|
||||
? t("proformas.update.totals.retentionAmount", "Total retenciones")
|
||||
: `${t("proformas.update.totals.retentionAmount", "Total retenciones")} ${formatPercent(
|
||||
totals.retentionPercentage
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<FormSectionCard
|
||||
className={className}
|
||||
@ -49,40 +55,42 @@ export const ProformaTotalsSummary = ({
|
||||
icon={<CurrencyIcon className="size-5" />}
|
||||
title={t("proformas.update.totals.title", "Totales")}
|
||||
>
|
||||
<FormSectionGrid className="md:grid-cols-1 space-y-5">
|
||||
<FormSectionGrid className="gap-5 md:grid-cols-1">
|
||||
<TotalsRow
|
||||
label={t("proformas.update.totals.subtotalBeforeDiscounts", "Subtotal sin descuentos")}
|
||||
label={t("proformas.update.totals.subtotalBeforeDiscounts", "Total de las líneas")}
|
||||
value={formatMoney(totals.subtotalBeforeDiscounts)}
|
||||
/>
|
||||
|
||||
<section className="space-y-3 rounded-lg border bg-background p-4">
|
||||
<h3 className="text-sm font-medium">
|
||||
{t("proformas.update.totals.discounts", "Descuentos")}
|
||||
</h3>
|
||||
<Card
|
||||
className={cn(disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary")}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("proformas.update.totals.discounts", "Descuentos")}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
<TotalsRow
|
||||
label={t("proformas.update.totals.lineDiscountTotal", "Descuento en líneas")}
|
||||
value={`-${formatMoney(totals.lineDiscountTotal)}`}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_10rem] items-end gap-3">
|
||||
<PercentageField
|
||||
disabled={disabled}
|
||||
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
|
||||
name="globalDiscountPercentage"
|
||||
readOnly={readOnly}
|
||||
{globalDiscountField ? <div>{globalDiscountField}</div> : null}
|
||||
|
||||
<TotalsRow
|
||||
description={formatPercent(totals.globalDiscountPercentage)}
|
||||
label={t("proformas.update.totals.globalDiscountAmount", "Descuento global")}
|
||||
value={`-${formatMoney(totals.globalDiscountAmount)}`}
|
||||
/>
|
||||
|
||||
<div className="pb-1 text-right">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("proformas.update.totals.globalDiscountAmount", "Importe descuento global")}
|
||||
</div>
|
||||
<div className="font-mono text-sm tabular-nums">
|
||||
-{formatMoney(totals.globalDiscountAmount)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Separator className="my-4" />
|
||||
|
||||
<TotalsRow
|
||||
label={t("proformas.update.totals.totalDiscountAmount", "Total de descuentos")}
|
||||
strong
|
||||
value={`-${formatMoney(totals.totalDiscountAmount)}`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<TotalsRow
|
||||
label={t("proformas.update.totals.taxableBase", "Base imponible")}
|
||||
@ -90,71 +98,68 @@ export const ProformaTotalsSummary = ({
|
||||
value={formatMoney(totals.taxableBase)}
|
||||
/>
|
||||
|
||||
<section className="space-y-3 rounded-lg border bg-background p-4">
|
||||
<h3 className="text-sm font-medium">{t("proformas.update.totals.taxes", "Impuestos")}</h3>
|
||||
<Card className="bg-muted text-muted-foreground">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("proformas.update.totals.taxes", "Impuestos")}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{totals.taxBreakdown.length > 0 ? (
|
||||
totals.taxBreakdown.map((tax) => (
|
||||
<div className="space-y-2" key={tax.taxPercentage}>
|
||||
totals.taxBreakdown.map((tax) => {
|
||||
const key = `${tax.taxPercentage}:${tax.recPercentage ?? "none"}`;
|
||||
|
||||
return (
|
||||
<div className="space-y-2" key={key}>
|
||||
<TotalsRow
|
||||
description={formatMoney(tax.taxableBase)}
|
||||
label={`${t("proformas.update.totals.taxPercentage", "IVA")} ${formatPercent(
|
||||
tax.taxPercentage
|
||||
)}`}
|
||||
label={`${t(
|
||||
"proformas.update.totals.taxPercentage",
|
||||
"Impuesto"
|
||||
)} ${formatPercent(tax.taxPercentage)}`}
|
||||
value={formatMoney(tax.taxAmount)}
|
||||
/>
|
||||
|
||||
{showEquivalenceSurcharge && tax.recPercentage !== null ? (
|
||||
{showRec && tax.recPercentage !== null ? (
|
||||
<TotalsRow
|
||||
description={formatMoney(tax.taxableBase)}
|
||||
label={`${t(
|
||||
"proformas.update.totals.equivalenceSurcharge",
|
||||
"proformas.update.totals.recPercentage",
|
||||
"Recargo equivalencia"
|
||||
)} ${formatPercent(tax.recPercentage)}`}
|
||||
value={formatMoney(tax.equivalenceSurchargeAmount)}
|
||||
value={formatMoney(tax.recAmount)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("proformas.update.totals.noTaxes", "Sin impuestos aplicados")}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
<TotalsRow
|
||||
label={t("proformas.update.totals.taxTotal", "Total IVA")}
|
||||
label={t("proformas.update.totals.taxTotal", "Total impuestos")}
|
||||
strong
|
||||
value={formatMoney(totals.taxTotal)}
|
||||
/>
|
||||
|
||||
{showEquivalenceSurcharge ? (
|
||||
{showRec ? (
|
||||
<TotalsRow
|
||||
label={t(
|
||||
"proformas.update.totals.equivalenceSurchargeTotal",
|
||||
"Total recargo equivalencia"
|
||||
)}
|
||||
label={t("proformas.update.totals.recTotal", "Total recargo equivalencia")}
|
||||
strong
|
||||
value={formatMoney(totals.equivalenceSurchargeTotal)}
|
||||
value={formatMoney(totals.recTotal)}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{showRetention ? (
|
||||
<section className="space-y-3 rounded-lg border bg-background p-4">
|
||||
<h3 className="text-sm font-medium">
|
||||
{t("proformas.update.totals.retention", "Retención")}
|
||||
</h3>
|
||||
|
||||
<TotalsRow
|
||||
label={`${t("proformas.update.totals.retentionPercentage", "IRPF")} ${formatPercent(
|
||||
totals.retentionPercentage ?? 0
|
||||
)}`}
|
||||
label={retentionLabel}
|
||||
strong
|
||||
value={`-${formatMoney(totals.retentionAmount)}`}
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<Separator />
|
||||
@ -163,6 +168,7 @@ export const ProformaTotalsSummary = ({
|
||||
<span className="text-sm font-semibold">
|
||||
{t("proformas.update.totals.total", "Total factura")}
|
||||
</span>
|
||||
|
||||
<span className="font-mono text-xl font-bold tabular-nums">
|
||||
{formatMoney(totals.total)}
|
||||
</span>
|
||||
@ -175,15 +181,21 @@ export const ProformaTotalsSummary = ({
|
||||
interface TotalsRowProps {
|
||||
label: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
description?: string;
|
||||
strong?: boolean;
|
||||
}
|
||||
|
||||
const TotalsRow = ({ label, value, description, strong = false }: TotalsRowProps) => {
|
||||
const TotalsRow = ({ label, value, description, className, strong = false }: TotalsRowProps) => {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className={strong ? "text-sm font-semibold" : "text-sm text-muted-foreground"}>
|
||||
<div className={cn("flex items-start justify-between gap-4", className)}>
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm",
|
||||
strong ? "font-bold text-foreground" : "font-medium text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
|
||||
@ -191,9 +203,10 @@ const TotalsRow = ({ label, value, description, strong = false }: TotalsRowProps
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
strong ? "font-mono text-sm font-semibold tabular-nums" : "font-mono text-sm tabular-nums"
|
||||
}
|
||||
className={cn(
|
||||
"shrink-0 text-right font-mono tabular-nums",
|
||||
strong ? "text-base font-bold" : "text-sm font-medium"
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
|
||||
@ -19,7 +19,11 @@ import {
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { FileText } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
|
||||
export const ProformaTaxesCard = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className="shadow">
|
||||
<CardHeader className="px-6 pb-7 pt-3">
|
||||
@ -79,13 +83,16 @@ export const ProformaTaxesCard = () => {
|
||||
className="cursor-pointer font-medium text-primary-700"
|
||||
htmlFor="same-vat"
|
||||
>
|
||||
Aplicar el mismo IVA a todas las líneas de la proforma
|
||||
{t(
|
||||
"proformas.update.taxes.disable_per_line",
|
||||
"Aplicar el mismo IVA a todas las líneas de la proforma"
|
||||
)}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
|
||||
<Field className="w-32">
|
||||
<FieldLabel className="text-primary-700" htmlFor="default-vat">
|
||||
IVA por defecto
|
||||
{t("form_fields.proformas.default_tax_percentage.label", "IVA por defecto")}
|
||||
</FieldLabel>
|
||||
|
||||
<Select defaultValue="21">
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx
|
||||
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { PercentageField } from "@repo/rdx-ui/components";
|
||||
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
|
||||
@ -75,21 +76,29 @@ export const ProformaUpdateEditorForm = ({
|
||||
|
||||
<ProformaUpdateItemsEditor disabled={isSubmitting} itemsCtrl={itemsCtrl} taxCtrl={taxCtrl} />
|
||||
|
||||
<div className="grid grid-cols-1">
|
||||
<ProformaTaxesCard />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
|
||||
<ProformaUpdateTaxEditor className="md:col-span-6" taxCtrl={taxCtrl} />
|
||||
<ProformaTotalsSummary
|
||||
className="md:col-span-6"
|
||||
currency={currencyCode}
|
||||
globalDiscountField={
|
||||
<PercentageField
|
||||
disabled={isSubmitting}
|
||||
showEquivalenceSurcharge={taxCtrl.hasRecPercentage}
|
||||
showRetention={taxCtrl.hasRetention}
|
||||
inputClassName="bg-background"
|
||||
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
|
||||
name="globalDiscountPercentage"
|
||||
/>
|
||||
}
|
||||
showRec={taxCtrl.hasRecPercentage}
|
||||
showRetention={taxCtrl.hasRetentionPercentage}
|
||||
totals={totalsCtrl.totals}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1">
|
||||
<ProformaTaxesCard />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">
|
||||
<Button disabled={isSubmitting} onClick={onReset} type="button" variant="outline">
|
||||
{t("common.reset", "Restablecer")}
|
||||
|
||||
@ -1,25 +1,29 @@
|
||||
import {
|
||||
CheckboxField,
|
||||
FormSectionCard,
|
||||
FormSectionGrid,
|
||||
PercentageField,
|
||||
SelectField,
|
||||
SwitchField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { PercentageHelper } from "@repo/rdx-utils";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
Checkbox,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
Label,
|
||||
Switch,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { ReceiptTextIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { getProformaRetentionOptions, getProformaTaxOptions } from "../../../shared";
|
||||
import type { UseUpdateProformaTaxControllerResult } from "../../controllers";
|
||||
|
||||
interface ProformaUpdateTaxEditorProps {
|
||||
@ -130,41 +134,38 @@ export const ProformaUpdateTaxEditor = ({
|
||||
<Card
|
||||
className={cn(disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary")}
|
||||
>
|
||||
<CardContent>
|
||||
<FieldSet>
|
||||
<FieldLegend className="font-semibold">Configuración de IVA</FieldLegend>
|
||||
<FieldDescription>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{" "}
|
||||
{t(
|
||||
"proformas.update.taxes.disable_per_line",
|
||||
"Mismo IVA en todas las líneas de la proforma"
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Puedes usar un tipo único para todos las líneas de detalle o permitir que cada línea
|
||||
tenga su propio IVA.
|
||||
</FieldDescription>
|
||||
|
||||
<FormSectionGrid>
|
||||
<Field className="md:col-span-12 md:col-start-1" orientation="horizontal">
|
||||
<Checkbox
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Switch
|
||||
checked={taxCtrl.usesSingleTax}
|
||||
className="not-disabled:cursor-pointer"
|
||||
disabled={disabled || readOnly}
|
||||
onCheckedChange={(checked) =>
|
||||
checked ? taxCtrl.disablePerLineTaxes() : taxCtrl.enablePerLineTaxes()
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="terms-checkbox">
|
||||
{t(
|
||||
"proformas.update.taxes.disable_per_line",
|
||||
"Aplicar el mismo IVA a todas las líneas de la proforma"
|
||||
)}
|
||||
</Label>
|
||||
</Field>
|
||||
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldSet>
|
||||
<FormSectionGrid>
|
||||
<SelectField
|
||||
className="md:col-span-4 md:col-start-1"
|
||||
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
||||
disabled={disabled}
|
||||
items={[
|
||||
{ value: "0", label: "0%" },
|
||||
{ value: "4", label: "4%" },
|
||||
{ value: "10", label: "10%" },
|
||||
{ value: "21", label: "21%" },
|
||||
]}
|
||||
inputClassName="bg-background"
|
||||
items={getProformaTaxOptions()}
|
||||
label={t("form_fields.proformas.default_tax_percentage.label", "IVA por defecto")}
|
||||
name="defaultTaxPercentage"
|
||||
placeholder={t(
|
||||
@ -175,47 +176,65 @@ export const ProformaUpdateTaxEditor = ({
|
||||
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||
/>
|
||||
</FormSectionGrid>
|
||||
</FieldSet>{" "}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</FieldSet>
|
||||
<FieldSeparator className="my-4" />
|
||||
<FieldSet>
|
||||
<FieldLegend>Impuestos adicionales</FieldLegend>
|
||||
<FieldDescription>
|
||||
Activa opciones fiscales adicionales como recargo de equivalencia o retenciones (IRPF),
|
||||
según el tipo de cliente y normativa aplicable.
|
||||
Activa opciones fiscales adicionales como recargo de equivalencia o retenciones
|
||||
(IRPF), según el tipo de cliente y normativa aplicable.
|
||||
</FieldDescription>
|
||||
|
||||
<FormSectionGrid>
|
||||
<CheckboxField
|
||||
<SwitchField
|
||||
className="md:col-span-12 md:col-start-1"
|
||||
disabled={disabled}
|
||||
label={t(
|
||||
label={
|
||||
<>
|
||||
{t(
|
||||
"form_fields.proformas.has_equivalence_surcharge.label",
|
||||
"Recargo de equivalencia"
|
||||
)}
|
||||
|
||||
{taxCtrl.hasRecPercentage ? (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{PercentageHelper.formatPercent(taxCtrl.defaultRecPercentage ?? 0)}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
name="hasRecPercentage"
|
||||
onCheckedChange={taxCtrl.updateDefaultRecPercentage}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
<CheckboxField
|
||||
<SwitchField
|
||||
className="md:col-span-12 md:col-start-1"
|
||||
disabled={disabled}
|
||||
label={t("form_fields.proformas.has_retention.label", "Incluir retención/IRPF")}
|
||||
name="hasRetention"
|
||||
name="hasRetentionPercentage"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
<PercentageField
|
||||
<SelectField
|
||||
className="md:col-span-4 md:col-start-1"
|
||||
disabled={disabled}
|
||||
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
||||
disabled={disabled || !taxCtrl.hasRetentionPercentage}
|
||||
inputClassName="bg-background"
|
||||
items={getProformaRetentionOptions()}
|
||||
label={t("form_fields.proformas.retention_percentage.label", "Retención")}
|
||||
name="retentionPercentage"
|
||||
readOnly={readOnly}
|
||||
name="defaultRetentionPercentage"
|
||||
placeholder={t(
|
||||
"form_fields.proformas.default_tax_percentage.placeholder",
|
||||
"Selecciona IVA"
|
||||
)}
|
||||
readOnly={readOnly || taxCtrl.usesPerLineTax}
|
||||
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||
/>
|
||||
</FormSectionGrid>
|
||||
</FieldSet>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</FormSectionCard>
|
||||
);
|
||||
};
|
||||
|
||||
@ -14,7 +14,7 @@ export const buildProformaItemUpdateDefault = (position: number): ProformaItemUp
|
||||
unitAmount: null,
|
||||
itemDiscountPercentage: null,
|
||||
|
||||
taxPercentage: null,
|
||||
ivaPercentage: null,
|
||||
recPercentage: null,
|
||||
};
|
||||
};
|
||||
|
||||
@ -20,14 +20,17 @@ export const buildProformaUpdateDefault = (): ProformaUpdateForm => {
|
||||
|
||||
taxMode: "single",
|
||||
taxRegimeCode: "01",
|
||||
|
||||
hasTaxPercentage: true,
|
||||
defaultTaxPercentage: 21,
|
||||
|
||||
hasRecPercentage: false,
|
||||
hasRetention: false,
|
||||
defaultRecPercentage: null,
|
||||
|
||||
retentionPercentage: null,
|
||||
hasRetentionPercentage: false,
|
||||
defaultRetentionPercentage: 15,
|
||||
|
||||
paymentMethod: "",
|
||||
paymentMethodId: null,
|
||||
|
||||
items: [],
|
||||
};
|
||||
|
||||
@ -1,13 +1,36 @@
|
||||
// update/utils/calculate-proforma-totals.ts
|
||||
import { mapProformaItemFormToProformaLineInputs } from "../adapters";
|
||||
import type { ProformaItemUpdateForm, ProformaTotals, ProformaUpdateForm } from "../entities";
|
||||
|
||||
import { mapProformaFormToCommercialDocumentLines } from "../adapters";
|
||||
import type { ProformaTotals, ProformaUpdateForm } from "../entities";
|
||||
import { calculateProformaTotalsFromLines } from "./calculations";
|
||||
|
||||
import { calculateCommercialDocumentTotals } from "./calculations";
|
||||
export interface CalculateProformaTotalsParams {
|
||||
globalDiscountPercentage: ProformaUpdateForm["globalDiscountPercentage"];
|
||||
|
||||
export const calculateProformaTotals = (form: ProformaUpdateForm): ProformaTotals => {
|
||||
return calculateCommercialDocumentTotals({
|
||||
lines: mapProformaFormToCommercialDocumentLines(form),
|
||||
globalDiscountPercentage: form.globalDiscountPercentage,
|
||||
items: ProformaItemUpdateForm[];
|
||||
|
||||
hasRetentionPercentage: boolean;
|
||||
|
||||
retentionPercentage: number | null;
|
||||
}
|
||||
|
||||
export const calculateProformaTotals = ({
|
||||
globalDiscountPercentage,
|
||||
|
||||
items,
|
||||
|
||||
hasRetentionPercentage,
|
||||
|
||||
retentionPercentage,
|
||||
}: CalculateProformaTotalsParams): ProformaTotals => {
|
||||
const lines = mapProformaItemFormToProformaLineInputs(items, {
|
||||
globalDiscountPercentage,
|
||||
});
|
||||
|
||||
return calculateProformaTotalsFromLines({
|
||||
lines,
|
||||
|
||||
globalDiscountPercentage,
|
||||
|
||||
retentionPercentage: hasRetentionPercentage ? retentionPercentage : null,
|
||||
});
|
||||
};
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
import type { ProformaLineInput, ProformaLineTotal } from "../../entities";
|
||||
|
||||
import { percentageAmount, roundMoney, toCalculationNumber } from "./money-calculation";
|
||||
|
||||
export const calculateProformaLineTotal = (line?: ProformaLineInput): ProformaLineTotal => {
|
||||
if (!line) {
|
||||
return {
|
||||
subtotalBeforeDiscounts: 0,
|
||||
itemDiscountAmount: 0,
|
||||
subtotalBeforeGlobalDiscount: 0,
|
||||
globalDiscountAmount: 0,
|
||||
totalDiscountAmount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const quantity = toCalculationNumber(line.quantity);
|
||||
const unitAmount = toCalculationNumber(line.unitAmount);
|
||||
|
||||
const itemDiscountPercentage = toCalculationNumber(line.itemDiscountPercentage);
|
||||
|
||||
const globalDiscountPercentage = toCalculationNumber(line.globalDiscountPercentage);
|
||||
|
||||
const subtotalBeforeDiscounts = quantity * unitAmount;
|
||||
|
||||
const itemDiscountAmount = percentageAmount(subtotalBeforeDiscounts, itemDiscountPercentage);
|
||||
|
||||
const subtotalBeforeGlobalDiscount = subtotalBeforeDiscounts - itemDiscountAmount;
|
||||
|
||||
const globalDiscountAmount = percentageAmount(
|
||||
subtotalBeforeGlobalDiscount,
|
||||
globalDiscountPercentage
|
||||
);
|
||||
|
||||
const totalDiscountAmount = itemDiscountAmount + globalDiscountAmount;
|
||||
|
||||
return {
|
||||
subtotalBeforeDiscounts: roundMoney(subtotalBeforeDiscounts),
|
||||
|
||||
itemDiscountAmount: roundMoney(itemDiscountAmount),
|
||||
|
||||
subtotalBeforeGlobalDiscount: roundMoney(subtotalBeforeGlobalDiscount),
|
||||
|
||||
globalDiscountAmount: roundMoney(globalDiscountAmount),
|
||||
|
||||
totalDiscountAmount: roundMoney(totalDiscountAmount),
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import type { ProformaLineInput } from "../../entities";
|
||||
|
||||
import { calculateProformaLineTotal } from "./calculate-proforma-line-total";
|
||||
import { roundMoney } from "./money-calculation";
|
||||
|
||||
export interface ProformaLinesTotals {
|
||||
subtotalBeforeDiscounts: number;
|
||||
lineDiscountTotal: number;
|
||||
taxableBaseBeforeGlobalDiscount: number;
|
||||
globalDiscountAmount: number;
|
||||
totalDiscountAmount: number;
|
||||
}
|
||||
|
||||
export const calculateProformaLinesTotals = (lines: ProformaLineInput[]): ProformaLinesTotals => {
|
||||
return lines.reduce<ProformaLinesTotals>(
|
||||
(acc, line) => {
|
||||
const totals = calculateProformaLineTotal(line);
|
||||
|
||||
return {
|
||||
subtotalBeforeDiscounts: roundMoney(
|
||||
acc.subtotalBeforeDiscounts + totals.subtotalBeforeDiscounts
|
||||
),
|
||||
|
||||
lineDiscountTotal: roundMoney(acc.lineDiscountTotal + totals.itemDiscountAmount),
|
||||
|
||||
taxableBaseBeforeGlobalDiscount: roundMoney(
|
||||
acc.taxableBaseBeforeGlobalDiscount + totals.subtotalBeforeGlobalDiscount
|
||||
),
|
||||
|
||||
globalDiscountAmount: roundMoney(acc.globalDiscountAmount + totals.globalDiscountAmount),
|
||||
|
||||
totalDiscountAmount: roundMoney(acc.totalDiscountAmount + totals.totalDiscountAmount),
|
||||
};
|
||||
},
|
||||
{
|
||||
subtotalBeforeDiscounts: 0,
|
||||
lineDiscountTotal: 0,
|
||||
taxableBaseBeforeGlobalDiscount: 0,
|
||||
globalDiscountAmount: 0,
|
||||
totalDiscountAmount: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,80 @@
|
||||
import type { ProformaLineInput, ProformaTaxBreakdown } from "../../entities";
|
||||
|
||||
import { calculateProformaLineTotal } from "./calculate-proforma-line-total";
|
||||
import { calculateProportionalDiscount } from "./calculate-proportional-discount";
|
||||
import { percentageAmount, roundMoney, toCalculationNumber } from "./money-calculation";
|
||||
|
||||
interface CalculateProformaTaxBreakdownParams {
|
||||
lines: ProformaLineInput[];
|
||||
|
||||
taxableBaseBeforeGlobalDiscount: number;
|
||||
|
||||
globalDiscountAmount: number;
|
||||
}
|
||||
|
||||
interface TaxBucket {
|
||||
taxPercentage: number;
|
||||
|
||||
recPercentage: number | null;
|
||||
|
||||
taxableBase: number;
|
||||
|
||||
taxAmount: number;
|
||||
|
||||
recAmount: number;
|
||||
}
|
||||
|
||||
const buildTaxBreakdownKey = (taxPercentage: number, recPercentage: number | null): string => {
|
||||
return `${taxPercentage}:${recPercentage ?? "none"}`;
|
||||
};
|
||||
|
||||
export const calculateProformaTaxBreakdown = ({
|
||||
lines,
|
||||
taxableBaseBeforeGlobalDiscount,
|
||||
globalDiscountAmount,
|
||||
}: CalculateProformaTaxBreakdownParams): ProformaTaxBreakdown[] => {
|
||||
const buckets = new Map<string, TaxBucket>();
|
||||
|
||||
for (const line of lines) {
|
||||
const lineTotals = calculateProformaLineTotal(line);
|
||||
|
||||
const proportionalGlobalDiscount = calculateProportionalDiscount(
|
||||
lineTotals.subtotalBeforeGlobalDiscount,
|
||||
taxableBaseBeforeGlobalDiscount,
|
||||
globalDiscountAmount
|
||||
);
|
||||
|
||||
const lineTaxableBase = lineTotals.subtotalBeforeGlobalDiscount - proportionalGlobalDiscount;
|
||||
|
||||
const taxPercentage = toCalculationNumber(line.taxPercentage);
|
||||
|
||||
const recPercentage =
|
||||
line.recPercentage === null ? null : toCalculationNumber(line.recPercentage);
|
||||
|
||||
const taxAmount = percentageAmount(lineTaxableBase, taxPercentage);
|
||||
|
||||
const recAmount = recPercentage === null ? 0 : percentageAmount(lineTaxableBase, recPercentage);
|
||||
|
||||
const key = buildTaxBreakdownKey(taxPercentage, recPercentage);
|
||||
|
||||
const current = buckets.get(key);
|
||||
|
||||
buckets.set(key, {
|
||||
taxPercentage,
|
||||
recPercentage,
|
||||
taxableBase: (current?.taxableBase ?? 0) + lineTaxableBase,
|
||||
taxAmount: (current?.taxAmount ?? 0) + taxAmount,
|
||||
recAmount: (current?.recAmount ?? 0) + recAmount,
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(buckets.values())
|
||||
.sort((a, b) => a.taxPercentage - b.taxPercentage)
|
||||
.map((bucket) => ({
|
||||
taxPercentage: bucket.taxPercentage,
|
||||
taxableBase: roundMoney(bucket.taxableBase),
|
||||
taxAmount: roundMoney(bucket.taxAmount),
|
||||
recPercentage: bucket.recPercentage,
|
||||
recAmount: roundMoney(bucket.recAmount),
|
||||
}));
|
||||
};
|
||||
@ -0,0 +1,76 @@
|
||||
import type { ProformaLineInput, ProformaTotals } from "../../entities";
|
||||
|
||||
import { calculateProformaLinesTotals } from "./calculate-proforma-lines-totals";
|
||||
import { calculateProformaTaxBreakdown } from "./calculate-proforma-tax-breakdown";
|
||||
import { percentageAmount, roundMoney, toCalculationNumber } from "./money-calculation";
|
||||
|
||||
interface CalculateProformaTotalsFromLinesParams {
|
||||
lines: ProformaLineInput[];
|
||||
|
||||
globalDiscountPercentage: number | null;
|
||||
|
||||
retentionPercentage: number | null;
|
||||
}
|
||||
|
||||
export const calculateProformaTotalsFromLines = ({
|
||||
lines,
|
||||
globalDiscountPercentage,
|
||||
retentionPercentage,
|
||||
}: CalculateProformaTotalsFromLinesParams): ProformaTotals => {
|
||||
const normalizedGlobalDiscountPercentage = toCalculationNumber(globalDiscountPercentage);
|
||||
|
||||
const lineTotals = calculateProformaLinesTotals(lines);
|
||||
|
||||
const globalDiscountAmount = percentageAmount(
|
||||
lineTotals.taxableBaseBeforeGlobalDiscount,
|
||||
normalizedGlobalDiscountPercentage
|
||||
);
|
||||
|
||||
const taxableBase = lineTotals.taxableBaseBeforeGlobalDiscount - globalDiscountAmount;
|
||||
|
||||
const taxBreakdown = calculateProformaTaxBreakdown({
|
||||
lines,
|
||||
|
||||
taxableBaseBeforeGlobalDiscount: lineTotals.taxableBaseBeforeGlobalDiscount,
|
||||
|
||||
globalDiscountAmount,
|
||||
});
|
||||
|
||||
const taxTotal = taxBreakdown.reduce((acc, item) => acc + item.taxAmount, 0);
|
||||
|
||||
const recTotal = taxBreakdown.reduce((acc, item) => acc + item.recAmount, 0);
|
||||
|
||||
const normalizedRetentionPercentage =
|
||||
retentionPercentage === null ? null : toCalculationNumber(retentionPercentage);
|
||||
|
||||
const retentionAmount =
|
||||
normalizedRetentionPercentage === null
|
||||
? 0
|
||||
: percentageAmount(taxableBase, normalizedRetentionPercentage);
|
||||
|
||||
return {
|
||||
subtotalBeforeDiscounts: roundMoney(lineTotals.subtotalBeforeDiscounts),
|
||||
|
||||
lineDiscountTotal: roundMoney(lineTotals.lineDiscountTotal),
|
||||
|
||||
globalDiscountPercentage: normalizedGlobalDiscountPercentage,
|
||||
|
||||
globalDiscountAmount: roundMoney(globalDiscountAmount),
|
||||
|
||||
totalDiscountAmount: roundMoney(lineTotals.lineDiscountTotal + globalDiscountAmount),
|
||||
|
||||
taxableBase: roundMoney(taxableBase),
|
||||
|
||||
taxBreakdown,
|
||||
|
||||
taxTotal: roundMoney(taxTotal),
|
||||
|
||||
recTotal: roundMoney(recTotal),
|
||||
|
||||
retentionPercentage: normalizedRetentionPercentage,
|
||||
|
||||
retentionAmount: roundMoney(retentionAmount),
|
||||
|
||||
total: roundMoney(taxableBase + taxTotal + recTotal - retentionAmount),
|
||||
};
|
||||
};
|
||||
@ -1 +1 @@
|
||||
export * from "./calculate-commercial-document-totals";
|
||||
export * from "./calculate-proforma-totals-from-lines";
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// update/utils/calculations/money-calculation.ts
|
||||
|
||||
import { NumberHelper } from "@repo/rdx-utils";
|
||||
|
||||
export const toCalculationNumber = (value: number | null | undefined): number => {
|
||||
@ -10,6 +8,9 @@ export const roundMoney = (value: number): number => {
|
||||
return NumberHelper.roundToScale(value, 2);
|
||||
};
|
||||
|
||||
export const percentageAmount = (baseAmount: number, percentage: number): number => {
|
||||
return baseAmount * (percentage / 100);
|
||||
export const percentageAmount = (
|
||||
baseAmount: number,
|
||||
percentage: number | null | undefined
|
||||
): number => {
|
||||
return baseAmount * (toCalculationNumber(percentage) / 100);
|
||||
};
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
export * from "../issued-invoices/adapters/issued-invoice-resume-dto.adapter";
|
||||
export * from "../proformas/adapters/proforma-dto.adapter";
|
||||
|
||||
export * from "./invoice.form.schema";
|
||||
export * from "./invoice-resume.form.schema";
|
||||
export * from "./invoices.api.schema";
|
||||
@ -1,23 +0,0 @@
|
||||
export type InvoiceSummaryFormData = CustomerInvoiceSummary & {
|
||||
subtotal_amount_fmt: string;
|
||||
subtotal_amount: number;
|
||||
|
||||
discount_percentage_fmt: string;
|
||||
discount_percentage: number;
|
||||
|
||||
discount_amount_fmt: string;
|
||||
discount_amount: number;
|
||||
|
||||
taxable_amount_fmt: string;
|
||||
taxable_amount: number;
|
||||
|
||||
taxes_amoun_fmt: string;
|
||||
taxes_amount: number;
|
||||
|
||||
total_amount_fmt: string;
|
||||
total_amount: number;
|
||||
};
|
||||
|
||||
export type InvoicesPageFormData = CustomerInvoicesPage & {
|
||||
items: InvoiceSummaryFormData[];
|
||||
};
|
||||
@ -1,122 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const InvoiceItemFormSchema = z.object({
|
||||
description: z.string().max(2000).optional().default(""),
|
||||
quantity: z.any(), //NumericStringSchema.optional(),
|
||||
unit_amount: z.any(), //NumericStringSchema.optional(),
|
||||
|
||||
subtotal_amount: z.any(), //z.number(),
|
||||
discount_percentage: z.any(), //NumericStringSchema.optional(),
|
||||
discount_amount: z.number(),
|
||||
taxable_amount: z.number(),
|
||||
|
||||
tax_codes: z.array(z.string()).default([]),
|
||||
|
||||
taxes_amount: z.number(),
|
||||
total_amount: z.number(),
|
||||
});
|
||||
|
||||
export const InvoiceFormSchema = z.object({
|
||||
invoice_number: z.string().optional(),
|
||||
series: z.string().optional(),
|
||||
|
||||
invoice_date: z.string().optional(),
|
||||
operation_date: z.string().optional(),
|
||||
|
||||
customer_id: z.string().optional(),
|
||||
recipient: z
|
||||
.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
tin: z.string().optional(),
|
||||
street: z.string().optional(),
|
||||
street2: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
province: z.string().optional(),
|
||||
postal_code: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
reference: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
|
||||
language_code: z
|
||||
.string({
|
||||
error: "El idioma es obligatorio",
|
||||
})
|
||||
.min(1, "Debe indicar un idioma")
|
||||
.toUpperCase() // asegura mayúsculas
|
||||
.default("es"),
|
||||
|
||||
currency_code: z
|
||||
.string({
|
||||
error: "La moneda es obligatoria",
|
||||
})
|
||||
.min(1, "La moneda no puede estar vacía")
|
||||
.toUpperCase() // asegura mayúsculas
|
||||
.default("EUR"),
|
||||
|
||||
taxes: z
|
||||
.array(
|
||||
z.object({
|
||||
tax_code: z.string(),
|
||||
tax_label: z.string(),
|
||||
taxable_amount: z.number(),
|
||||
taxes_amount: z.number(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
|
||||
items: z.array(InvoiceItemFormSchema).optional(),
|
||||
|
||||
subtotal_amount: z.number(),
|
||||
items_discount_amount: z.number(),
|
||||
discount_percentage: z.number(),
|
||||
discount_amount: z.number(),
|
||||
taxable_amount: z.number(),
|
||||
taxes_amount: z.number(),
|
||||
total_amount: z.number(),
|
||||
});
|
||||
|
||||
export type InvoiceFormData = z.infer<typeof InvoiceFormSchema>;
|
||||
export type InvoiceItemFormData = z.infer<typeof InvoiceItemFormSchema>;
|
||||
|
||||
export const defaultCustomerInvoiceItemFormData: InvoiceItemFormData = {
|
||||
description: "",
|
||||
quantity: "",
|
||||
unit_amount: "",
|
||||
subtotal_amount: 0,
|
||||
discount_percentage: "",
|
||||
discount_amount: 0,
|
||||
taxable_amount: 0,
|
||||
tax_codes: ["iva_21"],
|
||||
taxes_amount: 0,
|
||||
total_amount: 0,
|
||||
};
|
||||
|
||||
export const defaultCustomerInvoiceFormData: InvoiceFormData = {
|
||||
invoice_number: "",
|
||||
series: "",
|
||||
|
||||
invoice_date: "",
|
||||
operation_date: "",
|
||||
|
||||
reference: "",
|
||||
description: "",
|
||||
notes: "",
|
||||
|
||||
language_code: "es",
|
||||
currency_code: "EUR",
|
||||
|
||||
items: [],
|
||||
|
||||
subtotal_amount: 0,
|
||||
items_discount_amount: 0,
|
||||
discount_amount: 0,
|
||||
discount_percentage: 0,
|
||||
taxable_amount: 0,
|
||||
taxes_amount: 0,
|
||||
total_amount: 0,
|
||||
};
|
||||
@ -1,23 +0,0 @@
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { GetIssuedInvoiceByIdResponseSchema, ListIssuedInvoicesResponseSchema } from "../../common";
|
||||
|
||||
export const IssuedInvoiceschema = GetIssuedInvoiceByIdResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssueInvoice = z.infer<typeof IssuedInvoiceschema>;
|
||||
export type IssueInvoiceRecipient = IssueInvoice["recipient"];
|
||||
export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>;
|
||||
|
||||
// Resultado de consulta con criteria (paginado, etc.)
|
||||
export const IssuedInvoicesPageSchema = ListIssuedInvoicesResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
//export type PaginatedResponse = z.infer<typeof PaginationSchema>;
|
||||
//export type CustomerInvoicesPage = z.infer<typeof CustomerInvoicesPageSchema>;
|
||||
|
||||
// Ítem simplificado dentro del listado (no toda la entidad)
|
||||
//export type CustomerInvoiceSummary = Omit<ArrayElement<CustomerInvoicesPage["items"]>, "metadata">;
|
||||
@ -14,7 +14,7 @@ import { FormFieldLabel } from "./form-field-label.tsx";
|
||||
type CheckboxFieldProps<TFormValues extends FieldValues> = {
|
||||
name: FieldPath<TFormValues>;
|
||||
|
||||
label: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
reserveDescriptionSpace?: boolean;
|
||||
|
||||
|
||||
@ -9,5 +9,6 @@ export * from "./multi-select-field.tsx";
|
||||
export * from "./radio-group-field.tsx";
|
||||
export * from "./select-field.tsx";
|
||||
export * from "./semantic-fields/index.ts";
|
||||
export * from "./switch-field.tsx";
|
||||
export * from "./text-area-field.tsx";
|
||||
export * from "./text-field.tsx";
|
||||
|
||||
107
packages/rdx-ui/src/components/form/switch-field.tsx
Normal file
107
packages/rdx-ui/src/components/form/switch-field.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
// packages/rdx-ui/src/components/form/switch-field.tsx
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
Switch,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import * as React from "react";
|
||||
import { Controller, type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormFieldLabel } from "./form-field-label.tsx";
|
||||
|
||||
type SwitchFieldProps<TFormValues extends FieldValues> = {
|
||||
name: FieldPath<TFormValues>;
|
||||
|
||||
label?: string | React.ReactNode;
|
||||
description?: string;
|
||||
reserveDescriptionSpace?: boolean;
|
||||
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
|
||||
orientation?: "vertical" | "horizontal" | "responsive";
|
||||
|
||||
className?: string;
|
||||
inputClassName?: string;
|
||||
};
|
||||
|
||||
export const SwitchField = <TFormValues extends FieldValues>({
|
||||
name,
|
||||
|
||||
label,
|
||||
description,
|
||||
reserveDescriptionSpace = false,
|
||||
|
||||
onCheckedChange,
|
||||
|
||||
disabled = false,
|
||||
required = false,
|
||||
readOnly = false,
|
||||
|
||||
orientation = "horizontal",
|
||||
|
||||
className,
|
||||
inputClassName,
|
||||
}: SwitchFieldProps<TFormValues>) => {
|
||||
const { control, formState } = useFormContext<TFormValues>();
|
||||
|
||||
const inputId = React.useId();
|
||||
const descriptionId = description ? `${inputId}-description` : undefined;
|
||||
|
||||
const isDisabled = disabled || readOnly || formState.isSubmitting;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field, fieldState }) => {
|
||||
const hasError = Boolean(fieldState.error);
|
||||
|
||||
return (
|
||||
<Field
|
||||
className={cn("gap-2", className)}
|
||||
data-invalid={hasError}
|
||||
orientation={orientation}
|
||||
>
|
||||
<Switch
|
||||
aria-describedby={descriptionId}
|
||||
aria-invalid={hasError || undefined}
|
||||
aria-required={required || undefined}
|
||||
checked={field.value === true}
|
||||
className={cn(inputClassName)}
|
||||
disabled={isDisabled}
|
||||
id={inputId}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onCheckedChange={(checked) =>
|
||||
onCheckedChange ? onCheckedChange(checked) : field.onChange(checked)
|
||||
}
|
||||
ref={field.ref}
|
||||
required={required}
|
||||
/>
|
||||
|
||||
<FieldContent className="gap-1">
|
||||
<FormFieldLabel className="font-normal" htmlFor={inputId} required={required}>
|
||||
{label}
|
||||
</FormFieldLabel>
|
||||
|
||||
{description ? (
|
||||
<FieldDescription id={descriptionId}>{description}</FieldDescription>
|
||||
) : reserveDescriptionSpace ? (
|
||||
<div aria-hidden="true" className="min-h-5" />
|
||||
) : null}
|
||||
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</FieldContent>
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -10,7 +10,12 @@ const hasPositivePercentage = (value: number | null | undefined): boolean => {
|
||||
return value !== null && value !== undefined && value > 0;
|
||||
};
|
||||
|
||||
const normalizePercentage = (value: number): number => {
|
||||
return Math.round(value * 10000) / 10000;
|
||||
};
|
||||
|
||||
export const PercentageHelper = {
|
||||
formatPercent,
|
||||
hasPositivePercentage,
|
||||
normalizePercentage,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user