From 3b77fb7cb83e11c7bd15b95b612523b7f33e3f87 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 11 May 2026 21:47:38 +0200 Subject: [PATCH] . --- .../catalogs/taxes/spain-tax-catalog.json | 18 -- .../web/proformas/shared/constants/index.ts | 2 + .../constants/retentions-options.constants.ts | 25 ++ .../constants/taxes-options.constants.ts | 37 +++ .../entities/proforma-tax-summary.entity.ts | 4 +- .../src/web/proformas/shared/index.ts | 1 + .../utils/get-proforma-rec-percentage.ts | 5 + .../utils/get-proforma-retention-options.ts | 3 + .../shared/utils/get-proforma-tax-options.ts | 3 + .../src/web/proformas/shared/utils/index.ts | 3 + .../web/proformas/update/adapters/index.ts | 2 +- ...forma-form-to-commercial-document-lines.ts | 15 -- ...forma-items-form-to-proforma-line-inpus.ts | 31 +++ ...roforma-to-proforma-update-form.adapter.ts | 41 ++-- .../use-update-proforma-controller.ts | 2 + .../use-update-proforma-items-controller.ts | 53 ++--- .../use-update-proforma-tax-controller.ts | 118 ++++++++-- .../use-update-proforma-totals-controller.ts | 18 +- .../commercial-document-calculation.entity.ts | 35 --- .../web/proformas/update/entities/index.ts | 2 +- .../entities/proforma-calculation.entity.ts | 49 ++++ .../entities/proforma-update-form.entity.ts | 10 +- .../entities/proforma-update-form.schema.ts | 62 +++-- .../entities/proforma-update-totals.entity.ts | 26 +-- .../update/ui/blocks/proforma-line-editor.tsx | 7 + .../ui/blocks/proforma-totals-summary.tsx | 213 ++++++++++-------- .../update/ui/editors/proforma-taxes-card.tsx | 11 +- .../editors/proforma-update-editor-form.tsx | 23 +- .../ui/editors/proforma-update-tax-editor.tsx | 161 +++++++------ .../build-proforma-item-update-default.ts | 2 +- .../utils/build-proforma-update-default.ts | 9 +- .../update/utils/calculate-proforma-totals.ts | 39 +++- .../calculate-proforma-line-total.ts | 47 ++++ .../calculate-proforma-lines-totals.ts | 43 ++++ .../calculate-proforma-tax-breakdown.ts | 80 +++++++ .../calculate-proforma-totals-from-lines.ts | 76 +++++++ .../update/utils/calculations/index.ts | 2 +- .../utils/calculations/money-calculation.ts | 9 +- .../src/web/schemas/index.ts | 6 - .../web/schemas/invoice-resume.form.schema.ts | 23 -- .../src/web/schemas/invoice.form.schema.ts | 122 ---------- .../src/web/schemas/invoices.api.schema.ts | 23 -- .../src/components/form/checkbox-field.tsx | 2 +- packages/rdx-ui/src/components/form/index.ts | 1 + .../src/components/form/switch-field.tsx | 107 +++++++++ .../src/helpers/percentage-helper.ts | 5 + 46 files changed, 1017 insertions(+), 559 deletions(-) create mode 100644 modules/customer-invoices/src/web/proformas/shared/constants/retentions-options.constants.ts create mode 100644 modules/customer-invoices/src/web/proformas/shared/constants/taxes-options.constants.ts create mode 100644 modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-rec-percentage.ts create mode 100644 modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-retention-options.ts create mode 100644 modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-tax-options.ts create mode 100644 modules/customer-invoices/src/web/proformas/shared/utils/index.ts delete mode 100644 modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-form-to-commercial-document-lines.ts create mode 100644 modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-form-to-proforma-line-inpus.ts delete mode 100644 modules/customer-invoices/src/web/proformas/update/entities/commercial-document-calculation.entity.ts create mode 100644 modules/customer-invoices/src/web/proformas/update/entities/proforma-calculation.entity.ts create mode 100644 modules/customer-invoices/src/web/proformas/update/utils/calculations/calculate-proforma-line-total.ts create mode 100644 modules/customer-invoices/src/web/proformas/update/utils/calculations/calculate-proforma-lines-totals.ts create mode 100644 modules/customer-invoices/src/web/proformas/update/utils/calculations/calculate-proforma-tax-breakdown.ts create mode 100644 modules/customer-invoices/src/web/proformas/update/utils/calculations/calculate-proforma-totals-from-lines.ts delete mode 100644 modules/customer-invoices/src/web/schemas/index.ts delete mode 100644 modules/customer-invoices/src/web/schemas/invoice-resume.form.schema.ts delete mode 100644 modules/customer-invoices/src/web/schemas/invoice.form.schema.ts delete mode 100644 modules/customer-invoices/src/web/schemas/invoices.api.schema.ts create mode 100644 packages/rdx-ui/src/components/form/switch-field.tsx diff --git a/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json b/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json index fa04e008..1d4be6f9 100644 --- a/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json +++ b/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json @@ -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", diff --git a/modules/customer-invoices/src/web/proformas/shared/constants/index.ts b/modules/customer-invoices/src/web/proformas/shared/constants/index.ts index 44579d7c..a61736f2 100644 --- a/modules/customer-invoices/src/web/proformas/shared/constants/index.ts +++ b/modules/customer-invoices/src/web/proformas/shared/constants/index.ts @@ -1 +1,3 @@ export * from "./payment-method-options.constants"; +export * from "./retentions-options.constants"; +export * from "./taxes-options.constants"; diff --git a/modules/customer-invoices/src/web/proformas/shared/constants/retentions-options.constants.ts b/modules/customer-invoices/src/web/proformas/shared/constants/retentions-options.constants.ts new file mode 100644 index 00000000..fe3a90ff --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/constants/retentions-options.constants.ts @@ -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%" }, +]; diff --git a/modules/customer-invoices/src/web/proformas/shared/constants/taxes-options.constants.ts b/modules/customer-invoices/src/web/proformas/shared/constants/taxes-options.constants.ts new file mode 100644 index 00000000..f5a92738 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/constants/taxes-options.constants.ts @@ -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 = { + 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%" }, +]; diff --git a/modules/customer-invoices/src/web/proformas/shared/entities/proforma-tax-summary.entity.ts b/modules/customer-invoices/src/web/proformas/shared/entities/proforma-tax-summary.entity.ts index 4a976fde..75a3b319 100644 --- a/modules/customer-invoices/src/web/proformas/shared/entities/proforma-tax-summary.entity.ts +++ b/modules/customer-invoices/src/web/proformas/shared/entities/proforma-tax-summary.entity.ts @@ -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; diff --git a/modules/customer-invoices/src/web/proformas/shared/index.ts b/modules/customer-invoices/src/web/proformas/shared/index.ts index fea1f862..ddfcb349 100644 --- a/modules/customer-invoices/src/web/proformas/shared/index.ts +++ b/modules/customer-invoices/src/web/proformas/shared/index.ts @@ -4,3 +4,4 @@ export * from "./constants"; export * from "./entities"; export * from "./hooks"; export * from "./ui"; +export * from "./utils"; diff --git a/modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-rec-percentage.ts b/modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-rec-percentage.ts new file mode 100644 index 00000000..beab3c97 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-rec-percentage.ts @@ -0,0 +1,5 @@ +import { PROFORMA_TAX_DEFINITIONS, type TaxPercentageOption } from "../constants"; + +export const getProformaRecPercentage = (taxPercentage: TaxPercentageOption): number => { + return PROFORMA_TAX_DEFINITIONS[taxPercentage].recPercentage; +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-retention-options.ts b/modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-retention-options.ts new file mode 100644 index 00000000..ba66c165 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-retention-options.ts @@ -0,0 +1,3 @@ +import { PROFORMA_RETENTION_OPTIONS } from "../constants"; + +export const getProformaRetentionOptions = () => PROFORMA_RETENTION_OPTIONS; diff --git a/modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-tax-options.ts b/modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-tax-options.ts new file mode 100644 index 00000000..ab9ce945 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/utils/get-proforma-tax-options.ts @@ -0,0 +1,3 @@ +import { PROFORMA_TAX_OPTIONS } from "../constants"; + +export const getProformaTaxOptions = () => PROFORMA_TAX_OPTIONS; diff --git a/modules/customer-invoices/src/web/proformas/shared/utils/index.ts b/modules/customer-invoices/src/web/proformas/shared/utils/index.ts new file mode 100644 index 00000000..0b83e259 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./get-proforma-rec-percentage"; +export * from "./get-proforma-retention-options"; +export * from "./get-proforma-tax-options"; diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/index.ts b/modules/customer-invoices/src/web/proformas/update/adapters/index.ts index 5e0db6ac..b39e29a9 100644 --- a/modules/customer-invoices/src/web/proformas/update/adapters/index.ts +++ b/modules/customer-invoices/src/web/proformas/update/adapters/index.ts @@ -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"; diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-form-to-commercial-document-lines.ts b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-form-to-commercial-document-lines.ts deleted file mode 100644 index 2b25f5d4..00000000 --- a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-form-to-commercial-document-lines.ts +++ /dev/null @@ -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, - })); -}; diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-form-to-proforma-line-inpus.ts b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-form-to-proforma-line-inpus.ts new file mode 100644 index 00000000..72d96598 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-form-to-proforma-line-inpus.ts @@ -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, + })); +}; diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts index 7e8a35ba..928b6a40 100644 --- a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts +++ b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts @@ -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[] => { +const uniquePercentageValues = (values: Array): 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); -}; diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts index ae149e00..2cc48c11 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts @@ -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); diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-items-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-items-controller.ts index 62c0d116..95007d50 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-items-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-items-controller.ts @@ -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; - export type ProformaItemError = FieldErrors; export interface UseUpdateProformaItemsControllerResult { @@ -56,10 +56,6 @@ interface UseUpdateProformaItemsControllerParams { form: UseFormReturn; } -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(() => { - 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(() => { - if (!Array.isArray(errors.items)) { - return []; - } + if (!Array.isArray(errors.items)) return []; return errors.items.map((error) => error ?? {}); }, [errors.items]); diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts index 5c8aeb15..8886529a 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts @@ -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 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, }; }; diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-totals-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-totals-controller.ts index cd4bb416..f5d0de8c 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-totals-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-totals-controller.ts @@ -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 }; }; diff --git a/modules/customer-invoices/src/web/proformas/update/entities/commercial-document-calculation.entity.ts b/modules/customer-invoices/src/web/proformas/update/entities/commercial-document-calculation.entity.ts deleted file mode 100644 index df5edee4..00000000 --- a/modules/customer-invoices/src/web/proformas/update/entities/commercial-document-calculation.entity.ts +++ /dev/null @@ -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; -} diff --git a/modules/customer-invoices/src/web/proformas/update/entities/index.ts b/modules/customer-invoices/src/web/proformas/update/entities/index.ts index be0cf0af..b890ad17 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/index.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/index.ts @@ -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"; diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-calculation.entity.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-calculation.entity.ts new file mode 100644 index 00000000..279304b8 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-calculation.entity.ts @@ -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; +} diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.entity.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.entity.ts index a3a7e739..4e3db326 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.entity.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.entity.ts @@ -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[]; } diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.schema.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.schema.ts index 87d68f8c..1c5dd5bd 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.schema.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.schema.ts @@ -1,3 +1,4 @@ +import { PercentageHelper } from "@repo/rdx-utils"; import { z } from "zod/v4"; 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 */ -export const ProformaUpdateFormSchema = z.object({ - series: z.string(), +export const ProformaUpdateFormSchema = z + .object({ + series: z.string(), - invoiceDate: z.string().min(1), - operationDate: z.string(), + invoiceDate: z.string().min(1), + operationDate: z.string(), - customerId: z.string().min(1), + customerId: z.string().min(1), - description: z.string(), - reference: z.string(), - notes: z.string(), + description: z.string(), + reference: z.string(), + notes: z.string(), - languageCode: z.string().min(1), - currencyCode: z.string().min(1), + languageCode: 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"]), - taxRegimeCode: z.string(), - defaultTaxPercentage: z.number().nullable(), + taxMode: z.enum(["single", "perLine"]), + taxRegimeCode: z.string(), - hasRecPercentage: z.boolean(), - hasRetention: z.boolean(), - retentionPercentage: z.number().nullable(), + hasTaxPercentage: z.boolean(), + defaultTaxPercentage: 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; diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-totals.entity.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-totals.entity.ts index 257ce1d2..3119a381 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-totals.entity.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-totals.entity.ts @@ -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; } diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx index 766feb25..3312133e 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx @@ -1,5 +1,6 @@ import { AmountField, + CheckboxField, LineDescriptionField, PercentageField, QuantityField, @@ -63,6 +64,12 @@ export const ProformaLineEditor = ({ const { t } = useTranslation(); const columns: LineEditorColumn[] = [ + { + id: "isValued", + header: t("form_fields.items.description.is_valued", "¿Valorado?"), + headClassName: "w-[100px] text-right", + cell: ({ index }) => , + }, { id: "description", header: t("form_fields.items.description.label", "Descripción"), diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-totals-summary.tsx b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-totals-summary.tsx index eb16d2a8..7e1b219a 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-totals-summary.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-totals-summary.tsx @@ -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 ( } title={t("proformas.update.totals.title", "Totales")} > - + -
-

- {t("proformas.update.totals.discounts", "Descuentos")} -

+ + + {t("proformas.update.totals.discounts", "Descuentos")} + - - -
- + -
-
- {t("proformas.update.totals.globalDiscountAmount", "Importe descuento global")} -
-
- -{formatMoney(totals.globalDiscountAmount)} -
-
-
-
+ {globalDiscountField ?
{globalDiscountField}
: null} + + + + + + + + -
-

{t("proformas.update.totals.taxes", "Impuestos")}

+ + + {t("proformas.update.totals.taxes", "Impuestos")} + - {totals.taxBreakdown.length > 0 ? ( - totals.taxBreakdown.map((tax) => ( -
- + + {totals.taxBreakdown.length > 0 ? ( + totals.taxBreakdown.map((tax) => { + const key = `${tax.taxPercentage}:${tax.recPercentage ?? "none"}`; - {showEquivalenceSurcharge && tax.recPercentage !== null ? ( - - ) : null} -
- )) - ) : ( -

- {t("proformas.update.totals.noTaxes", "Sin impuestos aplicados")} -

- )} + return ( +
+ - + {showRec && tax.recPercentage !== null ? ( + + ) : null} +
+ ); + }) + ) : ( +

+ {t("proformas.update.totals.noTaxes", "Sin impuestos aplicados")} +

+ )} + +
+ + + + + {showRec ? ( - - {showEquivalenceSurcharge ? ( - - ) : null} -
+ ) : null} {showRetention ? ( -
-

- {t("proformas.update.totals.retention", "Retención")} -

- - -
+ ) : null} @@ -163,6 +168,7 @@ export const ProformaTotalsSummary = ({ {t("proformas.update.totals.total", "Total factura")} + {formatMoney(totals.total)} @@ -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 ( -
-
-
+
+
+
{label}
@@ -191,9 +203,10 @@ const TotalsRow = ({ label, value, description, strong = false }: TotalsRowProps
{value}
diff --git a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-taxes-card.tsx b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-taxes-card.tsx index 65bf5e5a..80d9f321 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-taxes-card.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-taxes-card.tsx @@ -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 ( @@ -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" + )} - IVA por defecto + {t("form_fields.proformas.default_tax_percentage.label", "IVA por defecto")}