feat: agregar funcionalidad para actualizar movimientos de inventario
This commit is contained in:
parent
3f4a03c9c5
commit
6b76b94e62
@ -3,10 +3,12 @@
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\App\InventoryEntryRequest;
|
use App\Http\Requests\App\InventoryEntryRequest;
|
||||||
use App\Http\Requests\App\InventoryExitRequest;
|
use App\Http\Requests\App\InventoryExitRequest;
|
||||||
|
use App\Http\Requests\App\InventoryMovementUpdateRequest;
|
||||||
use App\Http\Requests\App\InventoryTransferRequest;
|
use App\Http\Requests\App\InventoryTransferRequest;
|
||||||
use App\Models\InventoryMovement;
|
use App\Models\InventoryMovement;
|
||||||
use App\Services\InventoryMovementService;
|
use App\Services\InventoryMovementService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,6 +83,30 @@ public function show(int $id)
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editar movimiento de inventario
|
||||||
|
* Revierte el movimiento original y aplica el nuevo
|
||||||
|
*/
|
||||||
|
public function update(InventoryMovementUpdateRequest $request, int $id)
|
||||||
|
{
|
||||||
|
|
||||||
|
try {
|
||||||
|
$movement = $this->movementService->updateMovement(
|
||||||
|
$id,
|
||||||
|
$request->validated()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Movimiento actualizado correctamente',
|
||||||
|
'movement' => $movement,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registrar entrada de inventario
|
* Registrar entrada de inventario
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -18,22 +18,27 @@
|
|||||||
class KardexController extends Controller
|
class KardexController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Generar Kardex en Excel para un producto
|
* Generar Kardex en Excel para un producto o todos los movimientos
|
||||||
*/
|
*/
|
||||||
public function export(Request $request)
|
public function export(Request $request)
|
||||||
{
|
{
|
||||||
// 1. VALIDACIÓN
|
// 1. VALIDACIÓN
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'inventory_id' => 'required|exists:inventories,id',
|
'inventory_id' => 'nullable|exists:inventories,id',
|
||||||
'warehouse_id' => 'sometimes|nullable|exists:warehouses,id',
|
'warehouse_id' => 'nullable|exists:warehouses,id',
|
||||||
'fecha_inicio' => 'required|date',
|
'fecha_inicio' => 'required|date',
|
||||||
'fecha_fin' => 'required|date|after_or_equal:fecha_inicio',
|
'fecha_fin' => 'required|date|after_or_equal:fecha_inicio',
|
||||||
'movement_type' => 'sometimes|nullable|in:entry,exit,transfer,sale,return',
|
'movement_type' => 'nullable|in:entry,exit,transfer,sale,return',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay();
|
$fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay();
|
||||||
$fechaFin = Carbon::parse($request->fecha_fin)->endOfDay();
|
$fechaFin = Carbon::parse($request->fecha_fin)->endOfDay();
|
||||||
|
|
||||||
|
// Si no hay inventory_id, mostrar todos los movimientos
|
||||||
|
if (!$request->inventory_id) {
|
||||||
|
return $this->exportAllMovements($request, $fechaInicio, $fechaFin);
|
||||||
|
}
|
||||||
|
|
||||||
$inventory = Inventory::with(['category', 'price'])->findOrFail($request->inventory_id);
|
$inventory = Inventory::with(['category', 'price'])->findOrFail($request->inventory_id);
|
||||||
|
|
||||||
// 2. CONSULTA DE MOVIMIENTOS
|
// 2. CONSULTA DE MOVIMIENTOS
|
||||||
@ -58,27 +63,16 @@ public function export(Request $request)
|
|||||||
|
|
||||||
$movements = $query->get();
|
$movements = $query->get();
|
||||||
|
|
||||||
// 3. CALCULAR SALDO INICIAL (movimientos previos al rango)
|
// 3. CONSTRUIR LÍNEAS DEL KARDEX
|
||||||
$initialBalance = $this->calculateInitialBalance($inventory->id, $fechaInicio, $warehouseId);
|
|
||||||
|
|
||||||
// 4. CONSTRUIR LÍNEAS DEL KARDEX
|
|
||||||
$balance = $initialBalance;
|
|
||||||
$kardexLines = [];
|
$kardexLines = [];
|
||||||
|
|
||||||
foreach ($movements as $movement) {
|
foreach ($movements as $movement) {
|
||||||
$effect = $this->calculateMovementEffect($movement, $warehouseId);
|
|
||||||
$entryQty = $effect > 0 ? $effect : 0;
|
|
||||||
$exitQty = $effect < 0 ? abs($effect) : 0;
|
|
||||||
$balance += $effect;
|
|
||||||
|
|
||||||
$kardexLines[] = [
|
$kardexLines[] = [
|
||||||
'date' => $movement->created_at->format('d/m/Y H:i'),
|
'date' => $movement->created_at->format('d/m/Y H:i'),
|
||||||
'type' => $this->translateMovementType($movement->movement_type),
|
'type' => $this->translateMovementType($movement->movement_type),
|
||||||
'warehouse_from' => $movement->warehouseFrom?->name ?? '-',
|
'warehouse_from' => $movement->warehouseFrom?->name ?? '-',
|
||||||
'warehouse_to' => $movement->warehouseTo?->name ?? '-',
|
'warehouse_to' => $movement->warehouseTo?->name ?? '-',
|
||||||
'entry' => $entryQty,
|
'quantity' => $movement->quantity,
|
||||||
'exit' => $exitQty,
|
|
||||||
'balance' => $balance,
|
|
||||||
'unit_cost' => $movement->unit_cost ?? 0,
|
'unit_cost' => $movement->unit_cost ?? 0,
|
||||||
'notes' => $movement->notes ?? '',
|
'notes' => $movement->notes ?? '',
|
||||||
'invoice_reference' => $movement->invoice_reference ?? '',
|
'invoice_reference' => $movement->invoice_reference ?? '',
|
||||||
@ -120,7 +114,7 @@ public function export(Request $request)
|
|||||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||||
];
|
];
|
||||||
|
|
||||||
$lastCol = 'K';
|
$lastCol = 'J';
|
||||||
|
|
||||||
// --- TÍTULO ---
|
// --- TÍTULO ---
|
||||||
$sheet->mergeCells("A2:{$lastCol}2");
|
$sheet->mergeCells("A2:{$lastCol}2");
|
||||||
@ -175,15 +169,6 @@ public function export(Request $request)
|
|||||||
$sheet->getStyle("B{$row}:{$lastCol}{$row}")->applyFromArray($styleBox);
|
$sheet->getStyle("B{$row}:{$lastCol}{$row}")->applyFromArray($styleBox);
|
||||||
$sheet->getStyle("B{$row}")->getFont()->setSize(11);
|
$sheet->getStyle("B{$row}")->getFont()->setSize(11);
|
||||||
|
|
||||||
// --- SALDO INICIAL ---
|
|
||||||
$row++;
|
|
||||||
$sheet->setCellValue("A{$row}", 'SALDO INICIAL:');
|
|
||||||
$sheet->getStyle("A{$row}")->applyFromArray($styleLabel);
|
|
||||||
$sheet->mergeCells("B{$row}:C{$row}");
|
|
||||||
$sheet->setCellValue("B{$row}", $initialBalance);
|
|
||||||
$sheet->getStyle("B{$row}:C{$row}")->applyFromArray($styleBox);
|
|
||||||
$sheet->getStyle("B{$row}")->getFont()->setBold(true);
|
|
||||||
|
|
||||||
// --- ENCABEZADOS DE TABLA ---
|
// --- ENCABEZADOS DE TABLA ---
|
||||||
$row += 2;
|
$row += 2;
|
||||||
$headerRow = $row;
|
$headerRow = $row;
|
||||||
@ -193,13 +178,11 @@ public function export(Request $request)
|
|||||||
'B' => 'TIPO',
|
'B' => 'TIPO',
|
||||||
'C' => "ALMACÉN\nORIGEN",
|
'C' => "ALMACÉN\nORIGEN",
|
||||||
'D' => "ALMACÉN\nDESTINO",
|
'D' => "ALMACÉN\nDESTINO",
|
||||||
'E' => 'ENTRADA',
|
'E' => 'CANTIDAD',
|
||||||
'F' => 'SALIDA',
|
'F' => "COSTO\nUNITARIO",
|
||||||
'G' => 'SALDO',
|
'G' => 'REFERENCIA',
|
||||||
'H' => "COSTO\nUNITARIO",
|
'H' => 'NOTAS',
|
||||||
'I' => 'REFERENCIA',
|
'I' => 'USUARIO',
|
||||||
'J' => 'NOTAS',
|
|
||||||
'K' => 'USUARIO',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($headers as $col => $text) {
|
foreach ($headers as $col => $text) {
|
||||||
@ -211,24 +194,20 @@ public function export(Request $request)
|
|||||||
|
|
||||||
// --- DATOS ---
|
// --- DATOS ---
|
||||||
$row++;
|
$row++;
|
||||||
$totalEntries = 0;
|
$totalQuantity = 0;
|
||||||
$totalExits = 0;
|
|
||||||
|
|
||||||
foreach ($kardexLines as $line) {
|
foreach ($kardexLines as $line) {
|
||||||
$sheet->setCellValue("A{$row}", $line['date']);
|
$sheet->setCellValue("A{$row}", $line['date']);
|
||||||
$sheet->setCellValue("B{$row}", $line['type']);
|
$sheet->setCellValue("B{$row}", $line['type']);
|
||||||
$sheet->setCellValue("C{$row}", $line['warehouse_from']);
|
$sheet->setCellValue("C{$row}", $line['warehouse_from']);
|
||||||
$sheet->setCellValue("D{$row}", $line['warehouse_to']);
|
$sheet->setCellValue("D{$row}", $line['warehouse_to']);
|
||||||
$sheet->setCellValue("E{$row}", $line['entry'] ?: '');
|
$sheet->setCellValue("E{$row}", $line['quantity']);
|
||||||
$sheet->setCellValue("F{$row}", $line['exit'] ?: '');
|
$sheet->setCellValue("F{$row}", $line['unit_cost'] ? '$' . number_format($line['unit_cost'], 2) : '');
|
||||||
$sheet->setCellValue("G{$row}", $line['balance']);
|
$sheet->setCellValue("G{$row}", $line['invoice_reference']);
|
||||||
$sheet->setCellValue("H{$row}", $line['unit_cost'] ? '$' . number_format($line['unit_cost'], 2) : '');
|
$sheet->setCellValue("H{$row}", $line['notes']);
|
||||||
$sheet->setCellValue("I{$row}", $line['invoice_reference']);
|
$sheet->setCellValue("I{$row}", $line['user']);
|
||||||
$sheet->setCellValue("J{$row}", $line['notes']);
|
|
||||||
$sheet->setCellValue("K{$row}", $line['user']);
|
|
||||||
|
|
||||||
$totalEntries += $line['entry'];
|
$totalQuantity += $line['quantity'];
|
||||||
$totalExits += $line['exit'];
|
|
||||||
|
|
||||||
// Estilo de fila
|
// Estilo de fila
|
||||||
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([
|
||||||
@ -244,25 +223,13 @@ public function export(Request $request)
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resaltar entradas en verde y salidas en rojo
|
|
||||||
if ($line['entry'] > 0) {
|
|
||||||
$sheet->getStyle("E{$row}")->getFont()->setColor(new Color('006100'));
|
|
||||||
$sheet->getStyle("E{$row}")->getFont()->setBold(true);
|
|
||||||
}
|
|
||||||
if ($line['exit'] > 0) {
|
|
||||||
$sheet->getStyle("F{$row}")->getFont()->setColor(new Color('9C0006'));
|
|
||||||
$sheet->getStyle("F{$row}")->getFont()->setBold(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$row++;
|
$row++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FILA DE TOTALES ---
|
// --- FILA DE TOTALES ---
|
||||||
$sheet->mergeCells("A{$row}:D{$row}");
|
$sheet->mergeCells("A{$row}:D{$row}");
|
||||||
$sheet->setCellValue("A{$row}", 'TOTALES');
|
$sheet->setCellValue("A{$row}", 'TOTALES');
|
||||||
$sheet->setCellValue("E{$row}", $totalEntries);
|
$sheet->setCellValue("E{$row}", $totalQuantity);
|
||||||
$sheet->setCellValue("F{$row}", $totalExits);
|
|
||||||
$sheet->setCellValue("G{$row}", $balance);
|
|
||||||
|
|
||||||
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([
|
||||||
'font' => ['bold' => true, 'size' => 10],
|
'font' => ['bold' => true, 'size' => 10],
|
||||||
@ -278,12 +245,11 @@ public function export(Request $request)
|
|||||||
$sheet->getColumnDimension('C')->setWidth(18);
|
$sheet->getColumnDimension('C')->setWidth(18);
|
||||||
$sheet->getColumnDimension('D')->setWidth(18);
|
$sheet->getColumnDimension('D')->setWidth(18);
|
||||||
$sheet->getColumnDimension('E')->setWidth(12);
|
$sheet->getColumnDimension('E')->setWidth(12);
|
||||||
$sheet->getColumnDimension('F')->setWidth(12);
|
$sheet->getColumnDimension('F')->setWidth(14);
|
||||||
$sheet->getColumnDimension('G')->setWidth(12);
|
$sheet->getColumnDimension('G')->setWidth(18);
|
||||||
$sheet->getColumnDimension('H')->setWidth(14);
|
$sheet->getColumnDimension('H')->setWidth(25);
|
||||||
$sheet->getColumnDimension('I')->setWidth(18);
|
$sheet->getColumnDimension('I')->setWidth(16);
|
||||||
$sheet->getColumnDimension('J')->setWidth(25);
|
$sheet->getColumnDimension('J')->setWidth(16);
|
||||||
$sheet->getColumnDimension('K')->setWidth(16);
|
|
||||||
|
|
||||||
// 6. GUARDAR Y DESCARGAR
|
// 6. GUARDAR Y DESCARGAR
|
||||||
$writer = new Xlsx($spreadsheet);
|
$writer = new Xlsx($spreadsheet);
|
||||||
@ -292,58 +258,6 @@ public function export(Request $request)
|
|||||||
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calcular saldo inicial (movimientos previos al rango de fechas)
|
|
||||||
*/
|
|
||||||
private function calculateInitialBalance(int $inventoryId, Carbon $beforeDate, ?int $warehouseId): int
|
|
||||||
{
|
|
||||||
$query = InventoryMovement::where('inventory_id', $inventoryId)
|
|
||||||
->where('created_at', '<', $beforeDate)
|
|
||||||
->orderBy('created_at', 'asc')
|
|
||||||
->orderBy('id', 'asc');
|
|
||||||
|
|
||||||
if ($warehouseId) {
|
|
||||||
$query->where(function ($q) use ($warehouseId) {
|
|
||||||
$q->where('warehouse_from_id', $warehouseId)
|
|
||||||
->orWhere('warehouse_to_id', $warehouseId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$balance = 0;
|
|
||||||
foreach ($query->get() as $movement) {
|
|
||||||
$balance += $this->calculateMovementEffect($movement, $warehouseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calcular el efecto de un movimiento en el stock
|
|
||||||
*/
|
|
||||||
private function calculateMovementEffect(InventoryMovement $movement, ?int $warehouseId): int
|
|
||||||
{
|
|
||||||
// Sin filtro de almacén: stock global
|
|
||||||
if (!$warehouseId) {
|
|
||||||
return match ($movement->movement_type) {
|
|
||||||
'entry', 'return' => $movement->quantity,
|
|
||||||
'exit', 'sale' => -$movement->quantity,
|
|
||||||
'transfer' => 0,
|
|
||||||
default => 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Con filtro de almacén: depende de la dirección
|
|
||||||
if ($movement->warehouse_to_id == $warehouseId && $movement->warehouse_from_id != $warehouseId) {
|
|
||||||
return $movement->quantity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($movement->warehouse_from_id == $warehouseId && $movement->warehouse_to_id != $warehouseId) {
|
|
||||||
return -$movement->quantity;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traducir tipo de movimiento
|
* Traducir tipo de movimiento
|
||||||
*/
|
*/
|
||||||
@ -353,9 +267,153 @@ private function translateMovementType(string $type): string
|
|||||||
'entry' => 'Entrada',
|
'entry' => 'Entrada',
|
||||||
'exit' => 'Salida',
|
'exit' => 'Salida',
|
||||||
'transfer' => 'Traspaso',
|
'transfer' => 'Traspaso',
|
||||||
'sale' => 'Venta',
|
/* 'sale' => 'Venta',
|
||||||
'return' => 'Devolución',
|
'return' => 'Devolución', */
|
||||||
default => $type,
|
default => $type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exportar todos los movimientos (sin filtro de producto)
|
||||||
|
*/
|
||||||
|
private function exportAllMovements(Request $request, Carbon $fechaInicio, Carbon $fechaFin)
|
||||||
|
{
|
||||||
|
// CONSULTA DE MOVIMIENTOS
|
||||||
|
$query = InventoryMovement::with(['inventory', 'warehouseFrom', 'warehouseTo', 'user'])
|
||||||
|
->whereBetween('created_at', [$fechaInicio, $fechaFin])
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->orderBy('id', 'asc');
|
||||||
|
|
||||||
|
if ($request->warehouse_id) {
|
||||||
|
$query->where(function ($q) use ($request) {
|
||||||
|
$q->where('warehouse_from_id', $request->warehouse_id)
|
||||||
|
->orWhere('warehouse_to_id', $request->warehouse_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->movement_type) {
|
||||||
|
$query->where('movement_type', $request->movement_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
$movements = $query->get();
|
||||||
|
|
||||||
|
if ($movements->isEmpty()) {
|
||||||
|
return response()->json(['message' => 'No se encontraron movimientos en el periodo especificado'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GENERAR EXCEL
|
||||||
|
$fileName = 'Movimientos_' . $fechaInicio->format('Ymd') . '_' . $fechaFin->format('Ymd') . '.xlsx';
|
||||||
|
$filePath = storage_path('app/temp/' . $fileName);
|
||||||
|
if (!file_exists(dirname($filePath))) mkdir(dirname($filePath), 0755, true);
|
||||||
|
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->setTitle('Movimientos');
|
||||||
|
|
||||||
|
$sheet->getParent()->getDefaultStyle()->getFont()->setName('Arial');
|
||||||
|
|
||||||
|
$styleTableHeader = [
|
||||||
|
'font' => ['bold' => true, 'size' => 10, 'color' => ['rgb' => 'FFFFFF']],
|
||||||
|
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '2E75B6']],
|
||||||
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true],
|
||||||
|
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||||
|
];
|
||||||
|
|
||||||
|
$lastCol = 'J';
|
||||||
|
|
||||||
|
// TÍTULO
|
||||||
|
$sheet->mergeCells("A2:{$lastCol}2");
|
||||||
|
$sheet->setCellValue('A2', 'REPORTE DE MOVIMIENTOS DE INVENTARIO');
|
||||||
|
$sheet->getStyle('A2')->applyFromArray([
|
||||||
|
'font' => ['bold' => true, 'size' => 16, 'color' => ['rgb' => '2E75B6']],
|
||||||
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||||
|
]);
|
||||||
|
$sheet->getRowDimension(2)->setRowHeight(30);
|
||||||
|
|
||||||
|
// PERÍODO
|
||||||
|
$row = 4;
|
||||||
|
Carbon::setLocale('es');
|
||||||
|
$periodoTexto = 'del ' . $fechaInicio->format('d/m/Y') . ' al ' . $fechaFin->format('d/m/Y');
|
||||||
|
|
||||||
|
$sheet->setCellValue("A{$row}", 'PERÍODO:');
|
||||||
|
$sheet->getStyle("A{$row}")->getFont()->setBold(true);
|
||||||
|
$sheet->mergeCells("B{$row}:{$lastCol}{$row}");
|
||||||
|
$sheet->setCellValue("B{$row}", $periodoTexto);
|
||||||
|
$sheet->getStyle("B{$row}:{$lastCol}{$row}")->applyFromArray([
|
||||||
|
'borders' => ['outline' => ['borderStyle' => Border::BORDER_THIN]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ENCABEZADOS
|
||||||
|
$row += 2;
|
||||||
|
$headerRow = $row;
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'A' => 'FECHA',
|
||||||
|
'B' => 'PRODUCTO',
|
||||||
|
'C' => 'TIPO',
|
||||||
|
'D' => "ALMACÉN\nORIGEN",
|
||||||
|
'E' => "ALMACÉN\nDESTINO",
|
||||||
|
'F' => 'CANTIDAD',
|
||||||
|
'G' => "COSTO\nUNITARIO",
|
||||||
|
'H' => 'REFERENCIA',
|
||||||
|
'I' => 'NOTAS',
|
||||||
|
'J' => 'USUARIO',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($headers as $col => $text) {
|
||||||
|
$sheet->setCellValue("{$col}{$row}", $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray($styleTableHeader);
|
||||||
|
$sheet->getRowDimension($row)->setRowHeight(30);
|
||||||
|
|
||||||
|
// DATOS
|
||||||
|
$row++;
|
||||||
|
foreach ($movements as $movement) {
|
||||||
|
$sheet->setCellValue("A{$row}", $movement->created_at->format('d/m/Y H:i'));
|
||||||
|
$sheet->setCellValue("B{$row}", $movement->inventory?->name ?? 'N/A');
|
||||||
|
$sheet->setCellValue("C{$row}", $this->translateMovementType($movement->movement_type));
|
||||||
|
$sheet->setCellValue("D{$row}", $movement->warehouseFrom?->name ?? '-');
|
||||||
|
$sheet->setCellValue("E{$row}", $movement->warehouseTo?->name ?? '-');
|
||||||
|
$sheet->setCellValue("F{$row}", $movement->quantity);
|
||||||
|
$sheet->setCellValue("G{$row}", $movement->unit_cost ? '$' . number_format($movement->unit_cost, 2) : '');
|
||||||
|
$sheet->setCellValue("H{$row}", $movement->invoice_reference ?? '');
|
||||||
|
$sheet->setCellValue("I{$row}", $movement->notes ?? '');
|
||||||
|
$sheet->setCellValue("J{$row}", $movement->user?->name ?? '');
|
||||||
|
|
||||||
|
// Estilo de fila
|
||||||
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([
|
||||||
|
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||||
|
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||||
|
'font' => ['size' => 10],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Color alterno
|
||||||
|
if (($row - $headerRow) % 2 === 0) {
|
||||||
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([
|
||||||
|
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'D9E2F3']],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANCHOS DE COLUMNA
|
||||||
|
$sheet->getColumnDimension('A')->setWidth(18);
|
||||||
|
$sheet->getColumnDimension('B')->setWidth(30);
|
||||||
|
$sheet->getColumnDimension('C')->setWidth(14);
|
||||||
|
$sheet->getColumnDimension('D')->setWidth(18);
|
||||||
|
$sheet->getColumnDimension('E')->setWidth(18);
|
||||||
|
$sheet->getColumnDimension('F')->setWidth(12);
|
||||||
|
$sheet->getColumnDimension('G')->setWidth(14);
|
||||||
|
$sheet->getColumnDimension('H')->setWidth(18);
|
||||||
|
$sheet->getColumnDimension('I')->setWidth(25);
|
||||||
|
$sheet->getColumnDimension('J')->setWidth(16);
|
||||||
|
|
||||||
|
// GUARDAR Y DESCARGAR
|
||||||
|
$writer = new Xlsx($spreadsheet);
|
||||||
|
$writer->save($filePath);
|
||||||
|
|
||||||
|
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
app/Http/Requests/App/InventoryMovementUpdateRequest.php
Normal file
47
app/Http/Requests/App/InventoryMovementUpdateRequest.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\App;
|
||||||
|
|
||||||
|
use App\Models\InventoryMovement;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class InventoryMovementUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'quantity' => 'sometimes|integer|min:1',
|
||||||
|
'unit_cost' => 'sometimes|numeric|min:0',
|
||||||
|
'warehouse_from_id' => 'sometimes|nullable|exists:warehouses,id',
|
||||||
|
'warehouse_to_id' => 'sometimes|nullable|exists:warehouses,id',
|
||||||
|
'invoice_reference' => 'sometimes|nullable|string|max:255',
|
||||||
|
'notes' => 'sometimes|nullable|string|max:500',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'quantity.required' => 'La cantidad es requerida',
|
||||||
|
'quantity.integer' => 'La cantidad debe ser un número entero',
|
||||||
|
'quantity.min' => 'La cantidad debe ser al menos 1',
|
||||||
|
'unit_cost.required' => 'El costo unitario es requerido',
|
||||||
|
'unit_cost.numeric' => 'El costo unitario debe ser un número',
|
||||||
|
'unit_cost.min' => 'El costo unitario no puede ser negativo',
|
||||||
|
'warehouse_from_id.required' => 'El almacén origen es requerido',
|
||||||
|
'warehouse_from_id.exists' => 'El almacén origen no existe',
|
||||||
|
'warehouse_from_id.different' => 'El almacén origen debe ser diferente al destino',
|
||||||
|
'warehouse_to_id.required' => 'El almacén destino es requerido',
|
||||||
|
'warehouse_to_id.exists' => 'El almacén destino no existe',
|
||||||
|
'warehouse_to_id.different' => 'El almacén destino debe ser diferente al origen',
|
||||||
|
'notes.max' => 'Las notas no pueden exceder 1000 caracteres',
|
||||||
|
'invoice_reference.max' => 'La referencia de factura no puede exceder 255 caracteres',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -372,6 +372,175 @@ public function getMainWarehouseId(): int
|
|||||||
return $warehouse->id;
|
return $warehouse->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualizar un movimiento existente
|
||||||
|
*/
|
||||||
|
public function updateMovement(int $movementId, array $data): InventoryMovement
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($movementId, $data) {
|
||||||
|
$movement = InventoryMovement::findOrFail($movementId);
|
||||||
|
|
||||||
|
// No permitir editar movimientos de venta o devolución (son automáticos)
|
||||||
|
if (in_array($movement->movement_type, ['sale', 'return'])) {
|
||||||
|
throw new \Exception('No se pueden editar movimientos de venta o devolución.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Revertir el movimiento original
|
||||||
|
$this->revertMovement($movement);
|
||||||
|
|
||||||
|
// 2. Preparar datos completos para actualización según tipo
|
||||||
|
$updateData = $this->prepareUpdateData($movement, $data);
|
||||||
|
|
||||||
|
// 3. Aplicar el nuevo movimiento según el tipo
|
||||||
|
switch ($movement->movement_type) {
|
||||||
|
case 'entry':
|
||||||
|
$this->applyEntryUpdate($movement, $updateData);
|
||||||
|
break;
|
||||||
|
case 'exit':
|
||||||
|
$this->applyExitUpdate($movement, $updateData);
|
||||||
|
break;
|
||||||
|
case 'transfer':
|
||||||
|
$this->applyTransferUpdate($movement, $updateData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Actualizar el registro del movimiento
|
||||||
|
$movement->update($updateData);
|
||||||
|
|
||||||
|
return $movement->load(['inventory', 'warehouseFrom', 'warehouseTo', 'user']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preparar datos completos para actualización
|
||||||
|
*/
|
||||||
|
protected function prepareUpdateData(InventoryMovement $movement, array $data): array
|
||||||
|
{
|
||||||
|
$updateData = [
|
||||||
|
'quantity' => $data['quantity'] ?? $movement->quantity,
|
||||||
|
'notes' => $data['notes'] ?? $movement->notes,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Campos específicos por tipo de movimiento
|
||||||
|
if ($movement->movement_type === 'entry') {
|
||||||
|
$updateData['unit_cost'] = $data['unit_cost'] ?? $movement->unit_cost;
|
||||||
|
$updateData['invoice_reference'] = $data['invoice_reference'] ?? $movement->invoice_reference;
|
||||||
|
$updateData['warehouse_to_id'] = $data['warehouse_to_id'] ?? $movement->warehouse_to_id;
|
||||||
|
} elseif ($movement->movement_type === 'exit') {
|
||||||
|
$updateData['warehouse_from_id'] = $data['warehouse_from_id'] ?? $movement->warehouse_from_id;
|
||||||
|
} elseif ($movement->movement_type === 'transfer') {
|
||||||
|
$updateData['warehouse_from_id'] = $data['warehouse_from_id'] ?? $movement->warehouse_from_id;
|
||||||
|
$updateData['warehouse_to_id'] = $data['warehouse_to_id'] ?? $movement->warehouse_to_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updateData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revertir el impacto de un movimiento en el stock
|
||||||
|
*/
|
||||||
|
protected function revertMovement(InventoryMovement $movement): void
|
||||||
|
{
|
||||||
|
switch ($movement->movement_type) {
|
||||||
|
case 'entry':
|
||||||
|
// Revertir entrada: restar del almacén destino
|
||||||
|
$this->updateWarehouseStock(
|
||||||
|
$movement->inventory_id,
|
||||||
|
$movement->warehouse_to_id,
|
||||||
|
-$movement->quantity
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'exit':
|
||||||
|
// Revertir salida: devolver al almacén origen
|
||||||
|
$this->updateWarehouseStock(
|
||||||
|
$movement->inventory_id,
|
||||||
|
$movement->warehouse_from_id,
|
||||||
|
$movement->quantity
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'transfer':
|
||||||
|
// Revertir traspaso: devolver al origen, quitar del destino
|
||||||
|
$this->updateWarehouseStock(
|
||||||
|
$movement->inventory_id,
|
||||||
|
$movement->warehouse_from_id,
|
||||||
|
$movement->quantity
|
||||||
|
);
|
||||||
|
$this->updateWarehouseStock(
|
||||||
|
$movement->inventory_id,
|
||||||
|
$movement->warehouse_to_id,
|
||||||
|
-$movement->quantity
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aplicar actualización de entrada
|
||||||
|
*/
|
||||||
|
protected function applyEntryUpdate(InventoryMovement $movement, array $data): void
|
||||||
|
{
|
||||||
|
$inventory = $movement->inventory;
|
||||||
|
$warehouseToId = $data['warehouse_to_id'] ?? $movement->warehouse_to_id;
|
||||||
|
$quantity = $data['quantity'] ?? $movement->quantity;
|
||||||
|
$unitCost = $data['unit_cost'] ?? $movement->unit_cost;
|
||||||
|
|
||||||
|
// Recalcular costo promedio ponderado
|
||||||
|
$currentStock = $inventory->stock;
|
||||||
|
$currentCost = $inventory->price?->cost ?? 0.00;
|
||||||
|
|
||||||
|
$newCost = $this->calculateWeightedAverageCost(
|
||||||
|
$currentStock,
|
||||||
|
$currentCost,
|
||||||
|
$quantity,
|
||||||
|
$unitCost
|
||||||
|
);
|
||||||
|
|
||||||
|
// Actualizar costo
|
||||||
|
$this->updateProductCost($inventory, $newCost);
|
||||||
|
|
||||||
|
// Aplicar nuevo stock
|
||||||
|
$this->updateWarehouseStock($inventory->id, $warehouseToId, $quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aplicar actualización de salida
|
||||||
|
*/
|
||||||
|
protected function applyExitUpdate(InventoryMovement $movement, array $data): void
|
||||||
|
{
|
||||||
|
$warehouseFromId = $data['warehouse_from_id'] ?? $movement->warehouse_from_id;
|
||||||
|
$quantity = $data['quantity'] ?? $movement->quantity;
|
||||||
|
|
||||||
|
// Validar stock disponible
|
||||||
|
$this->validateStock($movement->inventory_id, $warehouseFromId, $quantity);
|
||||||
|
|
||||||
|
// Aplicar nueva salida
|
||||||
|
$this->updateWarehouseStock($movement->inventory_id, $warehouseFromId, -$quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aplicar actualización de traspaso
|
||||||
|
*/
|
||||||
|
protected function applyTransferUpdate(InventoryMovement $movement, array $data): void
|
||||||
|
{
|
||||||
|
$warehouseFromId = $data['warehouse_from_id'] ?? $movement->warehouse_from_id;
|
||||||
|
$warehouseToId = $data['warehouse_to_id'] ?? $movement->warehouse_to_id;
|
||||||
|
$quantity = $data['quantity'] ?? $movement->quantity;
|
||||||
|
|
||||||
|
// Validar que no sea el mismo almacén
|
||||||
|
if ($warehouseFromId === $warehouseToId) {
|
||||||
|
throw new \Exception('No se puede traspasar al mismo almacén.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar stock disponible en origen
|
||||||
|
$this->validateStock($movement->inventory_id, $warehouseFromId, $quantity);
|
||||||
|
|
||||||
|
// Aplicar nuevo traspaso
|
||||||
|
$this->updateWarehouseStock($movement->inventory_id, $warehouseFromId, -$quantity);
|
||||||
|
$this->updateWarehouseStock($movement->inventory_id, $warehouseToId, $quantity);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sincronizar stock en inventory_warehouse basado en seriales disponibles
|
* Sincronizar stock en inventory_warehouse basado en seriales disponibles
|
||||||
* Solo aplica para productos con track_serials = true
|
* Solo aplica para productos con track_serials = true
|
||||||
|
|||||||
@ -53,6 +53,7 @@
|
|||||||
Route::prefix('movimientos')->group(function () {
|
Route::prefix('movimientos')->group(function () {
|
||||||
Route::get('/', [InventoryMovementController::class, 'index']);
|
Route::get('/', [InventoryMovementController::class, 'index']);
|
||||||
Route::get('/{id}', [InventoryMovementController::class, 'show']);
|
Route::get('/{id}', [InventoryMovementController::class, 'show']);
|
||||||
|
Route::put('/{id}', [InventoryMovementController::class, 'update']);
|
||||||
Route::post('/entrada', [InventoryMovementController::class, 'entry']);
|
Route::post('/entrada', [InventoryMovementController::class, 'entry']);
|
||||||
Route::post('/salida', [InventoryMovementController::class, 'exit']);
|
Route::post('/salida', [InventoryMovementController::class, 'exit']);
|
||||||
Route::post('/traspaso', [InventoryMovementController::class, 'transfer']);
|
Route::post('/traspaso', [InventoryMovementController::class, 'transfer']);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user