| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- <?php
- namespace App\Models;
- use Illuminate\Database\Eloquent\Factories\HasFactory;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Database\Eloquent\Relations\BelongsTo;
- use Illuminate\Database\Eloquent\Relations\HasMany;
- /**
- * Дефицит (нехватка) запчасти.
- *
- * Создаётся когда при резервировании недостаточно свободного остатка.
- * НЕ влияет на физический остаток (вместо отрицательных значений).
- *
- * При поступлении новой партии система автоматически
- * резервирует под открытые дефициты (FIFO по дате создания).
- */
- class Shortage extends Model
- {
- use HasFactory;
- const STATUS_OPEN = 'open';
- const STATUS_CLOSED = 'closed';
- const STATUS_NAMES = [
- self::STATUS_OPEN => 'Открыт',
- self::STATUS_CLOSED => 'Закрыт',
- ];
- protected $fillable = [
- 'spare_part_id',
- 'reclamation_id',
- 'with_documents',
- 'required_qty',
- 'reserved_qty',
- 'missing_qty',
- 'status',
- 'note',
- ];
- protected $casts = [
- 'required_qty' => 'integer',
- 'reserved_qty' => 'integer',
- 'missing_qty' => 'integer',
- 'with_documents' => 'boolean',
- ];
- // Отношения
- public function sparePart(): BelongsTo
- {
- return $this->belongsTo(SparePart::class);
- }
- public function reclamation(): BelongsTo
- {
- return $this->belongsTo(Reclamation::class);
- }
- /**
- * Получить резервы, связанные с этим дефицитом
- *
- * Примечание: Не использовать для eager loading!
- * Для получения резервов используйте метод getRelatedReservations()
- */
- public function getRelatedReservations(): \Illuminate\Database\Eloquent\Collection
- {
- return Reservation::query()
- ->where('reclamation_id', $this->reclamation_id)
- ->where('spare_part_id', $this->spare_part_id)
- ->where('with_documents', $this->with_documents)
- ->get();
- }
- /**
- * Резервы для той же рекламации (для базовой связи)
- */
- public function reclamationReservations(): HasMany
- {
- return $this->hasMany(Reservation::class, 'reclamation_id', 'reclamation_id');
- }
- // Аксессоры
- public function getStatusNameAttribute(): string
- {
- return self::STATUS_NAMES[$this->status] ?? $this->status;
- }
- public function isOpen(): bool
- {
- return $this->status === self::STATUS_OPEN;
- }
- public function isClosed(): bool
- {
- return $this->status === self::STATUS_CLOSED;
- }
- /**
- * Процент покрытия дефицита
- */
- public function getCoveragePercentAttribute(): float
- {
- if ($this->required_qty === 0) {
- return 100;
- }
- return round(($this->reserved_qty / $this->required_qty) * 100, 1);
- }
- // Scopes
- public function scopeOpen($query)
- {
- return $query->where('status', self::STATUS_OPEN);
- }
- public function scopeClosed($query)
- {
- return $query->where('status', self::STATUS_CLOSED);
- }
- public function scopeForSparePart($query, int $sparePartId)
- {
- return $query->where('spare_part_id', $sparePartId);
- }
- public function scopeWithDocuments($query, bool $withDocs = true)
- {
- return $query->where('with_documents', $withDocs);
- }
- public function scopeForReclamation($query, int $reclamationId)
- {
- return $query->where('reclamation_id', $reclamationId);
- }
- /**
- * Дефициты с недостачей (для автозакрытия при поступлении)
- */
- public function scopeWithMissing($query)
- {
- return $query->where('missing_qty', '>', 0);
- }
- /**
- * FIFO сортировка для обработки
- */
- public function scopeOldestFirst($query)
- {
- return $query->orderBy('created_at', 'asc');
- }
- // Методы
- /**
- * Добавить резерв к дефициту (при поступлении партии)
- */
- public function addReserved(int $qty): void
- {
- $this->reserved_qty += $qty;
- $this->missing_qty = max(0, $this->required_qty - $this->reserved_qty);
- if ($this->missing_qty === 0) {
- $this->status = self::STATUS_CLOSED;
- }
- $this->save();
- }
- /**
- * Пересчитать дефицит на основе резервов
- */
- public function recalculate(): void
- {
- $totalReserved = Reservation::query()
- ->where('reclamation_id', $this->reclamation_id)
- ->where('spare_part_id', $this->spare_part_id)
- ->where('with_documents', $this->with_documents)
- ->whereIn('status', [Reservation::STATUS_ACTIVE, Reservation::STATUS_ISSUED])
- ->sum('reserved_qty');
- $this->reserved_qty = $totalReserved;
- $this->missing_qty = max(0, $this->required_qty - $this->reserved_qty);
- if ($this->missing_qty === 0) {
- $this->status = self::STATUS_CLOSED;
- }
- $this->save();
- }
- }
|