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;
|
||||
taxesAmount: InvoiceAmount;
|
||||
totalAmount: InvoiceAmount;
|
||||
|
||||
linkedInvoiceId: Maybe<UniqueID>;
|
||||
};
|
||||
|
||||
@ -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",
|
||||
},
|
||||
|
||||
@ -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<string, string>;
|
||||
}
|
||||
|
||||
@ -125,8 +125,10 @@ export class CustomerInvoiceModel extends Model<
|
||||
// Relaciones
|
||||
declare items: NonAttribute<CustomerInvoiceItemModel[]>;
|
||||
declare taxes: NonAttribute<CustomerInvoiceTaxModel[]>;
|
||||
declare current_customer: NonAttribute<CustomerModel>;
|
||||
declare verifactu: NonAttribute<VerifactuRecordModel>;
|
||||
declare current_customer?: NonAttribute<CustomerModel | null>;
|
||||
declare verifactu?: NonAttribute<VerifactuRecordModel | null>;
|
||||
declare proforma?: NonAttribute<CustomerInvoiceModel | null>;
|
||||
declare linked_invoice?: NonAttribute<CustomerInvoiceModel | null>;
|
||||
|
||||
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",
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -45,6 +45,8 @@ export const ListProformasResponseSchema = createPaginatedListSchema(
|
||||
taxes_amount: MoneySchema,
|
||||
total_amount: MoneySchema,
|
||||
|
||||
linked_invoice_id: z.string(),
|
||||
|
||||
metadata: MetadataSchema.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
@ -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<ProformaSummaryData, unknown>[];
|
||||
columns: ColumnDef<ProformaListRow, unknown>[];
|
||||
|
||||
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 (
|
||||
|
||||
@ -41,7 +41,7 @@ export const ProformaListPage = () => {
|
||||
onChangeStatusClick: handleChangeStatusProforma,
|
||||
});
|
||||
|
||||
if (listCtrl.isError || !listCtrl.data) {
|
||||
if (listCtrl.isError) {
|
||||
return (
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
@ -70,74 +70,81 @@ export const ProformaListPage = () => {
|
||||
title={t("pages.proformas.list.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
|
||||
<AppContent>
|
||||
{/* Search and filters */}
|
||||
<div className="flex items-center justify-between gap-16">
|
||||
<SimpleSearchInput
|
||||
loading={listCtrl.isLoading}
|
||||
onSearchChange={listCtrl.setSearchValue}
|
||||
value={listCtrl.search}
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
{/* Search and filters */}
|
||||
<div className="h-full min-w-0 overflow-auto w-full">
|
||||
<div className="flex items-center justify-between gap-16">
|
||||
<SimpleSearchInput
|
||||
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}>
|
||||
<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>
|
||||
{/* 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}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -94,6 +94,8 @@ export const ListProformasRowAdapter = {
|
||||
rowDto.currency_code,
|
||||
rowDto.language_code
|
||||
),
|
||||
|
||||
linked_invoice_id: rowDto.linked_invoice_id,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@ -46,4 +46,6 @@ export interface ProformaListRow {
|
||||
|
||||
total_amount: number;
|
||||
total_amount_fmt: string;
|
||||
|
||||
linked_invoice_id: string;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user