A megelőzés után a már felhalmozódott adat takarítása: 70 duplikált névcsoport, 146 rekord, ebből 76 beolvasztandó. Két parancs, mert a kivezetés három környezeten megy (local -> d2d -> éles), és minden környezetnek SAJÁT duplikátum-készlete van - a döntési lapot ezért ott kell újragenerálni, nem beégetett ID-listával dolgozunk. - producers:dedupe-report — döntési xlsx a megrendelőnek. A megtartandó a legtöbb TERMÉKKEL rendelkező rekord (döntetlennél több rendelési tétel, majd régebbi rekord); a végleges nevet a megrendelő hagyja jóvá, mert 48 csoport csak kis/nagybetűben tér el, és az írásmód üzleti döntés. - producers:dedupe — alapból csak kimutatás, --apply hajt végre. A lapot FEJLÉCNÉV alapján olvassa, nem oszlopbetű szerint, így a megrendelő beszúrhat oszlopot vagy átrendezheti a lapot anélkül, hogy eltörne. Hiányos csoportot és ismeretlen azonosítót visszautasít. - Visszafordíthatóság: a jelentés SORONKÉNT tárolja a régi producer_id-t, mert a fordított leképezés azokat a sorokat is átírná, amelyek eredetileg is a megtartott rekordra mutattak. --rollback ebből állít vissza. - Tartós nyom a jelentésfájltól függetlenül: a beolvasztott rekord archive + canSee=0 lesz, és a note-jába kerül, hova olvadt be. - A két nagy táblán nincs index a producer_id-n, ezért táblánként EGY UPDATE fut CASE leképezéssel - 76 külön WHERE 76 teljes scant jelentene. Mért eredmény a d2d másolaton: 27 mp alatt 314 termék + 27 272 archív + 115 árlista-sor átírva; a vizsgált árlistán a "Módosult" sorok 126 -> 23, a gyártó-diffek 114 -> 2. Visszagörgetés után minden szám visszaállt, és az újraszámolt diff ismét 126 - az ok-okozat mindkét irányban igazolt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
212 lines
9.3 KiB
PHP
212 lines
9.3 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Services\ProducerDeduplicator;
|
||
use Illuminate\Console\Command;
|
||
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}';
|
||
|
||
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ő)';
|
||
|
||
public function handle(ProducerDeduplicator $deduplicator): int
|
||
{
|
||
$groups = $deduplicator->duplicateGroups();
|
||
|
||
if ($groups === []) {
|
||
$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);
|
||
$headerRow = 8;
|
||
$this->writeHeader($sheet, $headerRow);
|
||
$lastRow = $this->writeGroups($sheet, $headerRow, $groups);
|
||
$this->finishLayout($sheet, $headerRow, $lastRow, count($groups), $groups);
|
||
|
||
$directory = dirname($path);
|
||
if (! is_dir($directory)) {
|
||
mkdir($directory, 0775, true);
|
||
}
|
||
|
||
(new Xlsx($spreadsheet))->save($path);
|
||
|
||
$records = array_sum(array_map('count', $groups));
|
||
$this->info('Döntési lap elkészült: ' . $path);
|
||
$this->line(' Csoportok: ' . count($groups) . ' | érintett gyártó rekord: ' . $records);
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
private function writeIntro($sheet): 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, vagy "Pick" és "PICK". 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:I5');
|
||
$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. Előre beírtuk a javaslatunkat: a legtöbb terméket tartalmazó '
|
||
. 'változat, szóközöktől megtisztítva. Ha egyetértesz vele, hagyd úgy. A kis- és nagybetűs '
|
||
. 'í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 '
|
||
. 'é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')->getFont()->setBold(true);
|
||
$sheet->getRowDimension(6)->setRowHeight(64);
|
||
}
|
||
|
||
private function writeHeader($sheet, int $headerRow): void
|
||
{
|
||
$headers = [self::COLUMN_GROUP, self::COLUMN_ID, '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 . ':I' . $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);
|
||
}
|
||
|
||
private function writeGroups($sheet, int $headerRow, array $groups): int
|
||
{
|
||
$row = $headerRow + 1;
|
||
$groupIndex = 0;
|
||
|
||
foreach ($groups as $members) {
|
||
$groupIndex++;
|
||
$proposed = trim((string) $members[0]['producer']->name);
|
||
$shade = $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->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 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, int $groupCount, array $groups): void
|
||
{
|
||
$first = $headerRow + 1;
|
||
|
||
$sheet->getStyle('E' . $first . ':F' . $lastRow)->getNumberFormat()->setFormatCode('# ##0');
|
||
$sheet->getStyle('A' . $first . ':B' . $lastRow)->getAlignment()
|
||
->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||
$sheet->getStyle('H' . $first . ':H' . $lastRow)->getBorders()->getAllBorders()
|
||
->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFBF8F00');
|
||
|
||
$widths = ['A' => 9, 'B' => 11, 'C' => 34, 'D' => 22, 'E' => 11,
|
||
'F' => 17, 'G' => 13, 'H' => 34, 'I' => 26];
|
||
foreach ($widths as $column => $width) {
|
||
$sheet->getColumnDimension($column)->setWidth($width);
|
||
}
|
||
|
||
$sheet->freezePane('A' . $first);
|
||
$sheet->setAutoFilter('A' . $headerRow . ':I' . $lastRow);
|
||
|
||
$summaryRow = $lastRow + 2;
|
||
$sheet->setCellValue('A' . $summaryRow, 'Összesen ' . $groupCount . ' gyártó-csoport, '
|
||
. array_sum(array_map('count', $groups)) . ' rekord. A csoportok többségénél mindkét névhez '
|
||
. 'tartoznak termékek, ezért az összevonás után is minden termék megmarad.');
|
||
$sheet->mergeCells('A' . $summaryRow . ':I' . $summaryRow);
|
||
$sheet->getStyle('A' . $summaryRow)->getFont()->setItalic(true);
|
||
}
|
||
}
|