From ce8a479097cac904a67e723b0894b237ec09c14c Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:21:11 -0500 Subject: [PATCH] up --- .../Resources/InventarioPrendaResource.php | 35 +++- .../Pages/CreateInventarioPrenda.php | 130 +++++++++++++ .../DistribucionesRelationManager.php | 44 +++++ app/Filament/Resources/TelaResource.php | 11 +- app/Models/InventarioPrenda.php | 5 + app/Models/InventarioPrendaDistribucion.php | 171 ++++++++++++++++++ ...inventario_prenda_distribuciones_table.php | 26 +++ tests/Feature/InventarioDistribucionTest.php | 81 +++++++++ 8 files changed, 495 insertions(+), 8 deletions(-) create mode 100644 app/Filament/Resources/InventarioPrendaResource/RelationManagers/DistribucionesRelationManager.php create mode 100644 app/Models/InventarioPrendaDistribucion.php create mode 100644 database/migrations/2026_01_19_103000_create_inventario_prenda_distribuciones_table.php create mode 100644 tests/Feature/InventarioDistribucionTest.php diff --git a/app/Filament/Resources/InventarioPrendaResource.php b/app/Filament/Resources/InventarioPrendaResource.php index cb28fdf..c7ae158 100644 --- a/app/Filament/Resources/InventarioPrendaResource.php +++ b/app/Filament/Resources/InventarioPrendaResource.php @@ -74,6 +74,39 @@ class InventarioPrendaResource extends Resource ->required(), ]) ->columns(2), + + Section::make('Distribución') + ->schema([ + \Filament\Forms\Components\Repeater::make('distribuciones') + ->label('Distribuciones por bodega / color / talla') + ->schema([ + Select::make('bodega_id') + ->label('Bodega') + ->relationship('bodega', 'nombre') + ->required(), + + Select::make('color_id') + ->label('Color') + ->relationship('color', 'nombre') + ->nullable(), + + Select::make('size_id') + ->label('Talla') + ->relationship('size', 'nombre') + ->nullable(), + + TextInput::make('cantidad') + ->label('Cantidad') + ->numeric() + ->required() + ->minValue(1), + ]) + ->columns(1) + ->dehydrated(false) + ->minItems(0) + ->helpMessage('Agrega una o varias filas para distribuir la cantidad terminada entre bodegas/variantes.'), + ]) + ->columns(1), ]); } @@ -118,7 +151,7 @@ class InventarioPrendaResource extends Resource public static function getRelations(): array { return [ - // + RelationManagers\DistribucionesRelationManager::class, ]; } diff --git a/app/Filament/Resources/InventarioPrendaResource/Pages/CreateInventarioPrenda.php b/app/Filament/Resources/InventarioPrendaResource/Pages/CreateInventarioPrenda.php index c42752d..726333a 100644 --- a/app/Filament/Resources/InventarioPrendaResource/Pages/CreateInventarioPrenda.php +++ b/app/Filament/Resources/InventarioPrendaResource/Pages/CreateInventarioPrenda.php @@ -6,7 +6,137 @@ use App\Filament\Resources\InventarioPrendaResource; use Filament\Actions; use Filament\Resources\Pages\CreateRecord; +use Illuminate\Support\Facades\DB; +use Illuminate\Validation\ValidationException; +use App\Models\ProductVariant; +use App\Models\InventarioPrendaDistribucion; + class CreateInventarioPrenda extends CreateRecord { protected static string $resource = InventarioPrendaResource::class; + + protected function handleRecordCreation(array $data): \Illuminate\Database\Eloquent\Model + { + $distribuciones = $data['distribuciones'] ?? []; + $cantidadTerminada = $data['cantidad_terminada'] ?? 0; + + // Validación: la suma de las distribuciones no puede exceder la cantidad terminada + $sum = 0; + foreach ($distribuciones as $d) { + $sum += intval($d['cantidad'] ?? 0); + } + + if ($sum > $cantidadTerminada) { + throw ValidationException::withMessages(['distribuciones' => 'La suma de las cantidades asignadas excede la cantidad terminada.']); + } + + // Remover distribuciones del payload antes de crear el registro principal + unset($data['distribuciones']); + + $record = parent::handleRecordCreation($data); + + // Procesar distribuciones y actualizar stock en bodegas + foreach ($distribuciones as $d) { + $cantidad = intval($d['cantidad'] ?? 0); + if ($cantidad <= 0) continue; + + // Crear registro de distribución + $created = InventarioPrendaDistribucion::create([ + 'inventario_prenda_id' => $record->id, + 'bodega_id' => $d['bodega_id'] ?? null, + 'color_id' => $d['color_id'] ?? null, + 'size_id' => $d['size_id'] ?? null, + 'cantidad' => $cantidad, + ]); + + // Actualizar stock en bodegas + $bodegaId = $d['bodega_id'] ?? null; + + // Si tiene color y talla -> variante + if (!empty($d['color_id']) && !empty($d['size_id'])) { + $productoId = $record->producto_id ?? ($record->ordenProduccion->producto_id ?? null); + + // Si no hay producto, intentar obtener por OP + if (! $productoId && $record->ordenProduccion) { + $productoId = $record->ordenProduccion->producto_id; + } + + if ($productoId) { + $variant = ProductVariant::firstOrCreate( + [ + 'producto_id' => $productoId, + 'color_id' => $d['color_id'], + 'size_id' => $d['size_id'], + ], + [ + 'stock' => 0, + 'sku' => null, + 'barcode' => null, + ] + ); + + $existing = DB::table('variante_bodega') + ->where('variante_id', $variant->id) + ->where('bodega_id', $bodegaId) + ->first(); + + if ($existing) { + DB::table('variante_bodega') + ->where('variante_id', $variant->id) + ->where('bodega_id', $bodegaId) + ->update([ + 'stock' => ($existing->stock ?? 0) + $cantidad, + 'updated_at' => now(), + ]); + } else { + DB::table('variante_bodega')->insert([ + 'variante_id' => $variant->id, + 'bodega_id' => $bodegaId, + 'stock' => $cantidad, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + // También incrementar stock total de la variante + $variant->increment('stock', $cantidad); + } + } else { + // Producto a nivel general + $productoId = $record->producto_id ?? ($record->ordenProduccion->producto_id ?? null); + if ($productoId) { + $existing = DB::table('producto_bodega') + ->where('producto_id', $productoId) + ->where('bodega_id', $bodegaId) + ->first(); + + if ($existing) { + DB::table('producto_bodega') + ->where('producto_id', $productoId) + ->where('bodega_id', $bodegaId) + ->update([ + 'stock' => ($existing->stock ?? 0) + $cantidad, + 'updated_at' => now(), + ]); + } else { + DB::table('producto_bodega')->insert([ + 'producto_id' => $productoId, + 'bodega_id' => $bodegaId, + 'stock' => $cantidad, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + // Incrementar stock total del producto si no tiene variantes + $prod = \App\Models\Producto::find($productoId); + if ($prod && ! $prod->variants()->exists()) { + $prod->increment('stock', $cantidad); + } + } + } + } + + return $record; + } } diff --git a/app/Filament/Resources/InventarioPrendaResource/RelationManagers/DistribucionesRelationManager.php b/app/Filament/Resources/InventarioPrendaResource/RelationManagers/DistribucionesRelationManager.php new file mode 100644 index 0000000..95ef6ff --- /dev/null +++ b/app/Filament/Resources/InventarioPrendaResource/RelationManagers/DistribucionesRelationManager.php @@ -0,0 +1,44 @@ +schema([ + Forms\Components\Select::make('bodega_id')->relationship('bodega', 'nombre')->required(), + Forms\Components\Select::make('color_id')->relationship('color', 'nombre')->nullable(), + Forms\Components\Select::make('size_id')->relationship('size', 'nombre')->nullable(), + Forms\Components\TextInput::make('cantidad')->numeric()->required()->minValue(1), + ]); + } + + public function table(Table $table): Table + { + return $table->columns([ + Tables\Columns\TextColumn::make('bodega.nombre')->label('Bodega'), + Tables\Columns\TextColumn::make('color.nombre')->label('Color'), + Tables\Columns\TextColumn::make('size.nombre')->label('Talla'), + Tables\Columns\TextColumn::make('cantidad'), + Tables\Columns\TextColumn::make('created_at')->label('Creado')->dateTime(), + ])->filters([ + // + ])->headerActions([ + Tables\Actions\CreateAction::make(), + ])->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]); + } +} diff --git a/app/Filament/Resources/TelaResource.php b/app/Filament/Resources/TelaResource.php index 56549b1..1b0e63d 100644 --- a/app/Filament/Resources/TelaResource.php +++ b/app/Filament/Resources/TelaResource.php @@ -34,13 +34,10 @@ class TelaResource extends Resource TextInput::make('codigo')->required()->unique(ignoreRecord: true), - Select::make('tipo') - ->options([ - 'algodon' => 'Algodón', - 'poliester' => 'Poliéster', - 'denim' => 'Denim', - ]) - ->required(), + TextInput::make('tipo') + ->label('Tipo de tela') + ->required() + ->helperText('Ej: Algodón, Poliéster, Denim'), FileUpload::make('foto')->image()->directory('telas')->disk('public')->visibility('public')->maxSize(1024)->imageEditor()->imageEditorAspectRatios(['16:9','4:3','1:1'])->helperText('Selecciona una imagen de la tela (máx. 1MB)')->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp'])->downloadable()->openable()->nullable() , diff --git a/app/Models/InventarioPrenda.php b/app/Models/InventarioPrenda.php index 030c6d0..c05058d 100644 --- a/app/Models/InventarioPrenda.php +++ b/app/Models/InventarioPrenda.php @@ -25,6 +25,11 @@ class InventarioPrenda extends Model return $this->belongsTo(\App\Models\Producto::class, 'producto_id'); } + public function distribuciones() + { + return $this->hasMany(\App\Models\InventarioPrendaDistribucion::class, 'inventario_prenda_id'); + } + protected static function booted() { static::creating(function ($inventario) { diff --git a/app/Models/InventarioPrendaDistribucion.php b/app/Models/InventarioPrendaDistribucion.php new file mode 100644 index 0000000..70f773b --- /dev/null +++ b/app/Models/InventarioPrendaDistribucion.php @@ -0,0 +1,171 @@ +belongsTo(InventarioPrenda::class); + } + + public function bodega() + { + return $this->belongsTo(Bodega::class); + } + + public function color() + { + return $this->belongsTo(Color::class); + } + + public function size() + { + return $this->belongsTo(Size::class); + } + + protected static function booted() + { + // Al crear una distribución, incrementar stock + static::created(function ($dist) { + $cantidad = $dist->cantidad ?? 0; + if ($cantidad <= 0) return; + + $bodegaId = $dist->bodega_id; + + if ($dist->color_id && $dist->size_id) { + // Variante + $variant = \App\Models\ProductVariant::firstOrCreate([ + 'producto_id' => $dist->inventarioPrenda->producto_id ?? ($dist->inventarioPrenda->ordenProduccion->producto_id ?? null), + 'color_id' => $dist->color_id, + 'size_id' => $dist->size_id, + ], ['stock' => 0, 'sku' => 'AUTO-'.uniqid(), 'barcode' => '']); + + $existing = \DB::table('variante_bodega') + ->where('variante_id', $variant->id) + ->where('bodega_id', $bodegaId) + ->first(); + + if ($existing) { + \DB::table('variante_bodega') + ->where('variante_id', $variant->id) + ->where('bodega_id', $bodegaId) + ->update(['stock' => ($existing->stock ?? 0) + $cantidad, 'updated_at' => now()]); + } else { + \DB::table('variante_bodega')->insert(['variante_id' => $variant->id, 'bodega_id' => $bodegaId, 'stock' => $cantidad, 'created_at' => now(), 'updated_at' => now()]); + } + + $variant->increment('stock', $cantidad); + } else { + // Producto + $productoId = $dist->inventarioPrenda->producto_id ?? ($dist->inventarioPrenda->ordenProduccion->producto_id ?? null); + if ($productoId) { + $existing = \DB::table('producto_bodega')->where('producto_id', $productoId)->where('bodega_id', $bodegaId)->first(); + if ($existing) { + \DB::table('producto_bodega')->where('producto_id', $productoId)->where('bodega_id', $bodegaId)->update(['stock' => ($existing->stock ?? 0) + $cantidad, 'updated_at' => now()]); + } else { + \DB::table('producto_bodega')->insert(['producto_id' => $productoId, 'bodega_id' => $bodegaId, 'stock' => $cantidad, 'created_at' => now(), 'updated_at' => now()]); + } + + $prod = \App\Models\Producto::find($productoId); + if ($prod && ! $prod->variants()->exists()) { + $prod->increment('stock', $cantidad); + } + } + } + }); + + // Al actualizar, ajustar la diferencia + static::updated(function ($dist) { + $original = $dist->getOriginal(); + $oldCantidad = intval($original['cantidad'] ?? 0); + $newCantidad = intval($dist->cantidad ?? 0); + $delta = $newCantidad - $oldCantidad; + if ($delta == 0) return; + + $bodegaId = $dist->bodega_id; + + if ($dist->color_id && $dist->size_id) { + $variant = \App\Models\ProductVariant::firstOrCreate([ + 'producto_id' => $dist->inventarioPrenda->producto_id ?? ($dist->inventarioPrenda->ordenProduccion->producto_id ?? null), + 'color_id' => $dist->color_id, + 'size_id' => $dist->size_id, + ], ['stock' => 0, 'sku' => 'AUTO-'.uniqid(), 'barcode' => '']); + + $existing = \DB::table('variante_bodega')->where('variante_id', $variant->id)->where('bodega_id', $bodegaId)->first(); + if ($existing) { + \DB::table('variante_bodega')->where('variante_id', $variant->id)->where('bodega_id', $bodegaId)->update(['stock' => ($existing->stock ?? 0) + $delta, 'updated_at' => now()]); + } else { + \DB::table('variante_bodega')->insert(['variante_id' => $variant->id, 'bodega_id' => $bodegaId, 'stock' => max(0, $delta), 'created_at' => now(), 'updated_at' => now()]); + } + + $variant->increment('stock', $delta); + } else { + $productoId = $dist->inventarioPrenda->producto_id ?? ($dist->inventarioPrenda->ordenProduccion->producto_id ?? null); + if ($productoId) { + $existing = \DB::table('producto_bodega')->where('producto_id', $productoId)->where('bodega_id', $bodegaId)->first(); + if ($existing) { + \DB::table('producto_bodega')->where('producto_id', $productoId)->where('bodega_id', $bodegaId)->update(['stock' => ($existing->stock ?? 0) + $delta, 'updated_at' => now()]); + } else { + \DB::table('producto_bodega')->insert(['producto_id' => $productoId, 'bodega_id' => $bodegaId, 'stock' => max(0, $delta), 'created_at' => now(), 'updated_at' => now()]); + } + + $prod = \App\Models\Producto::find($productoId); + if ($prod && ! $prod->variants()->exists()) { + $prod->increment('stock', $delta); + } + } + } + }); + + // Al eliminar, restar la cantidad + static::deleted(function ($dist) { + $cantidad = $dist->cantidad ?? 0; + if ($cantidad <= 0) return; + + $bodegaId = $dist->bodega_id; + + if ($dist->color_id && $dist->size_id) { + $variant = \App\Models\ProductVariant::where('color_id', $dist->color_id)->where('size_id', $dist->size_id)->first(); + if ($variant) { + $existing = \DB::table('variante_bodega')->where('variante_id', $variant->id)->where('bodega_id', $bodegaId)->first(); + if ($existing) { + $newStock = max(0, ($existing->stock ?? 0) - $cantidad); + \DB::table('variante_bodega')->where('variante_id', $variant->id)->where('bodega_id', $bodegaId)->update(['stock' => $newStock, 'updated_at' => now()]); + } + + $variant->decrement('stock', $cantidad); + } + } else { + $productoId = $dist->inventarioPrenda->producto_id ?? ($dist->inventarioPrenda->ordenProduccion->producto_id ?? null); + if ($productoId) { + $existing = \DB::table('producto_bodega')->where('producto_id', $productoId)->where('bodega_id', $bodegaId)->first(); + if ($existing) { + $newStock = max(0, ($existing->stock ?? 0) - $cantidad); + \DB::table('producto_bodega')->where('producto_id', $productoId)->where('bodega_id', $bodegaId)->update(['stock' => $newStock, 'updated_at' => now()]); + } + + $prod = \App\Models\Producto::find($productoId); + if ($prod && ! $prod->variants()->exists()) { + $prod->decrement('stock', $cantidad); + } + } + } + }); + } +} diff --git a/database/migrations/2026_01_19_103000_create_inventario_prenda_distribuciones_table.php b/database/migrations/2026_01_19_103000_create_inventario_prenda_distribuciones_table.php new file mode 100644 index 0000000..1303331 --- /dev/null +++ b/database/migrations/2026_01_19_103000_create_inventario_prenda_distribuciones_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('inventario_prenda_id')->constrained('inventario_prendas')->cascadeOnDelete(); + $table->foreignId('bodega_id')->constrained('bodegas')->cascadeOnDelete(); + $table->foreignId('color_id')->nullable()->constrained('colors'); + $table->foreignId('size_id')->nullable()->constrained('sizes'); + $table->integer('cantidad')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('inventario_prenda_distribuciones'); + } +}; \ No newline at end of file diff --git a/tests/Feature/InventarioDistribucionTest.php b/tests/Feature/InventarioDistribucionTest.php new file mode 100644 index 0000000..678c456 --- /dev/null +++ b/tests/Feature/InventarioDistribucionTest.php @@ -0,0 +1,81 @@ + 'Bodega A']); + $color = Color::create(['name' => 'Rojo', 'hex_code' => '#ff0000']); + $size = Size::create(['name' => 'M']); + + $categoria = \App\Models\Categoria::create(['nombre' => 'Ropa']); + $producto = Producto::create(['nombre' => 'Camiseta', 'descripcion' => '', 'stock' => 0, 'estado' => true, 'precio_compra' => 0, 'precio_venta' => 0, 'unidad_medida' => 'unidad', 'codigo_barras' => '', 'categoria_id' => $categoria->id, 'imagen' => '']); + $op = OrdenProduccion::create(['numero_orden' => 'OP-0001', 'prenda_modelo' => 'Camiseta', 'referencia' => 'C-01', 'cantidad_total' => 10, 'metros_requeridos' => 0, 'fecha_inicio' => now(), 'fecha_entrega_estimada' => now()->addDays(7)]); + + $inv = InventarioPrenda::create(['orden_produccion_id' => $op->id, 'cantidad_terminada' => 5, 'cantidad_disponible' => 5, 'fecha_ingreso' => now(), 'estado' => 'en_bodega', 'producto_id' => $producto->id]); + + // Crear distribución + $dist = InventarioPrendaDistribucion::create([ + 'inventario_prenda_id' => $inv->id, + 'bodega_id' => $bodega->id, + 'color_id' => $color->id, + 'size_id' => $size->id, + 'cantidad' => 3, + ]); + + // El evento debe haber creado la variante + $variant = ProductVariant::where('producto_id', $producto->id)->where('color_id', $color->id)->where('size_id', $size->id)->first(); + + $this->assertNotNull($variant, 'Variant should be created'); + $this->assertEquals(3, $variant->stock); + + // Revisar pivot variante_bodega + $pivot = \DB::table('variante_bodega')->where('variante_id', $variant->id)->where('bodega_id', $bodega->id)->first(); + $this->assertNotNull($pivot); + $this->assertEquals(3, $pivot->stock); + } + + public function test_creating_inventario_with_excessive_distributions_throws_validation_exception() + { + $categoria = \App\Models\Categoria::create(['nombre' => 'Ropa']); + $producto = Producto::create(['nombre' => 'Polo', 'descripcion' => '', 'stock' => 0, 'estado' => true, 'precio_compra' => 0, 'precio_venta' => 0, 'unidad_medida' => 'unidad', 'codigo_barras' => '', 'categoria_id' => $categoria->id, 'imagen' => '']); + + $op = OrdenProduccion::create(['numero_orden' => 'OP-0002', 'prenda_modelo' => 'Polo', 'referencia' => 'P-01', 'cantidad_total' => 10, 'metros_requeridos' => 0, 'fecha_inicio' => now(), 'fecha_entrega_estimada' => now()->addDays(7)]); + + $data = [ + 'orden_produccion_id' => $op->id, + 'cantidad_terminada' => 5, + 'cantidad_disponible' => 5, + 'fecha_ingreso' => now()->toDateString(), + 'producto_id' => $producto->id, + 'estado' => 'en_bodega', + 'distribuciones' => [ + ['bodega_id' => 1, 'cantidad' => 3], + ['bodega_id' => 1, 'cantidad' => 3], + ], + ]; + + $this->expectException(\Illuminate\Validation\ValidationException::class); + + $page = new \App\Filament\Resources\InventarioPrendaResource\Pages\CreateInventarioPrenda(); + $ref = new \ReflectionClass($page); + $method = $ref->getMethod('handleRecordCreation'); + $method->setAccessible(true); + $method->invoke($page, $data); + } +}