movementService = app(InventoryMovementService::class); } /** * Mapea y transforma los datos de cada fila antes de la validación */ public function map($row): array { return [ 'nombre' => $row['nombre'] ?? null, 'sku' => isset($row['sku']) ? (string) $row['sku'] : null, 'codigo_barras' => isset($row['codigo_barras']) ? (string) $row['codigo_barras'] : null, 'categoria' => $row['categoria'] ?? null, 'stock' => $row['stock'] ?? null, 'costo' => $row['costo'] ?? null, 'precio_venta' => $row['precio_venta'] ?? null, 'impuesto' => $row['impuesto'] ?? null, 'numeros_serie' => $row['numeros_serie'] ?? null, ]; } /** * Procesa cada fila del Excel */ public function model(array $row) { // Ignorar filas completamente vacías if (empty($row['nombre']) && empty($row['sku']) && empty($row['stock'])) { return null; } try { // 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 costo if ($existingInventory) { return $this->updateExistingProduct($existingInventory, $row); } // Producto nuevo: obtener valores $costo = isset($row['costo']) && $row['costo'] !== '' ? (float) $row['costo'] : 0; $precioVenta = (float) $row['precio_venta']; // Validar precio > costo solo si costo > 0 if ($costo > 0 && $precioVenta <= $costo) { $this->skipped++; $this->errors[] = "Fila con producto '{$row['nombre']}': El precio de venta ($precioVenta) debe ser mayor que el costo ($costo)"; return null; } // Buscar o crear categoría si se proporciona $categoryId = null; if (!empty($row['categoria'])) { $category = Category::firstOrCreate( ['name' => trim($row['categoria'])], ['is_active' => true] ); $categoryId = $category->id; } // Crear el producto en inventario (sin stock, vive en inventory_warehouse) $inventory = new Inventory(); $inventory->name = trim($row['nombre']); $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->is_active = true; $inventory->save(); // Crear el precio del producto Price::create([ 'inventory_id' => $inventory->id, 'cost' => $costo, 'retail_price' => $precioVenta, 'tax' => !empty($row['impuesto']) ? (float) $row['impuesto'] : 0, ]); // Agregar stock inicial si existe $stockFromExcel = (int) ($row['stock'] ?? 0); if ($stockFromExcel > 0) { $this->addStockWithCost( $inventory, $stockFromExcel, $costo, $row['numeros_serie'] ?? null ); } $this->imported++; return $inventory; } catch (\Exception $e) { $this->skipped++; $this->errors[] = "Error en fila: " . $e->getMessage(); return null; } } /** * Actualiza un producto existente: suma stock y actualiza costo */ private function updateExistingProduct(Inventory $inventory, array $row) { $mainWarehouseId = $this->movementService->getMainWarehouseId(); $stockToAdd = (int) ($row['stock'] ?? 0); $costo = isset($row['costo']) && $row['costo'] !== '' ? (float) $row['costo'] : null; // Si hay stock para agregar if ($stockToAdd > 0) { // Si tiene números de serie if (!empty($row['numeros_serie'])) { $serials = explode(',', $row['numeros_serie']); $serialsAdded = 0; $serialsSkipped = 0; foreach ($serials as $serial) { $serial = trim($serial); if (empty($serial)) continue; // Verificar si el serial ya existe $exists = InventorySerial::where('serial_number', $serial)->exists(); if (!$exists) { InventorySerial::create([ 'inventory_id' => $inventory->id, 'warehouse_id' => $mainWarehouseId, 'serial_number' => $serial, 'status' => 'disponible', ]); $serialsAdded++; } else { $serialsSkipped++; } } // Sincronizar stock basado en seriales $inventory->syncStock(); if ($serialsSkipped > 0) { $this->errors[] = "Producto '{$inventory->name}': {$serialsSkipped} seriales ya existían y fueron ignorados"; } } // Registrar movimiento de entrada con costo si existe if ($costo !== null && $costo > 0) { $this->movementService->entry([ 'inventory_id' => $inventory->id, 'warehouse_id' => $mainWarehouseId, 'quantity' => $stockToAdd, 'unit_cost' => $costo, 'invoice_reference' => 'IMP-' . date('YmdHis'), 'notes' => 'Importación desde Excel - actualización', ]); } else { // Sin costo, solo agregar stock sin movimiento de entrada $this->movementService->updateWarehouseStock($inventory->id, $mainWarehouseId, $stockToAdd); } } $this->updated++; return null; // No retornar modelo para evitar que Maatwebsite intente guardarlo } /** * Agrega stock inicial a un producto nuevo con registro de movimiento */ private function addStockWithCost(Inventory $inventory, int $quantity, float $cost, ?string $serialsString): void { $mainWarehouseId = $this->movementService->getMainWarehouseId(); // Si tiene números de serie if (!empty($serialsString)) { $serials = explode(',', $serialsString); foreach ($serials as $serial) { $serial = trim($serial); if (!empty($serial)) { InventorySerial::create([ 'inventory_id' => $inventory->id, 'warehouse_id' => $mainWarehouseId, 'serial_number' => $serial, 'status' => 'disponible', ]); } } // Sincronizar stock basado en seriales $inventory->syncStock(); } // Registrar movimiento de entrada con costo si existe if ($cost > 0) { $this->movementService->entry([ 'inventory_id' => $inventory->id, 'warehouse_id' => $mainWarehouseId, 'quantity' => $quantity, 'unit_cost' => $cost, 'invoice_reference' => 'IMP-' . date('YmdHis'), 'notes' => 'Importación desde Excel - stock inicial', ]); } else { // Sin costo, solo agregar stock $this->movementService->updateWarehouseStock($inventory->id, $mainWarehouseId, $quantity); } } /** * Reglas de validación para cada fila */ public function rules(): array { return InventoryImportRequest::rowRules(); } /** * Mensajes personalizados de validación */ public function customValidationMessages() { return InventoryImportRequest::rowMessages(); } /** * Chunk size for reading */ public function chunkSize(): int { return 100; } /** * Obtener estadísticas de la importación */ public function getStats(): array { return [ 'imported' => $this->imported, 'updated' => $this->updated, 'skipped' => $this->skipped, 'errors' => $this->errors, ]; } }