InventoryMovement.php 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. <?php
  2. namespace App\Models;
  3. use Illuminate\Database\Eloquent\Factories\HasFactory;
  4. use Illuminate\Database\Eloquent\Model;
  5. use Illuminate\Database\Eloquent\Relations\BelongsTo;
  6. /**
  7. * Движение запчастей — единственный механизм изменения остатков.
  8. *
  9. * Все операции фиксируются здесь для полного аудита:
  10. * - receipt: поступление на склад
  11. * - reserve: резервирование под рекламацию
  12. * - issue: списание (отгрузка)
  13. * - reserve_cancel: отмена резерва
  14. * - correction: инвентаризационная коррекция
  15. */
  16. class InventoryMovement extends Model
  17. {
  18. use HasFactory;
  19. // Типы движений
  20. const TYPE_RECEIPT = 'receipt';
  21. const TYPE_RESERVE = 'reserve';
  22. const TYPE_ISSUE = 'issue';
  23. const TYPE_RESERVE_CANCEL = 'reserve_cancel';
  24. const TYPE_CORRECTION_PLUS = 'correction_plus'; // Увеличение при инвентаризации
  25. const TYPE_CORRECTION_MINUS = 'correction_minus'; // Уменьшение при инвентаризации
  26. /** @deprecated Используйте TYPE_CORRECTION_PLUS или TYPE_CORRECTION_MINUS */
  27. const TYPE_CORRECTION = 'correction';
  28. const TYPE_NAMES = [
  29. self::TYPE_RECEIPT => 'Поступление',
  30. self::TYPE_RESERVE => 'Резервирование',
  31. self::TYPE_ISSUE => 'Списание',
  32. self::TYPE_RESERVE_CANCEL => 'Отмена резерва',
  33. self::TYPE_CORRECTION => 'Коррекция',
  34. self::TYPE_CORRECTION_PLUS => 'Коррекция (+)',
  35. self::TYPE_CORRECTION_MINUS => 'Коррекция (-)',
  36. ];
  37. // Типы источников
  38. const SOURCE_ORDER = 'order';
  39. const SOURCE_RECLAMATION = 'reclamation';
  40. const SOURCE_MANUAL = 'manual';
  41. const SOURCE_SHORTAGE_FULFILLMENT = 'shortage_fulfillment';
  42. const SOURCE_INVENTORY = 'inventory';
  43. protected $fillable = [
  44. 'spare_part_order_id',
  45. 'spare_part_id',
  46. 'qty',
  47. 'movement_type',
  48. 'source_type',
  49. 'source_id',
  50. 'with_documents',
  51. 'user_id',
  52. 'note',
  53. ];
  54. protected $casts = [
  55. 'qty' => 'integer',
  56. 'with_documents' => 'boolean',
  57. 'source_id' => 'integer',
  58. ];
  59. // Отношения
  60. public function sparePartOrder(): BelongsTo
  61. {
  62. return $this->belongsTo(SparePartOrder::class);
  63. }
  64. public function sparePart(): BelongsTo
  65. {
  66. return $this->belongsTo(SparePart::class);
  67. }
  68. public function user(): BelongsTo
  69. {
  70. return $this->belongsTo(User::class);
  71. }
  72. // Аксессоры
  73. public function getTypeNameAttribute(): string
  74. {
  75. return self::TYPE_NAMES[$this->movement_type] ?? $this->movement_type;
  76. }
  77. /**
  78. * Получить источник движения (полиморфная связь)
  79. */
  80. public function getSourceAttribute(): ?Model
  81. {
  82. if (!$this->source_type || !$this->source_id) {
  83. return null;
  84. }
  85. return match ($this->source_type) {
  86. self::SOURCE_RECLAMATION => Reclamation::find($this->source_id),
  87. self::SOURCE_SHORTAGE_FULFILLMENT => Shortage::find($this->source_id),
  88. default => null,
  89. };
  90. }
  91. // Scopes
  92. public function scopeOfType($query, string $type)
  93. {
  94. return $query->where('movement_type', $type);
  95. }
  96. public function scopeForSparePart($query, int $sparePartId)
  97. {
  98. return $query->where('spare_part_id', $sparePartId);
  99. }
  100. public function scopeWithDocuments($query, bool $withDocs = true)
  101. {
  102. return $query->where('with_documents', $withDocs);
  103. }
  104. /**
  105. * Движения, которые увеличивают доступный остаток
  106. */
  107. public function scopeIncreasing($query)
  108. {
  109. return $query->whereIn('movement_type', [
  110. self::TYPE_RECEIPT,
  111. self::TYPE_RESERVE_CANCEL,
  112. self::TYPE_CORRECTION_PLUS,
  113. ]);
  114. }
  115. /**
  116. * Движения, которые уменьшают доступный остаток
  117. */
  118. public function scopeDecreasing($query)
  119. {
  120. return $query->whereIn('movement_type', [
  121. self::TYPE_RESERVE,
  122. self::TYPE_ISSUE,
  123. self::TYPE_CORRECTION_MINUS,
  124. ]);
  125. }
  126. /**
  127. * Проверка типа коррекции
  128. */
  129. public function isCorrection(): bool
  130. {
  131. return in_array($this->movement_type, [
  132. self::TYPE_CORRECTION,
  133. self::TYPE_CORRECTION_PLUS,
  134. self::TYPE_CORRECTION_MINUS,
  135. ]);
  136. }
  137. }