Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
David Arranz 2026-05-13 13:16:09 +02:00
parent 9961383a9f
commit 4f331ceb14
50 changed files with 464 additions and 329 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@erp/factuges-server",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"scripts": {
"build": "tsup src/index.ts --config tsup.config.ts",

View File

@ -1,7 +1,7 @@
{
"name": "@erp/factuges-web",
"private": true,
"version": "0.6.5",
"version": "0.6.6",
"type": "module",
"scripts": {
"dev": "vite --host --clearScreen false",

View File

@ -1,6 +1,6 @@
{
"name": "@erp/auth",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/core",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -118,7 +118,7 @@
},
{
"name": "Retenc. 35%",
"code": "retencion_35",
"code": "retention_35",
"value": "3500",
"scale": "2",
"group": "retention",
@ -127,7 +127,7 @@
},
{
"name": "Retenc. 19%",
"code": "retencion_19",
"code": "retention_19",
"value": "1900",
"scale": "2",
"group": "retention",
@ -136,7 +136,7 @@
},
{
"name": "Retenc. 15%",
"code": "retencion_15",
"code": "retention_15",
"value": "1500",
"scale": "2",
"group": "retention",
@ -145,7 +145,7 @@
},
{
"name": "Retenc. 7%",
"code": "retencion_7",
"code": "retention_7",
"value": "700",
"scale": "2",
"group": "retention",
@ -154,7 +154,7 @@
},
{
"name": "Retenc. 2%",
"code": "retencion_2",
"code": "retention_2",
"value": "200",
"scale": "2",
"group": "retention",

View File

@ -1,6 +1,6 @@
{
"name": "@erp/customer-invoices",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -188,7 +188,8 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
return Result.ok(proformaPatchProps);
} catch (err: unknown) {
return Result.fail(new DomainError("Proforma props mapping failed", { cause: err }));
console.error(err);
return Result.fail(new DomainError("Proforma props mapping failed (update)", { cause: err }));
}
}

View File

@ -24,8 +24,7 @@ import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
import type { ProformaListRow } from "../../../shared";
import { useListProformasPageController } from "../../controllers";
import { ProformaSummaryPanel, ProformasGrid, useProformasGridColumns } from "../blocks";
import { OrdersTable } from "./orders-table";
import { ProformaStatusBadge } from "../components";
export const ListProformasPage = () => {
const { t } = useTranslation();
@ -86,7 +85,30 @@ export const ListProformasPage = () => {
pageSize={listCtrl.pageSize}
/>
<OrdersTable />
{/* Explicación técnica */}
<div className="mt-8 rounded border border-border bg-card p-3 sm:p-4 text-xs sm:text-sm text-muted-foreground space-y-2">
<p className="font-semibold text-foreground">Estado de proforma</p>
<ul className="list-disc list-inside space-y-1">
<li>
<ProformaStatusBadge status="draft" />
<strong className="text-foreground">Borrador:</strong> Un{" "}
<code className="rounded bg-muted px-1 text-xs">div</code> absoluto con{" "}
<code className="rounded bg-muted px-1 text-xs">pointer-events: none</code> y{" "}
<code className="rounded bg-muted px-1 text-xs">linear-gradient</code> se superpone
encima del scroll.
</li>
<li>
<strong className="text-foreground">Columna sticky:</strong> La celda de acciones usa{" "}
<code className="rounded bg-muted px-1 text-xs">sticky right-0 z-20</code>.
</li>
<li>
<strong className="text-foreground">Responsive:</strong> Columnas ocultas en móviles (
<code className="rounded bg-muted px-1 text-xs">hidden sm:</code>,{" "}
<code className="rounded bg-muted px-1 text-xs">hidden md:</code>,{" "}
<code className="rounded bg-muted px-1 text-xs">hidden lg:</code>).
</li>
</ul>
</div>
</div>
</div>
);

View File

@ -43,7 +43,7 @@ export const GetProformaByIdAdapter = {
recipient: mapRecipient(dto),
taxes: dto.taxes.map(mapTaxSummary),
paymentMethod: dto.payment_method?.id,
paymentMethodId: dto.payment_method?.id ?? null,
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
@ -85,24 +85,20 @@ const mapItem = (dto: GetProformaByIdResponseDTO["items"][number]): ProformaItem
taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount),
ivaCode: dto.iva_code,
ivaPercentage: PercentageDTOHelper.toNumber(dto.iva_percentage),
ivaAmount: MoneyDTOHelper.toNumber(dto.iva_amount),
taxCode: dto.iva_code,
taxPercentage: PercentageDTOHelper.toNumericNulleable(dto.iva_percentage),
taxAmount: MoneyDTOHelper.toNumber(dto.iva_amount),
recCode: dto.rec_code,
recPercentage: PercentageDTOHelper.toNumber(dto.rec_percentage),
recPercentage: PercentageDTOHelper.toNumericNulleable(dto.rec_percentage),
recAmount: MoneyDTOHelper.toNumber(dto.rec_amount),
retentionCode: dto.retention_code,
retentionPercentage: PercentageDTOHelper.toNumber(dto.retention_percentage),
retentionPercentage: PercentageDTOHelper.toNumericNulleable(dto.retention_percentage),
retentionAmount: MoneyDTOHelper.toNumber(dto.retention_amount),
taxesAmount: MoneyDTOHelper.toNumber(dto.taxes_amount),
totalAmount: MoneyDTOHelper.toNumber(dto.total_amount),
// Nota: el DTO de detalle actual no incluye `taxes` por línea.
// Dejamos fallback explícito para no inventar parseos.
taxes: "",
};
return item;

View File

@ -1,3 +1,2 @@
export * from "./payment-method-options.constants";
export * from "./retentions-options.constants";
export * from "./taxes-options.constants";
export * from "./proforma-fiscal-options.constants";

View File

@ -0,0 +1,90 @@
export type ProformaTaxPercentageOption = 21 | 10 | 4 | 0;
export type ProformaRecPercentageOption = 5.2 | 1.4 | 0.5 | 0;
export type ProformaRetentionPercentageOption = 15 | 7;
export interface ProformaSelectOption<TValue extends string = string> {
value: TValue;
label: string;
}
export interface ProformaTaxDefinition {
taxCode: string;
taxPercentage: ProformaTaxPercentageOption;
taxLabel: string;
recCode: string;
recPercentage: ProformaRecPercentageOption;
}
export interface ProformaRetentionDefinition {
retentionCode: string;
retentionPercentage: ProformaRetentionPercentageOption;
retentionLabel: string;
}
export const PROFORMA_TAX_DEFINITIONS: Record<ProformaTaxPercentageOption, ProformaTaxDefinition> =
{
21: {
taxCode: "iva_21",
taxPercentage: 21,
taxLabel: "General",
recCode: "rec_5_2",
recPercentage: 5.2,
},
10: {
taxCode: "iva_10",
taxPercentage: 10,
taxLabel: "Reducido",
recCode: "rec_1_4",
recPercentage: 1.4,
},
4: {
taxCode: "iva_4",
taxPercentage: 4,
taxLabel: "Superreducido",
recCode: "rec_0_5",
recPercentage: 0.5,
},
0: {
taxCode: "iva_0",
taxPercentage: 0,
taxLabel: "Exento",
recCode: "rec_0",
recPercentage: 0,
},
} as const;
export const PROFORMA_RETENTION_DEFINITIONS: Record<
ProformaRetentionPercentageOption,
ProformaRetentionDefinition
> = {
15: {
retentionCode: "retention_15",
retentionPercentage: 15,
retentionLabel: "General",
},
7: {
retentionCode: "retention_7",
retentionPercentage: 7,
retentionLabel: "Inicio actividad",
},
} as const;
export const PROFORMA_TAX_OPTIONS: ProformaSelectOption<`${ProformaTaxPercentageOption}`>[] = [
{ value: "0", label: "0%" },
{ value: "4", label: "4%" },
{ value: "10", label: "10%" },
{ value: "21", label: "21%" },
];
export const PROFORMA_REC_OPTIONS: ProformaSelectOption<`${ProformaRecPercentageOption}`>[] = [
{ value: "0", label: "0%" },
{ value: "0.5", label: "0,5%" },
{ value: "1.4", label: "1,4%" },
{ value: "5.2", label: "5,2%" },
];
export const PROFORMA_RETENTION_OPTIONS: ProformaSelectOption<`${ProformaRetentionPercentageOption}`>[] =
[
{ value: "15", label: "15%" },
{ value: "7", label: "7%" },
];

View File

@ -1,25 +0,0 @@
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%" },
];

View File

@ -1,37 +0,0 @@
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%" },
];

