Característica: Mejora de los modelos de facturas proforma y emitidas con atributos de serie de documentos

- Se han añadido `document_series_id`, `invoice_series_code` e `invoice_reference` a los modelos y instantáneas de facturas emitidas.
- Se han introducido `proforma_series_code` y `proforma_number`, y se han actualizado las referencias en los modelos y instantáneas de las facturas proforma.
- Se han actualizado los esquemas de la base de datos y los scripts SQL para dar cabida a los nuevos campos y garantizar la integridad de los datos.
- Se ha elaborado la documentación correspondiente a las nuevas series de documentos «contrato de instantánea» y «contrato de serie de facturas emitidas».
- Se han implementado scripts SQL de rellenado para completar los registros existentes con los nuevos campos.
This commit is contained in:
David Arranz 2026-08-04 11:05:02 +02:00
parent 1f5bd18665
commit ee040b8335
51 changed files with 456 additions and 43 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,31 @@
# Document Series Snapshot Contract
## Regla base
- `document-series` asigna numero y referencia.
- `proformas` e `issued_invoices` persisten un snapshot inmutable del resultado.
- las referencias visibles no se recalculan en lecturas normales.
## Snapshot persistido
### Proformas
- `document_series_id`
- `proforma_series_code`
- `proforma_number`
- `proforma_reference`
### Issued invoices
- `document_series_id`
- `invoice_series_code`
- `invoice_number`
- `invoice_reference`
## Motivo
- `document_series_id` conserva trazabilidad tecnica exacta.
- `series_code` conserva un snapshot legible e historico.
- `number` conserva el valor estructurado y consultable.
- `reference` conserva el valor visible/legal final.
- una factura emitida es historica e inalterable aunque luego cambie la configuracion viva de `document_series`.

View File

@ -0,0 +1,42 @@
# Issued Invoice Series Contract
## Objetivo
Definir el contrato backend para la numeracion documental de `issued_invoices`.
## Snapshot persistido
Una factura emitida debe persistir estos cuatro datos de numeracion:
- `document_series_id`
- `invoice_series_code`
- `invoice_number`
- `invoice_reference`
## Regla operativa
- `document-series` es la unica fuente de verdad para asignar numeracion de `document_type = issued_invoice`.
- `customer-invoices` no recompone manualmente la referencia final si `document-series` ya la devuelve.
- `invoice_reference` se calcula en el momento de emision.
- `invoice_reference` se persiste como snapshot documental inmutable.
- `invoice_reference` no se recalcula despues aunque cambie la configuracion de `document_series`.
## Emision desde proforma
- si `target_invoice_series_code` tiene valor, se usa como `seriesCode` de `issued_invoice`
- si `target_invoice_series_code` es `NULL`, `document-series` resuelve la serie default activa
- la respuesta de `assignNextNumber(...)` debe aportar `series_id`, `series_code`, `number` y `reference`
- `issued_invoices` debe persistir `document_series_id`, `invoice_series_code`, `invoice_number` e `invoice_reference`
## Semantica de campos
- `document_series_id`: id tecnico de la serie exacta usada al emitir para trazabilidad y backoffice
- `invoice_number`: numero secuencial dentro de la serie
- `invoice_reference`: referencia final visible/legal compuesta por la serie documental
- `reference`: referencia comercial libre del documento, si aplica; no sustituye a `invoice_reference`
## Regla de no contaminacion
- `proforma_reference` nunca se reutiliza como `invoice_reference`
- `target_invoice_series_code` solo selecciona la serie futura y no es en si mismo la referencia final
- no se usa `MAX(invoice_number)` para recomponer numeracion

View File

@ -9,6 +9,7 @@ Normalizar el contrato publico de proformas para separar definitivamente la seri
En proformas:
- `document_series_id` identifica la serie documental propia de la proforma
- `proforma_series_code` guarda el codigo snapshot de la serie propia de la proforma
- `proforma_number` guarda el numero propio de la proforma
- `proforma_reference` guarda la referencia visible de la proforma, por ejemplo `PF-0139`
- `proforma_series_code` es el codigo funcional que se usa solo en create para elegir una serie de `document_type = proforma`
@ -92,7 +93,7 @@ Si el request informa `proforma_series_code`:
- debe corresponder a una serie activa de `document_type = proforma`
- solo se usa para numerar la proforma en create
- despues queda congelado en `document_series_id`, `proforma_number` y `proforma_reference`
- despues queda congelado en `document_series_id`, `proforma_series_code`, `proforma_number` y `proforma_reference`
## Retirada futura de `series`

View File

@ -32,7 +32,7 @@ Preparar el esquema fisico para separar:
- `target_invoice_series_code` es opcional y solo puede apuntar a una serie de `issued_invoice`.
- una serie `PF` o cualquier otra serie de `document_type = proforma` nunca debe persistirse en `target_invoice_series_code`.
- si `target_invoice_series_code` queda `NULL`, la emision de la factura usara la serie default activa de `issued_invoice`.
- `issued_invoices` usa `invoice_series_code`, `invoice_number`, `invoice_date` y `source_proforma_id`.
- `issued_invoices` usa `invoice_series_code`, `invoice_number`, `invoice_reference`, `invoice_date` y `source_proforma_id`.
- `proformas` no guarda `issued_invoice_id`; la relacion se resuelve con `issued_invoices.source_proforma_id`.
- Los impuestos del esquema actual son por documento, no por linea: `proforma_taxes` y `issued_invoice_taxes`.
- `payment_method` y `payment_term` en `issued_invoices` son snapshot historico, no estado vivo de cobro.
@ -119,6 +119,8 @@ Preparar el esquema fisico para separar:
- al emitir una proforma, la factura definitiva se numera con `document_type = issued_invoice`
- si la proforma trae `target_invoice_series_code`, se usa como `seriesCode`
- si `target_invoice_series_code` es `NULL`, `document-series` resuelve la serie default activa de `issued_invoice`
- la referencia final devuelta por `document-series` se persiste en `issued_invoices.invoice_reference`
- `invoice_reference` es snapshot documental y no se recalcula despues
- `proforma_series_code` es un input de create para `document_type = proforma` y no vuelve a ser editable despues
## Estado Fase 2B
@ -218,6 +220,7 @@ Preparar el esquema fisico para separar:
- `source_proforma_id`
- `invoice_series_code`
- `invoice_number`
- `invoice_reference`
- `invoice_date`
- `operation_date`
- `reference`

