.
This commit is contained in:
parent
5ed6556036
commit
659df116a2
134
docs/customer-invoices/proforma-create-contract.md
Normal file
134
docs/customer-invoices/proforma-create-contract.md
Normal file
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 border-b border-blue-200/80 py-4 md:grid-cols-3">
|
||||
<div className="rounded-lg border border-blue-200/80 bg-white/60 p-4">
|
||||
<p className="text-sm font-medium text-slate-900">Datos que se guardarán al crear</p>
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Fecha, cliente y serie de proforma para numerar el documento inicial.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 border-y border-blue-200/80 py-4 md:grid-cols-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<CircleDollarSignIcon className="mt-0.5 size-5 text-blue-600" />
|
||||
<div>
|
||||
@ -43,10 +56,21 @@ export const ProformaCreateAutoAppliedInfo = () => {
|
||||
<div className="flex items-start gap-3 pt-1">
|
||||
<AlertCircleIcon className="mt-0.5 size-5 text-blue-600" />
|
||||
<div>
|
||||
<p className="font-medium">Fiscalidad inicial</p>
|
||||
<p className="font-medium">Configuración fiscal inicial</p>
|
||||
<p className="text-sm text-slate-600">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-lg bg-white/60 p-4">
|
||||
<CheckCircle2Icon className="mt-0.5 size-5 text-blue-600" />
|
||||
<div>
|
||||
<p className="font-medium">Cálculo pendiente</p>
|
||||
<p className="text-sm text-slate-600">
|
||||
Al crear sin líneas se guarda la cabecera. El desglose fiscal real se materializa
|
||||
en la edición, cuando existan líneas valoradas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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={<LandmarkIcon className="size-5" />}
|
||||
title={t("form_groups.proformas.taxes.title", "Impuestos y retenciones")}
|
||||
title={t("form_groups.proformas.taxes.title", "Configuración fiscal")}
|
||||
>
|
||||
<FormSectionGrid>
|
||||
<SelectField
|
||||
|
||||
Loading…
Reference in New Issue
Block a user