From 659df116a2d5d943a13f16525171491204fb68fa Mon Sep 17 00:00:00 2001 From: david Date: Wed, 29 Jul 2026 10:16:34 +0200 Subject: [PATCH] . --- .../proforma-create-contract.md | 134 ++++++++++++++++++ .../proforma-series-contract.md | 8 ++ ...lit-proformas-issued-invoices-migration.md | 32 +++++ .../document-series/document-series-design.md | 3 + .../proformas/services/proforma-creator.ts | 4 +- .../proforma-create-auto-applied-info.tsx | 34 ++++- .../ui/editors/proforma-update-tax-editor.tsx | 4 +- 7 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 docs/customer-invoices/proforma-create-contract.md diff --git a/docs/customer-invoices/proforma-create-contract.md b/docs/customer-invoices/proforma-create-contract.md new file mode 100644 index 00000000..7331ce0b --- /dev/null +++ b/docs/customer-invoices/proforma-create-contract.md @@ -0,0 +1,134 @@ +# Proforma Create Contract + +## Objetivo + +Definir la semantica correcta de `create proforma` como creacion de cabecera inicial y separarla del calculo fiscal real que depende de lineas valoradas. + +## Regla principal + +`tax_regime_code` no es un impuesto aplicado. + +En create: + +- `taxRegimeCode` representa configuracion fiscal inicial +- `taxDefaults` sigue siendo un concepto de UI o resolucion interna, no un desglose fiscal persistido +- `taxes` representa impuestos realmente calculados desde lineas valoradas +- `totals` representa importes realmente calculados desde lineas valoradas + +## Contrato funcional de create + +Una proforma puede crearse sin lineas. + +Si `items` se omite o se envia como `[]`: + +- la proforma debe crearse correctamente como borrador +- debe generarse `proforma_reference` +- pueden resolverse defaults comerciales y fiscales iniciales +- `items` debe devolverse como `[]` +- `taxes` debe devolverse como `[]` +- los totales monetarios deben devolverse a `0` +- no deben crearse filas artificiales en `proforma_taxes` + +## Payload minimo esperado + +```ts +{ + proforma_date: string; + customer_id: string; + + proforma_series_code?: string | null; + target_invoice_series_code?: string | null; + + currency_code?: string; + payment_method_id?: string | null; + payment_term_id?: string | null; + tax_regime_code?: string | null; + items?: []; +} +``` + +Notas: + +- el contrato actual del modulo puede seguir exigiendo otros campos tecnicos ya existentes +- `items` es opcional; si existe, puede ir vacio +- no se usan defaults ocultos en Zod para disfrazar la intencion del request + +## Response esperada al crear sin lineas + +Adaptando nombres exactos al contrato publico actual: + +```ts +{ + id: string; + proforma_reference: string; + proforma_date: string; + customer_id: string; + currency_code: string; + payment_method_id: string | null; + payment_term_id: string | null; + tax_regime_code: string | null; + target_invoice_series_code: string | null; + items: []; + taxes: []; + totals: { + subtotal_before_discounts: "0.00"; + line_discount_total: "0.00"; + taxable_base: "0.00"; + tax_total: "0.00"; + total: "0.00"; + }; +} +``` + +## Punto correcto de calculo + +- `create` crea cabecera minima, numera y resuelve defaults +- `update` permite anadir o editar lineas +- al existir lineas valoradas, `update` recalcula `taxes` y `totals` + +## Wording de UI + +La UI de create debe hablar de: + +- "Se preparara automaticamente" +- "valores iniciales" +- "configuracion fiscal inicial" + +La UI no debe sugerir: + +- que los impuestos ya quedaron aplicados al crear +- que existe desglose fiscal real sin lineas +- que los totales ya fueron calculados si todavia no hay lineas valoradas + +## Diagnostico SQL recomendado + +```sql +SELECT + p.id, + p.proforma_reference, + COUNT(DISTINCT pi.item_id) AS item_rows, + COUNT(DISTINCT pt.tax_id) AS tax_rows +FROM proformas p +LEFT JOIN proforma_items pi + ON pi.proforma_id = p.id +LEFT JOIN proforma_taxes pt + ON pt.proforma_id = p.id +GROUP BY p.id, p.proforma_reference +HAVING COUNT(DISTINCT pi.item_id) = 0 + AND COUNT(DISTINCT pt.tax_id) > 0; +``` + +Resultado esperado: + +- `0` filas + +Si devuelve filas: + +- documentar los ids afectados +- revisar si provienen de datos legacy o de una version intermedia del flujo +- no borrar datos sin confirmacion explicita + +## Pendientes conocidos + +- `payment_term_id` aparece ya en contratos de transporte, pero el dominio/snapshot de proformas sigue sin soporte funcional completo en esta fase +- no se introduce un motor fiscal nuevo en este ajuste diff --git a/docs/customer-invoices/proforma-series-contract.md b/docs/customer-invoices/proforma-series-contract.md index f9ed97df..a28562cc 100644 --- a/docs/customer-invoices/proforma-series-contract.md +++ b/docs/customer-invoices/proforma-series-contract.md @@ -24,6 +24,10 @@ En proformas: - se pide cliente - `proforma_series_code` es opcional; si falta, se usa la serie default activa de `document_type = proforma` - `target_invoice_series_code` es opcional; si falta, se persiste `NULL` +- la creacion de proforma genera una cabecera valida aunque no existan lineas +- `items` puede omitirse o enviarse como `[]` +- si no hay lineas valoradas, la response debe devolver `items = []`, `taxes = []` y totales a `0` +- `tax_regime_code` en create representa configuracion fiscal inicial, no impuestos ya aplicados - la UI obtiene ambas listas desde `GET /document-series`, filtrando por `document_type` ### Editar draft @@ -54,12 +58,16 @@ En proformas: - el backend emite `target_invoice_series_code` como nombre preferente - `series` puede mantenerse solo como alias legacy de salida mientras existan consumidores antiguos +- `taxes` representa impuestos calculados a partir de lineas valoradas +- `totals` representa importes calculados a partir de lineas valoradas +- create no debe inventar filas fiscales sobre base `0` ## Regla de emision - si `target_invoice_series_code` tiene valor, la emision usa ese `seriesCode` para `document_type = issued_invoice` - si `target_invoice_series_code` es `NULL`, `document-series` resuelve la serie default activa de `issued_invoice` - la UI nunca debe consumir numeracion directa desde `POST /document-series/assign-next` +- los impuestos y totales reales se recalculan cuando la proforma ya tiene lineas en update ## Validacion defensiva diff --git a/docs/customer-invoices/split-proformas-issued-invoices-migration.md b/docs/customer-invoices/split-proformas-issued-invoices-migration.md index 466c3d98..3566860d 100644 --- a/docs/customer-invoices/split-proformas-issued-invoices-migration.md +++ b/docs/customer-invoices/split-proformas-issued-invoices-migration.md @@ -146,6 +146,16 @@ Preparar el esquema fisico para separar: - `target_invoice_series_code` sigue siendo el unico concepto de serie futura de factura emitida - la UI nueva de proformas ya envia `proforma_series_code` y `target_invoice_series_code` +## Estado Fase 2F + +- create de proforma se consolida como creacion de cabecera/documento inicial +- una proforma sin lineas es valida y debe persistirse con `items = []` +- `tax_regime_code` queda como configuracion fiscal inicial de la cabecera +- `proforma_taxes` solo representa impuestos calculados, no defaults fiscales ni placeholders sobre base `0` +- si una proforma nace sin lineas, la response debe devolver `taxes = []` y totales monetarios a `0` +- update sigue siendo el punto donde se anaden lineas y se materializan impuestos/totales reales +- la UI deja de prometer que la fiscalidad queda "aplicada" en create y pasa a hablar de valores iniciales + ## Rollback recomendado - no aplicar nada en produccion en esta fase @@ -809,6 +819,28 @@ SELECT SUM(total_amount_value) FROM customer_invoices WHERE is_proforma = 0; SELECT SUM(total_amount_value) FROM issued_invoices; ``` +### Proformas sin lineas con impuestos materializados + +```sql +SELECT + p.id, + p.proforma_reference, + COUNT(DISTINCT pi.item_id) AS item_rows, + COUNT(DISTINCT pt.tax_id) AS tax_rows +FROM proformas p +LEFT JOIN proforma_items pi + ON pi.proforma_id = p.id +LEFT JOIN proforma_taxes pt + ON pt.proforma_id = p.id +GROUP BY p.id, p.proforma_reference +HAVING COUNT(DISTINCT pi.item_id) = 0 + AND COUNT(DISTINCT pt.tax_id) > 0; +``` + +Resultado esperado: + +- `0` filas + ## No incluido en fase 1A - no cutover del backend diff --git a/docs/document-series/document-series-design.md b/docs/document-series/document-series-design.md index 6118ab3b..1deebe1f 100644 --- a/docs/document-series/document-series-design.md +++ b/docs/document-series/document-series-design.md @@ -59,6 +59,9 @@ En V1: - `proformas.proforma_reference` guarda la referencia visible de la proforma - `proforma_series_code` es el input funcional de create para elegir la serie de `document_type = proforma` - `proformas.target_invoice_series_code` es opcional y, cuando existe, apunta a una serie de `issued_invoice`, nunca a una serie de `proforma` +- create de proforma solo usa `document-series` para numerar la cabecera inicial; no calcula impuestos reales por si mismo +- una proforma puede crearse sin lineas y seguir siendo valida como borrador inicial +- si no hay lineas valoradas, `proforma_taxes` debe quedar vacia y los totales monetarios a `0` - si `proformas.target_invoice_series_code` es `NULL`, la emision resuelve la serie default activa de `issued_invoice` - en Fase 2E, `series` deja de ser el contrato nuevo para create/update de proformas - la UI consulta `GET /document-series` con filtro por `document_type` e idealmente `is_active = true` diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts index 1dc51da1..74215d5e 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts @@ -162,7 +162,9 @@ export class ProformaCreator implements IProformaCreator { return Result.fail(paymentMethodResult.error); } - // Items + // En create una proforma puede nacer sin líneas: en ese caso solo persistimos + // cabecera + defaults iniciales. Los impuestos y totales materializados salen + // de `items`, así que con `items = []` no se generan filas fiscales. const resolvedItems: IProformaItemCreateProps[] = []; for (const item of props.items) { diff --git a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-auto-applied-info.tsx b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-auto-applied-info.tsx index 3b4d790f..f5b20dfe 100644 --- a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-auto-applied-info.tsx +++ b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-auto-applied-info.tsx @@ -1,4 +1,10 @@ -import { AlertCircleIcon, BanknoteIcon, CircleDollarSignIcon, ReceiptTextIcon } from "lucide-react"; +import { + AlertCircleIcon, + BanknoteIcon, + CheckCircle2Icon, + CircleDollarSignIcon, + ReceiptTextIcon, +} from "lucide-react"; export const ProformaCreateAutoAppliedInfo = () => { return ( @@ -14,7 +20,14 @@ export const ProformaCreateAutoAppliedInfo = () => {

