Explorar o código

fixed sum in spare, css

Alexander Musikhin hai 1 mes
pai
achega
ccff65eac9

+ 2 - 1
app/Models/ReclamationView.php

@@ -9,7 +9,8 @@ class ReclamationView extends Model
 {
     use SoftDeletes;
 
-    const DEFAULT_SORT_BY = 'created_at';
+    const DEFAULT_SORT_BY = 'create_date';
+    const DEFAULT_ORDER_BY = 'desc';
 
     protected $table = 'reclamations_view';
 

+ 6 - 6
app/Models/SparePart.php

@@ -71,24 +71,24 @@ class SparePart extends Model
     protected function purchasePrice(): Attribute
     {
         return Attribute::make(
-            get: fn($value) => $value ? $value / 100 : null,
-            set: fn($value) => $value ? round($value * 100) : null,
+            get: fn($value) => $value === null ? null : $value / 100,
+            set: fn($value) => $value === null || $value === '' ? null : round($value * 100),
         );
     }
 
     protected function customerPrice(): Attribute
     {
         return Attribute::make(
-            get: fn($value) => $value ? $value / 100 : null,
-            set: fn($value) => $value ? round($value * 100) : null,
+            get: fn($value) => $value === null ? null : $value / 100,
+            set: fn($value) => $value === null || $value === '' ? null : round($value * 100),
         );
     }
 
     protected function expertisePrice(): Attribute
     {
         return Attribute::make(
-            get: fn($value) => $value ? $value / 100 : null,
-            set: fn($value) => $value ? round($value * 100) : null,
+            get: fn($value) => $value === null ? null : $value / 100,
+            set: fn($value) => $value === null || $value === '' ? null : round($value * 100),
         );
     }
 

+ 21 - 2
app/Services/GenerateDocumentsService.php

@@ -380,16 +380,35 @@ class GenerateDocumentsService
      */
     public function generateReclamationPack(Reclamation $reclamation, int $userId): string
     {
-        Storage::disk('public')->makeDirectory('reclamations/' . $reclamation->id . '/tmp/' . fileName($reclamation->order->object_address) . '/ФОТО НАРУШЕНИЯ/');
+        $reclamation->loadMissing([
+            'order',
+            'photos_before',
+            'documents',
+            'skus.product',
+        ]);
+
+        $baseDir = 'reclamations/' . $reclamation->id . '/tmp/' . fileName($reclamation->order->object_address);
+        $photosDir = $baseDir . '/ФОТО НАРУШЕНИЯ';
+
+        Storage::disk('public')->makeDirectory($photosDir);
+
         // copy photos
         foreach ($reclamation->photos_before as $photo) {
             $from = $photo->path;
-            $to = 'reclamations/' . $reclamation->id . '/tmp/' . fileName($reclamation->order->object_address) . '/ФОТО НАРУШЕНИЯ/' . $photo->original_name;
+            $to = $photosDir . '/' . $photo->original_name;
             if (!Storage::disk('public')->exists($to)) {
                 Storage::disk('public')->copy($from, $to);
             }
         }
 
+        foreach ($reclamation->documents as $document) {
+            if ($this->shouldSkipArchiveDocument($document)) {
+                continue;
+            }
+
+            $this->copyFileToDir($document->path, $baseDir, $document->original_name);
+        }
+
         // create xls and pdf
         $this->generateReclamationOrder($reclamation);
         $this->generateReclamationAct($reclamation);

+ 8 - 11
app/Services/Import/ImportSparePartsService.php

@@ -178,25 +178,22 @@ class ImportSparePartsService
     }
 
     /**
-     * Парсинг цены
-     * Если цена уже в копейках (целое число > 1000), оставляем как есть
-     * Если цена в рублях (дробное или < 1000), конвертируем в копейки
+     * Импорт читает цены из Excel в рублях.
+     * Дальше модель SparePart сама конвертирует их в копейки при сохранении.
      */
-    private function parsePrice($value): ?int
+    private function parsePrice($value): ?float
     {
-        if (empty($value)) {
+        if ($value === null || $value === '') {
             return null;
         }
 
-        $price = (float)$value;
+        $normalized = str_replace([' ', ','], ['', '.'], trim((string) $value));
 
-        // Если число целое и большое (скорее всего уже в копейках)
-        if ($price == (int)$price && $price >= 1000) {
-            return (int)$price;
+        if ($normalized === '') {
+            return null;
         }
 
-        // Иначе конвертируем из рублей в копейки
-        return round($price * 100);
+        return round((float) $normalized, 2);
     }
 
     private function syncPricingCodes(SparePart $sparePart, array $codeStrings): void

+ 6 - 6
app/Services/NotificationService.php

@@ -31,7 +31,7 @@ class NotificationService
             sprintf('Добавлена новая площадка %s', $order->object_address),
             sprintf(
                 'Добавлена новая площадка <a href="%s">%s</a>.',
-                route('order.show', ['order' => $order->id]),
+                route('order.show', ['order' => $order->id, 'sync_year' => 1]),
                 e($order->object_address)
             ),
             $statusName,
@@ -49,7 +49,7 @@ class NotificationService
             sprintf('Статус площадки %s изменен на %s', $order->object_address, $statusName),
             sprintf(
                 'Статус площадки <a href="%s">%s</a> изменен на %s.',
-                route('order.show', ['order' => $order->id]),
+                route('order.show', ['order' => $order->id, 'sync_year' => 1]),
                 e($order->object_address),
                 e($statusName)
             ),
@@ -72,7 +72,7 @@ class NotificationService
 
         $messageHtml = sprintf(
             'Добавлена новая рекламация по адресу <a href="%s">%s</a> <a href="%s">#%d</a>.',
-            route('order.show', ['order' => $order->id]),
+            route('order.show', ['order' => $order->id, 'sync_year' => 1]),
             e($order->object_address),
             route('reclamations.show', ['reclamation' => $reclamation->id]),
             $reclamation->id,
@@ -106,7 +106,7 @@ class NotificationService
 
         $messageHtml = sprintf(
             'Статус рекламации по адресу <a href="%s">%s</a> <a href="%s">#%d</a> изменен на %s.',
-            route('order.show', ['order' => $order->id]),
+            route('order.show', ['order' => $order->id, 'sync_year' => 1]),
             e($order->object_address),
             route('reclamations.show', ['reclamation' => $reclamation->id]),
             $reclamation->id,
@@ -148,7 +148,7 @@ class NotificationService
                 ? route('reclamations.show', ['reclamation' => $reclamationId])
                 : route('schedule.index');
             $orderLink = $schedule->order_id
-                ? route('order.show', ['order' => $schedule->order_id])
+                ? route('order.show', ['order' => $schedule->order_id, 'sync_year' => 1])
                 : null;
 
             $addressHtml = $orderLink
@@ -172,7 +172,7 @@ class NotificationService
             );
 
             $orderLink = $schedule->order_id
-                ? route('order.show', ['order' => $schedule->order_id])
+                ? route('order.show', ['order' => $schedule->order_id, 'sync_year' => 1])
                 : route('schedule.index');
 
             $messageHtml = sprintf(

+ 3 - 0
resources/sass/app.scss

@@ -17,6 +17,9 @@
   font-weight: bold;
 }
 
+td p {
+  margin-bottom: 0;
+}
 
 .alerts{
 

+ 1 - 1
resources/views/partials/table.blade.php

@@ -99,7 +99,7 @@
                 @endif
             >
                 @foreach($header as $headerName => $headerTitle)
-                    <td class="column_{{$headerName}}"
+                    <td class="column_{{$headerName}} align-middle"
                     >
                         @if(str_contains($headerName, '-'))
                             @php

+ 7 - 5
resources/views/reclamations/edit.blade.php

@@ -86,11 +86,13 @@
                                 </td>
                                 <td>
                                     @if(hasRole('admin,manager'))
-                                    <a href="{{ route('product_sku.show', $p) }}">
-                                        {!! $p->product->article !!}
-                                    </a>
-                                    <br>
-                                    <a class="small" href="{{ route('catalog.show', $p->product) }}">каталог</a>
+                                        <a href="{{ route('product_sku.show', $p) }}">
+                                            {{ $p->product->article }}
+                                        </a>
+                                        <br>
+                                        <a class="small" href="{{ route('catalog.show', $p->product) }}">каталог</a>
+                                    @else
+                                        {{ $p->product->article }}
                                     @endif
                                 </td>
                                 <td>{!! $p->product->nomenclature_number !!}</td>

+ 8 - 2
tests/Feature/OrderControllerTest.php

@@ -59,7 +59,9 @@ class OrderControllerTest extends TestCase
 
     public function test_orders_index_displays_orders(): void
     {
-        $order = Order::factory()->create();
+        $order = Order::factory()->create([
+            'object_address' => 'ул. Проверочная, д. 101',
+        ]);
 
         $response = $this->actingAs($this->managerUser)
             ->get(route('order.index'));
@@ -72,10 +74,12 @@ class OrderControllerTest extends TestCase
     {
         $assignedOrder = Order::factory()->create([
             'brigadier_id' => $this->brigadierUser->id,
+            'object_address' => 'ул. Бригадирская, д. 10',
         ]);
 
         $otherOrder = Order::factory()->create([
             'brigadier_id' => User::factory()->create(['role' => Role::BRIGADIER])->id,
+            'object_address' => 'ул. Чужая, д. 20',
         ]);
 
         $response = $this->actingAs($this->brigadierUser)
@@ -172,7 +176,9 @@ class OrderControllerTest extends TestCase
 
     public function test_can_view_order_details(): void
     {
-        $order = Order::factory()->create();
+        $order = Order::factory()->create([
+            'object_address' => 'ул. Детальная, д. 5',
+        ]);
 
         $response = $this->actingAs($this->managerUser)
             ->get(route('order.show', $order));

+ 22 - 0
tests/Unit/Models/SparePartTest.php

@@ -25,11 +25,33 @@ class SparePartTest extends TestCase
 
         $sparePart->refresh();
 
+        $this->assertSame(15050, $sparePart->getRawOriginal('purchase_price'));
+        $this->assertSame(20000, $sparePart->getRawOriginal('customer_price'));
+        $this->assertSame(18025, $sparePart->getRawOriginal('expertise_price'));
         $this->assertEquals(150.50, $sparePart->purchase_price);
         $this->assertEquals(200.00, $sparePart->customer_price);
         $this->assertEquals(180.25, $sparePart->expertise_price);
     }
 
+    public function test_zero_prices_are_stored_and_displayed_as_zero(): void
+    {
+        $sparePart = SparePart::factory()->create([
+            'purchase_price' => 0,
+            'customer_price' => 0,
+            'expertise_price' => 0,
+        ]);
+
+        $sparePart->refresh();
+
+        $this->assertSame(0, $sparePart->getRawOriginal('purchase_price'));
+        $this->assertSame(0, $sparePart->getRawOriginal('customer_price'));
+        $this->assertSame(0, $sparePart->getRawOriginal('expertise_price'));
+        $this->assertEquals(0.0, $sparePart->purchase_price);
+        $this->assertEquals(0.0, $sparePart->customer_price);
+        $this->assertEquals(0.0, $sparePart->expertise_price);
+        $this->assertSame('0.00₽', $sparePart->purchase_price_txt);
+    }
+
     public function test_price_txt_accessors_format_prices(): void
     {
         $sparePart = SparePart::factory()->create([

+ 98 - 0
tests/Unit/Services/Import/ImportSparePartsServiceTest.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace Tests\Unit\Services\Import;
+
+use App\Models\PricingCode;
+use App\Models\SparePart;
+use App\Services\Import\ImportSparePartsService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use Tests\TestCase;
+
+class ImportSparePartsServiceTest extends TestCase
+{
+    use RefreshDatabase;
+
+    protected $seed = true;
+
+    private string $tempFilePath;
+
+    protected function tearDown(): void
+    {
+        if (isset($this->tempFilePath) && file_exists($this->tempFilePath)) {
+            unlink($this->tempFilePath);
+        }
+
+        parent::tearDown();
+    }
+
+    public function test_import_stores_prices_in_kopecks_from_rubles(): void
+    {
+        $this->createTestFile([
+            ['SP-IMPORT-001', 'Для теста', 'Заметка', 1500, 1999.99, 0, 'ТСН-1', 'PC-001', 3],
+        ]);
+
+        $service = new ImportSparePartsService($this->tempFilePath, 1);
+        $result = $service->handle();
+
+        $this->assertTrue($result['success']);
+
+        $sparePart = SparePart::where('article', 'SP-IMPORT-001')->firstOrFail();
+
+        $this->assertSame(150000, $sparePart->getRawOriginal('purchase_price'));
+        $this->assertSame(199999, $sparePart->getRawOriginal('customer_price'));
+        $this->assertSame(0, $sparePart->getRawOriginal('expertise_price'));
+        $this->assertEquals(1500.0, $sparePart->purchase_price);
+        $this->assertEquals(1999.99, $sparePart->customer_price);
+        $this->assertEquals(0.0, $sparePart->expertise_price);
+        $this->assertDatabaseHas('pricing_codes', [
+            'type' => PricingCode::TYPE_PRICING_CODE,
+            'code' => 'PC-001',
+        ]);
+    }
+
+    private function createTestFile(array $rows): void
+    {
+        $spreadsheet = new Spreadsheet();
+        $sheet = $spreadsheet->getActiveSheet();
+
+        $sheet->setCellValue('A1', 'ID');
+        $sheet->setCellValue('B1', 'Артикул');
+        $sheet->setCellValue('C1', 'Где используется');
+        $sheet->setCellValue('D1', 'Кол-во без док');
+        $sheet->setCellValue('E1', 'Кол-во с док');
+        $sheet->setCellValue('F1', 'Кол-во общее');
+        $sheet->setCellValue('G1', 'Примечание');
+        $sheet->setCellValue('H1', 'Цена закупки');
+        $sheet->setCellValue('I1', 'Цена для заказчика');
+        $sheet->setCellValue('J1', 'Цена экспертизы');
+        $sheet->setCellValue('K1', '№ по ТСН');
+        $sheet->setCellValue('L1', 'Шифры расценки');
+        $sheet->setCellValue('M1', 'Мин. остаток');
+
+        $rowNum = 2;
+        foreach ($rows as $row) {
+            $sheet->setCellValue('B' . $rowNum, $row[0] ?? '');
+            $sheet->setCellValue('C' . $rowNum, $row[1] ?? '');
+            $sheet->setCellValue('G' . $rowNum, $row[2] ?? '');
+            $sheet->setCellValue('H' . $rowNum, $row[3] ?? '');
+            $sheet->setCellValue('I' . $rowNum, $row[4] ?? '');
+            $sheet->setCellValue('J' . $rowNum, $row[5] ?? '');
+            $sheet->setCellValue('K' . $rowNum, $row[6] ?? '');
+            $sheet->setCellValue('L' . $rowNum, $row[7] ?? '');
+            $sheet->setCellValue('M' . $rowNum, $row[8] ?? 0);
+            $rowNum++;
+        }
+
+        $pricingCodesSheet = $spreadsheet->createSheet(1);
+        $pricingCodesSheet->setCellValue('A1', 'ID');
+        $pricingCodesSheet->setCellValue('B1', 'Тип');
+        $pricingCodesSheet->setCellValue('C1', 'Код');
+        $pricingCodesSheet->setCellValue('D1', 'Расшифровка');
+
+        $this->tempFilePath = sys_get_temp_dir() . '/test_spare_parts_' . uniqid() . '.xlsx';
+        $writer = new Xlsx($spreadsheet);
+        $writer->save($this->tempFilePath);
+    }
+}