View File

@ -8,6 +8,8 @@ Este directorio mezcla SQL vigentes de soporte con SQL historicos/transicionales
- `add-proforma-document-series-columns.sql`
- `add-proforma-tax-config-columns.sql`
- `add-issued-invoice-reference.sql`
- `normalize-document-series-snapshots.sql`
- `fix-proforma-series-semantics.sql`
- `validate-proforma-tax-config.sql`
- `validate-proforma-soft-delete.sql`
@ -24,8 +26,9 @@ Estos archivos no deben tratarse como el camino normal de entornos nuevos salvo
1. Verificar que la BD objetivo es inequívocamente de desarrollo.
2. Hacer backup o confirmar que los datos son descartables.
3. Ejecutar el DDL vigente que falte para `proformas`.
4. Ejecutar las validaciones SQL vigentes.
5. Levantar backend V2 y validar flujos funcionales.
4. Ejecutar `normalize-document-series-snapshots.sql` para completar snapshots y validaciones de seriado.
5. Ejecutar las validaciones SQL vigentes.
6. Levantar backend V2 y validar flujos funcionales.
## Propiedades de los scripts históricos de split

View File

@ -0,0 +1,53 @@
-- Estado: vigente / manual.
-- Objetivo:
-- 1. Anadir `issued_invoices.invoice_reference` si no existe.
-- 2. Backfill conservador para facturas emitidas existentes.
-- 3. Validar que no quedan filas sin snapshot documental.
-- 4. Endurecer a NOT NULL cuando la validacion sea correcta.
--
-- No ejecutar automaticamente desde backend.
-- Revisar antes el nombre real de la tabla `document_series` y sus columnas
-- `prefix`, `padding`, `suffix` en el entorno objetivo.
START TRANSACTION;
ALTER TABLE issued_invoices
ADD COLUMN IF NOT EXISTS invoice_reference VARCHAR(64) NULL AFTER invoice_number;
UPDATE issued_invoices ii
JOIN document_series ds
ON ds.company_id = ii.company_id
AND ds.code = ii.invoice_series_code
AND ds.document_type = 'issued_invoice'
SET ii.invoice_reference = CONCAT(
COALESCE(ds.prefix, ''),
LPAD(CAST(ii.invoice_number AS CHAR), ds.padding, '0'),
COALESCE(ds.suffix, '')
)
WHERE ii.invoice_reference IS NULL;
SELECT id, source_proforma_id, invoice_series_code, invoice_number, invoice_reference
FROM issued_invoices
WHERE invoice_reference IS NULL;
SELECT
ii.id,
ii.invoice_series_code,
ii.invoice_number,
ii.invoice_reference,
ds.prefix,
ds.padding,
ds.suffix
FROM issued_invoices ii
JOIN document_series ds
ON ds.company_id = ii.company_id
AND ds.code = ii.invoice_series_code
AND ds.document_type = 'issued_invoice'
ORDER BY ii.created_at DESC
LIMIT 20;
-- Ejecutar solo cuando la consulta anterior no devuelva filas.
ALTER TABLE issued_invoices
MODIFY COLUMN invoice_reference VARCHAR(64) NOT NULL;
COMMIT;

View File

