|
|
@@ -3,7 +3,9 @@
|
|
|
namespace App\Services;
|
|
|
|
|
|
use App\Models\InventoryMovement;
|
|
|
+use App\Models\ReclamationSparePart;
|
|
|
use App\Models\Reservation;
|
|
|
+use App\Models\Shortage;
|
|
|
use App\Models\SparePartOrder;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
@@ -114,6 +116,92 @@ class SparePartIssueService
|
|
|
return $results;
|
|
|
}
|
|
|
|
|
|
+ public function issueReservationFromSelectedOrder(
|
|
|
+ Reservation $reservation,
|
|
|
+ int $selectedOrderId,
|
|
|
+ string $note = ''
|
|
|
+ ): IssueResult {
|
|
|
+ if (!$reservation->isActive()) {
|
|
|
+ throw new \InvalidArgumentException('Резерв не активен, списание невозможно');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((int) $reservation->spare_part_order_id === $selectedOrderId) {
|
|
|
+ $result = $this->issueReservation($reservation, $note);
|
|
|
+ $this->syncPivotRowForReservation($reservation);
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ return DB::transaction(function () use ($reservation, $selectedOrderId, $note) {
|
|
|
+ $reservation = Reservation::query()->lockForUpdate()->findOrFail($reservation->id);
|
|
|
+ $selectedOrder = SparePartOrder::query()->lockForUpdate()->findOrFail($selectedOrderId);
|
|
|
+
|
|
|
+ if (!$reservation->isActive()) {
|
|
|
+ throw new \InvalidArgumentException('Резерв не активен, списание невозможно');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((int) $selectedOrder->spare_part_id !== (int) $reservation->spare_part_id) {
|
|
|
+ throw new \InvalidArgumentException('Выбран заказ другой запчасти');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((bool) $selectedOrder->with_documents !== (bool) $reservation->with_documents) {
|
|
|
+ throw new \InvalidArgumentException('Выбран заказ с другим признаком документов');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($selectedOrder->status !== SparePartOrder::STATUS_IN_STOCK) {
|
|
|
+ throw new \InvalidArgumentException('Списание возможно только из партии на складе');
|
|
|
+ }
|
|
|
+
|
|
|
+ $alreadyReservedOnSelected = Reservation::query()
|
|
|
+ ->where('spare_part_order_id', $selectedOrder->id)
|
|
|
+ ->where('status', Reservation::STATUS_ACTIVE)
|
|
|
+ ->sum('reserved_qty');
|
|
|
+
|
|
|
+ $freeQty = max(0, (int) $selectedOrder->available_qty - (int) $alreadyReservedOnSelected);
|
|
|
+ if ($freeQty < 1) {
|
|
|
+ throw new \RuntimeException('В выбранном заказе нет доступного остатка');
|
|
|
+ }
|
|
|
+
|
|
|
+ $requestedQty = (int) $reservation->reserved_qty;
|
|
|
+ $issueQty = min($requestedQty, $freeQty);
|
|
|
+
|
|
|
+ app(SparePartReservationService::class)
|
|
|
+ ->cancelReservation($reservation, 'Перенос резерва на другой заказ для списания');
|
|
|
+
|
|
|
+ $reserveMovement = InventoryMovement::create([
|
|
|
+ 'spare_part_order_id' => $selectedOrder->id,
|
|
|
+ 'spare_part_id' => $reservation->spare_part_id,
|
|
|
+ 'qty' => $issueQty,
|
|
|
+ 'movement_type' => InventoryMovement::TYPE_RESERVE,
|
|
|
+ 'source_type' => InventoryMovement::SOURCE_RECLAMATION,
|
|
|
+ 'source_id' => $reservation->reclamation_id,
|
|
|
+ 'with_documents' => $reservation->with_documents,
|
|
|
+ 'user_id' => auth()->id(),
|
|
|
+ 'note' => "Резервирование для списания из выбранного заказа #{$selectedOrder->id}",
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $selectedReservation = Reservation::create([
|
|
|
+ 'spare_part_id' => $reservation->spare_part_id,
|
|
|
+ 'spare_part_order_id' => $selectedOrder->id,
|
|
|
+ 'reclamation_id' => $reservation->reclamation_id,
|
|
|
+ 'reserved_qty' => $issueQty,
|
|
|
+ 'with_documents' => $reservation->with_documents,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ 'movement_id' => $reserveMovement->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $result = $this->issueReservation($selectedReservation, $note);
|
|
|
+
|
|
|
+ if ($issueQty < $requestedQty) {
|
|
|
+ $this->splitPivotRowAfterPartialIssue($reservation, $issueQty, $requestedQty - $issueQty);
|
|
|
+ } else {
|
|
|
+ $this->syncPivotRowForReservation($reservation);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Прямое списание без резерва (для ручных операций)
|
|
|
*
|
|
|
@@ -216,6 +304,85 @@ class SparePartIssueService
|
|
|
]);
|
|
|
});
|
|
|
}
|
|
|
+
|
|
|
+ private function syncPivotRowForReservation(Reservation $reservation): void
|
|
|
+ {
|
|
|
+ $row = ReclamationSparePart::query()
|
|
|
+ ->where('reclamation_id', $reservation->reclamation_id)
|
|
|
+ ->where('spare_part_id', $reservation->spare_part_id)
|
|
|
+ ->where('with_documents', $reservation->with_documents)
|
|
|
+ ->orderBy('id')
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if (!$row) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $activeQty = (int) Reservation::query()
|
|
|
+ ->where('reclamation_id', $reservation->reclamation_id)
|
|
|
+ ->where('spare_part_id', $reservation->spare_part_id)
|
|
|
+ ->where('with_documents', $reservation->with_documents)
|
|
|
+ ->where('status', Reservation::STATUS_ACTIVE)
|
|
|
+ ->sum('reserved_qty');
|
|
|
+
|
|
|
+ $issuedQty = (int) Reservation::query()
|
|
|
+ ->where('reclamation_id', $reservation->reclamation_id)
|
|
|
+ ->where('spare_part_id', $reservation->spare_part_id)
|
|
|
+ ->where('with_documents', $reservation->with_documents)
|
|
|
+ ->where('status', Reservation::STATUS_ISSUED)
|
|
|
+ ->sum('reserved_qty');
|
|
|
+
|
|
|
+ $row->update([
|
|
|
+ 'reserved_qty' => $activeQty,
|
|
|
+ 'issued_qty' => min($row->quantity, $issuedQty),
|
|
|
+ 'status' => $issuedQty >= $row->quantity
|
|
|
+ ? 'issued'
|
|
|
+ : ($activeQty > 0 ? 'reserved' : 'pending'),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function splitPivotRowAfterPartialIssue(Reservation $reservation, int $issuedQty, int $remainingQty): void
|
|
|
+ {
|
|
|
+ $row = ReclamationSparePart::query()
|
|
|
+ ->where('reclamation_id', $reservation->reclamation_id)
|
|
|
+ ->where('spare_part_id', $reservation->spare_part_id)
|
|
|
+ ->where('with_documents', $reservation->with_documents)
|
|
|
+ ->where('quantity', '>=', $reservation->reserved_qty)
|
|
|
+ ->orderBy('id')
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if (!$row) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $row->update([
|
|
|
+ 'quantity' => $issuedQty,
|
|
|
+ 'reserved_qty' => 0,
|
|
|
+ 'issued_qty' => $issuedQty,
|
|
|
+ 'status' => 'issued',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ ReclamationSparePart::query()->create([
|
|
|
+ 'reclamation_id' => $row->reclamation_id,
|
|
|
+ 'spare_part_id' => $row->spare_part_id,
|
|
|
+ 'quantity' => $remainingQty,
|
|
|
+ 'with_documents' => $row->with_documents,
|
|
|
+ 'status' => 'pending',
|
|
|
+ 'reserved_qty' => 0,
|
|
|
+ 'issued_qty' => 0,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ Shortage::query()->create([
|
|
|
+ 'spare_part_id' => $row->spare_part_id,
|
|
|
+ 'reclamation_id' => $row->reclamation_id,
|
|
|
+ 'with_documents' => $row->with_documents,
|
|
|
+ 'required_qty' => $remainingQty,
|
|
|
+ 'reserved_qty' => 0,
|
|
|
+ 'missing_qty' => $remainingQty,
|
|
|
+ 'status' => Shortage::STATUS_OPEN,
|
|
|
+ 'note' => 'Остаток после частичного списания из выбранного заказа',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|