Browse Source

Import product catalog from xls file

Alexander Musikhin 11 tháng trước cách đây
mục cha
commit
dead0c290a

+ 36 - 0
app/Console/Commands/ImportMaf.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Services\ImportService;
+use Exception;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+
+class ImportMaf extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'app:import-maf {path} {year}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Импорт каталога МАФ';
+
+    /**
+     * Execute the console command.
+     * @throws Exception
+     */
+    public function handle(): void
+    {
+        // run service command
+        (new ImportService)->handle($this->argument('path'), $this->argument('year'));
+    }
+}

+ 2 - 1
app/Events/SendWebSocketMessageEvent.php

@@ -17,7 +17,7 @@ class SendWebSocketMessageEvent implements ShouldBroadcastNow
     /**
      * Create a new event instance.
      */
-    public function __construct(private readonly string $message, private readonly int $userId)
+    public function __construct(private readonly string $message, private readonly int $userId, private readonly array $payload)
     {
         //
     }
@@ -37,6 +37,7 @@ class SendWebSocketMessageEvent implements ShouldBroadcastNow
             'action' => 'message',
             'message' => $this->message,
             'user_id' => $this->userId,
+            'payload' => $this->payload,
         ];
     }
 }

+ 0 - 18
app/Helpers/taskStatuses.php

@@ -1,18 +0,0 @@
-<?php
-if(!function_exists('getStatuses')){
-    function getStatuses($key = null): array|string
-    {
-        $statuses = [
-            'work'      => 'В работе',
-            'check'     => 'На проверке',
-            'done'      => 'Завершена',
-            'cancel'    => 'Отменена',
-        ];
-
-        if($key && isset($statuses[$key])){
-            return $statuses[$key];
-        } else {
-            return $statuses;
-        }
-    }
-}

+ 3 - 11
app/Http/Controllers/ProductController.php

@@ -37,19 +37,11 @@ class ProductController extends Controller
         ]);
 
         // load and save file
-        try {
-            $path = Str::random(2) . '/' . Str::uuid() . '.' .$request->file('import_file')->getClientOriginalExtension();
-            Storage::disk('upload')->put($path, $request->file('import_file')->getContent());
-        } catch (Exception $e) {
-            Log::error('[UPLOAD ERROR]' . $e->getMessage());
-            return view('catalog.index')->with(['danger' => 'Не удалось сохранить файл!']);
-        }
-
-        // todo validate header
-
+        $path = Str::random(2) . '/' . Str::uuid() . '.' .$request->file('import_file')->getClientOriginalExtension();
+        Storage::disk('upload')->put($path, $request->file('import_file')->getContent());
 
         // dispatch job
-        ImportCatalog::dispatch($request->year, $path);
+        ImportCatalog::dispatch($path, $request->year, $request->user()->id);
         Log::info('ImportCatalog job created!');
         return redirect()->route('catalog.index')->with(['success' => 'Задача импорта успешно создана!']);
     }

+ 12 - 8
app/Jobs/Import/ImportCatalog.php

@@ -3,6 +3,8 @@
 namespace App\Jobs\Import;
 
 use App\Events\SendWebSocketMessageEvent;
+use App\Services\ImportService;
+use Exception;
 use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Queue\Queueable;
@@ -15,21 +17,23 @@ class ImportCatalog implements ShouldQueue, ShouldBeUniqueUntilProcessing
     /**
      * Create a new job instance.
      */
-    public function __construct(private readonly int $year, private readonly string $path)
+    public function __construct(private readonly string $path, private readonly int $year, private readonly int $userId)
     {
         //
     }
 
     /**
-     * Execute the job.
+     * @return void
      */
     public function handle(): void
     {
-        Log::info('ImportCatalog job done!');
-
-
-        // make event
-        event(new SendWebSocketMessageEvent('TEsttttd', 1));
+        try {
+            (new ImportService)->handle($this->path, $this->year);
+            Log::info('ImportCatalog job done!');
+            event(new SendWebSocketMessageEvent('Импорт завершён!', $this->userId, ['year' => $this->year, 'path' => $this->path]));
+        } catch (Exception $e) {
+            Log::info('ImportCatalog job failed!');
+            event(new SendWebSocketMessageEvent('Ошибка импорта! ' . $e->getMessage(), $this->userId, ['error' => $e->getMessage()]));
+        }
     }
