Skip to content

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)
);
CampoDescripción
id_comprobanteID del comprobante (factura, credito o debito)
tipo_comprobante'FAC', 'NC' o 'ND'
id_condvtaFK a condvta.cod
montoImporte base asignado por el usuario, antes del recargo
porcentaje_recargoPorcentaje copiado al momento de emisión (histórico)
monto_recargoImporte del recargo calculado
id_tarjetaSolo 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 filas
  • getAllByComprobante(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_tarjeta obligatorio

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 con suma(monto + monto_recargo) de condiciones no-CtaCte
  • insertMovimientoCtaCte() → registra solo el monto de 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: int y condicionVentaRecargo: int por condicionesVenta: 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 = 4400

Integraciones

Tesorería

  • Movimiento de caja: un único movimiento con la suma de monto + monto_recargo de condiciones no-CtaCte
  • Salida bancaria: un movimiento por cada condición Tarjeta con monto + monto_recargo de esa tarjeta
  • Si todas las condiciones son Cta.Cte, no se genera movimiento de caja

Cuenta Corriente

  • Se registra deuda solo por el monto de 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->total que 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.condvta por JOIN contra comprobante_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.php
    • reports/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_venta en todos los schemas suc* 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

  1. Migración Phinx: crear comprobante_condiciones_venta (BASE, idempotente, multi-schema)
  2. Implementar lógica de fallback: buscar primero en comprobante_condiciones_venta, si no existe registro leer campos legacy del comprobante

Fase 2 — Backend core

  1. ComprobanteCondicionVentaModel.php — nuevo modelo
  2. ComprobanteCondicionVentaService.php — nuevo service con lógica de fallback y validaciones
  3. Modificar ComprobanteService.php — nuevo flujo con lista de condiciones y recargos parciales
  4. Modificar FacturaService.phpmapComprobante() suma todos los ajustes RECARGO
  5. Modificar ComprobanteRequest.php y DTO base

Fase 3 — NC / ND

  1. Modificar NotaCreditoService.php — precarga de condiciones desde factura original
  2. Modificar NotaDebitoService.php — ídem

Fase 4 — Consumidores

  1. Mapear todos los consumidores del endpoint antes de tocar nada
  2. Actualizar frontend legacy y módulo Membresia
  3. Actualizar reportes en informes/

Fase 5 — Validación

  1. Tests unitarios por regla de negocio
  2. Tests de integración por caso de uso
  3. 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