Files
2026-08-04 19:24:03 -03:00

294 lines
9.6 KiB
PHP

<?php
require_once __DIR__ . '/../database/database.php';
class ventaModel {
private PDO $db;
private ?string $error = null;
public function __construct() {
$this->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);
}
}