Autosave: 20260212-171727
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 38 KiB |
BIN
assets/uploads/vouchers/698e051acfbd1-024.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
assets/uploads/vouchers/698e05480d32b-533.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
assets/uploads/vouchers/698e057e8a398-705.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
assets/uploads/vouchers/698e064676305-658.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
assets/uploads/vouchers/698e06756f564-967.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
assets/uploads/vouchers/698e06ce0f979-210.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
assets/uploads/vouchers/698e06f48e8c8-495.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/uploads/vouchers/698e0720e7499-620.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
assets/uploads/vouchers/698e07521d053-833.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/uploads/vouchers/698e07d091e66-615.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
assets/uploads/vouchers/698e07f8d9647-586.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
assets/uploads/vouchers/698e082738c1d-076.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
assets/uploads/vouchers/698e08cf2dae2-269.png
Normal file
|
After Width: | Height: | Size: 517 KiB |
BIN
assets/uploads/vouchers/698e090dc9c47-338.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
assets/uploads/vouchers/698e093e0e322-259.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
assets/uploads/vouchers/698e0982c5392-538.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
assets/uploads/vouchers/698e09a26f28e-887.png
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
assets/uploads/vouchers/698e09e09c30f-324.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
assets/uploads/vouchers/698e0a2902f1a-269.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
assets/uploads/vouchers/698e0a577751e-097.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
assets/uploads/vouchers/698e0a7f17082-260.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
assets/uploads/vouchers/698e0af95fa3b-003.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
assets/uploads/vouchers/698e0b1ce2357-443.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
95
inventario_por_sede.php
Normal 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'; ?>
|
||||||
@ -97,6 +97,12 @@ $navItems = [
|
|||||||
'text' => 'Inventario General',
|
'text' => 'Inventario General',
|
||||||
'roles' => ['Administrador', 'admin', 'Control Logistico']
|
'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' => [
|
'generar_etiquetas' => [
|
||||||
'url' => 'generar_etiquetas.php',
|
'url' => 'generar_etiquetas.php',
|
||||||
'icon' => 'fa-barcode',
|
'icon' => 'fa-barcode',
|
||||||
|
|||||||
76
registrar_salida_manual_api.php
Normal 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);
|
||||||
|
?>
|
||||||
@ -24,6 +24,15 @@ try {
|
|||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
$error_page_load = "Error al cargar datos: " . $e->getMessage();
|
$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">
|
<div class="container mt-4">
|
||||||
@ -73,6 +82,43 @@ try {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -245,10 +291,58 @@ 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
|
// Mantener el foco en el input
|
||||||
barcodeInput.focus();
|
barcodeInput.focus();
|
||||||
document.body.addEventListener('click', (e) => {
|
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();
|
barcodeInput.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||