-
+
+

Datos que se guardarán al crear

+

+ Fecha, cliente y serie de proforma para numerar el documento inicial. +

+
+ +
@@ -43,10 +56,21 @@ export const ProformaCreateAutoAppliedInfo = () => {
-

Fiscalidad inicial

+

Configuración fiscal inicial

- Se tomará del cliente o de la configuración de empresa. Los impuestos no se - calcularán hasta que añadas líneas a la proforma. + Se tomará del cliente o de la configuración de empresa. Los impuestos y totales se + calcularán cuando añadas líneas a la proforma. +

+
+
+ +
+ +
+

Cálculo pendiente

+

+ Al crear sin líneas se guarda la cabecera. El desglose fiscal real se materializa + en la edición, cuando existan líneas valoradas.

diff --git a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-tax-editor.tsx b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-tax-editor.tsx index 75064661..a7068832 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-tax-editor.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-tax-editor.tsx @@ -42,11 +42,11 @@ export const ProformaUpdateTaxEditor = ({ className={className} description={t( "form_groups.proformas.taxes.description", - "Configuración fiscal de la proforma" + "Configura los valores fiscales por defecto de la proforma. Los impuestos aplicados y los totales se calculan desde las líneas valoradas." )} disabled={disabled} icon={} - title={t("form_groups.proformas.taxes.title", "Impuestos y retenciones")} + title={t("form_groups.proformas.taxes.title", "Configuración fiscal")} >