- Migrar manejo de stock de a . - Implementar para centralizar lógica de entradas, salidas y transferencias. - Añadir (CRUD) y Requests de validación. - Actualizar reportes, cálculos de valor y migraciones para la nueva estructura. - Agregar campo para rastreo de movimientos.
258 lines
8.6 KiB
PHP
258 lines
8.6 KiB
PHP
<?php
|
|
|
|
namespace App\Imports;
|
|
|
|
use App\Models\Inventory;
|
|
use App\Models\Price;
|
|
use App\Models\Category;
|
|
use App\Http\Requests\App\InventoryImportRequest;
|
|
use App\Models\InventorySerial;
|
|
use App\Services\InventoryMovementService;
|
|
use Maatwebsite\Excel\Concerns\ToModel;
|
|
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
|
use Maatwebsite\Excel\Concerns\WithValidation;
|
|
use Maatwebsite\Excel\Concerns\Importable;
|
|
use Maatwebsite\Excel\Concerns\WithChunkReading;
|
|
use Maatwebsite\Excel\Concerns\SkipsEmptyRows;
|
|
use Maatwebsite\Excel\Concerns\WithMapping;
|
|
|
|
/**
|
|
* Import de productos desde Excel
|
|
*
|
|
* Formato esperado del Excel:
|
|
* - nombre: Nombre del producto (requerido)
|
|
* - sku: Código SKU (opcional, único)
|
|
* - categoria: Nombre de la categoría (opcional)
|
|
* - stock: Cantidad inicial (requerido, mínimo 0)
|
|
* - costo: Precio de costo (requerido, mínimo 0)
|
|
* - precio_venta: Precio de venta (requerido, mayor que costo)
|
|
* - impuesto: Porcentaje de impuesto (opcional, 0-100)
|
|
*/
|
|
class ProductsImport implements ToModel, WithHeadingRow, WithValidation, WithChunkReading, SkipsEmptyRows, WithMapping
|
|
{
|
|
use Importable;
|
|
|
|
private $errors = [];
|
|
private $imported = 0;
|
|
private $updated = 0;
|
|
private $skipped = 0;
|
|
private InventoryMovementService $movementService;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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 seriales
|
|
if ($existingInventory) {
|
|
return $this->updateExistingProduct($existingInventory, $row);
|
|
}
|
|
|
|
// Producto nuevo: validar precios
|
|
$costo = (float) $row['costo'];
|
|
$precioVenta = (float) $row['precio_venta'];
|
|
|
|
if ($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,
|
|
]);
|
|
|
|
// Crear números de serie si se proporcionan
|
|
$this->addSerials($inventory, $row['numeros_serie'] ?? null, $row['stock'] ?? 0);
|
|
|
|
$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 agrega seriales nuevos
|
|
*/
|
|
private function updateExistingProduct(Inventory $inventory, array $row)
|
|
{
|
|
$serialsAdded = 0;
|
|
$serialsSkipped = 0;
|
|
$mainWarehouseId = $this->movementService->getMainWarehouseId();
|
|
|
|
// 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,
|
|
'warehouse_id' => $mainWarehouseId,
|
|
'serial_number' => $serial,
|
|
'status' => 'disponible',
|
|
]);
|
|
$serialsAdded++;
|
|
} else {
|
|
$serialsSkipped++;
|
|
}
|
|
}
|
|
|
|
// Sincronizar stock basado en seriales disponibles
|
|
$inventory->syncStock();
|
|
} else {
|
|
// Producto sin seriales: sumar stock en almacén principal
|
|
$stockToAdd = (int) ($row['stock'] ?? 0);
|
|
if ($stockToAdd > 0) {
|
|
$this->movementService->updateWarehouseStock($inventory->id, $mainWarehouseId, $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)
|
|
{
|
|
$mainWarehouseId = $this->movementService->getMainWarehouseId();
|
|
|
|
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',
|
|
]);
|
|
}
|
|
}
|
|
$inventory->syncStock();
|
|
} else {
|
|
// Producto sin seriales: registrar stock en almacén principal
|
|
if ($stockFromExcel > 0) {
|
|
$this->movementService->updateWarehouseStock($inventory->id, $mainWarehouseId, $stockFromExcel);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
];
|
|
}
|
|
}
|