Files
pos_heidiver/app/Models/OrdenProduccion.php
T

364 lines
12 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class OrdenProduccion extends Model
{
use HasFactory;
protected $fillable = [
'numero_orden',
'prenda_modelo',
'referencia',
'cantidad_total',
'tela_id',
'producto_id',
'fecha_inicio',
'fecha_entrega_estimada',
'estado',
'metros_requeridos',
'tela_reservada',
'imagenes',
];
/**
* Casts
*
* @var array
*/
protected $casts = [
'imagenes' => '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');
}
/**
* Verifica si ambos procesos (ojales y prensillas) están completos para una cantidad específica
*
* @param int $cantidad Cantidad de prendas a verificar
* @return array ['completo' => bool, 'ojales_terminados' => int, 'prensillas_terminadas' => int, 'minimo' => int]
*/
public function verificarProcesosCompletos(int $cantidad = null): array
{
$cantidadVerificar = $cantidad ?? $this->cantidad_total;
// Cantidad de ojales terminados
$ojalesTerminados = $this->ojales()
->where('terminada', true)
->sum('cantidad_enviada');
// Cantidad de prensillas terminadas
$prensillasTerminadas = $this->prensillas()
->where('terminada', true)
->sum('cantidad_enviada');
// El mínimo entre ambos procesos
$minimoCompleto = min($ojalesTerminados, $prensillasTerminadas);
// Están completos si el mínimo cubre la cantidad requerida
$completo = $minimoCompleto >= $cantidadVerificar;
return [
'completo' => $completo,
'ojales_terminados' => $ojalesTerminados,
'prensillas_terminadas' => $prensillasTerminadas,
'minimo_completo' => $minimoCompleto,
'cantidad_requerida' => $cantidadVerificar,
'faltante_ojales' => max(0, $cantidadVerificar - $ojalesTerminados),
'faltante_prensillas' => max(0, $cantidadVerificar - $prensillasTerminadas),
];
}
/**
* Obtiene la cantidad disponible para enviar a tintorería
* (solo las prendas que tienen ambos procesos completos)
*/
public function getCantidadDisponibleParaTintoreria(): int
{
$verificacion = $this->verificarProcesosCompletos();
// Ya enviadas a tintorería
$enviadasTintoreria = $this->tintorerias()->sum('cantidad_enviada');
return max(0, $verificacion['minimo_completo'] - $enviadasTintoreria);
}
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á)
}
}
}