|
|
@@ -3,6 +3,7 @@
|
|
|
namespace App\Services;
|
|
|
|
|
|
use App\Models\InventoryMovement;
|
|
|
+use App\Models\ReclamationSparePart;
|
|
|
use App\Models\Reservation;
|
|
|
use App\Models\Shortage;
|
|
|
use App\Models\SparePart;
|
|
|
@@ -355,6 +356,171 @@ class SparePartReservationService
|
|
|
->with(['sparePart', 'sparePartOrder'])
|
|
|
->get();
|
|
|
}
|
|
|
+
|
|
|
+ public function reassignReservationToSelectedOrder(
|
|
|
+ Reservation $reservation,
|
|
|
+ int $selectedOrderId
|
|
|
+ ): Reservation {
|
|
|
+ if (!$reservation->isActive()) {
|
|
|
+ throw new \InvalidArgumentException('Резерв не активен, перенос невозможен');
|
|
|
+ }
|
|
|
+
|
|
|
+ return DB::transaction(function () use ($reservation, $selectedOrderId) {
|
|
|
+ $reservation = Reservation::query()->lockForUpdate()->findOrFail($reservation->id);
|
|
|
+ $selectedOrder = SparePartOrder::query()->lockForUpdate()->findOrFail($selectedOrderId);
|
|
|
+
|
|
|
+ if (!$reservation->isActive()) {
|
|
|
+ throw new \InvalidArgumentException('Резерв не активен, перенос невозможен');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((int) $reservation->spare_part_order_id === $selectedOrderId) {
|
|
|
+ return $reservation;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ $reassignedQty = min($requestedQty, $freeQty);
|
|
|
+
|
|
|
+ $this->cancelReservation($reservation, 'Перенос резерва на другой заказ');
|
|
|
+
|
|
|
+ $reserveMovement = InventoryMovement::create([
|
|
|
+ 'spare_part_order_id' => $selectedOrder->id,
|
|
|
+ 'spare_part_id' => $reservation->spare_part_id,
|
|
|
+ 'qty' => $reassignedQty,
|
|
|
+ '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' => $reassignedQty,
|
|
|
+ 'with_documents' => $reservation->with_documents,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ 'movement_id' => $reserveMovement->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($reassignedQty < $requestedQty) {
|
|
|
+ $this->splitPivotRowAfterPartialReassignment(
|
|
|
+ $reservation,
|
|
|
+ $reassignedQty,
|
|
|
+ $requestedQty - $reassignedQty
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ $this->syncPivotRowForReservation($selectedReservation);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $selectedReservation;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ public 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 splitPivotRowAfterPartialReassignment(
|
|
|
+ Reservation $reservation,
|
|
|
+ int $reservedQty,
|
|
|
+ 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' => $reservedQty,
|
|
|
+ 'reserved_qty' => $reservedQty,
|
|
|
+ 'issued_qty' => 0,
|
|
|
+ 'status' => 'reserved',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ 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' => 'Остаток после частичного переноса резерва на выбранный заказ',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|