-
 }

+ 82 - 1
app/Models/Product.php

@@ -2,11 +2,92 @@
 
 namespace App\Models;
 
+use App\Helpers\Price;
+use Illuminate\Database\Eloquent\Casts\Attribute;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\SoftDeletes;
 
 class Product extends Model
 {
     use SoftDeletes;
-    protected $guarded = false;
+    protected $fillable = [
+        'name_tz',
+        'type_tz',
+        'nomenclature_number',
+        'sizes',
+        'manufacturer',
+        'unit',
+        'type',
+        'price_status',
+        'product_price',
+        'installation_price',
+        'service_price',
+        'total_price',
+        'manufacturer_name',
+        'article',
+        'note',
+    ];
+
+    protected $appends = ['product_price', 'installation_price', 'service_price', 'total_price',
+                          'product_price_txt', 'installation_price_txt', 'service_price_txt', 'total_price_txt',];
+    protected function productPrice(): Attribute
+    {
+        return Attribute::make(
+            get: fn(int $value) => (float) $value / 100,
+            set: fn (float $value) => (int) ($value * 100),
+        );
+    }
+
+    protected function installationPrice(): Attribute
+    {
+        return Attribute::make(
+            get: fn(int $value) => (float) $value / 100,
+            set: fn (float $value) => (int) ($value * 100),
+        );
+    }
+
+    protected function servicePrice(): Attribute
+    {
+        return Attribute::make(
+            get: fn(int $value) => (float) $value / 100,
+            set: fn (float $value) => (int) ($value * 100),
+        );
+    }
+
+    protected function totalPrice(): Attribute
+    {
+        return Attribute::make(
+            get: fn(int $value) => (float) $value / 100,
+            set: fn (float $value) => (int) ($value * 100),
+        );
+    }
+
+    public function productPriceTxt(): Attribute
+    {
+        return Attribute::make(
+            get: fn($value) => Price::format($this->product_price),
+        );
+    }
+
+    public function installationPriceTxt(): Attribute
+    {
+        return Attribute::make(
+            get: fn($value) => Price::format($this->installation_price),
+        );
+    }
+
+    protected function servicePriceTxt(): Attribute
+    {
+        return Attribute::make(
+            get: fn($value) => Price::format($this->service_price),
+        );
+    }
+
+    protected function totalPriceTxt(): Attribute
+    {
+        return Attribute::make(
+            get: fn($value) => Price::format($this->total_price),
+        );
+    }
+
 }

+ 88 - 0
app/Services/ImportService.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\Product;
+use Exception;
+use Illuminate\Support\Facades\Storage;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+
+class ImportService
+{
+    const HEADERS_TO_FIELDS = [
+        "Фото" => '',
+        "Наименование по ТЗ"            => 'name_tz',
+        "Тип по ТЗ"                     => 'type_tz',
+        "№ по номенкл."                 => 'nomenclature_number',
+        "Габаритные размеры"            => 'sizes',
+        "Производитель"                 => 'manufacturer',
+        "ед. изм."                      => 'unit',
+        "Тип оборудования"              => 'type',
+        "Статус цены"                   => 'price_status',
+        "Цена поставки"                 => 'product_price',
+        "Цена установки"                => 'installation_price',
+        "Цена обслуживания"             => 'service_price',
+        "Итого цена"                    => 'total_price',
+        "Наименование производителя"    => 'manufacturer_name',
+        "Артикул образца"               => 'article',
+        "Примечание"                    => 'note',
+    ];
+
+
+    /**
+     * @throws Exception
+     */
+    public function handle(string $path, int $year): void
+    {
+        $path = Storage::disk('upload')->path($path);
+        $xls = IOFactory::createReaderForFile($path);
+        $xls->setReadDataOnly(true);
+        $sheet = $xls->load($path);
+
+        $records = $sheet->setActiveSheetIndex(0)->toArray();
+
+        $data = [];
+
+        $err = [];
+        if($this->checkHeaders($records[0])) {
+            foreach ($records as $k => $record){
+                if($k === 0) continue;
+                Product::query()
+                    ->updateOrCreate(['year' => $year, 'nomenclature_number' => $record[3]],
+                    [
+                        'name_tz'           => $record[1],
+                        'type_tz'           => $record[2],
+                        'sizes'             => $record[4],
+                        'manufacturer'      => $record[5],
+                        'unit'              => $record[6],
+                        'type'              => $record[7],
+                        'price_status'      => $record[8],
+                        'product_price'     => $record[9],
+                        'installation_price'=> $record[10],
+                        'service_price'     => $record[11],
+                        'total_price'       => $record[12],
+                        'manufacturer_name' => $record[13],
+                        'article'           => $record[14],
+                        'note'              => $record[15],
+                    ]);
+            }
+        } else {
+            throw new Exception('Ошибка заголовков файла!');
+        }
+    }
+
+
+    protected function checkHeaders(array $headers): bool
+    {
+        return $this->getHeaders() == $headers;
+    }
+
+
+    /**
+     * @return array
+     */
+    protected function getHeaders(): array
+    {
+        return array_keys(self::HEADERS_TO_FIELDS);
+    }
+}

