|
|
@@ -0,0 +1,330 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+namespace Tests\Feature;
|
|
|
+
|
|
|
+use App\Enums\ProductionOrderExecutionType;
|
|
|
+use App\Enums\ProductionOrderStatus;
|
|
|
+use App\Models\CommonCatalogItem;
|
|
|
+use App\Models\ProductionCalendarDay;
|
|
|
+use App\Models\ProductionOrder;
|
|
|
+use App\Models\ProductionOrderItem;
|
|
|
+use App\Models\User;
|
|
|
+use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
+use Tests\TestCase;
|
|
|
+
|
|
|
+class ProductionOrderControllerTest extends TestCase
|
|
|
+{
|
|
|
+ use RefreshDatabase;
|
|
|
+
|
|
|
+ protected bool $seed = true;
|
|
|
+
|
|
|
+ private User $admin;
|
|
|
+
|
|
|
+ private User $manager;
|
|
|
+
|
|
|
+ private User $assistant;
|
|
|
+
|
|
|
+ private CommonCatalogItem $catalogItem;
|
|
|
+
|
|
|
+ protected function setUp(): void
|
|
|
+ {
|
|
|
+ parent::setUp();
|
|
|
+
|
|
|
+ $this->admin = User::factory()->admin()->create();
|
|
|
+ $this->manager = User::factory()->manager()->create();
|
|
|
+ $this->assistant = User::factory()->assistantHead()->create();
|
|
|
+ $this->catalogItem = CommonCatalogItem::factory()->create([
|
|
|
+ 'article' => 'ГЗ-001',
|
|
|
+ 'calculator_name' => 'Игровой комплекс',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_authorized_roles_can_view_year_independent_order_schedule(): void
|
|
|
+ {
|
|
|
+ $visible = ProductionOrder::factory()->create([
|
|
|
+ 'order_number' => 'ГЗ-2026',
|
|
|
+ 'order_year' => 2026,
|
|
|
+ 'manager_id' => $this->manager->id,
|
|
|
+ ]);
|
|
|
+ ProductionOrder::factory()->create([
|
|
|
+ 'order_number' => 'ГЗ-2024',
|
|
|
+ 'order_year' => 2024,
|
|
|
+ 'manager_id' => $this->manager->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $this->get(route('schedule.orders'))->assertRedirect(route('login'));
|
|
|
+
|
|
|
+ $this->actingAs($this->manager)
|
|
|
+ ->withSession(['year' => 2020])
|
|
|
+ ->get(route('schedule.orders'))
|
|
|
+ ->assertOk()
|
|
|
+ ->assertViewIs('production_orders.index')
|
|
|
+ ->assertSee($visible->order_number)
|
|
|
+ ->assertSee('ГЗ-2024');
|
|
|
+
|
|
|
+ $driver = User::factory()->driver()->create();
|
|
|
+ $this->actingAs($driver)->get(route('schedule.orders'))->assertOk();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_admin_can_create_order_with_items_and_calendar_shipment_date(): void
|
|
|
+ {
|
|
|
+ ProductionCalendarDay::factory()->create([
|
|
|
+ 'date' => '2026-09-07',
|
|
|
+ 'is_working_day' => false,
|
|
|
+ 'name' => 'Перенесённый выходной',
|
|
|
+ ]);
|
|
|
+ $secondCatalogItem = CommonCatalogItem::factory()->create([
|
|
|
+ 'article' => 'ГЗ-002',
|
|
|
+ 'calculator_name' => 'Качели',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->admin)->post(
|
|
|
+ route('schedule.orders.store'),
|
|
|
+ $this->validData([
|
|
|
+ 'order_number' => 'ЗАК-100',
|
|
|
+ 'payment_date' => '2026-09-04',
|
|
|
+ 'supply_working_days' => 1,
|
|
|
+ 'items' => [
|
|
|
+ [
|
|
|
+ 'common_catalog_item_id' => $this->catalogItem->id,
|
|
|
+ 'order_item_number' => 'МАФ-100/1',
|
|
|
+ ],
|
|
|
+ [
|
|
|
+ 'common_catalog_item_id' => $secondCatalogItem->id,
|
|
|
+ 'order_item_number' => 'МАФ-100/2',
|
|
|
+ 'factory_number' => 'ЗН-002',
|
|
|
+ 'manufacture_date' => '2026-09-03',
|
|
|
+ ],
|
|
|
+ ],
|
|
|
+ ]),
|
|
|
+ );
|
|
|
+
|
|
|
+ $order = ProductionOrder::query()->where('order_number', 'ЗАК-100')->firstOrFail();
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $this->assertSame('2026-09-08', $order->contract_shipment_date->toDateString());
|
|
|
+ $this->assertSame($this->admin->id, $order->created_by);
|
|
|
+ $this->assertSame(2, $order->items()->count());
|
|
|
+ $this->assertDatabaseHas('production_order_items', [
|
|
|
+ 'production_order_id' => $order->id,
|
|
|
+ 'factory_number' => 'ЗН-002',
|
|
|
+ 'manufacture_date' => '2026-09-03',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_order_number_is_unique_within_year(): void
|
|
|
+ {
|
|
|
+ ProductionOrder::factory()->create([
|
|
|
+ 'order_number' => 'ДУБЛЬ-1',
|
|
|
+ 'order_year' => 2026,
|
|
|
+ 'manager_id' => $this->manager->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $this->actingAs($this->admin)
|
|
|
+ ->post(route('schedule.orders.store'), $this->validData([
|
|
|
+ 'order_number' => 'ДУБЛЬ-1',
|
|
|
+ 'order_year' => 2026,
|
|
|
+ ]))
|
|
|
+ ->assertSessionHasErrors('order_number');
|
|
|
+
|
|
|
+ $this->actingAs($this->admin)
|
|
|
+ ->post(route('schedule.orders.store'), $this->validData([
|
|
|
+ 'order_number' => 'ДУБЛЬ-1',
|
|
|
+ 'order_year' => 2027,
|
|
|
+ ]))
|
|
|
+ ->assertSessionHasNoErrors();
|
|
|
+
|
|
|
+ $this->assertDatabaseHas('production_orders', [
|
|
|
+ 'order_number' => 'ДУБЛЬ-1',
|
|
|
+ 'order_year' => 2027,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_manager_cannot_create_or_update_production_order(): void
|
|
|
+ {
|
|
|
+ $order = ProductionOrder::factory()->create(['manager_id' => $this->manager->id]);
|
|
|
+
|
|
|
+ $this->actingAs($this->manager)
|
|
|
+ ->post(route('schedule.orders.store'), $this->validData())
|
|
|
+ ->assertForbidden();
|
|
|
+
|
|
|
+ $this->actingAs($this->manager)
|
|
|
+ ->put(route('schedule.orders.update', $order), ['note' => 'Подмена'])
|
|
|
+ ->assertForbidden();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_assistant_can_update_only_allowed_order_and_item_fields(): void
|
|
|
+ {
|
|
|
+ $otherCatalogItem = CommonCatalogItem::factory()->create();
|
|
|
+ $order = ProductionOrder::factory()->create([
|
|
|
+ 'order_number' => 'ЗАК-OLD',
|
|
|
+ 'customer_name' => 'Исходный заказчик',
|
|
|
+ 'object_address' => 'Старый адрес',
|
|
|
+ 'status' => ProductionOrderStatus::Placed,
|
|
|
+ 'manager_id' => $this->manager->id,
|
|
|
+ ]);
|
|
|
+ $item = ProductionOrderItem::factory()->create([
|
|
|
+ 'production_order_id' => $order->id,
|
|
|
+ 'common_catalog_item_id' => $this->catalogItem->id,
|
|
|
+ 'order_item_number' => 'МАФ-OLD',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $this->actingAs($this->assistant)
|
|
|
+ ->put(route('schedule.orders.update', $order), [
|
|
|
+ 'order_number' => 'ЗАК-FORGED',
|
|
|
+ 'customer_name' => 'Подменённый заказчик',
|
|
|
+ 'object_address' => 'Новый адрес',
|
|
|
+ 'status' => ProductionOrderStatus::Closed->value,
|
|
|
+ 'note' => 'Уточнённое примечание',
|
|
|
+ 'items' => [[
|
|
|
+ 'id' => $item->id,
|
|
|
+ 'common_catalog_item_id' => $otherCatalogItem->id,
|
|
|
+ 'order_item_number' => 'МАФ-FORGED',
|
|
|
+ 'factory_number' => 'ЗН-NEW',
|
|
|
+ 'manufacture_date' => '2026-09-02',
|
|
|
+ ]],
|
|
|
+ ])
|
|
|
+ ->assertRedirect();
|
|
|
+
|
|
|
+ $order->refresh();
|
|
|
+ $item->refresh();
|
|
|
+
|
|
|
+ $this->assertSame('ЗАК-OLD', $order->order_number);
|
|
|
+ $this->assertSame('Исходный заказчик', $order->customer_name);
|
|
|
+ $this->assertSame('Новый адрес', $order->object_address);
|
|
|
+ $this->assertSame(ProductionOrderStatus::Placed, $order->status);
|
|
|
+ $this->assertSame('Уточнённое примечание', $order->note);
|
|
|
+ $this->assertSame($this->catalogItem->id, $item->common_catalog_item_id);
|
|
|
+ $this->assertSame('МАФ-OLD', $item->order_item_number);
|
|
|
+ $this->assertSame('ЗН-NEW', $item->factory_number);
|
|
|
+ $this->assertSame('2026-09-02', $item->manufacture_date?->toDateString());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_order_card_displays_separate_catalog_items(): void
|
|
|
+ {
|
|
|
+ $order = ProductionOrder::factory()->create(['manager_id' => $this->manager->id]);
|
|
|
+ ProductionOrderItem::factory()->create([
|
|
|
+ 'production_order_id' => $order->id,
|
|
|
+ 'common_catalog_item_id' => $this->catalogItem->id,
|
|
|
+ 'order_item_number' => 'МАФ-КАРТОЧКА-1',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $this->actingAs($this->manager)
|
|
|
+ ->get(route('schedule.orders.show', $order))
|
|
|
+ ->assertOk()
|
|
|
+ ->assertSee('ГЗ-001')
|
|
|
+ ->assertSee('Игровой комплекс')
|
|
|
+ ->assertSee('МАФ-КАРТОЧКА-1');
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_status_filter_uses_labels_and_filters_by_enum_value(): void
|
|
|
+ {
|
|
|
+ ProductionOrder::factory()->create([
|
|
|
+ 'order_number' => 'ГЗ-РАЗМЕЩЕН',
|
|
|
+ 'status' => ProductionOrderStatus::Placed,
|
|
|
+ 'manager_id' => $this->manager->id,
|
|
|
+ ]);
|
|
|
+ ProductionOrder::factory()->create([
|
|
|
+ 'order_number' => 'ГЗ-СКЛАД',
|
|
|
+ 'status' => ProductionOrderStatus::InStock,
|
|
|
+ 'manager_id' => $this->manager->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $this->actingAs($this->manager)
|
|
|
+ ->getJson(route('getFilters', [
|
|
|
+ 'table' => 'schedule_orders',
|
|
|
+ 'column' => 'status_name',
|
|
|
+ ]))
|
|
|
+ ->assertOk()
|
|
|
+ ->assertJsonFragment(['Размещен'])
|
|
|
+ ->assertJsonFragment(['На складе']);
|
|
|
+
|
|
|
+ $this->actingAs($this->manager)
|
|
|
+ ->get(route('schedule.orders', [
|
|
|
+ 'filters' => ['status_name' => 'На складе'],
|
|
|
+ ]))
|
|
|
+ ->assertOk()
|
|
|
+ ->assertSee('ГЗ-СКЛАД')
|
|
|
+ ->assertDontSee('ГЗ-РАЗМЕЩЕН');
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_admin_can_search_common_catalog_when_adding_order_items(): void
|
|
|
+ {
|
|
|
+ $matchingByName = CommonCatalogItem::factory()->create([
|
|
|
+ 'article' => 'SEARCH-002',
|
|
|
+ 'calculator_name' => 'Особенная карусель',
|
|
|
+ ]);
|
|
|
+ CommonCatalogItem::factory()->create([
|
|
|
+ 'article' => 'OTHER-003',
|
|
|
+ 'calculator_name' => 'Другая позиция',
|
|
|
+ ]);
|
|
|
+ $deleted = CommonCatalogItem::factory()->create([
|
|
|
+ 'article' => 'SEARCH-DELETED',
|
|
|
+ 'calculator_name' => 'Удалённая карусель',
|
|
|
+ ]);
|
|
|
+ $deleted->delete();
|
|
|
+
|
|
|
+ $this->actingAs($this->admin)
|
|
|
+ ->getJson(route('schedule.orders.catalog-search', ['q' => 'карусель']))
|
|
|
+ ->assertOk()
|
|
|
+ ->assertJsonFragment([
|
|
|
+ 'id' => $matchingByName->id,
|
|
|
+ 'label' => 'SEARCH-002 — Особенная карусель',
|
|
|
+ ])
|
|
|
+ ->assertJsonMissing(['id' => $deleted->id]);
|
|
|
+
|
|
|
+ $this->actingAs($this->admin)
|
|
|
+ ->getJson(route('schedule.orders.catalog-search', ['q' => 'ГЗ-001']))
|
|
|
+ ->assertOk()
|
|
|
+ ->assertJsonFragment(['id' => $this->catalogItem->id]);
|
|
|
+
|
|
|
+ $this->actingAs($this->manager)
|
|
|
+ ->getJson(route('schedule.orders.catalog-search', ['q' => 'карусель']))
|
|
|
+ ->assertForbidden();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_create_form_loads_catalog_items_only_through_search(): void
|
|
|
+ {
|
|
|
+ $unrelated = CommonCatalogItem::factory()->create([
|
|
|
+ 'article' => 'НЕ-ЗАГРУЖАТЬ',
|
|
|
+ 'calculator_name' => 'Тяжёлая позиция каталога',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $this->actingAs($this->admin)
|
|
|
+ ->get(route('schedule.orders.create'))
|
|
|
+ ->assertOk()
|
|
|
+ ->assertSee('Добавить МАФ из общего каталога')
|
|
|
+ ->assertSee('production-order-catalog-search', false)
|
|
|
+ ->assertDontSee($unrelated->article);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** @return array<string, mixed> */
|
|
|
+ private function validData(array $overrides = []): array
|
|
|
+ {
|
|
|
+ return array_replace_recursive([
|
|
|
+ 'order_number' => 'ЗАК-001',
|
|
|
+ 'order_year' => 2026,
|
|
|
+ 'customer_name' => 'ООО Заказчик',
|
|
|
+ 'object_address' => 'г. Москва, ул. Строителей, 1',
|
|
|
+ 'invoice_number' => 'СЧ-001',
|
|
|
+ 'invoice_date' => '2026-09-01',
|
|
|
+ 'contract_number' => 'Д-001',
|
|
|
+ 'contract_date' => '2026-08-31',
|
|
|
+ 'payment_date' => '2026-09-01',
|
|
|
+ 'supply_working_days' => 10,
|
|
|
+ 'application_shipment_date' => '2026-09-20',
|
|
|
+ 'status' => ProductionOrderStatus::Placed->value,
|
|
|
+ 'execution_type' => ProductionOrderExecutionType::Delivery->value,
|
|
|
+ 'manager_id' => $this->manager->id,
|
|
|
+ 'note' => 'Тестовый заказ',
|
|
|
+ 'items' => [[
|
|
|
+ 'common_catalog_item_id' => $this->catalogItem->id,
|
|
|
+ 'order_item_number' => 'МАФ-001',
|
|
|
+ 'factory_number' => null,
|
|
|
+ 'manufacture_date' => null,
|
|
|
+ ]],
|
|
|
+ ], $overrides);
|
|
|
+ }
|
|
|
+}
|