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")}
>