Appearance
Comprobantes Pendientes - Documentación Técnica Backend
Módulo: ventas Feature: Comprobantes Pendientes (Pedidos, Remitos, Presupuestos) Fecha: 2026-02-11
⚠️ DOCUMENTACIÓN RETROSPECTIVA - Generada a partir de código implementado el 2026-02-11
Referencia a Documentación de Negocio
Arquitectura Implementada
Patrón Factory + Strategy
El sistema utiliza un patrón combinado para soportar múltiples tipos de comprobantes pendientes (Pedido, Remito, Presupuesto) de forma extensible:
- Factory (
PendienteServiceFactory): Crea instancias de servicios específicos basándose en el tipo de comprobante - Strategy (Clases abstractas):
PendienteServicedefine la estructura, subclases implementan comportamiento específico - Enum (
TipoComprobantePendiente): Define los tipos de comprobantes disponibles
Jerarquía de clases:
PendienteService (abstract)
├── PedidoService (implementado)
├── RemitoService (pendiente)
└── PresupuestoService (pendiente)
ItemPendienteService (abstract)
├── ItemPedidoService (implementado)
├── ItemRemitoService (pendiente)
└── ItemPresupuestoService (pendiente)Actualmente implementado: Solo PedidoService y ItemPedidoService.
Ubicación de Archivos
Controllers:
controller/modulo-venta/Pendiente/PedidoController.phpcontroller/modulo-venta/TipoComprobantePendienteController.php
Services:
service/Venta/Pendiente/PendienteService.php(abstract)service/Venta/Pendiente/PedidoService.phpservice/Venta/Pendiente/ItemPendienteService.php(abstract)service/Venta/Pendiente/ItemPedidoService.phpservice/Venta/TipoComprobantePendienteService.php
Models:
models/modulo-venta/Pendiente/Pendiente.phpmodels/modulo-venta/TipoComprobantePendiente.php
Resources (DTOs):
Resources/Venta/Pendiente/Pendiente.phpResources/Venta/Pendiente/PendienteRequest.phpResources/Venta/Pendiente/Pedido.phpResources/Venta/Pendiente/ItemPendiente.phpResources/Venta/Pendiente/ItemPendienteRequest.phpResources/Venta/TipoComprobantePendiente.phpResources/Enums/TipoComprobantePendiente.php
Routes:
Routes/Venta/VentaRoutes.phpRoutes/Venta/Pendiente/PendienteRoutes.phpRoutes/Venta/Pendiente/PedidoRoute.phpRoutes/Venta/TipoComprobantePendienteRoute.php
Factory:
Factories/Venta/PendienteServiceFactory.php
API Endpoints
Gestión de Tipos de Comprobantes Pendientes
Base Path: /api/mod-ventas/tipo-comprobante-pendiente
GET /api/mod-ventas/tipo-comprobante-pendiente
Responsabilidades:
- Obtener listado de todos los tipos de comprobantes pendientes configurados
- Incluye: id, nombre, reporte, numero_comprobante, maneja_stock
Request: Sin parámetros
Response DTO:
{
"status": 200,
"message": "...",
"data": [
{
"id": 1,
"nombre": "Pedido",
"reporte": "PEDIDO",
"numero_comprobante": 1523,
"maneja_stock": true
}
]
}Status Codes:
- 200: OK
- 500: Error interno
PUT /api/mod-ventas/tipo-comprobante-pendiente/{id}
Responsabilidades:
- Actualizar configuración de un tipo de comprobante pendiente
- Permite modificar: nombre, reporte, numero_comprobante, maneja_stock
- Validación dinámica de campos válidos
Request DTO:
json
{
"nombre": "Pedido Actualizado",
"maneja_stock": true
}Response: 204 No Content
Status Codes:
- 204: Actualizado exitosamente
- 400: Datos inválidos
- 404: Tipo no encontrado
- 500: Error interno
Gestión de Pedidos (Comprobantes Pendientes)
Base Path: /api/mod-ventas/pendiente/pedido
GET /api/mod-ventas/pendiente/pedido
Responsabilidades:
- Obtener listado de pedidos pendientes
- Filtrado por cliente (opcional)
- Filtrado por comprobante (opcional)
- Soporte de scopes:
min(default) omax - Solo retorna pedidos no eliminados y no facturados
Query Parameters:
cliente(int, opcional): Código de clientecomprobante(string, opcional): Número de comprobantescope(string, opcional):min|max
Scope min: id, codigo_cliente, numero_comprobante, fecha, total
Scope max: Todos los campos del pendiente
Response DTO:
{
"status": 200,
"data": [
{
"id": 1,
"codigo_cliente": 10,
"numero_comprobante": 1500,
"fecha": "2026-02-10",
"total": 15000.50
}
]
}Status Codes:
- 200: OK
- 500: Error interno
GET /api/mod-ventas/pendiente/pedido/{id}
Responsabilidades:
- Obtener detalle completo de un pedido
- Incluye datos del cliente (min)
- Si
scope=max: Incluye items del pedido con detalle completo
Query Parameters:
scope(string, opcional):min|max
Response DTO (scope=max):
{
"status": 200,
"data": {
"id": 1,
"codigo_cliente": 10,
"fecha": "2026-02-10",
"neto": 12000.00,
"subtotal": 12000.00,
"total": 11000.00,
"codigo_vendedor": 5,
"lista": "1",
"fecha_entrega": "2026-02-15",
"comentario": "Urgente",
"provincia": 1,
"sucursal": 1,
"numero_comprobante": 1500,
"id_preformul": 1,
"stock": true,
"cliente": {
"id": 10,
"nombre": "Juan Pérez",
...
},
"items": [
{
"id": 1,
"codigo": 100,
"cantidad": 10,
"precio_unitario": 95.00,
"importe": 950.00,
"neto_unitario": 100.00,
"bonificacion": 5.0,
"monto_bonificacion": 50.00,
"descripcion": "Producto A",
"costo": 70.00,
"manejaStock": 1
}
]
}
}Status Codes:
- 200: OK
- 404: Pedido no encontrado
- 500: Error interno
POST /api/mod-ventas/pendiente/pedido
Responsabilidades:
- Crear un nuevo pedido pendiente
- Validar estructura de datos (PendienteRequest)
- Asignar número de comprobante automáticamente
- Insertar pedido y sus items en transacción
- Generar movimientos de stock si aplica
- Incrementar numerador del tipo de comprobante
Request DTO (PendienteRequest):
json
{
"cliente": 10,
"vendedor": 5,
"sucursal": 1,
"lista": 1,
"fecha": "2026-02-10",
"fechaEntrega": "2026-02-15",
"provincia": 1,
"comentario": "Urgente",
"items": [
{
"id": 100,
"nombre": "Producto A",
"cantidad": 10,
"precio": 100.00,
"bonificacion": 5.0
}
]
}Validaciones estructurales:
cliente: NotNullvendedor: NotNullsucursal: NotNulllista: NotNullfecha: NotNull, formato CarbonfechaEntrega: NotNull, formato Carbonprovincia: NotNullcomentario: Length(max: 100)items: NotNull, array de ItemPendienteRequest
Response DTO:
json
{
"status": 200,
"data": {
"id": 123
}
}Status Codes:
- 200: Creado exitosamente (retorna ID)
- 400: Datos inválidos (validación estructural)
- 422: Error de negocio (producto no existe, etc.)
- 500: Error interno
PUT /api/mod-ventas/pendiente/pedido/{id}
Responsabilidades:
- Actualizar un pedido existente
- Reemplaza todos los datos del pedido
- Elimina y reinserta todos los items
- Elimina y regenera movimientos de stock si aplica
- Ejecuta en transacción
Request DTO: Igual que POST (PendienteRequest)
Response: 204 No Content
Status Codes:
- 204: Actualizado exitosamente
- 400: Datos inválidos
- 404: Pedido no encontrado
- 422: Error de negocio
- 500: Error interno
DELETE /api/mod-ventas/pendiente/pedido/{id}
Responsabilidades:
- Eliminar lógicamente un pedido (soft delete)
- Marca el campo
deleted_atcon timestamp actual - Elimina movimientos de stock asociados si aplica
- Ejecuta en transacción
Request: Sin body
Response: 204 No Content
Status Codes:
- 204: Eliminado exitosamente
- 404: Pedido no encontrado
- 500: Error interno
Endpoints de Consulta General
Base Path: /api/mod-ventas/pendiente
GET /api/mod-ventas/pendiente
Responsabilidades:
- Obtener listado de pendientes (todos los tipos)
- Usado para consultas generales sin filtrar por tipo específico
Query Parameters: Igual que GET de pedidos
Response: Lista de pendientes (sin filtro por tipo)
GET /api/mod-ventas/pendiente/{id}
Responsabilidades:
- Obtener un pendiente por ID sin especificar el tipo
Response: Datos del pendiente
Capa de Servicio
PendienteService (Abstract)
Ubicación: service/Venta/Pendiente/PendienteService.php
Responsabilidades:
- Define el contrato para todos los tipos de comprobantes pendientes
- Implementa lógica común de CRUD con transacciones
- Gestiona la creación/actualización/eliminación de items
- Coordina con MovimientoStockService para stock
- Coordina con TipoComprobantePendienteService para numeración
Métodos abstractos (cada subclase debe implementar):
php
abstract public function getAll(array $options): array;
abstract public function getOne(array $options): PendienteDTO;
abstract protected function getIdTipoComprobante(): int;
abstract protected function mapPendiente(
PendienteRequest $comprobantePendiente,
object $tipoComprobantePendiente
): PendienteDTO;
abstract protected function insertItems(
PendienteDTO $pendiente,
PendienteRequest $comprobantePendiente
): array;
abstract protected function actualizarStock(
PendienteDTO $pendiente,
object $tipoComprobantePendiente,
array $items
): void;Métodos públicos implementados:
insert(PendienteRequest $comprobantePendiente): int- Crea un nuevo comprobante pendiente
- Flujo:
- Obtiene tipo de comprobante pendiente
- Mapea datos del request a DTO
- Inicia transacción
- Inserta comprobante
- Inserta items
- Actualiza numerador del tipo
- Genera movimientos de stock si aplica
- Commit transacción
- Retorna: ID del pendiente creado
- Excepciones:
InsertErroren caso de error
update(string $id, PendienteRequest $comprobantePendiente): void- Actualiza un comprobante pendiente
- Flujo:
- Obtiene tipo de comprobante pendiente
- Mapea datos del request
- Inicia transacción
- Actualiza comprobante
- Elimina y reinserta items
- Elimina y regenera movimientos de stock
- Commit transacción
- Excepciones:
InsertErroren caso de error
delete(int $id): void- Elimina lógicamente un comprobante
- Flujo:
- Inicia transacción
- Marca el pendiente como eliminado
- Elimina movimientos de stock asociados
- Commit transacción
- Excepciones:
InsertErroren caso de error
patch(int $id, array $datosEditar): void- Actualización parcial de campos específicos
- Delega al modelo
Métodos privados auxiliares:
getTipoComprobantePendiente(int $idTipoComprobante)- Obtiene configuración del tipo de comprobante
- Usa TipoComprobantePendienteService
actualizarComprobantePendiente(int $idTipoComprobante, int $numeroComprobante): void- Incrementa el numerador del tipo de comprobante
- Ejecuta:
numero_comprobante = numeroComprobante + 1
tieneModuloStock(): bool- Verifica si la empresa tiene el módulo de Stock habilitado
- Consulta la configuración
PermisosEmpresaen tablaconfig - Retorna:
truesimodulos[STOCK] === true
Dependencias:
ConnectionManager: Gestión de conexiones multi-tenantPendiente(Model): Acceso a datosItemPendienteService: Gestión de itemsMovimientoStockService: Gestión de stockTipoComprobantePendienteService: Gestión de tiposConfig(Model): Consulta de configuración
Trait utilizado:
Conectable: Acceso a ConnectionManager
PedidoService
Ubicación: service/Venta/Pendiente/PedidoService.php
Responsabilidades:
- Implementación específica para comprobantes de tipo Pedido
- Define el ID del tipo:
TipoComprobantePendiente::PEDIDO(1) - Implementa mapeo específico de datos
- Gestiona creación de items via ItemPedidoService
- Genera movimientos de stock como EGRESO
Métodos implementados:
getAll(array $options): array- Filtra por
id_preformul = 1(Pedido) - Retorna lista de PedidoDTO
- Filtra por
getOne(array $options): PedidoDTO- Filtra por
id_preformul = 1 - Obtiene datos del cliente (scope min)
- Si
scope=max: Obtiene items con ItemPedidoService - Retorna PedidoDTO con cliente e items
- Filtra por
getIdTipoComprobante(): int- Retorna:
1(TipoComprobantePendiente::PEDIDO)
- Retorna:
mapPendiente(PendienteRequest $comprobantePendiente, object $tipoComprobantePendiente): PendienteDTO- Calcula neto: suma de
precio * cantidadde todos los items - Calcula bonificación: suma de
(bonificacion / 100) * precio * cantidad - Calcula total:
neto - bonificacion - Mapea todos los campos del request al DTO
- Asigna
numero_comprobantedel tipo
- Calcula neto: suma de
insertItems(PendienteDTO $pendiente, PendienteRequest $comprobantePendiente): array- Usa ItemPedidoService para mapear y insertar items
- Retorna: array de ItemPendienteDTO insertados
actualizarStock(PendienteDTO $pendiente, object $tipoComprobantePendiente, array $items): void- Itera sobre items
- Para cada item que
manejaStock === 1:- Crea MovimientoStock con:
posicion: TipoMovimiento::EGRESOconcepto: nombre del tipo de comprobantenrocomp: número de comprobanteid_prefa: ID del pendientemarca: MarcaOrigen::OFICIAL
- Crea MovimientoStock con:
- Inserta movimientos via MovimientoStockService
Dependencias adicionales:
Cliente(Model): Obtiene datos del clienteItemPedidoService: Gestión de items de pedido
ItemPedidoService
Ubicación: service/Venta/Pendiente/ItemPedidoService.php
Responsabilidades:
- Mapeo de items del request a DTOs
- Consulta de productos para obtener costo y manejo de stock
- Cálculo de importes y bonificaciones por item
Métodos:
mapItems(PendienteDTO $pendiente, PendienteRequest $comprobantePendiente): array- Itera sobre items del request
- Para cada item:
- Consulta producto desde
Productomodel - Calcula bonificación unitaria:
(bonificacion / 100) * precio - Calcula precio unitario:
precio - bonificacion_unitaria - Calcula monto bonificación:
bonificacion_unitaria * cantidad - Calcula importe:
(precio * cantidad) - monto_bonificacion - Crea ItemPendienteDTO con:
codigo: ID del productocantidadprecio_unitario: con bonificación aplicadaimporte: total del itemneto_unitario: precio sin bonificacióncosto: del productodescripcion: nombre del itemid_prefa: ID del pendientemanejaStock: del productobonificacion: porcentajemonto_bonificacion: importe descontado
- Consulta producto desde
- Cache de productos consultados para optimizar
- Retorna: array de ItemPendienteDTO
Dependencias:
Producto(Model): Consulta de productosConnectionManager: Acceso a base de datos
TipoComprobantePendienteService
Ubicación: service/Venta/TipoComprobantePendienteService.php
Responsabilidades:
- Gestión de tipos de comprobantes pendientes (configuración)
- CRUD de tipos
Métodos:
getAll(): array- Retorna todos los tipos configurados
- Retorna: TipoComprobantePendienteDTO[]
getById(int $id): TipoComprobantePendienteDTO- Obtiene un tipo por ID
- Excepciones:
ServerExceptionsi no existe
update(int $id, array $data): void- Actualiza campos específicos de un tipo
- Valida campos permitidos: id, nombre, reporte, numero_comprobante, maneja_stock
- Casteo especial:
maneja_stockse castea a int para almacenamiento boolean en BD
Dependencias:
TipoComprobantePendiente(Model): Acceso a datos
Lógica de Negocio
Asignación Automática de Número de Comprobante
Flujo:
- Al crear un pendiente, se consulta el tipo de comprobante
- Se obtiene
numero_comprobanteactual del tipo - Se asigna ese número al pendiente
- Se incrementa
numero_comprobantedel tipo en 1
Problema potencial identificado: ⚠️ No se detectó el uso de transacciones exclusivas o locks para el incremento del numerador. Esto podría causar race conditions en entornos concurrentes.
Generación de Movimientos de Stock
Condiciones:
- La empresa debe tener el módulo de Stock habilitado (
PermisosEmpresaen config) - El tipo de comprobante debe tener
maneja_stock = true - El producto del item debe tener
maneja_stock = 1
Flujo:
- Al crear/actualizar: Se generan movimientos de EGRESO
- Al actualizar: Se eliminan movimientos anteriores y se regeneran
- Al eliminar: Se eliminan todos los movimientos asociados
Tipo de movimiento: TipoMovimiento::EGRESO (salida de stock)
Marca: MarcaOrigen::OFICIAL (siempre oficial para pendientes)
Referencia: Los movimientos se vinculan al pendiente mediante id_prefa
Cálculo de Totales
Fórmulas implementadas:
Para el pedido completo:
- neto = suma( precio * cantidad ) de todos los items
- bonificacion_total = suma( (bonificacion / 100) * precio * cantidad )
- total = neto - bonificacion_total
- subtotal = neto
Para cada item:
- bonificacion_unitaria = (porcentaje_bonificacion / 100) * precio
- precio_unitario = precio - bonificacion_unitaria
- monto_bonificacion = bonificacion_unitaria * cantidad
- importe = (precio * cantidad) - monto_bonificacionEliminación Lógica (Soft Delete)
Implementación:
- Campo
deleted_atse marca con timestamp actual - Los items NO se eliminan (se mantienen)
- Los movimientos de stock SÍ se eliminan físicamente
Filtros automáticos:
- Todas las consultas incluyen:
WHERE deleted_at IS NULL - Todas las consultas incluyen:
AND id_factura IS NULL
Esto asegura que solo se muestren pendientes activos y no facturados.
Cancelación de Pendientes
Mecanismo:
- Campo
id_facturase asigna cuando se genera una factura a partir del pendiente - El filtro
id_factura IS NULLexcluye pendientes ya facturados de las consultas
⚠️ NOTA: El flujo de cancelación por facturación NO está implementado en el código de Pendientes analizado. Probablemente se gestiona desde el módulo de Facturación.
Esquema de Base de Datos
Tabla: prefa
Nivel: SUCURSAL o EMPRESA (configurable por empresa mediante configuracion_niveles_tablas)
Descripción: Almacena los comprobantes pendientes (pedidos, remitos, presupuestos)
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
id | SERIAL | PRIMARY KEY | ID único auto-incremental |
zf | VARCHAR(6) | NOT NULL | Código de cliente |
fecha | DATE | NOT NULL | Fecha del comprobante |
neto | DECIMAL(16,5) | NULL | Total neto (suma precio*cantidad sin bonificación) |
subtotal | DECIMAL(16,5) | NULL | Subtotal del comprobante (igual a neto) |
total | DECIMAL(16,5) | NULL | Total del comprobante (neto - bonificación) |
vend | DECIMAL(3) | NULL | Código de vendedor |
lista | VARCHAR(3) | NULL | Código de lista de precios |
vencimiento | DATE | NULL | Fecha de vencimiento del comprobante |
fecha_entrega | DATE | NULL | Fecha de entrega programada |
iva | DECIMAL(16,5) | NULL | Importe total de IVA |
financ | DECIMAL(16,5) | NULL | ⚠️ Sin uso actualmente |
bonif | DECIMAL(16,5) | NULL | ⚠️ Sin uso actualmente |
comen | VARCHAR(100) | NULL | Comentario del comprobante |
provincia | DECIMAL(2) | NULL | Código de provincia del cliente |
suc | SMALLINT | NOT NULL | Código de sucursal emisora |
nrocomp | BIGINT | NOT NULL | Número de comprobante |
imin | DECIMAL(16,5) | NULL | ⚠️ Sin uso actualmente |
id_preformul | INTEGER | NOT NULL, FK | ID del tipo de comprobante (FK a preformul) |
deleted_at | TIMESTAMP | NULL | Timestamp de eliminación lógica |
id_factura | INTEGER | NULL | ID de la factura que canceló el pendiente |
modo | SMALLINT | NULL | Modo de facturación: 0=Prueba, 1=Oficial, 2=Consolidado |
stock | BOOLEAN | NOT NULL | Flag que indica si registró movimiento de stock |
Índices: PRIMARY KEY (id)
Foreign Keys:
id_preformul→preformul(id)ON DELETE RESTRICT ON UPDATE CASCADE
Constraints:
- Soft delete:
deleted_at IS NULLpara registros activos - Cancelación:
id_factura IS NULLpara pendientes no facturados
Niveles configurables: Sigue configuración de preformul (default: EMPRESA y SUCURSAL)
Migración: migrations/tenancy/20250211134902_new_table_prefa.php
Condición de ejecución: Solo se crea si la empresa tiene empres.pedido = true OR empres.remito = true Y el módulo Ventas está habilitado.
Tabla: preformul
Nivel: EMPRESA o SUCURSAL (configurable)
Descripción: Configuración de tipos de comprobantes pendientes
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
id | INTEGER | PRIMARY KEY | ID único del tipo |
nombre | VARCHAR | NOT NULL | Nombre del tipo (ej: "Pedido") |
reporte | VARCHAR | NOT NULL | Código del reporte asociado |
nrofac | INTEGER | NOT NULL | Numerador de comprobantes (auto-incremental) |
maneja_stock | BOOLEAN | NOT NULL | Indica si genera movimientos de stock |
Datos iniciales:
- ID 1: "Pedido"
- Otros tipos pendientes de implementación
Tabla: items_prefa
Nivel: Sigue configuración de prefa (SUCURSAL o EMPRESA)
Descripción: Items (líneas de detalle) de los comprobantes pendientes
⚠️ Nota: Esta tabla NO fue encontrada en las migraciones analizadas. Se infiere su existencia por el código del modelo y servicio ItemPendiente.
Campos inferidos:
id: PRIMARY KEYcodigo: Código de productocantidad: Cantidad del itemprecio_unitario: Precio unitario con bonificación aplicadaimporte: Importe total del itemneto_unitario: Precio sin bonificaciónbonificacion: Porcentaje de bonificaciónmonto_bonificacion: Importe de bonificacióndescripcion: Descripción del productocosto: Costo del productoid_prefa: FK aprefa(id)ON DELETE CASCADEmanejaStock: Flag de manejo de stock (del producto)- Otros campos relacionados con IVA e impuestos internos
Relaciones
preformul (1) ----< (N) prefa
prefa (1) ----< (N) items_prefa
prefa (N) >---- (1) cliente
prefa (N) >---- (1) vendedor
prefa (N) >---- (0..1) factura (cancelación)
items_prefa (N) >---- (1) producto
prefa (1) ----< (N) movimiento_stock (via id_prefa)Capa de Datos
Modelo: Pendiente
Ubicación: models/modulo-venta/Pendiente/Pendiente.php
Responsabilidades:
- Acceso a datos de la tabla
prefa - Mapping de columnas de BD a DTOs
- Consultas CRUD con Doctrine DBAL
- Soporte de scopes para optimización de consultas
Scopes definidos:
min: id, codigo_cliente, numero_comprobante, fecha, totalmax: Todos los campos
Métodos públicos:
hasByCliente(int $cliente): bool- Verifica si un cliente tiene pendientes activos
- Filtra:
deleted_at IS NULL AND id_factura IS NULL
getAll(array $options): PendienteDTO[]- Obtiene listado de pendientes
- Filtros opcionales:
cliente,id_preformul - Scope:
minomax - Siempre filtra:
deleted_at IS NULL AND id_factura IS NULL
getOne(array $options): PendienteDTO- Obtiene un pendiente por ID o número de comprobante
- Filtros:
comprobante,id,id_preformul - Scope:
minomax - Excepciones:
ServerExceptionsi no existe
insert(PendienteDTO $pendiente): int- Inserta un nuevo pendiente
- Retorna: ID del pendiente insertado
- Usa
lastInsertId()para obtener ID
update(PendienteDTO $pendiente): void- Actualiza todos los campos de un pendiente
- Requiere
pendiente.idpara identificar el registro
delete(int $id): void- Eliminación lógica (soft delete)
- Marca
deleted_atcon timestamp actual
patch(int $id, array $datosEditar, int $id_preformul = null): void- Actualización parcial de campos específicos
- Valida campos contra
$fieldsdefinidos - Filtro opcional adicional por
id_preformul
Mapping de campos:
php
private $fields = [
'id' => "id",
'codigo_cliente' => "zf::int as codigo_cliente",
'fecha' => "fecha",
'neto' => 'neto',
'subtotal' => 'subtotal',
'total' => "total",
'codigo_vendedor' => "vend::int as codigo_vendedor",
'lista' => "lista::int",
'vencimiento' => "vencimiento",
'fecha_entrega' => 'fecha_entrega',
'iva' => "iva",
'comentario' => "comen as comentario",
'provincia' => "provincia::int",
'sucursal' => "suc::int as sucursal",
'numero_comprobante' => "nrocomp::bigint as numero_comprobante",
'id_preformul' => 'id_preformul',
'id_factura' => "id_factura",
'modo' => 'modo',
'stock' => 'stock::boolean'
];Dependencias:
Doctrine\DBAL\Connection: Acceso a base de datos
Modelo: TipoComprobantePendiente
Ubicación: models/modulo-venta/TipoComprobantePendiente.php
Responsabilidades:
- Acceso a datos de la tabla
preformul - CRUD de tipos de comprobantes pendientes
Métodos públicos:
getAll(): TipoComprobantePendienteDTO[]- Obtiene todos los tipos configurados
getById(int $id): TipoComprobantePendienteDTO- Obtiene un tipo por ID
- Excepciones:
ServerExceptionsi no existe
update(int $id, array $data): void- Actualiza campos de un tipo
- Validación dinámica de campos permitidos
- Casteo especial para
maneja_stock(boolean → int)
Mapping de campos:
php
private $fields = [
'id' => 'id',
'nombre' => 'nombre',
'reporte' => 'reporte',
'numero_comprobante' => 'nrofac',
'maneja_stock' => 'maneja_stock',
];DTOs (Data Transfer Objects)
Pendiente
Ubicación: Resources/Venta/Pendiente/Pendiente.php
Propiedades:
php
public ?int $id = null;
public int $codigo_cliente;
public string $fecha;
public ?float $neto = null;
public ?float $subtotal = null;
public ?float $total = null;
public ?int $codigo_vendedor = null;
public ?string $lista = null;
public ?string $vencimiento = null;
public ?string $fecha_entrega = null;
public ?float $iva = null;
public ?string $comentario = null;
public ?int $provincia = null;
public int $sucursal;
public int $numero_comprobante;
public int $id_preformul;
public ?int $id_factura = null;
public ?int $modo = null; // 0: Prueba | 1: Oficial | 2: Consolidado
public bool $stock;Casters:
- Casteo automático de neto, subtotal, total, iva a float
- Casteo de codigo_cliente a int
- Casteo de lista a string
- Casteo de id_preformul a int
PendienteRequest
Ubicación: Resources/Venta/Pendiente/PendienteRequest.php
Propiedades:
php
#[Assert\NotNull]
public int $cliente;
#[Assert\NotNull]
public int $vendedor;
#[Assert\NotNull]
public int $sucursal;
#[Assert\NotNull]
public int $lista;
#[Assert\Length(max: 100)]
public ?string $comentario = null;
#[Assert\NotNull]
public Carbon $fecha;
#[Assert\NotNull]
public Carbon $fechaEntrega;
#[Assert\NotNull]
public int $provincia;
#[Assert\NotNull]
public ?array $items = null; // ItemPendienteRequest[]Casters:
fechayfechaEntrega: Carbon::parse()lista: Cast a intitems: Array de ItemPendienteRequest
Validaciones: Usa Symfony Validator con atributos PHP 8.
Pedido
Ubicación: Resources/Venta/Pendiente/Pedido.php
Propiedades: Extiende Pendiente y agrega:
php
public ?object $cliente = null; // Datos del cliente (scope min)
public ?array $items = null; // Items del pedido (scope max)TipoComprobantePendiente
Ubicación: Resources/Venta/TipoComprobantePendiente.php
Propiedades:
php
public int $id;
public string $nombre;
public string $reporte;
public int $numero_comprobante;
public bool $maneja_stock;ItemPendiente
Ubicación: Resources/Venta/Pendiente/ItemPendiente.php
Propiedades inferidas:
php
public int $codigo; // ID del producto
public float $cantidad;
public float $precio_unitario; // Con bonificación aplicada
public float $importe; // Total del item
public float $neto_unitario; // Precio sin bonificación
public float $bonificacion; // Porcentaje
public float $monto_bonificacion; // Importe descontado
public string $descripcion;
public float $costo;
public int $id_prefa; // FK a prefa
public int $manejaStock; // 0 o 1Validaciones Implementadas
Validaciones Estructurales (Request DTOs)
Aplicadas mediante Symfony Validator en PendienteRequest:
| Campo | Validación |
|---|---|
cliente | NotNull |
vendedor | NotNull |
sucursal | NotNull |
lista | NotNull |
fecha | NotNull, formato Carbon |
fechaEntrega | NotNull, formato Carbon |
provincia | NotNull |
comentario | Length(max: 100) |
items | NotNull, array |
Validaciones de Negocio (Service Layer)
En PedidoService:
- Verificación de existencia de productos (ItemPedidoService)
- Si producto no existe:
BadRequest("No se encontró el producto con código: X")
En PendienteService:
- Verificación de módulo de Stock habilitado
- Si no se puede obtener módulos:
InsertError("Error al obtener los módulos de la empresa")
En TipoComprobantePendienteService:
- Validación de campos permitidos en actualización
- Si no hay campos válidos:
ServerException("No hay campos válidos para actualizar.") - Verificación de existencia del tipo
- Si no existe:
ServerException("No se encontró el comprobante con ID: {id}")
Puntos de Integración
Integración con Módulo de Stock
Servicio: MovimientoStockService
Responsabilidades:
- Insertar movimientos de stock (egreso) al crear pendiente
- Eliminar movimientos al actualizar o eliminar pendiente
- Filtro por
id_prefapara identificar movimientos asociados
Métodos utilizados:
insert(array $movimientos): Inserta movimientos en batchdeleteByIdPendiente(int $id_prefa): Elimina movimientos por ID de pendiente
Integración con Módulo de Clientes
Modelo: Cliente
Responsabilidades:
- Obtener datos del cliente para mostrar en detalle del pedido
Métodos utilizados:
getClienteById(int $codigo, string $scope): Obtiene cliente con scopemin
Integración con Módulo de Productos
Modelo: Producto
Responsabilidades:
- Consultar productos para validar existencia
- Obtener costo y flag de manejo de stock
Métodos utilizados:
getOne(array $options): Consulta producto por ID
Integración con Módulo de Facturación
Mecanismo: Campo id_factura en tabla prefa
Flujo (inferido, NO implementado en código analizado):
- Al generar una factura desde un pedido pendiente
- La factura asigna su ID al campo
id_facturadel pendiente - El pendiente queda "cancelado" y no aparece en consultas de pendientes
⚠️ NOTA: Este flujo se gestiona desde el módulo de Facturación, no está implementado en Pendientes.
Estrategia de Testing
Tests Unitarios (Recomendados)
PendienteService:
testInsertPendiente(): Verificar flujo completo de inserción con transaccióntestInsertGeneraMovimientosStock(): Verificar que se generan movimientos si aplicatestInsertNoGeneraMovimientosSinModuloStock(): Verificar que NO genera movimientos sin módulotestUpdatePendienteActualizaItems(): Verificar actualización con eliminación/reinserción de itemstestUpdateActualizaStock(): Verificar actualización de movimientos de stocktestDeleteEliminaLogicamente(): Verificar soft deletetestDeleteEliminaMovimientosStock(): Verificar eliminación de movimientos
PedidoService:
testCalculoTotalesConBonificacion(): Verificar cálculos de neto, bonificación y totaltestMapPendienteAsignaNumeroComprobante(): Verificar asignación de númerotestGetOneIncluyeCliente(): Verificar inclusión de datos del clientetestGetOneConScopeMaxIncluyeItems(): Verificar inclusión de items
ItemPedidoService:
testMapItemsCalculaBonificacion(): Verificar cálculo de bonificaciones por itemtestMapItemsConsultaProducto(): Verificar consulta y validación de productostestMapItemsLanzaExcepcionProductoInexistente(): Verificar error si producto no existetestMapItemsCacheaProductos(): Verificar que cachea productos consultados
Modelo Pendiente:
testGetAllFiltraEliminados(): Verificar filtro deleted_at IS NULLtestGetAllFiltraFacturados(): Verificar filtro id_factura IS NULLtestGetAllConScopeMin(): Verificar campos retornados en scope mintestGetAllConScopeMax(): Verificar campos retornados en scope maxtestHasByClienteRetornaTrue(): Verificar detección de pendientes por clientetestInsertRetornaId(): Verificar que insert retorna el ID
TipoComprobantePendienteService:
testUpdateIncrementaNumerador(): Verificar incremento de numero_comprobantetestUpdateValidaCamposPermitidos(): Verificar validación de campos
Tests de Integración (Recomendados)
Flujo completo CREATE:
- Crear pendiente → Verificar inserción en BD → Verificar items insertados → Verificar movimientos de stock generados → Verificar incremento de numerador
Flujo completo UPDATE:
- Actualizar pendiente → Verificar actualización en BD → Verificar eliminación/reinserción de items → Verificar actualización de movimientos de stock
Flujo completo DELETE:
- Eliminar pendiente → Verificar soft delete → Verificar eliminación de movimientos de stock → Verificar que NO aparece en consultas
Transacciones:
- Provocar error en mitad del proceso → Verificar rollback completo → Verificar que NO quedaron datos inconsistentes
Tests E2E (Recomendados)
Endpoint POST /pedido:
- Request válido → Verificar 200 OK → Verificar ID retornado → Verificar datos en BD
Endpoint GET /pedido/{id}:
- Request con scope max → Verificar datos completos con cliente e items
Endpoint PUT /pedido/{id}:
- Request con modificación de items → Verificar 204 → Verificar actualización en BD
Endpoint DELETE /pedido/{id}:
- Request de eliminación → Verificar 204 → Verificar que no aparece en listado
Consideraciones de Rendimiento
Scopes de Consulta
Implementación:
- Scope
min: Solo campos esenciales (id, cliente, número, fecha, total) - Scope
max: Todos los campos del pendiente
Recomendación: Usar scope min para listados, max solo para detalles.
Cache de Productos
Implementación en ItemPedidoService:
php
$productos = []; // Cache interno
foreach ($items as $item) {
if (isset($productos[$itemData->id])) {
$producto = $productos[$itemData->id];
} else {
$producto = $productoModel->getOne(['id' => $itemData->id]);
$productos[$producto->id] = $producto;
}
}Optimización: Evita consultas redundantes al mismo producto en un pedido.
Índices Sugeridos
⚠️ NOTA: No se encontraron índices definidos en la migración más allá del PRIMARY KEY.
Índices recomendados:
sql
CREATE INDEX idx_prefa_cliente ON prefa(zf) WHERE deleted_at IS NULL;
CREATE INDEX idx_prefa_comprobante ON prefa(nrocomp, id_preformul) WHERE deleted_at IS NULL;
CREATE INDEX idx_prefa_factura ON prefa(id_factura) WHERE id_factura IS NOT NULL;
CREATE INDEX idx_prefa_tipo ON prefa(id_preformul) WHERE deleted_at IS NULL;Justificación:
- Filtrado frecuente por cliente
- Búsqueda por número de comprobante
- Filtrado de pendientes no facturados
- Filtrado por tipo de comprobante
N+1 Queries
Problema identificado: En PedidoService.getOne() con scope max:
- Query para obtener el pedido
- Query para obtener el cliente
- Query para obtener items (ItemPedidoService)
- Dentro de items: Query por cada producto (con cache)
Optimización posible: Usar JOINs en el modelo para obtener cliente y items en una sola consulta.
Consideraciones de Seguridad
Autenticación y Autorización
Implementación:
- JWT en header
Authorization - Middleware
AuthMiddlewarevalida token - Middleware
ConnectionMiddlewareconfigura schema multi-tenant desde JWT
Permisos:
- Vista de tipos de comprobantes:
VENTAS_BASES_COMPROBANTES-PENDIENTES - Operaciones sobre pedidos: Requiere autenticación (permisos no especificados explícitamente en código)
Aislamiento Multi-Tenant
Implementación:
- Uso de
ConnectionManagercon configuración por schema - Schema se obtiene del JWT payload
- Todas las consultas ejecutan con
search_pathconfigurado al schema del tenant
Validación: El ConnectionMiddleware valida y configura el schema antes de llegar al Controller.
Inyección SQL
Protección:
- Uso de Doctrine DBAL Query Builder
- Todos los parámetros se pasan via
setParameter() - No se encontró concatenación directa de strings en queries
Ejemplo:
php
$builder->where('zf = :cliente')
->setParameter('cliente', (int)$options['cliente']);Sanitización de Inputs
Validación estructural: Symfony Validator en DTOs
Validación de tipos: Cast explícito a tipos esperados
Longitud de campos: Assert\Length(max: 100) en comentario
Auditoría
⚠️ CRÍTICO: NO se encontró implementación de auditoría en los servicios de Pendientes.
Recomendación: Implementar AuditableInterface y Auditable trait en PendienteService para registrar operaciones CUD.
Ejemplo esperado:
php
$this->registrarAuditoria(
"INSERT",
"VENTAS",
$this->model->getTable(),
$pendiente->id
);Preguntas Técnicas Pendientes
Si existen preguntas técnicas sobre esta implementación, ver: Preguntas sobre Comprobantes Pendientes
Referencias
⚠️ NOTA IMPORTANTE: Esta documentación fue generada automáticamente analizando el código implementado. Validar cambios futuros contra este baseline.