From 2d99bfe2442f37211046fcf91774e0f65f4bf808 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:02:56 -0500 Subject: [PATCH] up --- .../Resources/ProcesoAcabadoResource.php | 25 +++- .../AjustesRelationManager.php | 130 ++++++++++++++++++ .../CobrosRelationManager.php | 119 ++++++++++++++++ .../RecepcionesRelationManager.php | 125 +++++++++++++++++ app/Models/InventarioPrenda.php | 12 +- app/Models/ProcesoAcabado.php | 104 ++++++++++---- ...01_migrate_proceso_acabado_recepciones.php | 56 ++++++++ 7 files changed, 535 insertions(+), 36 deletions(-) create mode 100644 app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/AjustesRelationManager.php create mode 100644 app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/CobrosRelationManager.php create mode 100644 app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/RecepcionesRelationManager.php create mode 100644 database/migrations/2026_02_09_000001_migrate_proceso_acabado_recepciones.php diff --git a/app/Filament/Resources/ProcesoAcabadoResource.php b/app/Filament/Resources/ProcesoAcabadoResource.php index 0256ecb..b9a25a0 100644 --- a/app/Filament/Resources/ProcesoAcabadoResource.php +++ b/app/Filament/Resources/ProcesoAcabadoResource.php @@ -23,6 +23,7 @@ use Filament\Tables\Columns\{ TextColumn, BadgeColumn }; +use Illuminate\Support\Facades\DB; class ProcesoAcabadoResource extends Resource @@ -50,7 +51,7 @@ class ProcesoAcabadoResource extends Resource $tintoreriaRecords = \App\Models\Tintoreria::where('orden_produccion_id', $state)->get(); $disponible = 0; foreach ($tintoreriaRecords as $tintoreria) { - $recepciones = (int) $tintoreria->recepciones()->sum(\DB::raw('cantidad - COALESCE(prendas_defectuosas, 0)')); + $recepciones = (int) $tintoreria->recepciones()->sum(DB::raw('cantidad - COALESCE(prendas_defectuosas, 0)')); $reprocesos = (int) $tintoreria->ajustes()->where('tipo', 'reproceso')->sum('cantidad'); $cobros = (int) $tintoreria->cobros()->sum('cantidad'); $disponible += ($recepciones - $reprocesos - $cobros); @@ -96,7 +97,7 @@ class ProcesoAcabadoResource extends Resource $tintoreriaRecords = \App\Models\Tintoreria::where('orden_produccion_id', $op_id)->get(); $disponible = 0; foreach ($tintoreriaRecords as $tintoreria) { - $recepciones = (int) $tintoreria->recepciones()->sum(\DB::raw('cantidad - COALESCE(prendas_defectuosas, 0)')); + $recepciones = (int) $tintoreria->recepciones()->sum(DB::raw('cantidad - COALESCE(prendas_defectuosas, 0)')); $reprocesos = (int) $tintoreria->ajustes()->where('tipo', 'reproceso')->sum('cantidad'); $cobros = (int) $tintoreria->cobros()->sum('cantidad'); $disponible += ($recepciones - $reprocesos - $cobros); @@ -221,6 +222,22 @@ class ProcesoAcabadoResource extends Resource ->label('Cantidad') ->sortable(), + TextColumn::make('cantidad_recibida_total') + ->label('Recibidas') + ->getStateUsing(fn ($record) => $record->recibido_total ?? 0) + ->badge() + ->color('success'), + + TextColumn::make('estado_recepcion') + ->label('Estado Recepción') + ->getStateUsing(fn ($record) => $record->estado_recepcion ?? 'Sin envío') + ->badge() + ->color(fn ($state) => match($state) { + 'Completado' => 'success', + 'Parcial' => 'warning', + default => 'gray', + }), + TextColumn::make('proveedores_asignados') ->label('Proveedores') ->getStateUsing(function ($record) { @@ -282,7 +299,9 @@ class ProcesoAcabadoResource extends Resource public static function getRelations(): array { return [ - // + RelationManagers\RecepcionesRelationManager::class, + RelationManagers\AjustesRelationManager::class, + RelationManagers\CobrosRelationManager::class, ]; } diff --git a/app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/AjustesRelationManager.php b/app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/AjustesRelationManager.php new file mode 100644 index 0000000..d65cd8a --- /dev/null +++ b/app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/AjustesRelationManager.php @@ -0,0 +1,130 @@ +schema([ + Select::make('tipo') + ->label('Tipo de Ajuste') + ->options([ + 'reproceso' => 'Reproceso (permite re-recepción)', + 'ajuste_entrada' => 'Ajuste de Entrada', + 'ajuste_salida' => 'Ajuste de Salida', + ]) + ->required() + ->default('reproceso') + ->helperText('Los reprocesos permiten volver a recibir las prendas cuando regresen arregladas'), + + TextInput::make('cantidad') + ->label('Cantidad') + ->numeric() + ->required() + ->minValue(1), + + DateTimePicker::make('fecha') + ->label('Fecha del Ajuste') + ->required() + ->default(now()), + + Textarea::make('notas') + ->label('Notas / Motivo') + ->rows(3) + ->maxLength(65535), + ]); + } + + public function table(Tables\Table $table): Tables\Table + { + return $table + ->columns([ + TextColumn::make('id') + ->label('#') + ->sortable(), + + TextColumn::make('tipo') + ->label('Tipo') + ->badge() + ->color(fn ($state) => match($state) { + 'reproceso' => 'warning', + 'ajuste_entrada' => 'success', + 'ajuste_salida' => 'danger', + default => 'gray', + }) + ->formatStateUsing(fn ($state) => match($state) { + 'reproceso' => 'Reproceso', + 'ajuste_entrada' => 'Ajuste Entrada', + 'ajuste_salida' => 'Ajuste Salida', + default => $state, + }), + + TextColumn::make('cantidad') + ->label('Cantidad') + ->sortable(), + + TextColumn::make('fecha') + ->label('Fecha') + ->dateTime('d/m/Y H:i') + ->sortable(), + + TextColumn::make('usuario.name') + ->label('Usuario') + ->searchable(), + + TextColumn::make('notas') + ->label('Notas') + ->limit(50) + ->wrap() + ->toggleable(), + + TextColumn::make('created_at') + ->label('Creado') + ->dateTime('d/m/Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->defaultSort('fecha', 'desc') + ->headerActions([ + CreateAction::make() + ->label('Nuevo Ajuste') + ->icon('heroicon-o-plus-circle') + ->mutateFormDataUsing(function (array $data): array { + $data['user_id'] = Auth::id(); + return $data; + }), + ]) + ->actions([ + EditAction::make(), + DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]) + ->emptyStateHeading('Sin ajustes registrados') + ->emptyStateDescription('Los reprocesos permiten re-recepción de prendas que requieren corrección.') + ->emptyStateIcon('heroicon-o-arrow-path'); + } +} diff --git a/app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/CobrosRelationManager.php b/app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/CobrosRelationManager.php new file mode 100644 index 0000000..c5db328 --- /dev/null +++ b/app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/CobrosRelationManager.php @@ -0,0 +1,119 @@ +schema([ + TextInput::make('cantidad') + ->label('Cantidad de Prendas') + ->numeric() + ->required() + ->minValue(1) + ->helperText('Cantidad de prendas con defectos que se cobrarán al proveedor'), + + TextInput::make('valor') + ->label('Valor a Cobrar') + ->numeric() + ->required() + ->minValue(0) + ->prefix('$') + ->helperText('Monto total a descontar al proveedor'), + + DateTimePicker::make('fecha') + ->label('Fecha del Cobro') + ->required() + ->default(now()), + + Textarea::make('notas') + ->label('Motivo del Cobro') + ->rows(3) + ->maxLength(65535) + ->helperText('Describe los defectos encontrados'), + ]); + } + + public function table(Tables\Table $table): Tables\Table + { + return $table + ->columns([ + TextColumn::make('id') + ->label('#') + ->sortable(), + + TextColumn::make('cantidad') + ->label('Cantidad') + ->sortable() + ->badge() + ->color('danger'), + + TextColumn::make('valor') + ->label('Valor') + ->money('COP') + ->sortable(), + + TextColumn::make('fecha') + ->label('Fecha') + ->dateTime('d/m/Y H:i') + ->sortable(), + + TextColumn::make('usuario.name') + ->label('Usuario') + ->searchable(), + + TextColumn::make('notas') + ->label('Motivo') + ->limit(50) + ->wrap() + ->toggleable(), + + TextColumn::make('created_at') + ->label('Creado') + ->dateTime('d/m/Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->defaultSort('fecha', 'desc') + ->headerActions([ + CreateAction::make() + ->label('Nuevo Cobro') + ->icon('heroicon-o-minus-circle') + ->mutateFormDataUsing(function (array $data): array { + $data['user_id'] = Auth::id(); + return $data; + }), + ]) + ->actions([ + EditAction::make(), + DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]) + ->emptyStateHeading('Sin cobros registrados') + ->emptyStateDescription('Registra cobros por prendas defectuosas que no pasarán a inventario.') + ->emptyStateIcon('heroicon-o-currency-dollar'); + } +} diff --git a/app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/RecepcionesRelationManager.php b/app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/RecepcionesRelationManager.php new file mode 100644 index 0000000..daa16e9 --- /dev/null +++ b/app/Filament/Resources/ProcesoAcabadoResource/RelationManagers/RecepcionesRelationManager.php @@ -0,0 +1,125 @@ +schema([ + DateTimePicker::make('fecha_recepcion') + ->label('Fecha de Recepción') + ->required() + ->default(now()), + + TextInput::make('cantidad') + ->label('Cantidad Recibida') + ->numeric() + ->required() + ->minValue(1) + ->maxValue(fn () => $this->getOwnerRecord()?->faltantes ?? 0) + ->helperText(fn () => 'Disponible para recibir: ' . ($this->getOwnerRecord()?->faltantes ?? 0)), + + TextInput::make('prendas_defectuosas') + ->label('Prendas Defectuosas') + ->numeric() + ->default(0) + ->minValue(0) + ->helperText('Si hay prendas defectuosas, regístralas aquí. Estas no pasarán al inventario.'), + + Textarea::make('notas') + ->label('Notas / Observaciones') + ->rows(3) + ->maxLength(65535), + ]); + } + + public function table(Tables\Table $table): Tables\Table + { + return $table + ->columns([ + TextColumn::make('id') + ->label('#') + ->sortable(), + + TextColumn::make('fecha_recepcion') + ->label('Fecha Recepción') + ->dateTime('d/m/Y H:i') + ->sortable(), + + TextColumn::make('cantidad') + ->label('Cantidad') + ->sortable(), + + TextColumn::make('prendas_defectuosas') + ->label('Defectuosas') + ->default(0) + ->badge() + ->color(fn ($state) => $state > 0 ? 'danger' : 'gray'), + + TextColumn::make('cantidad_efectiva') + ->label('Cantidad Efectiva') + ->getStateUsing(fn ($record) => $record->cantidad - ($record->prendas_defectuosas ?? 0)) + ->badge() + ->color('success'), + + TextColumn::make('usuario.name') + ->label('Usuario') + ->searchable(), + + TextColumn::make('notas') + ->label('Notas') + ->limit(50) + ->wrap() + ->toggleable(), + + TextColumn::make('created_at') + ->label('Creado') + ->dateTime('d/m/Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->defaultSort('fecha_recepcion', 'desc') + ->headerActions([ + CreateAction::make() + ->label('Nueva Recepción') + ->icon('heroicon-o-plus-circle') + ->disabled(fn () => ($this->getOwnerRecord()?->faltantes ?? 0) <= 0) + ->mutateFormDataUsing(function (array $data): array { + // Asignar usuario actual + $data['user_id'] = Auth::id(); + return $data; + }), + ]) + ->actions([ + EditAction::make(), + DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]) + ->emptyStateHeading('Sin recepciones registradas') + ->emptyStateDescription('Las recepciones quedarán disponibles automáticamente para el inventario de prendas.') + ->emptyStateIcon('heroicon-o-inbox-arrow-down'); + } +} diff --git a/app/Models/InventarioPrenda.php b/app/Models/InventarioPrenda.php index e59fb4f..20b9f15 100644 --- a/app/Models/InventarioPrenda.php +++ b/app/Models/InventarioPrenda.php @@ -53,10 +53,14 @@ class InventarioPrenda extends Model // Validación y auto-filling: la cantidad terminada se calcula automáticamente desde lo recibido para esta OP (traslados a bodega) if ($inventario->orden_produccion_id) { - // Total desde procesos de acabados terminados - $totalRecibido = \App\Models\ProcesoAcabado::where('orden_produccion_id', $inventario->orden_produccion_id) - ->where('terminada', true) - ->sum('cantidad_enviada'); + // Total desde procesos de acabados: usar recepciones en lugar de cantidad_recibida + $totalRecibido = 0; + $procesosAcabados = \App\Models\ProcesoAcabado::where('orden_produccion_id', $inventario->orden_produccion_id)->get(); + + foreach ($procesosAcabados as $proceso) { + // Sumar recepciones efectivas (cantidad - defectuosas) menos reprocesos y cobros + $totalRecibido += $proceso->recibido_total ?? 0; + } $assigned = \App\Models\InventarioPrenda::where('orden_produccion_id', $inventario->orden_produccion_id)->sum('cantidad_terminada'); diff --git a/app/Models/ProcesoAcabado.php b/app/Models/ProcesoAcabado.php index 11ab493..00b9fd4 100644 --- a/app/Models/ProcesoAcabado.php +++ b/app/Models/ProcesoAcabado.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\DB; class ProcesoAcabado extends Model { @@ -38,6 +39,67 @@ class ProcesoAcabado extends Model return $this->belongsTo(Proveedor::class); } + /* Recepciones polimórficas */ + public function recepciones() + { + return $this->morphMany(\App\Models\Recepcion::class, 'referencia'); + } + + public function cobros() + { + return $this->morphMany(\App\Models\Cobro::class, 'referencia'); + } + + public function ajustes() + { + return $this->morphMany(\App\Models\Ajuste::class, 'referencia'); + } + + // Total recibido calculado desde recepciones (fuente de verdad) + // Menos reprocesos (permiten re-recepción) y menos cobros (no pasan a inventario) + public function getRecibidoTotalAttribute() + { + $recepciones = (int) $this->recepciones()->sum(DB::raw('cantidad - COALESCE(prendas_defectuosas, 0)')); + $reprocesos = (int) $this->ajustes()->where('tipo', 'reproceso')->sum('cantidad'); + $cobros = (int) $this->cobros()->sum('cantidad'); + + return max(0, $recepciones - $reprocesos - $cobros); + } + + // Total de pérdidas registradas en las recepciones + public function getPerdidasTotalAttribute() + { + return (int) $this->recepciones()->sum('prendas_defectuosas'); + } + + public function getCobrosTotalAttribute() + { + return (float) $this->cobros()->sum('valor'); + } + + // Cuantas faltan por recibir + public function getFaltantesAttribute() + { + $enviada = (int)($this->cantidad_enviada ?? 0); + $recepciones = (int) $this->recepciones()->sum(DB::raw('cantidad - COALESCE(prendas_defectuosas, 0)')); + $reprocesos = (int) $this->ajustes()->where('tipo', 'reproceso')->sum('cantidad'); + + // Lo que puede recibir = enviadas - recepciones + reprocesos (reprocesos permiten re-recepción) + return max(0, $enviada - $recepciones + $reprocesos); + } + + // Estado de recepción (Parcial / Completado) + public function getEstadoRecepcionAttribute() + { + $faltan = $this->faltantes ?? 0; + + if (($this->cantidad_enviada ?? 0) <= 0) { + return 'Sin envío'; + } + + return $faltan > 0 ? 'Parcial' : 'Completado'; + } + /* Lógica de negocio */ protected static function booted() { @@ -58,34 +120,21 @@ class ProcesoAcabado extends Model } }); - $createTrasladoFn = function ($acabado) { - if ($acabado->cantidad_recibida !== null) { - // Evitar duplicados - $exists = \App\Models\TrasladoPrenda::where('referencia_type', self::class) - ->where('referencia_id', $acabado->id) - ->exists(); + // Al guardar recepciones, avanzar estado de la OP + static::saved(function ($acabado) { + // Verificar si el proceso está completado (todas las prendas recibidas) + $faltantes = $acabado->faltantes ?? 0; + + if ($faltantes === 0 && ($acabado->cantidad_enviada ?? 0) > 0) { + // Avanzar la OP al completar este proceso + $acabado->ordenProduccion?->avanzarEstado(); - if (! $exists) { - $destino = 'bodega'; - - \App\Models\TrasladoPrenda::crearDesdeReferencia( - $acabado, - 'acabados', - $destino - ); - - // Avanzar la OP al cerrar este proceso - $acabado->ordenProduccion?->avanzarEstado(); - } - } - - if ( - $acabado->fecha_recepcion && - $acabado->cantidad_recibida !== null - ) { // Validar si ya no quedan procesos pendientes $pendientes = self::where('orden_produccion_id', $acabado->orden_produccion_id) - ->whereNull('fecha_recepcion') + ->where(function($q) { + $q->whereNull('cantidad_enviada') + ->orWhereRaw('cantidad_enviada > 0 AND (SELECT COALESCE(SUM(cantidad - COALESCE(prendas_defectuosas, 0)), 0) FROM recepciones WHERE referencia_type = ? AND referencia_id = proceso_acabados.id) < cantidad_enviada', [self::class]); + }) ->exists(); if (!$pendientes) { @@ -93,9 +142,6 @@ class ProcesoAcabado extends Model $acabado->ordenProduccion?->tryFinalize(); } } - }; - - static::updated($createTrasladoFn); - static::saved($createTrasladoFn); + }); } } diff --git a/database/migrations/2026_02_09_000001_migrate_proceso_acabado_recepciones.php b/database/migrations/2026_02_09_000001_migrate_proceso_acabado_recepciones.php new file mode 100644 index 0000000..ea63c96 --- /dev/null +++ b/database/migrations/2026_02_09_000001_migrate_proceso_acabado_recepciones.php @@ -0,0 +1,56 @@ +whereNotNull('cantidad_recibida') + ->where('cantidad_recibida', '>', 0) + ->get(); + + foreach ($acabados as $acabado) { + // Verificar si ya existe una recepción para este acabado + $existeRecepcion = DB::table('recepciones') + ->where('referencia_type', 'App\\Models\\ProcesoAcabado') + ->where('referencia_id', $acabado->id) + ->exists(); + + if (!$existeRecepcion) { + // Crear recepción desde el campo cantidad_recibida + DB::table('recepciones')->insert([ + 'referencia_type' => 'App\\Models\\ProcesoAcabado', + 'referencia_id' => $acabado->id, + 'user_id' => 1, // Usuario por defecto (admin) + 'cantidad' => $acabado->cantidad_recibida, + 'prendas_defectuosas' => 0, + 'fecha_recepcion' => $acabado->fecha_recepcion ?? $acabado->created_at, + 'notas' => 'Migración automática desde cantidad_recibida', + 'created_at' => $acabado->created_at, + 'updated_at' => $acabado->updated_at, + ]); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Eliminar recepciones migradas automáticamente + DB::table('recepciones') + ->where('referencia_type', 'App\\Models\\ProcesoAcabado') + ->where('notas', 'Migración automática desde cantidad_recibida') + ->delete(); + } +};