InventoryMovement.php 4.6 KB

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