diff --git a/app/Http/Controllers/App/InventoryMovementController.php b/app/Http/Controllers/App/InventoryMovementController.php index f7a77a4..dcd36fd 100644 --- a/app/Http/Controllers/App/InventoryMovementController.php +++ b/app/Http/Controllers/App/InventoryMovementController.php @@ -3,10 +3,12 @@ use App\Http\Controllers\Controller; use App\Http\Requests\App\InventoryEntryRequest; use App\Http\Requests\App\InventoryExitRequest; +use App\Http\Requests\App\InventoryMovementUpdateRequest; use App\Http\Requests\App\InventoryTransferRequest; use App\Models\InventoryMovement; use App\Services\InventoryMovementService; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; 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 */ diff --git a/app/Http/Controllers/App/KardexController.php b/app/Http/Controllers/App/KardexController.php index cd81412..d0c637a 100644 --- a/app/Http/Controllers/App/KardexController.php +++ b/app/Http/Controllers/App/KardexController.php @@ -18,22 +18,27 @@ 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) { // 1. VALIDACIÓN $request->validate([ - 'inventory_id' => 'required|exists:inventories,id', - 'warehouse_id' => 'sometimes|nullable|exists:warehouses,id', + 'inventory_id' => 'nullable|exists:inventories,id', + 'warehouse_id' => 'nullable|exists:warehouses,id', 'fecha_inicio' => 'required|date', '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(); $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); // 2. CONSULTA DE MOVIMIENTOS @@ -58,27 +63,16 @@ public function export(Request $request) $movements = $query->get(); - // 3. CALCULAR SALDO INICIAL (movimientos previos al rango) - $initialBalance = $this->calculateInitialBalance($inventory->id, $fechaInicio, $warehouseId); - - // 4. CONSTRUIR LÍNEAS DEL KARDEX - $balance = $initialBalance; + // 3. CONSTRUIR LÍNEAS DEL KARDEX $kardexLines = []; foreach ($movements as $movement) { - $effect = $this->calculateMovementEffect($movement, $warehouseId); - $entryQty = $effect > 0 ? $effect : 0; - $exitQty = $effect < 0 ? abs($effect) : 0; - $balance += $effect; - $kardexLines[] = [ 'date' => $movement->created_at->format('d/m/Y H:i'), 'type' => $this->translateMovementType($movement->movement_type), 'warehouse_from' => $movement->warehouseFrom?->name ?? '-', 'warehouse_to' => $movement->warehouseTo?->name ?? '-', - 'entry' => $entryQty, - 'exit' => $exitQty, - 'balance' => $balance, + 'quantity' => $movement->quantity, 'unit_cost' => $movement->unit_cost ?? 0, 'notes' => $movement->notes ?? '', 'invoice_reference' => $movement->invoice_reference ?? '', @@ -120,7 +114,7 @@ public function export(Request $request) 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]; - $lastCol = 'K'; + $lastCol = 'J'; // --- TÍTULO --- $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}")->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 --- $row += 2; $headerRow = $row; @@ -193,13 +178,11 @@ public function export(Request $request) 'B' => 'TIPO', 'C' => "ALMACÉN\nORIGEN", 'D' => "ALMACÉN\nDESTINO", - 'E' => 'ENTRADA', - 'F' => 'SALIDA', - 'G' => 'SALDO', - 'H' => "COSTO\nUNITARIO", - 'I' => 'REFERENCIA', - 'J' => 'NOTAS', - 'K' => 'USUARIO', + 'E' => 'CANTIDAD', + 'F' => "COSTO\nUNITARIO", + 'G' => 'REFERENCIA', + 'H' => 'NOTAS', + 'I' => 'USUARIO', ]; foreach ($headers as $col => $text) { @@ -211,24 +194,20 @@ public function export(Request $request) // --- DATOS --- $row++; - $totalEntries = 0; - $totalExits = 0; + $totalQuantity = 0; foreach ($kardexLines as $line) { $sheet->setCellValue("A{$row}", $line['date']); $sheet->setCellValue("B{$row}", $line['type']); $sheet->setCellValue("C{$row}", $line['warehouse_from']); $sheet->setCellValue("D{$row}", $line['warehouse_to']); - $sheet->setCellValue("E{$row}", $line['entry'] ?: ''); - $sheet->setCellValue("F{$row}", $line['exit'] ?: ''); - $sheet->setCellValue("G{$row}", $line['balance']); - $sheet->setCellValue("H{$row}", $line['unit_cost'] ? '$' . number_format($line['unit_cost'], 2) : ''); - $sheet->setCellValue("I{$row}", $line['invoice_reference']); - $sheet->setCellValue("J{$row}", $line['notes']); - $sheet->setCellValue("K{$row}", $line['user']); + $sheet->setCellValue("E{$row}", $line['quantity']); + $sheet->setCellValue("F{$row}", $line['unit_cost'] ? '$' . number_format($line['unit_cost'], 2) : ''); + $sheet->setCellValue("G{$row}", $line['invoice_reference']); + $sheet->setCellValue("H{$row}", $line['notes']); + $sheet->setCellValue("I{$row}", $line['user']); - $totalEntries += $line['entry']; - $totalExits += $line['exit']; + $totalQuantity += $line['quantity']; // Estilo de fila $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++; } // --- FILA DE TOTALES --- $sheet->mergeCells("A{$row}:D{$row}"); $sheet->setCellValue("A{$row}", 'TOTALES'); - $sheet->setCellValue("E{$row}", $totalEntries); - $sheet->setCellValue("F{$row}", $totalExits); - $sheet->setCellValue("G{$row}", $balance); + $sheet->setCellValue("E{$row}", $totalQuantity); $sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 10], @@ -278,12 +245,11 @@ public function export(Request $request) $sheet->getColumnDimension('C')->setWidth(18); $sheet->getColumnDimension('D')->setWidth(18); $sheet->getColumnDimension('E')->setWidth(12); - $sheet->getColumnDimension('F')->setWidth(12); - $sheet->getColumnDimension('G')->setWidth(12); - $sheet->getColumnDimension('H')->setWidth(14); - $sheet->getColumnDimension('I')->setWidth(18); - $sheet->getColumnDimension('J')->setWidth(25); - $sheet->getColumnDimension('K')->setWidth(16); + $sheet->getColumnDimension('F')->setWidth(14); + $sheet->getColumnDimension('G')->setWidth(18); + $sheet->getColumnDimension('H')->setWidth(25); + $sheet->getColumnDimension('I')->setWidth(16); + $sheet->getColumnDimension('J')->setWidth(16); // 6. GUARDAR Y DESCARGAR $writer = new Xlsx($spreadsheet); @@ -292,58 +258,6 @@ public function export(Request $request) 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 */ @@ -353,9 +267,153 @@ private function translateMovementType(string $type): string 'entry' => 'Entrada', 'exit' => 'Salida', 'transfer' => 'Traspaso', - 'sale' => 'Venta', - 'return' => 'Devolución', + /* 'sale' => 'Venta', + 'return' => 'Devolución', */ 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); + } } diff --git a/app/Http/Requests/App/InventoryMovementUpdateRequest.php b/app/Http/Requests/App/InventoryMovementUpdateRequest.php new file mode 100644 index 0000000..df49954 --- /dev/null +++ b/app/Http/Requests/App/InventoryMovementUpdateRequest.php @@ -0,0 +1,47 @@ + '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', + ]; + } +} diff --git a/app/Services/InventoryMovementService.php b/app/Services/InventoryMovementService.php index 15fae7e..10c36fd 100644 --- a/app/Services/InventoryMovementService.php +++ b/app/Services/InventoryMovementService.php @@ -372,6 +372,175 @@ public function getMainWarehouseId(): int 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 * Solo aplica para productos con track_serials = true diff --git a/routes/api.php b/routes/api.php index f1abd06..e5e0321 100644 --- a/routes/api.php +++ b/routes/api.php @@ -53,6 +53,7 @@ Route::prefix('movimientos')->group(function () { Route::get('/', [InventoryMovementController::class, 'index']); Route::get('/{id}', [InventoryMovementController::class, 'show']); + Route::put('/{id}', [InventoryMovementController::class, 'update']); Route::post('/entrada', [InventoryMovementController::class, 'entry']); Route::post('/salida', [InventoryMovementController::class, 'exit']); Route::post('/traspaso', [InventoryMovementController::class, 'transfer']);