'array', ]; public function tela() { return $this->belongsTo(Tela::class); } public function traslados() { return $this->hasMany(\App\Models\TrasladoPrenda::class); } public function confeccions() { return $this->hasMany(\App\Models\Confeccion::class); } public function confecciones() { return $this->confeccions(); } public function tintorerias() { return $this->hasMany(\App\Models\Tintoreria::class); } public function prensillas() { return $this->hasMany(\App\Models\Prensilla::class); } public function ojales() { return $this->hasMany(\App\Models\Ojal::class); } public function procesosAcabado() { return $this->hasMany(\App\Models\ProcesoAcabado::class); } public function inventarioPrenda() { return $this->hasMany(\App\Models\InventarioPrenda::class); } public function producto() { return $this->belongsTo(\App\Models\Producto::class, 'producto_id'); } protected static function booted() { static::creating(function ($orden) { if (!$orden->numero_orden) { $ultimoId = self::max('id') + 1; $orden->numero_orden = 'OP-' . str_pad($ultimoId, 6, '0', STR_PAD_LEFT); } // Fecha de inicio por defecto hoy if (! isset($orden->fecha_inicio) || ! $orden->fecha_inicio) { $orden->fecha_inicio = now()->toDateString(); } // Asegurar que 'referencia' no sea NULL para evitar violaciones en bases que aún no aplicaron la migración if (! isset($orden->referencia) || $orden->referencia === null) { $orden->referencia = ''; } // Reserva de tela al crear cuando la OP usa una tela: validar disponibilidad if ($orden->tela_id && ($orden->metros_requeridos ?? 0) > 0) { $tela = \App\Models\Tela::find($orden->tela_id); if (! $tela) { throw \Illuminate\Validation\ValidationException::withMessages([ 'tela_id' => 'No se encontró la tela asociada.', ]); } // Calcular metros ya usados por otras OPs $metrosUsados = \App\Models\OrdenProduccion::where('tela_id', $orden->tela_id)->sum('metros_requeridos'); $disponible = max(0, $tela->metros_comprados - $metrosUsados); if ($disponible < $orden->metros_requeridos) { throw \Illuminate\Validation\ValidationException::withMessages([ 'metros_requeridos' => 'No hay suficientes metros disponibles para reservar (' . $disponible . ' disponibles).', ]); } $orden->tela_reservada = true; } }); // Validar disponibilidad de tela al actualizar static::updating(function ($orden) { // Validación: si se especifica tela y metros_requeridos, verificar disponibilidad if ($orden->tela_id && $orden->metros_requeridos !== null) { $tela = \App\Models\Tela::find($orden->tela_id); $originalMetros = $orden->getOriginal('metros_requeridos') ?? 0; // Calcular metros usados por otras OPs (excluyendo esta OP) $metrosUsadosOtros = \App\Models\OrdenProduccion::where('tela_id', $orden->tela_id) ->where('id', '!=', $orden->id) ->sum('metros_requeridos'); $disponible = max(0, $tela->metros_comprados - $metrosUsadosOtros); if ($orden->metros_requeridos > $disponible) { throw \Illuminate\Validation\ValidationException::withMessages([ 'metros_requeridos' => 'No hay suficientes metros de tela disponibles. Disponibles: ' . $disponible, ]); } } // Marcar como reservada si tiene tela y metros if ($orden->tela_id && $orden->metros_requeridos > 0) { $orden->tela_reservada = true; } }); } // Cantidad completada (desde procesos de acabados recibidos) public function getRealizadoAttribute() { return (int) $this->procesosAcabado()->where('terminada', true)->sum('cantidad_enviada'); } // Faltante para completar la OP public function getFaltantesAttribute() { return max(0, (int)$this->cantidad_total - $this->realizado); } public function canBeFinalized(): bool { return $this->realizado >= (int)$this->cantidad_total; } public function tryFinalize(): bool { if ($this->canBeFinalized()) { $this->update(['estado' => 'finalizada']); // Crear inventario si corresponde $this->crearInventarioPorFinalizacion(); return true; } return false; } /** * Estado calculado para mostrar en UI: si está completada o la última etapa que tiene tareas pendientes. */ public function getEstadoDisplayAttribute() { if ($this->canBeFinalized() || $this->estado === 'finalizada') { return 'finalizada'; } // Revisar etapas de último a primero para encontrar la etapa que aún tiene tareas pendientes if (\App\Models\ProcesoAcabado::where('orden_produccion_id', $this->id)->whereNull('fecha_recepcion')->exists()) { return 'en_acabados'; } if (\App\Models\Tintoreria::where('orden_produccion_id', $this->id)->whereNull('fecha_recepcion')->exists()) { return 'en_tintoreria'; } if (\App\Models\Confeccion::where('orden_produccion_id', $this->id)->whereNull('fecha_recepcion')->exists()) { return 'en_confeccion'; } // Por defecto devolver el estado actual (incluye en_corte u otros) return $this->estado ?? 'en_corte'; } /** * Forzar finalización manualmente (útil si desea marcar como finalizada aunque no esté totalmente recibida) */ public function manualFinalize(): void { $this->update(['estado' => 'finalizada']); // Intentar crear inventario con lo recibido hasta ahora $this->crearInventarioPorFinalizacion(); } public function avanzarEstado(): void { $flujo = [ 'en_corte', 'en_confeccion', 'en_tintoreria', 'en_acabados', 'finalizada', ]; $actual = array_search($this->estado, $flujo, true); if ($actual !== false && isset($flujo[$actual + 1])) { $next = $flujo[$actual + 1]; // Si el siguiente estado es finalizada, verificar que esté completa if ($next === 'finalizada' && ! $this->canBeFinalized()) { return; // No avanzar a finalizada si no está completada } $this->update([ 'estado' => $next, ]); // Si la orden llega a finalizada, crear entrada en inventario con lo que haya ingresado if ($this->estado === 'finalizada') { $this->crearInventarioPorFinalizacion(); } } } protected function crearInventarioPorFinalizacion(): void { // Sumar procesos de acabados recibidos para determinar cantidad terminada $cantidad = \App\Models\ProcesoAcabado::where('orden_produccion_id', $this->id) ->whereNotNull('fecha_recepcion') ->sum('cantidad_recibida'); // Crear inventario solo si hay traslados a bodega con cantidad if ($cantidad > 0 && !\App\Models\InventarioPrenda::where('orden_produccion_id', $this->id)->exists()) { // Determinar producto asociado: preferir producto_id en la OP, sino crear/buscar por prenda_modelo $productoId = $this->producto_id; if (! $productoId) { $nombre = $this->prenda_modelo ?? $this->referencia ?? null; if ($nombre) { // Generar código de barras único por defecto para productos creados automáticamente $codigo = 'AUTOP-' . $this->id . '-' . time(); $codigo = substr($codigo, 0, 50); // Asegurar unicidad while (\App\Models\Producto::where('codigo_barras', $codigo)->exists()) { $codigo .= '-' . rand(0, 9); $codigo = substr($codigo, 0, 50); } $producto = \App\Models\Producto::firstOrCreate( ['nombre' => $nombre], [ 'descripcion' => 'Creado automáticamente desde OrdenProduccion #' . $this->id, 'stock' => 0, 'estado' => true, 'precio_compra' => 0, 'precio_venta' => 0, 'unidad_medida' => 'unidad', 'codigo_barras' => $codigo, ] ); $productoId = $producto->id; // Guardar vínculo en la OP para futuras referencias $this->producto_id = $productoId; $this->save(); } } \App\Models\InventarioPrenda::create([ 'orden_produccion_id' => $this->id, 'cantidad_terminada' => $cantidad, 'cantidad_disponible' => $cantidad, 'fecha_ingreso' => now(), 'estado' => 'en_bodega', 'producto_id' => $productoId ?? null, ]); // Si se creó inventario, también actualizar stock del producto asociado (handler en InventarioPrenda lo hará) } } }