db = Database::getConnection(); } // MÉTODO PARA OBTENER ERRORES Y MANEJARLOS A TRAVÉZ DEL CONTROLLER public function getError(): ?string { return $this->error; } // CALCULAR TOTAL PROVISORIO APLICANDO DESCUENTO Y RECARGO EN BASE A TIPO Y FORMA DE PAGO public function actualizarDatosProvisorio($descuentoPresupuesto, $recargoPresupuesto, $subtotal, $descuento): ?array { // El descuento propio de la venta se calcula en base al subtotal con los descuento de productos aplicados $descuentoProvisorio = ($descuentoPresupuesto * ($subtotal - $descuento) / 100) + $descuento; // Sumamos los dos descuentos // Calculo el recargo teniendo en cuenta el subtotal (descuentos de productos ya aplicados) $recargoProvisorio = ($recargoPresupuesto * ($subtotal - $descuento) / 100); $totalProvisorio = $subtotal + $recargoProvisorio - $descuentoProvisorio; return $_SESSION['prov_venta'] = [ 'recargo' => $recargoProvisorio, 'descuento' => $descuentoProvisorio, 'total' => $totalProvisorio ]; } // FUNCION DE GUARDADO DE VENTA public function guardarVenta(array $productosFinales, array $vendedor, array $cliente, array $datosExtraVenta, array $datosProvisorios): int|false { // Validaciones (ya verifica el controller también) if (empty($productosFinales)) { $this->error = 'El pedido no tiene productos.'; return false; } if (empty($vendedor['id'])) { $this->error = 'No se pudo vincular al usuario.'; return false; } if (empty($cliente['id'])) { $this->error = 'Cliente inválido.'; return false; } if ($datosExtraVenta['descuento'] < 0 || $datosExtraVenta['descuento'] > 100) { $this->error = 'El descuento debe estar entre 0 y 100%'; return false; } if ($datosExtraVenta['recargo'] < 0 || $datosExtraVenta['recargo'] > 100) { $this->error = 'El recargo debe estar entre 0 y 100%'; return false; } if ($datosProvisorios['total'] <= 0) { $this->error = 'Total de pedido inválido.'; return false; } foreach ($productosFinales as $p) { if (empty($p['id_producto']) || empty($p['precio']) || empty($p['cantidad']) || empty($p['precio_final'])) { $this->error = 'Producto inválido en el pedido.'; return false; } if ($p['cantidad'] <= 0 || $p['precio'] < 0 || $p['precio_final'] < 0) { $this->error = 'Cantidad o precio inválido.'; return false; } if ($p['stock'] < 0) { $this->error = 'Ocurrió un error en el stock.'; return false; } } try { $this->db->beginTransaction(); // 1. INSERT VENTA $sqlVenta = "INSERT INTO ventas (total, forma_pago, tipo_pago, descuento_pct, recargo_pct, id_cliente, id_usuario) VALUES (:total, :forma_pago, :tipo_pago, :descuento_pct, :recargo_pct, :id_cliente, :id_usuario) RETURNING id "; $stmtVenta = $this->db->prepare($sqlVenta); $stmtVenta->execute([ ':total' => $datosProvisorios['total'], ':forma_pago' => $datosExtraVenta['forma_pago'], ':tipo_pago' => $datosExtraVenta['tipo_pago'], ':descuento_pct' => $datosExtraVenta['descuento'], ':recargo_pct' => $datosExtraVenta['recargo'], ':id_cliente' => $cliente['id'], ':id_usuario' => $vendedor['id'] ]); $idVenta = $stmtVenta->fetchColumn(); if (!$idVenta) { $this->error = 'No se pudo crear la venta.'; $this->db->rollBack(); return false; } // 2. PREPARAR QUERIES AUXILIARES // Pedidos disponibles (FIFO) $sqlPedidos = "SELECT id_pedido, (cantidad - cantidad_vendida) AS disponible FROM pedido_detalle WHERE id_producto = :id_producto AND (cantidad - cantidad_vendida) > 0 ORDER BY id_pedido ASC "; $stmtPedidos = $this->db->prepare($sqlPedidos); // Insert Detalle Venta $sqlDetalle = "INSERT INTO venta_detalle (precio_unitario, cantidad, subtotal, descuento_pct, id_venta, id_producto, id_pedido) VALUES (:precio_unitario, :cantidad, :subtotal, :descuento_pct, :id_venta, :id_producto, :id_pedido) "; $stmtDetalle = $this->db->prepare($sqlDetalle); // Update Pedido Detalle $sqlUpdatePedido = "UPDATE pedido_detalle SET cantidad_vendida = cantidad_vendida + :cantidad WHERE id_pedido = :id_pedido AND id_producto = :id_producto "; $stmtUpdatePedido = $this->db->prepare($sqlUpdatePedido); // Update stock del producto $sqlStock = "UPDATE productos SET stock = stock - :cantidad WHERE id = :id_producto "; $stmtStock = $this->db->prepare($sqlStock); // 3. PROCESAR DETALLE + CONSUMO STOCK (FIFO) foreach ($productosFinales as $p) { $cantidadRestante = $p['cantidad']; $stmtPedidos->execute([ ':id_producto' => $p['id_producto'] ]); $pedidos = $stmtPedidos->fetchAll(PDO::FETCH_ASSOC); if (empty($pedidos)) { $this->db->rollBack(); $this->error = "No hay stock disponible para el producto ID {$p['id_producto']}"; return false; } foreach ($pedidos as $pedido) { if ($cantidadRestante <= 0) { break; } $aVender = min($cantidadRestante, $pedido['disponible']); $subtotal = $p['precio_final'] * $aVender; // Insert venta_detalle $stmtDetalle->execute([ ':precio_unitario' => $p['precio'], ':cantidad' => $aVender, ':subtotal' => $subtotal, ':descuento_pct' => $p['descuento_total'], ':id_venta' => $idVenta, ':id_producto' => $p['id_producto'], ':id_pedido' => $pedido['id_pedido'], ]); // Update pedido_detalle $stmtUpdatePedido->execute([ ':cantidad' => $aVender, ':id_pedido' => $pedido['id_pedido'], ':id_producto' => $p['id_producto'], ]); // Update stock producto $stmtStock->execute([ ':cantidad' => $aVender, ':id_producto' => $p['id_producto'], ]); $cantidadRestante -= $aVender; } // VALIDACIÓN IMPORTANTE DE STOCK if ($cantidadRestante > 0) { $this->error = "Stock insuficiente para el producto ID {$p['id_producto']}"; return false; } } // 4. COMMIT $this->db->commit(); return $idVenta; } catch (Throwable $e) { $this->db->rollBack(); // Log interno error_log($e->getMessage()); $this->error = 'Error interno al guardar la venta.'; return false; } } // OBTENER PREVIEW DE CODIGO DE VENTA (no implementado) /* public function getNumeroVentaPreview(): int { $sql = "SELECT nextval('venta_preview_seq')"; $stmt = $this->db->prepare($sql); $stmt->execute(); return (int) $stmt->fetchColumn(); } */ // LISTAR VENTAS public function listarVentas(int $anio): ?array { $sql = "SELECT v.id, v.fecha, v.total, v.forma_pago, v.tipo_pago, v.descuento_pct, v.recargo_pct, c.nombre AS cliente_nombre, c.apellido AS cliente_apellido, u.nombre AS vendedor_nombre, u.apellido AS vendedor_apellido FROM ventas AS v INNER JOIN clientes AS c ON (v.id_cliente = c.id) INNER JOIN usuarios AS u ON (v.id_usuario = u.id) WHERE EXTRACT(YEAR FROM v.fecha) = :anio ORDER BY v.id ASC"; $stmt = $this->db->prepare($sql); $stmt->execute(['anio' => $anio]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } // OBTENER DETALLE DE VENTA public function getDetalleVenta(int $idVenta): ?array { $sql = "SELECT p.id AS id_producto, p.marca, p.modelo, vd.precio_unitario, vd.cantidad, vd.subtotal, vd.descuento_pct, vd.id_pedido FROM venta_detalle AS vd INNER JOIN productos AS p ON (vd.id_producto = p.id) WHERE (vd.id_venta = :id_venta) ORDER BY p.id ASC"; $stmt = $this->db->prepare($sql); $stmt->execute([':id_venta' => $idVenta]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } }