296 lines
10 KiB
PHP
296 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Imports;
|
|
|
|
use App\Models\Producto;
|
|
use App\Models\ProductVariant;
|
|
use App\Models\Bodega;
|
|
use Maatwebsite\Excel\Concerns\ToCollection;
|
|
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
|
use Maatwebsite\Excel\Concerns\WithValidation;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class CompraDetallesImport implements ToCollection, WithHeadingRow, WithValidation
|
|
{
|
|
protected $previewData = [];
|
|
protected $errors = [];
|
|
|
|
/**
|
|
* Procesa la colección y genera vista preliminar
|
|
*/
|
|
public function collection(Collection $rows)
|
|
{
|
|
$this->previewData = [];
|
|
$rowNumber = 2; // Comenzar en 2 porque fila 1 son los encabezados
|
|
|
|
foreach ($rows as $row) {
|
|
$detalle = $this->processRow($row->toArray(), $rowNumber);
|
|
$this->previewData[] = $detalle;
|
|
$rowNumber++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Procesa una fila individual
|
|
*/
|
|
private function processRow(array $row, int $rowNumber): array
|
|
{
|
|
$result = [
|
|
'row_number' => $rowNumber,
|
|
'valid' => true,
|
|
'errors' => [],
|
|
'data' => [],
|
|
'producto_creado' => false,
|
|
];
|
|
|
|
// Normalizar nombres de columnas
|
|
$row = $this->normalizeKeys($row);
|
|
|
|
// Buscar o crear producto
|
|
$producto = null;
|
|
$variante = null;
|
|
$productoCreado = false;
|
|
|
|
// 1. Primero intentar buscar por código de barras
|
|
if (!empty($row['codigo_barras'])) {
|
|
// Buscar variante primero
|
|
$variante = ProductVariant::where('barcode', $row['codigo_barras'])->first();
|
|
if ($variante) {
|
|
$producto = $variante->producto;
|
|
} else {
|
|
// Buscar producto por código
|
|
$producto = Producto::where('codigo_barras', $row['codigo_barras'])->first();
|
|
}
|
|
}
|
|
|
|
// 2. Si no se encontró por código, buscar por nombre
|
|
if (!$producto && !empty($row['producto'])) {
|
|
$producto = Producto::where('nombre', 'like', '%' . trim($row['producto']) . '%')->first();
|
|
}
|
|
|
|
// 3. Si no existe, crear el producto
|
|
if (!$producto) {
|
|
// Validar que tenga al menos nombre o código
|
|
if (empty($row['producto']) && empty($row['codigo_barras'])) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Debe proporcionar código de barras o nombre del producto';
|
|
} else {
|
|
$nombreProducto = !empty($row['producto']) ? trim($row['producto']) : 'Producto ' . $row['codigo_barras'];
|
|
$precioUnitario = isset($row['precio_unitario']) ? (float) $row['precio_unitario'] : 0;
|
|
|
|
if ($precioUnitario <= 0) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Precio debe ser mayor a 0 para crear producto';
|
|
} else {
|
|
// Buscar o crear categoría "Importados"
|
|
$categoria = \App\Models\Categoria::firstOrCreate(
|
|
['nombre' => 'Importados'],
|
|
['descripcion' => 'Productos creados automáticamente durante importación de compras']
|
|
);
|
|
|
|
// Generar código de barras único si no se proporcionó
|
|
$codigoBarras = !empty($row['codigo_barras']) ? $row['codigo_barras'] : null;
|
|
|
|
if (!$codigoBarras) {
|
|
// Generar código único: IMP + timestamp + random
|
|
do {
|
|
$codigoBarras = 'IMP' . time() . rand(100, 999);
|
|
} while (Producto::where('codigo_barras', $codigoBarras)->exists());
|
|
}
|
|
|
|
$producto = Producto::create([
|
|
'nombre' => $nombreProducto,
|
|
'descripcion' => 'Creado automáticamente desde importación',
|
|
'codigo_barras' => $codigoBarras,
|
|
'categoria_id' => $categoria->id,
|
|
'precio_compra' => $precioUnitario,
|
|
'precio_venta' => round($precioUnitario * 1.3, 2), // 30% de margen por defecto
|
|
'unidad_medida' => 'unidad',
|
|
'estado' => true,
|
|
'imagen' => '',
|
|
]);
|
|
|
|
$productoCreado = true;
|
|
$result['producto_creado'] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Buscar bodega
|
|
$bodega = null;
|
|
if (!empty($row['bodega'])) {
|
|
$bodega = Bodega::where('nombre', 'like', '%' . $row['bodega'] . '%')->first();
|
|
} else {
|
|
$bodega = Bodega::where('nombre', 'Principal')->first();
|
|
}
|
|
|
|
if (!$bodega) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Bodega no encontrada';
|
|
}
|
|
|
|
// Validar cantidad
|
|
$cantidad = isset($row['cantidad']) ? (int) $row['cantidad'] : 1;
|
|
if ($cantidad <= 0) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Cantidad debe ser mayor a 0';
|
|
}
|
|
|
|
// Validar precio
|
|
$precio = isset($row['precio_unitario']) ? (float) $row['precio_unitario'] : 0;
|
|
if ($precio <= 0) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Precio debe ser mayor a 0';
|
|
}
|
|
|
|
// Calcular subtotal
|
|
$subtotal = $cantidad * $precio;
|
|
|
|
// Preparar datos
|
|
$result['data'] = [
|
|
'codigo_barras' => $row['codigo_barras'] ?? '',
|
|
'producto_nombre_excel' => $row['producto'] ?? '',
|
|
'bodega_id' => $bodega?->id,
|
|
'bodega_nombre' => $bodega?->nombre ?? 'No encontrada',
|
|
'producto_id' => $producto?->id,
|
|
'producto_nombre' => $producto?->nombre ?? 'No encontrado',
|
|
'variante_id' => $variante?->id,
|
|
'variante_info' => $variante ? $this->getVariantInfo($variante) : null,
|
|
'cantidad' => $cantidad,
|
|
'precio_unitario' => $precio,
|
|
'subtotal' => $subtotal,
|
|
'DetalleCompra' => $row['observaciones'] ?? $row['detalle'] ?? '',
|
|
];
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Obtiene información formateada de la variante
|
|
*/
|
|
private function getVariantInfo($variante): string
|
|
{
|
|
$colorName = $variante->color?->name ?? 'Sin color';
|
|
$sizeName = $variante->size?->name ?? 'Sin talla';
|
|
return "$colorName / $sizeName";
|
|
}
|
|
|
|
/**
|
|
* Normaliza las claves del array
|
|
*/
|
|
private function normalizeKeys(array $row): array
|
|
{
|
|
$normalized = [];
|
|
$mappings = [
|
|
'codigo_de_barras' => 'codigo_barras',
|
|
'codigo' => 'codigo_barras',
|
|
'barcode' => 'codigo_barras',
|
|
'precio' => 'precio_unitario',
|
|
'precio_unit' => 'precio_unitario',
|
|
'observacion' => 'observaciones',
|
|
'detalle' => 'observaciones',
|
|
'cantidad' => 'cantidad',
|
|
'cant' => 'cantidad',
|
|
'qty' => 'cantidad',
|
|
'producto' => 'producto',
|
|
'nombre' => 'producto',
|
|
'nombre_producto' => 'producto',
|
|
'articulo' => 'producto',
|
|
'bodega' => 'bodega',
|
|
];
|
|
|
|
foreach ($row as $key => $value) {
|
|
$normalizedKey = strtolower(trim($key));
|
|
$normalizedKey = str_replace(' ', '_', $normalizedKey);
|
|
$normalizedKey = $this->removeAccents($normalizedKey);
|
|
|
|
if (isset($mappings[$normalizedKey])) {
|
|
$normalized[$mappings[$normalizedKey]] = $value;
|
|
} else {
|
|
$normalized[$normalizedKey] = $value;
|
|
}
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
/**
|
|
* Elimina tildes
|
|
*/
|
|
private function removeAccents($string)
|
|
{
|
|
$unwanted = [
|
|
'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u',
|
|
'Á' => 'A', 'É' => 'E', 'Í' => 'I', 'Ó' => 'O', 'Ú' => 'U',
|
|
'ñ' => 'n', 'Ñ' => 'N',
|
|
];
|
|
return strtr($string, $unwanted);
|
|
}
|
|
|
|
/**
|
|
* Reglas de validación
|
|
*/
|
|
public function rules(): array
|
|
{
|
|
return [
|
|
'codigo_barras' => 'nullable|string',
|
|
'producto' => 'nullable|string',
|
|
'bodega' => 'nullable|string',
|
|
'cantidad' => 'nullable|integer|min:1',
|
|
'precio_unitario' => 'nullable|numeric|min:0',
|
|
'observaciones' => 'nullable|string',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Obtiene los datos de vista preliminar
|
|
*/
|
|
public function getPreviewData(): array
|
|
{
|
|
return $this->previewData;
|
|
}
|
|
|
|
/**
|
|
* Obtiene estadísticas de la importación
|
|
*/
|
|
public function getStats(): array
|
|
{
|
|
$valid = collect($this->previewData)->filter(fn($item) => $item['valid'])->count();
|
|
$invalid = collect($this->previewData)->filter(fn($item) => !$item['valid'])->count();
|
|
$productosCreados = collect($this->previewData)->filter(fn($item) => isset($item['producto_creado']) && $item['producto_creado'])->count();
|
|
$total = count($this->previewData);
|
|
$totalAmount = collect($this->previewData)
|
|
->where('valid', true)
|
|
->sum(fn($item) => $item['data']['subtotal']);
|
|
|
|
return [
|
|
'total' => $total,
|
|
'valid' => $valid,
|
|
'invalid' => $invalid,
|
|
'productos_creados' => $productosCreados,
|
|
'total_amount' => $totalAmount,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Convierte los datos preliminares a formato para guardar
|
|
*/
|
|
public function getDetallesForSave(): array
|
|
{
|
|
return collect($this->previewData)
|
|
->filter(fn($item) => $item['valid'])
|
|
->map(fn($item) => [
|
|
'bodega_id' => $item['data']['bodega_id'],
|
|
'producto_id' => $item['data']['producto_id'],
|
|
'variante_id' => $item['data']['variante_id'],
|
|
'cantidad' => $item['data']['cantidad'],
|
|
'precio_unitario' => $item['data']['precio_unitario'],
|
|
'subtotal' => $item['data']['subtotal'],
|
|
'DetalleCompra' => $item['data']['DetalleCompra'],
|
|
'producto_nombre_snapshot' => $item['data']['producto_nombre'],
|
|
'variante_info_snapshot' => $item['data']['variante_info'],
|
|
])
|
|
->toArray();
|
|
}
|
|
}
|