listarProductos(); if (isset($_SESSION['presupuesto']['cliente'])) { $cliente = $_SESSION['presupuesto']['cliente']; } else { $cliente = ''; } // Llamamos a los métodos del modelo para obtener calculos preliminares $subtotal = $presupuestomodel->calcularSubtotal() ?? 0; $descuento = $presupuestomodel->calcularDescuentoTotal() ?? 0; $total = $presupuestomodel->calcularPrecioTotal() ?? 0; ob_start(); require __DIR__ . '/../views/presupuestos/crearPresupuestoView.php'; return ob_get_clean(); } // AGREGAR PRODUCTO AL PRESUPUESTO (en casos de presupuesto, el manejo de productos es manejado por modelo de presupuesto) case 'agregar_producto_presupuesto': require_once __DIR__ . '/../models/productoModel.php'; $productomodel = new productoModel(); if ($_SERVER['REQUEST_METHOD'] === 'GET') { $productos = $productomodel->listarProductosVenta(); foreach ($productos as $i => $p) { if (!isset($p['id'])) { unset($productos[$i]); continue; } if ($p['stock'] === null || $p['stock'] === 0) { $productos[$i]['stock'] = '-'; } $productos[$i]['descuento_total'] = number_format($p['descuento_total'], 2, '.', '') . ' %'; } ob_start(); require __DIR__ . '/../views/presupuestos/agregarProductoView.php'; return ob_get_clean(); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $ids = $_POST['ids'] ?? []; $cantidades = $_POST['cantidades'] ?? []; if (empty($ids)) { $_SESSION['flash_error'] = 'No se seleccionó ningún producto.'; header('Location: index.php?opt=agregar_producto_presupuesto'); exit; } $agregados = 0; $errores = []; // Evitar duplicados $ids = array_unique($ids); foreach ($ids as $idProducto) { $idProducto = (int) $idProducto; $cantidad = isset($cantidades[$idProducto]) ? (int) $cantidades[$idProducto] : 1; if ($idProducto <= 0 || $cantidad <= 0) { $errores[] = 'Datos inválidos.'; continue; } $producto = $productomodel->obtenerPorIdParaCaja($idProducto); if (!$producto) { $errores[] = 'Producto inexistente.'; continue; } if ($presupuestomodel->agregarProducto($producto, $cantidad)) { $agregados++; } else { $errores[] = $presupuestomodel->getError(); } } if ($agregados === 1) { $_SESSION['flash_success'] = "Se agregó un producto correctamente."; } else if ($agregados > 1) { $_SESSION['flash_success'] = "Se agregaron {$agregados} productos correctamente."; } if (!empty($errores)) { $_SESSION['flash_error'] = implode('
', array_unique($errores)); header('Location: index.php?opt=agregar_producto_presupuesto'); exit; } header('Location: index.php?opt=crear_presupuesto'); exit; } // ELIMINAR PRODUCTO DE CAJA case 'eliminar_producto_presupuesto': $idProducto = (int) ($_POST['id_producto'] ?? 0); if ($idProducto <= 0) { $_SESSION['flash_error'] = 'Datos inválidos.'; header('Location: index.php?opt=crear_presupuesto'); exit; } if (!$presupuestomodel->eliminarProducto($idProducto)) { $_SESSION['flash_error'] = $cajamodel->getError() ?? 'No se pudo eliminar el producto.'; } else { $_SESSION['flash_success'] = 'Producto eliminado correctamente.'; } header('Location: index.php?opt=crear_presupuesto'); exit; // EDITAR CANTIDAD DEL PRODUCTO case 'editar_cantidad_presupuesto': $idProducto = (int) ($_POST['id_producto'] ?? 0); $nuevaCantidad = (int) ($_POST['cantidad'] ?? 0); if ($idProducto <= 0 || $nuevaCantidad <= 0) { $_SESSION['flash_error'] = 'Datos inválidos.'; header('Location: index.php?opt=crear_presupuesto'); exit; } if (!$presupuestomodel->editarCantidad($idProducto, $nuevaCantidad)) { $_SESSION['flash_error'] = $presupuestomodel->getError() ?? 'No se pudo actualizar la cantidad.'; } else { $_SESSION['flash_success'] = 'Cantidad editada correctamente.'; } header('Location: index.php?opt=crear_presupuesto'); exit; // SELECCIONAR CLIENTE PARA EL PRESUPUESTO case 'seleccionar_cliente_presupuesto': require_once __DIR__ . '/../models/clienteModel.php'; $clientemodel = new clienteModel(); // Se muestra la vista de selección de cliente if ($_SERVER['REQUEST_METHOD'] === 'GET') { $clientes = $clientemodel->listarClientes(); ob_start(); require __DIR__ . '/../views/presupuestos/seleccionarClienteView.php'; return ob_get_clean(); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $idCliente = (int) ($_POST['id_cliente'] ?? 0); if ($idCliente <= 0) { $_SESSION['flash_error'] = "No se seleccionó ningún cliente."; header("Location: index.php?opt=seleccionar_cliente_presupuesto"); exit; } $cliente = $clientemodel->obtenerPorId($idCliente); if (!$cliente) { $_SESSION['flash_error'] = "Cliente inválido."; header("Location: index.php?opt=seleccionar_cliente_presupuesto"); exit; } $_SESSION['presupuesto']['cliente'] = [ 'id' => $cliente['id'], 'nombre' => $cliente['nombre'], 'apellido' => $cliente['apellido'], 'telefono' => $cliente['telefono'], 'email' => $cliente['email'], 'cuit' => $cliente['cuit'], 'cond_iva' => $cliente['cond_iva'] ]; $_SESSION['presupuesto_extra']['cond_iva'] = $cliente['cond_iva']; $_SESSION['presupuesto_extra']['cuit'] = $cliente['cuit']; $_SESSION['flash_success'] = "Cliente agregado correctamente."; header("Location: index.php?opt=crear_presupuesto"); exit; } // ELIMINAR CLIENTE YA SELECCIONADO case 'eliminar_cliente_presupuesto': if (isset($_SESSION['presupuesto']['cliente'])) { unset($_SESSION['presupuesto']['cliente']); // También se eliminan datos del presupuesto guardados desde vista resumen if (isset($_SESSION['presupuesto_extra'])) { unset($_SESSION['presupuesto_extra']); unset($_SESSION['presupuesto_final']); } $_SESSION['flash_success'] = "Cliente eliminado correctamente."; } else { $_SESSION['flash_error'] = "No había cliente seleccionado."; } header("Location: index.php?opt=crear_presupuesto"); exit; // VACIAR CAJA DE PRODUCTOS case 'vaciar_caja_presupuesto': if ($presupuestomodel->vaciarCajaPresupuesto()) { $_SESSION['flash_success'] = 'Carrito vaciado.'; } else { $_SESSION['flash_error'] = 'No se pudo vaciar el carrito.'; } header('Location: index.php?opt=crear_presupuesto'); exit; // GENERAR VISTA RESUMEN DE PRESUPUESTO case 'resumen_presupuesto': $cliente = ($_SESSION['presupuesto']['cliente'] ?? null); $_SESSION['presupuesto_extra']['cond_iva'] = $_SESSION['presupuesto_extra']['cond_iva'] ?? $cliente['cond_iva'] ?? 'consumidor_final'; $_SESSION['presupuesto_extra']['cuit'] = $_SESSION['presupuesto_extra']['cuit'] ?? $cliente['cuit'] ?? '00000000000'; $productos = $presupuestomodel->listarProductos(); $descuento = $presupuestomodel->calcularDescuentoTotal(); if (!$cliente || empty($productos)) { $_SESSION['flash_error'] = "Debe seleccionar el cliente y al menos un producto."; header("Location: index.php?opt=crear_presupuesto"); exit; } if ($_SERVER['REQUEST_METHOD'] === 'GET') { // 1. Subtotal SIEMPRE se recalcula (pueden haber cambiado productos/cantidades) $subtotal = $presupuestomodel->calcularSubtotal() ?? 0; // 2. Datos extra guardados en sesión (si existen) $descuentoIngresado = isset($_SESSION['presupuesto_extra']['descuento']) ? (float) $_SESSION['presupuesto_extra']['descuento'] : 0; $recargoIngresado = isset($_SESSION['presupuesto_extra']['recargo']) ? (float) $_SESSION['presupuesto_extra']['recargo'] : 0; // 3. Recalculo siempre con subtotal actual $datosProvisorios = $presupuestomodel->actualizarDatosProvisorio($descuentoIngresado, $recargoIngresado, $subtotal, $descuento); $descuento = $datosProvisorios['descuento'] ?? 0; $recargo = $datosProvisorios['recargo'] ?? 0; $total = $datosProvisorios['total'] ?? 0; // Guardo en SESSION $_SESSION['presupuesto_final'] = [ 'descuento' => $descuento, 'recargo' => $recargo, 'total' => $total ]; ob_start(); require __DIR__ . '/../views/presupuestos/resumenPresupuestoView.php'; return ob_get_clean(); } // GUARDAR DATOS DEL PRESUPUESTO (permite volver hacia atrás en el proceso y no perder datos) case 'guardar_datos_presupuesto': // Validaciones de reglas de negocio // Condición IVA $condIvaPermitidas = [ 'consumidor_final', 'responsable_inscripto', 'monotributo' ]; $condIva = $_POST['cond_iva'] ?? 'consumidor_final'; if (!in_array($condIva, $condIvaPermitidas, true)) { $_SESSION['flash_error'] = 'Condición de IVA inválida.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // CUIT $cuit = ($_POST['cuit_1'] ?? '') . ($_POST['cuit_2'] ?? '') . ($_POST['cuit_3'] ?? ''); $cuit = trim($cuit); if ($cuit === '') { $_SESSION['flash_error'] = 'Debe ingresar un CUIT.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } if (!preg_match('/^\d{11}$/', $cuit)) { $_SESSION['flash_error'] = 'El CUIT debe contener 11 dígitos.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // Forma de pago + tipo de pago $reglasPago = [ 'efectivo' => ['pago_unico'], 'transferencia' => ['pago_unico'], 'echeq' => ['pago_unico'], 'debito' => ['pago_unico'], 'credito' => ['pago_unico', '3_cuotas', '6_cuotas', '12_cuotas'], ]; $formaPago = $_POST['forma_pago'] ?? 'efectivo'; $tipoPago = $_POST['tipo_pago'] ?? 'pago_unico'; if (!isset($reglasPago[$formaPago]) || !in_array($tipoPago, $reglasPago[$formaPago], true)) { $_SESSION['flash_error'] = 'Combinación de forma y tipo de pago inválida.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // Validación de descuento y recargo $descuentoPresupuesto = isset($_POST['descuento']) ? (float) $_POST['descuento'] : 0; $recargoPresupuesto = isset($_POST['recargo']) ? (float) $_POST['recargo'] : 0; if ($descuentoPresupuesto < 0 || $descuentoPresupuesto > 100) { $_SESSION['flash_error'] = 'El descuento debe estar entre 0 y 100%.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } if ($recargoPresupuesto < 0 || $recargoPresupuesto > 100) { $_SESSION['flash_error'] = 'El recargo debe estar entre 0 y 100%.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // Guardado de datos // Datos extra del presupuesto $datosExtraPresupuesto = [ 'cond_iva' => $condIva, 'cuit' => $cuit, 'forma_pago' => $formaPago, 'tipo_pago' => $tipoPago, 'descuento' => $descuentoPresupuesto, 'recargo' => $recargoPresupuesto ]; // También los persisto en sesión para mantener estado $_SESSION['presupuesto_extra'] = $datosExtraPresupuesto; // Datos para la vista require_once __DIR__ . '/../models/clienteModel.php'; $clientemodel = new clienteModel(); $idCliente = ($_SESSION['presupuesto']['cliente']['id'] ?? null); $productos = $presupuestomodel->listarProductos(); if ($idCliente === null) { $_SESSION['flash_error'] = 'No seleccionó un cliente.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } $vendedor = $_SESSION['user'] ?? null; $cliente = $clientemodel->obtenerPorId($idCliente); if (!$cliente) { $_SESSION['flash_error'] = 'No se pudo identificar el cliente.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } if (!$vendedor) { $_SESSION['flash_error'] = 'No se pudo identificar el usuario.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } /* Subtotal que no contiene descuentos. (Al ingresar descuento o recargo, lo guardo en un array separado, para no sobreescribir datos) */ $subtotal = $presupuestomodel->calcularSubtotal() ?? 0; $descuento = $presupuestomodel->calcularDescuentoTotal() ?? 0; // Validación del subtotal if ($subtotal <= 0) { $_SESSION['flash_error'] = 'El presupuesto no contiene productos válidos.'; header('Location: index.php?opt=crear_presupuesto'); exit; } // Llamamos a la función que devuelve los datos actualizados en un array nuevo $datosProvisorios = $presupuestomodel->actualizarDatosProvisorio($descuentoPresupuesto, $recargoPresupuesto, $subtotal, $descuento); $recargo = $datosProvisorios['recargo'] ?? 0; $descuento = $datosProvisorios['descuento'] ?? 0; $total = $datosProvisorios['total'] ?? 0; $_SESSION['presupuesto_final'] = [ 'recargo' => $recargo, 'descuento' => $descuento, 'total' => $total ]; $_SESSION['flash_success'] = "Datos guardados de manera provisoria."; header("Location: index.php?opt=resumen_presupuesto"); exit; // GENERACION DE PRESUPUESTO PDF MEDIANTE USO DE LIBRERIA DOMPDF case 'generar_presupuesto_pdf': // Requiero datos de empresa y configuración require __DIR__ . '/../models/configuracionModel.php'; $configuracionmodel = new configuracionModel(); $datosEmpresa = $configuracionmodel->obtenerConfiguracion(); $mapCondIVA = [ 'consumidor_final' => 'Consumidor Final', 'responsable_inscripto' => 'Responsable Inscripto', 'monotributo' => 'Monotributo', ]; $datosEmpresa['cond_iva'] = $mapCondIVA[$datosEmpresa['cond_iva']]; // Requiero modelos de presupuesto y cliente require_once __DIR__ . '/../models/presupuestoModel.php'; $presupuestomodel = new presupuestoModel(); require_once __DIR__ . '/../models/clienteModel.php'; $clientemodel = new clienteModel(); $productos = $presupuestomodel->listarProductos(); $idCliente = $_SESSION['presupuesto']['cliente']['id'] ?? null; if ($idCliente === null) { $_SESSION['flash_error'] = 'No seleccionó un cliente.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } $vendedor = $_SESSION['user'] ?? null; $cliente = $clientemodel->obtenerPorId($idCliente); // DATOS FORM (requiero todo de nuevo desde el formulario mediante POST, no utilizo los datos ya almacenados en $_SESSION (son unicamente para mantener estado)) $condIVA = $_POST['cond_iva'] ?? ''; $cuit = ($_POST['cuit_1'] ?? '') . ($_POST['cuit_2'] ?? '') . ($_POST['cuit_3'] ?? ''); $formaPago = $_POST['forma_pago'] ?? ''; $tipoPago = $_POST['tipo_pago'] ?? ''; $descuentoPresupuesto = $_POST['descuento'] ?? 0; $recargoPresupuesto = $_POST['recargo'] ?? 0; // VALIDACIONES BASE if (!$cliente || empty($productos)) { $_SESSION['flash_error'] = 'Datos incompletos para generar el presupuesto.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } if (!$vendedor) { $_SESSION['flash_error'] = 'No se pudo identificar el usuario.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // VALIDACIÓN CONDICIÓN IVA $condIvaPermitidas = [ 'consumidor_final', 'responsable_inscripto', 'monotributo' ]; if (!in_array($condIVA, $condIvaPermitidas, true)) { $_SESSION['flash_error'] = 'Condición de IVA inválida.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // VALIDACIÓN CUIT $cuit = trim($cuit); if (!preg_match('/^\d{10,11}$/', $cuit)) { $_SESSION['flash_error'] = 'CUIT inválido. Debe contener solo números.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // VALIDACIÓN FORMA + TIPO DE PAGO $reglasPago = [ 'efectivo' => ['pago_unico'], 'transferencia' => ['pago_unico'], 'echeq' => ['pago_unico'], 'debito' => ['pago_unico'], 'credito' => ['pago_unico', '3_cuotas', '6_cuotas', '12_cuotas'], ]; if (!isset($reglasPago[$formaPago]) || !in_array($tipoPago, $reglasPago[$formaPago], true)) { $_SESSION['flash_error'] = 'Combinación de forma y tipo de pago inválida.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // VALIDACIÓN DESCUENTO / RECARGO if ($descuentoPresupuesto < 0 || $descuentoPresupuesto > 100) { $_SESSION['flash_error'] = 'El descuento debe estar entre 0 y 100%.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } if ($recargoPresupuesto < 0 || $recargoPresupuesto > 100) { $_SESSION['flash_error'] = 'El recargo debe estar entre 0 y 100%.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // RECÁLCULO DE TOTALES (SIEMPRE) $subtotal = $presupuestomodel->calcularSubtotal() ?? 0; if ($subtotal <= 0) { $_SESSION['flash_error'] = 'El presupuesto no contiene productos válidos.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } $descuentoProductos = $presupuestomodel->calcularDescuentoTotal() ?? 0; $datosProvisorios = $presupuestomodel->actualizarDatosProvisorio($descuentoPresupuesto, $recargoPresupuesto, $subtotal, $descuentoProductos); $recargo = $datosProvisorios['recargo'] ?? 0; $descuento = $datosProvisorios['descuento'] ?? 0; $total = $datosProvisorios['total'] ?? 0; if ($total <= 0) { $_SESSION['flash_error'] = 'Total inválido.'; header('Location: index.php?opt=resumen_presupuesto'); exit; } // MAPEOS PARA PDF $mapCondIVA = [ 'consumidor_final' => 'Consumidor Final', 'responsable_inscripto' => 'Responsable Inscripto', 'monotributo' => 'Monotributo', ]; $mapFormaPago = [ 'efectivo'=> 'Efectivo', 'transferencia' => 'Transferencia', 'debito' => 'Débito', 'credito' => 'Crédito', 'echeq' => 'Echeq', ]; $mapTipoPago = [ 'pago_unico' => 'Pago Único', '3_cuotas' => '3 Cuotas', '6_cuotas' => '6 Cuotas', '12_cuotas' => '12 Cuotas', ]; $condIVALabel = $mapCondIVA[$condIVA]; $formaPagoLabel = $mapFormaPago[$formaPago]; $tipoPagoLabel = $mapTipoPago[$tipoPago]; // CÁLCULO DE CUOTAS if ($tipoPago !== 'pago_unico') { $cuota = (int) explode('_', $tipoPago)[0]; $valorCuota = $total / $cuota; } // NÚMERO DE PRESUPUESTO (RANDOM) (decidí dejarlo con código aleatorio, se podría modificar perfectamente) $parte2 = str_pad((string) rand(0, 99999999), 8, '0', STR_PAD_LEFT); $numeroPresupuesto = '0001' . '-' . $parte2; // RENDER PDF ob_start(); require __DIR__ . '/../pdf/presupuestos/presupuestoPDF.php'; $html = ob_get_clean(); $cssPath = '/var/www/html/css/stylesPDF.css'; $css = file_get_contents($cssPath); // Configuración de opciones $options = new Options(); $options->set('isRemoteEnabled', true); // Permite cargar recursos externos (CSS, imágenes, etc.) $options->set('chroot', '/var/www/html'); // Define directorios base para recursos locales $options->set('defaultFont', 'DejaVu Sans'); // Creación de instancia Dompdf con las opciones seteadas $dompdf = new Dompdf($options); // Cargar el HTML que se va a convertir en PDF $dompdf->loadHtml(" $html "); $dompdf->setPaper('A4', 'portrait'); // Define el tamanio de hoja y la orientación $dompdf->render(); // Renderiza el PDF (procesa el HTML) // Enviar el PDF al browser $dompdf->stream( 'Presupuesto N° ' . $numeroPresupuesto . '.pdf', ['Attachment' => true] // false = se muestra en el navegador | true = descarga directa ); // Eliminamos datos ingresados y vaciamos caja $presupuestomodel->vaciarCajaPresupuesto(); unset(($_SESSION)['presupuesto']['cliente']); unset($_SESSION['presupuesto_final']); unset($_SESSION['presupuesto_extra']); unset($_SESSION['prov_presupuesto']); exit; // DEFAULT (404) default: ob_start(); header("Location: index.php?opt=404View.php"); return ob_get_clean(); }