+ 3 - 2
composer.json

@@ -6,11 +6,12 @@
     "license": "MIT",
     "require": {
         "php": "^8.2",
+        "ext-pdo": "*",
         "laravel/framework": "^11.31",
         "laravel/tinker": "^2.9",
         "laravel/ui": "^4.6",
-        "predis/predis": "^2.3",
-        "ext-pdo": "*"
+        "phpoffice/phpspreadsheet": "^3.6",
+        "predis/predis": "^2.3"
     },
     "require-dev": {
         "fakerphp/faker": "^1.23",

+ 289 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "00648a7052ad5efdfbed2952a9ec8d54",
+    "content-hash": "6c837242b1f48f1bf285f8f3ad860f21",
     "packages": [
         {
             "name": "brick/math",
@@ -2069,6 +2069,190 @@
             ],
             "time": "2024-12-08T08:18:47+00:00"
         },
+        {
+            "name": "maennchen/zipstream-php",
+            "version": "3.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/maennchen/ZipStream-PHP.git",
+                "reference": "6187e9cc4493da94b9b63eb2315821552015fca9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9",
+                "reference": "6187e9cc4493da94b9b63eb2315821552015fca9",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "ext-zlib": "*",
+                "php-64bit": "^8.1"
+            },
+            "require-dev": {
+                "ext-zip": "*",
+                "friendsofphp/php-cs-fixer": "^3.16",
+                "guzzlehttp/guzzle": "^7.5",
+                "mikey179/vfsstream": "^1.6",
+                "php-coveralls/php-coveralls": "^2.5",
+                "phpunit/phpunit": "^10.0",
+                "vimeo/psalm": "^5.0"
+            },
+            "suggest": {
+                "guzzlehttp/psr7": "^2.4",
+                "psr/http-message": "^2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "ZipStream\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Paul Duncan",
+                    "email": "pabs@pablotron.org"
+                },
+                {
+                    "name": "Jonatan Männchen",
+                    "email": "jonatan@maennchen.ch"
+                },
+                {
+                    "name": "Jesse Donat",
+                    "email": "donatj@gmail.com"
+                },
+                {
+                    "name": "András Kolesár",
+                    "email": "kolesar@kolesar.hu"
+                }
+            ],
+            "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
+            "keywords": [
+                "stream",
+                "zip"
+            ],
+            "support": {
+                "issues": "https://github.com/maennchen/ZipStream-PHP/issues",
+                "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/maennchen",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-10-10T12:33:01+00:00"
+        },
+        {
+            "name": "markbaker/complex",
+            "version": "3.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/MarkBaker/PHPComplex.git",
+                "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+                "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+                "phpcompatibility/php-compatibility": "^9.3",
+                "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+                "squizlabs/php_codesniffer": "^3.7"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Complex\\": "classes/src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mark Baker",
+                    "email": "mark@lange.demon.co.uk"
+                }
+            ],
+            "description": "PHP Class for working with complex numbers",
+            "homepage": "https://github.com/MarkBaker/PHPComplex",
+            "keywords": [
+                "complex",
+                "mathematics"
+            ],
+            "support": {
+                "issues": "https://github.com/MarkBaker/PHPComplex/issues",
+                "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
+            },
+            "time": "2022-12-06T16:21:08+00:00"
+        },
+        {
+            "name": "markbaker/matrix",
+            "version": "3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/MarkBaker/PHPMatrix.git",
+                "reference": "728434227fe21be27ff6d86621a1b13107a2562c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
+                "reference": "728434227fe21be27ff6d86621a1b13107a2562c",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+                "phpcompatibility/php-compatibility": "^9.3",
+                "phpdocumentor/phpdocumentor": "2.*",
+                "phploc/phploc": "^4.0",
+                "phpmd/phpmd": "2.*",
+                "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+                "sebastian/phpcpd": "^4.0",
+                "squizlabs/php_codesniffer": "^3.7"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Matrix\\": "classes/src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mark Baker",
+                    "email": "mark@demon-angel.eu"
+                }
+            ],
+            "description": "PHP Class for working with matrices",
+            "homepage": "https://github.com/MarkBaker/PHPMatrix",
+            "keywords": [
+                "mathematics",
+                "matrix",
+                "vector"
+            ],
+            "support": {
+                "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
+                "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
+            },
+            "time": "2022-12-02T22:17:43+00:00"
+        },
         {
             "name": "monolog/monolog",
             "version": "3.8.1",
@@ -2571,6 +2755,110 @@
             ],
             "time": "2024-11-21T10:39:51+00:00"
         },
