d2d.emegrendeles.hu/app/Console/Commands/ProducersDedupeReport.php
E98Developer 2ced04c086 ADD EV3-357 gyártó duplikációk összevonása (producers:dedupe)
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>
2026-08-27 08:09:54 +02:00

212 lines
9.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
}