From 54e23899c430234abde16dd2871eec0191b921ca Mon Sep 17 00:00:00 2001 From: david Date: Tue, 11 Nov 2025 12:22:20 +0100 Subject: [PATCH] =?UTF-8?q?Facturas=20de=20cliente=20-=20Arreglado=20rec?= =?UTF-8?q?=C3=A1lculo=20de=20l=C3=ADneas=20y=20cabecera?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../customer-invoice-items.full.presenter.ts | 2 +- .../update-customer-invoice.use-case.ts | 2 + .../customer-invoice-item.ts | 10 +- .../sequelize/customer-invoice.repository.ts | 52 ++++-- ...date-customer-invoice-by-id.request.dto.ts | 4 +- ...get-customer-invoice-by-id.response.dto.ts | 2 +- .../src/common/locales/en.json | 20 +-- .../src/common/locales/es.json | 10 +- .../web/components/editor/invoice-totals.tsx | 18 +- .../items/items-data-table-row-actions.tsx | 56 +++--- .../components/editor/items/items-editor.tsx | 5 +- .../components/editor/items/table-view.tsx | 160 +++++++++--------- .../editor/items/use-items-columns.tsx | 68 +++++++- .../calculate-invoice-header-amounts.ts | 53 +++--- .../domain/calculate-invoice-item-amounts.ts | 50 +++--- .../hooks/calcs/use-invoice-auto-recalc.ts | 33 ++-- .../src/web/schemas/invoice-dto.adapter.ts | 2 - .../src/web/schemas/invoice.form.schema.ts | 5 +- ...get-verifactu-record-by-id.response.dto.ts | 2 +- 19 files changed, 321 insertions(+), 233 deletions(-) diff --git a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts index dc7592cb..b25d59bb 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts @@ -17,7 +17,7 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter { return { id: invoiceItem.id.toPrimitive(), - is_non_valued: String(invoiceItem.isNonValued), + is_valued: String(invoiceItem.isValued), position: String(index), description: toEmptyString(invoiceItem.description, (value) => value.toPrimitive()), diff --git a/modules/customer-invoices/src/api/application/use-cases/update/update-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/update/update-customer-invoice.use-case.ts index 7a0eb78f..3e5d39be 100644 --- a/modules/customer-invoices/src/api/application/use-cases/update/update-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/update/update-customer-invoice.use-case.ts @@ -61,6 +61,8 @@ export class UpdateCustomerInvoiceUseCase { updatedInvoice.data, transaction ); + if (invoiceOrError.isFailure) return Result.fail(invoiceOrError.error); + const invoice = invoiceOrError.data; const dto = presenter.toOutput(invoice); return Result.ok(dto); diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts index 1af1dd08..1016da15 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts +++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts @@ -22,7 +22,7 @@ export interface CustomerInvoiceItemProps { } export interface ICustomerInvoiceItem { - isNonValued: boolean; + isValued: boolean; description: Maybe; @@ -48,7 +48,7 @@ export class CustomerInvoiceItem extends DomainEntity implements ICustomerInvoiceItem { - protected _isNonValued!: boolean; + protected _isValued!: boolean; public static create( props: CustomerInvoiceItemProps, @@ -66,11 +66,11 @@ export class CustomerInvoiceItem protected constructor(props: CustomerInvoiceItemProps, id?: UniqueID) { super(props, id); - this._isNonValued = this.quantity.isNone() || this.unitAmount.isNone(); + this._isValued = this.quantity.isSome() || this.unitAmount.isSome(); } - get isNonValued(): boolean { - return this._isNonValued; + get isValued(): boolean { + return this._isValued; } get description(): Maybe { diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index 81b821ec..fb5e3f67 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -95,37 +95,59 @@ export class CustomerInvoiceRepository * @param transaction - Transacción activa para la operación. * @returns Result */ - async update(invoice: CustomerInvoice, transaction?: Transaction): Promise> { + async update(invoice: CustomerInvoice, transaction: Transaction): Promise> { try { const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({ resource: "customer-invoice", }); - const dto = mapper.mapToPersistence(invoice); + const dtoResult = mapper.mapToPersistence(invoice); - if (dto.isFailure) { - return Result.fail(dto.error); - } - const { id, ...updatePayload } = dto.data; + if (dtoResult.isFailure) return Result.fail(dtoResult.error); - console.log(id); + const dto = dtoResult.data; + const { id, items, taxes, ...updatePayload } = dto; - const [affected] = await CustomerInvoiceModel.update(updatePayload, { + // 1. Actualizar cabecera + const [affectedCount] = await CustomerInvoiceModel.update(updatePayload, { where: { id /*, version */ }, - //fields: Object.keys(updatePayload), transaction, - individualHooks: true, }); - console.log(affected); + console.log(affectedCount); - if (affected === 0) { + if (affectedCount === 0) { return Result.fail( - new InfrastructureRepositoryError( - "Concurrency conflict or not found update customer invoice" - ) + new InfrastructureRepositoryError(`Invoice ${id} not found or concurrency issue`) ); } + // 2. Borra items y taxes previos (simplifica sincronización) + await CustomerInvoiceItemModel.destroy({ + where: { invoice_id: id }, + transaction, + }); + await CustomerInvoiceTaxModel.destroy({ + where: { invoice_id: id }, + transaction, + }); + + // 3. Inserta taxes de cabecera + if (Array.isArray(taxes) && taxes.length > 0) { + await CustomerInvoiceTaxModel.bulkCreate(taxes, { transaction }); + } + + // 4. Inserta items + sus taxes + if (Array.isArray(items) && items.length > 0) { + for (const item of items) { + const { taxes: itemTaxes, ...itemData } = item; + await CustomerInvoiceItemModel.create(itemData, { transaction }); + + if (Array.isArray(itemTaxes) && itemTaxes.length > 0) { + await CustomerInvoiceItemTaxModel.bulkCreate(itemTaxes, { transaction }); + } + } + } + return Result.ok(); } catch (err: unknown) { return Result.fail(translateSequelizeError(err)); diff --git a/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts index 01781b65..107c83c2 100644 --- a/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts @@ -2,7 +2,7 @@ import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core"; import { z } from "zod/v4"; export const UpdateCustomerInvoiceByIdParamsRequestSchema = z.object({ - invoice_id: z.string(), + proforma_id: z.string(), }); export const UpdateCustomerInvoiceByIdRequestSchema = z.object({ @@ -23,7 +23,7 @@ export const UpdateCustomerInvoiceByIdRequestSchema = z.object({ items: z .array( z.object({ - is_non_valued: z.string().optional(), + is_valued: z.string().optional(), description: z.string().optional(), quantity: QuantitySchema.optional(), diff --git a/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts index d9b81fbf..9c377395 100644 --- a/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts @@ -59,7 +59,7 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({ items: z.array( z.object({ id: z.uuid(), - is_non_valued: z.string(), + is_valued: z.string(), position: z.string(), description: z.string(), quantity: QuantitySchema, diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index 67845065..62b122a3 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -173,20 +173,20 @@ "placeholder": "", "description": "Item unit price" }, - "subtotal_amount": { - "label": "Subtotal", - "placeholder": "", - "description": "" - }, "discount_percentage": { "label": "Dto (%)", "placeholder": "", "description": "Percentage discount" }, "discount_amount": { - "label": "Discount price", + "label": "Discount amount", "placeholder": "", - "description": "Percentage discount price" + "description": "Percentage discount amount" + }, + "taxable_amount": { + "label": "Taxable amount", + "placeholder": "", + "description": "" }, "tax_codes": { "label": "Taxes", @@ -194,12 +194,12 @@ "description": "Taxes" }, "taxes_amount": { - "label": "Taxes price", + "label": "Taxes amount", "placeholder": "", - "description": "Percentage taxes price" + "description": "Percentage taxes amount" }, "total_amount": { - "label": "Total", + "label": "Total amount", "placeholder": "", "description": "Invoice line total" } diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index 978ebc83..d7fa847f 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -165,11 +165,6 @@ "placeholder": "", "description": "Precio unitario del producto" }, - "subtotal_amount": { - "label": "Subtotal", - "placeholder": "", - "description": "" - }, "discount_percentage": { "label": "Dto (%)", "placeholder": "", @@ -180,6 +175,11 @@ "placeholder": "", "description": "Importe del descuento porcentual" }, + "taxable_amount": { + "label": "Subtotal", + "placeholder": "", + "description": "" + }, "tax_codes": { "label": "Impuestos", "placeholder": "", diff --git a/modules/customer-invoices/src/web/components/editor/invoice-totals.tsx b/modules/customer-invoices/src/web/components/editor/invoice-totals.tsx index e809d9c5..5a8cbb59 100644 --- a/modules/customer-invoices/src/web/components/editor/invoice-totals.tsx +++ b/modules/customer-invoices/src/web/components/editor/invoice-totals.tsx @@ -22,6 +22,11 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => { const displayTaxes = useWatch({ control, name: "taxes", defaultValue: [] }); const subtotal_amount = useWatch({ control, name: "subtotal_amount", defaultValue: 0 }); + const items_discount_amount = useWatch({ + control, + name: "items_discount_amount", + defaultValue: 0, + }); const discount_amount = useWatch({ control, name: "discount_amount", defaultValue: 0 }); const taxable_amount = useWatch({ control, name: "taxable_amount", defaultValue: 0 }); const taxes_amount = useWatch({ control, name: "taxes_amount", defaultValue: 0 }); @@ -39,12 +44,21 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
{/* Sección: Subtotal y Descuentos */}
- Subtotal sin descuento + Subtotal sin descuentos {formatCurrency(subtotal_amount, 2, currency_code, language_code)}
+
+
+ Descuento en líneas +
+ + -{formatCurrency(items_discount_amount, 2, currency_code, language_code)} + +
+
Descuento global @@ -52,7 +66,7 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => { control={control} name={"discount_percentage"} readOnly={readOnly} - inputId={"header-discount-percentage"} + inputId={"discount-percentage"} showSuffix={true} className={cn( "w-20 text-right tabular-nums bg-background", diff --git a/modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx b/modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx index ba752171..f64193f0 100644 --- a/modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx @@ -1,32 +1,26 @@ -"use client" +"use client"; +import { DataTableRowOps } from "@repo/rdx-ui/components"; +import { Button, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components"; import { Row, Table } from "@tanstack/react-table"; - - -import { DataTableRowOps } from '@repo/rdx-ui/components'; -import { - Button, - Tooltip, - TooltipContent, - TooltipTrigger -} from '@repo/shadcn-ui/components'; -import { ArrowDownIcon, ArrowUpIcon, CopyIcon, PencilIcon, Trash2Icon } from 'lucide-react'; +import { ArrowDownIcon, ArrowUpIcon, CopyIcon, PencilIcon, Trash2Icon } from "lucide-react"; interface DataTableRowActionsProps { - row: Row, - table: Table + row: Row; + table: Table; } -export function ItemDataTableRowActions({ - row, table -}: DataTableRowActionsProps) { +export function ItemDataTableRowActions({ row, table }: DataTableRowActionsProps) { const ops = (table.options.meta as any)?.rowOps as DataTableRowOps; - const openEditor = (table.options.meta as any)?.openEditor as (i: number, table: Table) => void; + const openEditor = (table.options.meta as any)?.openEditor as ( + i: number, + table: Table + ) => void; const lastRow = table.getRowModel().rows.length - 1; const rowIndex = row.index; return ( -
+
{openEditor && ( @@ -81,17 +75,21 @@ export function ItemDataTableRowActions({ - {ops?.move && } + {ops?.move && ( + + )} Down diff --git a/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx b/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx index 02a64016..84b6824d 100644 --- a/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx @@ -5,7 +5,6 @@ import { useInvoiceContext } from "../../../context"; import { useInvoiceAutoRecalc } from "../../../hooks"; import { useTranslation } from "../../../i18n"; import { defaultCustomerInvoiceItemFormData, InvoiceFormData } from "../../../schemas"; -import { debugIdCol } from "./debug-id-col"; import { ItemRowEditor } from "./item-row-editor"; import { useItemsColumns } from "./use-items-columns"; @@ -15,7 +14,7 @@ export const ItemsEditor = () => { const { t } = useTranslation(); const context = useInvoiceContext(); const form = useFormContext(); - const { control, getValues } = form; + const { control } = form; useInvoiceAutoRecalc(form, context); @@ -25,7 +24,7 @@ export const ItemsEditor = () => { }); const baseColumns = useWithRowSelection(useItemsColumns(), true); - const columns = useMemo(() => [...baseColumns, debugIdCol], [baseColumns]); + const columns = useMemo(() => baseColumns, [baseColumns]); return (
diff --git a/modules/customer-invoices/src/web/components/editor/items/table-view.tsx b/modules/customer-invoices/src/web/components/editor/items/table-view.tsx index 1fa7d766..35f516a6 100644 --- a/modules/customer-invoices/src/web/components/editor/items/table-view.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/table-view.tsx @@ -1,3 +1,4 @@ +import { useMoney } from "@erp/core/hooks"; import { Button, Input, @@ -14,17 +15,14 @@ import { TooltipTrigger, } from "@repo/shadcn-ui/components"; import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Plus, TrashIcon } from "lucide-react"; - - -import { useMoney } from '@erp/core/hooks'; -import { useEffect, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { useTranslation } from '../../../i18n'; -import { InvoiceItemFormData } from '../../../schemas'; -import { HoverCardTotalsSummary } from './hover-card-total-summary'; +import { useEffect, useState } from "react"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "../../../i18n"; +import { InvoiceItemFormData } from "../../../schemas"; +import { HoverCardTotalsSummary } from "./hover-card-total-summary"; import { CustomItemViewProps } from "./types"; -export interface TableViewProps extends CustomItemViewProps { } +export interface TableViewProps extends CustomItemViewProps {} export const TableView = ({ items, actions }: TableViewProps) => { const { t } = useTranslation(); @@ -33,8 +31,8 @@ export const TableView = ({ items, actions }: TableViewProps) => { const [lines, setLines] = useState(items); useEffect(() => { - setLines(items) - }, [items]) + setLines(items); + }, [items]); // Mantiene sincronía con el formulario padre const updateItems = (updated: InvoiceItemFormData[]) => { @@ -52,10 +50,7 @@ export const TableView = ({ items, actions }: TableViewProps) => { /** 🔹 Mueve la fila hacia arriba o abajo */ const moveItem = (index: number, direction: "up" | "down") => { - if ( - (direction === "up" && index === 0) || - (direction === "down" && index === lines.length - 1) - ) + if ((direction === "up" && index === 0) || (direction === "down" && index === lines.length - 1)) return; const newItems = [...lines]; @@ -80,7 +75,6 @@ export const TableView = ({ items, actions }: TableViewProps) => { /** 🔹 Añade una nueva línea vacía */ const addNewItem = () => { const newItem: InvoiceItemFormData = { - is_non_valued: false, description: "", quantity: { value: "0", scale: "2" }, unit_amount: { value: "0", scale: "2", currency_code: "EUR" }, @@ -96,60 +90,69 @@ export const TableView = ({ items, actions }: TableViewProps) => { }; return ( -
-
- - - - # +
+
+
+ + + # {t("form_fields.item.description.label")} - {t("form_fields.item.quantity.label")} - {t("form_fields.item.unit_amount.label")} - {t("form_fields.item.discount_percentage.label")} - {t("form_fields.item.tax_codes.label")} - {t("form_fields.item.total_amount.label")} - {t("common.actions")} + + {t("form_fields.item.quantity.label")} + + + {t("form_fields.item.unit_amount.label")} + + + {t("form_fields.item.discount_percentage.label")} + + + {t("form_fields.item.tax_codes.label")} + + + {t("form_fields.item.total_amount.label")} + + {t("common.actions")} {lines.map((item, i) => ( - + {/* ÍNDICE */} - + {i + 1} {/* DESCRIPCIÓN */} - +