Proformas: saber la issued invoice de una proforma
This commit is contained in:
parent
5bde207188
commit
400b90f631
@ -35,4 +35,6 @@ export type ProformaSummary = {
|
|||||||
taxableAmount: InvoiceAmount;
|
taxableAmount: InvoiceAmount;
|
||||||
taxesAmount: InvoiceAmount;
|
taxesAmount: InvoiceAmount;
|
||||||
totalAmount: InvoiceAmount;
|
totalAmount: InvoiceAmount;
|
||||||
|
|
||||||
|
linkedInvoiceId: Maybe<UniqueID>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -37,6 +37,8 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB
|
|||||||
taxes_amount: proforma.taxesAmount.toObjectString(),
|
taxes_amount: proforma.taxesAmount.toObjectString(),
|
||||||
total_amount: proforma.totalAmount.toObjectString(),
|
total_amount: proforma.totalAmount.toObjectString(),
|
||||||
|
|
||||||
|
linked_invoice_id: maybeToEmptyString(proforma.linkedInvoiceId, (value) => value.toString()),
|
||||||
|
|
||||||
metadata: {
|
metadata: {
|
||||||
entity: "proforma",
|
entity: "proforma",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -34,5 +34,7 @@ export interface IProformaSummarySnapshot {
|
|||||||
taxes_amount: { value: string; scale: string; currency_code: string };
|
taxes_amount: { value: string; scale: string; currency_code: string };
|
||||||
total_amount: { value: string; scale: string; currency_code: string };
|
total_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
|
linked_invoice_id: string;
|
||||||
|
|
||||||
metadata?: Record<string, string>;
|
metadata?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,8 +125,10 @@ export class CustomerInvoiceModel extends Model<
|
|||||||
// Relaciones
|
// Relaciones
|
||||||
declare items: NonAttribute<CustomerInvoiceItemModel[]>;
|
declare items: NonAttribute<CustomerInvoiceItemModel[]>;
|
||||||
declare taxes: NonAttribute<CustomerInvoiceTaxModel[]>;
|
declare taxes: NonAttribute<CustomerInvoiceTaxModel[]>;
|
||||||
declare current_customer: NonAttribute<CustomerModel>;
|
declare current_customer?: NonAttribute<CustomerModel | null>;
|
||||||
declare verifactu: NonAttribute<VerifactuRecordModel>;
|
declare verifactu?: NonAttribute<VerifactuRecordModel | null>;
|
||||||
|
declare proforma?: NonAttribute<CustomerInvoiceModel | null>;
|
||||||
|
declare linked_invoice?: NonAttribute<CustomerInvoiceModel | null>;
|
||||||
|
|
||||||
static associate(database: Sequelize) {
|
static associate(database: Sequelize) {
|
||||||
const models = database.models;
|
const models = database.models;
|
||||||
@ -182,6 +184,26 @@ export class CustomerInvoiceModel extends Model<
|
|||||||
onDelete: "CASCADE",
|
onDelete: "CASCADE",
|
||||||
onUpdate: "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) {
|
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_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: "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
|
// Para búsquedas simples
|
||||||
{
|
{
|
||||||
name: "ft_customer_invoice",
|
name: "ft_customer_invoice",
|
||||||
|
|||||||
@ -87,6 +87,8 @@ export class SequelizeProformaSummaryMapper extends SequelizeQueryMapper<
|
|||||||
taxableAmount: attributes.taxableAmount!,
|
taxableAmount: attributes.taxableAmount!,
|
||||||
taxesAmount: attributes.taxesAmount!,
|
taxesAmount: attributes.taxesAmount!,
|
||||||
totalAmount: attributes.totalAmount!,
|
totalAmount: attributes.totalAmount!,
|
||||||
|
|
||||||
|
linkedInvoiceId: attributes.linkedInvoiceId!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +199,12 @@ export class SequelizeProformaSummaryMapper extends SequelizeQueryMapper<
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const linkedInvoiceId = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.linked_invoice?.id, (value) => UniqueID.create(value)),
|
||||||
|
"linked_invoice_id",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invoiceId,
|
invoiceId,
|
||||||
companyId,
|
companyId,
|
||||||
@ -217,6 +225,8 @@ export class SequelizeProformaSummaryMapper extends SequelizeQueryMapper<
|
|||||||
taxableAmount,
|
taxableAmount,
|
||||||
taxesAmount,
|
taxesAmount,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
|
|
||||||
|
linkedInvoiceId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -399,6 +399,12 @@ export class ProformaRepository
|
|||||||
required: false,
|
required: false,
|
||||||
separate: true, // => query aparte, devuelve siempre array
|
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)
|
// Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento)
|
||||||
|
|||||||
@ -45,6 +45,8 @@ export const ListProformasResponseSchema = createPaginatedListSchema(
|
|||||||
taxes_amount: MoneySchema,
|
taxes_amount: MoneySchema,
|
||||||
total_amount: MoneySchema,
|
total_amount: MoneySchema,
|
||||||
|
|
||||||
|
linked_invoice_id: z.string(),
|
||||||
|
|
||||||
metadata: MetadataSchema.optional(),
|
metadata: MetadataSchema.optional(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,13 +3,13 @@ import type { ColumnDef } from "@tanstack/react-table";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
import type { ProformaSummaryData, ProformaSummaryPageData } from "../../../../types";
|
import type { ProformaList, ProformaListRow } from "../../../../shared";
|
||||||
|
|
||||||
interface ProformasGridProps {
|
interface ProformasGridProps {
|
||||||
data: ProformaSummaryPageData;
|
data?: ProformaList;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|
||||||
columns: ColumnDef<ProformaSummaryData, unknown>[];
|
columns: ColumnDef<ProformaListRow, unknown>[];
|
||||||
|
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
@ -31,7 +31,7 @@ export const ProformasGrid = ({
|
|||||||
}: ProformasGridProps) => {
|
}: ProformasGridProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { items, total_items } = data;
|
const { items, total_items } = data || { items: [], total_items: 0 };
|
||||||
|
|
||||||
if (loading)
|
if (loading)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export const ProformaListPage = () => {
|
|||||||
onChangeStatusClick: handleChangeStatusProforma,
|
onChangeStatusClick: handleChangeStatusProforma,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (listCtrl.isError || !listCtrl.data) {
|
if (listCtrl.isError) {
|
||||||
return (
|
return (
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
@ -70,74 +70,81 @@ export const ProformaListPage = () => {
|
|||||||
title={t("pages.proformas.list.title")}
|
title={t("pages.proformas.list.title")}
|
||||||
/>
|
/>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
|
|
||||||
<AppContent>
|
<AppContent>
|
||||||
{/* Search and filters */}
|
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||||
<div className="flex items-center justify-between gap-16">
|
{/* Search and filters */}
|
||||||
<SimpleSearchInput
|
<div className="h-full min-w-0 overflow-auto w-full">
|
||||||
loading={listCtrl.isLoading}
|
<div className="flex items-center justify-between gap-16">
|
||||||
onSearchChange={listCtrl.setSearchValue}
|
<SimpleSearchInput
|
||||||
value={listCtrl.search}
|
loading={listCtrl.isLoading}
|
||||||
|
onSearchChange={listCtrl.setSearchValue}
|
||||||
|
value={listCtrl.search}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
|
||||||
|
<SelectTrigger className="w-full sm:w-48">
|
||||||
|
<FilterIcon aria-hidden className="mr-2 size-4" />
|
||||||
|
<SelectValue placeholder={t("filters.status")} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">{t("catalog.proformas.status.all.label")}</SelectItem>
|
||||||
|
<SelectItem value="draft">{t("catalog.proformas.status.draft.label")}</SelectItem>
|
||||||
|
<SelectItem value="sent">{t("catalog.proformas.status.sent.label")}</SelectItem>
|
||||||
|
<SelectItem value="approved">
|
||||||
|
{t("catalog.proformas.status.approved.label")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="rejected">
|
||||||
|
{t("catalog.proformas.status.rejected.label")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="issued">
|
||||||
|
{t("catalog.proformas.status.issued.label")}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProformasGrid
|
||||||
|
columns={columns}
|
||||||
|
data={listCtrl.data}
|
||||||
|
loading={listCtrl.isLoading}
|
||||||
|
onPageChange={listCtrl.setPageIndex}
|
||||||
|
onPageSizeChange={listCtrl.setPageSize}
|
||||||
|
pageIndex={listCtrl.pageIndex}
|
||||||
|
pageSize={listCtrl.pageSize}
|
||||||
|
// acciones rápidas del grid → page controller
|
||||||
|
//onRowClick={(id) => navigate(`/proformas/${id}`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Emitir factura */}
|
||||||
|
<ProformaIssueDialog
|
||||||
|
isSubmitting={issueDialogCtrl.isSubmitting}
|
||||||
|
onConfirm={issueDialogCtrl.confirmIssue}
|
||||||
|
onOpenChange={(open) => !open && issueDialogCtrl.closeDialog()}
|
||||||
|
open={issueDialogCtrl.open}
|
||||||
|
proforma={issueDialogCtrl.proforma}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
|
{/* Cambiar estado */}
|
||||||
<SelectTrigger className="w-full sm:w-48">
|
<ChangeStatusDialog
|
||||||
<FilterIcon aria-hidden className="mr-2 size-4" />
|
isSubmitting={changeStatusDialogCtrl.isSubmitting}
|
||||||
<SelectValue placeholder={t("filters.status")} />
|
onConfirm={changeStatusDialogCtrl.confirmChangeStatus}
|
||||||
</SelectTrigger>
|
onOpenChange={(open) => !open && changeStatusDialogCtrl.closeDialog()}
|
||||||
<SelectContent>
|
open={changeStatusDialogCtrl.open}
|
||||||
<SelectItem value="all">{t("catalog.proformas.status.all.label")}</SelectItem>
|
proformas={changeStatusDialogCtrl.proformas} // ← recibe el status seleccionado
|
||||||
<SelectItem value="draft">{t("catalog.proformas.status.draft.label")}</SelectItem>
|
/>
|
||||||
<SelectItem value="sent">{t("catalog.proformas.status.sent.label")}</SelectItem>
|
|
||||||
<SelectItem value="approved">
|
{/* Eliminar */}
|
||||||
{t("catalog.proformas.status.approved.label")}
|
<DeleteProformaDialog
|
||||||
</SelectItem>
|
isSubmitting={deleteDialogCtrl.isSubmitting}
|
||||||
<SelectItem value="rejected">
|
onConfirm={deleteDialogCtrl.confirmDelete}
|
||||||
{t("catalog.proformas.status.rejected.label")}
|
onOpenChange={(open) => !open && deleteDialogCtrl.closeDialog()}
|
||||||
</SelectItem>
|
open={deleteDialogCtrl.open}
|
||||||
<SelectItem value="issued">{t("catalog.proformas.status.issued.label")}</SelectItem>
|
proformas={deleteDialogCtrl.proformas}
|
||||||
</SelectContent>
|
requireSecondConfirm={true}
|
||||||
</Select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProformasGrid
|
|
||||||
columns={columns}
|
|
||||||
data={listCtrl.data}
|
|
||||||
loading={listCtrl.isLoading}
|
|
||||||
onPageChange={listCtrl.setPageIndex}
|
|
||||||
onPageSizeChange={listCtrl.setPageSize}
|
|
||||||
// acciones rápidas del grid → page controller
|
|
||||||
//onRowClick={(id) => navigate(`/proformas/${id}`)}
|
|
||||||
pageIndex={listCtrl.pageIndex}
|
|
||||||
pageSize={listCtrl.pageSize}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Emitir factura */}
|
|
||||||
<ProformaIssueDialog
|
|
||||||
isSubmitting={issueDialogCtrl.isSubmitting}
|
|
||||||
onConfirm={issueDialogCtrl.confirmIssue}
|
|
||||||
onOpenChange={(open) => !open && issueDialogCtrl.closeDialog()}
|
|
||||||
open={issueDialogCtrl.open}
|
|
||||||
proforma={issueDialogCtrl.proforma}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Cambiar estado */}
|
|
||||||
<ChangeStatusDialog
|
|
||||||
isSubmitting={changeStatusDialogCtrl.isSubmitting}
|
|
||||||
onConfirm={changeStatusDialogCtrl.confirmChangeStatus}
|
|
||||||
onOpenChange={(open) => !open && changeStatusDialogCtrl.closeDialog()}
|
|
||||||
open={changeStatusDialogCtrl.open}
|
|
||||||
proformas={changeStatusDialogCtrl.proformas} // ← recibe el status seleccionado
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Eliminar */}
|
|
||||||
<DeleteProformaDialog
|
|
||||||
isSubmitting={deleteDialogCtrl.isSubmitting}
|
|
||||||
onConfirm={deleteDialogCtrl.confirmDelete}
|
|
||||||
onOpenChange={(open) => !open && deleteDialogCtrl.closeDialog()}
|
|
||||||
open={deleteDialogCtrl.open}
|
|
||||||
proformas={deleteDialogCtrl.proformas}
|
|
||||||
requireSecondConfirm={true}
|
|
||||||
/>
|
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -94,6 +94,8 @@ export const ListProformasRowAdapter = {
|
|||||||
rowDto.currency_code,
|
rowDto.currency_code,
|
||||||
rowDto.language_code
|
rowDto.language_code
|
||||||
),
|
),
|
||||||
|
|
||||||
|
linked_invoice_id: rowDto.linked_invoice_id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -46,4 +46,6 @@ export interface ProformaListRow {
|
|||||||
|
|
||||||
total_amount: number;
|
total_amount: number;
|
||||||
total_amount_fmt: string;
|
total_amount_fmt: string;
|
||||||
|
|
||||||
|
linked_invoice_id: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user