+        {
+            "name": "phpoffice/phpspreadsheet",
+            "version": "3.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+                "reference": "bce5db99872f9613121c3ad033c43318a3789396"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/bce5db99872f9613121c3ad033c43318a3789396",
+                "reference": "bce5db99872f9613121c3ad033c43318a3789396",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-dom": "*",
+                "ext-fileinfo": "*",
+                "ext-gd": "*",
+                "ext-iconv": "*",
+                "ext-libxml": "*",
+                "ext-mbstring": "*",
+                "ext-simplexml": "*",
+                "ext-xml": "*",
+                "ext-xmlreader": "*",
+                "ext-xmlwriter": "*",
+                "ext-zip": "*",
+                "ext-zlib": "*",
+                "maennchen/zipstream-php": "^2.1 || ^3.0",
+                "markbaker/complex": "^3.0",
+                "markbaker/matrix": "^3.0",
+                "php": "^8.1",
+                "psr/http-client": "^1.0",
+                "psr/http-factory": "^1.0",
+                "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
+                "dompdf/dompdf": "^2.0 || ^3.0",
+                "friendsofphp/php-cs-fixer": "^3.2",
+                "mitoteam/jpgraph": "^10.3",
+                "mpdf/mpdf": "^8.1.1",
+                "phpcompatibility/php-compatibility": "^9.3",
+                "phpstan/phpstan": "^1.1",
+                "phpstan/phpstan-phpunit": "^1.0",
+                "phpunit/phpunit": "^10.5",
+                "squizlabs/php_codesniffer": "^3.7",
+                "tecnickcom/tcpdf": "^6.5"
+            },
+            "suggest": {
+                "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+                "ext-intl": "PHP Internationalization Functions",
+                "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+                "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+                "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Maarten Balliauw",
+                    "homepage": "https://blog.maartenballiauw.be"
+                },
+                {
+                    "name": "Mark Baker",
+                    "homepage": "https://markbakeruk.net"
+                },
+                {
+                    "name": "Franck Lefevre",
+                    "homepage": "https://rootslabs.net"
+                },
+                {
+                    "name": "Erik Tilt"
+                },
+                {
+                    "name": "Adrien Crivelli"
+                }
+            ],
+            "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+            "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+            "keywords": [
+                "OpenXML",
+                "excel",
+                "gnumeric",
+                "ods",
+                "php",
+                "spreadsheet",
+                "xls",
+                "xlsx"
+            ],
+            "support": {
+                "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
+                "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/3.6.0"
+            },
+            "time": "2024-12-08T15:04:12+00:00"
+        },
         {
             "name": "phpoption/phpoption",
             "version": "1.9.3",

+ 1 - 1
routes/web.php

@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Route;
 
 Route::get('/', function () {
-    return 'root page';
+    return redirect()->route('home');
 });
 
 Auth::routes(['register' => false, 'reset' => false, 'verify' => false, 'confirm' => false]);