@ -170,6 +170,7 @@ CREATE TABLE IF NOT EXISTS issued_invoices (
source_proforma_id CHAR(36) NULL,
invoice_series_code VARCHAR(10) NOT NULL,
invoice_number VARCHAR(12) NOT NULL,
invoice_reference VARCHAR(64) NOT NULL,
invoice_date DATE NOT NULL,
operation_date DATE NULL,
reference VARCHAR(255) NULL,

View File

@ -0,0 +1,85 @@
-- Normaliza snapshots documentales en `proformas` e `issued_invoices`.
-- Fases:
-- 1. DDL conservadora.
-- 2. Backfill de snapshots.
-- 3. Diagnostico/validacion.
-- 4. Endurecimiento opcional cuando no queden nulos.
ALTER TABLE proformas
ADD COLUMN IF NOT EXISTS proforma_series_code VARCHAR(10) NULL AFTER document_series_id;
ALTER TABLE issued_invoices
ADD COLUMN IF NOT EXISTS document_series_id CHAR(36) NULL AFTER source_proforma_id,
ADD COLUMN IF NOT EXISTS invoice_reference VARCHAR(64) NULL AFTER invoice_number;
UPDATE proformas p
JOIN document_series ds ON ds.id = p.document_series_id
SET p.proforma_series_code = ds.code
WHERE p.proforma_series_code IS NULL;
SELECT id, document_series_id, proforma_series_code, proforma_number, proforma_reference
FROM proformas
WHERE proforma_series_code IS NULL;
SELECT
ds.company_id,
ds.document_type,
ds.code,
COUNT(*) AS series_count
FROM document_series ds
WHERE ds.document_type = 'issued_invoice'
GROUP BY ds.company_id, ds.document_type, ds.code
HAVING COUNT(*) > 1;
UPDATE issued_invoices ii
JOIN document_series ds
ON ds.company_id = ii.company_id
AND ds.document_type = 'issued_invoice'
AND ds.code = ii.invoice_series_code
SET ii.document_series_id = ds.id
WHERE ii.document_series_id IS NULL;
SELECT id, invoice_series_code, invoice_number
FROM issued_invoices
ORDER BY created_at DESC
LIMIT 20;
UPDATE issued_invoices ii
JOIN document_series ds ON ds.id = ii.document_series_id
SET ii.invoice_reference = CONCAT(
COALESCE(ds.prefix, ''),
LPAD(CAST(ii.invoice_number AS UNSIGNED), ds.padding, '0'),
COALESCE(ds.suffix, '')
)
WHERE ii.invoice_reference IS NULL;
SELECT id, source_proforma_id, document_series_id, invoice_series_code, invoice_number, invoice_reference
FROM issued_invoices
WHERE document_series_id IS NULL
OR invoice_reference IS NULL;
SELECT company_id, proforma_series_code, proforma_number, COUNT(*) AS c
FROM proformas
WHERE deleted_at IS NULL
GROUP BY company_id, proforma_series_code, proforma_number
HAVING c > 1;
SELECT company_id, invoice_series_code, invoice_number, COUNT(*) AS c
FROM issued_invoices
WHERE deleted_at IS NULL
GROUP BY company_id, invoice_series_code, invoice_number
HAVING c > 1;
CREATE INDEX idx_proformas_document_series_id
ON proformas (document_series_id);
CREATE INDEX idx_issued_invoices_document_series_id
ON issued_invoices (document_series_id);
-- Ejecutar solo cuando las validaciones anteriores devuelvan 0 filas pendientes.
-- ALTER TABLE proformas
-- MODIFY proforma_series_code VARCHAR(10) NOT NULL;
--
-- ALTER TABLE issued_invoices
-- MODIFY document_series_id CHAR(36) NOT NULL,
-- MODIFY invoice_reference VARCHAR(64) NOT NULL;

View File

@ -52,8 +52,11 @@ La asignación usa transacción obligatoria y bloqueo de fila con `LOCK.UPDATE`
En V1:
- `issued-invoices` delega la asignacion de numero en `document-series`
- `document-series.assignNextNumber(...)` devuelve `series_id`, `series_code`, `number` y `reference`
- `proformas` delega la asignacion de referencia en `document-series`
- `proformas` persiste `document_series_id` y `proforma_number` como snapshot tecnico adicional a `proforma_reference`
- `issued-invoices` persiste `invoice_reference` como snapshot documental inmutable adicional a `invoice_number`
- `issued-invoices.invoice_reference` no se recalcula si luego cambia `prefix`, `suffix` o `padding` de la serie
- `proformas.document_series_id` identifica la serie documental usada para numerar la proforma
- `proformas.proforma_number` guarda el numero propio de la proforma
- `proformas.proforma_reference` guarda la referencia visible de la proforma

View File

@ -5,7 +5,7 @@ import type { InvoiceNumber, InvoiceSerie } from "../../../domain";
export interface InvoiceSeriesAssignment {
invoiceNumber: InvoiceNumber;
formattedNumber: string;
invoiceReference: string;
invoiceSeriesId: string;
invoiceSeriesCode: InvoiceSerie;
}

View File

@ -14,7 +14,10 @@ export type IssuedInvoiceSummary = {
id: UniqueID;
companyId: UniqueID;
documentSeriesId: Maybe<string>;
invoiceSeriesCode: Maybe<InvoiceSerie>;
invoiceNumber: InvoiceNumber;
invoiceReference: string;
status: InvoiceStatus;
series: Maybe<InvoiceSerie>;

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 { IssuedInvoice } from "../../../domain";
import type { IInvoiceSeriesNumberAssigner } from "../../invoice-series";
@ -45,13 +45,13 @@ export class IssuedInvoiceCreator implements IIssuedInvoiceCreator {
return Result.fail(numberResult.error);
}
const invoiceNumber = numberResult.data.invoiceNumber;
const invoiceResult = IssuedInvoice.create(
{
...props,
documentSeriesId: Maybe.some(numberResult.data.invoiceSeriesId),
series: numberResult.data.invoiceSeriesCode,
invoiceNumber,
invoiceNumber: numberResult.data.invoiceNumber,
invoiceReference: numberResult.data.invoiceReference,
companyId,
},
id

View File

@ -34,8 +34,7 @@ export class IssuedInvoiceDocumentMetadataFactory
}
private buildFilename(snapshot: IIssuedInvoiceReportSnapshot): string {
// Ejemplo: factura-F2024-000123-FULANITO.pdf
return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`;
return `factura-${snapshot.invoice_reference}-${snapshot.recipient.name}.pdf`;
}
private buildCacheKey(snapshot: IIssuedInvoiceReportSnapshot): string {
@ -43,8 +42,7 @@ export class IssuedInvoiceDocumentMetadataFactory
return [
"issued-invoice",
snapshot.company_id,
snapshot.series,
snapshot.invoice_number,
snapshot.invoice_reference,
"v1",
].join(":");
}

View File

@ -1,6 +1,6 @@
import type { IDocumentProperties, IDocumentPropertiesFactory } from "@erp/core/api";
import type { IssuedInvoiceReportSnapshot } from "../models";
import type { IIssuedInvoiceReportSnapshot } from "../snapshot-builders";
/**
* Construye los metadatos del documento PDF de una factura emitida.
@ -10,11 +10,11 @@ import type { IssuedInvoiceReportSnapshot } from "../models";
* - Sin IO
*/
export class IssuedInvoiceDocumentPropertiesFactory
implements IDocumentPropertiesFactory<IssuedInvoiceReportSnapshot>
implements IDocumentPropertiesFactory<IIssuedInvoiceReportSnapshot>
{
build(snapshot: IssuedInvoiceReportSnapshot): IDocumentProperties {
build(snapshot: IIssuedInvoiceReportSnapshot): IDocumentProperties {
return {
title: snapshot.reference,
title: snapshot.invoice_reference,
subject: "issued-invoice",
author: snapshot.company_slug,
creator: "FactuGES ERP",

View File

@ -19,7 +19,7 @@ import type { ProformaIssueReadModel } from "../models";
export type ProformaToIssuedInvoiceCreateProps = Omit<
IIssuedInvoiceCreateProps,
"invoiceNumber" | "series"
"documentSeriesId" | "invoiceNumber" | "invoiceReference" | "series"
> & {
series: Maybe<InvoiceSerie>;
};

View File

@ -1,5 +1,6 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToNullable } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
import type { IssuedInvoice } from "../../../../domain";
@ -41,7 +42,10 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
company_id: invoice.companyId.toString(),
is_proforma: false,
document_series_id: maybeToNullable(invoice.documentSeriesId, (value) => value),
invoice_series_code: maybeToNullable(Maybe.some(invoice.series), (value) => value.toString()),
invoice_number: invoice.invoiceNumber.toString(),
invoice_reference: invoice.invoiceReference,
status: invoice.status.toPrimitive(),
series: invoice.series.toString(),

View File

@ -19,7 +19,10 @@ export interface IIssuedInvoiceFullSnapshot {
company_id: string;
is_proforma: boolean;
document_series_id: string | null;
invoice_series_code: string | null;
invoice_number: string;
invoice_reference: string;
status: string;
series: string;

View File

@ -15,7 +15,7 @@ export class IssuedInvoiceReportItemSnapshotBuilder
items: IIssuedInvoiceItemFullSnapshot[],
params?: ISnapshotBuilderParams
): IIssuedInvoiceReportItemSnapshot[] {
const locale = params?.locale as string;
const locale = (params as { locale?: string } | undefined)?.locale ?? "es";
const moneyOptions = {
hideZeros: true,
@ -23,7 +23,7 @@ export class IssuedInvoiceReportItemSnapshotBuilder
};
return items.map((item) => ({
description: item.description,
description: item.description ?? "",
quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }),
unit_amount: MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions),
subtotal_amount: MoneyDTOHelper.format(item.subtotal_amount, locale, moneyOptions),

View File

@ -21,7 +21,7 @@ export class IssuedInvoiceReportSnapshotBuilder implements IIssuedInvoiceReportS
snapshot: IIssuedInvoiceFullSnapshot,
params?: ISnapshotBuilderParams
): IIssuedInvoiceReportSnapshot {
const locale = params?.locale as string;
const locale = (params as { locale?: string } | undefined)?.locale ?? "es";
const moneyOptions = {
hideZeros: true,
@ -33,6 +33,7 @@ export class IssuedInvoiceReportSnapshotBuilder implements IIssuedInvoiceReportS
company_id: snapshot.company_id,
company_slug: "rodax",
invoice_number: snapshot.invoice_number,
invoice_reference: snapshot.invoice_reference,
series: snapshot.series,
status: snapshot.status,
reference: snapshot.reference,
@ -43,7 +44,7 @@ export class IssuedInvoiceReportSnapshotBuilder implements IIssuedInvoiceReportS
invoice_date: DateHelper.format(snapshot.invoice_date, locale),
payment_method: snapshot.payment_method?.payment_description ?? "",
notes: snapshot.notes,
notes: snapshot.notes ?? "",
recipient: {
name: snapshot.recipient.name,
@ -65,7 +66,8 @@ export class IssuedInvoiceReportSnapshotBuilder implements IIssuedInvoiceReportS
verifactu: {
...snapshot.verifactu,
qr_code: snapshot.verifactu.qr_code.replace("data:image/png;base64,", ""),
url: snapshot.verifactu.url ?? "",
qr_code: (snapshot.verifactu.qr_code ?? "").replace("data:image/png;base64,", ""),
},
};
}

View File

@ -6,9 +6,10 @@ export interface IIssuedInvoiceReportSnapshot {
company_id: string;
company_slug: string;
invoice_number: string;
invoice_reference: string;
series: string;
status: string;
reference: string;
reference: string | null;
language_code: string;
currency_code: string;

View File

@ -15,7 +15,7 @@ export class IssuedInvoiceReportTaxSnapshotBuilder
taxes: IIssuedInvoiceTaxFullSnapshot[],
params?: ISnapshotBuilderParams
): IIssuedInvoiceReportTaxSnapshot[] {
const locale = params?.locale as string;
const locale = (params as { locale?: string } | undefined)?.locale ?? "es";
const moneyOptions = {
hideZeros: true,
@ -29,11 +29,11 @@ export class IssuedInvoiceReportTaxSnapshotBuilder
iva_percentage: PercentageDTOHelper.format(tax.iva_percentage, locale),
iva_amount: MoneyDTOHelper.format(tax.iva_amount, locale, moneyOptions),
rec_code: tax.rec_code,
rec_code: tax.rec_code ?? "",
rec_percentage: PercentageDTOHelper.format(tax.rec_percentage, locale),
rec_amount: MoneyDTOHelper.format(tax.rec_amount, locale, moneyOptions),
retention_code: tax.retention_code,
retention_code: tax.retention_code ?? "",
retention_percentage: PercentageDTOHelper.format(tax.retention_percentage, locale),
retention_amount: MoneyDTOHelper.format(tax.retention_amount, locale, moneyOptions),

View File

@ -32,7 +32,10 @@ export class IssuedInvoiceSummarySnapshotBuilder implements IIssuedInvoiceSummar
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
document_series_id: maybeToNullable(invoice.documentSeriesId, (value) => value),
invoice_series_code: maybeToNullable(invoice.invoiceSeriesCode, (value) => value.toString()),
invoice_number: invoice.invoiceNumber.toString(),
invoice_reference: invoice.invoiceReference,
status: invoice.status.toPrimitive(),
series: maybeToEmptyString(invoice.series, (v) => v.toString()),

View File

@ -5,7 +5,10 @@ export interface IIssuedInvoiceSummarySnapshot {
id: string;
company_id: string;
document_series_id: string | null;
invoice_series_code: string | null;
invoice_number: string;
invoice_reference: string;
status: string;
series: string;

View File

@ -13,6 +13,9 @@ export type ProformaSummary = {
id: UniqueID;
companyId: UniqueID;
documentSeriesId: Maybe<string>;
proformaSeriesCode: Maybe<InvoiceSerie>;
proformaNumber: Maybe<InvoiceNumber>;
proformaReference: InvoiceNumber;
status: InvoiceStatus;
archivedAt: Maybe<UtcDate>;

View File

@ -6,6 +6,7 @@ import {
type IProformaCreateProps,
type IProformaItemCreateProps,
InvoiceNumber,
InvoiceSerie,
Proforma,
ProformaTaxConfig,
} from "../../../domain";
@ -98,6 +99,12 @@ export class ProformaCreator implements IProformaCreator {
return Result.fail(proformaNumberResult.error);
}
const proformaSeriesCodeResult = InvoiceSerie.create(numberResult.data.documentSeriesCode);
if (proformaSeriesCodeResult.isFailure) {
return Result.fail(proformaSeriesCodeResult.error);
}
const proformaReference = proformaReferenceResult.data;
// 2. Crear agregado
@ -107,6 +114,7 @@ export class ProformaCreator implements IProformaCreator {
companyId,
archivedAt: Maybe.none(),
documentSeriesId: Maybe.some(numberResult.data.documentSeriesId),
proformaSeriesCode: Maybe.some(proformaSeriesCodeResult.data),
proformaNumber: Maybe.some(proformaNumberResult.data),
proformaReference,
targetInvoiceSeriesCode: resolvedPropsResult.data.targetInvoiceSeriesCode,
@ -142,7 +150,10 @@ export class ProformaCreator implements IProformaCreator {
transaction?: unknown
): Promise<
Result<
Omit<IProformaCreateProps, "proformaReference" | "proformaNumber" | "documentSeriesId">,
Omit<
IProformaCreateProps,
"proformaReference" | "proformaNumber" | "documentSeriesId" | "proformaSeriesCode"
>,
Error
>
> {

View File

@ -14,7 +14,7 @@ export class ProformaDocumentPropertiesFactory
{
build(snapshot: ProformaReportSnapshot): IDocumentProperties {
return {
title: snapshot.reference ?? `Proforma ${snapshot.series}${snapshot.invoice_number}`,
title: snapshot.reference ?? snapshot.proforma_reference,
subject: "proforma",
author: snapshot.company.legalName,
creator: "FactuGES ERP",

View File

@ -40,6 +40,11 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
id: proforma.id.toString(),
company_id: proforma.companyId.toString(),
document_series_id: maybeToNullable(proforma.documentSeriesId, (value) => value),
proforma_series_code: maybeToNullable(proforma.proformaSeriesCode, (value) =>
value.toString()
),
proforma_number: maybeToNullable(proforma.proformaNumber, (value) => value.toString()),
proforma_reference: proforma.proformaReference.toString(),
status: proforma.status.toPrimitive() as ProformaFullSnapshot["status"],
archived_at: maybeToNullable(proforma.archivedAt, (value) => value.toISOString()),

View File

@ -12,8 +12,11 @@ export class IssueProformaResponseSnapshotBuilder implements IIssueProformaRespo
issued_invoice: {
id: invoice.id.toString(),
proforma_id: invoice.linkedProformaId.toString(),
document_series_id: invoice.documentSeriesId.getOrUndefined() ?? null,
invoice_series_code: invoice.series.toString(),
series: invoice.series.toString(),
invoice_number: invoice.invoiceNumber.toString(),
invoice_reference: invoice.invoiceReference,
invoice_date: invoice.invoiceDate.toDateString(),
status: "issued",
recipient: {

View File

@ -53,6 +53,7 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui
return {
id: snapshot.id,
company_id: snapshot.company_id,
proforma_reference: snapshot.proforma_reference,
invoice_number: snapshot.invoice_number,
series: snapshot.series,
status: snapshot.status,

View File

@ -5,6 +5,7 @@ import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.i
export interface ProformaReportSnapshot {
id: string;
company_id: string;
proforma_reference: string;
invoice_number: string;
series: string | null;
status: string;

View File

@ -15,11 +15,18 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB
id: proforma.id.toString(),
company_id: proforma.companyId.toString(),
document_series_id: maybeToNullable(proforma.documentSeriesId, (value) => value),
proforma_series_code: maybeToNullable(proforma.proformaSeriesCode, (value) =>
value.toString()
),
proforma_number: maybeToNullable(proforma.proformaNumber, (value) => value.toString()),
proforma_reference: proforma.proformaReference.toString(),
status: proforma.status.toPrimitive() as ProformaSummaryDTO["status"],
archived_at: maybeToNullable(proforma.archivedAt, (value) => value.toISOString()),
target_invoice_series_code: maybeToNullable(proforma.series, (value) => value.toString()),
series: maybeToNullable(proforma.series, (value) => value.toString()),
target_invoice_series_code: maybeToNullable(proforma.targetInvoiceSeriesCode, (value) =>
value.toString()
),
series: maybeToNullable(proforma.targetInvoiceSeriesCode, (value) => value.toString()),
proforma_date: proforma.proformaDate.toDateString(),
operation_date: maybeToNullable(proforma.operationDate, (value) => value.toDateString()),

View File

@ -35,8 +35,10 @@ export interface IIssuedInvoiceCreateProps {
linkedProformaId: UniqueID; // <- id de la proforma padre en caso de issue
documentSeriesId: Maybe<string>;
series: InvoiceSerie;
invoiceNumber: InvoiceNumber;
invoiceReference: string;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
@ -151,6 +153,16 @@ export class IssuedInvoice
);
}
if (!props.invoiceReference.trim()) {
return Result.fail(
new DomainValidationError(
"MISSING_INVOICE_REFERENCE",
"invoiceReference",
"Issued invoice requires invoice reference"
)
);
}
return Result.ok();
}
@ -194,6 +206,10 @@ export class IssuedInvoice
return this.props.linkedProformaId;
}
public get documentSeriesId(): Maybe<string> {
return this.props.documentSeriesId;
}
public get status(): InvoiceStatus {
return this.props.status;
}
@ -206,6 +222,10 @@ export class IssuedInvoice
return this.props.invoiceNumber;
}
public get invoiceReference(): string {
return this.props.invoiceReference;
}
public get invoiceDate(): UtcDate {
return this.props.invoiceDate;
}

View File

@ -51,6 +51,7 @@ export interface IProformaCreateProps {
archivedAt: Maybe<UtcDate>;
documentSeriesId: Maybe<string>;
proformaSeriesCode: Maybe<InvoiceSerie>;
proformaNumber: Maybe<InvoiceNumber>;
proformaReference: InvoiceNumber;
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
@ -105,6 +106,7 @@ export interface IProforma {
archivedAt: Maybe<UtcDate>;
documentSeriesId: Maybe<string>;
proformaSeriesCode: Maybe<InvoiceSerie>;
proformaNumber: Maybe<InvoiceNumber>;
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
proformaReference: InvoiceNumber;
@ -266,6 +268,10 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return this.props.documentSeriesId;
}
public get proformaSeriesCode(): Maybe<InvoiceSerie> {
return this.props.proformaSeriesCode;
}
public get proformaNumber(): Maybe<InvoiceNumber> {
return this.props.proformaNumber;
}

View File

@ -44,8 +44,10 @@ export class IssuedInvoiceModel extends Model<
declare status: string;
declare source_proforma_id: CreationOptional<string | null>;
declare document_series_id: CreationOptional<string | null>;
declare invoice_series_code: string;
declare invoice_number: string;
declare invoice_reference: string;
declare invoice_date: string;
declare operation_date: CreationOptional<string | null>;
@ -231,6 +233,13 @@ export default (database: Sequelize) => {
charset: "utf8mb4",
collate: "utf8mb4_bin",
} as any,
document_series_id: {
type: DataTypes.UUID,
allowNull: true,
defaultValue: null,
charset: "utf8mb4",
collate: "utf8mb4_bin",
} as any,
invoice_series_code: {
type: new DataTypes.STRING(10),
@ -240,6 +249,10 @@ export default (database: Sequelize) => {
type: new DataTypes.STRING(12),
allowNull: false,
},
invoice_reference: {
type: new DataTypes.STRING(64),
allowNull: false,
},
invoice_date: {
type: new DataTypes.DATEONLY(),
allowNull: false,
@ -502,6 +515,10 @@ export default (database: Sequelize) => {
name: "idx_issued_invoices_company",
fields: ["company_id"],
},
{
name: "idx_issued_invoices_document_series_id",
fields: ["document_series_id"],
},
{
name: "uq_issued_invoice_company_series_number",
fields: ["company_id", "invoice_series_code", "invoice_number"],

View File

@ -37,6 +37,7 @@ export class ProformaModel extends Model<
declare archived_at: CreationOptional<string | null>;
declare document_series_id: CreationOptional<string | null>;
declare proforma_series_code: CreationOptional<string | null>;
declare proforma_number: CreationOptional<string | null>;
declare proforma_reference: string;
declare proforma_date: string;
@ -192,6 +193,11 @@ export default (database: Sequelize) => {
charset: "utf8mb4",
collate: "utf8mb4_bin",
} as any,
proforma_series_code: {
type: new DataTypes.STRING(10),
allowNull: true,
defaultValue: null,
},
proforma_number: {
type: new DataTypes.STRING(32),
allowNull: true,
@ -479,6 +485,15 @@ export default (database: Sequelize) => {
name: "idx_proformas_company_document_series",
fields: ["company_id", "document_series_id"],
},
{
name: "idx_proformas_document_series_id",
fields: ["document_series_id"],
},
{
name: "uq_proformas_company_series_number",
fields: ["company_id", "proforma_series_code", "proforma_number"],
unique: true,
},
{
name: "uq_proformas_company_reference",
fields: ["company_id", "proforma_reference"],

View File

@ -60,7 +60,7 @@ export function buildIssuedInvoicePublicServices(
return Result.ok({
invoiceNumber: invoiceNumberResult.data,
formattedNumber: assignResult.data.reference,
invoiceReference: assignResult.data.reference,
invoiceSeriesId: assignResult.data.series_id,
invoiceSeriesCode: assignedSeriesResult.data,
});

View File

@ -83,6 +83,11 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
"invoice_number",
errors
);
const invoiceReference = extractOrPushError(
Result.ok(`${String(raw.series ?? "")}${String(raw.invoice_number ?? "")}`),
"invoice_reference",
errors
);
// Fechas
const invoiceDate = extractOrPushError(
@ -281,6 +286,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
status,
series,
invoiceNumber,
invoiceReference,
invoiceDate,
operationDate,
reference,
@ -375,6 +381,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
status: attributes.status!,
series: attributes.series!,
invoiceNumber: attributes.invoiceNumber!,
invoiceReference: attributes.invoiceReference!,
invoiceDate: attributes.invoiceDate!,
operationDate: attributes.operationDate!,

View File

@ -62,6 +62,11 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
"source_proforma_id",
errors
);
const documentSeriesId = extractOrPushError(
maybeFromNullableResult(raw.document_series_id, (value) => Result.ok(String(value))),
"document_series_id",
errors
);
const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
const series = extractOrPushError(
InvoiceSerie.create(raw.invoice_series_code ?? ""),
@ -73,6 +78,11 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
"invoice_number",
errors
);
const invoiceReference = extractOrPushError(
Result.ok(String(raw.invoice_reference ?? "")),
"invoice_reference",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(raw.invoice_date),
"invoice_date",
@ -217,9 +227,11 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
companyId,
customerId,
linkedProformaId,
documentSeriesId,
status,
series,
invoiceNumber,
invoiceReference,
invoiceDate,
operationDate,
reference,
@ -294,8 +306,10 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
const invoiceProps: InternalIssuedInvoiceProps = {
companyId: attributes.companyId!,
status: attributes.status!,
documentSeriesId: attributes.documentSeriesId!,
series: attributes.series!,
invoiceNumber: attributes.invoiceNumber!,
invoiceReference: attributes.invoiceReference!,
invoiceDate: attributes.invoiceDate!,
operationDate: attributes.operationDate!,
customerId: attributes.customerId!,
@ -372,8 +386,10 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
company_id: source.companyId.toPrimitive(),
status: source.status.toPrimitive(),
source_proforma_id: source.linkedProformaId.toPrimitive(),
document_series_id: maybeToNullable(source.documentSeriesId, (value) => value),
invoice_series_code: source.series.toPrimitive(),
invoice_number: source.invoiceNumber.toPrimitive(),
invoice_reference: source.invoiceReference,
invoice_date: source.invoiceDate.toPrimitive(),
operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()),
language_code: source.languageCode.toPrimitive(),

View File

@ -92,6 +92,7 @@ export class SequelizeIssuedInvoiceSummaryMapper extends SequelizeQueryMapper<
status: attributes.status!,
series: attributes.series!,
invoiceNumber: attributes.invoiceNumber!,
invoiceReference: attributes.invoiceReference!,
invoiceDate: attributes.invoiceDate!,
operationDate: attributes.operationDate!,
@ -141,6 +142,11 @@ export class SequelizeIssuedInvoiceSummaryMapper extends SequelizeQueryMapper<
"invoice_number",
errors
);
const invoiceReference = extractOrPushError(
Result.ok(`${String(raw.series ?? "")}${String(raw.invoice_number ?? "")}`),
"invoice_reference",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(raw.invoice_date),
@ -237,6 +243,7 @@ export class SequelizeIssuedInvoiceSummaryMapper extends SequelizeQueryMapper<
status,
series,
invoiceNumber,
invoiceReference,
invoiceDate,
operationDate,
reference,

View File

@ -75,9 +75,12 @@ export class SequelizeIssuedInvoiceV2SummaryMapper extends SequelizeQueryMapper<
id: attributes.invoiceId!,
companyId: attributes.companyId!,
isProforma: false,
documentSeriesId: attributes.documentSeriesId!,
invoiceSeriesCode: attributes.series!,
status: attributes.status!,
series: attributes.series!,
invoiceNumber: attributes.invoiceNumber!,
invoiceReference: attributes.invoiceReference!,
invoiceDate: attributes.invoiceDate!,
operationDate: attributes.operationDate!,
description: attributes.description!,
@ -102,6 +105,11 @@ export class SequelizeIssuedInvoiceV2SummaryMapper extends SequelizeQueryMapper<
const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors);
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
const documentSeriesId = extractOrPushError(
maybeFromNullableResult(raw.document_series_id, (value) => Result.ok(String(value))),
"document_series_id",
errors
);
const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
const series = extractOrPushError(
maybeFromNullableResult(raw.invoice_series_code, (value) => InvoiceSerie.create(value)),
@ -113,6 +121,11 @@ export class SequelizeIssuedInvoiceV2SummaryMapper extends SequelizeQueryMapper<
"invoice_number",
errors
);
const invoiceReference = extractOrPushError(
Result.ok(String(raw.invoice_reference ?? "")),
"invoice_reference",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(raw.invoice_date),
"invoice_date",
@ -182,9 +195,11 @@ export class SequelizeIssuedInvoiceV2SummaryMapper extends SequelizeQueryMapper<
invoiceId,
companyId,
customerId,
documentSeriesId,
status,
series,
invoiceNumber,
invoiceReference,
invoiceDate,
operationDate,
reference,

View File

@ -175,10 +175,11 @@ export class SequelizeIssuedInvoiceRepositoryV2
try {
const criteriaConverter = new CriteriaToSequelizeConverter();
const criteriaQuery = criteriaConverter.convert(criteria, {
searchableFields: ["invoice_number", "reference", "description"],
searchableFields: ["invoice_number", "invoice_reference", "reference", "description"],
mappings: {
invoice_date: { type: "root", column: "invoice_date" },
invoice_number: { type: "root", column: "invoice_number" },
invoice_reference: { type: "root", column: "invoice_reference" },
reference: { type: "root", column: "reference" },
description: { type: "root", column: "description" },
recipient_name: { type: "root", column: "customer_name" },

View File

@ -13,7 +13,7 @@ export class ProformaHtmlPreviewDocumentMetadataFactory
companySlug: snapshot.company.slug,
format: "HTML",
languageCode: snapshot.localization.languageCode,
filename: `proforma-${snapshot.series}${snapshot.invoice_number}.html`,
filename: `proforma-${snapshot.proforma_reference}.html`,
};
}
}

View File

@ -13,7 +13,7 @@ export class ProformaPdfDocumentMetadataFactory
companySlug: snapshot.company.slug,
format: "PDF",
languageCode: snapshot.localization.languageCode,
filename: `proforma-${snapshot.series}${snapshot.invoice_number}.pdf`,
filename: `proforma-${snapshot.proforma_reference}.pdf`,
storageKey: this.buildStorageKey(snapshot),
};
}

View File

@ -61,6 +61,11 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
"document_series_id",
errors
);
const proformaSeriesCode = extractOrPushError(
maybeFromNullableResult(raw.proforma_series_code, (value) => InvoiceSerie.create(value)),
"proforma_series_code",
errors
);
const targetInvoiceSeriesCode = extractOrPushError(
maybeFromNullableResult(raw.target_invoice_series_code, (value) =>
InvoiceSerie.create(value)
@ -167,6 +172,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
status,
archivedAt,
documentSeriesId,
proformaSeriesCode,
proformaNumber,
targetInvoiceSeriesCode,
proformaReference,
@ -220,6 +226,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
status: attributes.status!,
archivedAt: attributes.archivedAt!,
documentSeriesId: attributes.documentSeriesId!,
proformaSeriesCode: attributes.proformaSeriesCode!,
proformaNumber: attributes.proformaNumber!,
targetInvoiceSeriesCode: attributes.targetInvoiceSeriesCode!,
proformaReference: attributes.proformaReference!,
@ -286,6 +293,9 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
status: source.status.toPrimitive(),
archived_at: maybeToNullable(source.archivedAt, (value) => value.toPrimitive()),
document_series_id: maybeToNullable(source.documentSeriesId, (value) => value),
proforma_series_code: maybeToNullable(source.proformaSeriesCode, (value) =>
value.toPrimitive()
),
proforma_number: maybeToNullable(source.proformaNumber, (value) => value.toPrimitive()),
proforma_reference: source.proformaReference.toPrimitive(),
proforma_date: source.proformaDate.toPrimitive(),

View File

@ -59,6 +59,9 @@ export class SequelizeProformaV2SummaryMapper extends SequelizeQueryMapper<
return Result.ok({
id: attributes.invoiceId!,
companyId: attributes.companyId!,
documentSeriesId: attributes.documentSeriesId!,
proformaSeriesCode: attributes.proformaSeriesCode!,
proformaNumber: attributes.proformaNumber!,
status: attributes.status!,
archivedAt: attributes.archivedAt!,
targetInvoiceSeriesCode: attributes.targetInvoiceSeriesCode!,
@ -92,6 +95,21 @@ export class SequelizeProformaV2SummaryMapper extends SequelizeQueryMapper<
"archived_at",
errors
);
const documentSeriesId = extractOrPushError(
maybeFromNullableResult(raw.document_series_id, (value) => Result.ok(String(value))),
"document_series_id",
errors
);
const proformaSeriesCode = extractOrPushError(
maybeFromNullableResult(raw.proforma_series_code, (value) => InvoiceSerie.create(value)),
"proforma_series_code",
errors
);
const proformaNumber = extractOrPushError(
maybeFromNullableResult(raw.proforma_number, (value) => InvoiceNumber.create(value)),
"proforma_number",
errors
);
const targetInvoiceSeriesCode = extractOrPushError(
maybeFromNullableResult(raw.target_invoice_series_code, (value) =>
InvoiceSerie.create(value)
@ -172,6 +190,9 @@ export class SequelizeProformaV2SummaryMapper extends SequelizeQueryMapper<
invoiceId,
companyId,
customerId,
documentSeriesId,
proformaSeriesCode,
proformaNumber,
status,
archivedAt,
targetInvoiceSeriesCode,

View File

@ -22,7 +22,10 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
document_series_id: z.uuid().nullable(),
invoice_series_code: z.string().nullable(),
invoice_number: z.string(),
invoice_reference: z.string(),
status: IssuedInvoiceStatusSchema,
series: z.string(),

View File

@ -25,6 +25,9 @@ export const GetProformaByIdResponseSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
document_series_id: z.uuid().nullable(),
proforma_series_code: z.string().nullable(),
proforma_number: z.string().nullable(),
proforma_reference: z.string(),
status: ProformaStatusSchema,
archived_at: z.iso.datetime().nullable(),

View File

@ -19,8 +19,11 @@ export const IssueProformaIssuedInvoiceTotalsSchema = z.object({
export const IssueProformaIssuedInvoiceSchema = z.object({
id: z.uuid(),
proforma_id: z.uuid().nullable(),
document_series_id: z.uuid().nullable(),
invoice_series_code: z.string(),
series: z.string(),
invoice_number: z.string(),
invoice_reference: z.string(),
invoice_date: IsoDateSchema,
status: IssuedInvoiceStatusSchema,
recipient: IssueProformaIssuedInvoiceRecipientSchema,

View File

@ -9,7 +9,10 @@ export const IssuedInvoiceSummarySchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
document_series_id: z.uuid().nullable(),
invoice_series_code: z.string().nullable(),
invoice_number: z.string(),
invoice_reference: z.string(),
status: IssuedInvoiceStatusSchema,
series: z.string(),

View File

@ -8,6 +8,9 @@ export const ProformaSummarySchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
document_series_id: z.uuid().nullable(),
proforma_series_code: z.string().nullable(),
proforma_number: z.string().nullable(),
proforma_reference: z.string(),
status: ProformaStatusSchema,
archived_at: z.iso.datetime().nullable(),