Appearance
Diseño Técnico — Múltiples Condiciones de Venta
Módulo: Ventas Tipo: Technical Design Estado: Aprobado — pendiente implementación Fecha: 2026-03-12
Descripción
Actualmente cada comprobante de venta (factura, nota de crédito, nota de débito) admite una única condición de venta. Este cambio permite asociar múltiples condiciones de venta a un mismo comprobante, cada una con un importe base asignado libremente por el usuario.
Valor para el negocio:
- Refleja operaciones comerciales reales donde un cliente paga parte en contado, parte con tarjeta y parte en cuenta corriente en una misma transacción
- Cada condición aplica su propio recargo solo sobre su parte asignada
- Mantiene trazabilidad completa de cada forma de pago a través de tesorería, cuenta corriente y reportes
Reglas de Negocio
- La suma de los importes base de las condiciones debe igualar el subtotal del comprobante antes de recargos
- El recargo de cada condición se calcula sobre su importe base, no sobre el total del comprobante
- El total del comprobante es subtotal + suma de todos los recargos parciales
- No se permite la misma condición de venta más de una vez, excepto Tarjeta con tarjetas distintas
- Todos los importes base deben ser positivos y mayores a cero
- Si alguna condición es Cta.Cte, el cliente debe tener cuenta corriente habilitada
- Si alguna condición es Tarjeta, se debe especificar la tarjeta concreta
- Todo comprobante debe tener al menos una condición de venta
- Los impuestos (IVA y tributos) se calculan sobre la base que incluye el recargo
- Los recargos parciales no modifican la distribución de alícuotas hacia AFIP/ARCA
Base de Datos
Nueva tabla: comprobante_condiciones_venta
Tabla única que cubre factura, nota de crédito y nota de débito. El par (id_comprobante, tipo_comprobante) identifica a qué comprobante pertenece cada fila. La integridad referencial hacia cada tipo de comprobante se garantiza en el código (PostgreSQL no soporta FK polimórfica).
sql
CREATE TABLE comprobante_condiciones_venta (
id SERIAL PRIMARY KEY,
id_comprobante INT NOT NULL,
tipo_comprobante VARCHAR(3) NOT NULL, -- 'FAC', 'NC', 'ND'
id_condvta INT NOT NULL REFERENCES condvta(cod),
monto NUMERIC NOT NULL,
porcentaje_recargo NUMERIC NULL DEFAULT 0,
monto_recargo NUMERIC NULL DEFAULT 0,
id_tarjeta INT NULL REFERENCES tarjetas(id)
);| Campo | Descripción |
|---|---|
id_comprobante | ID del comprobante (factura, credito o debito) |
tipo_comprobante | 'FAC', 'NC' o 'ND' |
id_condvta | FK a condvta.cod |
monto | Importe base asignado por el usuario, antes del recargo |
porcentaje_recargo | Porcentaje copiado al momento de emisión (histórico) |
monto_recargo | Importe del recargo calculado |
id_tarjeta | Solo cuando la condición es de tipo Tarjeta |
Diagrama de relaciones
mermaid
erDiagram
factura {
int id PK
}
credito {
int id PK
}
debito {
int id PK
}
comprobante_condiciones_venta {
serial id PK
int id_comprobante
varchar tipo_comprobante
int id_condvta FK
numeric monto
numeric porcentaje_recargo
numeric monto_recargo
int id_tarjeta FK
}
condvta {
int cod PK
string descri
boolean defecto
boolean tarjeta
numeric porcentaje_recargo
}
tarjetas {
int id PK
string nombre
int cuenta
}
movimi {
serial id PK
int id_comprobante FK
numeric valor
string es
string numero
}
factura ||--o{ comprobante_condiciones_venta : "tipo FAC"
credito ||--o{ comprobante_condiciones_venta : "tipo NC"
debito ||--o{ comprobante_condiciones_venta : "tipo ND"
comprobante_condiciones_venta }o--|| condvta : "usa condición"
comprobante_condiciones_venta }o--o| tarjetas : "tarjeta opcional"
factura ||--o{ movimi : "1 mov de caja (suma no-CtaCte)"Contrato de API
Cambio en ComprobanteRequest
Antes:
json
{
"condicionVenta": 1,
"condicionVentaRecargo": 10
}Después:
json
{
"condicionesVenta": [
{ "idCondvta": 1, "monto": 6000, "idTarjeta": null },
{ "idCondvta": 3, "monto": 4000, "idTarjeta": 5 }
]
}Cambio breaking coordinado: todos los consumidores internos se actualizan en el mismo ciclo de desarrollo.
El relevamiento y actualización de consumidores internos queda a cargo del equipo de desarrollo durante la implementación.
Backend — Cambios por Capa
Model — ComprobanteCondicionVenta.php (nuevo)
insert(array $condiciones, int $idComprobante, string $tipo)— inserta N filasgetAllByComprobante(int $idComprobante, string $tipo)— retorna condiciones del comprobante
Domain — Comprobante.php (sin cambios)
El domain ya soporta múltiples ajustes internamente. No requiere modificaciones.
Service — ComprobanteService.php (modificar)
Nuevas validaciones antes de insertar:
suma(monto) == subtotal— rechazar si hay desfasaje- Sin condiciones duplicadas (excepto Tarjeta con distintas tarjetas)
- Todos los
monto > 0 - Si condición Cta.Cte →
validateCtaCte()sobre el monto parcial - Si condición Tarjeta →
id_tarjetaobligatorio
Cálculo de recargos:
- Por cada condición:
monto_recargo = monto * porcentaje_recargo / 100 - Se pasan al domain como N ajustes de tipo RECARGO con valor FIJO, uno por condición
- El domain los acumula y calcula IVA correctamente sobre
neto + suma(recargos)por ítem
Cambios en movimientos:
insertMovimientoCaja()→ un único movimiento consuma(monto + monto_recargo)de condiciones no-CtaCteinsertMovimientoCtaCte()→ registra solo elmontode la condición Cta.Cte
Después de insertar el comprobante: ComprobanteCondicionVentaService::insert()
Service — FacturaService.php (modificar)
mapComprobante() línea 148: en lugar de guardar solo ajustes[0], sumar todos los ajustes de tipo RECARGO y persistir el total en el campo recargo.
Service — NotaCreditoService.php / NotaDebitoService.php (modificar)
- Al recibir comprobante asociado, leer sus condiciones desde
comprobante_condiciones_venta - Precargar esas condiciones con sus importes como default
- El usuario puede modificarlos antes de confirmar
Controller
Sin cambios de lógica. Adaptar mapeo del request para leer condicionesVenta: array.
Resource (DTO) — ComprobanteRequest.php (modificar)
- Reemplazar
condicionVenta: intycondicionVentaRecargo: intporcondicionesVenta: array
Flujo de Cálculo
Entrada: condicionesVenta = [
{ idCondvta: 1, monto: 6000 }, ← Contado, sin recargo
{ idCondvta: 3, monto: 4000, idTarjeta: 5 } ← Tarjeta Visa, 10% recargo
]
1. Validar: 6000 + 4000 == subtotal (10000) ✓
2. Calcular recargos parciales en el Service:
Contado → monto_recargo = 6000 * 0% = 0
Tarjeta → monto_recargo = 4000 * 10% = 400
3. Pasar al domain como 2 ajustes RECARGO FIJO: [0, 400]
El domain acumula y calcula IVA por ítem sobre neto + 400
4. Total comprobante = 10000 + 400 = 10400
5. IVA calculado correctamente por ítem sobre su proporción de neto + recargo
6. Movimiento de caja = (6000 + 0) + (4000 + 400) = 10400
7. Salida bancaria Visa = 4000 + 400 = 4400Integraciones
Tesorería
- Movimiento de caja: un único movimiento con la suma de
monto + monto_recargode condiciones no-CtaCte - Salida bancaria: un movimiento por cada condición Tarjeta con
monto + monto_recargode esa tarjeta - Si todas las condiciones son Cta.Cte, no se genera movimiento de caja
Cuenta Corriente
- Se registra deuda solo por el
montode la condición Cta.Cte, no por el total del comprobante - La validación de límite de crédito aplica solo sobre el monto parcial de Cta.Cte
AFIP / ARCA
- Sin cambios en el mapeo.
formatToArcaRequest()usa$comp->totalque ya incluye la suma de recargos parciales - Los recargos parciales no modifican la distribución de alícuotas de IVA y tributos
Reportes (informes/)
- Reemplazar lectura directa de
factura.condvtapor JOIN contracomprobante_condiciones_venta - Un comprobante aparece una vez por cada condición asignada, con el importe parcial de esa condición
- Archivos identificados a modificar:
reports/mod-ventas/comprobantes/comprobantes-datos.phpreports/mod-ventas/historico-ventas/historico-ventas-datos.php- Relevar otros reportes de ventas con dependencia en
condvta
Migración de Datos
- Migración estructural (Phinx BASE, idempotente): crea la tabla
comprobante_condiciones_ventaen todos los schemassuc*activos. No migra datos históricos. - Comprobantes nuevos: desde el día de salida a producción se guardan directamente en
comprobante_condiciones_venta. - Comprobantes históricos: se leen con fallback. Si el comprobante no tiene filas en
comprobante_condiciones_venta, se lee la condición de venta desde los campos legacy del comprobante (condvta,id_tarjeta,condvta_recargo,recargo). El usuario no percibe diferencia. - Este patrón evita migrar miles de registros históricos en producción y elimina el riesgo operativo asociado.
Plan de Implementación
Fase 1 — Base de datos
- Migración Phinx: crear
comprobante_condiciones_venta(BASE, idempotente, multi-schema) - Implementar lógica de fallback: buscar primero en
comprobante_condiciones_venta, si no existe registro leer campos legacy del comprobante
Fase 2 — Backend core
ComprobanteCondicionVentaModel.php— nuevo modeloComprobanteCondicionVentaService.php— nuevo service con lógica de fallback y validaciones- Modificar
ComprobanteService.php— nuevo flujo con lista de condiciones y recargos parciales - Modificar
FacturaService.php—mapComprobante()suma todos los ajustes RECARGO - Modificar
ComprobanteRequest.phpy DTO base
Fase 3 — NC / ND
- Modificar
NotaCreditoService.php— precarga de condiciones desde factura original - Modificar
NotaDebitoService.php— ídem
Fase 4 — Consumidores
- Mapear todos los consumidores del endpoint antes de tocar nada
- Actualizar frontend legacy y módulo Membresia
- Actualizar reportes en
informes/
Fase 5 — Validación
- Tests unitarios por regla de negocio
- Tests de integración por caso de uso
- Validación de criterios de aceptación
Criterios de Aceptación
- [ ] El usuario puede asignar múltiples condiciones de venta a un comprobante, cada una con su importe base
- [ ] El sistema rechaza si la suma de importes base no iguala el subtotal
- [ ] Cada condición aplica su recargo solo sobre su importe base
- [ ] El total del comprobante incluye todos los recargos parciales sumados
- [ ] No se permite la misma condición más de una vez, excepto Tarjeta con tarjetas distintas
- [ ] Todos los importes base son positivos y mayores a cero
- [ ] Si hay condición Cta.Cte, se valida que el cliente tenga cuenta corriente habilitada
- [ ] Si hay condición Tarjeta, se exige especificar la tarjeta
- [ ] Se genera un único movimiento de caja por la suma de condiciones no-CtaCte
- [ ] Si todas las condiciones son Cta.Cte, no se genera movimiento de caja
- [ ] Se genera un movimiento de salida bancaria por cada tarjeta con su importe
- [ ] La deuda en cuenta corriente se registra solo por el importe de la condición Cta.Cte
- [ ] Al emitir una NC/ND, el sistema precarga las condiciones de la factura original
- [ ] Los movimientos de tesorería y cuenta corriente de la NC/ND son inversos a los de la factura
- [ ] Los reportes leen la condición de venta desde
comprobante_condiciones_venta - [ ] Los comprobantes existentes se migran con una única condición por su importe completo
- [ ] La migración es idempotente y se ejecuta sobre todos los schemas activos
- [ ] Asignar una sola condición produce comportamiento idéntico al actual
- [ ] El comportamiento aplica igual para comprobantes electrónicos y manuales