| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- <?php
- namespace App\Models;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Database\Eloquent\Relations\BelongsTo;
- /**
- * Движение запчастей — единственный механизм изменения остатков.
- *
- * Все операции фиксируются здесь для полного аудита:
- * - receipt: поступление на склад
- * - reserve: резервирование под рекламацию
- * - issue: списание (отгрузка)
- * - reserve_cancel: отмена резерва
- * - correction: инвентаризационная коррекция
- */
- class InventoryMovement extends Model
- {
- // Типы движений
- const TYPE_RECEIPT = 'receipt';
- const TYPE_RESERVE = 'reserve';
- const TYPE_ISSUE = 'issue';
- const TYPE_RESERVE_CANCEL = 'reserve_cancel';
- const TYPE_CORRECTION_PLUS = 'correction_plus'; // Увеличение при инвентаризации
- const TYPE_CORRECTION_MINUS = 'correction_minus'; // Уменьшение при инвентаризации
- /** @deprecated Используйте TYPE_CORRECTION_PLUS или TYPE_CORRECTION_MINUS */
- const TYPE_CORRECTION = 'correction';
- const TYPE_NAMES = [
- self::TYPE_RECEIPT => 'Поступление',
- self::TYPE_RESERVE => 'Резервирование',
- self::TYPE_ISSUE => 'Списание',
- self::TYPE_RESERVE_CANCEL => 'Отмена резерва',
- self::TYPE_CORRECTION => 'Коррекция',
- self::TYPE_CORRECTION_PLUS => 'Коррекция (+)',
- self::TYPE_CORRECTION_MINUS => 'Коррекция (-)',
- ];
- // Типы источников
- const SOURCE_ORDER = 'order';
- const SOURCE_RECLAMATION = 'reclamation';
- const SOURCE_MANUAL = 'manual';
- const SOURCE_SHORTAGE_FULFILLMENT = 'shortage_fulfillment';
- const SOURCE_INVENTORY = 'inventory';
- protected $fillable = [
- 'spare_part_order_id',
- 'spare_part_id',
- 'qty',
- 'movement_type',
- 'source_type',
- 'source_id',
- 'with_documents',
- 'user_id',
- 'note',
- ];
- protected $casts = [
- 'qty' => 'integer',
- 'with_documents' => 'boolean',
- 'source_id' => 'integer',
- ];
- // Отношения
- public function sparePartOrder(): BelongsTo
- {
- return $this->belongsTo(SparePartOrder::class);
- }
- public function sparePart(): BelongsTo
- {
- return $this->belongsTo(SparePart::class);
- }
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class);
- }
- // Аксессоры
- public function getTypeNameAttribute(): string
- {
- return self::TYPE_NAMES[$this->movement_type] ?? $this->movement_type;
- }
- /**
- * Получить источник движения (полиморфная связь)
- */
- public function getSourceAttribute(): ?Model
- {
- if (!$this->source_type || !$this->source_id) {
- return null;
- }
- return match ($this->source_type) {
- self::SOURCE_RECLAMATION => Reclamation::find($this->source_id),
- self::SOURCE_SHORTAGE_FULFILLMENT => Shortage::find($this->source_id),
- default => null,
- };
- }
- // Scopes
- public function scopeOfType($query, string $type)
- {
- return $query->where('movement_type', $type);
- }
- 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 scopeIncreasing($query)
- {
- return $query->whereIn('movement_type', [
- self::TYPE_RECEIPT,
- self::TYPE_RESERVE_CANCEL,
- self::TYPE_CORRECTION_PLUS,
- ]);
- }
- /**
- * Движения, которые уменьшают доступный остаток
- */
- public function scopeDecreasing($query)
- {
- return $query->whereIn('movement_type', [
- self::TYPE_RESERVE,
- self::TYPE_ISSUE,
- self::TYPE_CORRECTION_MINUS,
- ]);
- }
- /**
- * Проверка типа коррекции
- */
- public function isCorrection(): bool
- {
- return in_array($this->movement_type, [
- self::TYPE_CORRECTION,
- self::TYPE_CORRECTION_PLUS,
- self::TYPE_CORRECTION_MINUS,
- ]);
- }
- }
|