diff --git a/public/userarea/mago_probe_mescole.php b/public/userarea/mago_probe_mescole.php new file mode 100644 index 00000000..df6623e1 --- /dev/null +++ b/public/userarea/mago_probe_mescole.php @@ -0,0 +1,310 @@ + /tmp/probe_mescole.txt). + * CSV: /tmp/mago_mescole_descrizioni_*.csv e /tmp/mago_mescole_righe_*.csv + */ + +if (PHP_SAPI !== 'cli') { + http_response_code(403); + exit("Solo da terminale\n"); +} +require_once __DIR__ . '/class/MagoClient.php'; + +$opt = getopt('', ['months::', 'item::', 'types::']); +$months = max(1, (int)($opt['months'] ?? 24)); +$itemCode = (string)($opt['item'] ?? 'MESCOLE'); +$docTypes = array_values(array_filter(array_map('trim', explode(',', (string)($opt['types'] ?? '9830401'))), 'strlen')); +$since = date('Y-m-d', strtotime("-$months months")); +const MAX_PAGES = 100; + +function probe_load_env(string $dir): ?string +{ + for ($i = 0; $i < 5; $i++) { + if (is_file($dir . '/vendor/autoload.php')) require_once $dir . '/vendor/autoload.php'; + if (is_file($dir . '/.env')) { + if (class_exists('Dotenv\\Dotenv')) { + Dotenv\Dotenv::createImmutable($dir)->safeLoad(); + } else { + foreach (file($dir . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + if ($line[0] === '#' || strpos($line, '=') === false) continue; + [$k, $v] = array_map('trim', explode('=', $line, 2)); + $v = trim($v, "\"'"); + $_ENV[$k] = $v; + putenv("$k=$v"); + } + } + return $dir . '/.env'; + } + $dir = dirname($dir); + } + return null; +} +function h(string $t): void +{ + echo "\n" . str_repeat('=', 78) . "\n " . $t . "\n" . str_repeat('=', 78) . "\n"; +} +function cut($s, int $n): string +{ + $s = preg_replace('/\s+/', ' ', trim((string)$s)); + return mb_strlen($s) > $n ? mb_substr($s, 0, $n - 1) . '…' : $s; +} +function pad($s, int $n, bool $right = false): string +{ + $s = cut($s, $n); + $p = str_repeat(' ', max(0, $n - mb_strlen($s))); + return $right ? $p . $s : $s . $p; +} +function num($v, int $d = 0): string +{ + return number_format((float)$v, $d, ',', '.'); +} +function norm(string $s): string +{ + return strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $s)); +} +/** descrizione "pulita" per raggruppare: maiuscole, spazi singoli, senza spessori/misure finali tipo "X 9mm spessore" */ +function descKey(string $s): string +{ + $s = strtoupper(preg_replace('/\s+/', ' ', trim($s))); + return $s; +} + +probe_load_env(__DIR__); +echo "Prova MESCOLE in Mago4 · articolo $itemCode · tipi " . implode(',', $docTypes) . " · dal $since\n"; + +$mago = null; +$t0 = microtime(true); +try { + $mago = MagoClient::fromEnv(); + $mago->login(); + echo "Login Mago: OK\n"; + + $fields = [ + 'PurchaseDocId', + 'Line', + 'DocumentType', + 'DocumentDate', + 'DocNo', + 'Supplier', + 'Item', + 'Description', + 'Department', + 'SupplierCode', + 'ReferenceDocNo', + 'UoM', + 'Qty', + 'UnitValue', + 'TaxableAmount', + 'DiscountAmount', + 'Lot', + 'SupplierLotNo', + 'Notes', + 'PurchaseOrdNo' + ]; + + $rows = []; + foreach ($docTypes as $tp) { + $flt = [ + ['key' => 'Item', 'value' => $itemCode, 'operator' => '='], + ['key' => 'DocumentDate', 'value' => $since, 'operator' => '>='], + ['key' => 'DocumentType', 'value' => $tp, 'operator' => '='], + ]; + for ($page = 1; $page <= MAX_PAGES; $page++) { + $tq = microtime(true); + $r = $mago->getDataTable('MA_PurchaseDocDetail', ['fields' => $fields, 'filters' => $flt, 'recordsPerPage' => 200, 'pageNumber' => $page]); + fwrite(STDERR, sprintf(" tipo %s · pagina %d: %d righe su %s (%.1f s)\n", $tp, $page, $r['count'], num($r['total']), microtime(true) - $tq)); + foreach ($r['records'] as $rec) { + $rec['_type'] = $tp; + $rows[] = $rec; + } + if (!$r['records'] || $page * 200 >= $r['total']) break; + } + } + + /* nomi fornitori */ + $supp = []; + foreach ($rows as $x) $supp[(string)($x['Supplier'] ?? '')] = 1; + $suppNames = []; + foreach (array_keys($supp) as $s) { + if ($s === '') continue; + try { + $r = $mago->getDataTable('MA_CustSupp', ['fields' => ['CustSuppType', 'CustSupp', 'CompanyName'], 'filters' => [['key' => 'CustSupp', 'value' => $s, 'operator' => '=']], 'recordsPerPage' => 5]); + foreach ($r['records'] as $c) { + $suppNames[$s] = $c['CompanyName'] ?: $s; + if ((string)$c['CustSuppType'] !== '3211264') break; + } + } catch (Throwable $e) { + } + } + + h('1. RIGHE LETTE: ' . num(count($rows))); + if (!$rows) exit(0); + + /* quanto sono compilati i campi che potrebbero identificare la mescola */ + $fill = []; + foreach (['Description', 'Department', 'SupplierCode', 'ReferenceDocNo', 'Notes', 'Lot', 'SupplierLotNo'] as $f) { + $n = 0; + $vals = []; + foreach ($rows as $x) { + $v = trim((string)($x[$f] ?? '')); + if ($v !== '') { + $n++; + $vals[cut($v, 40)] = 1; + } + } + echo pad($f, 16) . pad(num($n) . ' righe compilate', 22, true) . ' ' . num(count($vals)) . " valori diversi" . + ($vals ? ' es: ' . cut(implode(' · ', array_slice(array_keys($vals), 0, 4)), 90) : '') . "\n"; + } + $uoms = []; + foreach ($rows as $x) $uoms[strtoupper(trim((string)($x['UoM'] ?? '')))] = ($uoms[strtoupper(trim((string)($x['UoM'] ?? '')))] ?? 0) + 1; + echo "UM: " . implode(', ', array_map(fn($k, $v) => "$k ($v)", array_keys($uoms), $uoms)) . "\n"; + + /* raggruppo per descrizione */ + $g = []; + foreach ($rows as $x) { + $k = descKey((string)($x['Description'] ?? '')); + $qty = MagoClient::toDecimal($x['Qty']); + $val = MagoClient::toDecimal($x['TaxableAmount']); + if ($val == 0) $val = $qty * MagoClient::toDecimal($x['UnitValue']); + $d = MagoClient::toMysqlDate($x['DocumentDate']); + $a = &$g[$k]; + $a['desc'] = $k; + $a['n'] = ($a['n'] ?? 0) + 1; + $a['qty'] = ($a['qty'] ?? 0) + $qty; + $a['val'] = ($a['val'] ?? 0) + $val; + $a['supp'][$suppNames[$x['Supplier'] ?? ''] ?? ($x['Supplier'] ?? '')] = 1; + $a['dep'][trim((string)($x['Department'] ?? ''))] = 1; + $a['sc'][trim((string)($x['SupplierCode'] ?? ''))] = 1; + if (($a['last'] ?? '') <= $d) { + $a['last'] = $d; + $a['last_price'] = $qty ? $val / $qty : 0; + } + $a['first'] = isset($a['first']) ? min($a['first'], $d) : $d; + unset($a); + } + uasort($g, fn($a, $b) => $b['val'] <=> $a['val']); + + /* mescole del portale */ + $pdo = null; + $names = []; + try { + foreach (['/class/DBHandlerSelect.php', '/include/DBHandlerSelect.php', '/class/DBHandler.php'] as $f) { + if (!class_exists('DBHandlerSelect') && is_file(__DIR__ . $f)) { + ob_start(); + require_once __DIR__ . $f; + ob_end_clean(); + } + } + if (class_exists('DBHandlerSelect')) $pdo = DBHandlerSelect::getInstance()->getConnection(); + } catch (Throwable $e) { + $pdo = null; + } + if ($pdo) { + foreach ($pdo->query("SELECT id, nome, nomeuscita FROM mescole") as $m) { + foreach (['nome', 'nomeuscita'] as $k) { + $n = norm((string)$m[$k]); + if (strlen($n) >= 3) $names[$n] = ['id' => $m['id'], 'name' => $m['nome'], 'via' => $k]; + } + } + try { + foreach ($pdo->query("SELECT sn.idmescola, sn.supplier_mix_name, m.nome FROM mescole_supplier_names sn JOIN mescole m ON m.id = sn.idmescola") as $m) { + $n = norm((string)$m['supplier_mix_name']); + if (strlen($n) >= 3) $names[$n] = ['id' => $m['idmescola'], 'name' => $m['nome'], 'via' => 'nome fornitore']; + } + } catch (Throwable $e) { + } + uksort($names, fn($a, $b) => strlen($b) <=> strlen($a)); // prima i nomi più lunghi + } + $matchOf = function (string $desc) use ($names) { + $nd = norm($desc); + foreach ($names as $n => $m) { + if (strpos($nd, $n) !== false) return $m; + } + return null; + }; + + h('2. DESCRIZIONI DIVERSE: ' . num(count($g)) . ($pdo ? ' · mescole portale confrontate: ' . num(count($names)) . ' nomi' : ' · database portale NON raggiungibile')); + echo pad('Descrizione', 40) . pad('Righe', 7, true) . pad('Kg', 12, true) . pad('Valore €', 12, true) . pad('€/kg', 8, true) . pad('Ultimo', 8, true) . + ' ' . pad('Ult. data', 10) . ' ' . pad('Mescola portale', 20) . " Fornitore / Department\n"; + echo str_repeat('-', 150) . "\n"; + $matched = 0; + $matchedVal = 0; + $totVal = 0; + foreach ($g as $a) { + $m = $pdo ? $matchOf($a['desc']) : null; + $totVal += $a['val']; + if ($m) { + $matched++; + $matchedVal += $a['val']; + } + $g[$a['desc']]['match'] = $m; + } + foreach (array_slice($g, 0, 100) as $a) { + $dep = implode(',', array_filter(array_keys($a['dep']))); + echo pad($a['desc'], 40) . pad(num($a['n']), 7, true) . pad(num($a['qty']), 12, true) . pad(num($a['val']), 12, true) . + pad($a['qty'] ? num($a['val'] / $a['qty'], 3) : '-', 8, true) . pad(num($a['last_price'], 3), 8, true) . ' ' . pad($a['last'], 10) . + ' ' . pad($a['match'] ? $a['match']['name'] : ($pdo ? '—' : ''), 20) . ' ' . cut(implode(' | ', array_keys($a['supp'])), 30) . ($dep ? ' · dep: ' . cut($dep, 20) : '') . "\n"; + } + if (count($g) > 100) echo "… e altre " . num(count($g) - 100) . " descrizioni (tutte nel CSV)\n"; + if ($pdo) echo "\nDescrizioni abbinate a una mescola: " . num($matched) . " su " . num(count($g)) . " · " . ($totVal ? num($matchedVal / $totVal * 100, 1) : 0) . "% del valore\n"; + + /* esempi grezzi */ + h('3. 15 RIGHE D\'ESEMPIO (tutti i campi letti)'); + foreach (array_slice($rows, -15) as $x) { + $out = []; + foreach ($fields as $f) if (isset($x[$f]) && trim((string)$x[$f]) !== '' && !in_array($f, ['PurchaseDocId', 'Item'], true)) $out[] = $f . '=' . cut($x[$f], 40); + echo ' · ' . implode(' | ', $out) . "\n"; + } + + /* CSV */ + $stamp = date('Ymd_His'); + $csv1 = sys_get_temp_dir() . "/mago_mescole_descrizioni_$stamp.csv"; + $fh = fopen($csv1, 'w'); + fputcsv($fh, ['descrizione', 'righe', 'kg', 'valore', 'prezzo_medio', 'ultimo_prezzo', 'prima_data', 'ultima_data', 'mescola_portale', 'fornitori', 'department', 'codice_fornitore'], ';'); + foreach ($g as $a) { + fputcsv($fh, [ + $a['desc'], + $a['n'], + number_format($a['qty'], 3, ',', ''), + number_format($a['val'], 2, ',', ''), + $a['qty'] ? number_format($a['val'] / $a['qty'], 4, ',', '') : '', + number_format($a['last_price'], 4, ',', ''), + $a['first'], + $a['last'], + $a['match']['name'] ?? '', + implode(' | ', array_keys($a['supp'])), + implode(' | ', array_filter(array_keys($a['dep']))), + implode(' | ', array_filter(array_keys($a['sc']))) + ], ';'); + } + fclose($fh); + $csv2 = sys_get_temp_dir() . "/mago_mescole_righe_$stamp.csv"; + $fh = fopen($csv2, 'w'); + fputcsv($fh, array_merge(['fornitore_nome'], $fields), ';'); + foreach ($rows as $x) { + $line = [$suppNames[$x['Supplier'] ?? ''] ?? ($x['Supplier'] ?? '')]; + foreach ($fields as $f) $line[] = is_scalar($x[$f] ?? '') ? (string)($x[$f] ?? '') : json_encode($x[$f]); + fputcsv($fh, $line, ';'); + } + fclose($fh); + h('FINE'); + echo "CSV descrizioni: $csv1\nCSV righe: $csv2\n"; +} catch (Throwable $e) { + echo "\nERRORE: " . $e->getMessage() . "\n"; +} finally { + if ($mago) { + $ok = $mago->logoff(); + echo "Logoff Mago: " . ($ok ? 'OK' : 'NON riuscito → ' . $mago->getLastLogoffError()) . "\n"; + } + echo "Durata: " . round(microtime(true) - $t0, 1) . " s\n"; +} diff --git a/public/userarea/mago_probe_purchases.php b/public/userarea/mago_probe_purchases.php index fcfe3016..0de28f81 100644 --- a/public/userarea/mago_probe_purchases.php +++ b/public/userarea/mago_probe_purchases.php @@ -10,6 +10,9 @@ * php mago_probe_purchases.php --months=36 periodo diverso * php mago_probe_purchases.php --pages=60 righe lette al massimo = pagine × 200 (default 40) * php mago_probe_purchases.php --fields stampa anche tutti i campi con un valore d'esempio + * php mago_probe_purchases.php --types=9830401,9830402 tipi documento da leggere (default fattura fornitore) + * + * L'avanzamento (pagina per pagina) esce a video anche se l'output va in un file. * * Stampa: * 1. quali tabelle d'acquisto esistono, quanti record hanno e i loro campi @@ -28,10 +31,12 @@ if (PHP_SAPI !== 'cli') { require_once __DIR__ . '/class/MagoClient.php'; /* ---------------- opzioni ---------------- */ -$opt = getopt('', ['months::', 'pages::', 'fields']); +$opt = getopt('', ['months::', 'pages::', 'fields', 'types::']); $months = max(1, (int)($opt['months'] ?? 24)); $maxPages = max(1, (int)($opt['pages'] ?? 40)); $showFields = isset($opt['fields']); +// tipi documento da leggere: 9830401 = fattura fornitore (dal primo test). Più tipi separati da virgola. +$docTypes = array_values(array_filter(array_map('trim', explode(',', (string)($opt['types'] ?? '9830401'))), 'strlen')); $since = date('Y-m-d', strtotime("-$months months")); /* ---------------- .env ---------------- */ @@ -127,13 +132,11 @@ try { } } - foreach ($found as $t => $f) { - if (!$f['keys']) continue; - echo "\n-- $t: campi --\n"; - if ($showFields) { + if ($showFields) { + foreach ($found as $t => $f) { + if (!$f['keys']) continue; + echo "\n-- $t: campi --\n"; foreach ($f['sample'] as $k => $v) echo ' ' . pad($k, 32) . cut(is_scalar($v) ? (string)$v : json_encode($v), 60) . "\n"; - } else { - echo ' ' . wordwrap(implode(', ', $f['keys']), 74, "\n ") . "\n"; } } @@ -170,80 +173,87 @@ try { echo "\nFonte usata: $srcLabel ($headT + $detT)\n"; echo "Campi riconosciuti: id=$hId/$dId data=$hDate n°=$hNo fornitore=$hSupp tipo=" . ($hType ?: '-') . " | articolo=$dItem UM=$dUom qtà=$dQty imponibile=" . ($dVal ?: '-') . " prezzo=" . ($dUnit ?: '-') . "\n"; - if (!$hId || !$dId || !$hDate || !$dItem || !$dQty) { + if (!$dId || !$dItem || !$dQty) { h('Mancano campi indispensabili (id, data, articolo, quantità): rilancia con --fields e mandami l\'output'); exit(0); } /* ========================================================= - * 2. TESTATE DEL PERIODO + * 2. TIPI DI DOCUMENTO DEL PERIODO * ========================================================= */ - $heads = []; - $types = []; - foreach ($mago->iterate($headT, ['filters' => [['key' => $hDate, 'value' => $since, 'operator' => '>=']]], $maxPages) as $rec) { - $id = (string)$rec[$hId]; - $heads[$id] = [ - 'date' => MagoClient::toMysqlDate($rec[$hDate] ?? null), - 'no' => $hNo ? (string)$rec[$hNo] : '', - 'supp' => $hSupp ? (string)$rec[$hSupp] : '', - 'type' => $hType ? (string)$rec[$hType] : '', - ]; - if ($hType) { - $tp = (string)$rec[$hType]; - $types[$tp]['n'] = ($types[$tp]['n'] ?? 0) + 1; - $types[$tp]['ex'] = $types[$tp]['ex'] ?? ($hNo ? $rec[$hNo] : ''); + $dType = field($dk, ['DocumentType']); + $dDate = field($dk, ['DocumentDate', 'OrderDate']); + $dNo = field($dk, ['DocNo', 'PurchaseOrdNo']); + $dSupp = field($dk, ['Supplier']); + + h('2. TIPI DI DOCUMENTO DEL PERIODO'); + $known = ['9830400' => 'DDT fornitore?', '9830401' => 'Fattura fornitore?', '9830402' => 'Nota di credito?']; + if ($hType && $hDate) { + foreach ($known as $tp => $lbl) { + try { + $n = $mago->count($headT, [['key' => $hDate, 'value' => $since, 'operator' => '>='], ['key' => $hType, 'value' => $tp, 'operator' => '=']]); + echo pad($tp, 12) . pad($lbl, 22) . pad(num($n), 10, true) . "\n"; + } catch (Throwable $e) { + echo pad($tp, 12) . " errore: " . cut($e->getMessage(), 60) . "\n"; + } } } - h("2. DOCUMENTI DEL PERIODO: " . num(count($heads))); - if ($types) { - echo pad('Tipo documento', 16) . pad('Quanti', 10, true) . " Esempio n°\n"; - arsort($types); - foreach ($types as $tp => $x) echo pad($tp, 16) . pad(num($x['n']), 10, true) . ' ' . $x['ex'] . "\n"; - echo "(Per le vendite: 3407874 fattura, 3407883 acconto, 3407876 nota credito, 3407873 DDT. Gli acquisti hanno codici simili.)\n"; - } - if (!$heads) { - echo "Nessun documento nel periodo.\n"; - exit(0); - } + echo "Righe lette solo dai tipi: " . implode(', ', $docTypes) . " (cambia con --types=…)\n"; /* ========================================================= - * 3. RIGHE + * 3. RIGHE (solo i campi che servono, filtro su data e tipo documento) * ========================================================= */ - $filters = $dMod ? [['key' => $dMod, 'value' => $since, 'operator' => '>=']] : []; + if (!$dDate) { + h('Nella tabella righe manca la data documento: rilancia con --fields e mandami l\'output'); + exit(0); + } + $fields = array_values(array_filter([$dId, $dDate, $dNo, $dSupp, $dType, $dItem, $dDesc, $dUom, $dQty, $dVal, $dUnit, field($dk, ['LineType'])])); $items = []; $last = []; $lines = 0; - $skipped = 0; - foreach ($mago->iterate($detT, ['filters' => $filters], $maxPages) as $rec) { - $id = (string)$rec[$dId]; - if (!isset($heads[$id])) { - $skipped++; - continue; - } - $item = trim((string)($rec[$dItem] ?? '')); - $qty = MagoClient::toDecimal($rec[$dQty] ?? 0); - if ($item === '' || $qty == 0) continue; - $val = $dVal ? MagoClient::toDecimal($rec[$dVal]) : 0; - if ($val == 0 && $dUnit) $val = $qty * MagoClient::toDecimal($rec[$dUnit]); - $hd = $heads[$id]; - $lines++; + $read = 0; + $total = null; + foreach ($docTypes as $tp) { + $flt = [['key' => $dDate, 'value' => $since, 'operator' => '>=']]; + if ($dType && $tp !== '') $flt[] = ['key' => $dType, 'value' => $tp, 'operator' => '=']; + for ($page = 1; $page <= $maxPages; $page++) { + $tq = microtime(true); + $r = $mago->getDataTable($detT, ['fields' => $fields, 'filters' => $flt, 'recordsPerPage' => 200, 'pageNumber' => $page]); + $total = $r['total']; + fwrite(STDERR, sprintf(" tipo %s · pagina %d: %d righe su %s (%.1f s)\n", $tp, $page, $r['count'], num($total), microtime(true) - $tq)); + if (!$r['records']) break; + foreach ($r['records'] as $rec) { + $read++; + $item = trim((string)($rec[$dItem] ?? '')); + $qty = MagoClient::toDecimal($rec[$dQty] ?? 0); + if ($item === '' || $qty == 0) continue; + $val = $dVal ? MagoClient::toDecimal($rec[$dVal]) : 0; + if ($val == 0 && $dUnit) $val = $qty * MagoClient::toDecimal($rec[$dUnit]); + $date = MagoClient::toMysqlDate($rec[$dDate] ?? null); + $supp = $dSupp ? (string)$rec[$dSupp] : ''; + $lines++; - $it = &$items[$item]; - $it['item'] = $item; - $it['desc'] = $it['desc'] ?? ($dDesc ? (string)$rec[$dDesc] : ''); - $it['uom'][$dUom ? strtoupper(trim((string)$rec[$dUom])) : '?'] = 1; - $it['n'] = ($it['n'] ?? 0) + 1; - $it['qty'] = ($it['qty'] ?? 0) + $qty; - $it['val'] = ($it['val'] ?? 0) + $val; - $it['supp'][$hd['supp']] = 1; - if (($it['last'] ?? '') <= $hd['date']) { - $it['last'] = $hd['date']; - $it['last_price'] = $qty ? $val / $qty : 0; - } - unset($it); + $it = &$items[$item]; + $it['item'] = $item; + $it['desc'] = $it['desc'] ?? ($dDesc ? (string)$rec[$dDesc] : ''); + $it['uom'][$dUom ? strtoupper(trim((string)$rec[$dUom])) : '?'] = 1; + $it['n'] = ($it['n'] ?? 0) + 1; + $it['qty'] = ($it['qty'] ?? 0) + $qty; + $it['val'] = ($it['val'] ?? 0) + $val; + $it['supp'][$supp] = 1; + if (($it['last'] ?? '') <= $date) { + $it['last'] = $date; + $it['last_price'] = $qty ? $val / $qty : 0; + } + unset($it); - $last[] = [$hd['date'], $hd['no'], $hd['supp'], $hd['type'], $item, $dDesc ? $rec[$dDesc] : '', $dUom ? $rec[$dUom] : '', $qty, $qty ? $val / $qty : 0, $val]; + $last[] = [$date, $dNo ? $rec[$dNo] : '', $supp, $tp, $item, $dDesc ? $rec[$dDesc] : '', $dUom ? $rec[$dUom] : '', $qty, $qty ? $val / $qty : 0, $val]; + } + if ($page * 200 >= $total) break; + } + if ($total !== null && $total > $maxPages * 200) echo "ATTENZIONE tipo $tp: lette " . num($maxPages * 200) . " righe su " . num($total) . " (aumenta --pages)\n"; } + $skipped = 0; uasort($items, fn($a, $b) => $b['val'] <=> $a['val']); /* nomi fornitori */