Browse Source

fix maf edit in order

Alexander Musikhin 2 weeks ago
parent
commit
12125408d1

+ 175 - 23
app/Http/Controllers/OrderController.php

@@ -30,6 +30,7 @@ use App\Models\User;
 use App\Services\FileService;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Storage;
 use Symfony\Component\HttpFoundation\StreamedResponse;
 use ZipArchive;
@@ -163,13 +164,79 @@ class OrderController extends Controller
 
         // меняем список товаров заказа только если статус новый
         if($products && $quantities && ($order->order_status_id == 1)) {
-            // remove from products_sku
-            ProductSKU::query()->where('order_id', $order->id)->delete();
-            foreach ($products as $key => $product) {
-                for($i = 0; $i < $quantities[$key]; $i++) {
+            $desiredCounts = [];
+            foreach ($products as $key => $productId) {
+                $quantity = (int)($quantities[$key] ?? 0);
+                if ($quantity < 1) {
+                    continue;
+                }
+
+                if (!isset($desiredCounts[$productId])) {
+                    $desiredCounts[$productId] = 0;
+                }
+                $desiredCounts[$productId] += $quantity;
+            }
+
+            $attachedSkus = ProductSKU::query()
+                ->where('order_id', $order->id)
+                ->whereNotNull('maf_order_id')
+                ->with('product:id,article')
+                ->get();
+
+            $attachedByProduct = [];
+            foreach ($attachedSkus as $sku) {
+                if (!isset($attachedByProduct[$sku->product_id])) {
+                    $attachedByProduct[$sku->product_id] = 0;
+                }
+                $attachedByProduct[$sku->product_id]++;
+            }
+
+            $currentCounts = ProductSKU::query()
+                ->where('order_id', $order->id)
+                ->selectRaw('product_id, COUNT(*) as cnt')
+                ->groupBy('product_id')
+                ->pluck('cnt', 'product_id')
+                ->map(fn ($count) => (int)$count)
+                ->toArray();
+
+            $errors = [];
+            foreach ($attachedByProduct as $productId => $attachedCount) {
+                $desiredCount = $desiredCounts[$productId] ?? 0;
+                if ($desiredCount < $attachedCount) {
+                    $article = $attachedSkus->firstWhere('product_id', $productId)?->product?->article ?? ('ID ' . $productId);
+                    $errors[] = "Нельзя удалить привязанные МАФ {$article}: привязано {$attachedCount} шт.";
+                }
+            }
+
+            // Если есть хотя бы один привязанный МАФ, запрещаем добавлять новые позиции/количество.
+            if (!empty($attachedByProduct)) {
+                foreach ($desiredCounts as $productId => $desiredCount) {
+                    $currentCount = $currentCounts[$productId] ?? 0;
+                    if ($desiredCount > $currentCount) {
+                        $errors[] = 'Нельзя добавлять новые позиции МАФ, пока на площадке есть привязанные МАФ.';
+                        break;
+                    }
+                }
+            }
+
+            if (!empty($errors)) {
+                return redirect()->back()->withInput()->with(['danger' => $errors]);
+            }
+
+            // Удаляем только непривязанные SKU и пересоздаём их по новому списку.
+            ProductSKU::query()
+                ->where('order_id', $order->id)
+                ->whereNull('maf_order_id')
+                ->delete();
+
+            foreach ($desiredCounts as $productId => $desiredCount) {
+                $attachedCount = $attachedByProduct[$productId] ?? 0;
+                $toCreate = $desiredCount - $attachedCount;
+
+                for ($i = 0; $i < $toCreate; $i++) {
                     ProductSKU::query()->create([
                         'order_id' => $order->id,
-                        'product_id' => $product,
+                        'product_id' => $productId,
                         'status' => 'требуется'
                     ]);
                 }
@@ -228,23 +295,64 @@ class OrderController extends Controller
      */
     public function getMafToOrder(Order $order)
     {
-        foreach ($order->products_sku as $product_sku) {
-            // dont connect maf to order if already connected
-            if($product_sku->maf_order) {
-                continue;
+        $attached = 0;
+        $missingByProduct = [];
+
+        DB::transaction(function () use ($order, &$attached, &$missingByProduct) {
+            foreach ($order->products_sku as $productSku) {
+                // Уже привязан, пропускаем
+                if ($productSku->maf_order_id) {
+                    continue;
+                }
+
+                $mafOrder = MafOrder::query()
+                    ->where('product_id', $productSku->product_id)
+                    ->where('in_stock', '>', 0)
+                    ->orderBy('created_at')
+                    ->lockForUpdate()
+                    ->first();
+
+                if (!$mafOrder) {
+                    $article = $productSku->product?->article ?? ('ID ' . $productSku->product_id);
+                    if (!isset($missingByProduct[$productSku->product_id])) {
+                        $missingByProduct[$productSku->product_id] = ['article' => $article, 'count' => 0];
+                    }
+                    $missingByProduct[$productSku->product_id]['count']++;
+                    continue;
+                }
+
+                $productSku->update(['maf_order_id' => $mafOrder->id, 'status' => 'отгружен']);
+                $mafOrder->decrement('in_stock');
+                $attached++;
             }
+        });
 
-            $mafOrder = MafOrder::query()
-                ->where('product_id', $product_sku->product_id)
-                ->where('in_stock', '>' , 0)
-                ->orderBy('created_at')
-                ->first();
-            $product_sku->update(['maf_order_id' => $mafOrder->id, 'status' => 'отгружен']);
-            $mafOrder->decrement('in_stock');
-            unset($mafOrder, $product_sku);
-        }
         $order->autoChangeStatus();
-        return redirect()->route('order.show', $order);
+
+        $success = [];
+        $danger = [];
+
+        if ($attached > 0) {
+            $success[] = "Привязано МАФ: {$attached}.";
+        }
+
+        foreach ($missingByProduct as $missing) {
+            $danger[] = "Не удалось привязать {$missing['count']} шт. МАФ {$missing['article']}: нет остатка на складе.";
+        }
+
+        if ($attached === 0 && empty($danger)) {
+            $danger[] = 'Нет МАФ для привязки: все МАФы на площадке уже привязаны.';
+        }
+
+        $flash = [];
+        if (!empty($success)) {
+            $flash['success'] = $success;
+        }
+        if (!empty($danger)) {
+            $flash['danger'] = $danger;
+        }
+
+        return redirect()->route('order.show', $order)->with($flash);
     }
 
     /**
@@ -260,12 +368,56 @@ class OrderController extends Controller
 
     public function revertMaf(Order $order)
     {
-        foreach ($order->products_sku as $maf) {
-            MafOrder::query()->where('id', $maf->maf_order_id)->increment('in_stock');
-            $maf->update(['maf_order_id' => null, 'status' => 'требуется']);
+        $detached = 0;
+        $notFoundMafOrders = 0;
+
+        DB::transaction(function () use ($order, &$detached, &$notFoundMafOrders) {
+            foreach ($order->products_sku as $maf) {
+                if (!$maf->maf_order_id) {
+                    continue;
+                }
+
+                $affectedRows = MafOrder::query()
+                    ->where('id', $maf->maf_order_id)
+                    ->lockForUpdate()
+                    ->increment('in_stock');
+
+                if (!$affectedRows) {
+                    $notFoundMafOrders++;
+                    continue;
+                }
+
+                $maf->update(['maf_order_id' => null, 'status' => 'требуется']);
+                $detached++;
+            }
+        });
+
+        $order->autoChangeStatus();
+
+        $success = [];
+        $danger = [];
+
+        if ($detached > 0) {
+            $success[] = "Отвязано МАФ: {$detached}.";
         }
 
-        return redirect()->route('order.show', $order);
+        if ($notFoundMafOrders > 0) {
+            $danger[] = "Не удалось отвязать {$notFoundMafOrders} шт. МАФ: связанный заказ МАФ не найден.";
+        }
+
+        if ($detached === 0 && empty($danger)) {
+            $danger[] = 'Нет МАФ для отвязки: на площадке нет привязанных МАФ.';
+        }
+
+        $flash = [];
+        if (!empty($success)) {
+            $flash['success'] = $success;
+        }
+        if (!empty($danger)) {
+            $flash['danger'] = $danger;
+        }
+
+        return redirect()->route('order.show', $order)->with($flash);
     }
 
     public function moveMaf(Request $request)

+ 12 - 4
resources/views/orders/edit.blade.php

@@ -44,10 +44,16 @@
 
             </div>
             <div class="col-xxl-6">
+                @php
+                    $hasAttachedMaf = isset($order) && $order->products_sku->contains(fn ($sku) => !empty($sku->maf_order_id));
+                @endphp
                 <h4>МАФ</h4>
                 <div>
-                    <input type="text" class="form-control mb-2" @disabled((($order->order_status_id ?? 0) > 1) || !hasRole('admin')) placeholder="Поиск номенклатуры" id="search_maf">
-                    <select id="select_maf" class="form-select mb-3" multiple @disabled((($order->order_status_id ?? 0) > 1) || !hasRole('admin'))></select>
+                    <input type="text" class="form-control mb-2" @disabled((($order->order_status_id ?? 0) > 1) || !hasRole('admin') || $hasAttachedMaf) placeholder="Поиск номенклатуры" id="search_maf">
+                    <select id="select_maf" class="form-select mb-3" multiple @disabled((($order->order_status_id ?? 0) > 1) || !hasRole('admin') || $hasAttachedMaf)></select>
+                    @if($hasAttachedMaf)
+                        <div class="small text-warning mb-2">Добавление новых позиций МАФ недоступно, пока есть привязанные МАФ.</div>
+                    @endif
                 </div>
 
                 <div id="selected_maf">
@@ -67,8 +73,10 @@
                                         <input @disabled((($order->order_status_id ?? 0) > 1) || !hasRole('admin')) class="form-control text-end form-control-sm quantity" type="number" name="quantity[]" value="1" @disabled($order->order_status_id > 1)>
                                     </div>
                                     <div class="p-1">
-                                        @if(($order->order_status_id == 1) && hasRole('admin'))
+                                        @if(($order->order_status_id == 1) && hasRole('admin') && !$p->maf_order_id)
                                             <i onclick="$(this).parent().parent().parent().remove(); $('.changes-message').removeClass('visually-hidden');" class="bi bi-trash text-danger cursor-pointer"></i>
+                                        @elseif($p->maf_order_id)
+                                            <i class="bi bi-lock text-secondary" title="Привязанный МАФ нельзя удалить"></i>
                                         @endif
                                     </div>
                                 </div>
@@ -128,4 +136,4 @@
 
     </script>
 
-@endpush
+@endpush

+ 1 - 1
resources/views/orders/show.blade.php

@@ -311,7 +311,7 @@
                         <div>
                             @if(hasRole('admin'))
                                 <a href="{{ route('order.get-maf', $order) }}"
-                                   class="btn btn-primary btn-sm mb-1 @disabled($order->ready_to_mount == 'Нет' )">Привязать
+                                   class="btn btn-primary btn-sm mb-1">Привязать
                                     все МАФы</a>
                                 <br class="d-md-none">
                                 <a href="{{ route('order.revert-maf', $order) }}" class="btn btn-primary btn-sm mb-1">Отвязать

+ 142 - 0
tests/Feature/OrderControllerTest.php

@@ -270,6 +270,9 @@ class OrderControllerTest extends TestCase
             ->get(route('order.get-maf', $order));
 
         $response->assertRedirect(route('order.show', $order));
+        $response->assertSessionHas('success', function ($messages) {
+            return str_contains(implode(' ', (array) $messages), 'Привязано МАФ: 1.');
+        });
 
         $productSku->refresh();
         $this->assertEquals($mafOrder->id, $productSku->maf_order_id);
@@ -279,6 +282,31 @@ class OrderControllerTest extends TestCase
         $this->assertEquals(4, $mafOrder->in_stock);
     }
 
+    public function test_get_maf_to_order_shows_error_when_not_enough_stock(): void
+    {
+        $product = Product::factory()->create();
+        $order = Order::factory()->create();
+
+        $productSku = ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $product->id,
+            'maf_order_id' => null,
+            'status' => 'требуется',
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->get(route('order.get-maf', $order));
+
+        $response->assertRedirect(route('order.show', $order));
+        $response->assertSessionHas('danger', function ($messages) {
+            return str_contains(implode(' ', (array) $messages), 'Не удалось привязать 1 шт. МАФ');
+        });
+
+        $productSku->refresh();
+        $this->assertNull($productSku->maf_order_id);
+        $this->assertEquals('требуется', $productSku->status);
+    }
+
     public function test_revert_maf_returns_maf_to_stock(): void
     {
         $product = Product::factory()->create();
@@ -301,6 +329,9 @@ class OrderControllerTest extends TestCase
             ->get(route('order.revert-maf', $order));
 
         $response->assertRedirect(route('order.show', $order));
+        $response->assertSessionHas('success', function ($messages) {
+            return str_contains(implode(' ', (array) $messages), 'Отвязано МАФ: 1.');
+        });
 
         $productSku->refresh();
         $this->assertNull($productSku->maf_order_id);
@@ -310,6 +341,117 @@ class OrderControllerTest extends TestCase
         $this->assertEquals(4, $mafOrder->in_stock);
     }
 
+    public function test_revert_maf_shows_error_when_nothing_is_attached(): void
+    {
+        $order = Order::factory()->create();
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'maf_order_id' => null,
+            'status' => 'требуется',
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->get(route('order.revert-maf', $order));
+
+        $response->assertRedirect(route('order.show', $order));
+        $response->assertSessionHas('danger', function ($messages) {
+            return str_contains(implode(' ', (array) $messages), 'Нет МАФ для отвязки');
+        });
+    }
+
+    public function test_store_order_cannot_remove_attached_maf_from_order_list(): void
+    {
+        $attachedProduct = Product::factory()->create();
+        $otherProduct = Product::factory()->create();
+        $order = Order::factory()->create([
+            'order_status_id' => Order::STATUS_NEW,
+        ]);
+
+        $mafOrder = MafOrder::factory()->create([
+            'product_id' => $attachedProduct->id,
+            'in_stock' => 1,
+        ]);
+
+        $attachedSku = ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $attachedProduct->id,
+            'maf_order_id' => $mafOrder->id,
+            'status' => 'отгружен',
+        ]);
+
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $otherProduct->id,
+            'maf_order_id' => null,
+            'status' => 'требуется',
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->post(route('order.store'), [
+                'id' => $order->id,
+                'products' => [$otherProduct->id],
+                'quantity' => [1],
+            ]);
+
+        $response->assertRedirect();
+        $response->assertSessionHas('danger', function ($messages) {
+            return str_contains(implode(' ', (array) $messages), 'Нельзя удалить привязанные МАФ');
+        });
+
+        $this->assertDatabaseHas('products_sku', [
+            'id' => $attachedSku->id,
+            'maf_order_id' => $mafOrder->id,
+            'deleted_at' => null,
+        ]);
+    }
+
+    public function test_store_order_cannot_add_new_positions_when_has_attached_maf(): void
+    {
+        $attachedProduct = Product::factory()->create();
+        $existingProduct = Product::factory()->create();
+        $newProduct = Product::factory()->create();
+        $order = Order::factory()->create([
+            'order_status_id' => Order::STATUS_NEW,
+        ]);
+
+        $mafOrder = MafOrder::factory()->create([
+            'product_id' => $attachedProduct->id,
+            'in_stock' => 1,
+        ]);
+
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $attachedProduct->id,
+            'maf_order_id' => $mafOrder->id,
+            'status' => 'отгружен',
+        ]);
+
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $existingProduct->id,
+            'maf_order_id' => null,
+            'status' => 'требуется',
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->post(route('order.store'), [
+                'id' => $order->id,
+                'products' => [$attachedProduct->id, $existingProduct->id, $newProduct->id],
+                'quantity' => [1, 1, 1],
+            ]);
+
+        $response->assertRedirect();
+        $response->assertSessionHas('danger', function ($messages) {
+            return str_contains(implode(' ', (array) $messages), 'Нельзя добавлять новые позиции МАФ');
+        });
+
+        $this->assertDatabaseMissing('products_sku', [
+            'order_id' => $order->id,
+            'product_id' => $newProduct->id,
+            'deleted_at' => null,
+        ]);
+    }
+
     public function test_move_maf_transfers_sku_to_another_order(): void
     {
         $product = Product::factory()->create();