|
@@ -7,11 +7,14 @@ namespace App\Services\Import;
|
|
|
use App\Models\CommonCatalogItem;
|
|
use App\Models\CommonCatalogItem;
|
|
|
use App\Models\File;
|
|
use App\Models\File;
|
|
|
use App\Models\Import;
|
|
use App\Models\Import;
|
|
|
|
|
+use App\Models\User;
|
|
|
|
|
+use App\Services\Access\AccessService;
|
|
|
use App\Services\Export\ExportCommonCatalogService;
|
|
use App\Services\Export\ExportCommonCatalogService;
|
|
|
use App\Services\FileService;
|
|
use App\Services\FileService;
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Support\Str;
|
|
|
|
|
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
|
|
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
|
|
@@ -32,38 +35,57 @@ class ImportCommonCatalogService
|
|
|
$path = Storage::disk('upload')->path((string) $this->import->filename);
|
|
$path = Storage::disk('upload')->path((string) $this->import->filename);
|
|
|
$spreadsheet = (new Xlsx)->load($path);
|
|
$spreadsheet = (new Xlsx)->load($path);
|
|
|
$sheet = $spreadsheet->getActiveSheet();
|
|
$sheet = $spreadsheet->getActiveSheet();
|
|
|
- $this->assertHeaders($sheet->rangeToArray('A1:S1', null, true, true, false)[0]);
|
|
|
|
|
|
|
+ $this->assertHeaders(
|
|
|
|
|
+ $sheet->rangeToArray('A1:AD1', null, true, true, false)[0],
|
|
|
|
|
+ $sheet->rangeToArray('A2:AD2', null, true, true, false)[0],
|
|
|
|
|
+ );
|
|
|
$drawings = $this->drawingsByRow($sheet->getDrawingCollection());
|
|
$drawings = $this->drawingsByRow($sheet->getDrawingCollection());
|
|
|
|
|
+ $articleCounts = $this->articleCounts($sheet);
|
|
|
|
|
+ $user = User::query()->findOrFail($this->userId);
|
|
|
|
|
+ $access = app(AccessService::class);
|
|
|
|
|
|
|
|
$created = 0;
|
|
$created = 0;
|
|
|
$updated = 0;
|
|
$updated = 0;
|
|
|
$errors = 0;
|
|
$errors = 0;
|
|
|
|
|
|
|
|
- for ($row = 2; $row <= $sheet->getHighestDataRow(); $row++) {
|
|
|
|
|
- $article = $this->stringValue($sheet->getCell("C{$row}")->getFormattedValue());
|
|
|
|
|
|
|
+ for ($row = 3; $row <= $sheet->getHighestDataRow(); $row++) {
|
|
|
|
|
+ $article = $this->stringValue($sheet->getCell("A{$row}")->getFormattedValue());
|
|
|
if ($article === null) {
|
|
if ($article === null) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$data = [
|
|
$data = [
|
|
|
'article' => $article,
|
|
'article' => $article,
|
|
|
- 'calculator_name' => $this->stringValue($sheet->getCell("D{$row}")->getValue()),
|
|
|
|
|
- 'kind' => $this->stringValue($sheet->getCell("E{$row}")->getValue()),
|
|
|
|
|
- 'dimensions' => $this->stringValue($sheet->getCell("F{$row}")->getValue()),
|
|
|
|
|
- 'fall_height' => $this->numericValue($sheet->getCell("G{$row}")->getCalculatedValue()),
|
|
|
|
|
- 'additional_info' => $this->stringValue($sheet->getCell("H{$row}")->getValue()),
|
|
|
|
|
- 'dimension_unit' => $this->stringValue($sheet->getCell("I{$row}")->getValue()),
|
|
|
|
|
- 'weight' => $this->numericValue($sheet->getCell("J{$row}")->getCalculatedValue()),
|
|
|
|
|
- 'volume' => $this->numericValue($sheet->getCell("K{$row}")->getCalculatedValue()),
|
|
|
|
|
- 'places' => $this->integerValue($sheet->getCell("L{$row}")->getCalculatedValue()),
|
|
|
|
|
- 'composition' => $this->stringValue($sheet->getCell("M{$row}")->getValue()),
|
|
|
|
|
- 'age_group' => $this->stringValue($sheet->getCell("N{$row}")->getFormattedValue()),
|
|
|
|
|
- 'max_users' => $this->integerValue($sheet->getCell("O{$row}")->getCalculatedValue()),
|
|
|
|
|
- 'unit' => $this->stringValue($sheet->getCell("P{$row}")->getValue()),
|
|
|
|
|
- 'series' => $this->stringValue($sheet->getCell("Q{$row}")->getValue()),
|
|
|
|
|
- 'trademark' => $this->stringValue($sheet->getCell("R{$row}")->getValue()),
|
|
|
|
|
- 'note' => $this->stringValue($sheet->getCell("S{$row}")->getValue()),
|
|
|
|
|
|
|
+ 'calculator_name' => $this->stringValue($sheet->getCell("B{$row}")->getValue()),
|
|
|
|
|
+ 'kind' => $this->stringValue($sheet->getCell("C{$row}")->getValue()),
|
|
|
|
|
+ 'dimension_length' => $this->dimensionValue($sheet->getCell("D{$row}")),
|
|
|
|
|
+ 'dimension_width' => $this->dimensionValue($sheet->getCell("E{$row}")),
|
|
|
|
|
+ 'dimension_height' => $this->dimensionValue($sheet->getCell("F{$row}")),
|
|
|
|
|
+ 'site_length' => $this->dimensionValue($sheet->getCell("G{$row}")),
|
|
|
|
|
+ 'site_width' => $this->dimensionValue($sheet->getCell("H{$row}")),
|
|
|
|
|
+ 'fall_height' => $this->numericValue($sheet->getCell("I{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'additional_info' => $this->stringValue($sheet->getCell("J{$row}")->getValue()),
|
|
|
|
|
+ 'dimension_unit' => $this->stringValue($sheet->getCell("K{$row}")->getValue()),
|
|
|
|
|
+ 'weight' => $this->numericValue($sheet->getCell("L{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'volume' => $this->numericValue($sheet->getCell("M{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'places' => $this->integerValue($sheet->getCell("N{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'composition' => $this->stringValue($sheet->getCell("O{$row}")->getValue()),
|
|
|
|
|
+ 'age_group' => $this->stringValue($sheet->getCell("P{$row}")->getFormattedValue()),
|
|
|
|
|
+ 'max_users' => $this->integerValue($sheet->getCell("Q{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'unit' => $this->stringValue($sheet->getCell("R{$row}")->getValue()),
|
|
|
|
|
+ 'series' => $this->stringValue($sheet->getCell("T{$row}")->getValue()),
|
|
|
|
|
+ 'trademark' => $this->stringValue($sheet->getCell("U{$row}")->getValue()),
|
|
|
|
|
+ 'calculator_enabled' => $this->booleanValue($sheet->getCell("V{$row}")->getValue()),
|
|
|
|
|
+ 'builders_price' => $this->numericValue($sheet->getCell("W{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'wholesale_price' => $this->numericValue($sheet->getCell("X{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'recommended_price' => $this->numericValue($sheet->getCell("Y{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'retail_price' => $this->numericValue($sheet->getCell("Z{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'project_price' => $this->numericValue($sheet->getCell("AA{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'project_with_installation_price' => $this->numericValue($sheet->getCell("AB{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'pik_price' => $this->numericValue($sheet->getCell("AC{$row}")->getCalculatedValue()),
|
|
|
|
|
+ 'recommended_plus_10_price' => $this->numericValue($sheet->getCell("AD{$row}")->getCalculatedValue()),
|
|
|
];
|
|
];
|
|
|
|
|
+ $data = $access->filterWritableData($user, 'common-catalog', $data);
|
|
|
|
|
|
|
|
$validator = Validator::make($data, $this->rules());
|
|
$validator = Validator::make($data, $this->rules());
|
|
|
if ($validator->fails()) {
|
|
if ($validator->fails()) {
|
|
@@ -76,12 +98,17 @@ class ImportCommonCatalogService
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $item = CommonCatalogItem::query()->where('article', $article)->first();
|
|
|
|
|
|
|
+ $validated = $validator->validated();
|
|
|
|
|
+ $item = $this->findExistingItem(
|
|
|
|
|
+ $article,
|
|
|
|
|
+ $validated['calculator_name'],
|
|
|
|
|
+ $articleCounts[$article] ?? 1,
|
|
|
|
|
+ );
|
|
|
if ($item) {
|
|
if ($item) {
|
|
|
- $item->update($validator->validated());
|
|
|
|
|
|
|
+ $item->update($validated);
|
|
|
$updated++;
|
|
$updated++;
|
|
|
} else {
|
|
} else {
|
|
|
- $item = CommonCatalogItem::query()->create($validator->validated());
|
|
|
|
|
|
|
+ $item = CommonCatalogItem::query()->create($validated);
|
|
|
$created++;
|
|
$created++;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -113,22 +140,62 @@ class ImportCommonCatalogService
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private function assertHeaders(array $headers): void
|
|
|
|
|
|
|
+ private function assertHeaders(array $headers, array $subheaders): void
|
|
|
{
|
|
{
|
|
|
$actual = array_map($this->normalizeHeader(...), $headers);
|
|
$actual = array_map($this->normalizeHeader(...), $headers);
|
|
|
$expected = array_map($this->normalizeHeader(...), ExportCommonCatalogService::HEADERS);
|
|
$expected = array_map($this->normalizeHeader(...), ExportCommonCatalogService::HEADERS);
|
|
|
|
|
+ $actualSubheaders = array_map($this->normalizeHeader(...), $subheaders);
|
|
|
|
|
+ $expectedSubheaders = array_map($this->normalizeHeader(...), ExportCommonCatalogService::SUBHEADERS);
|
|
|
|
|
|
|
|
- if ($actual !== $expected) {
|
|
|
|
|
- throw new RuntimeException('Некорректные заголовки файла общего каталога. Используйте утверждённый шаблон.');
|
|
|
|
|
|
|
+ if ($actual !== $expected || $actualSubheaders !== $expectedSubheaders) {
|
|
|
|
|
+ throw new RuntimeException('Некорректные заголовки файла общего каталога. Используйте утверждённый формат.');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private function articleCounts(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $counts = [];
|
|
|
|
|
+ for ($row = 3; $row <= $sheet->getHighestDataRow(); $row++) {
|
|
|
|
|
+ $article = $this->stringValue($sheet->getCell("A{$row}")->getFormattedValue());
|
|
|
|
|
+ if ($article !== null) {
|
|
|
|
|
+ $counts[$article] = ($counts[$article] ?? 0) + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $counts;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function findExistingItem(
|
|
|
|
|
+ string $article,
|
|
|
|
|
+ ?string $name,
|
|
|
|
|
+ int $sourceOccurrences,
|
|
|
|
|
+ ): ?CommonCatalogItem {
|
|
|
|
|
+ $exact = CommonCatalogItem::query()
|
|
|
|
|
+ ->where('article', $article)
|
|
|
|
|
+ ->where('calculator_name', $name)
|
|
|
|
|
+ ->first();
|
|
|
|
|
+ if ($exact) {
|
|
|
|
|
+ return $exact;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($sourceOccurrences !== 1) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $items = CommonCatalogItem::query()
|
|
|
|
|
+ ->where('article', $article)
|
|
|
|
|
+ ->limit(2)
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+ return $items->count() === 1 ? $items->first() : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** @param iterable<BaseDrawing> $drawings */
|
|
/** @param iterable<BaseDrawing> $drawings */
|
|
|
private function drawingsByRow(iterable $drawings): array
|
|
private function drawingsByRow(iterable $drawings): array
|
|
|
{
|
|
{
|
|
|
$result = [];
|
|
$result = [];
|
|
|
foreach ($drawings as $drawing) {
|
|
foreach ($drawings as $drawing) {
|
|
|
- if (preg_match('/^B(\d+)$/i', $drawing->getCoordinates(), $matches) === 1) {
|
|
|
|
|
|
|
+ if (preg_match('/^S(\d+)$/i', $drawing->getCoordinates(), $matches) === 1) {
|
|
|
$result[(int) $matches[1]] = $drawing;
|
|
$result[(int) $matches[1]] = $drawing;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -216,6 +283,16 @@ class ImportCommonCatalogService
|
|
|
return str_replace([' ', ','], ['', '.'], $value);
|
|
return str_replace([' ', ','], ['', '.'], $value);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private function dimensionValue(Cell $cell): ?string
|
|
|
|
|
+ {
|
|
|
|
|
+ $calculated = $cell->getCalculatedValue();
|
|
|
|
|
+ if (is_int($calculated) || is_float($calculated)) {
|
|
|
|
|
+ return rtrim(rtrim(number_format((float) $calculated, 10, '.', ''), '0'), '.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $this->stringValue($cell->getFormattedValue());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private function integerValue(mixed $value): int|string|null
|
|
private function integerValue(mixed $value): int|string|null
|
|
|
{
|
|
{
|
|
|
$value = $this->numericValue($value);
|
|
$value = $this->numericValue($value);
|
|
@@ -226,13 +303,31 @@ class ImportCommonCatalogService
|
|
|
return is_numeric($value) ? (int) $value : (string) $value;
|
|
return is_numeric($value) ? (int) $value : (string) $value;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private function booleanValue(mixed $value): bool|string|null
|
|
|
|
|
+ {
|
|
|
|
|
+ $value = $this->stringValue($value);
|
|
|
|
|
+ if ($value === null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return match (mb_strtolower($value)) {
|
|
|
|
|
+ 'да', 'yes', '1' => true,
|
|
|
|
|
+ 'нет', 'no', '0' => false,
|
|
|
|
|
+ default => $value,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private function rules(): array
|
|
private function rules(): array
|
|
|
{
|
|
{
|
|
|
return [
|
|
return [
|
|
|
'article' => ['required', 'string', 'max:100'],
|
|
'article' => ['required', 'string', 'max:100'],
|
|
|
- 'calculator_name' => ['required', 'string'],
|
|
|
|
|
|
|
+ 'calculator_name' => ['nullable', 'string', 'max:255'],
|
|
|
'kind' => ['nullable', 'string', 'max:255'],
|
|
'kind' => ['nullable', 'string', 'max:255'],
|
|
|
- 'dimensions' => ['nullable', 'string'],
|
|
|
|
|
|
|
+ 'dimension_length' => ['nullable', 'string', 'max:255'],
|
|
|
|
|
+ 'dimension_width' => ['nullable', 'string', 'max:255'],
|
|
|
|
|
+ 'dimension_height' => ['nullable', 'string', 'max:255'],
|
|
|
|
|
+ 'site_length' => ['nullable', 'string', 'max:255'],
|
|
|
|
|
+ 'site_width' => ['nullable', 'string', 'max:255'],
|
|
|
'fall_height' => ['nullable', 'numeric', 'min:0'],
|
|
'fall_height' => ['nullable', 'numeric', 'min:0'],
|
|
|
'additional_info' => ['nullable', 'string'],
|
|
'additional_info' => ['nullable', 'string'],
|
|
|
'dimension_unit' => ['nullable', 'string', 'max:50'],
|
|
'dimension_unit' => ['nullable', 'string', 'max:50'],
|
|
@@ -245,7 +340,15 @@ class ImportCommonCatalogService
|
|
|
'unit' => ['nullable', 'string', 'max:50'],
|
|
'unit' => ['nullable', 'string', 'max:50'],
|
|
|
'series' => ['nullable', 'string', 'max:255'],
|
|
'series' => ['nullable', 'string', 'max:255'],
|
|
|
'trademark' => ['nullable', 'string', 'max:255'],
|
|
'trademark' => ['nullable', 'string', 'max:255'],
|
|
|
- 'note' => ['nullable', 'string'],
|
|
|
|
|
|
|
+ 'calculator_enabled' => ['nullable', 'boolean'],
|
|
|
|
|
+ 'builders_price' => ['nullable', 'numeric', 'min:0'],
|
|
|
|
|
+ 'wholesale_price' => ['nullable', 'numeric', 'min:0'],
|
|
|
|
|
+ 'recommended_price' => ['nullable', 'numeric', 'min:0'],
|
|
|
|
|
+ 'retail_price' => ['nullable', 'numeric', 'min:0'],
|
|
|
|
|
+ 'project_price' => ['nullable', 'numeric', 'min:0'],
|
|
|
|
|
+ 'project_with_installation_price' => ['nullable', 'numeric', 'min:0'],
|
|
|
|
|
+ 'pik_price' => ['nullable', 'numeric', 'min:0'],
|
|
|
|
|
+ 'recommended_plus_10_price' => ['nullable', 'numeric', 'min:0'],
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|