diff --git a/modules/customer-invoices/src/api/application/proformas/models/proforma-summary.ts b/modules/customer-invoices/src/api/application/proformas/models/proforma-summary.ts index 142ba444..e6540ba5 100644 --- a/modules/customer-invoices/src/api/application/proformas/models/proforma-summary.ts +++ b/modules/customer-invoices/src/api/application/proformas/models/proforma-summary.ts @@ -35,4 +35,6 @@ export type ProformaSummary = { taxableAmount: InvoiceAmount; taxesAmount: InvoiceAmount; totalAmount: InvoiceAmount; + + linkedInvoiceId: Maybe; }; diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts index 359261ce..df0d5554 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts @@ -37,6 +37,8 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB taxes_amount: proforma.taxesAmount.toObjectString(), total_amount: proforma.totalAmount.toObjectString(), + linked_invoice_id: maybeToEmptyString(proforma.linkedInvoiceId, (value) => value.toString()), + metadata: { entity: "proforma", }, diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot.interface.ts index 1ff4cf9e..074b16c7 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot.interface.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot.interface.ts @@ -34,5 +34,7 @@ export interface IProformaSummarySnapshot { taxes_amount: { value: string; scale: string; currency_code: string }; total_amount: { value: string; scale: string; currency_code: string }; + linked_invoice_id: string; + metadata?: Record; } diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts index 62bfaf56..ca2b7451 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts @@ -125,8 +125,10 @@ export class CustomerInvoiceModel extends Model< // Relaciones declare items: NonAttribute; declare taxes: NonAttribute; - declare current_customer: NonAttribute; - declare verifactu: NonAttribute; + declare current_customer?: NonAttribute; + declare verifactu?: NonAttribute; + declare proforma?: NonAttribute; + declare linked_invoice?: NonAttribute; static associate(database: Sequelize) { const models = database.models; @@ -182,6 +184,26 @@ export class CustomerInvoiceModel extends Model< onDelete: "CASCADE", onUpdate: "CASCADE", }); + + // Relaciones con proforma e invoice vinculada (si esta factura es una proforma o tiene una proforma origen) + + CustomerInvoiceModel.belongsTo(CustomerInvoiceModel, { + as: "proforma", + foreignKey: "proforma_id", + targetKey: "id", + constraints: true, + onDelete: "SET NULL", + onUpdate: "CASCADE", + }); + + CustomerInvoiceModel.hasOne(CustomerInvoiceModel, { + as: "linked_invoice", + foreignKey: "proforma_id", + sourceKey: "id", + constraints: true, + onDelete: "SET NULL", + onUpdate: "CASCADE", + }); } static hooks(_database: Sequelize) { @@ -507,10 +529,10 @@ export default (database: Sequelize) => { { name: "idx_invoice_company_id", fields: ["id", "company_id"], unique: true }, // <- para consulta get - { name: "idx_invoice_proforma_id", fields: ["proforma_id"], unique: false }, // <- para localizar factura por medio de proforma - { name: "idx_invoice_factuges", fields: ["factuges_id"], unique: false }, // <- para el proceso python + { name: "uq_invoice_proforma_id", fields: ["proforma_id"], unique: true }, // <- para asegurar que una proforma solo tenga una factura vinculada + // Para búsquedas simples { name: "ft_customer_invoice", diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-summary.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-summary.mapper.ts index b13f38fc..e1efd0d4 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-summary.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-summary.mapper.ts @@ -87,6 +87,8 @@ export class SequelizeProformaSummaryMapper extends SequelizeQueryMapper< taxableAmount: attributes.taxableAmount!, taxesAmount: attributes.taxesAmount!, totalAmount: attributes.totalAmount!, + + linkedInvoiceId: attributes.linkedInvoiceId!, }); } @@ -197,6 +199,12 @@ export class SequelizeProformaSummaryMapper extends SequelizeQueryMapper< errors ); + const linkedInvoiceId = extractOrPushError( + maybeFromNullableResult(raw.linked_invoice?.id, (value) => UniqueID.create(value)), + "linked_invoice_id", + errors + ); + return { invoiceId, companyId, @@ -217,6 +225,8 @@ export class SequelizeProformaSummaryMapper extends SequelizeQueryMapper< taxableAmount, taxesAmount, totalAmount, + + linkedInvoiceId, }; } } diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts index 35ccfb35..21d3f27c 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts @@ -399,6 +399,12 @@ export class ProformaRepository required: false, separate: true, // => query aparte, devuelve siempre array }, + { + model: CustomerInvoiceModel, + as: "linked_invoice", + required: false, + attributes: ["id"], + }, ]; // Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento) diff --git a/modules/customer-invoices/src/common/dto/response/proformas/list-proformas.response.dto.ts b/modules/customer-invoices/src/common/dto/response/proformas/list-proformas.response.dto.ts index 28652083..49d523df 100644 --- a/modules/customer-invoices/src/common/dto/response/proformas/list-proformas.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/proformas/list-proformas.response.dto.ts @@ -45,6 +45,8 @@ export const ListProformasResponseSchema = createPaginatedListSchema( taxes_amount: MoneySchema, total_amount: MoneySchema, + linked_invoice_id: z.string(), + metadata: MetadataSchema.optional(), }) ); diff --git a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/proformas-grid.tsx b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/proformas-grid.tsx index 6c430ee1..be12910c 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/proformas-grid.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/proformas-grid.tsx @@ -3,13 +3,13 @@ import type { ColumnDef } from "@tanstack/react-table"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "../../../../../i18n"; -import type { ProformaSummaryData, ProformaSummaryPageData } from "../../../../types"; +import type { ProformaList, ProformaListRow } from "../../../../shared"; interface ProformasGridProps { - data: ProformaSummaryPageData; + data?: ProformaList; loading: boolean; - columns: ColumnDef[]; + columns: ColumnDef[]; pageIndex: number; pageSize: number; @@ -31,7 +31,7 @@ export const ProformasGrid = ({ }: ProformasGridProps) => { const navigate = useNavigate(); const { t } = useTranslation(); - const { items, total_items } = data; + const { items, total_items } = data || { items: [], total_items: 0 }; if (loading) return ( diff --git a/modules/customer-invoices/src/web/proformas/list/ui/pages/proforma-list-page.tsx b/modules/customer-invoices/src/web/proformas/list/ui/pages/proforma-list-page.tsx index 47c688f9..01eace58 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/pages/proforma-list-page.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/pages/proforma-list-page.tsx @@ -41,7 +41,7 @@ export const ProformaListPage = () => { onChangeStatusClick: handleChangeStatusProforma, }); - if (listCtrl.isError || !listCtrl.data) { + if (listCtrl.isError) { return ( { title={t("pages.proformas.list.title")} /> + - {/* Search and filters */} -
- + {/* Search and filters */} +
+
+ + + +
+ + navigate(`/proformas/${id}`)} + /> +
+ + {/* Emitir factura */} + !open && issueDialogCtrl.closeDialog()} + open={issueDialogCtrl.open} + proforma={issueDialogCtrl.proforma} /> - + {/* Cambiar estado */} + !open && changeStatusDialogCtrl.closeDialog()} + open={changeStatusDialogCtrl.open} + proformas={changeStatusDialogCtrl.proformas} // ← recibe el status seleccionado + /> + + {/* Eliminar */} + !open && deleteDialogCtrl.closeDialog()} + open={deleteDialogCtrl.open} + proformas={deleteDialogCtrl.proformas} + requireSecondConfirm={true} + />
- - navigate(`/proformas/${id}`)} - pageIndex={listCtrl.pageIndex} - pageSize={listCtrl.pageSize} - /> - - {/* Emitir factura */} - !open && issueDialogCtrl.closeDialog()} - open={issueDialogCtrl.open} - proforma={issueDialogCtrl.proforma} - /> - - {/* Cambiar estado */} - !open && changeStatusDialogCtrl.closeDialog()} - open={changeStatusDialogCtrl.open} - proformas={changeStatusDialogCtrl.proformas} // ← recibe el status seleccionado - /> - - {/* Eliminar */} - !open && deleteDialogCtrl.closeDialog()} - open={deleteDialogCtrl.open} - proformas={deleteDialogCtrl.proformas} - requireSecondConfirm={true} - />
); diff --git a/modules/customer-invoices/src/web/proformas/shared/adapters/list-proformas.adapter.ts b/modules/customer-invoices/src/web/proformas/shared/adapters/list-proformas.adapter.ts index 8e5228fd..9cbf3c1d 100644 --- a/modules/customer-invoices/src/web/proformas/shared/adapters/list-proformas.adapter.ts +++ b/modules/customer-invoices/src/web/proformas/shared/adapters/list-proformas.adapter.ts @@ -94,6 +94,8 @@ export const ListProformasRowAdapter = { rowDto.currency_code, rowDto.language_code ), + + linked_invoice_id: rowDto.linked_invoice_id, }; }, }; diff --git a/modules/customer-invoices/src/web/proformas/shared/entities/proforma-list-row.entity.ts b/modules/customer-invoices/src/web/proformas/shared/entities/proforma-list-row.entity.ts index ef169091..1d564db6 100644 --- a/modules/customer-invoices/src/web/proformas/shared/entities/proforma-list-row.entity.ts +++ b/modules/customer-invoices/src/web/proformas/shared/entities/proforma-list-row.entity.ts @@ -46,4 +46,6 @@ export interface ProformaListRow { total_amount: number; total_amount_fmt: string; + + linked_invoice_id: string; }