393 lines
15 KiB
PHP
393 lines
15 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 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: restar metros_disponibles inmediatamente
|
|
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.',
|
|
]);
|
|
}
|
|
|
|
if ($tela->metros_disponibles < $orden->metros_requeridos) {
|
|
throw \Illuminate\Validation\ValidationException::withMessages([
|
|
'metros_requeridos' => 'No hay suficientes metros disponibles para reservar (' . $tela->metros_disponibles . ').',
|
|
]);
|
|
}
|
|
|
|
// Reservar: restar metros y marcar orden
|
|
$tela->metros_disponibles = $tela->metros_disponibles - $orden->metros_requeridos;
|
|
$tela->save();
|
|
|
|
$orden->tela_reservada = true;
|
|
}
|
|
});
|
|
|
|
// Validar y reservar tela al cambiar de estado a 'en_corte'
|
|
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;
|
|
$reservedAlready = $orden->tela_reservada ? $originalMetros : 0;
|
|
$available = $tela ? ($tela->metros_disponibles + $reservedAlready) : null;
|
|
|
|
if ($available !== null && $orden->metros_requeridos > $available) {
|
|
throw \Illuminate\Validation\ValidationException::withMessages([
|
|
'metros_requeridos' => 'No hay suficientes metros de tela disponibles. Disponibles: ' . ($available),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Ajustes automáticos de reserva cuando cambia la tela o los metros requeridos
|
|
$originalTelaId = $orden->getOriginal('tela_id');
|
|
$originalMetros = $orden->getOriginal('metros_requeridos') ?? 0;
|
|
$newTelaId = $orden->tela_id;
|
|
$newMetros = $orden->metros_requeridos ?? 0;
|
|
|
|
// Si cambió la tela en la OP, devolver los metros reservados en la tela anterior (si aplica)
|
|
if ($originalTelaId && $originalTelaId != $newTelaId && $originalMetros > 0) {
|
|
$oldTela = \App\Models\Tela::find($originalTelaId);
|
|
if ($oldTela) {
|
|
$oldTela->metros_disponibles = $oldTela->metros_disponibles + $originalMetros;
|
|
$oldTela->save();
|
|
}
|
|
}
|
|
|
|
// Reservar metros en la nueva tela si se cambió
|
|
if ($newTelaId && $originalTelaId != $newTelaId && $newMetros > 0) {
|
|
$newTela = \App\Models\Tela::find($newTelaId);
|
|
if (! $newTela) {
|
|
throw \Illuminate\Validation\ValidationException::withMessages([
|
|
'tela_id' => 'No se encontró la tela asociada.',
|
|
]);
|
|
}
|
|
|
|
if ($newTela->metros_disponibles < $newMetros) {
|
|
throw \Illuminate\Validation\ValidationException::withMessages([
|
|
'metros_requeridos' => 'No hay suficientes metros disponibles para reservar (' . $newTela->metros_disponibles . ').',
|
|
]);
|
|
}
|
|
|
|
$newTela->metros_disponibles = $newTela->metros_disponibles - $newMetros;
|
|
$newTela->save();
|
|
$orden->tela_reservada = true;
|
|
}
|
|
|
|
// Si la misma tela y cambiaron los metros, ajustar la diferencia
|
|
if ($originalTelaId == $newTelaId && $originalMetros != $newMetros && $newTelaId) {
|
|
$delta = $newMetros - $originalMetros;
|
|
$tela = \App\Models\Tela::find($newTelaId);
|
|
if ($tela) {
|
|
if ($delta > 0) {
|
|
if ($tela->metros_disponibles < $delta) {
|
|
throw \Illuminate\Validation\ValidationException::withMessages([
|
|
'metros_requeridos' => 'No hay suficientes metros disponibles para aumentar la reserva.',
|
|
]);
|
|
}
|
|
$tela->metros_disponibles = $tela->metros_disponibles - $delta;
|
|
} elseif ($delta < 0) {
|
|
$tela->metros_disponibles = $tela->metros_disponibles + abs($delta);
|
|
}
|
|
$tela->save();
|
|
}
|
|
}
|
|
|
|
$originalEstado = $orden->getOriginal('estado');
|
|
$nuevoEstado = $orden->estado;
|
|
|
|
// Reservar cuando pasamos a en_corte y no estaba reservado
|
|
if ($originalEstado !== 'en_corte' && $nuevoEstado === 'en_corte' && ! $orden->tela_reservada) {
|
|
if ($orden->tela_id && $orden->metros_requeridos > 0) {
|
|
$tela = \App\Models\Tela::find($orden->tela_id);
|
|
if (! $tela) {
|
|
throw \Illuminate\Validation\ValidationException::withMessages([
|
|
'tela_id' => 'No se encontró la tela asociada.',
|
|
]);
|
|
}
|
|
|
|
if ($tela->metros_disponibles < $orden->metros_requeridos) {
|
|
throw \Illuminate\Validation\ValidationException::withMessages([
|
|
'metros_requeridos' => 'No hay suficientes metros disponibles para reservar (' . $tela->metros_disponibles . ').',
|
|
]);
|
|
}
|
|
|
|
// Reservar: restar metros y marcar orden
|
|
$tela->metros_disponibles = $tela->metros_disponibles - $orden->metros_requeridos;
|
|
$tela->save();
|
|
|
|
$orden->tela_reservada = true;
|
|
}
|
|
}
|
|
|
|
// Si retrocede desde en_corte y ya estaba reservado, devolver la tela
|
|
if ($originalEstado === 'en_corte' && $nuevoEstado !== 'en_corte' && $orden->tela_reservada) {
|
|
if ($orden->tela_id && $orden->metros_requeridos > 0) {
|
|
$tela = \App\Models\Tela::find($orden->tela_id);
|
|
if ($tela) {
|
|
$tela->metros_disponibles = $tela->metros_disponibles + $orden->metros_requeridos;
|
|
$tela->save();
|
|
}
|
|
}
|
|
|
|
$orden->tela_reservada = false;
|
|
}
|
|
}); }
|
|
|
|
// Cantidad completada (desde procesos de acabados recibidos)
|
|
public function getRealizadoAttribute()
|
|
{
|
|
return (int) $this->procesosAcabado()->whereNotNull('fecha_recepcion')->sum('cantidad_recibida');
|
|
}
|
|
|
|
// 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á)
|
|
}
|
|
}
|
|
}
|