Carpeta models

This commit is contained in:
Axel Axt
2026-08-04 19:24:03 -03:00
parent ea2a40d0d5
commit 24d0bf55c9
12 changed files with 2944 additions and 0 deletions
+433
View File
@@ -0,0 +1,433 @@
<?php
class cajaModel {
// Variables privadas de la clase que se utilizan para armar los arrays de caja
private string $ventaKey = 'productosVenta';
private string $pedidoKey = 'productosPedido';
private string $servicioKey = 'insumosServicio';
private ?string $error = null;
public function __construct() {
// Si no estan seteados, se crean los arrays vacios (para luego trabajar con los métodos)
if (!isset($_SESSION[$this->ventaKey])) {
$_SESSION[$this->ventaKey] = [];
}
if (!isset($_SESSION[$this->pedidoKey])) {
$_SESSION[$this->pedidoKey] = [];
}
if (!isset($_SESSION[$this->servicioKey])) {
$_SESSION[$this->servicioKey] = [];
}
}
// MÉTODO PARA OBTENER ERRORES Y MANEJARLOS EN CONTROLLER
public function getError(): ?string {
return $this->error;
}
// <---------------------------------- MANEJO DE CAJA PARA VENTA ---------------------------------->
// AGREGAR PRODUCTO
public function agregarProducto(array $producto, int $cantidad): bool {
if ($cantidad <= 0 || empty($producto['id'])) {
$this->error = 'Datos inválidos del producto.';
return false;
}
$id = $producto['id'];
$stockDisponible = $producto['stock'];
// Si el producto ya se encuentra agregado
if (isset($_SESSION[$this->ventaKey][$id])) {
if ($cantidad > $stockDisponible) {
$this->error = 'La cantidad supera el stock disponible.';
return false;
}
$_SESSION[$this->ventaKey][$id]['cantidad'] = $cantidad;
return true;
}
// Si aún no existe en el array continua la ejecución
if ($cantidad > $stockDisponible) {
$this->error = 'La cantidad supera el stock disponible.';
return false;
}
// Cálculos
$descuentoTotal = $producto['descuento_producto'] + $producto['descuento_categoria'];
$precioFinal = ((100 - $descuentoTotal) * $producto['precio'] / 100); // PRECIO UNITARIO MENOS EL DESCUENTO
// Se guardan los datos en el array
$_SESSION[$this->ventaKey][$id] = [
'id_producto' => $id,
'marca' => $producto['marca'],
'modelo' => $producto['modelo'],
'precio' => $producto['precio'],
'descuento_total' => $descuentoTotal,
'cantidad' => $cantidad,
'stock' => $producto['stock'],
'precio_final' => $precioFinal
];
return true;
}
// ELIMINAR PRODUCTO
public function eliminarProducto(int $id): bool {
if (!isset($_SESSION[$this->ventaKey][$id])) {
$this->error = 'El producto no existe en la caja.';
return false;
} else{
unset($_SESSION[$this->ventaKey][$id]);
}
return true;
}
// EDITAR CANTIDAD DE PRODUCTO
public function editarCantidad(int $id, int $nuevaCantidad): bool {
if (!isset($_SESSION[$this->ventaKey][$id])) {
$this->error = 'Producto inexistente en la caja.';
return false;
}
if ($nuevaCantidad <= 0) {
$this->error = 'La cantidad debe ser mayor a cero.';
return false;
}
if ($nuevaCantidad > $_SESSION[$this->ventaKey][$id]['stock']) {
$this->error = 'La cantidad supera el stock disponible.';
return false;
}
$_SESSION[$this->ventaKey][$id]['cantidad'] = $nuevaCantidad;
return true;
}
// LISTAR PRODUCTOS
public function listarProductos(): array {
return $_SESSION[$this->ventaKey];
}
// CALCULAR SUBTOTAL
public function calcularSubtotal(): float {
$total = 0;
foreach ($_SESSION[$this->ventaKey] as $item) {
$precio = (float) $item['precio'];
$total += $precio * $item['cantidad'];
}
return round($total, 2);
}
// CALCULAR PRECIO TOTAL
public function calcularPrecioTotal(): float {
$total = 0;
foreach ($_SESSION[$this->ventaKey] as $item) {
$precio = (float) $item['precio'];
if (!empty($item['descuento_total'])) {
$precio -= ($item['descuento_total'] * $precio / 100);
}
$total += $precio * $item['cantidad'];
}
return round($total, 2);
}
// CALCULAR DESCUENTO TOTAL
public function calcularDescuentoTotal(): float {
$total = 0;
foreach ($_SESSION[$this->ventaKey] as $item) {
$precio = (float) $item['precio'];
$descuento = 0;
if (!empty($item['descuento_total'])) {
$descuento += ($item['descuento_total'] * $precio / 100);
}
$total += $descuento * $item['cantidad'];
}
return round($total, 2);
}
// VACIAR CAJA DE PRODUCTOS
public function vaciarCajaVenta(): bool {
$_SESSION[$this->ventaKey] = [];
return true;
}
// <---------------------------------- MANEJO DE CAJA PARA PEDIDO ---------------------------------->
// AGREGAR PRODUCTO PARA PEDIDO
public function agregarProductoPedido(array $producto, float $costo, int $cantidad): bool {
if ($cantidad <= 0 || $costo <= 0 || empty($producto['id'])) {
$this->error = 'Datos inválidos del producto.';
return false;
}
$id = $producto['id'];
$ganancia = (float) ($producto['porcentaje_ganancia'] ?? 0);
$precio = (100 + $ganancia) * $costo / 100;
$stockfinal = $producto['stock'] + $cantidad;
// Si el producto se encuentra en el array
if (isset($_SESSION[$this->pedidoKey][$id])) {
$_SESSION[$this->pedidoKey][$id]['costo'] = $costo;
$_SESSION[$this->pedidoKey][$id]['precio'] = $precio;
$_SESSION[$this->pedidoKey][$id]['cantidad'] = $cantidad;
$_SESSION[$this->pedidoKey][$id]['stock_final'] = $stockfinal;
return true;
}
// Caso contrario se guarda en el arreglo
$_SESSION[$this->pedidoKey][$id] = [
'id_producto' => $id,
'marca' => $producto['marca'],
'modelo' => $producto['modelo'],
'costo' => $costo,
'precio' => $precio,
'cantidad' => $cantidad,
'stock' => $producto['stock'],
'stock_final' => $stockfinal,
'porcentaje_ganancia' => $ganancia
];
return true;
}
// CALCULAR TOTAL DE PEDIDO
public function calcularPrecioTotalPedido(): float {
$total = 0;
if (empty($_SESSION[$this->pedidoKey])) {
return 0;
}
foreach ($_SESSION[$this->pedidoKey] as $item) {
$costo = (float) $item['costo'];
$total += $costo * $item['cantidad'];
}
return round($total, 2);
}
// LISTAR PRODUCTOS PARA PEDIDO
public function listarProductosPedido(): array {
return $_SESSION[$this->pedidoKey] ?? [];
}
// ELIMINAR PRODUCTO PARA PEDIDO DE LA CAJA
public function eliminarProductoPedido($id): bool {
if (!isset($_SESSION[$this->pedidoKey][$id])) {
$this->error = 'El producto no existe en el pedido.';
return false;
} else {
unset($_SESSION[$this->pedidoKey][$id]);
}
return true;
}
// VACIAR CAJA DE PRODUCTOS PARA PEDIDO
public function vaciarCajaPedido(): bool {
$_SESSION[$this->pedidoKey] = [];
return true;
}
// EDITAR DATOS DE PRODUCTO PARA PEDIDO
public function editarDatosProductoPedido(int $id, $nuevoCosto, $nuevaCantidad): bool {
if ($nuevoCosto <= 0 || $nuevaCantidad <= 0) {
$this->error = 'Costo o cantidad inválidos.';
return false;
}
if (!isset($_SESSION[$this->pedidoKey][$id])) {
return false;
} else {
$_SESSION[$this->pedidoKey][$id]['costo'] = $nuevoCosto;
$_SESSION[$this->pedidoKey][$id]['cantidad'] = $nuevaCantidad;
// Actualización de otro datos
$precio = (100 + $_SESSION[$this->pedidoKey][$id]['porcentaje_ganancia']) * $nuevoCosto / 100;
$stockfinal = $_SESSION[$this->pedidoKey][$id]['stock'] + $nuevaCantidad;
$_SESSION[$this->pedidoKey][$id]['precio'] = $precio;
$_SESSION[$this->pedidoKey][$id]['stock_final'] = $stockfinal;
}
return true;
}
// <---------------------------------- MANEJO DE CAJA PARA SERVICIO ---------------------------------->
// LISTAR INSUMOS SERVICIO
public function listarInsumos(): array {
return $_SESSION[$this->servicioKey];
}
// CALCULAR EL TOTAL DEL SERVICIO
public function calcularPrecioTotalServicio(): float {
$total = 0;
if (empty($_SESSION[$this->servicioKey])) {
return 0;
}
foreach ($_SESSION[$this->servicioKey] as $item) {
$precio = (float) $item['precio'];
$total += $precio * $item['cantidad'];
}
return round($total, 2);
}
// AGREGAR INSUMO A LA CAJA
public function agregarInsumo(array $insumo, int $cantidad): bool {
if ($cantidad <= 0 || empty($insumo['id'])) {
$this->error = 'Datos inválidos del insumo.';
return false;
}
$id = $insumo['id'];
$stockDisponible = $insumo['stock'];
// Si el insumo ya existe en el array
if (isset($_SESSION[$this->servicioKey][$id])) {
if ($cantidad > $stockDisponible) {
$this->error = 'La cantidad supera el stock disponible.';
return false;
}
$_SESSION[$this->servicioKey][$id]['cantidad'] = $cantidad;
return true;
}
// Si no se encuentra en el array, lo inserta
if ($cantidad > $stockDisponible) {
$this->error = 'La cantidad supera el stock disponible.';
return false;
}
$_SESSION[$this->servicioKey][$id] = [
'id_producto' => $id,
'marca' => $insumo['marca'],
'modelo' => $insumo['modelo'],
'precio' => $insumo['precio'],
'cantidad' => $cantidad,
'stock' => $insumo['stock'],
];
return true;
}
// VACIAR CAJA DE INSUMOS PARA SERVICIO
public function vaciarCajaServicio(): bool {
$_SESSION[$this->servicioKey] = [];
return true;
}
// ELIMINAR INSUMO PARA SERVICIO DE LA CAJA
public function eliminarInsumoServicio($id): bool {
if (!isset($_SESSION[$this->servicioKey][$id])) {
$this->error = 'El insumo no existe en el pedido.';
return false;
} else {
unset($_SESSION[$this->servicioKey][$id]);
}
return true;
}
// EDITAR CANTIDAD DE INSUMO
public function editarCantidadInsumo(int $id, int $nuevaCantidad): bool {
if (!isset($_SESSION[$this->servicioKey][$id])) {
$this->error = 'Insumo inexistente en la caja.';
return false;
}
if ($nuevaCantidad <= 0) {
$this->error = 'La cantidad debe ser mayor a cero.';
return false;
}
if ($nuevaCantidad > $_SESSION[$this->servicioKey][$id]['stock']) {
$this->error = 'La cantidad supera el stock disponible.';
return false;
}
$_SESSION[$this->servicioKey][$id]['cantidad'] = $nuevaCantidad;
return true;
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
require_once __DIR__ . '/../database/database.php';
class clienteModel {
private PDO $db;
public function __construct() {
$this->db = Database::getConnection();
}
// LISTAR CLIENTES
public function listarClientes(): array {
$sql = "SELECT id, nombre, apellido, telefono, email FROM clientes ORDER BY id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// CREAR CLIENTE
public function crearCliente(array $data): bool {
$sql = "INSERT INTO clientes (nombre, apellido, telefono, email, cuit, cond_iva) VALUES (:nombre, :apellido, :telefono, :email, :cuit, :cond_iva)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':nombre' => $data['nombre'],
':apellido' => $data['apellido'],
':telefono' => $data['telefono'],
':email' => $data['email'],
':cuit' => $data['cuit'],
':cond_iva' => $data['cond_iva']
]);
}
// OBTENER CLIENTE POR ID
public function obtenerPorId(int $id): ?array { // El ? lo uso para que pueda devolver NULL
$sql = "SELECT id, nombre, apellido, telefono, email, cuit, cond_iva FROM clientes WHERE (id = :id) LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]); // Le asigna al parámetro nombrado el valor de $id
$cliente = $stmt->fetch(PDO::FETCH_ASSOC); // Devuelve un array asociativo
return $cliente ?: null; // Verifica si el array no esta vacío y lo devuelve, sino devuelve null
}
// ACTUALIZAR DATOS DE CLIENTE
public function actualizarCliente(int $id, array $data): bool {
if (empty($data)) {
return false;
}
$campos = [];
$params = []; // Se utiliza para evitar inyección SQL usando parámetros nombrados de PDO
foreach ($data as $campo => $valor) {
$campos[] = "$campo = :$campo"; // Arma el string relacionando $campo con los parámetros nombrados (se usa en la consulta)
$params[":$campo"] = $valor; // Arma array relacionando el parámetro nombrado con el valor (se usa en la consulta)
}
$params[':id'] = $id;
$sql = "UPDATE clientes SET " . implode(', ', $campos) . " WHERE id = :id";
$stmt = $this->db->prepare($sql);
return $stmt->execute($params); // Relaciona los nombrados con los valores
}
// VERIFICAR EXISTENCIA DE CLIENTE (al crear)
public function verificarExistencia(array $datosCliente, ?int $idExcluir = null): bool {
$sql = "SELECT 1 FROM clientes WHERE ((nombre = :nombre AND apellido = :apellido AND email = :email) OR (email = :email) OR (cuit = :cuit))";
if ($idExcluir !== null) {
$sql .= " AND id <> :id_excluir";
}
$sql .= " LIMIT 1";
$stmt = $this->db->prepare($sql);
$params = [
':nombre' => $datosCliente['nombre'],
':apellido' => $datosCliente['apellido'],
':email' => $datosCliente['email'],
':cuit' => $datosCliente['cuit']
];
if ($idExcluir !== null) {
$params[':id_excluir'] = $idExcluir;
}
$stmt->execute($params);
return (bool) $stmt->fetchColumn();
}
}
+137
View File
@@ -0,0 +1,137 @@
<?php
require_once __DIR__ . '/../database/database.php';
class configuracionModel {
private PDO $db;
public function __construct() {
$this->db = Database::getConnection();
}
// OBTENER CONFIGRACIÓN
public function obtenerConfiguracion() {
$sql = "SELECT nombre, direccion, ciudad, codigo_postal, provincia, telefono, email, cond_iva, cuit, iva, inicio_act,
hora_apertura_maniana, hora_cierre_maniana, hora_apertura_tarde, hora_cierre_tarde, mail_automatico, logo_ruta, cargo_cancelacion_servicio,
email_emisor, contrasenia_emisor
FROM configuracion WHERE id = 1";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// ACTUALIZAR CONFIGURACIÓN
public function actualizarConfiguracion(array $datosEmpresa): bool {
$sql = "UPDATE configuracion
SET nombre = :nombre,
direccion = :direccion,
ciudad = :ciudad,
codigo_postal = :codigo_postal,
provincia = :provincia,
telefono = :telefono,
email = :email,
cond_iva = :cond_iva,
cuit = :cuit,
iva = :iva,
hora_apertura_maniana = :hora_apertura_maniana,
hora_cierre_maniana = :hora_cierre_maniana,
hora_apertura_tarde = :hora_apertura_tarde,
hora_cierre_tarde = :hora_cierre_tarde,
mail_automatico = :mail_automatico,
cargo_cancelacion_servicio = :cargo_cancelacion_servicio,
email_emisor = :email_emisor,
contrasenia_emisor = :contrasenia_emisor
WHERE id = 1";
$stmt = $this->db->prepare($sql);
return (bool) $stmt->execute([
'nombre' => $datosEmpresa['nombre'],
'direccion' => $datosEmpresa['direccion'],
'ciudad' => $datosEmpresa['ciudad'],
'codigo_postal' => $datosEmpresa['codigo_postal'],
'provincia' => $datosEmpresa['provincia'],
'telefono' => $datosEmpresa['telefono'],
'email' => $datosEmpresa['email'],
'cond_iva' => $datosEmpresa['cond_iva'],
'cuit' => $datosEmpresa['cuit'],
'iva' => $datosEmpresa['iva'],
'hora_apertura_maniana' => $datosEmpresa['hora_apertura_maniana'],
'hora_cierre_maniana' => $datosEmpresa['hora_cierre_maniana'],
'hora_apertura_tarde' => $datosEmpresa['hora_apertura_tarde'],
'hora_cierre_tarde' => $datosEmpresa['hora_cierre_tarde'],
'mail_automatico' => $datosEmpresa['mail_automatico'],
'cargo_cancelacion_servicio' => $datosEmpresa['cargo_cancelacion_servicio'],
'email_emisor' => $datosEmpresa['email_emisor'],
'contrasenia_emisor' => $datosEmpresa['contrasenia_emisor']
]);
}
// OBTENER EL MONTO AL CANCELAR EL SERVICIO
public function obtenerCargoCancelacion(): float {
$sql = "SELECT cargo_cancelacion_servicio FROM configuracion WHERE id = 1";
$stmt = $this->db->prepare($sql);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return isset($row['cargo_cancelacion_servicio']) ? (float) $row['cargo_cancelacion_servicio'] : 0.0;
}
// OBTENER DATOS NECESARIOS PARA EL ENVIO DE EMAIL
public function obtenerDatosEnvioMail(): array {
$sql = "SELECT direccion, ciudad, telefono, email, hora_apertura_maniana, hora_cierre_maniana, hora_apertura_tarde, hora_cierre_tarde, email_emisor, contrasenia_emisor, mail_automatico, cargo_cancelacion_servicio FROM configuracion
WHERE id = 1";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// OBTENER FECHA DE ULTIMO BACKUP
public function obtenerFechaUltimoBackup(): ?string {
$backupDir = __DIR__ . '/../backups/';
$archivos = glob($backupDir . '*.sql');
if (!$archivos) {
return null;
}
// Ordenar por fecha de modificación (más reciente primero)
usort($archivos, function ($a, $b) {
return filemtime($b) - filemtime($a);
});
// Tomar el archivo más reciente
$ultimoArchivo = $archivos[0];
// Obtener fecha desde filemtime
$timestamp = filemtime($ultimoArchivo);
// Formatear a Y-m-d
return date('Y-m-d', $timestamp);
}
}
+217
View File
@@ -0,0 +1,217 @@
<?php
require_once __DIR__ . '/../database/database.php';
class informeModel {
private PDO $db;
public function __construct() {
$this->db = Database::getConnection();
}
// OBTENER BALANCE ANUAL
public function obtenerBalanceAnual($anio) {
$sql = "WITH meses AS (
SELECT generate_series(1,12) AS mes
),
-- INGRESOS DE VENTAS
ingresos_ventas AS (
SELECT mes, SUM(total) AS total FROM (
SELECT
EXTRACT(MONTH FROM v.fecha)::int AS mes,
SUM(v.total) AS total
FROM ventas v
WHERE EXTRACT(YEAR FROM v.fecha)::int = :anio
GROUP BY mes
) sub GROUP BY mes
),
-- INGRESOS DE SERVICIOS
ingresos_servicios AS (
SELECT mes, SUM(total) AS total FROM (
SELECT
EXTRACT(MONTH FROM s.fecha_terminado)::int AS mes,
SUM(s.total) AS total
FROM servicios s
WHERE EXTRACT(YEAR FROM s.fecha_terminado)::int = :anio
GROUP BY mes
) sub GROUP BY mes
),
-- COSTOS DE PEDIDOS
costos_pedidos AS (
SELECT mes, SUM(total) AS total FROM (
SELECT
EXTRACT(MONTH FROM p.fecha)::int AS mes,
SUM(p.total) AS total
FROM pedidos p
WHERE EXTRACT(YEAR FROM p.fecha)::int = :anio
GROUP BY mes
) sub GROUP BY mes
)
-- SUMAS Y CALCULO DE UTILIDAD
SELECT
m.mes,
COALESCE(SUM(iv.total),0) AS ventas,
COALESCE(SUM(isv.total),0) AS servicios,
COALESCE(SUM(cp.total),0) AS costos,
COALESCE(SUM(iv.total),0)
+ COALESCE(SUM(isv.total),0)
- COALESCE(SUM(cp.total),0) AS utilidad
FROM meses m
LEFT JOIN ingresos_ventas iv ON (iv.mes = m.mes)
LEFT JOIN ingresos_servicios isv ON (isv.mes = m.mes)
LEFT JOIN costos_pedidos cp ON (cp.mes = m.mes)
GROUP BY m.mes
ORDER BY m.mes";
try {
$stmt = $this->db->prepare($sql);
$stmt->bindValue(':anio', $anio, PDO::PARAM_INT);
$stmt->execute();
$resultados = $stmt->fetchAll(PDO::FETCH_ASSOC);
$meses = [];
$ventas = [];
$servicios = [];
$costos = [];
$utilidades = [];
$totalVentas = 0;
$totalServicios = 0;
$totalCostos = 0;
$totalUtilidad = 0;
$mejorMes = '';
$maxUtilidad = null;
// Calculo de totales
foreach ($resultados as $row) {
$meses[] = (int) $row['mes'];
$ventas[] = (float) $row['ventas'];
$servicios[] = (float) $row['servicios'];
$costos[] = (float) $row['costos'];
$utilidades[] = (float) $row['utilidad'];
$totalVentas += $row['ventas'];
$totalServicios += $row['servicios'];
$totalCostos += $row['costos'];
$totalUtilidad += $row['utilidad'];
if ($maxUtilidad === null || $row['utilidad'] > $maxUtilidad) {
$maxUtilidad = $row['utilidad'];
$mejorMes = $row['mes'];
}
}
// Mapeo de nombres de mes
$nombresMeses = [
1=>'Enero',2=>'Febrero',3=>'Marzo',4=>'Abril',
5=>'Mayo',6=>'Junio',7=>'Julio',8=>'Agosto',
9=>'Septiembre',10=>'Octubre',11=>'Noviembre',12=>'Diciembre'
];
$mejorMesNombre = $nombresMeses[$mejorMes] ?? '';
// Retorno de array completo
return [
'meses' => $meses,
'ventas' => $ventas,
'servicios' => $servicios,
'costos' => $costos,
'utilidad' => $utilidades,
'kpis' => [
'totalIngresos' => $totalVentas + $totalServicios,
'totalCostos' => $totalCostos,
'resultado' => $totalUtilidad,
'mejorMes' => $mejorMesNombre
]
];
} catch (PDOException $e) {
// Log interno
error_log($e->getMessage());
return [];
}
}
// OBTENER MONTOS A COBRAR
public function obtenerACobrar($anio) {
$sql = "WITH meses AS (
SELECT generate_series(1,12) AS mes
),
-- SERVICIOS PENDIENTES
servicios_pendientes AS (
SELECT
EXTRACT(MONTH FROM s.fecha_registro)::int AS mes,
SUM(s.total) AS total
FROM servicios s
WHERE s.estado = 'pendiente - enviado'
AND EXTRACT(YEAR FROM s.fecha_registro)::int = :anio
GROUP BY mes
)
-- RESULTADO FINAL
SELECT
m.mes,
COALESCE(sp.total,0) AS servicios
FROM meses m
LEFT JOIN servicios_pendientes sp ON (sp.mes = m.mes)
ORDER BY m.mes";
try {
$stmt = $this->db->prepare($sql);
$stmt->bindValue(':anio', $anio, PDO::PARAM_INT);
$stmt->execute();
$resultados = $stmt->fetchAll(PDO::FETCH_ASSOC);
$meses = $totales = [];
foreach ($resultados as $row) {
$meses[] = (int) $row['mes'];
$totales[] = (float) $row['servicios'];
}
return [
'meses' => $meses,
'totales' => $totales,
'kpis' => [
'totalACobrar' => array_sum($totales)
]
];
} catch (PDOException $e) {
// Log interno
error_log($e->getMessage());
return [];
}
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
require_once __DIR__ . '/../database/database.php';
class logInModel {
private $db;
// ESTABLECER CONECCIÓN CON BASE DE DATOS
public function __construct() {
$this->db = Database::getConnection(); // Guardamos el objeto PDO en $db
}
// BUSCAR USUARIO
public function buscarPorUsuario($usuario) {
$sql = "SELECT * FROM usuarios WHERE (usuario = :usuario) AND (usuarios.activo = true) LIMIT 1"; // LIMIT 1, más que nada por seguridad u optimizacón. :usuario es reemplazado luego con el valor real
$stmt = $this->db->prepare($sql); // Uso de prepare para evitar SQL Injection
$stmt->bindParam(':usuario', $usuario, PDO::PARAM_STR); // Asocia la variable $usuario con el placeholder :usuario en la consulta.
$stmt->execute(); // Ejecuta la consulta ya preparada con los parámetros vinculados
return $stmt->fetch(PDO::FETCH_ASSOC); // Obtiene la primera fila del resultado como un array asociativo
}
// INVALIDAR POR USUARIO
public function invalidarPorUsuario(int $id): bool {
$sql = "UPDATE recuperacion_cuenta
SET usado = 1
WHERE (id_usuario = :id_usuario)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([':id_usuario' => $id]);
}
// CREAR RECUPERACIÓN
public function crear(array $data): bool {
$sql = "INSERT INTO recuperacion_cuenta (id_usuario, codigo, expira_a, usado) VALUES (:id_usuario, :codigo, :expira_a, 0)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':id_usuario' => $data['id_usuario'],
':codigo' => $data['codigo'],
':expira_a' => $data['expira_a']
]);
}
// OBTENER ÚLTIMO CÓDIGO VÁLIDO
public function obtenerCodigoValido(int $idUsuario): ?array {
$sql = "SELECT id, codigo, expira_a, usado FROM recuperacion_cuenta
WHERE (id_usuario = :id_usuario) AND (usado = 0)
ORDER BY id DESC LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id_usuario' => $idUsuario]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
// MARCAR COMO USADO
public function marcarComoUsado(int $id): bool {
$sql = "UPDATE recuperacion_cuenta
SET usado = 1
WHERE id = :id";
$stmt = $this->db->prepare($sql);
return $stmt->execute([':id' => $id]);
}
}
+254
View File
@@ -0,0 +1,254 @@
<?php
require_once __DIR__ . '/../database/database.php';
class pedidoModel {
private PDO $db;
private ?string $error = null;
public function __construct() {
$this->db = Database::getConnection();
}
// MÉTODO PARA OBTENER ERRORES E INFORMARLOS A TRAVÉZ DEL CONTROLLER
public function getError(): ?string {
return $this->error;
}
// GUARDAR PEDIDO (retorna false o el ID del pedido generado)
public function guardarPedido(array $productosFinales, array $proveedor, array $vendedor, float $total): int|false {
// Validaciones (ya verifica el controller también)
if (empty($productosFinales)) {
$this->error = 'El pedido no tiene productos.';
return false;
}
if (empty($proveedor['id'])) {
$this->error = 'Proveedor inválido.';
return false;
}
if (empty($vendedor['id'])) {
$this->error = 'No se pudo vincular al usuario.';
return false;
}
if ($total <= 0) {
$this->error = 'Total de pedido inválido.';
return false;
}
foreach ($productosFinales as $p) {
if (empty($p['id_producto']) || empty($p['cantidad']) || empty($p['costo']) || empty($p['precio'])) {
$this->error = 'Producto inválido en el pedido.';
return false;
}
if ($p['cantidad'] <= 0 || $p['costo'] < 0) {
$this->error = 'Cantidad o costo inválido.';
return false;
}
}
try {
$this->db->beginTransaction();
// 1. INSERT PEDIDO
$sqlPedido = "
INSERT INTO pedidos (total, id_proveedor, id_usuario)
VALUES (:total, :id_proveedor, :id_usuario)
RETURNING id
";
$stmtPedido = $this->db->prepare($sqlPedido);
$stmtPedido->execute([
':total' => $total,
':id_proveedor' => $proveedor['id'],
':id_usuario' => $vendedor['id']
]);
$idPedido = $stmtPedido->fetchColumn();
if (!$idPedido) {
$this->error = 'No se pudo crear el pedido.';
$this->db->rollBack();
return false;
}
// 2. INSERT DETALLE (LOOP)
$sqlDetalle = "
INSERT INTO pedido_detalle
(precio_unitario, cantidad, subtotal, id_pedido, id_producto)
VALUES
(:precio_unitario, :cantidad, :subtotal, :id_pedido, :id_producto)
";
$stmtDetalle = $this->db->prepare($sqlDetalle);
foreach ($productosFinales as $p) {
$subtotal = $p['costo'] * $p['cantidad'];
if (!$stmtDetalle->execute([
':precio_unitario' => $p['costo'],
':cantidad' => $p['cantidad'],
':subtotal' => $subtotal,
':id_pedido' => $idPedido,
':id_producto' => $p['id_producto'],
])
) {
$this->error = 'Error al guardar el detalle del pedido.';
$this->db->rollBack();
return false;
}
}
// 3. ACTUALIZAR DATOS DE PRODUCTO
$sqlStock = "
UPDATE productos
SET
stock = COALESCE(stock, 0) + :cantidad,
precio = :precio
WHERE id = :id_producto
";
$stmtStock = $this->db->prepare($sqlStock);
foreach ($productosFinales as $p) {
if (!$stmtStock->execute([
':cantidad' => $p['cantidad'],
':precio' => $p['precio'],
':id_producto' => $p['id_producto'],
])
) {
$this->error = 'Error al actualizar el producto.';
$this->db->rollBack();
return false;
}
}
// 4. COMMIT
$this->db->commit();
return $idPedido;
} catch (Throwable $e) {
$this->db->rollBack();
// Log interno
error_log($e->getMessage());
$this->error = 'Error interno al guardar el pedido.';
return false;
}
}
// LISTAR PEDIDOS
public function listarPedidos(int $anio): ?array {
$sql = "SELECT p.id, p.fecha, p.total, pe.razon_social, u.nombre AS vendedor_nombre, u.apellido AS vendedor_apellido FROM pedidos AS p
INNER JOIN proveedores AS pe ON (p.id_proveedor = pe.id)
INNER JOIN usuarios AS u ON (p.id_usuario = u.id)
WHERE EXTRACT(YEAR FROM p.fecha) = :anio
ORDER BY p.id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([':anio' => $anio]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// OBTENER DETALLE DE PEDIDO
public function getDetallePedido(int $idPedido): ?array {
$sql = "SELECT p.id AS id_producto, p.marca, p.modelo, pd.precio_unitario, pd.cantidad, pd.subtotal FROM pedido_detalle AS pd
INNER JOIN productos AS p ON (pd.id_producto = p.id)
WHERE (pd.id_pedido = :id_pedido)
ORDER BY p.id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id_pedido' => $idPedido]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// OBTENER PREVIEW DE CODIGO DE REMITO (no se utiliza más)
/*
public function getNumeroRemitoPreview(): int {
$sql = "SELECT nextval('remito_preview_seq')";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return (int) $stmt->fetchColumn();
}
*/
// OBTENER COSTO DE PRODUCTO
public function obtenerCosto(int $idProducto): ?float {
$sql = "SELECT pd.precio_unitario FROM pedido_detalle AS pd
INNER JOIN productos AS p ON (pd.id_producto = p.id)
WHERE (p.id = :idProducto)
AND pd.id_pedido = (
SELECT MAX(pd2.id_pedido)
FROM pedido_detalle AS pd2
WHERE pd2.id_producto = :idProducto2
)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':idProducto' => $idProducto,
':idProducto2' => $idProducto
]);
$resultado = $stmt->fetchColumn();
return $resultado !== false ? (float) $resultado : null;
}
// OBTENER COSTOS DE PRODUCTOS EN BASE A UNA CATEGORIA
public function obtenerCostosPorCategoria(int $idCategoria): array {
// Uso de LATERAL en LEFT JOIN -> permite traer el costo mas reciente (actúa como un foreach, ejecutando la subconsulta por cada producto)
$sql = "SELECT
p.id AS id_producto, pd.precio_unitario AS costo FROM productos AS p
LEFT JOIN LATERAL (
SELECT pd.precio_unitario FROM pedido_detalle AS pd
INNER JOIN pedidos AS pe ON (pe.id = pd.id_pedido)
WHERE (pd.id_producto = p.id)
ORDER BY pe.fecha DESC, pd.id DESC LIMIT 1
) pd ON true
WHERE (p.id_categoria = :idCategoria)";
$stmt = $this->db->prepare($sql);
$stmt->execute([':idCategoria' => $idCategoria]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
+190
View File
@@ -0,0 +1,190 @@
<?php
class presupuestoModel {
private string $presupuestoKey = 'productosPresupuesto';
private ?string $error = null;
public function __construct() {
if (!isset($_SESSION[$this->presupuestoKey])) {
$_SESSION[$this->presupuestoKey] = [];
}
}
// MÉTODO PARA OBTENER ERRORES E INFORMAR A TRAVEZ DEL CONTROLLER
public function getError(): ?string {
return $this->error;
}
// AGREGAR PRODUCTO
public function agregarProducto(array $producto, int $cantidad): bool {
if ($cantidad <= 0 || empty($producto['id'])) {
$this->error = 'Datos inválidos del producto.';
return false;
}
$id = $producto['id'];
$cantidadActual = $_SESSION[$this->presupuestoKey][$id]['cantidad'] ?? 0;
$stockDisponible = $producto['stock'];
if (($cantidadActual + $cantidad) > $stockDisponible) {
$this->error = 'La cantidad supera el stock disponible.';
return false;
}
$descuentoTotal = $producto['descuento_producto'] + $producto['descuento_categoria'];
$precioFinal = ((100 - $descuentoTotal) * $producto['precio'] / 100); // PRECIO UNITARIO MENOS EL DESCUENTO
if (isset($_SESSION[$this->presupuestoKey][$id])) {
$_SESSION[$this->presupuestoKey][$id]['cantidad'] = $cantidad;
return true;
}
$_SESSION[$this->presupuestoKey][$id] = [
'id_producto' => $id,
'marca' => $producto['marca'],
'modelo' => $producto['modelo'],
'precio' => $producto['precio'],
'descuento_total' => $descuentoTotal,
'cantidad' => $cantidad,
'stock' => $producto['stock'],
'precio_final' => $precioFinal
];
return true;
}
// LISTAR PRODUCTOS
public function listarProductos(): array {
return $_SESSION[$this->presupuestoKey];
}
// ELIMINAR PRODUCTO
public function eliminarProducto(int $id): bool {
if (!isset($_SESSION[$this->presupuestoKey][$id])) {
$this->error = 'El producto no existe en la caja.';
return false;
} else{
unset($_SESSION[$this->presupuestoKey][$id]);
}
return true;
}
// EDITAR CANTIDAD DE PRODUCTO
public function editarCantidad(int $id, int $nuevaCantidad): bool {
if (!isset($_SESSION[$this->presupuestoKey][$id])) {
$this->error = 'Producto inexistente en la caja.';
return false;
}
if ($nuevaCantidad <= 0) {
$this->error = 'La cantidad debe ser mayor a cero.';
return false;
}
if ($nuevaCantidad > $_SESSION[$this->presupuestoKey][$id]['stock']) {
$this->error = 'La cantidad supera el stock disponible.';
return false;
}
$_SESSION[$this->presupuestoKey][$id]['cantidad'] = $nuevaCantidad;
return true;
}
// CALCULAR SUBTOTAL
public function calcularSubtotal(): float {
$total = 0;
foreach ($_SESSION[$this->presupuestoKey] as $item) {
$precio = (float) $item['precio'];
$total += $precio * $item['cantidad'];
}
return round($total, 2);
}
// CALCULAR PRECIO TOTAL
public function calcularPrecioTotal(): float {
$total = 0;
foreach ($_SESSION[$this->presupuestoKey] as $item) {
$precio = (float) $item['precio'];
if (!empty($item['descuento_total'])) {
$precio -= ($item['descuento_total'] * $precio / 100);
}
$total += $precio * $item['cantidad'];
}
return round($total, 2);
}
// CALCULAR DESCUENTO TOTAL
public function calcularDescuentoTotal(): float {
$total = 0;
foreach ($_SESSION[$this->presupuestoKey] as $item) {
$precio = (float) $item['precio'];
$descuento = 0;
if (!empty($item['descuento_total'])) {
$descuento += ($item['descuento_total'] * $precio / 100);
}
$total += $descuento * $item['cantidad'];
}
return round($total, 2);
}
// VACIAR CAJA DE PRODUCTOS
public function vaciarCajaPresupuesto(): bool {
$_SESSION[$this->presupuestoKey] = [];
return true;
}
// 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 del presupuesto 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_presupuesto'] = [
'recargo' => $recargoProvisorio,
'descuento' => $descuentoProvisorio,
'total' => $totalProvisorio
];
}
}
+490
View File
@@ -0,0 +1,490 @@
<?php
require_once __DIR__ . '/../database/database.php';
class productoModel {
private PDO $db;
public function __construct() {
$this->db = Database::getConnection();
}
// LISTAR PRODUCTOS
public function listarProductos(): array {
$sql = "SELECT p.id, p.marca, p.modelo, p.precio, p.stock, (COALESCE(p.descuento, 0) + COALESCE(c.descuento, 0)) AS descuento_total, c.nombre AS categoria, t.nombre AS tipo
FROM productos AS p
INNER JOIN categorias AS c ON (p.id_categoria = c.id)
INNER JOIN tipos AS t ON (p.id_tipo = t.id)
WHERE (p.estado IS TRUE)
ORDER BY p.id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// LISTAR PRODUCTOS INACTIVOS
public function listarProductosInactivos(): array {
$sql = "SELECT p.id, p.marca, p.modelo, p.precio, c.nombre AS categoria, t.nombre AS tipo
FROM productos AS p
INNER JOIN categorias AS c ON (p.id_categoria = c.id)
INNER JOIN tipos AS t ON (p.id_tipo = t.id)
WHERE (p.estado IS FALSE)
ORDER BY p.id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// LISTAR PRODUCTOS PARA VENTA
public function listarProductosVenta(): array {
$sql = "SELECT p.id, p.marca, p.modelo, p.precio, CAST(p.precio - ((COALESCE(p.descuento, 0) + COALESCE(c.descuento, 0)) * p.precio / 100) AS DECIMAL(10,2)) AS precio_final, p.stock,
(COALESCE(p.descuento, 0) + COALESCE(c.descuento, 0)) AS descuento_total
FROM productos AS p
INNER JOIN categorias AS c ON (p.id_categoria = c.id)
INNER JOIN tipos AS t ON (p.id_tipo = t.id)
WHERE (p.estado IS TRUE) AND (p.id_tipo = 1) AND (p.precio IS NOT NULL) AND (p.stock <> 0)
ORDER BY p.id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// LISTAR PORDUCTOS PARA PEDIDO
public function listarProductosPedido(): array {
$sql = "SELECT p.id, p.marca, p.modelo, p.stock FROM productos AS p
INNER JOIN tipos AS t ON (p.id_tipo = t.id)
WHERE (p.estado IS TRUE)
ORDER BY p.id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// LISTAR INSUMOS PARA SERVICIO
public function listarInsumosServicio(): array {
$sql = "SELECT p.id, p.marca, p.modelo, p.precio, p.stock
FROM productos AS p
INNER JOIN categorias AS c ON (p.id_categoria = c.id)
INNER JOIN tipos AS t ON (p.id_tipo = t.id)
WHERE (p.estado IS TRUE) AND (p.id_tipo = 2) AND (p.precio IS NOT NULL) AND (p.stock <> 0)
ORDER BY p.id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// CREAR PRODUCTO
public function crearProducto(array $data): bool {
$sql = "INSERT INTO productos (marca, modelo, precio, stock, descuento, estado, id_categoria, id_tipo)
VALUES (:marca, :modelo, :precio, :stock, :descuento, :estado, :id_categoria, :id_tipo)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':marca' => $data['marca'],
':modelo' => $data['modelo'],
':precio' => $data['precio'],
':stock' => $data['stock'],
':descuento' => $data['descuento'],
':estado' => 1, // Cuando un producto se crea, automáticamente se setea el estado en true, es decir está activo
':id_categoria' => $data['id_categoria'],
':id_tipo' => $data['id_tipo']
]);
}
// OBTENER PRODUCTO MEDIANTE ID
public function obtenerPorId(int $id): ?array {
$sql = "SELECT p.id, p.marca, p.modelo, p.precio, p.stock, p.descuento, p.id_categoria, p.id_tipo, pd.id_pedido AS id_pedido FROM productos AS p
LEFT JOIN pedido_detalle AS pd ON (p.id = pd.id_producto)
WHERE p.id = :id
ORDER BY pd.id_pedido DESC LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
$producto = $stmt->fetch(PDO::FETCH_ASSOC);
return $producto ?: null;
}
// OBTENER PRODUCTO POR ID PARA CAJA
public function obtenerPorIdParaCaja(int $id): ?array {
$sql = "SELECT p.id, p.marca, p.modelo, p.precio, COALESCE(p.descuento, 0) AS descuento_producto, COALESCE(c.descuento, 0) AS descuento_categoria, p.stock
FROM productos AS p
LEFT JOIN categorias AS c ON (c.id = p.id_categoria)
WHERE (p.id = :id) AND (p.estado IS TRUE) AND (p.id_tipo = 1) LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
// OBTENER PRODUCTOS DE PEDIDO POR ID PARA CAJA
public function obtenerPorIdParaCajaPedido(int $id): ?array {
$sql = "SELECT p.id, p.marca, p.modelo, p.stock, COALESCE(c.porcentaje_ganancia, 0) AS porcentaje_ganancia
FROM productos AS p
LEFT JOIN categorias AS c ON (c.id = p.id_categoria)
WHERE (p.id = :id) AND (p.estado IS TRUE) LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
// OBTENER INSUMO DE SERVICIO POR ID PARA CAJA
public function obtenerPorIdParaCajaServicio(int $id): ?array {
$sql = "SELECT p.id, p.marca, p.modelo, p.precio, p.stock
FROM productos AS p
LEFT JOIN categorias AS c ON (c.id = p.id_categoria)
WHERE (p.id = :id) AND (p.estado IS TRUE) AND (p.id_tipo = 2) LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
// EDITAR PRODUCTO
public function actualizarProducto(int $id, array $data): bool {
if (empty($data)) {
return false;
}
$campos = [];
$params = [];
foreach ($data as $campo => $valor) {
$campos[] = "$campo = :$campo";
$params[":$campo"] = $valor;
}
$params[':id'] = $id;
$sql = "UPDATE productos SET " . implode(', ', $campos) . " WHERE id = :id";
$stmt = $this->db->prepare($sql);
return $stmt->execute($params);
}
// ACTUALIZAR ESTADO (ACTIVO / INACTIVO)
public function actualizarEstado(int $idProducto, bool $nuevoEstado): bool {
try {
$this->db->beginTransaction();
// Actualizamaos datos del producto
$sql1 = "UPDATE productos
SET estado = :estado,
precio = NULL,
stock = NULL,
descuento = NULL
WHERE id = :id";
$stmt1 = $this->db->prepare($sql1);
$stmt1->bindValue(':estado', $nuevoEstado, PDO::PARAM_BOOL);
$stmt1->bindValue(':id', $idProducto, PDO::PARAM_INT);
$stmt1->execute();
// Actualizamos los stock y cantidades de pedido_detalle
$sql2 = "UPDATE pedido_detalle
SET cantidad_vendida = cantidad
WHERE id_producto = :id";
$stmt2 = $this->db->prepare($sql2);
$stmt2->bindValue(':id', $idProducto, PDO::PARAM_INT);
$stmt2->execute();
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
// Log interno
error_log($e->getMessage());
return false;
}
}
// LISTAR CATEGORIAS
public function listarCategorias(): array {
$sql = "SELECT id, nombre, porcentaje_ganancia, descuento FROM categorias ORDER BY nombre ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// VERIFICAR SI EXISTE UNA CATEGORIA
public function existeCategoria(int $id): bool {
$sql = "SELECT 1 FROM categorias WHERE id = :id LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
return (bool) $stmt->fetchColumn();
}
// OBTENER CATEGORIA POR ID
public function obtenerPorIdCategoria(int $id): ?array {
$sql = "SELECT id, nombre, porcentaje_ganancia, descuento FROM categorias WHERE id = :id";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
$categoria = $stmt->fetch(PDO::FETCH_ASSOC);
return $categoria ?: null;
}
// CREAR CATEGORIA
public function crearCategoria(array $data): bool {
$sql = "INSERT INTO categorias (nombre, porcentaje_ganancia, descuento) VALUES (:nombre, :porcentaje_ganancia, :descuento)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':nombre' => $data['nombre'],
':porcentaje_ganancia' => $data['porcentaje_ganancia'],
':descuento' => $data['descuento']
]);
}
// EDITAR CATEGORIA
public function actualizarCategoria(int $id, array $data): bool {
if (empty($data)) {
return false;
}
$campos = [];
$params = [];
foreach ($data as $campo => $valor) {
$campos[] = "$campo = :$campo";
$params[":$campo"] = $valor;
}
$params[':id'] = $id;
$sql = "UPDATE categorias SET " . implode(', ', $campos) . " WHERE id = :id";
$stmt = $this->db->prepare($sql);
return $stmt->execute($params);
}
// ACTUALIZAR PRECIOS DE PRODUCTOS AL EDITAR LA CATEGORIA
public function actualizarPreciosPorCategoria(int $idCategoria, array $data, array $costosProductos): bool {
try {
$this->db->beginTransaction();
// Actualizamos la categoría
$ok = $this->actualizarCategoria($idCategoria, $data);
if (!$ok) {
$this->db->rollBack();
return false;
}
// Obtenemos el nuevo porcentaje de ganancia
$stmt = $this->db->prepare("
SELECT porcentaje_ganancia
FROM categorias
WHERE id = :id
");
$stmt->execute([':id' => $idCategoria]);
$categoria = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$categoria) {
$this->db->rollBack();
return false;
}
$porcentaje_ganancia = (float) $categoria['porcentaje_ganancia'];
$stmt = $this->db->prepare("
UPDATE productos
SET precio = :precio
WHERE (id = :id) AND (id_categoria = :idCategoria)
");
foreach ($costosProductos as $c) {
// Validaciones defensivas
if (empty($c['id_producto']) || $c['costo'] === null) {
continue;
}
$costo = (float) $c['costo'];
// Fórmula correcta
$nuevoPrecio = (100 + $porcentaje_ganancia) * $costo / 100;
$stmt->execute([
':precio' => $nuevoPrecio,
':id' => $c['id_producto'],
':idCategoria' => $idCategoria
]);
}
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
// Log interno
error_log($e->getMessage());
return false;
}
}
// LISTAR TIPOS DE PRODUCTOS
public function listarTipos(): array {
$sql = "SELECT id, nombre FROM tipos ORDER BY id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// VERIFICAR SI EXISTE EL TIPO DE PRODUCTO
public function existeTipo(int $id): bool {
$sql = "SELECT 1 FROM tipos WHERE id = :id LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
return (bool) $stmt->fetchColumn();
}
// OBTENER TIPO POR ID
public function obtenerPorIdTipo(int $tipo): ?array {
$sql = "SELECT id, nombre FROM tipos WHERE id = :id";
$stmt = $this->db->prepare($sql);
$tipo = $stmt->execute();
return $tipo ?: null;
}
// OBTENER DESCUENTOS DE PRODUCTOS EN BASE A UNA CATEGORIA
public function obtenerDescuento(int $idCategoria): ?array {
$sql = "SELECT productos.id, productos.descuento FROM productos
INNER JOIN categorias ON (categorias.id = productos.id_categoria)
WHERE (categorias.id = :idCategoria)";
$stmt = $this->db->prepare($sql);
$stmt->execute([':idCategoria' => $idCategoria]);
$resultado = $stmt->fetchAll(PDO::FETCH_ASSOC);
return !empty($resultado) ? $resultado : null;
}
// BÚSQUEDA EN INPUT DE DESCRIPCIÓN DEL EQUIPO (MÓDULO SERVICIOS)
public function busquedaParaServicio(): array {
$sql = "SELECT marca, modelo FROM productos WHERE (id_tipo = 1)";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
require_once __DIR__ . '/../database/database.php';
class proveedorModel {
private PDO $db;
public function __construct() {
$this->db = Database::getConnection();
}
// LISTAR PROVEEDORES
public function listarProveedores(): array {
$sql = "SELECT id, razon_social, telefono, email from proveedores ORDER BY id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// CREAR PROVEEDOR
public function crearProveedor(array $data): bool {
$sql = "INSERT INTO proveedores (razon_social, telefono, email, cuit, cond_iva) VALUES (:razon_social, :telefono, :email, :cuit, :cond_iva)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':razon_social' => $data['razon_social'],
':telefono' => $data['telefono'],
':email' => $data['email'],
':cuit' => $data['cuit'],
':cond_iva' => $data['cond_iva']
]);
}
// BUSCAR PROVEEDOR POR ID
public function obtenerPorId(int $id): ?array {
$sql = "SELECT id, razon_social, telefono, email, cuit, cond_iva FROM proveedores WHERE id = :id LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
$proveedor = $stmt->fetch(PDO::FETCH_ASSOC);
return $proveedor ?: null;
}
// EDITAR PROVEEDOR
public function actualizarProveedor(int $id, array $data): bool {
if (empty($data)) {
return false;
}
$campos = [];
$params = [];
foreach ($data as $campo => $valor) {
$campos[] = "$campo = :$campo";
$params[":$campo"] = $valor;
}
$params[':id'] = $id;
$sql = "UPDATE proveedores SET " . implode(', ', $campos) . " WHERE id = :id";
$stmt = $this->db->prepare($sql);
return $stmt->execute($params);
}
// OBTENER ID DE PROVEEDOR CORRESPONDIENTE A UN ID DE PEDIDO
public function obtenerIDProveedorPorPedido(int $idPedido): ?array {
$sql = "SELECT p.id, p.razon_social, p.telefono, p.email FROM proveedores AS p
INNER JOIN pedidos AS pe ON (p.id = pe.id_proveedor)
WHERE (pe.id = :id_pedido) LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id_pedido' => $idPedido]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// VERIFICAR EXISTENCIA DE PROVEEDOR
public function verificarExistencia(array $datosProveedor, ?int $idExcluir = null): bool {
$sql = "SELECT 1 FROM proveedores WHERE (razon_social = :razon_social OR email = :email OR cuit = :cuit)";
if ($idExcluir !== null) {
$sql .= " AND id <> :id_excluir";
}
$sql .= " LIMIT 1";
$stmt = $this->db->prepare($sql);
$params = [
':razon_social' => $datosProveedor['razon_social'],
':email' => $datosProveedor['email'],
':cuit' => $datosProveedor['cuit']
];
if ($idExcluir !== null) {
$params[':id_excluir'] = $idExcluir;
}
$stmt->execute($params);
return (bool) $stmt->fetchColumn();
}
}
+447
View File
@@ -0,0 +1,447 @@
<?php
require_once __DIR__ . '/../database/database.php';
class servicioModel {
private PDO $db;
private ?string $error = null;
public function __construct() {
$this->db = Database::getConnection();
}
// MÉTODO PARA OBTENER ERRORES Y MANEJARLOS EN EL CONTROLLER
public function getError(): ?string {
return $this->error;
}
// GUARDADO INCIAL DEL SERVICIO
public function guardarServicio(int $numero, array $datosServicio, array $cliente, array $vendedor): bool {
if (!isset($numero)) {
$this->error = 'No se pudo obtener el código de servicio.';
return false;
}
if (empty($datosServicio)) {
$this->error = 'El servicio no posee datos.';
return false;
}
$sql = "INSERT INTO servicios
(id, equipo, contrasenia_equipo, problema, observacion, cuenta, contrasenia_cuenta, id_cliente, id_usuario)
VALUES
(:id, :equipo, :contrasenia_equipo, :problema, :observacion, :cuenta, :contrasenia_cuenta, :id_cliente, :id_usuario)";
$stmt = $this->db->prepare($sql);
return (bool) $stmt->execute([
':id' => $numero,
':equipo' => $datosServicio['descripcion_equipo'],
':contrasenia_equipo' => $datosServicio['contrasenia_equipo'] ?? null,
':problema' => $datosServicio['descripcion_problema'],
':observacion' => ($datosServicio['observacion'] ?? '') !== '' ? $datosServicio['observacion'] : null,
':cuenta' => ($datosServicio['cuenta'] ?? '') !== '' ? $datosServicio['cuenta'] : null,
':contrasenia_cuenta' => $datosServicio['contrasenia_cuenta'] ?? null,
':id_cliente' => $cliente['id'],
':id_usuario' => $vendedor['id']
]);
}
// OBTENER PREVIEW DE CODIGO DE SERVICIO
public function getNumeroServicioPreview(): int {
$sql = "SELECT nextval('servicio_preview_seq')";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return (int) $stmt->fetchColumn();
}
// LISTAR SERVICIOS
public function listarServicios(int $anio): ?array {
$sql = "SELECT s.id, s.fecha_registro, s.fecha_terminado, s.equipo, s.estado, c.nombre AS cliente_nombre, c.apellido AS cliente_apellido, u.nombre AS vendedor_nombre, u.apellido AS vendedor_apellido FROM servicios AS s
INNER JOIN clientes AS c ON (s.id_cliente = c.id)
INNER JOIN usuarios AS u ON (s.id_usuario = u.id)
WHERE EXTRACT(YEAR FROM s.fecha_registro) = :anio
ORDER BY s.id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([':anio' => $anio]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// OBTENER DETALLE DEL SERVICIO
public function getDetalleServicio(int $idServicio): ?array {
$sql = "SELECT id, equipo, contrasenia_equipo, problema, observacion, cuenta, contrasenia_cuenta, detalle_realizado, mano_obra, total, estado FROM servicios
WHERE (id = :id)";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $idServicio]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// OBTENER DETALLE DE INSUMOS UTILIZADOS EN SERVICIO
public function getDetalleInsumos(int $idServicio): ?array {
$sql = "SELECT p.id AS id_insumo, p.marca, p.modelo, p.stock, sd.precio_unitario, sd.cantidad, sd.subtotal, sd.id_pedido FROM servicio_detalle AS sd
INNER JOIN productos AS p ON (sd.id_producto = p.id)
WHERE (sd.id_servicio = :id_servicio)
ORDER BY p.id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id_servicio' => $idServicio]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// ACTUALIZAR SERVICIO
public function actualizarServicio(array $datosServicio, array $insumos): bool {
// Validaciones (ya verifica el controller también)
if (empty($datosServicio) || $datosServicio['id_servicio'] <= 0) {
$this->error = 'Datos de servicio inválidos.';
return false;
}
if (!isset($datosServicio['mano_obra']) || $datosServicio['mano_obra'] < 0) {
$this->error = 'Mano de obra inválida.';
return false;
}
if ($datosServicio['total_final'] < 0) {
$this->error = 'Total inválido.';
return false;
}
foreach ($insumos as $i) {
if (!empty($insumos) && (empty($i['id_producto']) || empty($i['precio']) || empty($i['cantidad']))) {
$this->error = 'Producto inválido en el pedido.';
return false;
}
if (!empty($insumos) && ($i['cantidad'] <= 0 || $i['precio'] < 0)) {
$this->error = 'Cantidad o precio inválido.';
return false;
}
if (!empty($insumos) && $i['stock'] < 0) {
$this->error = 'Ocurrió un error en el stock.';
return false;
}
}
try {
$this->db->beginTransaction();
// 1. ACTUALIZAR EL SERVICIO
$sqlServicio = "UPDATE servicios
SET detalle_realizado = :detalle_realizado,
mano_obra = :mano_obra,
total = :total,
estado = 'pendiente - enviado' -- SE ACTUALIZA EL ESTADO DEL SERVICIO A (PENDIENTE - ENVIADO)
WHERE id = :id
";
$stmtServicio = $this->db->prepare($sqlServicio);
$stmtServicio->execute([
':detalle_realizado' => $datosServicio['detalle_realizado'],
':mano_obra' => $datosServicio['mano_obra'],
':total' => $datosServicio['total_final'],
':id' => $datosServicio['id_servicio'],
]);
// 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 Servicio
$sqlDetalle = "INSERT INTO servicio_detalle (precio_unitario, cantidad, subtotal, id_servicio, id_producto, id_pedido) VALUES
(:precio_unitario, :cantidad, :subtotal, :id_servicio, :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)
if (!empty($insumos)) {
foreach ($insumos 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 insumo ID {$p['id_producto']}";
return false;
}
foreach ($pedidos as $pedido) {
if ($cantidadRestante <= 0) {
break;
}
$aVender = min($cantidadRestante, $pedido['disponible']);
$subtotal = $p['precio'] * $aVender;
// Insert venta_detalle
$stmtDetalle->execute([
':precio_unitario' => $p['precio'],
':cantidad' => $aVender,
':subtotal' => $subtotal,
':id_servicio' => $datosServicio['id_servicio'],
':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->db->rollBack();
$this->error = "Stock insuficiente para el insumo ID {$p['id_producto']}";
return false;
}
}
}
// 4. COMMIT
$this->db->commit();
return true;
} catch (Throwable $e) {
$this->db->rollBack();
// Log interno
error_log($e->getMessage());
$this->error = 'Error interno al actualizar el servicio.';
return false;
}
}
// FINALIZAR SERVICIO
public function finalizarServicio(int $idServicio): bool {
if ($idServicio <= 0) {
$this->error = 'Servicio inválido.';
return false;
}
try {
$stmt = $this->db->prepare(
"UPDATE servicios
SET estado = 'finalizado',
fecha_terminado = CURRENT_DATE
WHERE id = :id"
);
return $stmt->execute([
':id' => $idServicio
]);
} catch (Throwable $e) {
$this->error = 'Error interno al finalizar el servicio.';
return false;
}
}
// CANCELAR SERVICIO
public function cancelarServicio(int $idServicio, float $cargoCancelacion): bool {
if ($idServicio <= 0) {
$this->error = 'Servicio inválido.';
return false;
}
try {
$this->db->beginTransaction();
// 1. OBTENER INSUMOS DEL SERVICIO
$sqlDetalle = "SELECT id_producto, cantidad, id_pedido FROM servicio_detalle WHERE id_servicio = :id";
$stmtDetalle = $this->db->prepare($sqlDetalle);
$stmtDetalle->execute([
':id' => $idServicio
]);
$insumos = $stmtDetalle->fetchAll(PDO::FETCH_ASSOC);
// 2. PREPARAR QUERIES
$sqlStock = "UPDATE productos
SET stock = stock + :cantidad
WHERE id = :id_producto";
$stmtStock = $this->db->prepare($sqlStock);
$sqlPedido = "UPDATE pedido_detalle
SET cantidad_vendida = cantidad_vendida - :cantidad
WHERE id_pedido = :id_pedido
AND id_producto = :id_producto";
$stmtPedido = $this->db->prepare($sqlPedido);
// 3. REVERTIR INVENTARIO
if (!empty($insumo)) {
foreach ($insumos as $i) {
// devolver stock
$stmtStock->execute([
':cantidad' => $i['cantidad'],
':id_producto' => $i['id_producto']
]);
// revertir FIFO
$stmtPedido->execute([
':cantidad' => $i['cantidad'],
':id_pedido' => $i['id_pedido'],
':id_producto' => $i['id_producto']
]);
}
}
// 4. ACTUALIZAR ESTADO DEL SERVICIO
$stmt = $this->db->prepare(
"UPDATE servicios
SET estado = 'cancelado',
total = :total,
fecha_terminado = CURRENT_DATE
WHERE id = :id"
);
$stmt->execute([
':id' => $idServicio,
':total' => $cargoCancelacion
]);
$this->db->commit();
return true;
} catch (Throwable $e) {
$this->db->rollBack();
// Log interno
error_log($e->getMessage());
$this->error = 'Error al cancelar el servicio.';
return false;
}
}
// OBTENER CLIENTE DEL SERVICIO
public function getClienteServicio(int $idServicio): array {
$sql = "SELECT c.nombre, c.apellido, c.email FROM clientes AS c
INNER JOIN servicios AS s ON (s.id_cliente = c.id)
WHERE (s.id = :id)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':id' => $idServicio,
]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// OBTENER ESTADO DE SERVICIO
public function getEstadoServicio(int $idServicio): bool {
$sql = "SELECT estado FROM servicios
WHERE (servicios.id = :id) AND (servicios.estado = 'pendiente') LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $idServicio]);
return $stmt->fetch() !== false;
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
require_once __DIR__ . '/../database/database.php';
class usuarioModel {
private PDO $db;
public function __construct() {
$this->db = Database::getConnection();
}
// ACTUALIZAR USUARIO
public function actualizarUsuario(int $id, array $data): bool {
if (empty($data)) {
return false;
}
$campos = [];
$params = []; // Se utiliza para evitar inyección SQL usando parámetros nombrados de PDO
foreach ($data as $campo => $valor) {
$campos[] = "$campo = :$campo"; // Arma el string relacionando $campo con los parámetros nombrados (se usa en la consulta)
$params[":$campo"] = $valor; // Arma array relacionando el parámetro nombrado con el valor (se usa en la consulta)
}
$params[':id'] = $id;
$sql = "UPDATE usuarios SET " . implode(', ', $campos) . " WHERE id = :id";
$stmt = $this->db->prepare($sql);
return $stmt->execute($params); // Relaciona los nombrados con los valores
}
// OBTENER USUARIO POR ID
public function obtenerPorId(int $id): ?array { // El ? lo uso para que pueda devolver NULL
$sql = "SELECT id, nombre, apellido, email, telefono, usuario, rol FROM usuarios WHERE id = :id LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]); // Le asigna al parámetro nombrado el valor de $id
$user = $stmt->fetch(PDO::FETCH_ASSOC); // Devuelve un array asociativo
return $user ?: null; // Verifica si el array no esta vacío y lo devuelve, sino devuelve null
}
// LISTAR USUARIOS
public function listarUsuarios(): array {
$sql = "SELECT id, nombre, apellido, email, telefono, usuario, rol FROM usuarios WHERE (activo = true) ORDER BY id ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// CREAR USUARIO
public function crearUsuario(array $data): bool {
$sql = "INSERT INTO usuarios (nombre, apellido, email, telefono, usuario, password, rol) VALUES (:nombre, :apellido, :email, :telefono, :usuario, :password, :rol)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':nombre' => $data['nombre'],
':apellido' => $data['apellido'],
':email' => $data['email'],
':telefono' => $data['telefono'],
':usuario' => $data['usuario'],
':password' => $data['password'],
':rol' => $data['rol']
]);
}
// VERIFICAR EXISTENCIA DE USUARIO
public function verificarExistencia(array $datosUsuario, ?int $idExcluir = null): bool {
$sql = "SELECT 1 FROM usuarios WHERE ((nombre = :nombre AND apellido = :apellido AND email = :email) OR (usuario = :usuario) OR (email = :email))";
if ($idExcluir !== null) {
$sql .= " AND id <> :id_excluir";
}
$sql .= " LIMIT 1";
$stmt = $this->db->prepare($sql);
$params = [
':nombre' => $datosUsuario['nombre'],
':apellido' => $datosUsuario['apellido'],
':usuario' => $datosUsuario['usuario'],
':email' => $datosUsuario['email']
];
if ($idExcluir !== null) {
$params[':id_excluir'] = $idExcluir;
}
$stmt->execute($params);
return (bool) $stmt->fetchColumn();
}
// ELIMINAR USUARIO (BAJA LÓGICA)
public function eliminarUsuario(int $id): bool {
$sql = "UPDATE usuarios
SET activo = false
WHERE (usuarios.id = :id)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([':id' => $id]);
}
// OBTENER USUARIO POR MAIL
public function obtenerPorEmail ($email): ?array {
$sql = "SELECT id, nombre, apellido, email, telefono, usuario, rol FROM usuarios WHERE email = :email LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
return $user ?: null;
}
// RESTABLECER CONTRASENIA
public function actualizarContrasenia(int $id, string $password): bool {
$sql = "UPDATE usuarios
SET password = :password
WHERE id = :id";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':password' => $password,
':id' => $id
]);
}
}
+294
View File
@@ -0,0 +1,294 @@
<?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);
}
}