This commit is contained in:
David Arranz 2026-07-15 13:10:26 +02:00
parent 0129c5963c
commit a810312f01
32 changed files with 149 additions and 77 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@erp/factuges-server",
"version": "0.9.0",
"version": "0.9.1",
"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.9.0",
"version": "0.9.1",
"type": "module",
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",

View File

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

View File

@ -1,7 +1,7 @@
{
"name": "@erp/catalogs",
"description": "Catalogs module",
"version": "0.9.0",
"version": "0.9.1",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/companies",
"description": "Companies module",
"version": "0.9.0",
"version": "0.9.1",
"private": true,
"type": "module",
"sideEffects": false,

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { Maybe, Result } from "@repo/rdx-utils";
import {
type IProformaCreateProps,
@ -55,11 +55,11 @@ export class ProformaCreator implements IProformaCreator {
return Result.fail(numberResult.error);
}
const invoiceNumber = numberResult.data;
const proformaReference = numberResult.data;
// 2. Crear agregado
const proformaResult = Proforma.create(
{ ...resolvedProps.data, proformaReference: invoiceNumber, companyId },
{ ...resolvedProps.data, proformaReference, companyId },
id
);
@ -81,11 +81,17 @@ export class ProformaCreator implements IProformaCreator {
private async resolveCreateProps(
props: ProformaCreateInputProps
): Promise<Result<Omit<IProformaCreateProps, "invoiceNumber">, Error>> {
): Promise<Result<Omit<IProformaCreateProps, "proformaReference">, Error>> {
// TODO: Esto hay que cambiarlo en el futuro para buscar valores por defecto, desde la empresa o desde el cliente
const _newProps = {
...props,
taxRegimeCode: props.taxRegimeCode.isNone() ? Maybe.some("01") : props.taxRegimeCode,
};
// Tax Regime => comprobar que existe
const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
companyId: props.companyId,
code: props.taxRegimeCode,
companyId: _newProps.companyId,
code: _newProps.taxRegimeCode,
});
if (taxRegimeResult.isFailure) {
@ -94,8 +100,8 @@ export class ProformaCreator implements IProformaCreator {
// Payment method => comprobar que existe
const paymentMethodResult = await this.deps.paymentResolver.ensurePaymentMethodById({
companyId: props.companyId,
id: props.paymentMethodId,
companyId: _newProps.companyId,
id: _newProps.paymentMethodId,
});
if (paymentMethodResult.isFailure) {
@ -107,8 +113,8 @@ export class ProformaCreator implements IProformaCreator {
for (const item of props.items) {
const taxes = await this.deps.taxResolver.ensureItemTaxes({
companyId: props.companyId,
atDate: props.proformaDate,
companyId: _newProps.companyId,
atDate: _newProps.proformaDate,
taxCodes: item.taxes,
});
@ -123,7 +129,7 @@ export class ProformaCreator implements IProformaCreator {
}
return Result.ok({
...props,
..._newProps,
items: resolvedItems,
});
}

View File

@ -8,6 +8,7 @@ import {
} from "@repo/rdx-ddd";
import {
focusFirstInputFormError,
getFirstFormErrorMessage,
showErrorToast,
showSuccessToast,
showWarningToast,
@ -15,6 +16,7 @@ import {
import { useEffect, useId, useMemo, useState } from "react";
import type { FieldErrors } from "react-hook-form";
import { useTranslation } from "../../../i18n";
import type { Proforma } from "../../shared";
import {
buildInvoiceSeriesSelectItems,
@ -42,6 +44,7 @@ const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection
};
export const useCreateProformaController = (options?: UseCreateProformaControllerOptions) => {
const { t } = useTranslation();
const formId = useId();
const [selectedCustomer, setSelectedCustomer] = useState<CustomerSelectionOption | null>(null);
@ -70,9 +73,7 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle
const defaultSeries = useMemo(() => {
const invoiceSeries = seriesQuery.data ?? [];
return (
invoiceSeries.find((series) => series.isDefault)?.code ?? invoiceSeries[0]?.code ?? ""
);
return invoiceSeries.find((series) => series.isDefault)?.code ?? invoiceSeries[0]?.code ?? "";
}, [seriesQuery.data]);
useEffect(() => {
@ -140,8 +141,13 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle
if (isValidationErrorCollection(normalizedError)) {
applyValidationErrorCollection(form, normalizedError);
focusFirstInputFormError(form);
showWarningToast("Revisa los campos", "Hay errores de validación en el formulario.");
focusFirstInputFormError(form.formState.errors, form);
const errorMessage = getFirstFormErrorMessage(form.formState.errors, form);
showWarningToast(
"Revisa los campos",
errorMessage ??
t("forms.validation.message", "Hay errores de validación en el formulario.")
);
return;
}
@ -149,8 +155,12 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle
}
},
(_errors: FieldErrors<ProformaCreateForm>) => {
focusFirstInputFormError(form);
showWarningToast("Revisa los campos", "Hay errores de validación en el formulario.");
focusFirstInputFormError(_errors, form);
const errorMessage = getFirstFormErrorMessage(_errors, form);
showWarningToast(
"Revisa los campos",
errorMessage ?? t("forms.validation.message", "Hay errores de validación en el formulario.")
);
}
);

View File

@ -4,9 +4,13 @@ import { buildProformaUpdateDefault } from "../utils";
import { mapProformaItemsToProformaItemsUpdateForm } from "./map-proforma-items-to-proforma-items-update-form.adapter";
interface FiscalDefaults {
interface FiscalState {
taxMode: ProformaTaxMode;
hasTaxPercentage: boolean;
hasRecPercentage: boolean;
hasRetentionPercentage: boolean;
defaultTaxPercentage: number | null;
defaultRecPercentage: number | null;
defaultRetentionPercentage: number | null;
@ -18,7 +22,7 @@ interface FiscalDefaults {
export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpdateForm => {
const proformaDefaults = buildProformaUpdateDefault();
const fiscalDefaults = resolveFiscalDefaults(proforma, proformaDefaults);
const fiscalState = resolveFiscalState(proforma, proformaDefaults);
return {
proformaReference: proforma.proformaReference,
@ -40,18 +44,17 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
globalDiscountPercentage:
proforma.globalDiscountPercentage ?? proformaDefaults.globalDiscountPercentage,
taxMode: fiscalDefaults.taxMode,
taxMode: fiscalState.taxMode,
taxRegimeCode: proforma.taxRegimeCode ?? proformaDefaults.taxRegimeCode,
hasTaxPercentage:
fiscalDefaults.taxMode === "single" && fiscalDefaults.defaultTaxPercentage !== null,
taxPercentage: fiscalDefaults.defaultTaxPercentage,
hasTaxPercentage: fiscalState.hasTaxPercentage,
taxPercentage: fiscalState.defaultTaxPercentage,
hasRecPercentage: fiscalDefaults.defaultRecPercentage !== null,
recPercentage: fiscalDefaults.defaultRecPercentage,
hasRecPercentage: fiscalState.hasRecPercentage,
recPercentage: fiscalState.defaultRecPercentage,
hasRetentionPercentage: fiscalDefaults.defaultRetentionPercentage !== null,
retentionPercentage: fiscalDefaults.defaultRetentionPercentage,
hasRetentionPercentage: fiscalState.defaultRetentionPercentage !== null,
retentionPercentage: fiscalState.defaultRetentionPercentage,
paymentMethodId: proforma.paymentMethodId,
paymentTermId: proforma.paymentTermId,
@ -60,20 +63,26 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
};
};
const resolveFiscalDefaults = (
const resolveFiscalState = (
proforma: Proforma,
proformaDefaults: ProformaUpdateForm
): FiscalDefaults => {
): FiscalState => {
// Caso habitual: una sola combinación de impuestos
if (proforma.taxes.length === 1) {
const taxSummary = proforma.taxes[0];
const hasTaxPercentage = taxSummary.ivaCode !== null;
const hasRecPercentage = taxSummary.recCode !== null;
const hasRetentionPercentage = taxSummary.retentionCode !== null;
return {
taxMode: "single",
defaultTaxPercentage: taxSummary.ivaCode === null ? null : taxSummary.ivaPercentage,
defaultRecPercentage: taxSummary.recCode === null ? null : taxSummary.recPercentage,
defaultRetentionPercentage:
taxSummary.retentionCode === null ? null : taxSummary.retentionPercentage,
defaultTaxPercentage: hasTaxPercentage ? taxSummary.ivaPercentage : null,
defaultRecPercentage: hasRecPercentage ? taxSummary.recPercentage : null,
defaultRetentionPercentage: hasRetentionPercentage ? taxSummary.retentionPercentage : null,
hasTaxPercentage,
hasRecPercentage,
hasRetentionPercentage,
};
}
@ -81,17 +90,26 @@ const resolveFiscalDefaults = (
if (proforma.taxes.length === 0) {
return {
taxMode: "single",
defaultTaxPercentage: proformaDefaults.taxPercentage,
defaultRecPercentage: proformaDefaults.recPercentage,
defaultRetentionPercentage: proformaDefaults.retentionPercentage,
defaultTaxPercentage: null,
defaultRecPercentage: null,
defaultRetentionPercentage: null,
hasTaxPercentage: false,
hasRecPercentage: false,
hasRetentionPercentage: false,
};
}
// Proforma con varias combinaciones de impuestos
const taxSummary = proforma.taxes[0];
const hasRecPercentage = taxSummary.recCode !== null;
return {
taxMode: "perLine",
defaultTaxPercentage: proformaDefaults.taxPercentage,
defaultRecPercentage: proformaDefaults.recPercentage,
defaultRetentionPercentage: proformaDefaults.retentionPercentage,
defaultTaxPercentage: null,
defaultRecPercentage: hasRecPercentage ? taxSummary.recPercentage : null,
defaultRetentionPercentage: null,
hasTaxPercentage: false,
hasRecPercentage,
hasRetentionPercentage: false,
};
};

View File

@ -5,6 +5,7 @@ import type { CustomerSelectionOption } from "@erp/customers";
import { type ValidationErrorCollection, isValidationErrorCollection } from "@repo/rdx-ddd";
import {
focusFirstInputFormError,
getFirstFormErrorMessage,
showErrorToast,
showSuccessToast,
showWarningToast,
@ -14,7 +15,6 @@ import type { FieldErrors } from "react-hook-form";
import { useTranslation } from "../../../i18n";
import type { UpdateProformaByIdParams } from "../../shared";
import type { Proforma } from "../../shared/entities";
import {
buildInvoiceSeriesSelectItems,
ensureCurrentValueInSelectItems,
@ -22,6 +22,7 @@ import {
useProformaGetQuery,
useProformaUpdateMutation,
} from "../../shared";
import type { Proforma } from "../../shared/entities";
import { mapProformaToProformaUpdateForm, mapProformaToSelectedCustomer } from "../adapters";
import { type ProformaUpdateForm, ProformaUpdateFormSchema } from "../entities";
import {
@ -147,8 +148,8 @@ export const useUpdateProformaController = (
if (!formHasAnyDirty(form.formState.dirtyFields)) {
showWarningToast(
t("proformas.update.no_changes.title"),
t("proformas.update.no_changes.message")
t("pages.update.no_changes.title", "Sin cambios"),
t("pages.update.no_changes.message", "No has realizado ningún cambio.")
);
return;
}
@ -181,8 +182,8 @@ export const useUpdateProformaController = (
if (options?.successToasts !== false) {
showSuccessToast(
t("proformas.update.success.title"),
t("proformas.update.success.message")
t("pages.update.success.title", "Guardado"),
t("pages.update.success.message", "Los cambios se han guardado correctamente.")
);
}
@ -208,7 +209,7 @@ export const useUpdateProformaController = (
console.log("Errores de validación aplicados al form:", form.formState.errors);
focusFirstInputFormError(form);
focusFirstInputFormError(form.formState.errors, form);
if (options?.errorToasts !== false) {
showWarningToast(
@ -230,11 +231,12 @@ export const useUpdateProformaController = (
}
},
(_errors: FieldErrors<ProformaUpdateForm>) => {
focusFirstInputFormError(form);
focusFirstInputFormError(_errors, form);
const errorMessage = getFirstFormErrorMessage(_errors, form);
showWarningToast(
t("forms.validation.title", "Revisa los campos"),
t("forms.validation.message", "Hay errores de validación en el formulario.")
errorMessage ?? t("forms.validation.message", "Hay errores de validación en el formulario.")
);
}
);

View File

@ -230,6 +230,14 @@ export const useUpdateProformaItemsController = ({
);
const itemErrors = React.useMemo<ProformaItemError[]>(() => {
//console.log("itemErrors => ", errors.items);
if (!errors.items) return [];
if (errors.items.root) {
return [errors.items.root];
//console.error("Errores de validación en items.root:", errors.items.root);
}
if (!Array.isArray(errors.items)) return [];
return errors.items.map((error) => error ?? {});

View File

@ -19,6 +19,7 @@ import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema
export const ProformaUpdateFormSchema = z
.object({
proformaReference: z.string(),
series: z.string(),
proformaDate: z.string().min(1),
@ -50,7 +51,9 @@ export const ProformaUpdateFormSchema = z
paymentMethodId: z.string().nullable(),
paymentTermId: z.string().nullable(),
items: z.array(ProformaItemUpdateFormSchema).min(1),
items: z
.array(ProformaItemUpdateFormSchema)
.min(1, "Debe haber al menos un item en la proforma"),
})
.refine(
(formValues) => {

View File

@ -1,4 +1,5 @@
import { FormSectionCard } from "@repo/rdx-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { ListIcon } from "lucide-react";
import type { ComponentProps } from "react";
@ -24,10 +25,13 @@ export const ProformaUpdateItemsEditor = ({
...props
}: ProformaUpdateItemsEditorProps) => {
const { t } = useTranslation();
const { totals } = totalsCtrl;
const { itemErrors } = itemsCtrl;
return (
<FormSectionCard
className={cn(itemErrors.length ? "bg-red-100" : "")}
disabled={disabled}
icon={<ListIcon className="size-3" />}
title={t("form_groups.items.title", "Líneas de detalle")}

View File

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

View File

@ -274,12 +274,13 @@ export default (database: Sequelize) => {
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
{ name: "idx_factuges", fields: ["factuges_id"], unique: true }, // <- para el proceso python
// Para búsquedas simples => se hace con el "CriteriaToSequelizeConverter"
/*{
name: "ft_customer",
// Quiksearch => "CriteriaToSequelizeConverter" construye la sentencia SQL de búsqueda FULLTEXT
// a partir de este índice. Las columnas deben coincidir con los campos de búsqueda rápida definidos en el repositorio.
{
name: "idx_qs_customer",
type: "FULLTEXT",
fields: ["name", "trade_name", "reference", "tin", "email_primary", "mobile_primary"],
},*/
},
],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope

View File

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

View File

@ -1,7 +1,7 @@
{
"name": "@erp/identity",
"description": "Identity module",
"version": "0.9.0",
"version": "0.9.1",
"private": true,
"type": "module",
"sideEffects": false,

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@repo/i18next",
"version": "0.9.0",
"version": "0.9.1",
"private": true,
"type": "module",
"scripts": {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
import type { FieldErrors, FieldValues, Path, UseFormReturn } from "react-hook-form";
export const focusFirstInputFormError = <T extends FieldValues>(form: UseFormReturn<T>) => {
const errors = form.formState.errors;
export const focusFirstInputFormError = <T extends FieldValues>(
errors: FieldErrors<T>,
form: UseFormReturn<T>
) => {
const firstKey = Object.keys(errors)[0] as keyof T | undefined;
if (firstKey) {

View File

@ -0,0 +1,17 @@
import type { FieldErrors, FieldValues, UseFormReturn } from "react-hook-form";
export const getFirstFormErrorMessage = <T extends FieldValues>(
errors: FieldErrors<T>,
form: UseFormReturn<T>
) => {
const firstKey = Object.keys(errors)[0] as keyof T | undefined;
const firstError = errors[firstKey] as
| { message?: string; root?: { message?: string } }
| undefined;
return firstError?.message
? firstError.message
: firstError?.root?.message
? firstError.root?.message
: undefined;
};

View File

@ -1,2 +1,3 @@
export * from "./focus-first-input-form-error.ts";
export * from "./get-first-form-error-message.ts";
export * from "./prevent-enter-key-submit-form.ts";

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@repo/shadcn-ui",
"version": "0.9.0",
"version": "0.9.1",
"type": "module",
"private": true,
"exports": {

View File

@ -1,6 +1,6 @@
{
"name": "@repo/typescript-config",
"version": "0.9.0",
"version": "0.9.1",
"private": true,
"publishConfig": {
"access": "public"