ADD EV3-357 laza duplikáció-jelöltek a döntési lapon (--loose)
A takarítás első köre után is maradtak nyilvánvaló duplikátumok: "Békás Kft." / "Békás Kft", "Gast Food" / "Gast-Food", "Alföldi Tej" / "Alfölditej". A NameNormalizer csak a kis/nagybetűt, a / és _ szeparátort és a többszörös szóközt kezeli - az írásjelekhez, a kötőjelhez, az egybeíráshoz és az ékezethez nem nyúl. A szabályt SZÁNDÉKOSAN nem lazítottuk, mert két, ellentétes igényű feladatot szolgál ki: az import-párosításnál egy téves egyezés csendben rossz gyártóhoz rendelne termékeket, a takarítási javaslatnál viszont a megrendelő nemet mond rá. Ezért a kettő szétválik. - ProducerDeduplicator::looseCandidateGroups(): megengedőbb kulcs (írásjel, ékezet, szóköz elhagyása) KIZÁRÓLAG javaslatokhoz. A különböző cégformát (Kft vs Zrt) itt sem vonja össze - az valódi különbség, nem elírás -, a cégforma nélküli név viszont párba állhat, ha csak egyféle forma van. - producers:dedupe-report --loose: a javaslatok "ellenőrizendő" típussal, ÜRES végleges névvel kerülnek a lapra, a javaslat a Megjegyzés oszlopban. Így az alapértelmezett viselkedés a "nem nyúlunk hozzá", a döntés tudatos kitöltés. - buildPlan: az üres végleges név mostantól KIHAGYÁS, nem hiba - ez a javaslat elutasításának módja. A kihagyott csoportokat a parancs tételesen kiírja. - buildPlan bármely aktív gyártót elfogad a lapon, nem csak a szigorú duplikátumok tagjait. A d2d másolaton az első kör után: 0 biztos duplikáció, 101 ellenőrizendő javaslat 241 rekorddal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7e32a870ae
commit
4198c03f5a
@ -41,12 +41,21 @@ public function handle(ProducerDeduplicator $deduplicator): int
|
|||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
['merges' => $merges, 'errors' => $errors] = $deduplicator->buildPlan($decisions);
|
$plan = $deduplicator->buildPlan($decisions);
|
||||||
|
$merges = $plan['merges'];
|
||||||
|
$errors = $plan['errors'];
|
||||||
|
|
||||||
foreach ($errors as $error) {
|
foreach ($errors as $error) {
|
||||||
$this->warn(' ! ' . $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 === []) {
|
if ($merges === []) {
|
||||||
$this->error('Nincs végrehajtható összevonás.');
|
$this->error('Nincs végrehajtható összevonás.');
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Services\ProducerDeduplicator;
|
use App\Services\ProducerDeduplicator;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
@ -13,7 +14,9 @@
|
|||||||
|
|
||||||
class ProducersDedupeReport extends Command
|
class ProducersDedupeReport extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'producers:dedupe-report {--path= : a kimeneti xlsx útvonala}';
|
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)';
|
protected $description = 'Döntési lapot készít a duplikált gyártónevekről (a producers:dedupe bemenete)';
|
||||||
|
|
||||||
@ -28,11 +31,16 @@ class ProducersDedupeReport extends Command
|
|||||||
|
|
||||||
public const COLUMN_FINAL_NAME = 'VÉGLEGES NÉV (kitöltendő)';
|
public const COLUMN_FINAL_NAME = 'VÉGLEGES NÉV (kitöltendő)';
|
||||||
|
|
||||||
|
private const TYPE_CERTAIN = 'biztos';
|
||||||
|
|
||||||
|
private const TYPE_REVIEW = 'ellenőrizendő';
|
||||||
|
|
||||||
public function handle(ProducerDeduplicator $deduplicator): int
|
public function handle(ProducerDeduplicator $deduplicator): int
|
||||||
{
|
{
|
||||||
$groups = $deduplicator->duplicateGroups();
|
$certain = $deduplicator->duplicateGroups();
|
||||||
|
$loose = $this->option('loose') ? $deduplicator->looseCandidateGroups() : [];
|
||||||
|
|
||||||
if ($groups === []) {
|
if ($certain === [] && $loose === []) {
|
||||||
$this->info('Nincs duplikált gyártónév ebben az adatbázisban.');
|
$this->info('Nincs duplikált gyártónév ebben az adatbázisban.');
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
@ -46,11 +54,11 @@ public function handle(ProducerDeduplicator $deduplicator): int
|
|||||||
$sheet = $spreadsheet->getActiveSheet();
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
$sheet->setTitle('Gyártó duplikációk');
|
$sheet->setTitle('Gyártó duplikációk');
|
||||||
|
|
||||||
$this->writeIntro($sheet);
|
$this->writeIntro($sheet, $loose !== []);
|
||||||
$headerRow = 8;
|
$headerRow = 9;
|
||||||
$this->writeHeader($sheet, $headerRow);
|
$this->writeHeader($sheet, $headerRow);
|
||||||
$lastRow = $this->writeGroups($sheet, $headerRow, $groups);
|
$lastRow = $this->writeGroups($sheet, $headerRow, $certain, $loose);
|
||||||
$this->finishLayout($sheet, $headerRow, $lastRow, count($groups), $groups);
|
$this->finishLayout($sheet, $headerRow, $lastRow, $certain, $loose);
|
||||||
|
|
||||||
$directory = dirname($path);
|
$directory = dirname($path);
|
||||||
if (! is_dir($directory)) {
|
if (! is_dir($directory)) {
|
||||||
@ -59,50 +67,69 @@ public function handle(ProducerDeduplicator $deduplicator): int
|
|||||||
|
|
||||||
(new Xlsx($spreadsheet))->save($path);
|
(new Xlsx($spreadsheet))->save($path);
|
||||||
|
|
||||||
$records = array_sum(array_map('count', $groups));
|
|
||||||
$this->info('Döntési lap elkészült: ' . $path);
|
$this->info('Döntési lap elkészült: ' . $path);
|
||||||
$this->line(' Csoportok: ' . count($groups) . ' | érintett gyártó rekord: ' . $records);
|
$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;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function writeIntro($sheet): void
|
private function writeIntro($sheet, bool $hasLoose): void
|
||||||
{
|
{
|
||||||
$sheet->setCellValue('A1', 'Gyártó nevek összevonása – döntési lap');
|
$sheet->setCellValue('A1', 'Gyártó nevek összevonása – döntési lap');
|
||||||
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(16);
|
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(16);
|
||||||
$sheet->setCellValue('A2', 'EV3-357 · Árlista feltöltés és feldolgozás újragondolása');
|
$sheet->setCellValue('A2', 'EV3-357 · Árlista feltöltés és feldolgozás újragondolása');
|
||||||
$sheet->setCellValue('A3', 'Adatforrás: ' . \DB::connection()->getDatabaseName()
|
$sheet->setCellValue('A3', 'Adatforrás: ' . DB::connection()->getDatabaseName()
|
||||||
. ' adatbázis · Készült: ' . now()->format('Y-m-d H:i'));
|
. ' 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 '
|
$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, vagy "Pick" és "PICK". Ez két gondot okoz: az '
|
. '"Danone" és "Danone " egy záró szóközzel, "Békás Kft." és "Békás Kft", vagy '
|
||||||
. 'árlista feldolgozó valódi változás nélkül is módosulást jelez, a statisztikában pedig egy '
|
. '"Gast Food" és "Gast-Food". Ez két gondot okoz: az árlista feldolgozó valódi változás '
|
||||||
. 'gyártóra szűrve a másik név alá könyvelt tételek kimaradnak a riportból.');
|
. 'nélkül is módosulást jelez, a statisztikában pedig egy gyártóra szűrve a másik név alá '
|
||||||
$sheet->mergeCells('A5:I5');
|
. 'könyvelt tételek kimaradnak a riportból.');
|
||||||
|
$sheet->mergeCells('A5:J5');
|
||||||
$sheet->getStyle('A5')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
|
$sheet->getStyle('A5')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
|
||||||
$sheet->getRowDimension(5)->setRowHeight(46);
|
$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 '
|
$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. Előre beírtuk a javaslatunkat: a legtöbb terméket tartalmazó '
|
. 'a megmaradó, egységes név. A többi név nem vész el: összevonjuk őket a véglegesbe, a '
|
||||||
. 'változat, szóközöktől megtisztítva. Ha egyetértesz vele, hagyd úgy. A kis- és nagybetűs '
|
. 'termékek és a rendelési előzmények megmaradnak. Az "Azonosító" oszlopot kérjük ne módosítsd.');
|
||||||
. 'írásmód a te döntésed. A többi név nem vész el: összevonjuk őket a véglegesbe, a termékek '
|
$sheet->mergeCells('A6:J6');
|
||||||
. 'és a rendelési előzmények megmaradnak. Az "Azonosító" oszlopot kérjük ne módosítsd.');
|
|
||||||
$sheet->mergeCells('A6:I6');
|
|
||||||
$sheet->getStyle('A6')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
|
$sheet->getStyle('A6')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
|
||||||
$sheet->getStyle('A6')->getFont()->setBold(true);
|
$sheet->getStyle('A6')->getFont()->setBold(true);
|
||||||
$sheet->getRowDimension(6)->setRowHeight(64);
|
$sheet->getRowDimension(6)->setRowHeight(46);
|
||||||
|
|
||||||
|
if ($hasLoose) {
|
||||||
|
$sheet->setCellValue('A7', 'A "Típus" oszlop kétféle sort különböztet meg. A "' . self::TYPE_CERTAIN
|
||||||
|
. '" csoportoknál a nevek csak írásmódban térnek el, ott már beírtuk a javaslatunkat. '
|
||||||
|
. 'Az "' . self::TYPE_REVIEW . '" csoportoknál a nevek jobban különböznek (elírás, kötőjel, '
|
||||||
|
. 'egybeírás, hiányzó ékezet), ezért NEM töltöttük ki a végleges nevet: '
|
||||||
|
. 'ha ugyanaz a cég, írd be a nevet (a javaslat a "Megjegyzés" oszlopban van); '
|
||||||
|
. 'HA NEM UGYANAZ, 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
|
private function writeHeader($sheet, int $headerRow): void
|
||||||
{
|
{
|
||||||
$headers = [self::COLUMN_GROUP, self::COLUMN_ID, 'Gyártó neve a rendszerben', 'Eltérés',
|
$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'];
|
'Termékek', 'Rendelési tételek', 'Létrehozva', self::COLUMN_FINAL_NAME, 'Megjegyzés'];
|
||||||
|
|
||||||
foreach ($headers as $i => $text) {
|
foreach ($headers as $i => $text) {
|
||||||
$sheet->setCellValueByColumnAndRow($i + 1, $headerRow, $text);
|
$sheet->setCellValueByColumnAndRow($i + 1, $headerRow, $text);
|
||||||
}
|
}
|
||||||
|
|
||||||
$range = 'A' . $headerRow . ':I' . $headerRow;
|
$range = 'A' . $headerRow . ':J' . $headerRow;
|
||||||
$sheet->getStyle($range)->getFont()->setBold(true)->getColor()->setARGB('FFFFFFFF');
|
$sheet->getStyle($range)->getFont()->setBold(true)->getColor()->setARGB('FFFFFFFF');
|
||||||
$sheet->getStyle($range)->getFill()->setFillType(Fill::FILL_SOLID)
|
$sheet->getStyle($range)->getFill()->setFillType(Fill::FILL_SOLID)
|
||||||
->getStartColor()->setARGB('FF44546A');
|
->getStartColor()->setARGB('FF44546A');
|
||||||
@ -111,54 +138,74 @@ private function writeHeader($sheet, int $headerRow): void
|
|||||||
$sheet->getRowDimension($headerRow)->setRowHeight(32);
|
$sheet->getRowDimension($headerRow)->setRowHeight(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function writeGroups($sheet, int $headerRow, array $groups): int
|
private function writeGroups($sheet, int $headerRow, array $certain, array $loose): int
|
||||||
{
|
{
|
||||||
$row = $headerRow + 1;
|
$row = $headerRow + 1;
|
||||||
$groupIndex = 0;
|
$groupIndex = 0;
|
||||||
|
|
||||||
foreach ($groups as $members) {
|
foreach ([self::TYPE_CERTAIN => $certain, self::TYPE_REVIEW => $loose] as $type => $groups) {
|
||||||
$groupIndex++;
|
foreach ($groups as $members) {
|
||||||
$proposed = trim((string) $members[0]['producer']->name);
|
$groupIndex++;
|
||||||
$shade = $groupIndex % 2 === 0 ? 'FFF2F2F2' : 'FFFFFFFF';
|
$row = $this->writeGroup($sheet, $row, $groupIndex, $type, $members);
|
||||||
$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->setCellValueExplicit('C' . $row, '"' . $name . '"', DataType::TYPE_STRING);
|
|
||||||
$sheet->setCellValue('D' . $row, $index === 0
|
|
||||||
? 'javasolt megtartani'
|
|
||||||
: implode(', ', $this->describeDifference($name, $proposed)));
|
|
||||||
$sheet->setCellValue('E' . $row, $member['counts']['products']);
|
|
||||||
$sheet->setCellValue('F' . $row, $member['counts']['order_archives_items']);
|
|
||||||
$sheet->setCellValue('G' . $row, substr((string) $producer->created_at, 0, 10));
|
|
||||||
|
|
||||||
$sheet->getStyle('A' . $row . ':I' . $row)->getFill()
|
|
||||||
->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB($shade);
|
|
||||||
|
|
||||||
if ($index === 0) {
|
|
||||||
$sheet->setCellValueExplicit('H' . $row, $proposed, DataType::TYPE_STRING);
|
|
||||||
$sheet->getStyle('H' . $row)->getFill()->setFillType(Fill::FILL_SOLID)
|
|
||||||
->getStartColor()->setARGB('FFFFF2CC');
|
|
||||||
$sheet->getStyle('H' . $row)->getFont()->setBold(true);
|
|
||||||
$sheet->getStyle('C' . $row)->getFont()->setBold(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$row++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->getStyle('A' . $groupFirstRow . ':I' . ($row - 1))->getBorders()->getTop()
|
|
||||||
->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFBFBFBF');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $row - 1;
|
return $row - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function writeGroup($sheet, int $row, int $groupIndex, string $type, array $members): int
|
||||||
|
{
|
||||||
|
$isReview = $type === self::TYPE_REVIEW;
|
||||||
|
$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>
|
* @return array<int, string>
|
||||||
*/
|
*/
|
||||||
@ -182,30 +229,37 @@ private function describeDifference(string $name, string $proposed): array
|
|||||||
return $differences === [] ? ['eltérő írásmód'] : $differences;
|
return $differences === [] ? ['eltérő írásmód'] : $differences;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function finishLayout($sheet, int $headerRow, int $lastRow, int $groupCount, array $groups): void
|
private function finishLayout($sheet, int $headerRow, int $lastRow, array $certain, array $loose): void
|
||||||
{
|
{
|
||||||
$first = $headerRow + 1;
|
$first = $headerRow + 1;
|
||||||
|
|
||||||
$sheet->getStyle('E' . $first . ':F' . $lastRow)->getNumberFormat()->setFormatCode('# ##0');
|
$sheet->getStyle('F' . $first . ':G' . $lastRow)->getNumberFormat()->setFormatCode('# ##0');
|
||||||
$sheet->getStyle('A' . $first . ':B' . $lastRow)->getAlignment()
|
$sheet->getStyle('A' . $first . ':C' . $lastRow)->getAlignment()
|
||||||
->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
$sheet->getStyle('H' . $first . ':H' . $lastRow)->getBorders()->getAllBorders()
|
$sheet->getStyle('I' . $first . ':I' . $lastRow)->getBorders()->getAllBorders()
|
||||||
->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFBF8F00');
|
->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFBF8F00');
|
||||||
|
|
||||||
$widths = ['A' => 9, 'B' => 11, 'C' => 34, 'D' => 22, 'E' => 11,
|
$widths = ['A' => 9, 'B' => 11, 'C' => 15, 'D' => 34, 'E' => 20,
|
||||||
'F' => 17, 'G' => 13, 'H' => 34, 'I' => 26];
|
'F' => 11, 'G' => 17, 'H' => 13, 'I' => 34, 'J' => 34];
|
||||||
foreach ($widths as $column => $width) {
|
foreach ($widths as $column => $width) {
|
||||||
$sheet->getColumnDimension($column)->setWidth($width);
|
$sheet->getColumnDimension($column)->setWidth($width);
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->freezePane('A' . $first);
|
$sheet->freezePane('A' . $first);
|
||||||
$sheet->setAutoFilter('A' . $headerRow . ':I' . $lastRow);
|
$sheet->setAutoFilter('A' . $headerRow . ':J' . $lastRow);
|
||||||
|
|
||||||
$summaryRow = $lastRow + 2;
|
$summaryRow = $lastRow + 2;
|
||||||
$sheet->setCellValue('A' . $summaryRow, 'Összesen ' . $groupCount . ' gyártó-csoport, '
|
$summary = 'Biztos duplikáció: ' . count($certain) . ' csoport, '
|
||||||
. array_sum(array_map('count', $groups)) . ' rekord. A csoportok többségénél mindkét névhez '
|
. array_sum(array_map('count', $certain)) . ' rekord.';
|
||||||
. 'tartoznak termékek, ezért az összevonás után is minden termék megmarad.');
|
|
||||||
$sheet->mergeCells('A' . $summaryRow . ':I' . $summaryRow);
|
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);
|
$sheet->getStyle('A' . $summaryRow)->getFont()->setItalic(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,11 +40,9 @@ class ProducerDeduplicator
|
|||||||
*/
|
*/
|
||||||
public function duplicateGroups(): array
|
public function duplicateGroups(): array
|
||||||
{
|
{
|
||||||
$counts = $this->referenceCounts();
|
|
||||||
|
|
||||||
$byNormalized = [];
|
$byNormalized = [];
|
||||||
foreach (DB::table('producers')->orderBy('id')->get() as $producer) {
|
foreach ($this->activeMembers() as $member) {
|
||||||
$byNormalized[NameNormalizer::normalize($producer->name)][] = $producer;
|
$byNormalized[NameNormalizer::normalize($member['producer']->name)][] = $member;
|
||||||
}
|
}
|
||||||
|
|
||||||
$groups = [];
|
$groups = [];
|
||||||
@ -53,28 +51,148 @@ public function duplicateGroups(): array
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$decorated = array_map(fn ($producer) => [
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = NameNormalizer::normalize($name);
|
||||||
|
$normalized = preg_replace('/[.,\-\'"()]/u', ' ', $normalized) ?? $normalized;
|
||||||
|
$normalized = strtr($normalized, [
|
||||||
|
'Á' => 'A', 'É' => 'E', 'Í' => 'I', 'Ó' => 'O', 'Ö' => 'O',
|
||||||
|
'Ő' => 'O', 'Ú' => 'U', 'Ü' => 'U', 'Ű' => 'U',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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,
|
'producer' => $producer,
|
||||||
'counts' => [
|
'counts' => [
|
||||||
'products' => $counts['products'][$producer->id] ?? 0,
|
'products' => $counts['products'][$producer->id] ?? 0,
|
||||||
'order_archives_items' => $counts['order_archives_items'][$producer->id] ?? 0,
|
'order_archives_items' => $counts['order_archives_items'][$producer->id] ?? 0,
|
||||||
'pricelist_file_lines' => $counts['pricelist_file_lines'][$producer->id] ?? 0,
|
'pricelist_file_lines' => $counts['pricelist_file_lines'][$producer->id] ?? 0,
|
||||||
],
|
],
|
||||||
], $members);
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
// A megtartandó a legtöbb TERMÉKKEL rendelkező rekord: a termékek a napi
|
/**
|
||||||
// működés alapja. Döntetlennél a több rendelési tétel, majd a régebbi rekord.
|
* A megtartandó rekord kerül előre: a legtöbb TERMÉKKEL rendelkező, mert így mozdul
|
||||||
usort($decorated, function ($a, $b) {
|
* a legkevesebb sor. Döntetlennél a több rendelési tétel, majd a régebbi rekord.
|
||||||
return [$b['counts']['products'], $b['counts']['order_archives_items'], $a['producer']->id]
|
*
|
||||||
<=> [$a['counts']['products'], $a['counts']['order_archives_items'], $b['producer']->id];
|
* @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];
|
||||||
|
});
|
||||||
|
|
||||||
$groups[$normalized] = $decorated;
|
return array_values($members);
|
||||||
}
|
|
||||||
|
|
||||||
ksort($groups);
|
|
||||||
|
|
||||||
return $groups;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,25 +230,29 @@ public function buildPlan(array $decisions): array
|
|||||||
{
|
{
|
||||||
$groups = $this->duplicateGroups();
|
$groups = $this->duplicateGroups();
|
||||||
|
|
||||||
$normalizedById = [];
|
|
||||||
$memberById = [];
|
$memberById = [];
|
||||||
|
foreach ($this->activeMembers() as $member) {
|
||||||
|
$memberById[$member['producer']->id] = $member;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedById = [];
|
||||||
foreach ($groups as $normalized => $members) {
|
foreach ($groups as $normalized => $members) {
|
||||||
foreach ($members as $member) {
|
foreach ($members as $member) {
|
||||||
$normalizedById[$member['producer']->id] = $normalized;
|
$normalizedById[$member['producer']->id] = $normalized;
|
||||||
$memberById[$member['producer']->id] = $member;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheetGroups = [];
|
$sheetGroups = [];
|
||||||
$finalNames = [];
|
$finalNames = [];
|
||||||
|
$skipped = [];
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
foreach ($decisions as $row) {
|
foreach ($decisions as $row) {
|
||||||
$id = (int) ($row['id'] ?? 0);
|
$id = (int) ($row['id'] ?? 0);
|
||||||
|
|
||||||
if (! isset($normalizedById[$id])) {
|
if (! isset($memberById[$id])) {
|
||||||
$errors[] = "A(z) {$id} azonosítójú gyártó nem szerepel duplikált csoportban "
|
$errors[] = "A(z) {$id} azonosítójú gyártó nem található (vagy már összevonták, "
|
||||||
. '(időközben megváltozott az adat, vagy más környezetből származik a lap).';
|
. 'vagy más környezetből származik a lap).';
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -165,20 +287,19 @@ public function buildPlan(array $decisions): array
|
|||||||
|
|
||||||
$members = array_values(array_map(fn ($id) => $memberById[$id], array_unique($ids)));
|
$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])) {
|
if (! isset($finalNames[$key])) {
|
||||||
$errors[] = 'Hiányzó végleges név a(z) ' . $key . '. csoportnál: '
|
$skipped[] = [
|
||||||
. implode(' / ', array_map(fn ($m) => '"' . $m['producer']->name . '"', $members));
|
'group' => $key,
|
||||||
|
'names' => array_map(fn ($m) => (string) $m['producer']->name, $members),
|
||||||
|
];
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A megtartott REKORD a legtöbb terméket tartalmazó (így mozdul a legkevesebb
|
$members = $this->sortByKeeperPriority($members);
|
||||||
// sor), a NEVE viszont a megrendelő által választott végleges név lesz.
|
|
||||||
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];
|
|
||||||
});
|
|
||||||
|
|
||||||
$keeper = $members[0]['producer'];
|
$keeper = $members[0]['producer'];
|
||||||
$losers = array_slice($members, 1);
|
$losers = array_slice($members, 1);
|
||||||
|
|
||||||
@ -200,7 +321,7 @@ public function buildPlan(array $decisions): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['merges' => $merges, 'errors' => $errors];
|
return ['merges' => $merges, 'errors' => $errors, 'skipped' => $skipped];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,6 +342,12 @@ private function validateGrouping(array $sheetGroups, array $normalizedById, arr
|
|||||||
|
|
||||||
foreach ($sheetGroups as $key => $ids) {
|
foreach ($sheetGroups as $key => $ids) {
|
||||||
foreach ($ids as $id) {
|
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];
|
$normalized = $normalizedById[$id];
|
||||||
$sheetKeysByNormalized[$normalized][$key] = true;
|
$sheetKeysByNormalized[$normalized][$key] = true;
|
||||||
$idsByNormalized[$normalized][$id] = true;
|
$idsByNormalized[$normalized][$id] = true;
|
||||||
|
|||||||
@ -65,7 +65,7 @@ function decisionsFor(array $rows): array
|
|||||||
expect($this->deduplicator->duplicateGroups())->toBe([]);
|
expect($this->deduplicator->duplicateGroups())->toBe([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hiányzó végleges név esetén hibát jelez, és nem tervez összevonást', function () {
|
test('az üres végleges név kihagyja a csoportot – ez a javaslat elutasításának módja', function () {
|
||||||
$a = producer('Danone');
|
$a = producer('Danone');
|
||||||
$b = producer('Danone ');
|
$b = producer('Danone ');
|
||||||
|
|
||||||
@ -74,8 +74,12 @@ function decisionsFor(array $rows): array
|
|||||||
['id' => $b->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([])
|
expect($plan['merges'])->toBe([])
|
||||||
->and($plan['errors'][0])->toContain('Hiányzó végleges név');
|
->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 () {
|
test('hiányos csoport esetén hibát jelez', function () {
|
||||||
@ -95,7 +99,60 @@ function decisionsFor(array $rows): array
|
|||||||
test('ismeretlen azonosítót jelez, ha a lap más adatállapotból származik', function () {
|
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']]));
|
$plan = $this->deduplicator->buildPlan(decisionsFor([['id' => 999999, 'final_name' => 'Bármi']]));
|
||||||
|
|
||||||
expect($plan['errors'][0])->toContain('nem szerepel duplikált csoportban');
|
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 () {
|
test('a lap egy csoportba tehet olyan gyártókat is, amiket a normalizálás nem köt össze', function () {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user