View File

@ -24,21 +24,19 @@ export interface ProformaItem {
taxableAmount: number;
ivaCode: string;
ivaPercentage: number;
ivaAmount: number;
taxCode: string | null;
taxPercentage: number | null;
taxAmount: number;
recCode: string | null;
recPercentage: number;
recPercentage: number | null;
recAmount: number;
retentionCode: string | null;
retentionPercentage: number;
retentionPercentage: number | null;
retentionAmount: number;
taxesAmount: number;
totalAmount: number;
taxes: string;
}

View File

@ -7,8 +7,8 @@
export interface ProformaTaxSummary {
taxableAmount: number;
ivaCode: string;
ivaPercentage: number;
ivaCode: string | null;
ivaPercentage: number | null;
ivaAmount: number;
recCode: string | null;

View File

@ -33,7 +33,7 @@ export interface Proforma {
taxes: ProformaTaxSummary[];
paymentMethod?: string;
paymentMethodId: string | null;
subtotalAmount: number;

View File

@ -1,5 +0,0 @@
import { PROFORMA_TAX_DEFINITIONS, type TaxPercentageOption } from "../constants";
export const getProformaRecPercentage = (taxPercentage: TaxPercentageOption): number => {
return PROFORMA_TAX_DEFINITIONS[taxPercentage].recPercentage;
};

View File

@ -1,3 +0,0 @@
import { PROFORMA_RETENTION_OPTIONS } from "../constants";
export const getProformaRetentionOptions = () => PROFORMA_RETENTION_OPTIONS;

View File

@ -1,3 +0,0 @@
import { PROFORMA_TAX_OPTIONS } from "../constants";
export const getProformaTaxOptions = () => PROFORMA_TAX_OPTIONS;

View File

@ -1,3 +1 @@
export * from "./get-proforma-rec-percentage";
export * from "./get-proforma-retention-options";
export * from "./get-proforma-tax-options";
export * from "./proforma-fiscal-options.utils";

View File

@ -0,0 +1,96 @@
import { PercentageHelper, isNullishOrEmpty } from "@repo/rdx-utils";
import {
PROFORMA_REC_OPTIONS,
PROFORMA_RETENTION_DEFINITIONS,
PROFORMA_RETENTION_OPTIONS,
PROFORMA_TAX_DEFINITIONS,
PROFORMA_TAX_OPTIONS,
type ProformaRecPercentageOption,
type ProformaRetentionPercentageOption,
type ProformaTaxPercentageOption,
} from "../constants";
export const parseProformaTaxPercentage = (
value: number | string | null | undefined
): ProformaTaxPercentageOption | null => {
if (isNullishOrEmpty(value)) {
return null;
}
const numericValue = Number(value);
const normalizedValue = PercentageHelper.normalizePercentage(numericValue);
if (normalizedValue in PROFORMA_TAX_DEFINITIONS) {
return normalizedValue as ProformaTaxPercentageOption;
}
return null;
};
export const parseProformaRetentionPercentage = (
value: number | string | null | undefined
): ProformaRetentionPercentageOption | null => {
if (isNullishOrEmpty(value)) {
return null;
}
const numericValue = Number(value);
const normalizedValue = PercentageHelper.normalizePercentage(numericValue);
if (normalizedValue in PROFORMA_RETENTION_DEFINITIONS) {
return normalizedValue as ProformaRetentionPercentageOption;
}
return null;
};
export const getProformaTaxCode = (
taxPercentage: number | string | null | undefined
): string | null => {
const parsedTaxPercentage = parseProformaTaxPercentage(taxPercentage);
if (parsedTaxPercentage === null) return null;
return PROFORMA_TAX_DEFINITIONS[parsedTaxPercentage].taxCode;
};
export const getProformaRecPercentage = (
taxPercentage: ProformaTaxPercentageOption | null | undefined
): ProformaRecPercentageOption | null => {
if (taxPercentage === null || taxPercentage === undefined) return null;
return PROFORMA_TAX_DEFINITIONS[taxPercentage].recPercentage;
};
export const getProformaRecCode = (
recPercentage: number | string | null | undefined
): string | null => {
if (recPercentage === null || recPercentage === undefined || recPercentage === "") {
return null;
}
const numericValue = Number(recPercentage);
const definition = Object.values(PROFORMA_TAX_DEFINITIONS).find(
(taxDefinition) => taxDefinition.recPercentage === numericValue
);
return definition?.recCode ?? null;
};
export const getProformaRetentionCode = (
retentionPercentage: number | string | null | undefined
): string | null => {
const parsedRetentionPercentage = parseProformaRetentionPercentage(retentionPercentage);
if (parsedRetentionPercentage === null) return null;
return PROFORMA_RETENTION_DEFINITIONS[parsedRetentionPercentage].retentionCode;
};
export const getProformaTaxOptions = () => PROFORMA_TAX_OPTIONS;
export const getProformaRecOptions = () => PROFORMA_REC_OPTIONS;
export const getProformaRetentionOptions = () => PROFORMA_RETENTION_OPTIONS;

View File

@ -23,7 +23,7 @@ export const mapProformaItemsToProformaItemsUpdateForm = (
itemDiscountPercentage: item.itemDiscountPercentage ?? null,
taxPercentage: item.ivaPercentage ?? null,
taxPercentage: item.taxPercentage ?? null,
recPercentage: item.recPercentage ?? null,
};
};

View File

@ -1,34 +1,24 @@
import { PercentageHelper } from "@repo/rdx-utils";
import type { Proforma, ProformaItem } from "../../shared";
import type { Proforma } from "../../shared";
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
import { buildProformaUpdateDefault } from "../utils";
import { mapProformaItemsToProformaItemsUpdateForm } from "./map-proforma-items-to-proforma-items-update-form.adapter";
interface FiscalDefaults {
taxMode: ProformaTaxMode;
defaultTaxPercentage: number | null;
defaultRecPercentage: number | null;
defaultRetentionPercentage: number | null;
}
/**
* Mapea una proforma a un formulario de actualización de proforma.
*/
export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpdateForm => {
const proformaDefaults = buildProformaUpdateDefault();
const taxMode = inferProformaTaxMode(proforma.items);
const defaultTaxSummary = proforma.taxes[0];
const firstTaxableItem = getFirstTaxableItem(proforma.items);
const defaultTaxPercentage =
defaultTaxSummary?.ivaPercentage ??
firstTaxableItem?.ivaPercentage ??
proformaDefaults.defaultTaxPercentage;
const defaultRecPercentage =
defaultTaxSummary?.recPercentage ?? firstTaxableItem?.recPercentage ?? null;
const defaultRetentionPercentage =
defaultTaxSummary?.retentionPercentage ?? firstTaxableItem?.retentionPercentage ?? null;
console.log({ defaultTaxPercentage, defaultRecPercentage, defaultRetentionPercentage, taxMode });
const fiscalDefaults = resolveFiscalDefaults(proforma, proformaDefaults);
return {
series: proforma.series ?? proformaDefaults.series,
@ -48,55 +38,56 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
globalDiscountPercentage:
proforma.globalDiscountPercentage ?? proformaDefaults.globalDiscountPercentage,
taxMode,
taxRegimeCode: proformaDefaults.taxRegimeCode,
taxMode: fiscalDefaults.taxMode,
taxRegimeCode: "01", //taxRegimeCode: proforma.taxRegimeCode ?? proformaDefaults.taxRegimeCode, // TODO: implementar en API
hasTaxPercentage: PercentageHelper.hasPositivePercentage(defaultTaxPercentage),
defaultTaxPercentage: defaultTaxPercentage,
hasTaxPercentage: fiscalDefaults.defaultTaxPercentage !== null,
taxPercentage: fiscalDefaults.defaultTaxPercentage,
hasRecPercentage: PercentageHelper.hasPositivePercentage(defaultRecPercentage),
defaultRecPercentage: defaultRecPercentage,
hasRecPercentage: fiscalDefaults.defaultRecPercentage !== null,
recPercentage: fiscalDefaults.defaultRecPercentage,
hasRetentionPercentage: PercentageHelper.hasPositivePercentage(defaultRetentionPercentage),
defaultRetentionPercentage: defaultRetentionPercentage,
hasRetentionPercentage: fiscalDefaults.defaultRetentionPercentage !== null,
retentionPercentage: fiscalDefaults.defaultRetentionPercentage,
paymentMethodId: proforma.paymentMethod ?? proformaDefaults.paymentMethodId,
paymentMethodId: proforma.paymentMethodId ?? proformaDefaults.paymentMethodId,
items: proforma.items.map(mapProformaItemsToProformaItemsUpdateForm),
};
};
const getFirstTaxableItem = (items: ProformaItem[]): ProformaItem | undefined => {
return items.find((item) => item.isValued) ?? items[0];
};
const resolveFiscalDefaults = (
proforma: Proforma,
proformaDefaults: ProformaUpdateForm
): FiscalDefaults => {
// Caso habitual: una sola combinación de impuestos
if (proforma.taxes.length === 1) {
const taxSummary = proforma.taxes[0];
const inferProformaTaxMode = (items: ProformaItem[]): ProformaTaxMode => {
const comparableItems = items.filter((item) => item.isValued);
if (comparableItems.length === 0) {
return "single";
return {
taxMode: "single",
defaultTaxPercentage: taxSummary.ivaCode === null ? null : taxSummary.ivaPercentage,
defaultRecPercentage: taxSummary.recCode === null ? null : taxSummary.recPercentage,
defaultRetentionPercentage:
taxSummary.retentionCode === null ? null : taxSummary.retentionPercentage,
};
}
const sourceItems = comparableItems;
// Caso excepcional: proforma sin impuestos
if (proforma.taxes.length === 0) {
return {
taxMode: "single",
defaultTaxPercentage: proformaDefaults.taxPercentage,
defaultRecPercentage: proformaDefaults.recPercentage,
defaultRetentionPercentage: proformaDefaults.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 =
ivaPercentages.length <= 1 && recPercentages.length <= 1 && retentionPercentages.length <= 1;
return hasSingleTaxSetup ? "single" : "perLine";
};
const uniquePercentageValues = (values: Array<number | null | undefined>): number[] => {
return Array.from(
new Set(
values
.filter((value): value is number => value !== null && value !== undefined)
.map((value) => PercentageHelper.normalizePercentage(value))
)
);
// Proforma con varias combinaciones de impuestos
return {
taxMode: "perLine",
defaultTaxPercentage: proformaDefaults.taxPercentage,
defaultRecPercentage: proformaDefaults.recPercentage,
defaultRetentionPercentage: proformaDefaults.retentionPercentage,
};
};

View File

@ -156,12 +156,8 @@ export const useUpdateProformaController = (
return;
}
console.log("Parche de actualización construido:", patchData);
const params = buildUpdateProformaByIdParams(proformaId, patchData, formData);
console.log("Enviando actualización con params:", params);
try {
// Enviamos cambios al servidor
const updated = await mutateAsync(params);
@ -224,7 +220,7 @@ export const useUpdateProformaController = (
}
},
(errors: FieldErrors<ProformaUpdateForm>) => {
console.log(errors);
console.error(errors);
focusFirstInputFormError(form);
showWarningToast(

View File

@ -2,8 +2,8 @@ import { useCallback, useEffect, useRef } from "react";
import { type UseFormReturn, useWatch } from "react-hook-form";
import {
type RetentionPercentageOption,
type TaxPercentageOption,
type ProformaRetentionPercentageOption,
type ProformaTaxPercentageOption,
getProformaRecPercentage,
} from "../../shared";
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
@ -16,13 +16,13 @@ export interface UseUpdateProformaTaxControllerResult {
taxMode: ProformaTaxMode;
hasTaxPercentage: boolean;
defaultTaxPercentage: number | null;
taxPercentage: number | null;
hasRecPercentage: boolean;
defaultRecPercentage: number | null;
recPercentage: number | null;
hasRetentionPercentage: boolean;
defaultRetentionPercentage: number | null;
retentionPercentage: number | null;
usesSingleTax: boolean;
usesPerLineTax: boolean;
@ -30,20 +30,20 @@ export interface UseUpdateProformaTaxControllerResult {
enablePerLineTaxes: () => void;
disablePerLineTaxes: () => void;
updateDefaultTaxPercentage: (newTaxPercentage: RetentionPercentageOption) => void;
updateDefaultRecPercentage: (enabled: boolean) => void;
updateDefaultRetentionPercentage: (enabled: boolean) => void;
updateTaxPercentage: (newTaxPercentage: ProformaTaxPercentageOption) => void;
updateRecPercentage: (enabled: boolean) => void;
updateRetentionPercentage: (newRetentionPercentage: ProformaRetentionPercentageOption) => void;
}
const resolveRecPercentage = (
enabled: boolean,
taxPercentage: RetentionPercentageOption | null
taxPercentage: ProformaTaxPercentageOption | null
): number | null => {
if (!enabled || taxPercentage === null) {
return null;
}
return getProformaRecPercentage(taxPercentage as TaxPercentageOption);
return getProformaRecPercentage(taxPercentage as ProformaTaxPercentageOption);
};
export const useUpdateProformaTaxController = ({
@ -54,14 +54,13 @@ export const useUpdateProformaTaxController = ({
const taxMode = useWatch({ control, name: "taxMode" });
const hasTaxPercentage = useWatch({ control, name: "hasTaxPercentage" }) ?? false;
const defaultTaxPercentage = useWatch({ control, name: "defaultTaxPercentage" }) ?? null;
const taxPercentage = useWatch({ control, name: "taxPercentage" }) ?? null;
const hasRecPercentage = useWatch({ control, name: "hasRecPercentage" }) ?? false;
const defaultRecPercentage = useWatch({ control, name: "defaultRecPercentage" }) ?? null;
const recPercentage = useWatch({ control, name: "recPercentage" }) ?? null;
const hasRetentionPercentage = useWatch({ control, name: "hasRetentionPercentage" }) ?? false;
const defaultRetentionPercentage =
useWatch({ control, name: "defaultRetentionPercentage" }) ?? null;
const retentionPercentage = useWatch({ control, name: "retentionPercentage" }) ?? null;
const hasMountedRef = useRef(false);
@ -72,14 +71,14 @@ export const useUpdateProformaTaxController = ({
const nextRecPercentage = resolveRecPercentage(
hasRecPercentage,
defaultTaxPercentage as RetentionPercentageOption | null
taxPercentage as ProformaTaxPercentageOption | null
);
const shouldMarkDirty = hasMountedRef.current;
currentItems.forEach((item, index) => {
if (item.taxPercentage !== defaultTaxPercentage) {
setValue(`items.${index}.taxPercentage`, defaultTaxPercentage, {
if (item.taxPercentage !== taxPercentage) {
setValue(`items.${index}.taxPercentage`, taxPercentage, {
shouldDirty: shouldMarkDirty,
shouldTouch: false,
shouldValidate: true,
@ -96,7 +95,7 @@ export const useUpdateProformaTaxController = ({
});
hasMountedRef.current = true;
}, [defaultTaxPercentage, getValues, hasRecPercentage, setValue, taxMode]);
}, [taxPercentage, getValues, hasRecPercentage, setValue, taxMode]);
const enablePerLineTaxes = useCallback(() => {
setValue("taxMode", "perLine", {
@ -114,20 +113,25 @@ export const useUpdateProformaTaxController = ({
});
}, [setValue]);
const updateDefaultTaxPercentage = useCallback(
(newTaxPercentage: RetentionPercentageOption): void => {
setValue("defaultTaxPercentage", newTaxPercentage, {
const updateTaxPercentage = useCallback(
(newTaxPercentage: ProformaTaxPercentageOption): void => {
setValue("taxPercentage", newTaxPercentage, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
});
setValue("hasTaxPercentage", true, {
shouldDirty: true,
shouldValidate: true,
});
const isRecPercentageEnabled = getValues("hasRecPercentage");
if (isRecPercentageEnabled) {
setValue(
"defaultRecPercentage",
getProformaRecPercentage(newTaxPercentage as TaxPercentageOption),
"recPercentage",
getProformaRecPercentage(newTaxPercentage as ProformaTaxPercentageOption),
{
shouldDirty: true,
shouldValidate: true,
@ -138,7 +142,7 @@ export const useUpdateProformaTaxController = ({
[getValues, setValue]
);
const updateDefaultRecPercentage = useCallback(
const updateRecPercentage = useCallback(
(enabled: boolean): void => {
setValue("hasRecPercentage", enabled, {
shouldDirty: true,
@ -146,9 +150,9 @@ export const useUpdateProformaTaxController = ({
shouldValidate: true,
});
const taxPercentage = getValues("defaultTaxPercentage") as RetentionPercentageOption | null;
const taxPercentage = getValues("taxPercentage") as ProformaTaxPercentageOption | null;
setValue("defaultRecPercentage", resolveRecPercentage(enabled, taxPercentage), {
setValue("recPercentage", resolveRecPercentage(enabled, taxPercentage), {
shouldDirty: true,
shouldValidate: true,
});
@ -156,13 +160,18 @@ export const useUpdateProformaTaxController = ({
[getValues, setValue]
);
const updateDefaultRetentionPercentage = useCallback(
(enabled: boolean): void => {
setValue("hasRetentionPercentage", enabled, {
const updateRetentionPercentage = useCallback(
(newRetentionPercentage: ProformaRetentionPercentageOption): void => {
setValue("retentionPercentage", newRetentionPercentage, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
});
setValue("hasRetentionPercentage", true, {
shouldDirty: true,
shouldValidate: true,
});
},
[setValue]
);
@ -171,13 +180,13 @@ export const useUpdateProformaTaxController = ({
taxMode,
hasTaxPercentage,
defaultTaxPercentage,
taxPercentage,
hasRecPercentage,
defaultRecPercentage,
recPercentage,
hasRetentionPercentage,
defaultRetentionPercentage,
retentionPercentage,
usesSingleTax: taxMode === "single",
usesPerLineTax: taxMode === "perLine",
@ -185,8 +194,8 @@ export const useUpdateProformaTaxController = ({
enablePerLineTaxes,
disablePerLineTaxes,
updateDefaultTaxPercentage,
updateDefaultRecPercentage,
updateDefaultRetentionPercentage,
updateTaxPercentage,
updateRecPercentage,
updateRetentionPercentage,
};
};

View File

@ -21,7 +21,7 @@ export const useUpdateProformaTotalsController = ({
const items = useWatch({ control, name: "items" });
const hasRetentionPercentage = useWatch({ control, name: "hasRetentionPercentage" });
const retentionPercentage = useWatch({ control, name: "defaultRetentionPercentage" });
const retentionPercentage = useWatch({ control, name: "retentionPercentage" });
const totals = useMemo(() => {
return calculateProformaTotals({

View File

@ -5,4 +5,3 @@ export * from "./proforma-item-update-patch.entity";
export * from "./proforma-update-form.entity";
export * from "./proforma-update-form.schema";
export * from "./proforma-update-patch.entity";
export * from "./proforma-update-totals.entity";

View File

@ -38,13 +38,13 @@ export interface ProformaUpdateForm {
taxRegimeCode: string | null;
hasTaxPercentage: boolean;
defaultTaxPercentage: number | null;
taxPercentage: number | null;
hasRecPercentage: boolean;
defaultRecPercentage: number | null;
recPercentage: number | null;
hasRetentionPercentage: boolean;
defaultRetentionPercentage: number | null;
retentionPercentage: number | null;
paymentMethodId: string | null;

View File

@ -39,13 +39,13 @@ export const ProformaUpdateFormSchema = z
taxRegimeCode: z.string(),
hasTaxPercentage: z.boolean(),
defaultTaxPercentage: z.number().nullable(),
taxPercentage: z.number().nullable(),
hasRecPercentage: z.boolean(),
defaultRecPercentage: z.number().nullable(),
recPercentage: z.number().nullable(),
hasRetentionPercentage: z.boolean(),
defaultRetentionPercentage: z.number().nullable(),
retentionPercentage: z.number().nullable(),
paymentMethodId: z.string().nullable(),
@ -55,7 +55,7 @@ export const ProformaUpdateFormSchema = z
(formValues) => {
if (
formValues.hasRetentionPercentage &&
!PercentageHelper.hasPositivePercentage(formValues.defaultRetentionPercentage)
!PercentageHelper.hasPositivePercentage(formValues.retentionPercentage)
) {
return false;
}

View File

@ -29,13 +29,13 @@ export type ProformaUpdatePatch = {
taxRegimeCode?: string | null;
hasTaxPercentage?: boolean;
defaultTaxPercentage?: number | null;
taxPercentage?: number | null;
hasRecPercentage?: boolean;
defaultRecPercentage?: number | null;
recPercentage?: number | null;
hasRetentionPercentage?: boolean;
defaultRetentionPercentage?: number | null;
retentionPercentage?: number | null;
paymentMethodId?: string | null;

View File

@ -1,17 +0,0 @@
import type { ProformaHeaderTotals } from "./proforma-calculation.entity";
export interface ProformaTaxBreakdownLine {
taxableBase: number;
ivaPercentage: number;
ivaAmount: number;
recPercentage: number | null;
recAmount: number;
}
export interface ProformaTotals extends ProformaHeaderTotals {
recPercentage: number | null;
recAmount: number;
retentionPercentage: number | null;
retentionAmount: number;
}

View File

@ -113,8 +113,8 @@ export const ProformaLineEditor = ({
},
{
id: "itemDiscountPercentage",
header: t("form_fields.items.discount_percentage.label", "% Dto"),
headClassName: "w-[100px] text-right",
header: t("form_fields.items.discount_percentage.label", "Dto (%)"),
headClassName: "w-[100px] text-left",
cell: ({ index }) => (
<PercentageField
inputClassName="border-none"
@ -129,8 +129,8 @@ export const ProformaLineEditor = ({
? [
{
id: "taxPercentage",
header: t("form_fields.items.tax_percentage.label", "IVA"),
headClassName: "w-[110px] text-right",
header: t("form_fields.items.tax_percentage.label", "IVA (%)"),
headClassName: "w-[110px] text-left",
cell: ({ index }) => (
<SelectField
deserialize={(value) => (value === null || value === "" ? null : Number(value))}

View File

@ -15,7 +15,6 @@ import type {
} from "../../controllers";
import { ProformaTotalsSummary } from "../blocks";
import { ProformaTaxesCard } from "./proforma-taxes-card";
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
import { ProformaUpdateItemsEditor } from "./proforma-update-items-editor";
import { ProformaUpdateTaxEditor } from "./proforma-update-tax-editor";
@ -95,10 +94,6 @@ export const ProformaUpdateEditorForm = ({
/>
</div>
<div className="grid grid-cols-1">
<ProformaTaxesCard />
</div>
<div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">
<Button disabled={isSubmitting} onClick={onReset} type="button" variant="outline">
{t("common.reset", "Restablecer")}

View File

@ -23,7 +23,12 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
import { ReceiptTextIcon } from "lucide-react";
import { useTranslation } from "../../../../i18n";
import { getProformaRetentionOptions, getProformaTaxOptions } from "../../../shared";
import {
getProformaRetentionOptions,
getProformaTaxOptions,
parseProformaRetentionPercentage,
parseProformaTaxPercentage,
} from "../../../shared";
import type { UseUpdateProformaTaxControllerResult } from "../../controllers";
interface ProformaUpdateTaxEditorProps {
@ -167,7 +172,13 @@ export const ProformaUpdateTaxEditor = ({
inputClassName="bg-background"
items={getProformaTaxOptions()}
label={t("form_fields.proformas.default_tax_percentage.label", "IVA por defecto")}
name="defaultTaxPercentage"
name="taxPercentage"
onChange={(value) => {
const parsed = parseProformaTaxPercentage(value as number | null);
if (parsed !== null) {
taxCtrl.updateTaxPercentage(parsed);
}
}}
placeholder={t(
"form_fields.proformas.default_tax_percentage.placeholder",
"Selecciona IVA"
@ -198,13 +209,13 @@ export const ProformaUpdateTaxEditor = ({
{taxCtrl.hasRecPercentage ? (
<span className="text-sm text-muted-foreground">
{PercentageHelper.formatPercent(taxCtrl.defaultRecPercentage ?? 0)}
{PercentageHelper.formatPercent(taxCtrl.recPercentage ?? 0)}
</span>
) : null}
</>
}
name="hasRecPercentage"
onCheckedChange={taxCtrl.updateDefaultRecPercentage}
onCheckedChange={taxCtrl.updateRecPercentage}
readOnly={readOnly}
/>
@ -223,7 +234,13 @@ export const ProformaUpdateTaxEditor = ({
inputClassName="bg-background"
items={getProformaRetentionOptions()}
label={t("form_fields.proformas.retention_percentage.label", "Retención")}
name="defaultRetentionPercentage"
name="retentionPercentage"
onChange={(value) => {
const parsed = parseProformaRetentionPercentage(value as number | null);
if (parsed !== null) {
taxCtrl.updateRetentionPercentage(parsed);
}
}}
placeholder={t(
"form_fields.proformas.default_tax_percentage.placeholder",
"Selecciona IVA"

View File

@ -22,13 +22,13 @@ export const buildProformaUpdateDefault = (): ProformaUpdateForm => {
taxRegimeCode: "01",
hasTaxPercentage: true,
defaultTaxPercentage: 21,
taxPercentage: 21,
hasRecPercentage: false,
defaultRecPercentage: null,
recPercentage: null,
hasRetentionPercentage: false,
defaultRetentionPercentage: 15,
retentionPercentage: 15,
paymentMethodId: null,

View File

@ -2,13 +2,17 @@ import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/cor
import { toNullable } from "@repo/rdx-ddd";
import { ObjectHelper } from "@repo/rdx-utils";
import { getProformaRecCode, getProformaRetentionCode, getProformaTaxCode } from "../../shared";
import type { UpdateProformaByIdParams } from "../../shared/api";
import type {
ProformaItemUpdateForm,
ProformaItemUpdatePatch,
ProformaUpdateForm,
ProformaUpdatePatch,
} from "../entities";
import type { ProformaItemUpdateForm, ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
interface ProformaItemUpdateDTOContext {
currencyCode: string;
taxMode: ProformaUpdateForm["taxMode"];
ivaCode: string | null;
recCode: string | null;
retentionCode: string | null;
}
/**
* Convierte el patch del formulario de actualización de proforma
@ -85,9 +89,8 @@ export const buildUpdateProformaByIdParams = (
// Si se han tocado los detalles, se envían todos
if (shouldReplaceItems(patch)) {
data.items = formData.items.map((item, index) =>
toProformaItemUpdateDTO(item, index, formData)
);
const context = buildProformaItemUpdateDTOContext(formData);
data.items = formData.items.map((item, index) => toProformaItemUpdateDTO(item, index, context));
}
return {
@ -97,41 +100,40 @@ export const buildUpdateProformaByIdParams = (
};
const toProformaItemUpdateDTO = (
item: ProformaItemUpdatePatch,
item: ProformaItemUpdateForm,
index: number,
formData: ProformaUpdateForm
context: ProformaItemUpdateDTOContext
): NonNullable<UpdateProformaByIdParams["data"]["items"]>[number] => {
const currencyCode = formData.currencyCode;
const quantity =
item.quantity === null ? null : QuantityDTOHelper.fromNumberNulleable(item.quantity, 2);
const unit_amount =
item.unitAmount === null
? null
: MoneyDTOHelper.fromNumberNulleable(item.unitAmount, currencyCode, 4);
const ivaPercentage = resolveLineIvaPercentage(item, formData);
const recPercentage = resolveLineRecPercentage(item, formData);
const retentionPercentage = resolveLineRetentionPercentage(formData);
return {
position: index,
description: item.description,
quantity,
unit_amount,
quantity: toNullable(item.quantity, (value) => QuantityDTOHelper.fromNumberNulleable(value, 2)),
unit_amount: toNullable(item.unitAmount, (value) =>
MoneyDTOHelper.fromNumberNulleable(value, context.currencyCode, 4)
),
item_discount_percentage: toNullable(item.itemDiscountPercentage, (value) =>
PercentageDTOHelper.fromNumber(value, 2)
),
iva_percentage: toNullable(ivaPercentage, (value) => PercentageDTOHelper.fromNumber(value, 2)),
rec_percentage: toNullable(recPercentage, (value) => PercentageDTOHelper.fromNumber(value, 2)),
retention_percentage: toNullable(retentionPercentage, (value) =>
PercentageDTOHelper.fromNumber(value, 2)
),
iva_code: resolveItemIvaCode(item, context),
rec_code: resolveItemRecCode(item, context),
retention_code: context.retentionCode,
};
};
const buildProformaItemUpdateDTOContext = (
formData: ProformaUpdateForm
): ProformaItemUpdateDTOContext => {
return {
currencyCode: formData.currencyCode,
taxMode: formData.taxMode,
ivaCode: formData.taxMode === "single" ? resolveSingleIvaCode(formData) : null,
recCode: formData.taxMode === "single" ? resolveSingleRecCode(formData) : null,
retentionCode: resolveRetentionCode(formData),
};
};
@ -140,38 +142,56 @@ const shouldReplaceItems = (patch: ProformaUpdatePatch): boolean => {
ObjectHelper.hasOwn(patch, "items") ||
ObjectHelper.hasOwn(patch, "taxMode") ||
ObjectHelper.hasOwn(patch, "hasTaxPercentage") ||
ObjectHelper.hasOwn(patch, "defaultTaxPercentage") ||
ObjectHelper.hasOwn(patch, "taxPercentage") ||
ObjectHelper.hasOwn(patch, "hasRecPercentage") ||
ObjectHelper.hasOwn(patch, "defaultRecPercentage")
ObjectHelper.hasOwn(patch, "recPercentage") ||
ObjectHelper.hasOwn(patch, "hasRetentionPercentage") ||
ObjectHelper.hasOwn(patch, "retentionPercentage")
);
};
const resolveLineIvaPercentage = (
item: ProformaItemUpdateForm,
formData: ProformaUpdateForm
): number | null => {
if (formData.taxMode === "single") {
return formData.hasTaxPercentage ? formData.defaultTaxPercentage : null;
const resolveSingleIvaCode = (formData: ProformaUpdateForm): string | null => {
if (!formData.hasTaxPercentage) {
return null;
}
return item.taxPercentage;
return getProformaTaxCode(formData.taxPercentage);
};
const resolveLineRecPercentage = (
item: ProformaItemUpdateForm,
formData: ProformaUpdateForm
): number | null => {
if (formData.taxMode === "single") {
return formData.hasRecPercentage ? formData.defaultRecPercentage : null;
const resolveSingleRecCode = (formData: ProformaUpdateForm): string | null => {
if (!formData.hasRecPercentage) {
return null;
}
return item.recPercentage;
return getProformaRecCode(formData.recPercentage);
};
const resolveLineRetentionPercentage = (formData: ProformaUpdateForm): number | null => {
return formData.hasRetentionPercentage ? formData.defaultRetentionPercentage : null;
const resolveRetentionCode = (formData: ProformaUpdateForm): string | null => {
if (!formData.hasRetentionPercentage) {
return null;
}
return getProformaRetentionCode(formData.retentionPercentage);
};
const toPercentageDTOOrNull = (value: number | null) => {
return value === null ? null : PercentageDTOHelper.fromNumber(value, 2);
const resolveItemIvaCode = (
item: ProformaItemUpdateForm,
context: ProformaItemUpdateDTOContext
): string | null => {
if (context.taxMode === "single") {
return context.ivaCode;
}
return getProformaTaxCode(item.taxPercentage);
};
const resolveItemRecCode = (
item: ProformaItemUpdateForm,
context: ProformaItemUpdateDTOContext
): string | null => {
if (context.taxMode === "single") {
return context.recCode;
}
return getProformaRecCode(item.recPercentage);
};

View File

@ -5,21 +5,15 @@ import { calculateProformaTotalsFromLines } from "./calculations";
export interface CalculateProformaTotalsParams {
globalDiscountPercentage: ProformaUpdateForm["globalDiscountPercentage"];
items: ProformaItemUpdateForm[];
hasRetentionPercentage: boolean;
retentionPercentage: number | null;
}
export const calculateProformaTotals = ({
globalDiscountPercentage,
items,
hasRetentionPercentage,
retentionPercentage,
}: CalculateProformaTotalsParams): ProformaTotals => {
const lines = mapProformaItemFormToProformaLineInputs(items, {
@ -28,9 +22,7 @@ export const calculateProformaTotals = ({
return calculateProformaTotalsFromLines({
lines,
globalDiscountPercentage,
retentionPercentage: hasRetentionPercentage ? retentionPercentage : null,
});
};

View File

@ -1,7 +1,7 @@
{
"name": "@erp/customers",
"description": "Customers",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/factuges",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/supplier-invoices",
"description": "Supplier invoices",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/suppliers",
"description": "Suppliers",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "uecko-erp-2025",
"private": true,
"version": "0.6.5",
"version": "0.6.6",
"workspaces": [
"apps/*",
"modules/*",

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-criteria",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-ddd",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-logger",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-ui",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -36,6 +36,7 @@ type SelectFieldProps<TFormValues extends FieldValues> = {
orientation?: "vertical" | "horizontal" | "responsive";
onChange?: (value: unknown) => void;
serialize?: (value: unknown) => string;
deserialize?: (value: SelectFieldValue) => unknown;
@ -59,6 +60,7 @@ export function SelectField<TFormValues extends FieldValues>({
orientation = "vertical",
onChange,
serialize,
deserialize,
@ -102,7 +104,11 @@ export function SelectField<TFormValues extends FieldValues>({
<Select
disabled={isDisabled}
onValueChange={(value) => field.onChange(deserializeValue(value))}
onValueChange={(value) =>
onChange &&
onChange(deserializeValue(value)) &&
field.onChange(deserializeValue(value))
}
value={fieldValue}
>
<SelectTrigger

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-utils",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"type": "module",
"sideEffects": false,