d2d.emegrendeles.hu/app/Console/Commands/ProducersDedupeReport.php
E98Developer 87cab4cad5 ADD EV3-357 a laza javaslatok eltérés-jelleg szerint kategorizálva
101 javaslat egy kupacban túl sok egy kézi átnézéshez, ráadásul nagyon eltérő
mérlegelést igényelnek: a "Békás Kft." és a "Békás Kft" között nincs mit
eldönteni, a "Dénes Natura" és a "Dénes Natura Kft" között viszont van.

- ProducerDeduplicator::differenceCategory(): a LEGKEVÉSBÉ agresszív átalakítás,
  ami már egy kulcsra hozza a csoport tagjait - csak írásjel / egybeírás /
  ékezet / cégforma / vegyes.
- a riport ebben a sorrendben írja ki a csoportokat (elöl a gyakorlatilag
  eldöntött esetek), és a "csak írásjel" kategóriánál előre beírja a javaslatot;
  a többinél a végleges név üresen marad, a javaslat a Megjegyzés oszlopban.

A d2d másolaton: 14 csak írásjel (előre kitöltve, azonnal végrehajtható),
7 egybeírás, 8 ékezet, 72 cégforma - utóbbi 87 vár a megrendelő döntésére.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 07:05:35 +02:00

292 lines
13 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 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);
}
}