فهرست منبع

spare-parts/reclamations: ordered row and article note in autocomplete

Alexander Musikhin 2 هفته پیش
والد
کامیت
2d47409283

+ 2 - 2
app/Http/Controllers/SparePartController.php

@@ -294,11 +294,11 @@ class SparePartController extends Controller
         $spareParts = SparePart::query()
             ->where(function ($q) use ($query) {
                 $q->where('article', 'LIKE', '%' . $query . '%')
-                    ->orWhere('used_in_maf', 'LIKE', '%' . $query . '%');
+                    ->orWhere('note', 'LIKE', '%' . $query . '%');
             })
             ->orderBy('article')
             ->limit(20)
-            ->get(['id', 'article', 'used_in_maf']);
+            ->get(['id', 'article', 'note']);
 
         return response()->json($spareParts);
     }

+ 24 - 0
app/Models/SparePart.php

@@ -66,6 +66,9 @@ class SparePart extends Model
         'quantity_without_docs',
         'quantity_with_docs',
         'total_quantity',
+        'ordered_without_docs',
+        'ordered_with_docs',
+        'total_ordered',
     ];
 
     // Аксессоры для цен (копейки -> рубли)
@@ -263,6 +266,27 @@ class SparePart extends Model
         return $this->reserved_without_docs + $this->reserved_with_docs;
     }
 
+    public function getOrderedWithoutDocsAttribute(): int
+    {
+        return (int) ($this->orders()
+            ->where('status', SparePartOrder::STATUS_ORDERED)
+            ->where('with_documents', false)
+            ->sum('ordered_quantity') ?? 0);
+    }
+
+    public function getOrderedWithDocsAttribute(): int
+    {
+        return (int) ($this->orders()
+            ->where('status', SparePartOrder::STATUS_ORDERED)
+            ->where('with_documents', true)
+            ->sum('ordered_quantity') ?? 0);
+    }
+
+    public function getTotalOrderedAttribute(): int
+    {
+        return $this->ordered_without_docs + $this->ordered_with_docs;
+    }
+
     /**
      * Общий свободный остаток
      */

+ 6 - 6
resources/views/reclamations/edit.blade.php

@@ -134,7 +134,7 @@
                                     <div class="col-12 col-md-6 position-relative">
                                         <input type="hidden" name="rows[{{ $idx }}][spare_part_id]" value="{{ $sp->id }}" class="spare-part-id">
                                         <input type="text" class="form-control form-control-sm spare-part-search"
-                                               value="{{ $sp->article }}@if($sp->used_in_maf) ({{ $sp->used_in_maf }})@endif"
+                                               value="{{ $sp->article }}@if($sp->note) ({{ $sp->note }})@endif"
                                                placeholder="Введите артикул или название"
                                                autocomplete="off"
                                                @disabled(!hasRole('admin,manager'))>
