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>
This commit is contained in:
E98Developer 2026-08-28 07:05:35 +02:00
parent 4198c03f5a
commit 87cab4cad5
2 changed files with 95 additions and 18 deletions

View File

@ -33,8 +33,6 @@ class ProducersDedupeReport extends Command
private const TYPE_CERTAIN = 'biztos';
private const TYPE_REVIEW = 'ellenőrizendő';
public function handle(ProducerDeduplicator $deduplicator): int
{
$certain = $deduplicator->duplicateGroups();
@ -107,12 +105,14 @@ private function writeIntro($sheet, bool $hasLoose): void
$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: '
$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, HAGYD ÜRESEN akkor nem nyúlunk hozzájuk.');
. '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');
@ -138,24 +138,50 @@ private function writeHeader($sheet, int $headerRow): void
$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 ([self::TYPE_CERTAIN => $certain, self::TYPE_REVIEW => $loose] as $type => $groups) {
foreach ($groups as $members) {
foreach ($certain as $members) {
$groupIndex++;
$row = $this->writeGroup($sheet, $row, $groupIndex, $type, $members);
$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): int
private function writeGroup($sheet, int $row, int $groupIndex, string $type, array $members, bool $isReview): int
{
$isReview = $type === self::TYPE_REVIEW;
$proposed = trim((string) $members[0]['producer']->name);
$shade = $isReview ? 'FFFDF2E9' : ($groupIndex % 2 === 0 ? 'FFF2F2F2' : 'FFFFFFFF');
$groupFirstRow = $row;

View File

@ -32,6 +32,17 @@ class ProducerDeduplicator
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).
@ -129,6 +140,51 @@ public function looseCandidateGroups(): array
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.
@ -137,12 +193,7 @@ public function looseCandidateGroups(): array
*/
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',
]);
$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';