This commit is contained in:
Lizandro Guarnizo
2026-01-19 13:21:11 -05:00
parent b63995d74a
commit ce8a479097
8 changed files with 495 additions and 8 deletions
@@ -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,
];
}
@@ -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;
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Filament\Resources\InventarioPrendaResource\RelationManagers;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Tables\Table;
class DistribucionesRelationManager extends RelationManager
{
protected static string $relationship = 'distribuciones';
protected static ?string $recordTitleAttribute = 'id';
public function form(Form $form): Form
{
return $form->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(),
]);
}
}
+4 -7
View File
@@ -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()
,
+5
View File
@@ -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) {
+171
View File
@@ -0,0 +1,171 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class InventarioPrendaDistribucion extends Model
{
use HasFactory;
protected $table = 'inventario_prenda_distribuciones';
protected $fillable = [
'inventario_prenda_id',
'bodega_id',
'color_id',
'size_id',
'cantidad',
];
public function inventarioPrenda()
{
return $this->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);
}
}
}
});
}
}
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('inventario_prenda_distribuciones', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,81 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\Bodega;
use App\Models\Color;
use App\Models\Size;
use App\Models\Producto;
use App\Models\OrdenProduccion;
use App\Models\InventarioPrenda;
use App\Models\InventarioPrendaDistribucion;
use App\Models\ProductVariant;
class InventarioDistribucionTest extends TestCase
{
use RefreshDatabase;
public function test_distribution_create_updates_variant_and_bodega_stock()
{
$bodega = Bodega::create(['nombre' => '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);
}
}