@@ -699,13 +699,13 @@
                 items.forEach(function(item) {
                     const $item = $('<div class="sp-item"></div>');
                     const highlightedArticle = highlightMatch(item.article, query);
-                    const highlightedUsed = item.used_in_maf ? highlightMatch(item.used_in_maf, query) : '';
+                    const highlightedNote = item.note ? highlightMatch(item.note, query) : '';
 
                     $item.html('<span class="sp-article">' + highlightedArticle + '</span>' +
-                               (item.used_in_maf ? ' <span class="sp-used">(' + highlightedUsed + ')</span>' : ''));
+                               (item.note ? ' <span class="sp-used">(' + highlightedNote + ')</span>' : ''));
                     $item.data('id', item.id);
                     $item.data('article', item.article);
-                    $item.data('used', item.used_in_maf || '');
+                    $item.data('note', item.note || '');
 
                     $item.on('click', function() {
                         selectItem($(this));
@@ -729,8 +729,8 @@
 
             function selectItem($item) {
                 const article = $item.data('article');
-                const used = $item.data('used');
-                const displayText = article + (used ? ' (' + used + ')' : '');
+                const note = $item.data('note');
+                const displayText = article + (note ? ' (' + note + ')' : '');
 
                 $input.val(displayText);
                 $hiddenId.val($item.data('id'));

+ 12 - 6
resources/views/spare_parts/edit.blade.php

@@ -259,18 +259,24 @@
                                         <td class="text-center">{{ $spare_part->reserved_with_docs }}</td>
                                         <td class="text-center fw-bold">{{ $spare_part->total_reserved }}</td>
                                     </tr>
-                                    <tr class="table-success">
-                                        <td>Свободный остаток</td>
-                                        <td class="text-center">{{ $spare_part->free_stock_without_docs }}</td>
-                                        <td class="text-center">{{ $spare_part->free_stock_with_docs }}</td>
-                                        <td class="text-center fw-bold">{{ $spare_part->total_free_stock }}</td>
-                                    </tr>
                                     @if($spare_part->min_stock > 0)
                                         <tr class="@if($spare_part->total_free_stock < $spare_part->min_stock) table-danger @endif">
                                             <td>Минимальный остаток</td>
                                             <td colspan="3" class="text-center">{{ $spare_part->min_stock }}</td>
                                         </tr>
                                     @endif
+                                    <tr class="table-info">
+                                        <td>Заказано</td>
+                                        <td class="text-center">{{ $spare_part->ordered_without_docs }}</td>
+                                        <td class="text-center">{{ $spare_part->ordered_with_docs }}</td>
+                                        <td class="text-center fw-bold">{{ $spare_part->total_ordered }}</td>
+                                    </tr>
+                                    <tr class="table-success">
+                                        <td>Свободный остаток</td>
+                                        <td class="text-center">{{ $spare_part->free_stock_without_docs }}</td>
+                                        <td class="text-center">{{ $spare_part->free_stock_with_docs }}</td>
+                                        <td class="text-center fw-bold">{{ $spare_part->total_free_stock }}</td>
+                                    </tr>
                                 </tbody>
                             </table>
                         </div>

+ 24 - 0
tests/Feature/ReclamationControllerTest.php

@@ -156,6 +156,30 @@ class ReclamationControllerTest extends TestCase
         $response->assertViewIs('reclamations.edit');
     }
 
+    public function test_reclamation_details_show_spare_part_note_in_input_instead_of_used_in_maf(): void
+    {
+        $sparePart = \App\Models\SparePart::factory()->create([
+            'article' => 'SP-100',
+            'used_in_maf' => 'Старое значение',
+            'note' => 'Показать это примечание',
+        ]);
+        $reclamation = Reclamation::factory()->create();
+        $reclamation->spareParts()->attach($sparePart->id, [
+            'quantity' => 1,
+            'with_documents' => false,
+            'status' => 'pending',
+            'reserved_qty' => 0,
+            'issued_qty' => 0,
+        ]);
+
+        $response = $this->actingAs($this->managerUser)
+            ->get(route('reclamations.show', $reclamation));
+
+        $response->assertOk();
+        $response->assertSee('SP-100 (Показать это примечание)');
+        $response->assertDontSee('SP-100 (Старое значение)');
+    }
+
     public function test_brigadier_cannot_view_reclamation_details_with_hidden_status(): void
     {
         $reclamation = Reclamation::factory()->create([

+ 43 - 2
tests/Feature/SparePartControllerTest.php

@@ -260,13 +260,54 @@ class SparePartControllerTest extends TestCase
 
     public function test_admin_can_search_spare_parts(): void
     {
-        SparePart::factory()->create(['article' => 'SP-SEARCH-001']);
+        SparePart::factory()->create([
+            'article' => 'SP-SEARCH-001',
+            'note' => 'Комментарий поиска',
+        ]);
 
         $response = $this->actingAs($this->adminUser)
             ->getJson(route('spare_parts.search', ['query' => 'SP-SEARCH']));
 
         $response->assertStatus(200);
-        $response->assertJsonStructure([['id', 'article', 'used_in_maf']]);
+        $response->assertJsonStructure([['id', 'article', 'note']]);
+        $response->assertJsonFragment([
+            'article' => 'SP-SEARCH-001',
+            'note' => 'Комментарий поиска',
+        ]);
+    }
+
+    public function test_spare_part_show_displays_ordered_row_separately_from_stock(): void
+    {
+        $sparePart = SparePart::factory()->create(['min_stock' => 5]);
+        \App\Models\SparePartOrder::factory()->forSparePart($sparePart)->ordered()->withDocuments(false)->withQuantity(4)->create();
+        \App\Models\SparePartOrder::factory()->forSparePart($sparePart)->ordered()->withDocuments(true)->withQuantity(6)->create();
+        \App\Models\SparePartOrder::factory()->forSparePart($sparePart)->inStock()->withDocuments(false)->withQuantity(10)->create();
+
+        $response = $this->actingAs($this->adminUser)
+            ->get(route('spare_parts.show', $sparePart));
+
+        $response->assertOk();
+        $response->assertSee('Заказано');
+        $response->assertSee('>4<', false);
+        $response->assertSee('>6<', false);
+        $response->assertSee('>10<', false);
+    }
+
+    public function test_spare_part_search_can_find_by_note(): void
+    {
+        SparePart::factory()->create([
+            'article' => 'SP-NOTE-001',
+            'note' => 'Особая отметка',
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->getJson(route('spare_parts.search', ['query' => 'Особая отметка']));
+
+        $response->assertOk();
+        $response->assertJsonFragment([
+            'article' => 'SP-NOTE-001',
+            'note' => 'Особая отметка',
+        ]);
     }
 
     public function test_guest_cannot_search_spare_parts(): void