Compare commits
8 Commits
3cfaa6c9d5
...
87cab4cad5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87cab4cad5 | ||
|
|
4198c03f5a | ||
|
|
7e32a870ae | ||
|
|
2ced04c086 | ||
|
|
c0888db4a2 | ||
|
|
67d2f73eee | ||
|
|
f376a820a2 | ||
|
|
d26ba5e5e6 |
263
app/Console/Commands/ProducersDedupe.php
Normal file
263
app/Console/Commands/ProducersDedupe.php
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\ProducerDeduplicator;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
|
||||||
|
class ProducersDedupe extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'producers:dedupe
|
||||||
|
{file? : a kitöltött döntési lap (producers:dedupe-report kimenete)}
|
||||||
|
{--apply : ténylegesen végrehajtja az összevonást (alapértelmezés: csak kimutatás)}
|
||||||
|
{--report= : hova írja a jelentést (ez a visszagörgetés bemenete)}
|
||||||
|
{--rollback= : egy korábbi jelentés visszagörgetése}
|
||||||
|
{--force : megerősítő kérdések nélkül fut (szkriptelt futtatáshoz)}';
|
||||||
|
|
||||||
|
protected $description = 'Duplikált gyártó rekordok összevonása a kitöltött döntési lap alapján';
|
||||||
|
|
||||||
|
public function handle(ProducerDeduplicator $deduplicator): int
|
||||||
|
{
|
||||||
|
if ($rollbackPath = $this->option('rollback')) {
|
||||||
|
return $this->rollback($deduplicator, $rollbackPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $this->argument('file');
|
||||||
|
|
||||||
|
if (! $file || ! is_file($file)) {
|
||||||
|
$this->error('Add meg a kitöltött döntési lapot. Ha még nincs: php artisan producers:dedupe-report');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decisions = $this->readDecisions($file);
|
||||||
|
|
||||||
|
if ($decisions === []) {
|
||||||
|
$this->error('A lapon nem találtam feldolgozható sort. Megvan az "'
|
||||||
|
. ProducersDedupeReport::COLUMN_ID . '" oszlop a fejlécben?');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$plan = $deduplicator->buildPlan($decisions);
|
||||||
|
$merges = $plan['merges'];
|
||||||
|
$errors = $plan['errors'];
|
||||||
|
|
||||||
|
foreach ($errors as $error) {
|
||||||
|
$this->warn(' ! ' . $error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Az üres végleges név a bizonytalan javaslatok elutasításának módja - nem hiba,
|
||||||
|
// de kiírjuk, hogy egy véletlen kihagyás se maradjon észrevétlen.
|
||||||
|
foreach ($plan['skipped'] ?? [] as $skip) {
|
||||||
|
$this->line(' - kimarad a(z) ' . $skip['group'] . '. csoport (nincs végleges név): '
|
||||||
|
. implode(' / ', array_map(fn ($n) => '"' . $n . '"', $skip['names'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($merges === []) {
|
||||||
|
$this->error('Nincs végrehajtható összevonás.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->summarize($merges);
|
||||||
|
|
||||||
|
if (! $this->option('apply')) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('Ez csak kimutatás volt. A végrehajtáshoz: --apply');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errors !== [] && ! $this->confirmed('Voltak figyelmeztetések. Biztosan folytatod?')) {
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DB::connection()->getDatabaseName();
|
||||||
|
if (! $this->confirmed("Végrehajtás a(z) '{$database}' adatbázison. Folytatod?")) {
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = $deduplicator->apply($merges);
|
||||||
|
$reportPath = $this->writeReport($report);
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('Az összevonás megtörtént.');
|
||||||
|
foreach ($report['totals'] as $table => $count) {
|
||||||
|
$this->line(' ' . str_pad($table, 24) . number_format($count) . ' sor átírva');
|
||||||
|
}
|
||||||
|
$this->line(' Jelentés: ' . $reportPath);
|
||||||
|
$this->newLine();
|
||||||
|
$this->comment('Visszagörgetés: php artisan producers:dedupe --rollback="' . $reportPath . '"');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Megerősítés, --force esetén kérdés nélkül.
|
||||||
|
*/
|
||||||
|
private function confirmed(string $question): bool
|
||||||
|
{
|
||||||
|
return $this->option('force') || $this->confirm($question, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lap fejlécnév alapján olvasódik, nem oszlopbetű szerint: a megrendelő
|
||||||
|
* beszúrhat oszlopot vagy átrendezheti a lapot anélkül, hogy ez eltörne.
|
||||||
|
*
|
||||||
|
* @return array<int, array{group: mixed, id: int, final_name: ?string}>
|
||||||
|
*/
|
||||||
|
private function readDecisions(string $file): array
|
||||||
|
{
|
||||||
|
$sheet = IOFactory::load($file)->getActiveSheet();
|
||||||
|
$highestRow = $sheet->getHighestDataRow();
|
||||||
|
$highestColumn = $sheet->getHighestDataColumn();
|
||||||
|
$columnCount = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
|
||||||
|
|
||||||
|
$headerRow = null;
|
||||||
|
$columns = [];
|
||||||
|
|
||||||
|
for ($row = 1; $row <= min($highestRow, 30); $row++) {
|
||||||
|
for ($column = 1; $column <= $columnCount; $column++) {
|
||||||
|
$value = trim((string) $sheet->getCellByColumnAndRow($column, $row)->getValue());
|
||||||
|
|
||||||
|
if ($value === ProducersDedupeReport::COLUMN_ID) {
|
||||||
|
$headerRow = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($headerRow !== null) {
|
||||||
|
for ($column = 1; $column <= $columnCount; $column++) {
|
||||||
|
$value = trim((string) $sheet->getCellByColumnAndRow($column, $headerRow)->getValue());
|
||||||
|
if ($value !== '') {
|
||||||
|
$columns[$value] = $column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($headerRow === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$idColumn = $columns[ProducersDedupeReport::COLUMN_ID] ?? null;
|
||||||
|
$groupColumn = $columns[ProducersDedupeReport::COLUMN_GROUP] ?? null;
|
||||||
|
$nameColumn = $columns[ProducersDedupeReport::COLUMN_FINAL_NAME] ?? null;
|
||||||
|
|
||||||
|
if (! $idColumn || ! $nameColumn) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decisions = [];
|
||||||
|
|
||||||
|
for ($row = $headerRow + 1; $row <= $highestRow; $row++) {
|
||||||
|
$id = (int) $sheet->getCellByColumnAndRow($idColumn, $row)->getValue();
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decisions[] = [
|
||||||
|
'group' => $groupColumn ? $sheet->getCellByColumnAndRow($groupColumn, $row)->getValue() : null,
|
||||||
|
'id' => $id,
|
||||||
|
'final_name' => trim((string) $sheet->getCellByColumnAndRow($nameColumn, $row)->getValue()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function summarize(array $merges): void
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
$totals = ['products' => 0, 'order_archives_items' => 0, 'pricelist_file_lines' => 0];
|
||||||
|
|
||||||
|
foreach ($merges as $merge) {
|
||||||
|
foreach ($merge['from'] as $from) {
|
||||||
|
foreach ($totals as $key => $value) {
|
||||||
|
$totals[$key] += $from['counts'][$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'"' . $from['name'] . '"',
|
||||||
|
'→ #' . $merge['keeper_id'] . ' "' . $merge['final_name'] . '"'
|
||||||
|
. ($merge['rename'] ? ' (átnevezés)' : ''),
|
||||||
|
number_format($from['counts']['products']),
|
||||||
|
number_format($from['counts']['order_archives_items']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->table(['Beolvasztandó', 'Célrekord', 'Termék', 'Rendelési tétel'], $rows);
|
||||||
|
|
||||||
|
$this->line('Csoport: ' . count($merges)
|
||||||
|
. ' | beolvasztandó rekord: ' . array_sum(array_map(fn ($m) => count($m['from']), $merges)));
|
||||||
|
$this->line('Átírandó sorok: products ' . number_format($totals['products'])
|
||||||
|
. ' · order_archives_items ' . number_format($totals['order_archives_items'])
|
||||||
|
. ' · pricelist_file_lines ' . number_format($totals['pricelist_file_lines']));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeReport(array $report): string
|
||||||
|
{
|
||||||
|
$path = $this->option('report') ?: storage_path(
|
||||||
|
'app/private/producers-dedupe-' . now()->format('Y-m-d_His') . '.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
$directory = dirname($path);
|
||||||
|
if (! is_dir($directory)) {
|
||||||
|
mkdir($directory, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($path, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function rollback(ProducerDeduplicator $deduplicator, string $path): int
|
||||||
|
{
|
||||||
|
if (! is_file($path)) {
|
||||||
|
$this->error('A jelentésfájl nem található: ' . $path);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = json_decode((string) file_get_contents($path), true);
|
||||||
|
|
||||||
|
if (! is_array($report) || ! isset($report['restore'])) {
|
||||||
|
$this->error('A fájl nem érvényes producers:dedupe jelentés.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DB::connection()->getDatabaseName();
|
||||||
|
|
||||||
|
if (($report['database'] ?? null) !== $database) {
|
||||||
|
$this->warn('A jelentés a(z) "' . ($report['database'] ?? '?')
|
||||||
|
. '" adatbázisról készült, most viszont "' . $database . '" az aktív.');
|
||||||
|
|
||||||
|
if (! $this->confirmed('Biztosan folytatod?')) {
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line('Visszagörgetés: ' . count($report['merges'] ?? []) . ' összevonás ('
|
||||||
|
. ($report['created_at'] ?? '?') . ')');
|
||||||
|
|
||||||
|
if (! $this->confirmed("Visszagörgetés a(z) '{$database}' adatbázison. Folytatod?")) {
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$restored = $deduplicator->rollback($report);
|
||||||
|
|
||||||
|
$this->info('Visszagörgetve.');
|
||||||
|
foreach ($restored as $table => $count) {
|
||||||
|
$this->line(' ' . str_pad($table, 24) . number_format($count) . ' sor visszaállítva');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
291
app/Console/Commands/ProducersDedupeReport.php
Normal file
291
app/Console/Commands/ProducersDedupeReport.php
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\ProducerDeduplicator;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
|
|
||||||
|
class ProducersDedupeReport extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'producers:dedupe-report
|
||||||
|
{--path= : a kimeneti xlsx útvonala}
|
||||||
|
{--loose : a bizonytalan jelöltek is kerüljenek a lapra (elírás, kötőjel, egybeírás, ékezet)}';
|
||||||
|
|
||||||
|
protected $description = 'Döntési lapot készít a duplikált gyártónevekről (a producers:dedupe bemenete)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lap oszlopai. A producers:dedupe FEJLÉCNÉV alapján olvassa vissza, nem
|
||||||
|
* oszlopbetű szerint - így a megrendelő beszúrhat oszlopot vagy átrendezheti a
|
||||||
|
* lapot anélkül, hogy a beolvasás eltörne.
|
||||||
|
*/
|
||||||
|
public const COLUMN_GROUP = 'Csoport';
|
||||||
|
|
||||||
|
public const COLUMN_ID = 'Azonosító';
|
||||||
|
|
||||||
|
public const COLUMN_FINAL_NAME = 'VÉGLEGES NÉV (kitöltendő)';
|
||||||
|
|
||||||
|
private const TYPE_CERTAIN = 'biztos';
|
||||||
|
|
||||||
|
public function handle(ProducerDeduplicator $deduplicator): int
|
||||||
|
{
|
||||||
|
$certain = $deduplicator->duplicateGroups();
|
||||||
|
$loose = $this->option('loose') ? $deduplicator->looseCandidateGroups() : [];
|
||||||
|
|
||||||
|
if ($certain === [] && $loose === []) {
|
||||||
|
$this->info('Nincs duplikált gyártónév ebben az adatbázisban.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $this->option('path') ?: storage_path(
|
||||||
|
'app/private/gyarto_duplikaciok_' . now()->format('Y-m-d') . '.xlsx'
|
||||||
|
);
|
||||||
|
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->setTitle('Gyártó duplikációk');
|
||||||
|
|
||||||
|
$this->writeIntro($sheet, $loose !== []);
|
||||||
|
$headerRow = 9;
|
||||||
|
$this->writeHeader($sheet, $headerRow);
|
||||||
|
$lastRow = $this->writeGroups($sheet, $headerRow, $certain, $loose);
|
||||||
|
$this->finishLayout($sheet, $headerRow, $lastRow, $certain, $loose);
|
||||||
|
|
||||||
|
$directory = dirname($path);
|
||||||
|
if (! is_dir($directory)) {
|
||||||
|
mkdir($directory, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
(new Xlsx($spreadsheet))->save($path);
|
||||||
|
|
||||||
|
$this->info('Döntési lap elkészült: ' . $path);
|
||||||
|
$this->line(' Biztos duplikáció: ' . count($certain) . ' csoport, '
|
||||||
|
. array_sum(array_map('count', $certain)) . ' rekord');
|
||||||
|
|
||||||
|
if ($this->option('loose')) {
|
||||||
|
$this->line(' Ellenőrizendő javaslat: ' . count($loose) . ' csoport, '
|
||||||
|
. array_sum(array_map('count', $loose)) . ' rekord');
|
||||||
|
} else {
|
||||||
|
$this->comment(' A bizonytalan jelöltekhez (elírás, kötőjel, egybeírás, ékezet): --loose');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeIntro($sheet, bool $hasLoose): void
|
||||||
|
{
|
||||||
|
$sheet->setCellValue('A1', 'Gyártó nevek összevonása – döntési lap');
|
||||||
|
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(16);
|
||||||
|
$sheet->setCellValue('A2', 'EV3-357 · Árlista feltöltés és feldolgozás újragondolása');
|
||||||
|
$sheet->setCellValue('A3', 'Adatforrás: ' . DB::connection()->getDatabaseName()
|
||||||
|
. ' adatbázis · Készült: ' . now()->format('Y-m-d H:i'));
|
||||||
|
|
||||||
|
$sheet->setCellValue('A5', 'A rendszerben ugyanaz a gyártó több néven is szerepel – például '
|
||||||
|
. '"Danone" és "Danone " egy záró szóközzel, "Békás Kft." és "Békás Kft", vagy '
|
||||||
|
. '"Gast Food" és "Gast-Food". Ez két gondot okoz: az árlista feldolgozó valódi változás '
|
||||||
|
. 'nélkül is módosulást jelez, a statisztikában pedig egy gyártóra szűrve a másik név alá '
|
||||||
|
. 'könyvelt tételek kimaradnak a riportból.');
|
||||||
|
$sheet->mergeCells('A5:J5');
|
||||||
|
$sheet->getStyle('A5')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
|
||||||
|
$sheet->getRowDimension(5)->setRowHeight(46);
|
||||||
|
|
||||||
|
$sheet->setCellValue('A6', 'TEENDŐ: minden csoportnál töltsd ki a sárga "VÉGLEGES NÉV" mezőt – ez lesz '
|
||||||
|
. 'a megmaradó, egységes név. A többi név nem vész el: összevonjuk őket a véglegesbe, a '
|
||||||
|
. 'termékek és a rendelési előzmények megmaradnak. Az "Azonosító" oszlopot kérjük ne módosítsd.');
|
||||||
|
$sheet->mergeCells('A6:J6');
|
||||||
|
$sheet->getStyle('A6')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
|
||||||
|
$sheet->getStyle('A6')->getFont()->setBold(true);
|
||||||
|
$sheet->getRowDimension(6)->setRowHeight(46);
|
||||||
|
|
||||||
|
if ($hasLoose) {
|
||||||
|
$sheet->setCellValue('A7', 'A "Típus" oszlop megmutatja, miben térnek el a nevek – így kötegelve '
|
||||||
|
. 'tudsz haladni. A "' . ProducerDeduplicator::DIFF_PUNCTUATION . '" csoportoknál csak pont, '
|
||||||
|
. 'kötőjel vagy vessző a különbség ("Békás Kft." és "Békás Kft"), ott már beírtuk a '
|
||||||
|
. 'javaslatot. A többinél ("' . ProducerDeduplicator::DIFF_SPACING . '", "'
|
||||||
|
. ProducerDeduplicator::DIFF_ACCENT . '", "' . ProducerDeduplicator::DIFF_COMPANY_FORM
|
||||||
|
. '") NEM töltöttük ki a végleges nevet, mert ott valódi döntés kell: '
|
||||||
|
. 'ha ugyanaz a cég, írd be a nevet (a javaslat a "Megjegyzés" oszlopban van); '
|
||||||
|
. 'HA NEM UGYANAZ A CÉG, HAGYD ÜRESEN – akkor nem nyúlunk hozzájuk.');
|
||||||
|
$sheet->mergeCells('A7:J7');
|
||||||
|
$sheet->getStyle('A7')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
|
||||||
|
$sheet->getStyle('A7')->getFont()->setBold(true)->getColor()->setARGB('FF9C5700');
|
||||||
|
$sheet->getRowDimension(7)->setRowHeight(62);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeHeader($sheet, int $headerRow): void
|
||||||
|
{
|
||||||
|
$headers = [self::COLUMN_GROUP, self::COLUMN_ID, 'Típus', 'Gyártó neve a rendszerben', 'Eltérés',
|
||||||
|
'Termékek', 'Rendelési tételek', 'Létrehozva', self::COLUMN_FINAL_NAME, 'Megjegyzés'];
|
||||||
|
|
||||||
|
foreach ($headers as $i => $text) {
|
||||||
|
$sheet->setCellValueByColumnAndRow($i + 1, $headerRow, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
$range = 'A' . $headerRow . ':J' . $headerRow;
|
||||||
|
$sheet->getStyle($range)->getFont()->setBold(true)->getColor()->setARGB('FFFFFFFF');
|
||||||
|
$sheet->getStyle($range)->getFill()->setFillType(Fill::FILL_SOLID)
|
||||||
|
->getStartColor()->setARGB('FF44546A');
|
||||||
|
$sheet->getStyle($range)->getAlignment()->setWrapText(true)
|
||||||
|
->setVertical(Alignment::VERTICAL_CENTER);
|
||||||
|
$sheet->getRowDimension($headerRow)->setRowHeight(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A laza javaslatok sorrendje: elöl a gyakorlatilag eldöntött esetek, hátul azok,
|
||||||
|
* amik valódi cégismeretet igényelnek. Így a megrendelő kötegelve tud haladni.
|
||||||
|
*/
|
||||||
|
private const CATEGORY_ORDER = [
|
||||||
|
ProducerDeduplicator::DIFF_PUNCTUATION,
|
||||||
|
ProducerDeduplicator::DIFF_SPACING,
|
||||||
|
ProducerDeduplicator::DIFF_ACCENT,
|
||||||
|
ProducerDeduplicator::DIFF_COMPANY_FORM,
|
||||||
|
ProducerDeduplicator::DIFF_MIXED,
|
||||||
|
];
|
||||||
|
|
||||||
|
private function writeGroups($sheet, int $headerRow, array $certain, array $loose): int
|
||||||
|
{
|
||||||
|
$row = $headerRow + 1;
|
||||||
|
$groupIndex = 0;
|
||||||
|
|
||||||
|
foreach ($certain as $members) {
|
||||||
|
$groupIndex++;
|
||||||
|
$row = $this->writeGroup($sheet, $row, $groupIndex, self::TYPE_CERTAIN, $members, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deduplicator = app(ProducerDeduplicator::class);
|
||||||
|
$byCategory = [];
|
||||||
|
foreach ($loose as $key => $members) {
|
||||||
|
$byCategory[$deduplicator->differenceCategory($members)][$key] = $members;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::CATEGORY_ORDER as $category) {
|
||||||
|
foreach ($byCategory[$category] ?? [] as $members) {
|
||||||
|
$groupIndex++;
|
||||||
|
// Az írásjel-eltérésnél nincs mérlegelnivaló (a "Kft." és a "Kft" ugyanaz),
|
||||||
|
// ezért ott előre beírjuk a javaslatot - a megrendelő így csak a valóban
|
||||||
|
// kérdéses eseteket nézi át tételesen.
|
||||||
|
$prefill = $category === ProducerDeduplicator::DIFF_PUNCTUATION;
|
||||||
|
$row = $this->writeGroup($sheet, $row, $groupIndex, $category, $members, ! $prefill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $row - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeGroup($sheet, int $row, int $groupIndex, string $type, array $members, bool $isReview): int
|
||||||
|
{
|
||||||
|
$proposed = trim((string) $members[0]['producer']->name);
|
||||||
|
$shade = $isReview ? 'FFFDF2E9' : ($groupIndex % 2 === 0 ? 'FFF2F2F2' : 'FFFFFFFF');
|
||||||
|
$groupFirstRow = $row;
|
||||||
|
|
||||||
|
foreach ($members as $index => $member) {
|
||||||
|
$producer = $member['producer'];
|
||||||
|
$name = (string) $producer->name;
|
||||||
|
|
||||||
|
// A csoport sorszáma MINDEN soron szerepel: így a lap szűrhető és
|
||||||
|
// rendezhető anélkül, hogy a csoportok összekeverednének.
|
||||||
|
$sheet->setCellValue('A' . $row, $groupIndex);
|
||||||
|
$sheet->setCellValue('B' . $row, $producer->id);
|
||||||
|
$sheet->setCellValue('C' . $row, $type);
|
||||||
|
$sheet->setCellValueExplicit('D' . $row, '"' . $name . '"', DataType::TYPE_STRING);
|
||||||
|
$sheet->setCellValue('E' . $row, $index === 0
|
||||||
|
? ($isReview ? 'legtöbb termék' : 'javasolt megtartani')
|
||||||
|
: implode(', ', $this->describeDifference($name, $proposed)));
|
||||||
|
$sheet->setCellValue('F' . $row, $member['counts']['products']);
|
||||||
|
$sheet->setCellValue('G' . $row, $member['counts']['order_archives_items']);
|
||||||
|
$sheet->setCellValue('H' . $row, substr((string) $producer->created_at, 0, 10));
|
||||||
|
|
||||||
|
$sheet->getStyle('A' . $row . ':J' . $row)->getFill()
|
||||||
|
->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB($shade);
|
||||||
|
|
||||||
|
if ($index === 0) {
|
||||||
|
$sheet->getStyle('D' . $row)->getFont()->setBold(true);
|
||||||
|
|
||||||
|
if ($isReview) {
|
||||||
|
// Bizonytalan javaslatnál a végleges név ÜRESEN marad: az alapértelmezett
|
||||||
|
// viselkedés a "nem nyúlunk hozzá", a döntés pedig tudatos kitöltés.
|
||||||
|
$sheet->setCellValue('J' . $row, 'ha ugyanaz a cég: ' . $proposed);
|
||||||
|
$sheet->getStyle('J' . $row)->getFont()->setItalic(true);
|
||||||
|
} else {
|
||||||
|
$sheet->setCellValueExplicit('I' . $row, $proposed, DataType::TYPE_STRING);
|
||||||
|
$sheet->getStyle('I' . $row)->getFont()->setBold(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->getStyle('I' . $row)->getFill()->setFillType(Fill::FILL_SOLID)
|
||||||
|
->getStartColor()->setARGB($isReview ? 'FFFFE0B2' : 'FFFFF2CC');
|
||||||
|
}
|
||||||
|
|
||||||
|
$row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->getStyle('A' . $groupFirstRow . ':J' . ($row - 1))->getBorders()->getTop()
|
||||||
|
->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFBFBFBF');
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function describeDifference(string $name, string $proposed): array
|
||||||
|
{
|
||||||
|
$differences = [];
|
||||||
|
|
||||||
|
if ($name !== trim($name)) {
|
||||||
|
$differences[] = 'szóköz a név szélén';
|
||||||
|
}
|
||||||
|
if (trim($name) !== $proposed && mb_strtoupper(trim($name)) === mb_strtoupper($proposed)) {
|
||||||
|
$differences[] = 'eltérő kis/nagybetű';
|
||||||
|
}
|
||||||
|
if (str_contains($name, '/') || str_contains($name, '_')) {
|
||||||
|
$differences[] = 'eltérő elválasztó';
|
||||||
|
}
|
||||||
|
if (preg_match('/\s{2,}/', $name)) {
|
||||||
|
$differences[] = 'dupla szóköz';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $differences === [] ? ['eltérő írásmód'] : $differences;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function finishLayout($sheet, int $headerRow, int $lastRow, array $certain, array $loose): void
|
||||||
|
{
|
||||||
|
$first = $headerRow + 1;
|
||||||
|
|
||||||
|
$sheet->getStyle('F' . $first . ':G' . $lastRow)->getNumberFormat()->setFormatCode('# ##0');
|
||||||
|
$sheet->getStyle('A' . $first . ':C' . $lastRow)->getAlignment()
|
||||||
|
->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle('I' . $first . ':I' . $lastRow)->getBorders()->getAllBorders()
|
||||||
|
->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFBF8F00');
|
||||||
|
|
||||||
|
$widths = ['A' => 9, 'B' => 11, 'C' => 15, 'D' => 34, 'E' => 20,
|
||||||
|
'F' => 11, 'G' => 17, 'H' => 13, 'I' => 34, 'J' => 34];
|
||||||
|
foreach ($widths as $column => $width) {
|
||||||
|
$sheet->getColumnDimension($column)->setWidth($width);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->freezePane('A' . $first);
|
||||||
|
$sheet->setAutoFilter('A' . $headerRow . ':J' . $lastRow);
|
||||||
|
|
||||||
|
$summaryRow = $lastRow + 2;
|
||||||
|
$summary = 'Biztos duplikáció: ' . count($certain) . ' csoport, '
|
||||||
|
. array_sum(array_map('count', $certain)) . ' rekord.';
|
||||||
|
|
||||||
|
if ($loose !== []) {
|
||||||
|
$summary .= ' Ellenőrizendő javaslat: ' . count($loose) . ' csoport, '
|
||||||
|
. array_sum(array_map('count', $loose)) . ' rekord – ezeknél az üresen hagyott '
|
||||||
|
. 'végleges név azt jelenti, hogy nem vonjuk össze őket.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->setCellValue('A' . $summaryRow, $summary);
|
||||||
|
$sheet->mergeCells('A' . $summaryRow . ':J' . $summaryRow);
|
||||||
|
$sheet->getStyle('A' . $summaryRow)->getFont()->setItalic(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ enum PricelistSellerUnitEnum: string
|
|||||||
case liter = 'liter';
|
case liter = 'liter';
|
||||||
case pár = 'pár';
|
case pár = 'pár';
|
||||||
case tekercs = 'tekercs';
|
case tekercs = 'tekercs';
|
||||||
|
case m = 'm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visszaadja az összes értéket tömbként
|
* Visszaadja az összes értéket tömbként
|
||||||
|
|||||||
@ -24,4 +24,6 @@ class ProductUnitEnum extends BasicUnitEnum
|
|||||||
const pár = 'pár';
|
const pár = 'pár';
|
||||||
|
|
||||||
const zsák = 'zsák';
|
const zsák = 'zsák';
|
||||||
|
|
||||||
|
const m = 'm';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,4 +32,15 @@ protected function handleRecordCreation(array $data): Model
|
|||||||
|
|
||||||
return $lastRecord;
|
return $lastRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Mentés és új létrehozása" esetén a Beszállító mezőt megtartja, a többit üríti (EV3-432).
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
protected function preserveFormDataWhenCreatingAnother(array $data): array
|
||||||
|
{
|
||||||
|
return ['supplier_id' => $data['supplier_id'] ?? null];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,12 +139,17 @@ public function fetchEvents(array $info): array
|
|||||||
$assignment
|
$assignment
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// A szállítási napok halmaza a hétvége-festéshez (4. pont) is kell.
|
||||||
|
$deliveryDayLookup = array_flip(
|
||||||
|
$deliveryDates->map(fn (Carbon $date) => $date->format('Y-m-d'))->all()
|
||||||
|
);
|
||||||
|
|
||||||
foreach ($deliveryDates as $date) {
|
foreach ($deliveryDates as $date) {
|
||||||
$events[] = [
|
$events[] = [
|
||||||
'id' => 'delivery-' . $date->format('Y-m-d'),
|
'id' => 'delivery-' . $date->format('Y-m-d'),
|
||||||
'title' => '',
|
'title' => '',
|
||||||
'start' => $date->format('Y-m-d'),
|
'start' => $date->format('Y-m-d'),
|
||||||
'end' => $date->addDay()->format('Y-m-d'),
|
'end' => $date->copy()->addDay()->format('Y-m-d'),
|
||||||
'allDay' => true,
|
'allDay' => true,
|
||||||
'display' => 'background',
|
'display' => 'background',
|
||||||
'backgroundColor' => '#4CAF50',
|
'backgroundColor' => '#4CAF50',
|
||||||
@ -182,9 +187,11 @@ public function fetchEvents(array $info): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Hétvégék – szürke háttér (inaktív napok)
|
// 4. Hétvégék – szürke háttér (inaktív napok)
|
||||||
|
// Csak azok a hétvégi napok, amelyek nem szállítási napok: a heti sablon
|
||||||
|
// tartalmazhatja a szombatot/vasárnapot is (EV3-466).
|
||||||
$current = $start->copy();
|
$current = $start->copy();
|
||||||
while ($current->lte($end)) {
|
while ($current->lte($end)) {
|
||||||
if ($current->isWeekend()) {
|
if ($current->isWeekend() && ! isset($deliveryDayLookup[$current->format('Y-m-d')])) {
|
||||||
$events[] = [
|
$events[] = [
|
||||||
'id' => 'weekend-' . $current->format('Y-m-d'),
|
'id' => 'weekend-' . $current->format('Y-m-d'),
|
||||||
'title' => '',
|
'title' => '',
|
||||||
|
|||||||
@ -1759,7 +1759,7 @@ private function updateSend(Request $request, int $orderId): JsonResponse
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config('mail.MailToOrderAddress') != 'city99@e98.hu') {
|
if (config('mail.MailToOrderAddress') != 'city99@e98.hu') {
|
||||||
$m->bcc('city@e98.hu');
|
$m->bcc('city99@e98.hu');
|
||||||
}
|
}
|
||||||
// $m->attach($attach,['as'=>$fileNameWithExt]);
|
// $m->attach($attach,['as'=>$fileNameWithExt]);
|
||||||
if ($fileType !== 'none') {
|
if ($fileType !== 'none') {
|
||||||
|
|||||||
@ -72,9 +72,11 @@ public function isDeliveryDay(ProfitCenter $pc, Supplier $supplier, Carbon $date
|
|||||||
return $override->is_delivery_day;
|
return $override->is_delivery_day;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Prioritás: Hivatalos ünnepnap (WorkCalendar)
|
// 4. Prioritás: Hivatalos munkaszüneti nap (WorkCalendar)
|
||||||
// Ha aznap nem hivatalos munkanap van, akkor nincs szállítás (kivéve ha az 1-3. pont felülbírálta).
|
// Csak a naptárban ünnepnapként rögzített dátum tilt. A naptári hétvége önmagában
|
||||||
if (! $this->workCalendarService->isWorkDay($date)) {
|
// NEM: azt az 5. pont heti sablonja dönti el, különben egy hétvégét is tartalmazó
|
||||||
|
// sablon napjai elérhetetlenek maradnának (EV3-466).
|
||||||
|
if ($this->workCalendarService->isHoliday($date)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,36 +62,65 @@ public function calculate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->findNextDeliveryDay($startDay, $supplier, $pc, $leadDays);
|
return $this->findNextDeliveryDay($startDay, $supplier, $pc, $leadDays, $assignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Munkanapokban számol előre ($leadDays darabot), majd az első olyan napot adja vissza,
|
* Előreszámol $leadDays darab átfutási napot, majd az első olyan napot adja vissza,
|
||||||
* amelyik szállítási nap is.
|
* amelyik szállítási nap is.
|
||||||
|
*
|
||||||
|
* A szállítási nap keresése NINCS munkanaphoz kötve: hétvégi szállítást a heti sablon
|
||||||
|
* engedhet, ezt az isDeliveryDay() dönti el (EV3-466).
|
||||||
*/
|
*/
|
||||||
private function findNextDeliveryDay(
|
private function findNextDeliveryDay(
|
||||||
Carbon $candidate,
|
Carbon $candidate,
|
||||||
Supplier $supplier,
|
Supplier $supplier,
|
||||||
ProfitCenter $pc,
|
ProfitCenter $pc,
|
||||||
int $leadDays,
|
int $leadDays,
|
||||||
|
ProfitCenterSupplierSchedule $assignment,
|
||||||
): ?Carbon {
|
): ?Carbon {
|
||||||
$workingDaysCounted = 0;
|
$leadDaysCounted = 0;
|
||||||
|
|
||||||
// Max. 365 nap iteráció (végtelen ciklus-védelem)
|
// Max. 365 nap iteráció (végtelen ciklus-védelem)
|
||||||
for ($i = 0; $i < 365; $i++) {
|
for ($i = 0; $i < 365; $i++) {
|
||||||
if ($this->workCalendarService->isWorkDay($candidate)) {
|
if ($leadDaysCounted >= $leadDays) {
|
||||||
if ($workingDaysCounted >= $leadDays) {
|
// Átfutási idő letelt – az első szállítási napot keressük
|
||||||
// Átfutási idő letelt – az első szállítási napot keressük
|
if ($this->deliveryCalendarService->isDeliveryDay($pc, $supplier, $candidate, $assignment)) {
|
||||||
if ($this->deliveryCalendarService->isDeliveryDay($pc, $supplier, $candidate)) {
|
return $candidate->copy();
|
||||||
return $candidate->copy();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$workingDaysCounted++;
|
|
||||||
}
|
}
|
||||||
|
} elseif ($this->countsTowardLeadTime($candidate, $assignment)) {
|
||||||
|
$leadDaysCounted++;
|
||||||
}
|
}
|
||||||
$candidate->addDay();
|
$candidate->addDay();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eldönti, hogy az adott nap beleszámít-e az átfutási idő visszaszámolásába.
|
||||||
|
*
|
||||||
|
* A hivatalos munkanapokon túl beleszámítanak azok a hétvégi napok is, amelyeket a
|
||||||
|
* PC-hez rendelt heti sablon szállítási napként jelöl – egy 7 napos sablonnál tehát
|
||||||
|
* minden nap telik, egy H–P sablonnál viszont a hétvége változatlanul nem (EV3-466).
|
||||||
|
* Munkaszüneti napon sosem telik az átfutás.
|
||||||
|
*/
|
||||||
|
private function countsTowardLeadTime(Carbon $date, ProfitCenterSupplierSchedule $assignment): bool
|
||||||
|
{
|
||||||
|
if ($this->workCalendarService->isHoliday($date)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->workCalendarService->isWorkDay($date)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedule = $assignment->deliverySchedule;
|
||||||
|
|
||||||
|
if (! $schedule || ! $schedule->is_active) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) $schedule->{strtolower($date->format('l'))};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
use App\Repositories\ProducerRepository;
|
use App\Repositories\ProducerRepository;
|
||||||
use App\Repositories\ProductRepository;
|
use App\Repositories\ProductRepository;
|
||||||
use App\Repositories\SupplierRepository;
|
use App\Repositories\SupplierRepository;
|
||||||
|
use App\Support\NameNormalizer;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\MessageBag;
|
use Illuminate\Support\MessageBag;
|
||||||
|
|
||||||
@ -22,6 +23,9 @@ class PriceListService
|
|||||||
|
|
||||||
protected ProducerRepository $producerRepository;
|
protected ProducerRepository $producerRepository;
|
||||||
|
|
||||||
|
/** @var array<string, array>|null normalizált gyártónév => rekord (lusta index) */
|
||||||
|
private ?array $producersByNormalizedName = null;
|
||||||
|
|
||||||
protected ProductRepository $productRepository;
|
protected ProductRepository $productRepository;
|
||||||
|
|
||||||
protected MessageBag $error;
|
protected MessageBag $error;
|
||||||
@ -567,6 +571,7 @@ private function loadProducers(?string $index = null)
|
|||||||
$this->producerIndexedBy = $index;
|
$this->producerIndexedBy = $index;
|
||||||
}
|
}
|
||||||
$this->producers = $this->producerRepository->allByIndex($index);
|
$this->producers = $this->producerRepository->allByIndex($index);
|
||||||
|
$this->producersByNormalizedName = null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,7 +582,32 @@ private function getProducerByName($name)
|
|||||||
return $this->producers[$name];
|
return $this->producers[$name];
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// Normalizált egyeztetés, ha a karakterre pontos találat elmarad.
|
||||||
|
//
|
||||||
|
// A beszállítói Excelekben a gyártó neve rendszeresen eltérő kis/nagybetűvel
|
||||||
|
// vagy fölösleges szóközzel érkezik ("Danone " vs "Danone"). Korábban ilyenkor
|
||||||
|
// ÚJ gyártó jött létre - így halmozódott fel a rendszerben 70 duplikált név,
|
||||||
|
// ami az árlista feldolgozóban hamis módosulást, a statisztikában pedig hiányzó
|
||||||
|
// tételeket okozott (egy névre szűrve a másik ID alá könyvelt sorok kimaradtak).
|
||||||
|
$normalized = NameNormalizer::normalize((string) $name);
|
||||||
|
|
||||||
|
if ($normalized === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->producersByNormalizedName === null) {
|
||||||
|
$this->producersByNormalizedName = [];
|
||||||
|
|
||||||
|
foreach ($this->producers as $producerName => $producer) {
|
||||||
|
// Az ELSŐ előfordulás nyer: a Producer::all() id szerint jön, tehát a
|
||||||
|
// legrégebbi rekord a kanonikus - jellemzően az, amelyikre a termékek
|
||||||
|
// többsége mutat.
|
||||||
|
$key = NameNormalizer::normalize((string) $producerName);
|
||||||
|
$this->producersByNormalizedName[$key] ??= $producer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->producersByNormalizedName[$normalized] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function initProducerRepository()
|
private function initProducerRepository()
|
||||||
@ -590,6 +620,9 @@ private function initProducerRepository()
|
|||||||
private function addNewProducer($data)
|
private function addNewProducer($data)
|
||||||
{
|
{
|
||||||
$this->initProducerRepository();
|
$this->initProducerRepository();
|
||||||
|
// Körülvágás mentés előtt: a nyers Excel-értékből származó záró szóköz miatt
|
||||||
|
// keletkezett a duplikátumok egyharmada.
|
||||||
|
$data['name'] = trim((string) ($data['name'] ?? ''));
|
||||||
$data['canSee'] = 1;
|
$data['canSee'] = 1;
|
||||||
$data['status'] = \App\Enums\DbStatusFieldEnum::active;
|
$data['status'] = \App\Enums\DbStatusFieldEnum::active;
|
||||||
if ($id = $this->producerRepository->add($data)) {
|
if ($id = $this->producerRepository->add($data)) {
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Enums\PricelistWorkflowStep;
|
use App\Enums\PricelistWorkflowStep;
|
||||||
use App\Models\PriceList;
|
use App\Models\PriceList;
|
||||||
use App\Models\PricelistFile;
|
use App\Models\PricelistFile;
|
||||||
|
use App\Support\NameNormalizer;
|
||||||
use App\Enums\PricelistFileStatusEnum;
|
use App\Enums\PricelistFileStatusEnum;
|
||||||
use App\Models\PricelistFileLine;
|
use App\Models\PricelistFileLine;
|
||||||
use App\Enums\PricelistFileLineStatusEnum;
|
use App\Enums\PricelistFileLineStatusEnum;
|
||||||
@ -814,17 +815,10 @@ public function validate(PricelistFile $pricelistFile): bool
|
|||||||
*/
|
*/
|
||||||
protected function normalizeForComparison(string $text): string
|
protected function normalizeForComparison(string $text): string
|
||||||
{
|
{
|
||||||
// 1. Kis/Nagybetű érzéketlenség UTF-8 támogatással
|
// A szabály a NameNormalizerben él, mert a legacy import és a gyártó-összevonó
|
||||||
$text = mb_strtoupper(trim($text), 'UTF-8');
|
// karbantartás is UGYANEZT az egyeztetést kell hogy használja - ha ezek
|
||||||
|
// elcsúsznak egymástól, az újra duplikált törzsadatot eredményez.
|
||||||
// 2. Szeparátorok egységesítése (perjel és alulvonás cseréje)
|
return NameNormalizer::normalize($text);
|
||||||
// A hibaüzenetben látható alulvonás és a felhasználó által említett perjel miatt
|
|
||||||
$text = str_replace(['/', '_'], ' ', $text);
|
|
||||||
|
|
||||||
// 3. Felesleges szóközök eltávolítása (ha a csere után több szóköz maradt)
|
|
||||||
$text = preg_replace('/\s+/', ' ', $text);
|
|
||||||
|
|
||||||
return trim($text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -857,7 +851,12 @@ protected function getProductGroupLookupMap(): array
|
|||||||
*/
|
*/
|
||||||
protected function getProducerLookupMap(): array
|
protected function getProducerLookupMap(): array
|
||||||
{
|
{
|
||||||
return \App\Models\Producer::all()
|
// Az összevont (archive) és a törölt gyártók kimaradnak: enélkül a
|
||||||
|
// duplikáció-takarítás hatástalan lenne, mert a beolvasztott rekord
|
||||||
|
// továbbra is visszakerülne a feloldásba.
|
||||||
|
return \App\Models\Producer::query()
|
||||||
|
->whereNotIn('status', [DbStatusFieldEnum::archive, DbStatusFieldEnum::deleted])
|
||||||
|
->get()
|
||||||
->keyBy(fn($p) => $this->normalizeForComparison($p->name))
|
->keyBy(fn($p) => $this->normalizeForComparison($p->name))
|
||||||
->map->id
|
->map->id
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|||||||
583
app/Services/ProducerDeduplicator.php
Normal file
583
app/Services/ProducerDeduplicator.php
Normal file
@ -0,0 +1,583 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Enums\DbStatusFieldEnum;
|
||||||
|
use App\Support\NameNormalizer;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplikált gyártó törzsadat felderítése és összevonása.
|
||||||
|
*
|
||||||
|
* A duplikátumok onnan származnak, hogy a legacy árlista import karakterre pontos
|
||||||
|
* egyezést követelt a gyártónévre, és találat híján újat vett fel a nyers Excel
|
||||||
|
* értékkel - így lett a "Danone" mellett "Danone " is. Két helyen fáj:
|
||||||
|
*
|
||||||
|
* - az árlista feldolgozó valódi változás nélkül is módosulást jelez, mert a termék
|
||||||
|
* az egyik, a névfeloldás a másik rekordra mutat,
|
||||||
|
* - a Termék mennyiség statisztika gyártóra szűrve a másik ID alá könyvelt tételeket
|
||||||
|
* kihagyja a riportból.
|
||||||
|
*
|
||||||
|
* A keletkezés útját a NameNormalizer bevezetése zárta le; ez az osztály a már
|
||||||
|
* felhalmozódott adatot takarítja.
|
||||||
|
*/
|
||||||
|
class ProducerDeduplicator
|
||||||
|
{
|
||||||
|
/** A hivatkozó táblák: tábla => oszlop. FK csak a pricelist_file_lines-on van. */
|
||||||
|
public const REFERENCING_TABLES = [
|
||||||
|
'products' => 'producer_id',
|
||||||
|
'order_archives_items' => 'producer_id',
|
||||||
|
'pricelist_file_lines' => 'producer_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const UPDATE_CHUNK = 5000;
|
||||||
|
|
||||||
|
/** Az eltérés jellege egy laza jelölt-csoportban (lásd differenceCategory) */
|
||||||
|
public const DIFF_PUNCTUATION = 'csak írásjel';
|
||||||
|
|
||||||
|
public const DIFF_SPACING = 'egybeírás';
|
||||||
|
|
||||||
|
public const DIFF_ACCENT = 'ékezet';
|
||||||
|
|
||||||
|
public const DIFF_COMPANY_FORM = 'cégforma';
|
||||||
|
|
||||||
|
public const DIFF_MIXED = 'vegyes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplikált névcsoportok, csoportonként a tagokkal és a hivatkozás-darabszámokkal.
|
||||||
|
* A tagok rendezettek: az első a javasolt megtartandó (legtöbb termék).
|
||||||
|
*
|
||||||
|
* @return array<string, array<int, array{producer: object, counts: array<string, int>}>>
|
||||||
|
*/
|
||||||
|
public function duplicateGroups(): array
|
||||||
|
{
|
||||||
|
$byNormalized = [];
|
||||||
|
foreach ($this->activeMembers() as $member) {
|
||||||
|
$byNormalized[NameNormalizer::normalize($member['producer']->name)][] = $member;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups = [];
|
||||||
|
foreach ($byNormalized as $normalized => $members) {
|
||||||
|
if (count($members) < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups[$normalized] = $this->sortByKeeperPriority($members);
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($groups);
|
||||||
|
|
||||||
|
return $groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Laza jelöltek: olyan gyártók, amelyeket a szigorú szabály NEM köt össze, de nagy
|
||||||
|
* eséllyel ugyanaz a cég - elírás, kötőjel, egybeírás, hiányzó ékezet miatt térnek el.
|
||||||
|
*
|
||||||
|
* Ezek SOSEM olvadnak össze maguktól: a riportba javaslatként kerülnek, a döntést a
|
||||||
|
* megrendelő hozza. Ezért lehet itt megengedőbb a szabály, mint az import-párosításnál,
|
||||||
|
* ahol egy téves egyezés csendben rossz gyártóhoz rendelne termékeket.
|
||||||
|
*
|
||||||
|
* Egy dolgot viszont itt is tiszteletben tartunk: a KÜLÖNBÖZŐ cégforma (Kft vs Zrt)
|
||||||
|
* valódi különbség, nem elírás - azokat nem javasoljuk összevonásra. A cégforma
|
||||||
|
* nélküli név viszont párba állhat egy cégformással, ha csak egyféle forma van.
|
||||||
|
*
|
||||||
|
* @return array<string, array<int, array{producer: object, counts: array<string, int>}>>
|
||||||
|
*/
|
||||||
|
public function looseCandidateGroups(): array
|
||||||
|
{
|
||||||
|
$byBase = [];
|
||||||
|
foreach ($this->activeMembers() as $member) {
|
||||||
|
[$base, $form] = $this->splitCompanyForm($member['producer']->name);
|
||||||
|
|
||||||
|
if ($base === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$byBase[$base][$form][] = $member;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = [];
|
||||||
|
|
||||||
|
foreach ($byBase as $base => $byForm) {
|
||||||
|
$explicitForms = array_diff(array_keys($byForm), ['']);
|
||||||
|
|
||||||
|
if (count($explicitForms) > 1) {
|
||||||
|
// Kft ÉS Zrt is van ugyanarra a névre: ezek nem egymás elírásai. A
|
||||||
|
// formánként külön csoportok mehetnek, a forma nélküli viszont nem
|
||||||
|
// rendelhető egyértelműen egyikhez sem, ezért kimarad.
|
||||||
|
foreach ($explicitForms as $form) {
|
||||||
|
$candidates[$base . '|' . $form] = $byForm[$form];
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates[$base] = array_merge(...array_values($byForm));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Csak az érdekes: ami több szigorú csoportot fog össze (vagy olyan rekordokat,
|
||||||
|
// amiket a szigorú szabály külön hagyott).
|
||||||
|
$strictKeys = [];
|
||||||
|
foreach ($candidates as $key => $members) {
|
||||||
|
$strictKeys[$key] = array_unique(array_map(
|
||||||
|
fn ($m) => NameNormalizer::normalize($m['producer']->name),
|
||||||
|
$members,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($candidates as $key => $members) {
|
||||||
|
if (count($members) < 2 || count($strictKeys[$key]) < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[$key] = $this->sortByKeeperPriority($members);
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($result);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Egy laza jelölt-csoport eltérésének jellege - a LEGKEVÉSBÉ agresszív átalakítás,
|
||||||
|
* ami már egy kulcsra hozza a tagokat.
|
||||||
|
*
|
||||||
|
* Ez adja meg a megrendelőnek, mennyi mérlegelést igényel a sor: a "csak írásjel"
|
||||||
|
* eseteknél gyakorlatilag nincs döntés, a "cégforma" és a "vegyes" viszont valódi
|
||||||
|
* ismeretet kíván a cégekről.
|
||||||
|
*
|
||||||
|
* @param array<int, array{producer: object, counts: array<string, int>}> $members
|
||||||
|
*/
|
||||||
|
public function differenceCategory(array $members): string
|
||||||
|
{
|
||||||
|
$names = array_map(fn ($m) => (string) $m['producer']->name, $members);
|
||||||
|
|
||||||
|
$levels = [
|
||||||
|
self::DIFF_PUNCTUATION => fn ($n) => $this->stripPunctuation(NameNormalizer::normalize($n)),
|
||||||
|
self::DIFF_SPACING => fn ($n) => preg_replace('/\s+/u', '',
|
||||||
|
$this->stripPunctuation(NameNormalizer::normalize($n))),
|
||||||
|
self::DIFF_ACCENT => fn ($n) => preg_replace('/\s+/u', '',
|
||||||
|
$this->stripAccents($this->stripPunctuation(NameNormalizer::normalize($n)))),
|
||||||
|
self::DIFF_COMPANY_FORM => fn ($n) => $this->splitCompanyForm($n)[0],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($levels as $category => $key) {
|
||||||
|
if (count(array_unique(array_map($key, $names))) === 1) {
|
||||||
|
return $category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::DIFF_MIXED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stripPunctuation(string $text): string
|
||||||
|
{
|
||||||
|
return trim(preg_replace('/\s+/u', ' ', preg_replace('/[.,\-\'"()]/u', ' ', $text) ?? $text) ?? $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stripAccents(string $text): string
|
||||||
|
{
|
||||||
|
return strtr($text, [
|
||||||
|
'Á' => 'A', 'É' => 'E', 'Í' => 'I', 'Ó' => 'O', 'Ö' => 'O',
|
||||||
|
'Ő' => 'O', 'Ú' => 'U', 'Ü' => 'U', 'Ű' => 'U',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Név szétbontása alapnévre és cégforma-toldatra, a "csak elírás" eltérések
|
||||||
|
* kiszűrésével: írásjelek, ékezetek és szóközök elhagyásával.
|
||||||
|
*
|
||||||
|
* @return array{0: string, 1: string} [alapnév, cégforma]
|
||||||
|
*/
|
||||||
|
private function splitCompanyForm(string $name): array
|
||||||
|
{
|
||||||
|
$normalized = $this->stripAccents($this->stripPunctuation(NameNormalizer::normalize($name)));
|
||||||
|
|
||||||
|
$form = '';
|
||||||
|
$pattern = '/\b(KFT|ZRT|BT|NYRT|KKT|RT|GMBH|LTD|INC|SRL|NV|BV|AG|SA|SPA)\b/u';
|
||||||
|
|
||||||
|
if (preg_match($pattern, $normalized, $matches)) {
|
||||||
|
$form = $matches[1];
|
||||||
|
$normalized = preg_replace($pattern, ' ', $normalized) ?? $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A szóközök teljes elhagyása fogja meg az egybeírást ("Alfölditej" = "Alföldi Tej")
|
||||||
|
return [preg_replace('/\s+/u', '', $normalized) ?? '', $form];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{producer: object, counts: array<string, int>}>
|
||||||
|
*/
|
||||||
|
private function activeMembers(): array
|
||||||
|
{
|
||||||
|
$counts = $this->referenceCounts();
|
||||||
|
|
||||||
|
return DB::table('producers')
|
||||||
|
->whereNotIn('status', [DbStatusFieldEnum::archive, DbStatusFieldEnum::deleted])
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->map(fn ($producer) => [
|
||||||
|
'producer' => $producer,
|
||||||
|
'counts' => [
|
||||||
|
'products' => $counts['products'][$producer->id] ?? 0,
|
||||||
|
'order_archives_items' => $counts['order_archives_items'][$producer->id] ?? 0,
|
||||||
|
'pricelist_file_lines' => $counts['pricelist_file_lines'][$producer->id] ?? 0,
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A megtartandó rekord kerül előre: a legtöbb TERMÉKKEL rendelkező, mert így mozdul
|
||||||
|
* a legkevesebb sor. Döntetlennél a több rendelési tétel, majd a régebbi rekord.
|
||||||
|
*
|
||||||
|
* @param array<int, array> $members
|
||||||
|
* @return array<int, array>
|
||||||
|
*/
|
||||||
|
private function sortByKeeperPriority(array $members): array
|
||||||
|
{
|
||||||
|
usort($members, function ($a, $b) {
|
||||||
|
return [$b['counts']['products'], $b['counts']['order_archives_items'], $a['producer']->id]
|
||||||
|
<=> [$a['counts']['products'], $a['counts']['order_archives_items'], $b['producer']->id];
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_values($members);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<int, int>> tábla => [producer_id => darab]
|
||||||
|
*/
|
||||||
|
private function referenceCounts(): array
|
||||||
|
{
|
||||||
|
$counts = [];
|
||||||
|
|
||||||
|
foreach (self::REFERENCING_TABLES as $table => $column) {
|
||||||
|
$counts[$table] = DB::table($table)
|
||||||
|
->selectRaw("{$column} as pid, COUNT(*) as c")
|
||||||
|
->whereNotNull($column)
|
||||||
|
->groupBy($column)
|
||||||
|
->pluck('c', 'pid')
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Összevonási terv a döntési lap sorai alapján.
|
||||||
|
*
|
||||||
|
* A csoportosítás forrása a LAP "Csoport" oszlopa, nem az adatbázis normalizálása.
|
||||||
|
* Ez azért fontos, mert a megrendelő olyan gyártókat is egybe akarhat vonni, amiket a
|
||||||
|
* névnormalizálás nem tud összekötni: a "Kőröstej" és a "Kőröstej Kft" ugyanaz a cég,
|
||||||
|
* de a nevük érdemben különbözik. Ha a lap ezeket egy csoportba teszi, itt is egy
|
||||||
|
* összevonás lesz belőlük - így nem kell a végrehajtásba csoportokon átívelő logika.
|
||||||
|
*
|
||||||
|
* @param array<int, array{group: int|string, id: int, final_name: ?string}> $decisions
|
||||||
|
* @return array{merges: array<int, array>, errors: array<int, string>}
|
||||||
|
*/
|
||||||
|
public function buildPlan(array $decisions): array
|
||||||
|
{
|
||||||
|
$groups = $this->duplicateGroups();
|
||||||
|
|
||||||
|
$memberById = [];
|
||||||
|
foreach ($this->activeMembers() as $member) {
|
||||||
|
$memberById[$member['producer']->id] = $member;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedById = [];
|
||||||
|
foreach ($groups as $normalized => $members) {
|
||||||
|
foreach ($members as $member) {
|
||||||
|
$normalizedById[$member['producer']->id] = $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheetGroups = [];
|
||||||
|
$finalNames = [];
|
||||||
|
$skipped = [];
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($decisions as $row) {
|
||||||
|
$id = (int) ($row['id'] ?? 0);
|
||||||
|
|
||||||
|
if (! isset($memberById[$id])) {
|
||||||
|
$errors[] = "A(z) {$id} azonosítójú gyártó nem található (vagy már összevonták, "
|
||||||
|
. 'vagy más környezetből származik a lap).';
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = trim((string) ($row['group'] ?? ''));
|
||||||
|
|
||||||
|
if ($key === '') {
|
||||||
|
$errors[] = "A(z) {$id} azonosítójú sorból hiányzik a csoport száma.";
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheetGroups[$key][] = $id;
|
||||||
|
|
||||||
|
$name = trim((string) ($row['final_name'] ?? ''));
|
||||||
|
if ($name !== '') {
|
||||||
|
$finalNames[$key] ??= $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$validation = $this->validateGrouping($sheetGroups, $normalizedById, $groups);
|
||||||
|
$errors = array_merge($errors, $validation['errors']);
|
||||||
|
|
||||||
|
$merges = [];
|
||||||
|
|
||||||
|
foreach ($sheetGroups as $key => $ids) {
|
||||||
|
// A hibás csoportot NEM hajtjuk végre: egy hiányzó tag azt jelentené, hogy
|
||||||
|
// az összevonás után is maradna azonos nevű rekord.
|
||||||
|
if (isset($validation['invalid'][$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$members = array_values(array_map(fn ($id) => $memberById[$id], array_unique($ids)));
|
||||||
|
|
||||||
|
// Üres végleges név = a megrendelő NEM kéri az összevonást. Ez a laza
|
||||||
|
// javaslatok elutasításának módja, ezért nem hiba, hanem kihagyás - de
|
||||||
|
// jelentjük, hogy egy véletlen kihagyás se maradjon észrevétlen.
|
||||||
|
if (! isset($finalNames[$key])) {
|
||||||
|
$skipped[] = [
|
||||||
|
'group' => $key,
|
||||||
|
'names' => array_map(fn ($m) => (string) $m['producer']->name, $members),
|
||||||
|
];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$members = $this->sortByKeeperPriority($members);
|
||||||
|
$keeper = $members[0]['producer'];
|
||||||
|
$losers = array_slice($members, 1);
|
||||||
|
|
||||||
|
if ($losers === []) {
|
||||||
|
continue; // egytagú csoport: nincs mit összevonni
|
||||||
|
}
|
||||||
|
|
||||||
|
$merges[] = [
|
||||||
|
'group' => $key,
|
||||||
|
'keeper_id' => $keeper->id,
|
||||||
|
'keeper_name' => $keeper->name,
|
||||||
|
'final_name' => $finalNames[$key],
|
||||||
|
'rename' => trim((string) $keeper->name) !== $finalNames[$key],
|
||||||
|
'from' => array_map(fn ($m) => [
|
||||||
|
'id' => $m['producer']->id,
|
||||||
|
'name' => $m['producer']->name,
|
||||||
|
'counts' => $m['counts'],
|
||||||
|
], $losers),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['merges' => $merges, 'errors' => $errors, 'skipped' => $skipped];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lap-csoportosítás ellenőrzése.
|
||||||
|
*
|
||||||
|
* A lap SZÉLESEBB csoportot csinálhat, mint a névnormalizálás (ez a cél), de
|
||||||
|
* SZŰKEBBET nem: ha az azonos nevű rekordok külön lap-csoportba kerülnének, a
|
||||||
|
* végén két azonos nevű gyártó maradna - vagyis pont a duplikációt állítanánk elő.
|
||||||
|
*
|
||||||
|
* @return array{errors: array<int, string>, invalid: array<string, true>}
|
||||||
|
*/
|
||||||
|
private function validateGrouping(array $sheetGroups, array $normalizedById, array $groups): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
$invalid = [];
|
||||||
|
$sheetKeysByNormalized = [];
|
||||||
|
$idsByNormalized = [];
|
||||||
|
|
||||||
|
foreach ($sheetGroups as $key => $ids) {
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
// A lapon szerepelhet olyan gyártó is, ami NEM tagja szigorú duplikátum
|
||||||
|
// csoportnak (a laza javaslatok ilyenek) - ott nincs mit ellenőrizni.
|
||||||
|
if (! isset($normalizedById[$id])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = $normalizedById[$id];
|
||||||
|
$sheetKeysByNormalized[$normalized][$key] = true;
|
||||||
|
$idsByNormalized[$normalized][$id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($sheetKeysByNormalized as $normalized => $keys) {
|
||||||
|
if (count($keys) > 1) {
|
||||||
|
$errors[] = 'Az azonos nevű gyártók ("' . $normalized . '") több csoportba kerültek a lapon ('
|
||||||
|
. implode(', ', array_keys($keys)) . '). Ezeket egy csoportba kell tenni, '
|
||||||
|
. 'különben duplikátum maradna utánuk.';
|
||||||
|
|
||||||
|
foreach (array_keys($keys) as $key) {
|
||||||
|
$invalid[$key] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected = array_map(fn ($m) => $m['producer']->id, $groups[$normalized]);
|
||||||
|
$missing = array_diff($expected, array_keys($idsByNormalized[$normalized]));
|
||||||
|
|
||||||
|
if ($missing !== []) {
|
||||||
|
$errors[] = 'A döntési lap nem tartalmazza a csoport minden tagját (hiányzó azonosító: '
|
||||||
|
. implode(', ', $missing) . '). Generáld újra a lapot ebben a környezetben.';
|
||||||
|
|
||||||
|
foreach (array_keys($keys) as $key) {
|
||||||
|
$invalid[$key] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['errors' => $errors, 'invalid' => $invalid];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A terv végrehajtása. Előbb visszaállítási pontot ír, csak utána módosít.
|
||||||
|
*
|
||||||
|
* @return array a jelentés, ami egyben a visszagörgetés bemenete is
|
||||||
|
*/
|
||||||
|
public function apply(array $merges): array
|
||||||
|
{
|
||||||
|
$report = [
|
||||||
|
'created_at' => now()->toDateTimeString(),
|
||||||
|
'database' => DB::connection()->getDatabaseName(),
|
||||||
|
'merges' => [],
|
||||||
|
'restore' => [],
|
||||||
|
'totals' => array_fill_keys(array_keys(self::REFERENCING_TABLES), 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// producer_id leképezés: honnan => hova
|
||||||
|
$map = [];
|
||||||
|
foreach ($merges as $merge) {
|
||||||
|
foreach ($merge['from'] as $from) {
|
||||||
|
$map[$from['id']] = $merge['keeper_id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($map === []) {
|
||||||
|
return $report;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Visszaállítási pont: soronként a régi érték. Enélkül a művelet nem lenne
|
||||||
|
// visszafordítható, mert a fordított leképezés azokat a sorokat is átírná,
|
||||||
|
// amelyek eredetileg is a megtartott rekordra mutattak.
|
||||||
|
foreach (self::REFERENCING_TABLES as $table => $column) {
|
||||||
|
$report['restore'][$table] = DB::table($table)
|
||||||
|
->whereIn($column, array_keys($map))
|
||||||
|
->pluck($column, 'id')
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
$report['restore']['producers'] = DB::table('producers')
|
||||||
|
->whereIn('id', array_merge(array_keys($map), array_column($merges, 'keeper_id')))
|
||||||
|
->get(['id', 'name', 'status', 'canSee', 'note'])
|
||||||
|
->keyBy('id')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
// 2. Hivatkozások átírása - táblánként EGY menetben, mert a producer_id-n a nagy
|
||||||
|
// táblákon nincs index, tehát minden külön WHERE teljes scant jelentene.
|
||||||
|
foreach (self::REFERENCING_TABLES as $table => $column) {
|
||||||
|
$report['totals'][$table] = $this->remap($table, $column, $map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. A gyártó rekordok rendezése
|
||||||
|
foreach ($merges as $merge) {
|
||||||
|
if ($merge['rename']) {
|
||||||
|
DB::table('producers')->where('id', $merge['keeper_id'])
|
||||||
|
->update(['name' => $merge['final_name'], 'updated_at' => now()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($merge['from'] as $from) {
|
||||||
|
DB::table('producers')->where('id', $from['id'])->update([
|
||||||
|
'status' => DbStatusFieldEnum::archive,
|
||||||
|
'canSee' => 0,
|
||||||
|
// Tartós nyom az adatbázisban: a jelentésfájl elveszhet, ez nem.
|
||||||
|
'note' => trim(sprintf(
|
||||||
|
'Összevonva ide: #%d (%s) – %s producers:dedupe',
|
||||||
|
$merge['keeper_id'],
|
||||||
|
$merge['final_name'],
|
||||||
|
now()->toDateString(),
|
||||||
|
)),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$report['merges'][] = [
|
||||||
|
'keeper_id' => $merge['keeper_id'],
|
||||||
|
'final_name' => $merge['final_name'],
|
||||||
|
'from' => array_map(fn ($f) => ['id' => $f['id'], 'name' => $f['name']], $merge['from']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Egyetlen UPDATE táblánként, CASE leképezéssel.
|
||||||
|
*/
|
||||||
|
private function remap(string $table, string $column, array $map): int
|
||||||
|
{
|
||||||
|
$ids = array_map('intval', array_keys($map));
|
||||||
|
$updated = 0;
|
||||||
|
|
||||||
|
foreach (array_chunk($ids, 200) as $chunk) {
|
||||||
|
$cases = '';
|
||||||
|
foreach ($chunk as $from) {
|
||||||
|
$cases .= sprintf(' WHEN %d THEN %d', $from, (int) $map[$from]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated += DB::update(sprintf(
|
||||||
|
'UPDATE `%s` SET `%s` = CASE `%s`%s END WHERE `%s` IN (%s)',
|
||||||
|
$table,
|
||||||
|
$column,
|
||||||
|
$column,
|
||||||
|
$cases,
|
||||||
|
$column,
|
||||||
|
implode(',', $chunk),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visszagörgetés a jelentésfájlból: minden érintett sor visszakapja a saját,
|
||||||
|
* eredeti producer_id-ját, a gyártó rekordok pedig az eredeti nevüket/státuszukat.
|
||||||
|
*/
|
||||||
|
public function rollback(array $report): array
|
||||||
|
{
|
||||||
|
$restored = array_fill_keys(array_keys(self::REFERENCING_TABLES), 0);
|
||||||
|
|
||||||
|
foreach (self::REFERENCING_TABLES as $table => $column) {
|
||||||
|
$rows = (array) ($report['restore'][$table] ?? []);
|
||||||
|
|
||||||
|
foreach (array_chunk($rows, self::UPDATE_CHUNK, true) as $chunk) {
|
||||||
|
$byValue = [];
|
||||||
|
foreach ($chunk as $rowId => $producerId) {
|
||||||
|
$byValue[(int) $producerId][] = (int) $rowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($byValue as $producerId => $rowIds) {
|
||||||
|
$restored[$table] += DB::table($table)
|
||||||
|
->whereIn('id', $rowIds)
|
||||||
|
->update([$column => $producerId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((array) ($report['restore']['producers'] ?? []) as $id => $producer) {
|
||||||
|
$producer = (array) $producer;
|
||||||
|
|
||||||
|
DB::table('producers')->where('id', (int) $id)->update([
|
||||||
|
'name' => $producer['name'],
|
||||||
|
'status' => $producer['status'],
|
||||||
|
'canSee' => $producer['canSee'],
|
||||||
|
'note' => $producer['note'],
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $restored;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -110,6 +110,21 @@ public function isWorkDay(Carbon $date): bool
|
|||||||
return !$date->isWeekend();
|
return !$date->isWeekend();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Megállapítja, hogy az adott nap hivatalos munkaszüneti nap-e.
|
||||||
|
*
|
||||||
|
* Az isWorkDay()-jel ellentétben a puszta naptári hétvégét NEM tekinti annak:
|
||||||
|
* csak a work_calendars táblában ünnepnapként rögzített dátumra ad true-t.
|
||||||
|
* A szállítási naptár ezt használja, hogy a heti sablon engedhessen hétvégi
|
||||||
|
* szállítást is (EV3-466).
|
||||||
|
*/
|
||||||
|
public function isHoliday(Carbon $date): bool
|
||||||
|
{
|
||||||
|
$record = WorkCalendar::where('date', $date->toDateString())->first();
|
||||||
|
|
||||||
|
return $record?->type === WorkDayType::Holiday;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kiszámítja a következő érvényes munkanapot.
|
* Kiszámítja a következő érvényes munkanapot.
|
||||||
*/
|
*/
|
||||||
|
|||||||
44
app/Support/NameNormalizer.php
Normal file
44
app/Support/NameNormalizer.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nevek egységesítése összehasonlításhoz.
|
||||||
|
*
|
||||||
|
* Egy helyen van definiálva, mert három független ponton kell UGYANAZT a szabályt
|
||||||
|
* alkalmazni, és ha ezek elcsúsznak egymástól, az duplikált törzsadatot szül:
|
||||||
|
*
|
||||||
|
* - a legacy árlista import gyártó-párosítása (`PriceListService`),
|
||||||
|
* - az új árlista feldolgozó gyártó- és termékcsoport-feloldása
|
||||||
|
* (`PricelistFileProcessService`),
|
||||||
|
* - a gyártó-összevonó karbantartás.
|
||||||
|
*
|
||||||
|
* Pontosan ennek a hiánya okozta, hogy a `products` mellé 70 duplikált gyártónév
|
||||||
|
* keletkezett: a legacy import nyers, karakterre pontos egyezést követelt, ezért a
|
||||||
|
* "Danone " nem találta meg a meglévő "Danone"-t, és felvett egy újat.
|
||||||
|
*/
|
||||||
|
class NameNormalizer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Kis/nagybetű-érzéketlen, a `/` és `_` szeparátorokat szóközzé alakító,
|
||||||
|
* a többszörös szóközöket összevonó, körülvágott alak.
|
||||||
|
*/
|
||||||
|
public static function normalize(?string $text): string
|
||||||
|
{
|
||||||
|
$text = mb_strtoupper(trim((string) $text), 'UTF-8');
|
||||||
|
$text = str_replace(['/', '_'], ' ', $text);
|
||||||
|
$text = preg_replace('/\s+/u', ' ', $text) ?? $text;
|
||||||
|
|
||||||
|
return trim($text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Egyeznek-e a nevek a fenti szabály szerint.
|
||||||
|
*/
|
||||||
|
public static function matches(?string $a, ?string $b): bool
|
||||||
|
{
|
||||||
|
$normalizedA = self::normalize($a);
|
||||||
|
|
||||||
|
return $normalizedA !== '' && $normalizedA === self::normalize($b);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\ProductUnitEnum;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const TABLES = ['products', 'order_archives_items'];
|
||||||
|
|
||||||
|
private const COLUMNS = ['sellerUnit', 'amountUnit'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `products`/`order_archives_items` sellerUnit és amountUnit valódi DB enum oszlopok
|
||||||
|
* (lásd 2022_06_16_160516_... és 2022_06_16_160959_...), ezért az új ProductUnitEnum::m
|
||||||
|
* case önmagában nem elég - az oszlopdefiníciókat is bővíteni kell (EV3-465, Bunzl miatt).
|
||||||
|
*
|
||||||
|
* A `productUnit` (Mértékegység) oszlopot szándékosan NEM bővítjük: a jegy csak a
|
||||||
|
* Legkisebb eladási egységet és a Mennyiségi egységet említi, azt más enum
|
||||||
|
* (BasicUnitEnum/PricelistUnitEnum) validálja importkor.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$enumStr = $this->enumDefinition();
|
||||||
|
|
||||||
|
foreach (self::TABLES as $table) {
|
||||||
|
foreach (self::COLUMNS as $column) {
|
||||||
|
DB::statement("ALTER TABLE `{$table}` MODIFY COLUMN `{$column}` ENUM({$enumStr}) DEFAULT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visszagörgetéskor az 'm' értékű sorokat előbb NULL-ra állítjuk, különben a szűkített
|
||||||
|
* enum miatt az ALTER hibára futna (vagy némán ürítené a mezőt).
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$enumStr = $this->enumDefinition(withoutMeter: true);
|
||||||
|
|
||||||
|
foreach (self::TABLES as $table) {
|
||||||
|
foreach (self::COLUMNS as $column) {
|
||||||
|
DB::table($table)->where($column, 'm')->update([$column => null]);
|
||||||
|
DB::statement("ALTER TABLE `{$table}` MODIFY COLUMN `{$column}` ENUM({$enumStr}) DEFAULT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function enumDefinition(bool $withoutMeter = false): string
|
||||||
|
{
|
||||||
|
$keys = ProductUnitEnum::getKeys();
|
||||||
|
|
||||||
|
if ($withoutMeter) {
|
||||||
|
$keys = array_filter($keys, fn ($key) => $key !== ProductUnitEnum::m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(', ', array_map(fn ($key) => "'{$key}'", $keys));
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -63,9 +63,9 @@ CREATE TABLE `audits` (
|
|||||||
`event` varchar(255) NOT NULL,
|
`event` varchar(255) NOT NULL,
|
||||||
`auditable_type` varchar(255) NOT NULL,
|
`auditable_type` varchar(255) NOT NULL,
|
||||||
`auditable_id` bigint(20) unsigned NOT NULL,
|
`auditable_id` bigint(20) unsigned NOT NULL,
|
||||||
`old_values` text DEFAULT NULL,
|
`old_values` mediumtext DEFAULT NULL,
|
||||||
`new_values` text DEFAULT NULL,
|
`new_values` mediumtext DEFAULT NULL,
|
||||||
`url` text DEFAULT NULL,
|
`url` mediumtext DEFAULT NULL,
|
||||||
`ip_address` varchar(45) DEFAULT NULL,
|
`ip_address` varchar(45) DEFAULT NULL,
|
||||||
`user_agent` varchar(1023) DEFAULT NULL,
|
`user_agent` varchar(1023) DEFAULT NULL,
|
||||||
`tags` varchar(255) DEFAULT NULL,
|
`tags` varchar(255) DEFAULT NULL,
|
||||||
@ -86,6 +86,17 @@ CREATE TABLE `cache` (
|
|||||||
UNIQUE KEY `cache_key_unique` (`key`)
|
UNIQUE KEY `cache_key_unique` (`key`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
DROP TABLE IF EXISTS `cache_locks`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `cache_locks` (
|
||||||
|
`key` varchar(255) NOT NULL,
|
||||||
|
`owner` varchar(255) NOT NULL,
|
||||||
|
`expiration` int(11) NOT NULL,
|
||||||
|
PRIMARY KEY (`key`),
|
||||||
|
KEY `cache_locks_expiration_index` (`expiration`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
DROP TABLE IF EXISTS `contacts`;
|
DROP TABLE IF EXISTS `contacts`;
|
||||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
/*!40101 SET character_set_client = utf8mb4 */;
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
@ -95,7 +106,7 @@ CREATE TABLE `contacts` (
|
|||||||
`phone` varchar(255) DEFAULT NULL,
|
`phone` varchar(255) DEFAULT NULL,
|
||||||
`email` varchar(255) DEFAULT NULL,
|
`email` varchar(255) DEFAULT NULL,
|
||||||
`type` set('orderMail','contactMember','customerService') DEFAULT NULL,
|
`type` set('orderMail','contactMember','customerService') DEFAULT NULL,
|
||||||
`note` text DEFAULT NULL,
|
`note` mediumtext DEFAULT NULL,
|
||||||
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
||||||
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
`contactable_id` int(10) unsigned DEFAULT NULL,
|
`contactable_id` int(10) unsigned DEFAULT NULL,
|
||||||
@ -194,8 +205,8 @@ DROP TABLE IF EXISTS `failed_jobs`;
|
|||||||
CREATE TABLE `failed_jobs` (
|
CREATE TABLE `failed_jobs` (
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`uuid` varchar(255) NOT NULL,
|
`uuid` varchar(255) NOT NULL,
|
||||||
`connection` text NOT NULL,
|
`connection` mediumtext NOT NULL,
|
||||||
`queue` text NOT NULL,
|
`queue` mediumtext NOT NULL,
|
||||||
`payload` longtext NOT NULL,
|
`payload` longtext NOT NULL,
|
||||||
`exception` longtext NOT NULL,
|
`exception` longtext NOT NULL,
|
||||||
`failed_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
`failed_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
@ -257,6 +268,38 @@ CREATE TABLE `features` (
|
|||||||
UNIQUE KEY `features_name_scope_unique` (`name`,`scope`)
|
UNIQUE KEY `features_name_scope_unique` (`name`,`scope`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
DROP TABLE IF EXISTS `job_batches`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `job_batches` (
|
||||||
|
`id` varchar(255) NOT NULL,
|
||||||
|
`name` varchar(255) NOT NULL,
|
||||||
|
`total_jobs` int(11) NOT NULL,
|
||||||
|
`pending_jobs` int(11) NOT NULL,
|
||||||
|
`failed_jobs` int(11) NOT NULL,
|
||||||
|
`failed_job_ids` longtext NOT NULL,
|
||||||
|
`options` mediumtext DEFAULT NULL,
|
||||||
|
`cancelled_at` int(11) DEFAULT NULL,
|
||||||
|
`created_at` int(11) NOT NULL,
|
||||||
|
`finished_at` int(11) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
DROP TABLE IF EXISTS `jobs`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `jobs` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`queue` varchar(255) NOT NULL,
|
||||||
|
`payload` longtext NOT NULL,
|
||||||
|
`attempts` tinyint(3) unsigned NOT NULL,
|
||||||
|
`reserved_at` int(10) unsigned DEFAULT NULL,
|
||||||
|
`available_at` int(10) unsigned NOT NULL,
|
||||||
|
`created_at` int(10) unsigned NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `jobs_queue_index` (`queue`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
DROP TABLE IF EXISTS `migrations`;
|
DROP TABLE IF EXISTS `migrations`;
|
||||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
/*!40101 SET character_set_client = utf8mb4 */;
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
@ -275,7 +318,7 @@ CREATE TABLE `order_archives` (
|
|||||||
`profit_center_id` int(10) unsigned DEFAULT NULL,
|
`profit_center_id` int(10) unsigned DEFAULT NULL,
|
||||||
`supplier_id` int(10) unsigned NOT NULL,
|
`supplier_id` int(10) unsigned NOT NULL,
|
||||||
`contactName` varchar(255) DEFAULT NULL,
|
`contactName` varchar(255) DEFAULT NULL,
|
||||||
`note` text DEFAULT NULL,
|
`note` mediumtext DEFAULT NULL,
|
||||||
`orderType` enum('daily','weekly') NOT NULL DEFAULT 'daily',
|
`orderType` enum('daily','weekly') NOT NULL DEFAULT 'daily',
|
||||||
`parent_id` bigint(20) DEFAULT NULL,
|
`parent_id` bigint(20) DEFAULT NULL,
|
||||||
`orderFlag` set('modifier','modified','storno') DEFAULT NULL,
|
`orderFlag` set('modifier','modified','storno') DEFAULT NULL,
|
||||||
@ -294,7 +337,7 @@ CREATE TABLE `order_archives` (
|
|||||||
`confirmed` datetime DEFAULT NULL,
|
`confirmed` datetime DEFAULT NULL,
|
||||||
`supplierOrderNumber` int(10) unsigned DEFAULT NULL,
|
`supplierOrderNumber` int(10) unsigned DEFAULT NULL,
|
||||||
`humanId` varchar(255) DEFAULT NULL,
|
`humanId` varchar(255) DEFAULT NULL,
|
||||||
`sumPrice` float NOT NULL,
|
`sumPrice` double DEFAULT NULL,
|
||||||
`APILastGetting` datetime DEFAULT NULL,
|
`APILastGetting` datetime DEFAULT NULL,
|
||||||
`APICallBackId` char(36) DEFAULT NULL,
|
`APICallBackId` char(36) DEFAULT NULL,
|
||||||
`APIConfirmed` datetime DEFAULT NULL,
|
`APIConfirmed` datetime DEFAULT NULL,
|
||||||
@ -326,11 +369,11 @@ CREATE TABLE `order_archives_items` (
|
|||||||
`supplier_id` int(11) DEFAULT NULL,
|
`supplier_id` int(11) DEFAULT NULL,
|
||||||
`producer_id` int(11) DEFAULT NULL,
|
`producer_id` int(11) DEFAULT NULL,
|
||||||
`packing` int(11) DEFAULT NULL,
|
`packing` int(11) DEFAULT NULL,
|
||||||
`unitValue` double NOT NULL,
|
`unitValue` double DEFAULT NULL,
|
||||||
`productUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','kg','l','db') DEFAULT NULL,
|
`productUnit` enum('g','kg','ml','l','db','tálca','csom','dob','kart','rúd','pár','zsák') DEFAULT NULL,
|
||||||
`sellerUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','kg','l','db') DEFAULT NULL,
|
`sellerUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','m','kg','l','db') DEFAULT NULL,
|
||||||
`unitMultiplier` double DEFAULT NULL,
|
`unitMultiplier` double DEFAULT NULL,
|
||||||
`amountUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','kg','l','db') DEFAULT NULL,
|
`amountUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','m','kg','l','db') DEFAULT NULL,
|
||||||
`vat` double DEFAULT NULL,
|
`vat` double DEFAULT NULL,
|
||||||
`supplierProductNumber` varchar(255) DEFAULT NULL,
|
`supplierProductNumber` varchar(255) DEFAULT NULL,
|
||||||
`product_group_id` int(11) DEFAULT NULL,
|
`product_group_id` int(11) DEFAULT NULL,
|
||||||
@ -339,7 +382,7 @@ CREATE TABLE `order_archives_items` (
|
|||||||
`HooreycaMultiplier` double DEFAULT NULL,
|
`HooreycaMultiplier` double DEFAULT NULL,
|
||||||
`buyerProductName` varchar(255) DEFAULT NULL,
|
`buyerProductName` varchar(255) DEFAULT NULL,
|
||||||
`buyerProductNumber` varchar(255) DEFAULT NULL,
|
`buyerProductNumber` varchar(255) DEFAULT NULL,
|
||||||
`note` text DEFAULT NULL,
|
`note` varchar(255) DEFAULT NULL,
|
||||||
`type` enum('F','N','X') DEFAULT NULL,
|
`type` enum('F','N','X') DEFAULT NULL,
|
||||||
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
||||||
`stock_status` varchar(255) NOT NULL DEFAULT 'in_stock',
|
`stock_status` varchar(255) NOT NULL DEFAULT 'in_stock',
|
||||||
@ -355,9 +398,9 @@ CREATE TABLE `order_archives_items` (
|
|||||||
`producer_name` varchar(255) DEFAULT NULL,
|
`producer_name` varchar(255) DEFAULT NULL,
|
||||||
`product_group_name` varchar(255) DEFAULT NULL,
|
`product_group_name` varchar(255) DEFAULT NULL,
|
||||||
`product_group_path` varchar(255) DEFAULT NULL,
|
`product_group_path` varchar(255) DEFAULT NULL,
|
||||||
`quantity` double DEFAULT NULL,
|
`quantity` double(8,2) DEFAULT NULL,
|
||||||
`price` double DEFAULT NULL,
|
`price` double(8,2) DEFAULT NULL,
|
||||||
`amount` double DEFAULT NULL,
|
`amount` double(10,2) DEFAULT NULL,
|
||||||
`comment` text DEFAULT NULL,
|
`comment` text DEFAULT NULL,
|
||||||
`krel` tinyint(1) NOT NULL DEFAULT 0,
|
`krel` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`specialOffer` tinyint(1) NOT NULL DEFAULT 0,
|
`specialOffer` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
@ -370,14 +413,16 @@ DROP TABLE IF EXISTS `order_numbers`;
|
|||||||
/*!40101 SET character_set_client = utf8mb4 */;
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
CREATE TABLE `order_numbers` (
|
CREATE TABLE `order_numbers` (
|
||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`supplier_order_number` int(10) unsigned NOT NULL,
|
||||||
`year` tinyint(3) unsigned NOT NULL,
|
`year` tinyint(3) unsigned NOT NULL,
|
||||||
`supplier_id` int(10) unsigned NOT NULL,
|
`supplier_id` int(10) unsigned NOT NULL,
|
||||||
`order_id` int(10) unsigned DEFAULT NULL,
|
`order_id` int(10) unsigned DEFAULT NULL,
|
||||||
`created_at` timestamp NULL DEFAULT NULL,
|
`created_at` timestamp NULL DEFAULT NULL,
|
||||||
`updated_at` timestamp NULL DEFAULT NULL,
|
`updated_at` timestamp NULL DEFAULT NULL,
|
||||||
PRIMARY KEY (`supplier_id`,`year`,`id`),
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `unique_supplier_order_number_year_supplier_id` (`supplier_order_number`,`year`,`supplier_id`),
|
||||||
UNIQUE KEY `order_numbers_order_id_unique` (`order_id`)
|
UNIQUE KEY `order_numbers_order_id_unique` (`order_id`)
|
||||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
DROP TABLE IF EXISTS `orders`;
|
DROP TABLE IF EXISTS `orders`;
|
||||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
@ -387,7 +432,7 @@ CREATE TABLE `orders` (
|
|||||||
`profit_center_id` int(10) unsigned DEFAULT NULL,
|
`profit_center_id` int(10) unsigned DEFAULT NULL,
|
||||||
`supplier_id` int(10) unsigned NOT NULL,
|
`supplier_id` int(10) unsigned NOT NULL,
|
||||||
`contactName` varchar(255) DEFAULT NULL,
|
`contactName` varchar(255) DEFAULT NULL,
|
||||||
`note` text DEFAULT NULL,
|
`note` mediumtext DEFAULT NULL,
|
||||||
`orderType` enum('daily','weekly') NOT NULL DEFAULT 'daily',
|
`orderType` enum('daily','weekly') NOT NULL DEFAULT 'daily',
|
||||||
`parent_id` bigint(20) DEFAULT NULL,
|
`parent_id` bigint(20) DEFAULT NULL,
|
||||||
`orderFlag` set('modifier','modified','storno') DEFAULT NULL,
|
`orderFlag` set('modifier','modified','storno') DEFAULT NULL,
|
||||||
@ -406,7 +451,7 @@ CREATE TABLE `orders` (
|
|||||||
`confirmed` datetime DEFAULT NULL,
|
`confirmed` datetime DEFAULT NULL,
|
||||||
`supplierOrderNumber` int(10) unsigned DEFAULT NULL,
|
`supplierOrderNumber` int(10) unsigned DEFAULT NULL,
|
||||||
`humanId` varchar(255) DEFAULT NULL,
|
`humanId` varchar(255) DEFAULT NULL,
|
||||||
`sumPrice` float NOT NULL,
|
`sumPrice` double DEFAULT NULL,
|
||||||
`created_at` timestamp NULL DEFAULT NULL,
|
`created_at` timestamp NULL DEFAULT NULL,
|
||||||
`updated_at` timestamp NULL DEFAULT NULL,
|
`updated_at` timestamp NULL DEFAULT NULL,
|
||||||
`deleted_at` timestamp NULL DEFAULT NULL,
|
`deleted_at` timestamp NULL DEFAULT NULL,
|
||||||
@ -514,8 +559,8 @@ DROP TABLE IF EXISTS `price_lists`;
|
|||||||
CREATE TABLE `price_lists` (
|
CREATE TABLE `price_lists` (
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`supplier_id` int(11) NOT NULL,
|
`supplier_id` int(11) NOT NULL,
|
||||||
`note` text DEFAULT NULL,
|
|
||||||
`available` date NOT NULL,
|
`available` date NOT NULL,
|
||||||
|
`note` varchar(255) DEFAULT NULL,
|
||||||
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
||||||
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
`created_at` timestamp NULL DEFAULT NULL,
|
`created_at` timestamp NULL DEFAULT NULL,
|
||||||
@ -595,7 +640,7 @@ DROP TABLE IF EXISTS `producers`;
|
|||||||
CREATE TABLE `producers` (
|
CREATE TABLE `producers` (
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`name` varchar(255) DEFAULT NULL,
|
`name` varchar(255) DEFAULT NULL,
|
||||||
`note` text DEFAULT NULL,
|
`note` mediumtext DEFAULT NULL,
|
||||||
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
||||||
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
`created_at` timestamp NULL DEFAULT NULL,
|
`created_at` timestamp NULL DEFAULT NULL,
|
||||||
@ -637,11 +682,11 @@ CREATE TABLE `products` (
|
|||||||
`supplier_id` int(11) DEFAULT NULL,
|
`supplier_id` int(11) DEFAULT NULL,
|
||||||
`producer_id` int(11) DEFAULT NULL,
|
`producer_id` int(11) DEFAULT NULL,
|
||||||
`packing` int(11) DEFAULT NULL,
|
`packing` int(11) DEFAULT NULL,
|
||||||
`unitValue` double NOT NULL,
|
`unitValue` double DEFAULT NULL,
|
||||||
`productUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','kg','l','db') DEFAULT NULL,
|
`productUnit` enum('g','kg','ml','l','db','tálca','csom','dob','kart','rúd','pár','zsák') DEFAULT NULL,
|
||||||
`sellerUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','kg','l','db') DEFAULT NULL,
|
`sellerUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','m','kg','l','db') DEFAULT NULL,
|
||||||
`unitMultiplier` double DEFAULT NULL,
|
`unitMultiplier` double DEFAULT NULL,
|
||||||
`amountUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','kg','l','db') DEFAULT NULL,
|
`amountUnit` enum('g','ml','tálca','csom','dob','kart','rúd','pár','zsák','m','kg','l','db') DEFAULT NULL,
|
||||||
`vat` double DEFAULT NULL,
|
`vat` double DEFAULT NULL,
|
||||||
`supplierProductNumber` varchar(255) DEFAULT NULL,
|
`supplierProductNumber` varchar(255) DEFAULT NULL,
|
||||||
`product_group_id` int(11) DEFAULT NULL,
|
`product_group_id` int(11) DEFAULT NULL,
|
||||||
@ -650,7 +695,7 @@ CREATE TABLE `products` (
|
|||||||
`HooreycaMultiplier` double DEFAULT NULL,
|
`HooreycaMultiplier` double DEFAULT NULL,
|
||||||
`buyerProductName` varchar(255) DEFAULT NULL,
|
`buyerProductName` varchar(255) DEFAULT NULL,
|
||||||
`buyerProductNumber` varchar(255) DEFAULT NULL,
|
`buyerProductNumber` varchar(255) DEFAULT NULL,
|
||||||
`note` text NOT NULL,
|
`note` text DEFAULT NULL,
|
||||||
`type` enum('F','N','X') DEFAULT NULL,
|
`type` enum('F','N','X') DEFAULT NULL,
|
||||||
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
||||||
`stock_status` varchar(255) NOT NULL DEFAULT 'in_stock',
|
`stock_status` varchar(255) NOT NULL DEFAULT 'in_stock',
|
||||||
@ -699,8 +744,8 @@ CREATE TABLE `profit_center_supplier` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `profit_center_supplier_profit_center_id_foreign` (`profit_center_id`),
|
KEY `profit_center_supplier_profit_center_id_foreign` (`profit_center_id`),
|
||||||
KEY `profit_center_supplier_supplier_id_foreign` (`supplier_id`),
|
KEY `profit_center_supplier_supplier_id_foreign` (`supplier_id`),
|
||||||
CONSTRAINT `profit_center_supplier_profit_center_id_foreign` FOREIGN KEY (`profit_center_id`) REFERENCES `profit_centers` (`id`) ON DELETE NO ACTION,
|
CONSTRAINT `profit_center_supplier_profit_center_id_foreign` FOREIGN KEY (`profit_center_id`) REFERENCES `profit_centers` (`id`),
|
||||||
CONSTRAINT `profit_center_supplier_supplier_id_foreign` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE NO ACTION
|
CONSTRAINT `profit_center_supplier_supplier_id_foreign` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
DROP TABLE IF EXISTS `profit_center_supplier_codes`;
|
DROP TABLE IF EXISTS `profit_center_supplier_codes`;
|
||||||
@ -757,8 +802,8 @@ CREATE TABLE `profit_center_users` (
|
|||||||
`updated_at` timestamp NULL DEFAULT NULL,
|
`updated_at` timestamp NULL DEFAULT NULL,
|
||||||
KEY `profit_center_users_profit_center_id_foreign` (`profit_center_id`),
|
KEY `profit_center_users_profit_center_id_foreign` (`profit_center_id`),
|
||||||
KEY `profit_center_users_user_id_foreign` (`user_id`),
|
KEY `profit_center_users_user_id_foreign` (`user_id`),
|
||||||
CONSTRAINT `profit_center_users_profit_center_id_foreign` FOREIGN KEY (`profit_center_id`) REFERENCES `profit_centers` (`id`) ON DELETE NO ACTION,
|
CONSTRAINT `profit_center_users_profit_center_id_foreign` FOREIGN KEY (`profit_center_id`) REFERENCES `profit_centers` (`id`),
|
||||||
CONSTRAINT `profit_center_users_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE NO ACTION
|
CONSTRAINT `profit_center_users_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
DROP TABLE IF EXISTS `profit_centers`;
|
DROP TABLE IF EXISTS `profit_centers`;
|
||||||
@ -777,8 +822,9 @@ CREATE TABLE `profit_centers` (
|
|||||||
`mobil` varchar(255) DEFAULT NULL,
|
`mobil` varchar(255) DEFAULT NULL,
|
||||||
`addressEqual` varchar(255) NOT NULL DEFAULT '1',
|
`addressEqual` varchar(255) NOT NULL DEFAULT '1',
|
||||||
`multiAddress` varchar(255) NOT NULL DEFAULT '0',
|
`multiAddress` varchar(255) NOT NULL DEFAULT '0',
|
||||||
`note` text NOT NULL,
|
`note` text DEFAULT NULL,
|
||||||
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
|
`hooreycaDataActive` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
||||||
`created_at` timestamp NULL DEFAULT NULL,
|
`created_at` timestamp NULL DEFAULT NULL,
|
||||||
`updated_at` timestamp NULL DEFAULT NULL,
|
`updated_at` timestamp NULL DEFAULT NULL,
|
||||||
@ -861,8 +907,8 @@ CREATE TABLE `supplier_notifiers` (
|
|||||||
KEY `supplier_notifiers_supplier_id_foreign` (`supplier_id`),
|
KEY `supplier_notifiers_supplier_id_foreign` (`supplier_id`),
|
||||||
KEY `supplier_notifiers_need_send_index` (`need_send`),
|
KEY `supplier_notifiers_need_send_index` (`need_send`),
|
||||||
KEY `supplier_notifiers_sent_index` (`sent`),
|
KEY `supplier_notifiers_sent_index` (`sent`),
|
||||||
CONSTRAINT `supplier_notifiers_order_archive_id_foreign` FOREIGN KEY (`order_archive_id`) REFERENCES `order_archives` (`id`) ON DELETE NO ACTION,
|
CONSTRAINT `supplier_notifiers_order_archive_id_foreign` FOREIGN KEY (`order_archive_id`) REFERENCES `order_archives` (`id`),
|
||||||
CONSTRAINT `supplier_notifiers_supplier_id_foreign` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE NO ACTION
|
CONSTRAINT `supplier_notifiers_supplier_id_foreign` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
DROP TABLE IF EXISTS `supplier_product_group`;
|
DROP TABLE IF EXISTS `supplier_product_group`;
|
||||||
@ -894,8 +940,8 @@ CREATE TABLE `supplier_services` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `supplier_services_supplier_id_foreign` (`supplier_id`),
|
KEY `supplier_services_supplier_id_foreign` (`supplier_id`),
|
||||||
KEY `supplier_services_service_id_foreign` (`service_id`),
|
KEY `supplier_services_service_id_foreign` (`service_id`),
|
||||||
CONSTRAINT `supplier_services_service_id_foreign` FOREIGN KEY (`service_id`) REFERENCES `services` (`id`) ON DELETE NO ACTION,
|
CONSTRAINT `supplier_services_service_id_foreign` FOREIGN KEY (`service_id`) REFERENCES `services` (`id`),
|
||||||
CONSTRAINT `supplier_services_supplier_id_foreign` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE NO ACTION
|
CONSTRAINT `supplier_services_supplier_id_foreign` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
DROP TABLE IF EXISTS `suppliers`;
|
DROP TABLE IF EXISTS `suppliers`;
|
||||||
@ -913,7 +959,7 @@ CREATE TABLE `suppliers` (
|
|||||||
`phone` varchar(255) DEFAULT NULL,
|
`phone` varchar(255) DEFAULT NULL,
|
||||||
`fax` varchar(255) DEFAULT NULL,
|
`fax` varchar(255) DEFAULT NULL,
|
||||||
`customerServicePhone` varchar(255) DEFAULT NULL,
|
`customerServicePhone` varchar(255) DEFAULT NULL,
|
||||||
`note` text NOT NULL,
|
`note` text DEFAULT NULL,
|
||||||
`emailAttachmentType` enum('pdf','excel','none') NOT NULL DEFAULT 'none',
|
`emailAttachmentType` enum('pdf','excel','none') NOT NULL DEFAULT 'none',
|
||||||
`logoFile` varchar(255) DEFAULT NULL,
|
`logoFile` varchar(255) DEFAULT NULL,
|
||||||
`openHours` text DEFAULT NULL,
|
`openHours` text DEFAULT NULL,
|
||||||
@ -925,6 +971,7 @@ CREATE TABLE `suppliers` (
|
|||||||
`orderCutOffTime` tinyint(4) NOT NULL DEFAULT 12,
|
`orderCutOffTime` tinyint(4) NOT NULL DEFAULT 12,
|
||||||
`deliveryLeadTime` smallint(5) unsigned NOT NULL DEFAULT 48,
|
`deliveryLeadTime` smallint(5) unsigned NOT NULL DEFAULT 48,
|
||||||
`hasCustomerCode` tinyint(1) NOT NULL DEFAULT 0,
|
`hasCustomerCode` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`hooreycaDataActive` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
||||||
`created_at` timestamp NULL DEFAULT NULL,
|
`created_at` timestamp NULL DEFAULT NULL,
|
||||||
`updated_at` timestamp NULL DEFAULT NULL,
|
`updated_at` timestamp NULL DEFAULT NULL,
|
||||||
@ -945,7 +992,7 @@ CREATE TABLE `system_parameters` (
|
|||||||
`data_type` varchar(255) NOT NULL,
|
`data_type` varchar(255) NOT NULL,
|
||||||
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
`canSee` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
`note` varchar(255) DEFAULT NULL,
|
`note` varchar(255) DEFAULT NULL,
|
||||||
`stored_data` text NOT NULL,
|
`stored_data` text DEFAULT NULL,
|
||||||
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
`status` enum('draft','active','archive','deleted') NOT NULL DEFAULT 'draft',
|
||||||
`created_at` timestamp NULL DEFAULT NULL,
|
`created_at` timestamp NULL DEFAULT NULL,
|
||||||
`updated_at` timestamp NULL DEFAULT NULL,
|
`updated_at` timestamp NULL DEFAULT NULL,
|
||||||
@ -984,6 +1031,9 @@ CREATE TABLE `users` (
|
|||||||
`remember_token` varchar(100) DEFAULT NULL,
|
`remember_token` varchar(100) DEFAULT NULL,
|
||||||
`created_at` timestamp NULL DEFAULT NULL,
|
`created_at` timestamp NULL DEFAULT NULL,
|
||||||
`updated_at` timestamp NULL DEFAULT NULL,
|
`updated_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`created_by` int(10) unsigned DEFAULT NULL,
|
||||||
|
`updated_by` int(10) unsigned DEFAULT NULL,
|
||||||
|
`deleted_by` int(10) unsigned DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `users_email_unique` (`email`),
|
UNIQUE KEY `users_email_unique` (`email`),
|
||||||
KEY `users_supplier_id_foreign` (`supplier_id`),
|
KEY `users_supplier_id_foreign` (`supplier_id`),
|
||||||
@ -1019,107 +1069,115 @@ CREATE TABLE `work_calendars` (
|
|||||||
|
|
||||||
/*M!999999\- enable the sandbox mode */
|
/*M!999999\- enable the sandbox mode */
|
||||||
set autocommit=0;
|
set autocommit=0;
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'2014_10_12_000000_create_users_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (718,'2014_10_12_000000_create_users_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'2014_10_12_100000_create_password_reset_tokens_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (719,'2014_10_12_100000_create_password_resets_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'2014_10_12_100000_create_password_resets_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (720,'2019_08_19_000000_create_failed_jobs_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2019_08_19_000000_create_failed_jobs_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (721,'2021_04_06_100750_create_products_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2019_12_14_000001_create_personal_access_tokens_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (722,'2021_04_06_101015_create_product_groups_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2021_04_06_100750_create_products_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (723,'2021_05_05_141334_create_orders_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2021_04_06_101015_create_product_groups_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (724,'2021_05_17_092917_create_suppliers_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2021_05_05_141334_create_orders_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (725,'2021_05_25_165917_create_contacts_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2021_05_17_092917_create_suppliers_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (726,'2021_06_23_100149_create_price_lists_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2021_05_25_165917_create_contacts_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (727,'2021_07_19_065008_create_producers_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2021_06_23_100149_create_price_lists_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (728,'2021_07_29_121054_create_price_list_price',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2021_07_19_065008_create_producers_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (729,'2021_08_02_075611_create_audits_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2021_07_29_121054_create_price_list_price',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (730,'2021_10_15_180023_create_attachments_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2021_08_02_075611_create_audits_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (731,'2021_10_22_121054_create_supplier_product_group',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2021_10_15_180023_create_attachments_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (732,'2021_10_29_045551_create_order_numbers_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2021_10_22_121054_create_supplier_product_group',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (733,'2021_10_31_085743_add_confirmed_time_to_orders',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2021_10_29_045551_create_order_numbers_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (734,'2021_11_08_175100_add_parameterFields_to_orders',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2021_10_31_085743_add_confirmed_time_to_orders',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (735,'2021_11_10_070207_create_profit_centers_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2021_11_08_175100_add_parameterFields_to_orders',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (736,'2021_11_22_161100_add_parameterFields_to_ProfitCenter',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2021_11_10_070207_create_profit_centers_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (737,'2021_11_26_053327_create_addresses_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2021_11_22_161100_add_parameterFields_to_ProfitCenter',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (738,'2021_11_28_094809_laratrust_setup_tables',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2021_11_26_053327_create_addresses_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (739,'2021_11_30_181255_create_profit_center_users',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2021_11_28_094809_laratrust_setup_tables',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (740,'2021_12_06_044820_create_order_archives_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2021_11_30_181255_create_profit_center_users',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (741,'2021_12_06_051438_create_order_archives_items_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2021_12_06_044820_create_order_archives_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (742,'2021_12_09_071814_create_profit_center_suppliers_table',2);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2021_12_06_051438_create_order_archives_items_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (743,'2021_12_21_085950_change_note_to_text_table',3);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (27,'2021_12_09_071814_create_profit_center_suppliers_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (744,'2021_12_21_090255_change_note_to_text_products_table',3);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (28,'2021_12_21_085950_change_note_to_text_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (745,'2022_01_26_144810_alter_table_products_change_unit_value',4);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (29,'2021_12_21_090255_change_note_to_text_products_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (746,'2022_01_26_145301_alter_table_order_archives_items_change_unit_value',4);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (30,'2022_01_26_144810_alter_table_products_change_unit_value',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (747,'2022_03_06_183718_add_product_comment_to_order_table',5);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (31,'2022_01_26_145301_alter_table_order_archives_items_change_unit_value',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (748,'2022_03_06_183948_add_product_comment_to_order_archive_table',5);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (32,'2022_03_06_183718_add_product_comment_to_order_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (749,'2022_03_06_184132_add_product_comment_to_order_archives_items_table',5);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (33,'2022_03_06_183948_add_product_comment_to_order_archive_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (750,'2022_03_23_054908_create_profit_center_favorites_table',6);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (34,'2022_03_06_184132_add_product_comment_to_order_archives_items_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (751,'2022_03_31_060015_change_note_to_text_supplier_table',7);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (35,'2022_03_23_054908_create_profit_center_favorites_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (752,'2022_03_31_084931_add_note_to_price_list_table',8);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (36,'2022_03_31_060015_change_note_to_text_supplier_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (753,'2022_04_06_185438_change_order_sum_to_bigger_float_order_table',9);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (37,'2022_03_31_084931_add_note_to_price_list_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (754,'2022_04_06_185733_change_order_sum_to_bigger_float_order_archive_table',9);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (38,'2022_04_06_185438_change_order_sum_to_bigger_float_order_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (755,'2022_05_10_095803_change_order_add_delivery_address_name_order_table',10);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (39,'2022_04_06_185733_change_order_sum_to_bigger_float_order_archive_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (756,'2022_05_10_100303_change_order_add_delivery_address_name_order_archive_table',10);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (40,'2022_05_10_095803_change_order_add_delivery_address_name_order_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (757,'2022_05_26_095237_create_system_parameters_table',11);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (41,'2022_05_10_100303_change_order_add_delivery_address_name_order_archive_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (758,'2022_06_16_160516_change_product_seller_uint_to_new_items_product_table',11);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (42,'2022_05_26_095237_create_system_parameters_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (759,'2022_06_16_160959_change_product_seller_uint_to_new_items_order_archive_items_table',11);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (43,'2022_06_16_160516_change_product_seller_uint_to_new_items_product_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (760,'2022_10_06_100212_change_product_seller_uint_to_new_items_product_table',12);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (44,'2022_06_16_160959_change_product_seller_uint_to_new_items_order_archive_items_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (761,'2022_10_06_100434_change_product_seller_uint_to_new_items_order_archive_items_table',12);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (45,'2023_01_28_105430_change_order_add_custom_notification_email_order_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (762,'2019_12_14_000001_create_personal_access_tokens_table',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (46,'2023_01_28_111049_change_order_archive_add_custom_notification_email_order_archive_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (763,'2023_01_28_105430_change_order_add_custom_notification_email_order_table',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (47,'2023_02_17_154501_add_api_fields_to_order_archives_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (764,'2023_01_28_111049_change_order_archive_add_custom_notification_email_order_archive_table',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (48,'2023_03_20_094108_alter_table_orders_change_order_type',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (765,'2023_02_17_154501_add_api_fields_to_order_archives_table',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (49,'2023_03_24_075952_create_table_tests',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (766,'2023_03_20_094108_alter_table_orders_change_order_type',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (50,'2023_03_27_082804_add_parent_id_fields_to_order_archives_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (767,'2023_03_24_075952_create_table_tests',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (51,'2023_03_27_083102_add_parent_id_fields_to_orders_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (768,'2023_03_27_082804_add_parent_id_fields_to_order_archives_table',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (52,'2023_04_24_195258_add_modier_fields_to_orders_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (769,'2023_03_27_083102_add_parent_id_fields_to_orders_table',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (53,'2023_04_25_063313_add_modifier_fields_to_order_archives_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (770,'2023_04_24_195258_add_modier_fields_to_orders_table',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (54,'2023_09_03_062731_add_softdelete_fields_to_system_parameters_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (771,'2023_04_25_063313_add_modifier_fields_to_order_archives_table',13);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (55,'2023_09_07_071418_add_krel_fields_to_products_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (772,'2023_09_03_062731_add_softdelete_fields_to_system_parameters_table',14);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (56,'2023_09_07_071717_add_krel_to_order_archives_items_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (773,'2023_09_07_071418_add_krel_fields_to_products_table',14);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (57,'2023_09_10_090802_add_fields_p_m75_to_suppliers_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (774,'2023_09_07_071717_add_krel_to_order_archives_items_table',14);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (58,'2023_09_14_070521_add_fields_p_m75_order_see_to_suppliers_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (775,'2023_09_10_090802_add_fields_p_m75_to_suppliers_table',14);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (59,'2023_09_21_053850_create_supplier_notifier_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (776,'2023_09_14_070521_add_fields_p_m75_order_see_to_suppliers_table',14);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (60,'2023_10_04_125603_add_fields_p_m139_hooreyca_to_product_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (777,'2023_09_21_053850_create_supplier_notifier_table',14);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (61,'2023_10_04_152249_add_fields_p_m139_hooreyca_to_order_archives_item_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (778,'2023_10_04_125603_add_fields_p_m139_hooreyca_to_product_table',15);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (62,'2023_10_15_113109_add_fields_p_m125attach_type_to_attachments_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (779,'2023_10_04_152249_add_fields_p_m139_hooreyca_to_order_archives_item_table',15);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (63,'2023_10_17_070426_add_fields_p_m153_type_to_contacts_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (780,'2023_10_15_113109_add_fields_p_m125attach_type_to_attachments_table',16);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (64,'2024_07_31_161548_alter_table_suppliers__p_m179_change_null_fields',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (781,'2023_10_17_070426_add_fields_p_m153_type_to_contacts_table',17);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (65,'2024_07_31_173139_create_table_pm179_services',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (782,'2024_07_31_161548_alter_table_suppliers__p_m179_change_null_fields',18);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (66,'2024_07_31_183829_create_table_supplier_services_pm179',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (783,'2024_07_31_173139_create_table_pm179_services',18);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (67,'2025_03_01_111538_add_special_offer_fields_to_products_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (784,'2024_07_31_183829_create_table_supplier_services_pm179',18);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (68,'2025_03_01_111538_add_special_offer_to_order_archives_items_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (785,'2025_03_01_111538_add_special_offer_fields_to_products_table',19);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (69,'2025_07_24_142415_create_sessions_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (786,'2025_03_01_111538_add_special_offer_to_order_archives_items_table',19);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (70,'2025_07_24_142624_create_cache_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (787,'2025_05_23_083138_add_hooreyca_data_active_fields_to_profit_centers_table',19);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (71,'2026_01_24_000000_add_expires_at_to_personal_access_tokens_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (788,'2025_05_23_083138_add_hooreyca_data_active_fields_to_suppliers_table',19);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (72,'2026_01_24_000000_rename_password_resets_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (800,'0001_01_01_000000_create_users_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (73,'2026_03_02_065700_create_pricelist_files_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (801,'0001_01_01_000001_create_cache_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (74,'2026_03_02_072000_add_fields_to_pricelist_files_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (802,'2014_10_12_100000_create_password_reset_tokens_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (75,'2026_03_02_073000_add_processing_fields_to_pricelist_files_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (804,'2025_07_24_142624_create_cache_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (76,'2026_03_02_073505_add_relations_to_pricelist_files_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (808,'0001_01_01_000002_create_jobs_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (77,'2026_03_02_091110_add_available_and_note_to_pricelist_files_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (809,'2025_07_24_142415_create_sessions_table',1);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (78,'2026_03_02_135934_add_workflow_steps_to_pricelist_files_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (812,'2026_03_02_065700_create_pricelist_files_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (79,'2026_03_06_175613_fix_pricelist_files_status_enum',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (815,'2026_03_02_072000_add_fields_to_pricelist_files_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (80,'2026_03_07_072716_add_file_meta_to_pricelist_files_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (818,'2026_03_02_073000_add_processing_fields_to_pricelist_files_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (81,'2026_03_08_222836_create_pricelist_file_lines_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (821,'2026_03_02_073505_add_relations_to_pricelist_files_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (82,'2026_03_08_223838_add_audit_fields_to_pricelist_file_lines_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (824,'2026_03_02_091110_add_available_and_note_to_pricelist_files_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (83,'2026_03_09_212047_add_relation_ids_to_pricelist_file_lines_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (827,'2026_03_02_135934_add_workflow_steps_to_pricelist_files_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (84,'2026_03_11_153652_update_enums_in_pricelist_tables',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (830,'2026_03_06_175613_fix_pricelist_files_status_enum',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (85,'2026_04_08_184130_create_work_calendars_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (833,'2026_03_07_072716_add_file_meta_to_pricelist_files_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (86,'2026_04_08_223732_create_delivery_schedules_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (836,'2026_03_08_222836_create_pricelist_file_lines_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (87,'2026_04_08_223733_create_profit_center_supplier_schedules_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (839,'2026_03_08_223838_add_audit_fields_to_pricelist_file_lines_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (88,'2026_04_08_223735_create_delivery_calendar_overrides_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (842,'2026_03_09_212047_add_relation_ids_to_pricelist_file_lines_table',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (89,'2026_04_20_162446_add_scope_to_delivery_calendar_overrides_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (845,'2026_03_11_153652_update_enums_in_pricelist_tables',20);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (90,'2026_04_20_171028_change_region_to_delivery_schedule_id_in_delivery_calendar_overrides',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (848,'2026_04_08_184130_create_work_calendars_table',21);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (91,'2026_04_24_184012_add_delivery_constraint_to_suppliers_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (851,'2026_04_08_223732_create_delivery_schedules_table',21);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (92,'2026_04_25_163803_add_supplier_id_to_users_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (854,'2026_04_08_223733_create_profit_center_supplier_schedules_table',21);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (93,'2026_04_25_164154_add_stock_status_to_products_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (857,'2026_04_08_223735_create_delivery_calendar_overrides_table',21);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (94,'2026_05_05_070449_add_stock_status_to_order_archives_items_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (860,'2026_04_20_162446_add_scope_to_delivery_calendar_overrides_table',21);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (95,'2026_07_20_171642_restore_missing_id_auto_increment',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (863,'2026_04_20_171028_change_region_to_delivery_schedule_id_in_delivery_calendar_overrides',21);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (96,'2026_08_08_152549_create_features_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (866,'2026_04_24_184012_add_delivery_constraint_to_suppliers_table',21);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (97,'2026_08_08_153000_create_feature_flags_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (869,'2026_04_25_163803_add_supplier_id_to_users_table',21);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (98,'2026_08_08_154500_create_feature_flag_overrides_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (872,'2026_04_25_164154_add_stock_status_to_products_table',21);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (99,'2026_08_09_090000_add_customer_code_to_suppliers_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (875,'2026_05_05_070449_add_stock_status_to_order_archives_items_table',22);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (100,'2026_08_09_091500_create_profit_center_supplier_codes_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (878,'2026_01_24_000000_add_expires_at_to_personal_access_tokens_table',23);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (101,'2026_08_10_090000_add_execution_failed_to_pricelist_files_status_enum',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (881,'2026_01_24_000000_rename_password_resets_table',23);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (102,'2026_08_10_090100_add_execution_fields_to_pricelist_file_lines_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (882,'2026_07_20_171642_restore_missing_id_auto_increment',24);
|
||||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (103,'2026_08_16_090000_create_deployment_packages_table',1);
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (883,'2026_08_08_152549_create_features_table',24);
|
||||||
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (884,'2026_08_08_153000_create_feature_flags_table',25);
|
||||||
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (885,'2026_08_08_154500_create_feature_flag_overrides_table',26);
|
||||||
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (886,'2026_08_09_090000_add_customer_code_to_suppliers_table',27);
|
||||||
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (887,'2026_08_09_091500_create_profit_center_supplier_codes_table',27);
|
||||||
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (888,'2026_08_10_090000_add_execution_failed_to_pricelist_files_status_enum',28);
|
||||||
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (889,'2026_08_10_090100_add_execution_fields_to_pricelist_file_lines_table',28);
|
||||||
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (890,'2026_08_16_090000_create_deployment_packages_table',29);
|
||||||
|
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (892,'2026_08_24_172116_add_meter_unit_to_seller_and_amount_unit_columns',30);
|
||||||
commit;
|
commit;
|
||||||
|
|||||||
@ -264,3 +264,78 @@
|
|||||||
expect($dateStrings)->toContain('2026-04-15');
|
expect($dateStrings)->toContain('2026-04-15');
|
||||||
expect($dateStrings)->toContain('2026-04-17');
|
expect($dateStrings)->toContain('2026-04-17');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows weekend delivery when the schedule includes it', function () {
|
||||||
|
$pc = ProfitCenter::factory()->create();
|
||||||
|
$supplier = Supplier::factory()->create();
|
||||||
|
$schedule = DeliverySchedule::factory()->create(['saturday' => true]);
|
||||||
|
ProfitCenterSupplierSchedule::factory()->create([
|
||||||
|
'profit_center_id' => $pc->id,
|
||||||
|
'supplier_id' => $supplier->id,
|
||||||
|
'delivery_schedule_id' => $schedule->id,
|
||||||
|
]);
|
||||||
|
$service = app(DeliveryCalendarService::class);
|
||||||
|
$saturday = Carbon::parse('2026-04-18'); // Saturday
|
||||||
|
expect($service->isDeliveryDay($pc, $supplier, $saturday))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still blocks weekends the schedule does not include', function () {
|
||||||
|
$pc = ProfitCenter::factory()->create();
|
||||||
|
$supplier = Supplier::factory()->create();
|
||||||
|
$schedule = DeliverySchedule::factory()->create(['monday' => true, 'saturday' => false]);
|
||||||
|
ProfitCenterSupplierSchedule::factory()->create([
|
||||||
|
'profit_center_id' => $pc->id,
|
||||||
|
'supplier_id' => $supplier->id,
|
||||||
|
'delivery_schedule_id' => $schedule->id,
|
||||||
|
]);
|
||||||
|
$service = app(DeliveryCalendarService::class);
|
||||||
|
$saturday = Carbon::parse('2026-04-18'); // Saturday
|
||||||
|
expect($service->isDeliveryDay($pc, $supplier, $saturday))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blocks official holidays even on a scheduled weekend day', function () {
|
||||||
|
$pc = ProfitCenter::factory()->create();
|
||||||
|
$supplier = Supplier::factory()->create();
|
||||||
|
$schedule = DeliverySchedule::factory()->create(['saturday' => true]);
|
||||||
|
ProfitCenterSupplierSchedule::factory()->create([
|
||||||
|
'profit_center_id' => $pc->id,
|
||||||
|
'supplier_id' => $supplier->id,
|
||||||
|
'delivery_schedule_id' => $schedule->id,
|
||||||
|
]);
|
||||||
|
WorkCalendar::create([
|
||||||
|
'date' => '2026-04-18',
|
||||||
|
'type' => WorkDayType::Holiday,
|
||||||
|
'data_source' => DataSource::Manual,
|
||||||
|
]);
|
||||||
|
$service = app(DeliveryCalendarService::class);
|
||||||
|
$saturday = Carbon::parse('2026-04-18'); // Saturday
|
||||||
|
expect($service->isDeliveryDay($pc, $supplier, $saturday))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes scheduled weekend days in an available date range', function () {
|
||||||
|
$pc = ProfitCenter::factory()->create();
|
||||||
|
$supplier = Supplier::factory()->create();
|
||||||
|
$schedule = DeliverySchedule::factory()->create([
|
||||||
|
'monday' => true,
|
||||||
|
'tuesday' => true,
|
||||||
|
'wednesday' => true,
|
||||||
|
'thursday' => true,
|
||||||
|
'friday' => true,
|
||||||
|
'saturday' => true,
|
||||||
|
'sunday' => true,
|
||||||
|
]);
|
||||||
|
ProfitCenterSupplierSchedule::factory()->create([
|
||||||
|
'profit_center_id' => $pc->id,
|
||||||
|
'supplier_id' => $supplier->id,
|
||||||
|
'delivery_schedule_id' => $schedule->id,
|
||||||
|
]);
|
||||||
|
$service = app(DeliveryCalendarService::class);
|
||||||
|
$dates = $service->getAvailableDeliveryDates(
|
||||||
|
$pc,
|
||||||
|
$supplier,
|
||||||
|
Carbon::parse('2026-04-13'), // Monday
|
||||||
|
Carbon::parse('2026-04-19'), // Sunday
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($dates)->toHaveCount(7);
|
||||||
|
});
|
||||||
|
|||||||
81
tests/Feature/NextDeliveryDateCalculatorServiceTest.php
Normal file
81
tests/Feature/NextDeliveryDateCalculatorServiceTest.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\DeliverySchedule;
|
||||||
|
use App\Models\ProfitCenter;
|
||||||
|
use App\Models\ProfitCenterSupplierSchedule;
|
||||||
|
use App\Models\Supplier;
|
||||||
|
use App\Services\NextDeliveryDateCalculatorService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, bool> $days
|
||||||
|
*/
|
||||||
|
function makeAssignment(array $days, bool $hasConstraint = true): array
|
||||||
|
{
|
||||||
|
$pc = ProfitCenter::factory()->create();
|
||||||
|
$supplier = Supplier::factory()->create([
|
||||||
|
'hasDeliveryConstraint' => $hasConstraint,
|
||||||
|
'orderCutOffTime' => 12,
|
||||||
|
'deliveryLeadTime' => 48, // 2 nap
|
||||||
|
]);
|
||||||
|
$schedule = DeliverySchedule::factory()->create($days);
|
||||||
|
ProfitCenterSupplierSchedule::factory()->create([
|
||||||
|
'profit_center_id' => $pc->id,
|
||||||
|
'supplier_id' => $supplier->id,
|
||||||
|
'delivery_schedule_id' => $schedule->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [$pc, $supplier];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EVERY_DAY = [
|
||||||
|
'monday' => true,
|
||||||
|
'tuesday' => true,
|
||||||
|
'wednesday' => true,
|
||||||
|
'thursday' => true,
|
||||||
|
'friday' => true,
|
||||||
|
'saturday' => true,
|
||||||
|
'sunday' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
const WEEKDAYS_ONLY = [
|
||||||
|
'monday' => true,
|
||||||
|
'tuesday' => true,
|
||||||
|
'wednesday' => true,
|
||||||
|
'thursday' => true,
|
||||||
|
'friday' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
it('counts scheduled weekend days towards the lead time', function () {
|
||||||
|
[$pc, $supplier] = makeAssignment(EVERY_DAY);
|
||||||
|
|
||||||
|
$result = app(NextDeliveryDateCalculatorService::class)
|
||||||
|
->calculate($supplier->id, $pc->id, Carbon::parse('2026-04-17 08:00')); // Friday, cut-off előtt
|
||||||
|
|
||||||
|
// Péntek (1.) + szombat (2.) telik el az átfutásból, így vasárnap már szállítható.
|
||||||
|
expect($result?->toDateString())->toBe('2026-04-19'); // Sunday
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not count weekends for a weekday-only schedule', function () {
|
||||||
|
[$pc, $supplier] = makeAssignment(WEEKDAYS_ONLY);
|
||||||
|
|
||||||
|
$result = app(NextDeliveryDateCalculatorService::class)
|
||||||
|
->calculate($supplier->id, $pc->id, Carbon::parse('2026-04-17 08:00')); // Friday, cut-off előtt
|
||||||
|
|
||||||
|
// Péntek (1.) + hétfő (2.) telik el – a hétvége kimarad –, így kedd az első szállítási nap.
|
||||||
|
expect($result?->toDateString())->toBe('2026-04-21'); // Tuesday
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a weekend day when there is no delivery constraint', function () {
|
||||||
|
[$pc, $supplier] = makeAssignment(EVERY_DAY, hasConstraint: false);
|
||||||
|
|
||||||
|
$result = app(NextDeliveryDateCalculatorService::class)
|
||||||
|
->calculate($supplier->id, $pc->id, Carbon::parse('2026-04-18 08:00')); // Saturday
|
||||||
|
|
||||||
|
// Átfutási idő nélkül a szombat maga az első szállítási nap.
|
||||||
|
expect($result?->toDateString())->toBe('2026-04-18'); // Saturday
|
||||||
|
});
|
||||||
63
tests/Feature/PricelistUnitValidationTest.php
Normal file
63
tests/Feature/PricelistUnitValidationTest.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\PricelistSellerUnitEnum;
|
||||||
|
use App\Enums\ProductUnitEnum;
|
||||||
|
use App\Models\ProfitCenter;
|
||||||
|
use App\Models\Supplier;
|
||||||
|
use App\Services\PriceListService;
|
||||||
|
use App\Services\PricelistFileProcessService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A validateSellerUnit()/validateAmountUnit() protected metódusok meghívása reflectionnel
|
||||||
|
* (lásd PricelistColumnMappingTest.php hasonló mintáját a buildColumnMap()-hoz).
|
||||||
|
*/
|
||||||
|
function invokeUnitValidator(string $method, string $unitValue): ?string
|
||||||
|
{
|
||||||
|
$service = app(PricelistFileProcessService::class);
|
||||||
|
$reflection = new ReflectionMethod($service, $method);
|
||||||
|
$reflection->setAccessible(true);
|
||||||
|
|
||||||
|
$header = match ($method) {
|
||||||
|
'validateSellerUnit' => PriceListService::EXPECTED_HEADERS[9],
|
||||||
|
'validateAmountUnit' => PriceListService::EXPECTED_HEADERS[11],
|
||||||
|
};
|
||||||
|
|
||||||
|
return $reflection->invoke($service, [$header => $unitValue]);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('accepts "m" as a valid seller unit in the new pricelist pipeline (EV3-465)', function () {
|
||||||
|
expect(invokeUnitValidator('validateSellerUnit', 'm'))->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts "m" as a valid amount unit in the new pricelist pipeline (EV3-465)', function () {
|
||||||
|
expect(invokeUnitValidator('validateAmountUnit', 'm'))->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still rejects an unknown seller unit', function () {
|
||||||
|
expect(invokeUnitValidator('validateSellerUnit', 'nincs-ilyen'))->not->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts "m" as a valid ProductUnitEnum value, used by the legacy pricelist pipeline', function () {
|
||||||
|
expect(ProductUnitEnum::hasValue('m'))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists a product with "m" seller and amount unit (DB enum column check)', function () {
|
||||||
|
$supplier = Supplier::factory()->create();
|
||||||
|
|
||||||
|
$product = App\Models\Product::create([
|
||||||
|
'name' => 'Bunzl teszttermék',
|
||||||
|
'supplier_id' => $supplier->id,
|
||||||
|
'sellerUnit' => 'm',
|
||||||
|
'amountUnit' => 'm',
|
||||||
|
'status' => App\Enums\DbStatusFieldEnum::active,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$product->refresh();
|
||||||
|
|
||||||
|
expect($product->sellerUnit)->toBe('m');
|
||||||
|
expect($product->amountUnit)->toBe('m');
|
||||||
|
});
|
||||||
325
tests/Feature/ProducerDedupeTest.php
Normal file
325
tests/Feature/ProducerDedupeTest.php
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Console\Commands\ProducersDedupeReport;
|
||||||
|
use App\Enums\DbStatusFieldEnum;
|
||||||
|
use App\Models\Producer;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Services\ProducerDeduplicator;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
function producer(string $name): Producer
|
||||||
|
{
|
||||||
|
return Producer::create(['name' => $name, 'status' => DbStatusFieldEnum::active, 'canSee' => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function productFor(Producer $producer, string $sku): Product
|
||||||
|
{
|
||||||
|
return Product::create([
|
||||||
|
'name' => 'Termék ' . $sku,
|
||||||
|
'supplierProductNumber' => $sku,
|
||||||
|
'producer_id' => $producer->id,
|
||||||
|
'unitValue' => 1,
|
||||||
|
'note' => '',
|
||||||
|
'status' => DbStatusFieldEnum::active,
|
||||||
|
'canSee' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{id: int, final_name: ?string}> $rows
|
||||||
|
*/
|
||||||
|
function decisionsFor(array $rows): array
|
||||||
|
{
|
||||||
|
return array_map(fn ($r) => ['group' => 1, 'id' => $r['id'], 'final_name' => $r['final_name'] ?? null], $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->deduplicator = app(ProducerDeduplicator::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a duplikált csoportban a legtöbb termékkel rendelkező rekord a javasolt megtartandó', function () {
|
||||||
|
$few = producer('Danone ');
|
||||||
|
$many = producer('Danone');
|
||||||
|
|
||||||
|
productFor($many, 'A1');
|
||||||
|
productFor($many, 'A2');
|
||||||
|
productFor($few, 'B1');
|
||||||
|
|
||||||
|
$groups = $this->deduplicator->duplicateGroups();
|
||||||
|
|
||||||
|
expect($groups)->toHaveKey('DANONE')
|
||||||
|
->and($groups['DANONE'][0]['producer']->id)->toBe($many->id)
|
||||||
|
->and($groups['DANONE'][0]['counts']['products'])->toBe(2)
|
||||||
|
->and($groups['DANONE'][1]['producer']->id)->toBe($few->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az egyedi nevű gyártó nem kerül a duplikátumok közé', function () {
|
||||||
|
producer('Danone');
|
||||||
|
producer('Bonduelle');
|
||||||
|
|
||||||
|
expect($this->deduplicator->duplicateGroups())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az üres végleges név kihagyja a csoportot – ez a javaslat elutasításának módja', function () {
|
||||||
|
$a = producer('Danone');
|
||||||
|
$b = producer('Danone ');
|
||||||
|
|
||||||
|
$plan = $this->deduplicator->buildPlan(decisionsFor([
|
||||||
|
['id' => $a->id, 'final_name' => null],
|
||||||
|
['id' => $b->id, 'final_name' => null],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Nem hiba: a bizonytalan javaslatokat a megrendelő így utasítja el. De jelentjük,
|
||||||
|
// hogy egy véletlen kihagyás se maradjon észrevétlen.
|
||||||
|
expect($plan['merges'])->toBe([])
|
||||||
|
->and($plan['errors'])->toBe([])
|
||||||
|
->and($plan['skipped'])->toHaveCount(1)
|
||||||
|
->and($plan['skipped'][0]['names'])->toContain('Danone ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hiányos csoport esetén hibát jelez', function () {
|
||||||
|
$a = producer('Danone');
|
||||||
|
producer('Danone ');
|
||||||
|
|
||||||
|
// A lapról lemaradt a csoport másik tagja - ilyenkor nem szabad összevonni,
|
||||||
|
// mert a döntés nem a teljes csoportra vonatkozott.
|
||||||
|
$plan = $this->deduplicator->buildPlan(decisionsFor([
|
||||||
|
['id' => $a->id, 'final_name' => 'Danone'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect($plan['merges'])->toBe([])
|
||||||
|
->and($plan['errors'][0])->toContain('nem tartalmazza a csoport minden tagját');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ismeretlen azonosítót jelez, ha a lap más adatállapotból származik', function () {
|
||||||
|
$plan = $this->deduplicator->buildPlan(decisionsFor([['id' => 999999, 'final_name' => 'Bármi']]));
|
||||||
|
|
||||||
|
expect($plan['errors'][0])->toContain('nem található');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a laza jelöltek megfogják az elírásokat, amiket a szigorú szabály nem', function () {
|
||||||
|
// Ezek egyike sem kerül össze a szigorú szabállyal: írásjel, kötőjel, egybeírás,
|
||||||
|
// hiányzó ékezet. A megrendelő szemével viszont nyilvánvalóan ugyanaz a cég.
|
||||||
|
producer('Békás Kft.');
|
||||||
|
producer('Békás Kft');
|
||||||
|
producer('Gast Food');
|
||||||
|
producer('Gast-Food');
|
||||||
|
producer('Alföldi Tej');
|
||||||
|
producer('Alfölditej');
|
||||||
|
|
||||||
|
expect($this->deduplicator->duplicateGroups())->toBe([]);
|
||||||
|
|
||||||
|
$loose = $this->deduplicator->looseCandidateGroups();
|
||||||
|
|
||||||
|
expect($loose)->toHaveCount(3);
|
||||||
|
|
||||||
|
$names = array_map(
|
||||||
|
fn ($members) => array_map(fn ($m) => $m['producer']->name, $members),
|
||||||
|
array_values($loose),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(collect($names)->flatten()->all())->toContain('Békás Kft.', 'Gast-Food', 'Alfölditej');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a laza jelöltek NEM vonnak össze eltérő cégformát', function () {
|
||||||
|
// A Kft és a Zrt valódi különbség, nem elírás - ezt a rendszer nem döntheti el.
|
||||||
|
producer('Alföldi Tej Kft');
|
||||||
|
producer('Alföldi Tej Zrt');
|
||||||
|
|
||||||
|
expect($this->deduplicator->looseCandidateGroups())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a cégforma nélküli név párba állhat, ha csak egyféle forma van', function () {
|
||||||
|
$bare = producer('Szegedi Sütödék');
|
||||||
|
$kft = producer('Szegedi Sütödék Kft.');
|
||||||
|
|
||||||
|
$loose = $this->deduplicator->looseCandidateGroups();
|
||||||
|
|
||||||
|
expect($loose)->toHaveCount(1);
|
||||||
|
|
||||||
|
$ids = array_map(fn ($m) => $m['producer']->id, array_values($loose)[0]);
|
||||||
|
|
||||||
|
expect($ids)->toContain($bare->id)->toContain($kft->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az archivált gyártó nem kerül a laza jelöltek közé', function () {
|
||||||
|
producer('Békás Kft.');
|
||||||
|
$merged = producer('Békás Kft');
|
||||||
|
DB::table('producers')->where('id', $merged->id)->update(['status' => DbStatusFieldEnum::archive]);
|
||||||
|
|
||||||
|
expect($this->deduplicator->looseCandidateGroups())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a lap egy csoportba tehet olyan gyártókat is, amiket a normalizálás nem köt össze', function () {
|
||||||
|
// A megrendelő döntése: a "Kőröstej" és a "Kőröstej Kft" ugyanaz a cég. A
|
||||||
|
// névnormalizálás ezt nem tudhatja, a lap Csoport oszlopa viszont kifejezi.
|
||||||
|
$short = producer('Kőröstej');
|
||||||
|
$shortDup = producer('Kőröstej ');
|
||||||
|
$long = producer('Kőröstej Kft');
|
||||||
|
$longDup = producer('KŐRÖSTEJ KFT');
|
||||||
|
|
||||||
|
productFor($short, 'A1');
|
||||||
|
productFor($short, 'A2');
|
||||||
|
productFor($long, 'B1');
|
||||||
|
|
||||||
|
$plan = app(ProducerDeduplicator::class)->buildPlan([
|
||||||
|
['group' => 7, 'id' => $short->id, 'final_name' => 'Kőröstej Kft'],
|
||||||
|
['group' => 7, 'id' => $shortDup->id, 'final_name' => null],
|
||||||
|
['group' => 7, 'id' => $long->id, 'final_name' => null],
|
||||||
|
['group' => 7, 'id' => $longDup->id, 'final_name' => null],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($plan['errors'])->toBe([])
|
||||||
|
->and($plan['merges'])->toHaveCount(1);
|
||||||
|
|
||||||
|
$merge = $plan['merges'][0];
|
||||||
|
|
||||||
|
// A megtartott REKORD a legtöbb terméket tartalmazó, a NEVE viszont a választott
|
||||||
|
// cégnév - így a legkevesebb sor mozdul, mégis a kívánt név marad.
|
||||||
|
expect($merge['keeper_id'])->toBe($short->id)
|
||||||
|
->and($merge['final_name'])->toBe('Kőröstej Kft')
|
||||||
|
->and($merge['rename'])->toBeTrue()
|
||||||
|
->and($merge['from'])->toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az azonos nevű rekordok szétszórása külön csoportokba hibát ad', function () {
|
||||||
|
// Ez duplikátumot hagyna maga után, ezért nem hajtható végre.
|
||||||
|
$a = producer('Danone');
|
||||||
|
$b = producer('Danone ');
|
||||||
|
|
||||||
|
$plan = app(ProducerDeduplicator::class)->buildPlan([
|
||||||
|
['group' => 1, 'id' => $a->id, 'final_name' => 'Danone'],
|
||||||
|
['group' => 2, 'id' => $b->id, 'final_name' => 'Danone'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($plan['merges'])->toBe([])
|
||||||
|
->and($plan['errors'][0])->toContain('több csoportba kerültek');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az összevonás átírja a hivatkozásokat és archiválja a beolvasztott rekordot', function () {
|
||||||
|
$keeper = producer('Danone');
|
||||||
|
$loser = producer('Danone ');
|
||||||
|
|
||||||
|
productFor($keeper, 'A1');
|
||||||
|
$moved = productFor($loser, 'B1');
|
||||||
|
|
||||||
|
$plan = $this->deduplicator->buildPlan(decisionsFor([
|
||||||
|
['id' => $keeper->id, 'final_name' => 'Danone'],
|
||||||
|
['id' => $loser->id, 'final_name' => null],
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect($plan['errors'])->toBe([]);
|
||||||
|
|
||||||
|
$report = $this->deduplicator->apply($plan['merges']);
|
||||||
|
|
||||||
|
expect($moved->refresh()->producer_id)->toBe($keeper->id)
|
||||||
|
->and($report['totals']['products'])->toBe(1);
|
||||||
|
|
||||||
|
$loser->refresh();
|
||||||
|
|
||||||
|
expect($loser->status)->toBe(DbStatusFieldEnum::archive)
|
||||||
|
->and((bool) $loser->canSee)->toBeFalse()
|
||||||
|
// Tartós nyom az adatbázisban, a jelentésfájltól függetlenül
|
||||||
|
->and($loser->note)->toContain('Összevonva ide: #' . $keeper->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a végleges név átnevezi a megtartott rekordot', function () {
|
||||||
|
$keeper = producer('danone');
|
||||||
|
$loser = producer('Danone ');
|
||||||
|
productFor($keeper, 'A1');
|
||||||
|
|
||||||
|
$plan = $this->deduplicator->buildPlan(decisionsFor([
|
||||||
|
['id' => $keeper->id, 'final_name' => 'Danone Magyarország'],
|
||||||
|
['id' => $loser->id, 'final_name' => null],
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect($plan['merges'][0]['rename'])->toBeTrue();
|
||||||
|
|
||||||
|
$this->deduplicator->apply($plan['merges']);
|
||||||
|
|
||||||
|
expect($keeper->refresh()->name)->toBe('Danone Magyarország');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a visszagörgetés minden érintett sort a saját eredeti értékére állít vissza', function () {
|
||||||
|
$keeper = producer('Danone');
|
||||||
|
$loser = producer('Danone ');
|
||||||
|
|
||||||
|
$stayed = productFor($keeper, 'A1'); // eredetileg is a megtartotton volt
|
||||||
|
$moved = productFor($loser, 'B1'); // ezt mozgatjuk
|
||||||
|
|
||||||
|
$plan = $this->deduplicator->buildPlan(decisionsFor([
|
||||||
|
['id' => $keeper->id, 'final_name' => 'Danone'],
|
||||||
|
['id' => $loser->id, 'final_name' => null],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$report = $this->deduplicator->apply($plan['merges']);
|
||||||
|
expect($moved->refresh()->producer_id)->toBe($keeper->id);
|
||||||
|
|
||||||
|
$this->deduplicator->rollback($report);
|
||||||
|
|
||||||
|
// A lényeg: a mozgatott sor visszakerül, a helyben maradt NEM mozdul el.
|
||||||
|
// Egy egyszerű fordított leképezés mindkettőt átírná - ezért tárolunk soronként.
|
||||||
|
expect($moved->refresh()->producer_id)->toBe($loser->id)
|
||||||
|
->and($stayed->refresh()->producer_id)->toBe($keeper->id);
|
||||||
|
|
||||||
|
$loser->refresh();
|
||||||
|
|
||||||
|
expect($loser->name)->toBe('Danone ')
|
||||||
|
->and($loser->status)->toBe(DbStatusFieldEnum::active)
|
||||||
|
->and((bool) $loser->canSee)->toBeTrue()
|
||||||
|
->and($loser->note)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a döntési lap legenerálható és visszaolvasható', function () {
|
||||||
|
$keeper = producer('Danone');
|
||||||
|
$loser = producer('Danone ');
|
||||||
|
productFor($keeper, 'A1');
|
||||||
|
|
||||||
|
$path = storage_path('app/private/teszt_dontesi_lap.xlsx');
|
||||||
|
@unlink($path);
|
||||||
|
|
||||||
|
$this->artisan('producers:dedupe-report', ['--path' => $path])->assertExitCode(0);
|
||||||
|
|
||||||
|
expect(file_exists($path))->toBeTrue();
|
||||||
|
|
||||||
|
$sheet = IOFactory::load($path)->getActiveSheet();
|
||||||
|
$found = [];
|
||||||
|
for ($row = 1; $row <= $sheet->getHighestDataRow(); $row++) {
|
||||||
|
for ($col = 1; $col <= 9; $col++) {
|
||||||
|
$found[] = (string) $sheet->getCellByColumnAndRow($col, $row)->getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fejlécnevek adják a gépi visszaolvasás horgonyait
|
||||||
|
expect($found)->toContain(ProducersDedupeReport::COLUMN_ID)
|
||||||
|
->and($found)->toContain(ProducersDedupeReport::COLUMN_FINAL_NAME)
|
||||||
|
->and($found)->toContain((string) $keeper->id)
|
||||||
|
->and($found)->toContain((string) $loser->id)
|
||||||
|
->and($found)->toContain('"Danone "');
|
||||||
|
|
||||||
|
@unlink($path);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a dry-run nem módosít semmit', function () {
|
||||||
|
$keeper = producer('Danone');
|
||||||
|
$loser = producer('Danone ');
|
||||||
|
$moved = productFor($loser, 'B1');
|
||||||
|
productFor($keeper, 'A1');
|
||||||
|
|
||||||
|
$path = storage_path('app/private/teszt_dry_run.xlsx');
|
||||||
|
@unlink($path);
|
||||||
|
$this->artisan('producers:dedupe-report', ['--path' => $path])->assertExitCode(0);
|
||||||
|
|
||||||
|
$this->artisan('producers:dedupe', ['file' => $path])->assertExitCode(0);
|
||||||
|
|
||||||
|
expect($moved->refresh()->producer_id)->toBe($loser->id)
|
||||||
|
->and($loser->refresh()->status)->toBe(DbStatusFieldEnum::active)
|
||||||
|
->and(DB::table('producers')->where('status', 'archive')->count())->toBe(0);
|
||||||
|
|
||||||
|
@unlink($path);
|
||||||
|
});
|
||||||
113
tests/Feature/ProducerDuplicationTest.php
Normal file
113
tests/Feature/ProducerDuplicationTest.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\DbStatusFieldEnum;
|
||||||
|
use App\Models\Producer;
|
||||||
|
use App\Services\PriceListService;
|
||||||
|
use App\Services\PricelistFileProcessService;
|
||||||
|
use App\Support\NameNormalizer;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
function callPrivate(object $object, string $method, mixed ...$args): mixed
|
||||||
|
{
|
||||||
|
$reflection = new ReflectionMethod($object, $method);
|
||||||
|
$reflection->setAccessible(true);
|
||||||
|
|
||||||
|
return $reflection->invoke($object, ...$args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeProducer(string $name, string $status = DbStatusFieldEnum::active): Producer
|
||||||
|
{
|
||||||
|
return Producer::create(['name' => $name, 'status' => $status, 'canSee' => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a normalizálás összevonja a kis/nagybetű, szóköz és szeparátor eltéréseket', function () {
|
||||||
|
expect(NameNormalizer::normalize('Danone'))->toBe('DANONE')
|
||||||
|
->and(NameNormalizer::normalize('Danone '))->toBe('DANONE')
|
||||||
|
->and(NameNormalizer::normalize(' danone '))->toBe('DANONE')
|
||||||
|
->and(NameNormalizer::normalize('Alba-Gel Kft'))->toBe('ALBA-GEL KFT')
|
||||||
|
->and(NameNormalizer::normalize('Tej/Sajt'))->toBe('TEJ SAJT')
|
||||||
|
->and(NameNormalizer::normalize('Tej_Sajt'))->toBe('TEJ SAJT')
|
||||||
|
->and(NameNormalizer::normalize(null))->toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a matches() üres nevet nem tekint egyezésnek', function () {
|
||||||
|
expect(NameNormalizer::matches('Danone', 'danone '))->toBeTrue()
|
||||||
|
->and(NameNormalizer::matches('', ''))->toBeFalse()
|
||||||
|
->and(NameNormalizer::matches(null, 'Danone'))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a legacy import megtalálja a meglévő gyártót eltérő írásmód esetén is', function () {
|
||||||
|
$existing = makeProducer('Danone');
|
||||||
|
|
||||||
|
$service = app(PriceListService::class);
|
||||||
|
callPrivate($service, 'loadProducers', 'name');
|
||||||
|
|
||||||
|
// Ezek korábban mind ÚJ gyártót hoztak létre volna - így keletkezett 70 duplikátum.
|
||||||
|
foreach (['Danone ', ' danone', 'DANONE', 'Danone '] as $variant) {
|
||||||
|
$found = callPrivate($service, 'getProducerByName', $variant);
|
||||||
|
|
||||||
|
expect($found)->not->toBeFalse()
|
||||||
|
->and($found['id'])->toBe($existing->id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a legacy import változatlanul hagyja a valóban új gyártót', function () {
|
||||||
|
makeProducer('Danone');
|
||||||
|
|
||||||
|
$service = app(PriceListService::class);
|
||||||
|
callPrivate($service, 'loadProducers', 'name');
|
||||||
|
|
||||||
|
expect(callPrivate($service, 'getProducerByName', 'Bonduelle'))->toBeFalse()
|
||||||
|
->and(callPrivate($service, 'getProducerByName', ' '))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a karakterre pontos találat elsőbbséget élvez', function () {
|
||||||
|
$old = makeProducer('Voyagex ');
|
||||||
|
$new = makeProducer('Voyagex');
|
||||||
|
|
||||||
|
$service = app(PriceListService::class);
|
||||||
|
callPrivate($service, 'loadProducers', 'name');
|
||||||
|
|
||||||
|
// Ha a fájlban pontosan az egyik rekord neve szerepel, azt kell visszaadni -
|
||||||
|
// a normalizált egyeztetés csak tartalék útvonal.
|
||||||
|
expect(callPrivate($service, 'getProducerByName', 'Voyagex')['id'])->toBe($new->id)
|
||||||
|
->and(callPrivate($service, 'getProducerByName', 'Voyagex ')['id'])->toBe($old->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pontos találat híján duplikátumból a legrégebbi rekordot adja vissza', function () {
|
||||||
|
// Ez a takarítás előtti adatállapot: a termékek túlnyomó része a régi rekordra
|
||||||
|
// mutat, tehát az a kanonikus. Korábban a feloldás a LEGÚJABBAT választotta.
|
||||||
|
$old = makeProducer('Voyagex ');
|
||||||
|
$new = makeProducer('Voyagex');
|
||||||
|
|
||||||
|
$service = app(PriceListService::class);
|
||||||
|
callPrivate($service, 'loadProducers', 'name');
|
||||||
|
|
||||||
|
// Egyik rekord nevével sem egyezik karakterre
|
||||||
|
$found = callPrivate($service, 'getProducerByName', 'VOYAGEX');
|
||||||
|
|
||||||
|
expect($found['id'])->toBe($old->id)
|
||||||
|
->and($found['id'])->not->toBe($new->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az új gyártó neve körülvágva kerül mentésre', function () {
|
||||||
|
$service = app(PriceListService::class);
|
||||||
|
$id = callPrivate($service, 'addNewProducer', ['name' => ' Vadonatúj Gyártó ']);
|
||||||
|
|
||||||
|
expect(Producer::find($id)->name)->toBe('Vadonatúj Gyártó');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az archivált gyártó kimarad az árlista feldolgozó feloldásából', function () {
|
||||||
|
$active = makeProducer('Danone');
|
||||||
|
$merged = makeProducer('Danone ', DbStatusFieldEnum::archive);
|
||||||
|
|
||||||
|
$lookup = callPrivate(app(PricelistFileProcessService::class), 'getProducerLookupMap');
|
||||||
|
|
||||||
|
// Enélkül a beolvasztott rekord visszakerülne a feloldásba, és a takarítás
|
||||||
|
// hatástalan maradna.
|
||||||
|
expect($lookup['DANONE'] ?? null)->toBe($active->id)
|
||||||
|
->and(in_array($merged->id, $lookup, true))->toBeFalse();
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user