diff --git a/app/Http/Controllers/App/InventoryController.php b/app/Http/Controllers/App/InventoryController.php index 07955d9..9a93c6e 100644 --- a/app/Http/Controllers/App/InventoryController.php +++ b/app/Http/Controllers/App/InventoryController.php @@ -90,6 +90,7 @@ public function import(InventoryImportRequest $request) return ApiResponse::OK->response([ 'message' => 'Importación completada exitosamente.', 'imported' => $stats['imported'], + 'updated' => $stats['updated'], 'skipped' => $stats['skipped'], 'errors' => $stats['errors'], ]); diff --git a/app/Http/Requests/App/InventoryImportRequest.php b/app/Http/Requests/App/InventoryImportRequest.php index d45b29f..7c9b2d2 100644 --- a/app/Http/Requests/App/InventoryImportRequest.php +++ b/app/Http/Requests/App/InventoryImportRequest.php @@ -44,13 +44,15 @@ public function messages(): array /** * Reglas de validación para cada fila del Excel + * Nota: SKU y código de barras no tienen 'unique' porque se permite reimportar + * para agregar stock/seriales a productos existentes */ public static function rowRules(): array { return [ 'nombre' => ['required', 'string', 'max:100'], - 'sku' => ['nullable', 'string', 'max:50', 'unique:inventories,sku'], - 'codigo_barras' => ['nullable', 'string', 'max:100', 'unique:inventories,barcode'], + 'sku' => ['nullable', 'string', 'max:50'], + 'codigo_barras' => ['nullable', 'string', 'max:100'], 'categoria' => ['nullable', 'string', 'max:100'], 'stock' => ['required', 'integer', 'min:0'], 'costo' => ['required', 'numeric', 'min:0'], @@ -67,9 +69,7 @@ public static function rowMessages(): array return [ 'nombre.required' => 'El nombre del producto es requerido.', 'nombre.max' => 'El nombre no debe exceder los 100 caracteres.', - 'sku.unique' => 'El SKU ya existe en el sistema.', 'sku.max' => 'El SKU no debe exceder los 50 caracteres.', - 'codigo_barras.unique' => 'El código de barras ya existe en el sistema.', 'stock.required' => 'El stock es requerido.', 'stock.integer' => 'El stock debe ser un número entero.', 'stock.min' => 'El stock no puede ser negativo.', diff --git a/app/Imports/ProductsImport.php b/app/Imports/ProductsImport.php index 108b6c9..bde158a 100644 --- a/app/Imports/ProductsImport.php +++ b/app/Imports/ProductsImport.php @@ -33,6 +33,7 @@ class ProductsImport implements ToModel, WithHeadingRow, WithValidation, WithChu private $errors = []; private $imported = 0; + private $updated = 0; private $skipped = 0; /** @@ -64,7 +65,21 @@ public function model(array $row) } try { - // Validar que el precio de venta sea mayor que el costo + // Buscar producto existente por SKU o código de barras + $existingInventory = null; + if (!empty($row['sku'])) { + $existingInventory = Inventory::where('sku', trim($row['sku']))->first(); + } + if (!$existingInventory && !empty($row['codigo_barras'])) { + $existingInventory = Inventory::where('barcode', trim($row['codigo_barras']))->first(); + } + + // Si el producto ya existe, solo agregar stock y seriales + if ($existingInventory) { + return $this->updateExistingProduct($existingInventory, $row); + } + + // Producto nuevo: validar precios $costo = (float) $row['costo']; $precioVenta = (float) $row['precio_venta']; @@ -90,7 +105,7 @@ public function model(array $row) $inventory->sku = !empty($row['sku']) ? trim($row['sku']) : null; $inventory->barcode = !empty($row['codigo_barras']) ? trim($row['codigo_barras']) : null; $inventory->category_id = $categoryId; - $inventory->stock = 0; // Se calculará automáticamente + $inventory->stock = 0; $inventory->is_active = true; $inventory->save(); @@ -103,28 +118,7 @@ public function model(array $row) ]); // Crear números de serie si se proporcionan - if (!empty($row['numeros_serie'])) { - $serials = explode(',', $row['numeros_serie']); - - foreach ($serials as $serial) { - $serial = trim($serial); - - if (!empty($serial)) { - InventorySerial::create([ - 'inventory_id' => $inventory->id, - 'serial_number' => $serial, - 'status' => 'disponible', - ]); - } - } - // Sincronizar stock basado en los seriales - $inventory->syncStock(); - } else { - // Si no se proporcionan seriales, es un producto no serializado. - // Asignar el stock directamente desde la columna 'stock'. - $inventory->stock = (int) ($row['stock'] ?? 0); - $inventory->save(); - } + $this->addSerials($inventory, $row['numeros_serie'] ?? null, $row['stock'] ?? 0); $this->imported++; @@ -136,6 +130,80 @@ public function model(array $row) } } + /** + * Actualiza un producto existente: suma stock y agrega seriales nuevos + */ + private function updateExistingProduct(Inventory $inventory, array $row) + { + $serialsAdded = 0; + $serialsSkipped = 0; + + // Agregar seriales nuevos (ignorar duplicados) + if (!empty($row['numeros_serie'])) { + $serials = explode(',', $row['numeros_serie']); + + foreach ($serials as $serial) { + $serial = trim($serial); + if (empty($serial)) continue; + + // Verificar si el serial ya existe (global, no solo en este producto) + $exists = InventorySerial::where('serial_number', $serial)->exists(); + + if (!$exists) { + InventorySerial::create([ + 'inventory_id' => $inventory->id, + 'serial_number' => $serial, + 'status' => 'disponible', + ]); + $serialsAdded++; + } else { + $serialsSkipped++; + } + } + + // Sincronizar stock basado en seriales disponibles + $inventory->syncStock(); + } else { + // Producto sin seriales: sumar stock + $stockToAdd = (int) ($row['stock'] ?? 0); + $inventory->increment('stock', $stockToAdd); + } + + $this->updated++; + + if ($serialsSkipped > 0) { + $this->errors[] = "Producto '{$inventory->name}': {$serialsSkipped} seriales ya existían y fueron ignorados"; + } + + return null; // No retornar modelo para evitar que Maatwebsite intente guardarlo + } + + /** + * Agrega seriales a un producto nuevo + */ + private function addSerials(Inventory $inventory, ?string $serialsString, int $stockFromExcel) + { + if (!empty($serialsString)) { + $serials = explode(',', $serialsString); + + foreach ($serials as $serial) { + $serial = trim($serial); + if (!empty($serial)) { + InventorySerial::create([ + 'inventory_id' => $inventory->id, + 'serial_number' => $serial, + 'status' => 'disponible', + ]); + } + } + $inventory->syncStock(); + } else { + // Producto sin seriales + $inventory->stock = $stockFromExcel; + $inventory->save(); + } + } + /** * Reglas de validación para cada fila */ @@ -167,6 +235,7 @@ public function getStats(): array { return [ 'imported' => $this->imported, + 'updated' => $this->updated, 'skipped' => $this->skipped, 'errors' => $this->errors, ]; diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index 0f2dae5..7fff133 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -166,6 +166,7 @@ public function run(): void $salesCreate, $salesCancel, $inventoryIndex, + $inventoryImport, $inventoryCreate, $inventoryEdit, $inventoryDestroy, @@ -190,8 +191,11 @@ public function run(): void $salesIndex, // Ver historial de ventas $salesCreate, // Crear ventas // Inventario (solo lectura) - $inventoryIndex, // Listar productos + $inventoryIndex, $inventoryImport, // Importar productos + $inventoryCreate, + $inventoryEdit, + $inventoryDestroy, // Clientes $clientIndex, // Buscar clientes );