.
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
|
- se pide cliente
|
||||||
- `proforma_series_code` es opcional; si falta, se usa la serie default activa de `document_type = proforma`
|
- `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`
|
- `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`
|
- la UI obtiene ambas listas desde `GET /document-series`, filtrando por `document_type`
|
||||||
|
|
||||||
### Editar draft
|
### Editar draft
|
||||||
@ -54,12 +58,16 @@ En proformas:
|
|||||||
|
|
||||||
- el backend emite `target_invoice_series_code` como nombre preferente
|
- el backend emite `target_invoice_series_code` como nombre preferente
|
||||||
- `series` puede mantenerse solo como alias legacy de salida mientras existan consumidores antiguos
|
- `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
|
## 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` 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`
|
- 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`
|
- 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
|
## 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
|
- `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`
|
- 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
|
## Rollback recomendado
|
||||||
|
|
||||||
- no aplicar nada en produccion en esta fase
|
- 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;
|
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 incluido en fase 1A
|
||||||
|
|
||||||
- no cutover del backend
|
- no cutover del backend
|
||||||
|
|||||||
@ -59,6 +59,9 @@ En V1:
|
|||||||
- `proformas.proforma_reference` guarda la referencia visible de la proforma
|
- `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`
|
- `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`
|
- `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`
|
- 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
|
- 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`
|
- 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);
|
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[] = [];
|
const resolvedItems: IProformaItemCreateProps[] = [];
|
||||||
|
|
||||||
for (const item of props.items) {
|
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 = () => {
|
export const ProformaCreateAutoAppliedInfo = () => {
|
||||||
return (
|
return (
|
||||||
@ -14,7 +20,14 @@ export const ProformaCreateAutoAppliedInfo = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div className="flex items-start gap-3">
|
||||||
<CircleDollarSignIcon className="mt-0.5 size-5 text-blue-600" />
|
<CircleDollarSignIcon className="mt-0.5 size-5 text-blue-600" />
|
||||||
<div>
|
<div>
|
||||||
@ -43,10 +56,21 @@ export const ProformaCreateAutoAppliedInfo = () => {
|
|||||||
<div className="flex items-start gap-3 pt-1">
|
<div className="flex items-start gap-3 pt-1">
|
||||||
<AlertCircleIcon className="mt-0.5 size-5 text-blue-600" />
|
<AlertCircleIcon className="mt-0.5 size-5 text-blue-600" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Fiscalidad inicial</p>
|
<p className="font-medium">Configuración fiscal inicial</p>
|
||||||
<p className="text-sm text-slate-600">
|
<p className="text-sm text-slate-600">
|
||||||
Se tomará del cliente o de la configuración de empresa. Los impuestos no se
|
Se tomará del cliente o de la configuración de empresa. Los impuestos y totales se
|
||||||
calcularán hasta que añadas líneas a la proforma.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -42,11 +42,11 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
className={className}
|
className={className}
|
||||||
description={t(
|
description={t(
|
||||||
"form_groups.proformas.taxes.description",
|
"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}
|
disabled={disabled}
|
||||||
icon={<LandmarkIcon className="size-5" />}
|
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>
|
<FormSectionGrid>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user