.
This commit is contained in:
parent
a248e8cdc0
commit
3b77fb7cb8
@ -8,24 +8,6 @@
|
|||||||
"description": "IVA general. Tipo estándar nacional.",
|
"description": "IVA general. Tipo estándar nacional.",
|
||||||
"aeat_code": "01"
|
"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%",
|
"name": "IVA 10%",
|
||||||
"code": "iva_10",
|
"code": "iva_10",
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
export * from "./payment-method-options.constants";
|
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;
|
ivaAmount: number;
|
||||||
|
|
||||||
recCode: string | null;
|
recCode: string | null;
|
||||||
recPercentage: number;
|
recPercentage: number | null;
|
||||||
recAmount: number;
|
recAmount: number;
|
||||||
|
|
||||||
retentionCode: string | null;
|
retentionCode: string | null;
|
||||||
retentionPercentage: number;
|
retentionPercentage: number | null;
|
||||||
retentionAmount: number;
|
retentionAmount: number;
|
||||||
|
|
||||||
taxesAmount: number;
|
taxesAmount: number;
|
||||||
|
|||||||
@ -4,3 +4,4 @@ export * from "./constants";
|
|||||||
export * from "./entities";
|
export * from "./entities";
|
||||||
export * from "./hooks";
|
export * from "./hooks";
|
||||||
export * from "./ui";
|
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-proforma-update-form.adapter";
|
||||||
export * from "./map-proforma-to-selected-customer.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,
|
taxMode,
|
||||||
taxRegimeCode: proformaDefaults.taxRegimeCode,
|
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),
|
items: proforma.items.map(mapProformaItemsToProformaItemsUpdateForm),
|
||||||
};
|
};
|
||||||
@ -66,11 +71,17 @@ const getFirstTaxableItem = (items: ProformaItem[]): ProformaItem | undefined =>
|
|||||||
const inferProformaTaxMode = (items: ProformaItem[]): ProformaTaxMode => {
|
const inferProformaTaxMode = (items: ProformaItem[]): ProformaTaxMode => {
|
||||||
const comparableItems = items.filter((item) => item.isValued);
|
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 sourceItems = comparableItems;
|
||||||
const recPercentages = uniqueNumbers(sourceItems.map((item) => item.recPercentage));
|
|
||||||
const retentionPercentages = uniqueNumbers(sourceItems.map((item) => item.retentionPercentage));
|
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 =
|
const hasSingleTaxSetup =
|
||||||
ivaPercentages.length <= 1 && recPercentages.length <= 1 && retentionPercentages.length <= 1;
|
ivaPercentages.length <= 1 && recPercentages.length <= 1 && retentionPercentages.length <= 1;
|
||||||
@ -78,20 +89,12 @@ const inferProformaTaxMode = (items: ProformaItem[]): ProformaTaxMode => {
|
|||||||
return hasSingleTaxSetup ? "single" : "perLine";
|
return hasSingleTaxSetup ? "single" : "perLine";
|
||||||
};
|
};
|
||||||
|
|
||||||
const uniqueNumbers = (values: Array<number | null | undefined>): number[] => {
|
const uniquePercentageValues = (values: Array<number | null | undefined>): number[] => {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
values
|
values
|
||||||
.filter((value): value is number => value !== null && value !== undefined)
|
.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);
|
console.log("Enviando actualización con params:", params);
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enviamos cambios al servidor
|
// Enviamos cambios al servidor
|
||||||
const updated = await mutateAsync(params);
|
const updated = await mutateAsync(params);
|
||||||
|
|||||||
@ -7,10 +7,11 @@ import {
|
|||||||
useWatch,
|
useWatch,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
|
||||||
|
import { mapProformaItemFormToProformaLineInputs } from "../adapters";
|
||||||
import type { ProformaItemUpdateForm, ProformaUpdateForm } from "../entities";
|
import type { ProformaItemUpdateForm, ProformaUpdateForm } from "../entities";
|
||||||
import { buildProformaItemUpdateDefault } from "../utils";
|
import { buildProformaItemUpdateDefault } from "../utils";
|
||||||
import { calculateCommercialDocumentLineAmounts } from "../utils/calculations/calculate-commercial-document-line-amounts";
|
import { calculateProformaLineTotal } from "../utils/calculations/calculate-proforma-line-total";
|
||||||
import { calculateCommercialDocumentLinesTotals } from "../utils/calculations/calculate-commercial-document-lines-totals";
|
import { calculateProformaLinesTotals } from "../utils/calculations/calculate-proforma-lines-totals";
|
||||||
|
|
||||||
export interface ProformaItemAmounts {
|
export interface ProformaItemAmounts {
|
||||||
subtotal: number;
|
subtotal: number;
|
||||||
@ -25,7 +26,6 @@ export interface ProformaItemsTotals {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ProformaItemField = FieldArrayWithId<ProformaUpdateForm, "items", "fieldId">;
|
export type ProformaItemField = FieldArrayWithId<ProformaUpdateForm, "items", "fieldId">;
|
||||||
|
|
||||||
export type ProformaItemError = FieldErrors<ProformaItemUpdateForm>;
|
export type ProformaItemError = FieldErrors<ProformaItemUpdateForm>;
|
||||||
|
|
||||||
export interface UseUpdateProformaItemsControllerResult {
|
export interface UseUpdateProformaItemsControllerResult {
|
||||||
@ -56,10 +56,6 @@ interface UseUpdateProformaItemsControllerParams {
|
|||||||
form: UseFormReturn<ProformaUpdateForm>;
|
form: UseFormReturn<ProformaUpdateForm>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roundCurrency = (value: number): number => {
|
|
||||||
return Math.round(value * 100) / 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeItemPositions = (items: ProformaItemUpdateForm[]): ProformaItemUpdateForm[] => {
|
const normalizeItemPositions = (items: ProformaItemUpdateForm[]): ProformaItemUpdateForm[] => {
|
||||||
return items.map((item, index) => ({
|
return items.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
@ -83,11 +79,7 @@ export const useUpdateProformaItemsController = ({
|
|||||||
keyName: "fieldId",
|
keyName: "fieldId",
|
||||||
});
|
});
|
||||||
|
|
||||||
const watchedItems = useWatch({
|
const watchedItems = useWatch({ control, name: "items" });
|
||||||
control,
|
|
||||||
name: "items",
|
|
||||||
});
|
|
||||||
|
|
||||||
const items = React.useMemo(() => watchedItems ?? [], [watchedItems]);
|
const items = React.useMemo(() => watchedItems ?? [], [watchedItems]);
|
||||||
|
|
||||||
const replaceItems = React.useCallback(
|
const replaceItems = React.useCallback(
|
||||||
@ -113,9 +105,7 @@ export const useUpdateProformaItemsController = ({
|
|||||||
(index: number) => {
|
(index: number) => {
|
||||||
const currentItems = getValues("items") ?? [];
|
const currentItems = getValues("items") ?? [];
|
||||||
|
|
||||||
if (index < 0 || index >= currentItems.length) {
|
if (index < 0 || index >= currentItems.length) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceItems(currentItems.filter((_, currentIndex) => currentIndex !== index));
|
replaceItems(currentItems.filter((_, currentIndex) => currentIndex !== index));
|
||||||
},
|
},
|
||||||
@ -127,9 +117,7 @@ export const useUpdateProformaItemsController = ({
|
|||||||
const currentItems = getValues("items") ?? [];
|
const currentItems = getValues("items") ?? [];
|
||||||
const item = currentItems[index];
|
const item = currentItems[index];
|
||||||
|
|
||||||
if (!item) {
|
if (!item) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicatedItem: ProformaItemUpdateForm = {
|
const duplicatedItem: ProformaItemUpdateForm = {
|
||||||
...item,
|
...item,
|
||||||
@ -149,9 +137,7 @@ export const useUpdateProformaItemsController = ({
|
|||||||
(index: number) => {
|
(index: number) => {
|
||||||
const currentItems = getValues("items") ?? [];
|
const currentItems = getValues("items") ?? [];
|
||||||
|
|
||||||
if (index <= 0 || index >= currentItems.length) {
|
if (index <= 0 || index >= currentItems.length) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextItems = [...currentItems];
|
const nextItems = [...currentItems];
|
||||||
[nextItems[index - 1], nextItems[index]] = [nextItems[index], nextItems[index - 1]];
|
[nextItems[index - 1], nextItems[index]] = [nextItems[index], nextItems[index - 1]];
|
||||||
@ -165,9 +151,7 @@ export const useUpdateProformaItemsController = ({
|
|||||||
(index: number) => {
|
(index: number) => {
|
||||||
const currentItems = getValues("items") ?? [];
|
const currentItems = getValues("items") ?? [];
|
||||||
|
|
||||||
if (index < 0 || index >= currentItems.length - 1) {
|
if (index < 0 || index >= currentItems.length - 1) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextItems = [...currentItems];
|
const nextItems = [...currentItems];
|
||||||
[nextItems[index], nextItems[index + 1]] = [nextItems[index + 1], nextItems[index]];
|
[nextItems[index], nextItems[index + 1]] = [nextItems[index + 1], nextItems[index]];
|
||||||
@ -179,16 +163,21 @@ export const useUpdateProformaItemsController = ({
|
|||||||
|
|
||||||
const getItemAmounts = React.useCallback(
|
const getItemAmounts = React.useCallback(
|
||||||
(index: number): ProformaItemAmounts => {
|
(index: number): ProformaItemAmounts => {
|
||||||
const amounts = calculateCommercialDocumentLineAmounts(items[index]);
|
const line = mapProformaItemFormToProformaLineInputs([items[index]], {
|
||||||
|
globalDiscountPercentage: null,
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
const amounts = calculateProformaLineTotal(line);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subtotal: amounts.grossAmount,
|
subtotal: amounts.subtotalBeforeDiscounts,
|
||||||
itemDiscountAmount: amounts.itemDiscountAmount,
|
itemDiscountAmount: amounts.itemDiscountAmount,
|
||||||
total: amounts.taxableBaseBeforeGlobalDiscount,
|
total: amounts.subtotalBeforeGlobalDiscount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[items]
|
[items]
|
||||||
);
|
);
|
||||||
|
|
||||||
const insertItemAt = React.useCallback(
|
const insertItemAt = React.useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const currentItems = getValues("items") ?? [];
|
const currentItems = getValues("items") ?? [];
|
||||||
@ -221,7 +210,11 @@ export const useUpdateProformaItemsController = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const totals = React.useMemo<ProformaItemsTotals>(() => {
|
const totals = React.useMemo<ProformaItemsTotals>(() => {
|
||||||
const lineTotals = calculateCommercialDocumentLinesTotals(items);
|
const lineTotals = calculateProformaLinesTotals(
|
||||||
|
mapProformaItemFormToProformaLineInputs(items, {
|
||||||
|
globalDiscountPercentage: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subtotal: lineTotals.subtotalBeforeDiscounts,
|
subtotal: lineTotals.subtotalBeforeDiscounts,
|
||||||
@ -231,9 +224,7 @@ export const useUpdateProformaItemsController = ({
|
|||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const itemErrors = React.useMemo<ProformaItemError[]>(() => {
|
const itemErrors = React.useMemo<ProformaItemError[]>(() => {
|
||||||
if (!Array.isArray(errors.items)) {
|
if (!Array.isArray(errors.items)) return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.items.map((error) => error ?? {});
|
return errors.items.map((error) => error ?? {});
|
||||||
}, [errors.items]);
|
}, [errors.items]);
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { type UseFormReturn, useWatch } from "react-hook-form";
|
import { type UseFormReturn, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type RetentionPercentageOption,
|
||||||
|
type TaxPercentageOption,
|
||||||
|
getProformaRecPercentage,
|
||||||
|
} from "../../shared";
|
||||||
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
||||||
|
|
||||||
interface UseUpdateProformaTaxControllerParams {
|
interface UseUpdateProformaTaxControllerParams {
|
||||||
@ -10,24 +15,35 @@ interface UseUpdateProformaTaxControllerParams {
|
|||||||
export interface UseUpdateProformaTaxControllerResult {
|
export interface UseUpdateProformaTaxControllerResult {
|
||||||
taxMode: ProformaTaxMode;
|
taxMode: ProformaTaxMode;
|
||||||
|
|
||||||
|
hasTaxPercentage: boolean;
|
||||||
defaultTaxPercentage: number | null;
|
defaultTaxPercentage: number | null;
|
||||||
|
|
||||||
hasRecPercentage: boolean;
|
hasRecPercentage: boolean;
|
||||||
hasRetention: boolean;
|
defaultRecPercentage: number | null;
|
||||||
retentionPercentage: number | null;
|
|
||||||
|
hasRetentionPercentage: boolean;
|
||||||
|
defaultRetentionPercentage: number | null;
|
||||||
|
|
||||||
usesSingleTax: boolean;
|
usesSingleTax: boolean;
|
||||||
usesPerLineTax: boolean;
|
usesPerLineTax: boolean;
|
||||||
|
|
||||||
enablePerLineTaxes: () => void;
|
enablePerLineTaxes: () => void;
|
||||||
disablePerLineTaxes: () => void;
|
disablePerLineTaxes: () => void;
|
||||||
|
|
||||||
|
updateDefaultTaxPercentage: (newTaxPercentage: RetentionPercentageOption) => void;
|
||||||
|
updateDefaultRecPercentage: (enabled: boolean) => void;
|
||||||
|
updateDefaultRetentionPercentage: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRecPercentage = (taxPercentage: number | null | undefined): number | null => {
|
const resolveRecPercentage = (
|
||||||
if (taxPercentage === 21) return 5.2;
|
enabled: boolean,
|
||||||
if (taxPercentage === 10) return 1.4;
|
taxPercentage: RetentionPercentageOption | null
|
||||||
if (taxPercentage === 4) return 0.5;
|
): number | null => {
|
||||||
|
if (!enabled || taxPercentage === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return getProformaRecPercentage(taxPercentage as TaxPercentageOption);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateProformaTaxController = ({
|
export const useUpdateProformaTaxController = ({
|
||||||
@ -36,17 +52,28 @@ export const useUpdateProformaTaxController = ({
|
|||||||
const { control, getValues, setValue } = form;
|
const { control, getValues, setValue } = form;
|
||||||
|
|
||||||
const taxMode = useWatch({ control, name: "taxMode" });
|
const taxMode = useWatch({ control, name: "taxMode" });
|
||||||
const hasRecPercentage = useWatch({ control, name: "hasRecPercentage" }) ?? false;
|
|
||||||
const hasRetention = useWatch({ control, name: "hasRetention" }) ?? false;
|
const hasTaxPercentage = useWatch({ control, name: "hasTaxPercentage" }) ?? false;
|
||||||
const retentionPercentage = useWatch({ control, name: "retentionPercentage" }) ?? null;
|
|
||||||
const defaultTaxPercentage = useWatch({ control, name: "defaultTaxPercentage" }) ?? null;
|
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);
|
const hasMountedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (taxMode !== "single") return;
|
if (taxMode !== "single") return;
|
||||||
|
|
||||||
const currentItems = getValues("items") ?? [];
|
const currentItems = getValues("items") ?? [];
|
||||||
const recPercentage = hasRecPercentage ? getRecPercentage(defaultTaxPercentage) : null;
|
|
||||||
|
const nextRecPercentage = resolveRecPercentage(
|
||||||
|
hasRecPercentage,
|
||||||
|
defaultTaxPercentage as RetentionPercentageOption | null
|
||||||
|
);
|
||||||
|
|
||||||
const shouldMarkDirty = hasMountedRef.current;
|
const shouldMarkDirty = hasMountedRef.current;
|
||||||
|
|
||||||
@ -59,8 +86,8 @@ export const useUpdateProformaTaxController = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.recPercentage !== recPercentage) {
|
if (item.recPercentage !== nextRecPercentage) {
|
||||||
setValue(`items.${index}.recPercentage`, recPercentage, {
|
setValue(`items.${index}.recPercentage`, nextRecPercentage, {
|
||||||
shouldDirty: shouldMarkDirty,
|
shouldDirty: shouldMarkDirty,
|
||||||
shouldTouch: false,
|
shouldTouch: false,
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
@ -87,18 +114,79 @@ export const useUpdateProformaTaxController = ({
|
|||||||
});
|
});
|
||||||
}, [setValue]);
|
}, [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 {
|
return {
|
||||||
taxMode,
|
taxMode,
|
||||||
|
|
||||||
|
hasTaxPercentage,
|
||||||
defaultTaxPercentage,
|
defaultTaxPercentage,
|
||||||
|
|
||||||
hasRecPercentage,
|
hasRecPercentage,
|
||||||
hasRetention,
|
defaultRecPercentage,
|
||||||
retentionPercentage,
|
|
||||||
|
hasRetentionPercentage,
|
||||||
|
defaultRetentionPercentage,
|
||||||
|
|
||||||
usesSingleTax: taxMode === "single",
|
usesSingleTax: taxMode === "single",
|
||||||
usesPerLineTax: taxMode === "perLine",
|
usesPerLineTax: taxMode === "perLine",
|
||||||
|
|
||||||
enablePerLineTaxes,
|
enablePerLineTaxes,
|
||||||
disablePerLineTaxes,
|
disablePerLineTaxes,
|
||||||
|
|
||||||
|
updateDefaultTaxPercentage,
|
||||||
|
updateDefaultRecPercentage,
|
||||||
|
updateDefaultRetentionPercentage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,22 +15,22 @@ export interface UseUpdateProformaTotalsControllerResult {
|
|||||||
export const useUpdateProformaTotalsController = ({
|
export const useUpdateProformaTotalsController = ({
|
||||||
form,
|
form,
|
||||||
}: UseUpdateProformaTotalsControllerParams): UseUpdateProformaTotalsControllerResult => {
|
}: UseUpdateProformaTotalsControllerParams): UseUpdateProformaTotalsControllerResult => {
|
||||||
const { control, getValues } = form;
|
const { control } = form;
|
||||||
|
|
||||||
const items = useWatch({ control, name: "items" });
|
|
||||||
const globalDiscountPercentage = useWatch({ control, name: "globalDiscountPercentage" });
|
const globalDiscountPercentage = useWatch({ control, name: "globalDiscountPercentage" });
|
||||||
const hasRetention = useWatch({ control, name: "hasRetention" });
|
const items = useWatch({ control, name: "items" });
|
||||||
const retentionPercentage = useWatch({ control, name: "retentionPercentage" });
|
|
||||||
|
const hasRetentionPercentage = useWatch({ control, name: "hasRetentionPercentage" });
|
||||||
|
const retentionPercentage = useWatch({ control, name: "defaultRetentionPercentage" });
|
||||||
|
|
||||||
const totals = useMemo(() => {
|
const totals = useMemo(() => {
|
||||||
return calculateProformaTotals({
|
return calculateProformaTotals({
|
||||||
...getValues(),
|
|
||||||
items,
|
|
||||||
globalDiscountPercentage,
|
globalDiscountPercentage,
|
||||||
hasRetention,
|
items: items ?? [],
|
||||||
retentionPercentage,
|
hasRetentionPercentage: hasRetentionPercentage ?? false,
|
||||||
|
retentionPercentage: retentionPercentage ?? null,
|
||||||
});
|
});
|
||||||
}, [items, globalDiscountPercentage, hasRetention, retentionPercentage]);
|
}, [globalDiscountPercentage, items, hasRetentionPercentage, retentionPercentage]);
|
||||||
|
|
||||||
return { totals };
|
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.entity";
|
||||||
export * from "./proforma-item-update-form.schema";
|
export * from "./proforma-item-update-form.schema";
|
||||||
export * from "./proforma-item-update-patch.entity";
|
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;
|
taxMode: ProformaTaxMode;
|
||||||
taxRegimeCode: string | null;
|
taxRegimeCode: string | null;
|
||||||
|
|
||||||
|
hasTaxPercentage: boolean;
|
||||||
defaultTaxPercentage: number | null;
|
defaultTaxPercentage: number | null;
|
||||||
|
|
||||||
hasRecPercentage: boolean;
|
hasRecPercentage: boolean;
|
||||||
hasRetention: boolean;
|
defaultRecPercentage: number | null;
|
||||||
retentionPercentage: number | null;
|
|
||||||
|
|
||||||
paymentMethod: string;
|
hasRetentionPercentage: boolean;
|
||||||
|
defaultRetentionPercentage: number | null;
|
||||||
|
|
||||||
|
paymentMethodId: string | null;
|
||||||
|
|
||||||
items: ProformaItemUpdateForm[];
|
items: ProformaItemUpdateForm[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { PercentageHelper } from "@repo/rdx-utils";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema";
|
import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema";
|
||||||
@ -16,34 +17,55 @@ import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema
|
|||||||
* - sin detalles impuestos por el widget
|
* - sin detalles impuestos por el widget
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const ProformaUpdateFormSchema = z.object({
|
export const ProformaUpdateFormSchema = z
|
||||||
series: z.string(),
|
.object({
|
||||||
|
series: z.string(),
|
||||||
|
|
||||||
invoiceDate: z.string().min(1),
|
invoiceDate: z.string().min(1),
|
||||||
operationDate: z.string(),
|
operationDate: z.string(),
|
||||||
|
|
||||||
customerId: z.string().min(1),
|
customerId: z.string().min(1),
|
||||||
|
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
reference: z.string(),
|
reference: z.string(),
|
||||||
notes: z.string(),
|
notes: z.string(),
|
||||||
|
|
||||||
languageCode: z.string().min(1),
|
languageCode: z.string().min(1),
|
||||||
currencyCode: z.string().min(1),
|
currencyCode: z.string().min(1),
|
||||||
|
|
||||||
globalDiscountPercentage: z.number().min(0).max(100),
|
globalDiscountPercentage: z.number().min(0).max(100),
|
||||||
|
|
||||||
taxMode: z.enum(["single", "perLine"]),
|
taxMode: z.enum(["single", "perLine"]),
|
||||||
taxRegimeCode: z.string(),
|
taxRegimeCode: z.string(),
|
||||||
defaultTaxPercentage: z.number().nullable(),
|
|
||||||
|
|
||||||
hasRecPercentage: z.boolean(),
|
hasTaxPercentage: z.boolean(),
|
||||||
hasRetention: z.boolean(),
|
defaultTaxPercentage: z.number().nullable(),
|
||||||
retentionPercentage: z.number().nullable(),
|
|
||||||
|
|
||||||
paymentMethod: z.string(),
|
hasRecPercentage: z.boolean(),
|
||||||
|
defaultRecPercentage: z.number().nullable(),
|
||||||
|
|
||||||
items: z.array(ProformaItemUpdateFormSchema).min(1),
|
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>;
|
export type ProformaUpdateFormSchemaType = z.infer<typeof ProformaUpdateFormSchema>;
|
||||||
|
|||||||
@ -1,27 +1,17 @@
|
|||||||
|
import type { ProformaHeaderTotals } from "./proforma-calculation.entity";
|
||||||
|
|
||||||
export interface ProformaTaxBreakdownLine {
|
export interface ProformaTaxBreakdownLine {
|
||||||
taxPercentage: number;
|
|
||||||
taxableBase: number;
|
taxableBase: number;
|
||||||
taxAmount: number;
|
ivaPercentage: number;
|
||||||
|
ivaAmount: number;
|
||||||
recPercentage: number | null;
|
recPercentage: number | null;
|
||||||
equivalenceSurchargeAmount: number;
|
recAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProformaTotals {
|
export interface ProformaTotals extends ProformaHeaderTotals {
|
||||||
subtotalBeforeDiscounts: number;
|
recPercentage: number | null;
|
||||||
|
recAmount: number;
|
||||||
lineDiscountTotal: number;
|
|
||||||
globalDiscountPercentage: number;
|
|
||||||
globalDiscountAmount: number;
|
|
||||||
|
|
||||||
taxableBase: number;
|
|
||||||
|
|
||||||
taxBreakdown: ProformaTaxBreakdownLine[];
|
|
||||||
taxTotal: number;
|
|
||||||
|
|
||||||
equivalenceSurchargeTotal: number;
|
|
||||||
|
|
||||||
retentionPercentage: number | null;
|
retentionPercentage: number | null;
|
||||||
retentionAmount: number;
|
retentionAmount: number;
|
||||||
|
|
||||||
total: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AmountField,
|
AmountField,
|
||||||
|
CheckboxField,
|
||||||
LineDescriptionField,
|
LineDescriptionField,
|
||||||
PercentageField,
|
PercentageField,
|
||||||
QuantityField,
|
QuantityField,
|
||||||
@ -63,6 +64,12 @@ export const ProformaLineEditor = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const columns: LineEditorColumn<ProformaItemField>[] = [
|
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",
|
id: "description",
|
||||||
header: t("form_fields.items.description.label", "Descripción"),
|
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 { 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 { CurrencyIcon } from "lucide-react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { ProformaTotals } from "../../entities";
|
import type { ProformaTotals } from "../../entities";
|
||||||
@ -10,25 +12,22 @@ interface ProformaTotalsSummaryProps {
|
|||||||
totals: ProformaTotals;
|
totals: ProformaTotals;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
|
|
||||||
showEquivalenceSurcharge?: boolean;
|
showRec?: boolean;
|
||||||
showRetention?: boolean;
|
showRetention?: boolean;
|
||||||
|
|
||||||
disabled?: boolean;
|
globalDiscountField?: ReactNode;
|
||||||
readOnly?: boolean;
|
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProformaTotalsSummary = ({
|
export const ProformaTotalsSummary = ({
|
||||||
totals,
|
totals,
|
||||||
currency = "EUR",
|
currency = "EUR",
|
||||||
|
showRec = false,
|
||||||
showEquivalenceSurcharge = false,
|
|
||||||
showRetention = false,
|
showRetention = false,
|
||||||
|
globalDiscountField,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
readOnly = false,
|
|
||||||
|
|
||||||
className,
|
className,
|
||||||
}: ProformaTotalsSummaryProps) => {
|
}: ProformaTotalsSummaryProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -41,6 +40,13 @@ export const ProformaTotalsSummary = ({
|
|||||||
return PercentageHelper.formatPercent(value);
|
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 (
|
return (
|
||||||
<FormSectionCard
|
<FormSectionCard
|
||||||
className={className}
|
className={className}
|
||||||
@ -49,40 +55,42 @@ export const ProformaTotalsSummary = ({
|
|||||||
icon={<CurrencyIcon className="size-5" />}
|
icon={<CurrencyIcon className="size-5" />}
|
||||||
title={t("proformas.update.totals.title", "Totales")}
|
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
|
<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)}
|
value={formatMoney(totals.subtotalBeforeDiscounts)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section className="space-y-3 rounded-lg border bg-background p-4">
|
<Card
|
||||||
<h3 className="text-sm font-medium">
|
className={cn(disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary")}
|
||||||
{t("proformas.update.totals.discounts", "Descuentos")}
|
>
|
||||||
</h3>
|
<CardHeader>
|
||||||
|
<CardTitle>{t("proformas.update.totals.discounts", "Descuentos")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
<TotalsRow
|
<CardContent className="space-y-3">
|
||||||
label={t("proformas.update.totals.lineDiscountTotal", "Descuento en líneas")}
|
<TotalsRow
|
||||||
value={`-${formatMoney(totals.lineDiscountTotal)}`}
|
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}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="pb-1 text-right">
|
{globalDiscountField ? <div>{globalDiscountField}</div> : null}
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{t("proformas.update.totals.globalDiscountAmount", "Importe descuento global")}
|
<TotalsRow
|
||||||
</div>
|
description={formatPercent(totals.globalDiscountPercentage)}
|
||||||
<div className="font-mono text-sm tabular-nums">
|
label={t("proformas.update.totals.globalDiscountAmount", "Descuento global")}
|
||||||
-{formatMoney(totals.globalDiscountAmount)}
|
value={`-${formatMoney(totals.globalDiscountAmount)}`}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
<Separator className="my-4" />
|
||||||
</section>
|
|
||||||
|
<TotalsRow
|
||||||
|
label={t("proformas.update.totals.totalDiscountAmount", "Total de descuentos")}
|
||||||
|
strong
|
||||||
|
value={`-${formatMoney(totals.totalDiscountAmount)}`}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<TotalsRow
|
<TotalsRow
|
||||||
label={t("proformas.update.totals.taxableBase", "Base imponible")}
|
label={t("proformas.update.totals.taxableBase", "Base imponible")}
|
||||||
@ -90,71 +98,68 @@ export const ProformaTotalsSummary = ({
|
|||||||
value={formatMoney(totals.taxableBase)}
|
value={formatMoney(totals.taxableBase)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section className="space-y-3 rounded-lg border bg-background p-4">
|
<Card className="bg-muted text-muted-foreground">
|
||||||
<h3 className="text-sm font-medium">{t("proformas.update.totals.taxes", "Impuestos")}</h3>
|
<CardHeader>
|
||||||
|
<CardTitle>{t("proformas.update.totals.taxes", "Impuestos")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
{totals.taxBreakdown.length > 0 ? (
|
<CardContent className="space-y-3">
|
||||||
totals.taxBreakdown.map((tax) => (
|
{totals.taxBreakdown.length > 0 ? (
|
||||||
<div className="space-y-2" key={tax.taxPercentage}>
|
totals.taxBreakdown.map((tax) => {
|
||||||
<TotalsRow
|
const key = `${tax.taxPercentage}:${tax.recPercentage ?? "none"}`;
|
||||||
description={formatMoney(tax.taxableBase)}
|
|
||||||
label={`${t("proformas.update.totals.taxPercentage", "IVA")} ${formatPercent(
|
|
||||||
tax.taxPercentage
|
|
||||||
)}`}
|
|
||||||
value={formatMoney(tax.taxAmount)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showEquivalenceSurcharge && tax.recPercentage !== null ? (
|
return (
|
||||||
<TotalsRow
|
<div className="space-y-2" key={key}>
|
||||||
description={formatMoney(tax.taxableBase)}
|
<TotalsRow
|
||||||
label={`${t(
|
label={`${t(
|
||||||
"proformas.update.totals.equivalenceSurcharge",
|
"proformas.update.totals.taxPercentage",
|
||||||
"Recargo equivalencia"
|
"Impuesto"
|
||||||
)} ${formatPercent(tax.recPercentage)}`}
|
)} ${formatPercent(tax.taxPercentage)}`}
|
||||||
value={formatMoney(tax.equivalenceSurchargeAmount)}
|
value={formatMoney(tax.taxAmount)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("proformas.update.totals.noTaxes", "Sin impuestos aplicados")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator />
|
{showRec && tax.recPercentage !== null ? (
|
||||||
|
<TotalsRow
|
||||||
|
label={`${t(
|
||||||
|
"proformas.update.totals.recPercentage",
|
||||||
|
"Recargo equivalencia"
|
||||||
|
)} ${formatPercent(tax.recPercentage)}`}
|
||||||
|
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 impuestos")}
|
||||||
|
strong
|
||||||
|
value={formatMoney(totals.taxTotal)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showRec ? (
|
||||||
<TotalsRow
|
<TotalsRow
|
||||||
label={t("proformas.update.totals.taxTotal", "Total IVA")}
|
label={t("proformas.update.totals.recTotal", "Total recargo equivalencia")}
|
||||||
strong
|
strong
|
||||||
value={formatMoney(totals.taxTotal)}
|
value={formatMoney(totals.recTotal)}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
{showEquivalenceSurcharge ? (
|
|
||||||
<TotalsRow
|
|
||||||
label={t(
|
|
||||||
"proformas.update.totals.equivalenceSurchargeTotal",
|
|
||||||
"Total recargo equivalencia"
|
|
||||||
)}
|
|
||||||
strong
|
|
||||||
value={formatMoney(totals.equivalenceSurchargeTotal)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{showRetention ? (
|
{showRetention ? (
|
||||||
<section className="space-y-3 rounded-lg border bg-background p-4">
|
<TotalsRow
|
||||||
<h3 className="text-sm font-medium">
|
label={retentionLabel}
|
||||||
{t("proformas.update.totals.retention", "Retención")}
|
strong
|
||||||
</h3>
|
value={`-${formatMoney(totals.retentionAmount)}`}
|
||||||
|
/>
|
||||||
<TotalsRow
|
|
||||||
label={`${t("proformas.update.totals.retentionPercentage", "IRPF")} ${formatPercent(
|
|
||||||
totals.retentionPercentage ?? 0
|
|
||||||
)}`}
|
|
||||||
value={`-${formatMoney(totals.retentionAmount)}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@ -163,6 +168,7 @@ export const ProformaTotalsSummary = ({
|
|||||||
<span className="text-sm font-semibold">
|
<span className="text-sm font-semibold">
|
||||||
{t("proformas.update.totals.total", "Total factura")}
|
{t("proformas.update.totals.total", "Total factura")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="font-mono text-xl font-bold tabular-nums">
|
<span className="font-mono text-xl font-bold tabular-nums">
|
||||||
{formatMoney(totals.total)}
|
{formatMoney(totals.total)}
|
||||||
</span>
|
</span>
|
||||||
@ -175,15 +181,21 @@ export const ProformaTotalsSummary = ({
|
|||||||
interface TotalsRowProps {
|
interface TotalsRowProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
className?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
strong?: boolean;
|
strong?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TotalsRow = ({ label, value, description, strong = false }: TotalsRowProps) => {
|
const TotalsRow = ({ label, value, description, className, strong = false }: TotalsRowProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className={cn("flex items-start justify-between gap-4", className)}>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className={strong ? "text-sm font-semibold" : "text-sm text-muted-foreground"}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"text-sm",
|
||||||
|
strong ? "font-bold text-foreground" : "font-medium text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -191,9 +203,10 @@ const TotalsRow = ({ label, value, description, strong = false }: TotalsRowProps
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={cn(
|
||||||
strong ? "font-mono text-sm font-semibold tabular-nums" : "font-mono text-sm tabular-nums"
|
"shrink-0 text-right font-mono tabular-nums",
|
||||||
}
|
strong ? "text-base font-bold" : "text-sm font-medium"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,7 +19,11 @@ import {
|
|||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { FileText } from "lucide-react";
|
import { FileText } from "lucide-react";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
|
||||||
export const ProformaTaxesCard = () => {
|
export const ProformaTaxesCard = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="shadow">
|
<Card className="shadow">
|
||||||
<CardHeader className="px-6 pb-7 pt-3">
|
<CardHeader className="px-6 pb-7 pt-3">
|
||||||
@ -79,13 +83,16 @@ export const ProformaTaxesCard = () => {
|
|||||||
className="cursor-pointer font-medium text-primary-700"
|
className="cursor-pointer font-medium text-primary-700"
|
||||||
htmlFor="same-vat"
|
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>
|
</FieldLabel>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field className="w-32">
|
<Field className="w-32">
|
||||||
<FieldLabel className="text-primary-700" htmlFor="default-vat">
|
<FieldLabel className="text-primary-700" htmlFor="default-vat">
|
||||||
IVA por defecto
|
{t("form_fields.proformas.default_tax_percentage.label", "IVA por defecto")}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
|
|
||||||
<Select defaultValue="21">
|
<Select defaultValue="21">
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx
|
// modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx
|
||||||
|
|
||||||
import type { CustomerSelectionOption } from "@erp/customers";
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
|
import { PercentageField } from "@repo/rdx-ui/components";
|
||||||
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
@ -75,21 +76,29 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
|
|
||||||
<ProformaUpdateItemsEditor disabled={isSubmitting} itemsCtrl={itemsCtrl} taxCtrl={taxCtrl} />
|
<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">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
|
||||||
<ProformaUpdateTaxEditor className="md:col-span-6" taxCtrl={taxCtrl} />
|
<ProformaUpdateTaxEditor className="md:col-span-6" taxCtrl={taxCtrl} />
|
||||||
<ProformaTotalsSummary
|
<ProformaTotalsSummary
|
||||||
className="md:col-span-6"
|
className="md:col-span-6"
|
||||||
currency={currencyCode}
|
currency={currencyCode}
|
||||||
disabled={isSubmitting}
|
globalDiscountField={
|
||||||
showEquivalenceSurcharge={taxCtrl.hasRecPercentage}
|
<PercentageField
|
||||||
showRetention={taxCtrl.hasRetention}
|
disabled={isSubmitting}
|
||||||
|
inputClassName="bg-background"
|
||||||
|
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
|
||||||
|
name="globalDiscountPercentage"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
showRec={taxCtrl.hasRecPercentage}
|
||||||
|
showRetention={taxCtrl.hasRetentionPercentage}
|
||||||
totals={totalsCtrl.totals}
|
totals={totalsCtrl.totals}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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">
|
<Button disabled={isSubmitting} onClick={onReset} type="button" variant="outline">
|
||||||
{t("common.reset", "Restablecer")}
|
{t("common.reset", "Restablecer")}
|
||||||
|
|||||||
@ -1,25 +1,29 @@
|
|||||||
import {
|
import {
|
||||||
CheckboxField,
|
|
||||||
FormSectionCard,
|
FormSectionCard,
|
||||||
FormSectionGrid,
|
FormSectionGrid,
|
||||||
PercentageField,
|
|
||||||
SelectField,
|
SelectField,
|
||||||
|
SwitchField,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
|
import { PercentageHelper } from "@repo/rdx-utils";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
CardAction,
|
||||||
CardContent,
|
CardContent,
|
||||||
Checkbox,
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldLegend,
|
FieldLegend,
|
||||||
FieldSeparator,
|
FieldSeparator,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
Label,
|
Switch,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { ReceiptTextIcon } from "lucide-react";
|
import { ReceiptTextIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
import { getProformaRetentionOptions, getProformaTaxOptions } from "../../../shared";
|
||||||
import type { UseUpdateProformaTaxControllerResult } from "../../controllers";
|
import type { UseUpdateProformaTaxControllerResult } from "../../controllers";
|
||||||
|
|
||||||
interface ProformaUpdateTaxEditorProps {
|
interface ProformaUpdateTaxEditorProps {
|
||||||
@ -130,41 +134,38 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
<Card
|
<Card
|
||||||
className={cn(disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary")}
|
className={cn(disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary")}
|
||||||
>
|
>
|
||||||
|
<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.
|
||||||
|
</CardDescription>
|
||||||
|
<CardAction>
|
||||||
|
<Switch
|
||||||
|
checked={taxCtrl.usesSingleTax}
|
||||||
|
className="not-disabled:cursor-pointer"
|
||||||
|
disabled={disabled || readOnly}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
checked ? taxCtrl.disablePerLineTaxes() : taxCtrl.enablePerLineTaxes()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<FieldSet>
|
<FieldSet>
|
||||||
<FieldLegend className="font-semibold">Configuración de IVA</FieldLegend>
|
|
||||||
<FieldDescription>
|
|
||||||
Puedes usar un tipo único para todos las líneas de detalle o permitir que cada línea
|
|
||||||
tenga su propio IVA.
|
|
||||||
</FieldDescription>
|
|
||||||
|
|
||||||
<FormSectionGrid>
|
<FormSectionGrid>
|
||||||
<Field className="md:col-span-12 md:col-start-1" orientation="horizontal">
|
|
||||||
<Checkbox
|
|
||||||
checked={taxCtrl.usesSingleTax}
|
|
||||||
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>
|
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
className="md:col-span-4 md:col-start-1"
|
className="md:col-span-4 md:col-start-1"
|
||||||
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
items={[
|
inputClassName="bg-background"
|
||||||
{ value: "0", label: "0%" },
|
items={getProformaTaxOptions()}
|
||||||
{ value: "4", label: "4%" },
|
|
||||||
{ value: "10", label: "10%" },
|
|
||||||
{ value: "21", label: "21%" },
|
|
||||||
]}
|
|
||||||
label={t("form_fields.proformas.default_tax_percentage.label", "IVA por defecto")}
|
label={t("form_fields.proformas.default_tax_percentage.label", "IVA por defecto")}
|
||||||
name="defaultTaxPercentage"
|
name="defaultTaxPercentage"
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
@ -175,47 +176,65 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||||
/>
|
/>
|
||||||
</FormSectionGrid>
|
</FormSectionGrid>
|
||||||
</FieldSet>{" "}
|
</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.
|
||||||
|
</FieldDescription>
|
||||||
|
|
||||||
|
<FormSectionGrid>
|
||||||
|
<SwitchField
|
||||||
|
className="md:col-span-12 md:col-start-1"
|
||||||
|
disabled={disabled}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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="hasRetentionPercentage"
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
|
className="md:col-span-4 md:col-start-1"
|
||||||
|
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="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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<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.
|
|
||||||
</FieldDescription>
|
|
||||||
|
|
||||||
<FormSectionGrid>
|
|
||||||
<CheckboxField
|
|
||||||
className="md:col-span-12 md:col-start-1"
|
|
||||||
disabled={disabled}
|
|
||||||
label={t(
|
|
||||||
"form_fields.proformas.has_equivalence_surcharge.label",
|
|
||||||
"Recargo de equivalencia"
|
|
||||||
)}
|
|
||||||
name="hasRecPercentage"
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CheckboxField
|
|
||||||
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"
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PercentageField
|
|
||||||
className="md:col-span-4 md:col-start-1"
|
|
||||||
disabled={disabled}
|
|
||||||
label={t("form_fields.proformas.retention_percentage.label", "Retención")}
|
|
||||||
name="retentionPercentage"
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
</FormSectionGrid>
|
|
||||||
</FieldSet>
|
|
||||||
</FormSectionCard>
|
</FormSectionCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export const buildProformaItemUpdateDefault = (position: number): ProformaItemUp
|
|||||||
unitAmount: null,
|
unitAmount: null,
|
||||||
itemDiscountPercentage: null,
|
itemDiscountPercentage: null,
|
||||||
|
|
||||||
taxPercentage: null,
|
ivaPercentage: null,
|
||||||
recPercentage: null,
|
recPercentage: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,14 +20,17 @@ export const buildProformaUpdateDefault = (): ProformaUpdateForm => {
|
|||||||
|
|
||||||
taxMode: "single",
|
taxMode: "single",
|
||||||
taxRegimeCode: "01",
|
taxRegimeCode: "01",
|
||||||
|
|
||||||
|
hasTaxPercentage: true,
|
||||||
defaultTaxPercentage: 21,
|
defaultTaxPercentage: 21,
|
||||||
|
|
||||||
hasRecPercentage: false,
|
hasRecPercentage: false,
|
||||||
hasRetention: false,
|
defaultRecPercentage: null,
|
||||||
|
|
||||||
retentionPercentage: null,
|
hasRetentionPercentage: false,
|
||||||
|
defaultRetentionPercentage: 15,
|
||||||
|
|
||||||
paymentMethod: "",
|
paymentMethodId: null,
|
||||||
|
|
||||||
items: [],
|
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 { calculateProformaTotalsFromLines } from "./calculations";
|
||||||
import type { ProformaTotals, ProformaUpdateForm } from "../entities";
|
|
||||||
|
|
||||||
import { calculateCommercialDocumentTotals } from "./calculations";
|
export interface CalculateProformaTotalsParams {
|
||||||
|
globalDiscountPercentage: ProformaUpdateForm["globalDiscountPercentage"];
|
||||||
|
|
||||||
export const calculateProformaTotals = (form: ProformaUpdateForm): ProformaTotals => {
|
items: ProformaItemUpdateForm[];
|
||||||
return calculateCommercialDocumentTotals({
|
|
||||||
lines: mapProformaFormToCommercialDocumentLines(form),
|
hasRetentionPercentage: boolean;
|
||||||
globalDiscountPercentage: form.globalDiscountPercentage,
|
|
||||||
|
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";
|
import { NumberHelper } from "@repo/rdx-utils";
|
||||||
|
|
||||||
export const toCalculationNumber = (value: number | null | undefined): number => {
|
export const toCalculationNumber = (value: number | null | undefined): number => {
|
||||||
@ -10,6 +8,9 @@ export const roundMoney = (value: number): number => {
|
|||||||
return NumberHelper.roundToScale(value, 2);
|
return NumberHelper.roundToScale(value, 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const percentageAmount = (baseAmount: number, percentage: number): number => {
|
export const percentageAmount = (
|
||||||
return baseAmount * (percentage / 100);
|
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> = {
|
type CheckboxFieldProps<TFormValues extends FieldValues> = {
|
||||||
name: FieldPath<TFormValues>;
|
name: FieldPath<TFormValues>;
|
||||||
|
|
||||||
label: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
reserveDescriptionSpace?: boolean;
|
reserveDescriptionSpace?: boolean;
|
||||||
|
|
||||||
|
|||||||
@ -9,5 +9,6 @@ export * from "./multi-select-field.tsx";
|
|||||||
export * from "./radio-group-field.tsx";
|
export * from "./radio-group-field.tsx";
|
||||||
export * from "./select-field.tsx";
|
export * from "./select-field.tsx";
|
||||||
export * from "./semantic-fields/index.ts";
|
export * from "./semantic-fields/index.ts";
|
||||||
|
export * from "./switch-field.tsx";
|
||||||
export * from "./text-area-field.tsx";
|
export * from "./text-area-field.tsx";
|
||||||
export * from "./text-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;
|
return value !== null && value !== undefined && value > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizePercentage = (value: number): number => {
|
||||||
|
return Math.round(value * 10000) / 10000;
|
||||||
|
};
|
||||||
|
|
||||||
export const PercentageHelper = {
|
export const PercentageHelper = {
|
||||||
formatPercent,
|
formatPercent,
|
||||||
hasPositivePercentage,
|
hasPositivePercentage,
|
||||||
|
normalizePercentage,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user