Autosave: 20260212-171727

This commit is contained in:
Flatlogic Bot 2026-02-12 17:17:28 +00:00
parent f59550e0b4
commit 914c98e457
50 changed files with 273 additions and 2 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

95
inventario_por_sede.php Normal file
View File

@ -0,0 +1,95 @@
<?php
require_once 'layout_header.php';
require_once 'db/config.php';
$db = db();
$sedes = $db->query("SELECT id, nombre FROM sedes ORDER BY nombre ASC")->fetchAll(PDO::FETCH_ASSOC);
$selected_sede_id = isset($_GET['sede_id']) ? (int)$_GET['sede_id'] : 0;
$inventory = [];
$sede_name = '';
if ($selected_sede_id) {
$stmt = $db->prepare("SELECT nombre FROM sedes WHERE id = ?");
$stmt->execute([$selected_sede_id]);
$sede_name = $stmt->fetchColumn();
$stmt = $db->prepare("
SELECT p.nombre, p.sku, ss.quantity
FROM products p
JOIN stock_sedes ss ON p.id = ss.product_id
WHERE ss.sede_id = ? AND ss.quantity > 0
ORDER BY p.nombre ASC
");
$stmt->execute([$selected_sede_id]);
$inventory = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
?>
<div class="container mt-4">
<div class="card">
<div class="card-header">
<h2 class="card-title">Auditoría de Inventario por Sede</h2>
<p class="card-subtitle mb-2 text-muted">Selecciona una sede para ver el detalle de su stock.</p>
</div>
<div class="card-body">
<form method="GET" action="inventario_por_sede.php" class="mb-4">
<div class="row align-items-end">
<div class="col-md-4">
<label for="sede_id" class="form-label">Sede:</label>
<select name="sede_id" id="sede_id" class="form-select">
<option value="">-- Elige una sede --</option>
<?php foreach ($sedes as $sede): ?>
<option value="<?php echo $sede['id']; ?>" <?php echo $selected_sede_id == $sede['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($sede['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Consultar</button>
</div>
</div>
</form>
<?php if ($selected_sede_id): ?>
<hr>
<?php if ($sede_name): ?>
<h3>Inventario de: <?php echo htmlspecialchars($sede_name); ?></h3>
<?php if (count($inventory) > 0): ?>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Producto</th>
<th>SKU</th>
<th class="text-end">Cantidad en Stock</th>
</tr>
</thead>
<tbody>
<?php foreach ($inventory as $item): ?>
<tr>
<td><?php echo htmlspecialchars($item['nombre']); ?></td>
<td><?php echo htmlspecialchars($item['sku']); ?></td>
<td class="text-end"><?php echo htmlspecialchars($item['quantity']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="alert alert-info" role="alert">
No se encontraron productos con stock en esta sede.
</div>
<?php endif; ?>
<?php else: ?>
<div class="alert alert-warning" role="alert">
La sede seleccionada no es válida.
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
<?php require_once 'layout_footer.php'; ?>

View File

@ -97,6 +97,12 @@ $navItems = [
'text' => 'Inventario General',
'roles' => ['Administrador', 'admin', 'Control Logistico']
],
'inventario_por_sede' => [
'url' => 'inventario_por_sede.php',
'icon' => 'fa-search-location',
'text' => 'Auditoría por Sede',
'roles' => ['Administrador', 'admin', 'Control Logistico']
],
'generar_etiquetas' => [
'url' => 'generar_etiquetas.php',
'icon' => 'fa-barcode',

View File

@ -0,0 +1,76 @@
<?php
header('Content-Type: application/json');
require_once 'db/config.php';
$response = ['success' => false, 'message' => 'Petición inválida.'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$sede_id = isset($_POST['sede_id']) ? trim($_POST['sede_id']) : '';
$product_id_raw = isset($_POST['product_id']) ? trim($_POST['product_id']) : '';
$quantity_to_remove = isset($_POST['quantity']) ? (int)$_POST['quantity'] : 0;
// Extraer el ID numérico del producto
$product_id_parts = explode('-', $product_id_raw);
$product_id = end($product_id_parts);
if (empty($sede_id) || !is_numeric($product_id) || $quantity_to_remove <= 0) {
$response['message'] = 'Por favor, complete todos los campos correctamente.';
echo json_encode($response);
exit;
}
$pdo = db();
$pdo->beginTransaction();
try {
// 1. Obtener el stock actual y bloquear la fila para evitar concurrencia
$stmt = $pdo->prepare("SELECT quantity FROM stock_sedes WHERE sede_id = :sede_id AND product_id = :product_id FOR UPDATE");
$stmt->execute(['sede_id' => $sede_id, 'product_id' => $product_id]);
$stock = $stmt->fetch(PDO::FETCH_ASSOC);
$current_quantity = $stock ? (int)$stock['quantity'] : 0;
if ($current_quantity < $quantity_to_remove) {
$response['message'] = "No se puede retirar más stock del disponible. Stock actual: {$current_quantity}.";
$pdo->rollBack();
echo json_encode($response);
exit;
}
// 2. Calcular la nueva cantidad
$new_quantity = $current_quantity - $quantity_to_remove;
// 3. Actualizar la tabla de stock
$update_stmt = $pdo->prepare(
"UPDATE stock_sedes SET quantity = :new_quantity WHERE sede_id = :sede_id AND product_id = :product_id"
);
$update_stmt->execute([
'new_quantity' => $new_quantity,
'sede_id' => $sede_id,
'product_id' => $product_id
]);
// 4. (Opcional pero recomendado) Registrar el movimiento
$movement_stmt = $pdo->prepare(
"INSERT INTO stock_movements (product_id, sede_id, quantity, type, movement_date) VALUES (:product_id, :sede_id, :quantity, 'salida', NOW())"
);
$movement_stmt->execute([
'product_id' => $product_id,
'sede_id' => $sede_id,
'quantity' => $quantity_to_remove
]);
$pdo->commit();
$response['success'] = true;
$response['message'] = "Salida manual registrada con éxito. Stock actualizado a {$new_quantity} unidades.";
} catch (PDOException $e) {
$pdo->rollBack();
// En un entorno de producción, no deberías exponer el mensaje de error detallado.
// Considera registrar el error en un archivo de logs.
$response['message'] = 'Error en la base de datos al procesar la solicitud.'; // Mensaje genérico para el usuario
}
}
echo json_encode($response);
?>

View File

@ -24,6 +24,15 @@ try {
} catch (PDOException $e) {
$error_page_load = "Error al cargar datos: " . $e->getMessage();
}
// Obtener productos para el dropdown manual
$products = [];
try {
$products_stmt = $pdo->query("SELECT id, nombre, sku FROM products ORDER BY nombre ASC");
$products = $products_stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$error_page_load .= " Error al cargar productos: " . $e->getMessage();
}
?>
<div class="container mt-4">
@ -73,6 +82,43 @@ try {
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<i class="fa fa-edit"></i> Salida Manual de Stock
</div>
<div class="card-body">
<p class="card-text">Utilice este formulario para ajustar el inventario y poner en cero el stock de productos antiguos o sin SKU.</p>
<form id="manual-exit-form" onsubmit="return false;">
<div class="mb-3">
<label for="manual_sede" class="form-label">Sede de Origen</label>
<select class="form-select" id="manual_sede" name="sede_id" required>
<option value="">Seleccione una sede</option>
<?php foreach ($sedes as $sede): ?>
<option value="<?php echo htmlspecialchars($sede['id']); ?>"><?php echo htmlspecialchars($sede['nombre']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="manual_product" class="form-label">Producto</label>
<select class="form-select" id="manual_product" name="product_id" required>
<option value="">Seleccione un producto</option>
<?php foreach ($products as $product): ?>
<option value="<?php echo htmlspecialchars($product['id']); ?>">
<?php echo htmlspecialchars($product['nombre']) . ' (' . htmlspecialchars($product['sku'] ?: 'Sin SKU') . ')'; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="manual_quantity" class="form-label">Cantidad a Retirar</label>
<input type="number" class="form-control" id="manual_quantity" name="quantity" min="1" placeholder="Escriba la cantidad exacta a retirar" required>
</div>
<button type="submit" class="btn btn-warning w-100">Registrar Salida Manual y Poner en Cero</button>
</form>
</div>
</div>
</div>
</div>
</div>
@ -245,14 +291,62 @@ document.addEventListener('DOMContentLoaded', (event) => {
});
});
// --- LÓGICA DEL FORMULARIO MANUAL ---
const manualExitForm = document.getElementById('manual-exit-form');
if (manualExitForm) {
manualExitForm.addEventListener('submit', function(e) {
e.preventDefault();
const sedeId = document.getElementById('manual_sede').value;
const productId = document.getElementById('manual_product').value;
const quantity = document.getElementById('manual_quantity').value;
if (!sedeId || !productId || !quantity) {
showNotification("Por favor, complete todos los campos del formulario manual.", false);
return;
}
if (parseInt(quantity, 10) <= 0) {
showNotification("La cantidad debe ser un número positivo.", false);
return;
}
if (!confirm(`¿Está seguro de que desea retirar ${quantity} unidad(es) de este producto? Esta acción ajustará el inventario.`)) {
return;
}
const formData = new FormData();
formData.append('sede_id', sedeId);
formData.append('product_id', productId);
formData.append('quantity', quantity);
fetch('registrar_salida_manual_api.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, true);
manualExitForm.reset();
} else {
throw new Error(data.message || 'Error desconocido al registrar la salida manual.');
}
})
.catch(error => {
showNotification(error.message, false);
});
});
}
// Mantener el foco en el input
barcodeInput.focus();
document.body.addEventListener('click', (e) => {
if (!['INPUT', 'SELECT', 'BUTTON', 'A'].includes(e.target.tagName) && !e.target.closest('button, a')) {
if (!['INPUT', 'SELECT', 'BUTTON', 'A'].includes(e.target.tagName) && !e.target.closest('button, a, .modal')) {
barcodeInput.focus();
}
});
});
</script>
<?php require_once 'layout_footer.php'; ?>
<?php require_once 'layout_footer.php'; ?>