387 lines
16 KiB
PHP
387 lines
16 KiB
PHP
<?php
|
||
|
||
/**
|
||
* mago_probe_purchases.php - PROVA in sola lettura: acquisti di Zibo Gomma in Mago4 (mescole).
|
||
*
|
||
* Non scrive niente nel database. Fa sempre il logoff da Mago.
|
||
*
|
||
* Uso (da terminale, nella cartella userarea):
|
||
* php mago_probe_purchases.php ultimi 24 mesi
|
||
* 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
|
||
*
|
||
* Stampa:
|
||
* 1. quali tabelle d'acquisto esistono, quanti record hanno e i loro campi
|
||
* 2. i tipi di documento d'acquisto (per capire qual è la fattura fornitore)
|
||
* 3. gli articoli acquistati, ordinati per valore, con UM, quantità, prezzo medio e ultimo
|
||
* 4. quali articoli somigliano alle mescole del portale (se il database è raggiungibile)
|
||
* 5. le ultime righe d'acquisto
|
||
* e salva un CSV con gli articoli in /tmp.
|
||
*/
|
||
|
||
if (PHP_SAPI !== 'cli') {
|
||
http_response_code(403);
|
||
exit("Solo da terminale\n");
|
||
}
|
||
|
||
require_once __DIR__ . '/class/MagoClient.php';
|
||
|
||
/* ---------------- opzioni ---------------- */
|
||
$opt = getopt('', ['months::', 'pages::', 'fields']);
|
||
$months = max(1, (int)($opt['months'] ?? 24));
|
||
$maxPages = max(1, (int)($opt['pages'] ?? 40));
|
||
$showFields = isset($opt['fields']);
|
||
$since = date('Y-m-d', strtotime("-$months months"));
|
||
|
||
/* ---------------- .env ---------------- */
|
||
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;
|
||
}
|
||
|
||
/* ---------------- stampa ---------------- */
|
||
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 field(array $keys, array $candidates): ?string
|
||
{
|
||
foreach ($candidates as $c) {
|
||
foreach ($keys as $k) if (strcasecmp($k, $c) === 0) return $k;
|
||
}
|
||
return null;
|
||
}
|
||
function norm(string $s): string
|
||
{
|
||
return strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $s));
|
||
}
|
||
|
||
$envFile = probe_load_env(__DIR__);
|
||
echo "Prova acquisti Mago4 · dal $since ($months mesi) · max " . ($maxPages * 200) . " righe\n";
|
||
echo ".env: " . ($envFile ?: 'non trovato, uso le variabili d\'ambiente') . "\n";
|
||
|
||
$mago = null;
|
||
$t0 = microtime(true);
|
||
|
||
try {
|
||
$mago = MagoClient::fromEnv();
|
||
$mago->login();
|
||
echo "Login Mago: OK\n";
|
||
|
||
/* =========================================================
|
||
* 1. TABELLE
|
||
* ========================================================= */
|
||
h('1. TABELLE D\'ACQUISTO IN MAGO');
|
||
$tables = [
|
||
'MA_PurchaseOrd' => 'Ordini a fornitore (testata)',
|
||
'MA_PurchaseOrdDetails' => 'Ordini a fornitore (righe)',
|
||
'MA_PurchaseOrdSummary' => 'Ordini a fornitore (totali)',
|
||
'MA_PurchaseDoc' => 'DDT / fatture fornitore (testata)',
|
||
'MA_PurchaseDocDetail' => 'DDT / fatture fornitore (righe)',
|
||
'MA_PurchaseDocSummary' => 'DDT / fatture fornitore (totali)',
|
||
'MA_Items' => 'Anagrafica articoli',
|
||
];
|
||
$found = [];
|
||
foreach ($tables as $t => $label) {
|
||
try {
|
||
$r = $mago->getDataTable($t, ['recordsPerPage' => 1]);
|
||
$keys = $r['records'] ? array_keys($r['records'][0]) : [];
|
||
$found[$t] = ['keys' => $keys, 'sample' => $r['records'][0] ?? []];
|
||
echo pad($t, 24) . pad($label, 36) . pad(num($r['total']) . ' record', 16, true) . ' ' . count($keys) . " campi\n";
|
||
} catch (Throwable $e) {
|
||
echo pad($t, 24) . pad($label, 36) . " NON DISPONIBILE: " . cut($e->getMessage(), 80) . "\n";
|
||
}
|
||
}
|
||
|
||
foreach ($found as $t => $f) {
|
||
if (!$f['keys']) continue;
|
||
echo "\n-- $t: campi --\n";
|
||
if ($showFields) {
|
||
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";
|
||
}
|
||
}
|
||
|
||
/* scelgo la fonte: prima i documenti fornitore (prezzo pagato), poi gli ordini */
|
||
$src = null;
|
||
foreach ([['MA_PurchaseDoc', 'MA_PurchaseDocDetail', 'Documenti fornitore'], ['MA_PurchaseOrd', 'MA_PurchaseOrdDetails', 'Ordini a fornitore']] as $c) {
|
||
if (!empty($found[$c[0]]['keys']) && !empty($found[$c[1]]['keys'])) {
|
||
$src = $c;
|
||
break;
|
||
}
|
||
}
|
||
if (!$src) {
|
||
h('Nessuna coppia testata/righe d\'acquisto leggibile: mi fermo qui');
|
||
exit(0);
|
||
}
|
||
[$headT, $detT, $srcLabel] = $src;
|
||
$hk = $found[$headT]['keys'];
|
||
$dk = $found[$detT]['keys'];
|
||
|
||
$hId = field($hk, ['PurchaseDocId', 'PurchaseOrdId']);
|
||
$dId = field($dk, ['PurchaseDocId', 'PurchaseOrdId']);
|
||
$hDate = field($hk, ['DocumentDate', 'OrderDate', 'PostingDate']);
|
||
$hNo = field($hk, ['DocNo', 'InternalOrdNo', 'SupplierDocNo']);
|
||
$hSupp = field($hk, ['Supplier', 'CustSupp']);
|
||
$hType = field($hk, ['DocumentType']);
|
||
$dItem = field($dk, ['Item']);
|
||
$dDesc = field($dk, ['Description']);
|
||
$dUom = field($dk, ['UoM', 'BaseUoM']);
|
||
$dQty = field($dk, ['Qty']);
|
||
$dVal = field($dk, ['TaxableAmount']);
|
||
$dUnit = field($dk, ['UnitValue']);
|
||
$dMod = field($dk, ['TBModified', 'TbModified']);
|
||
|
||
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) {
|
||
h('Mancano campi indispensabili (id, data, articolo, quantità): rilancia con --fields e mandami l\'output');
|
||
exit(0);
|
||
}
|
||
|
||
/* =========================================================
|
||
* 2. TESTATE 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] : '');
|
||
}
|
||
}
|
||
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);
|
||
}
|
||
|
||
/* =========================================================
|
||
* 3. RIGHE
|
||
* ========================================================= */
|
||
$filters = $dMod ? [['key' => $dMod, 'value' => $since, 'operator' => '>=']] : [];
|
||
$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++;
|
||
|
||
$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);
|
||
|
||
$last[] = [$hd['date'], $hd['no'], $hd['supp'], $hd['type'], $item, $dDesc ? $rec[$dDesc] : '', $dUom ? $rec[$dUom] : '', $qty, $qty ? $val / $qty : 0, $val];
|
||
}
|
||
uasort($items, fn($a, $b) => $b['val'] <=> $a['val']);
|
||
|
||
/* nomi fornitori */
|
||
$suppNames = [];
|
||
$allSupp = [];
|
||
foreach ($items as $it) foreach (array_keys($it['supp']) as $s) $allSupp[$s] = ($allSupp[$s] ?? 0) + $it['val'];
|
||
arsort($allSupp);
|
||
foreach (array_slice(array_keys($allSupp), 0, 30) as $s) {
|
||
if ($s === '') continue;
|
||
try {
|
||
$r = $mago->getDataTable('MA_CustSupp', ['filters' => [['key' => 'CustSupp', 'value' => $s, 'operator' => '=']], 'recordsPerPage' => 5]);
|
||
foreach ($r['records'] as $c) {
|
||
$suppNames[$s] = $c['CompanyName'] ?? $s;
|
||
if (($c['CustSuppType'] ?? '') != 3211264) break; // preferisco il record fornitore
|
||
}
|
||
} catch (Throwable $e) {
|
||
}
|
||
}
|
||
|
||
h("3. ARTICOLI ACQUISTATI: " . num(count($items)) . " articoli su " . num($lines) . " righe" . ($skipped ? " (" . num($skipped) . " righe fuori periodo)" : ''));
|
||
echo pad('Articolo', 16) . pad('Descrizione', 30) . pad('UM', 6) . pad('Quantità', 12, true) . pad('Valore €', 13, true) .
|
||
pad('€/UM medio', 11, true) . pad('Ultimo €', 10, true) . ' ' . pad('Ultimo', 10) . " Fornitore\n";
|
||
echo str_repeat('-', 140) . "\n";
|
||
foreach (array_slice($items, 0, 80) as $it) {
|
||
$s = array_keys($it['supp']);
|
||
$sn = $suppNames[$s[0]] ?? $s[0];
|
||
echo pad($it['item'], 16) . pad($it['desc'], 30) . pad(implode('/', array_keys($it['uom'])), 6) .
|
||
pad(num($it['qty'], 1), 12, true) . pad(num($it['val'], 0), 13, true) .
|
||
pad($it['qty'] ? num($it['val'] / $it['qty'], 3) : '-', 11, true) . pad(num($it['last_price'], 3), 10, true) .
|
||
' ' . pad($it['last'], 10) . ' ' . cut($sn, 26) . (count($s) > 1 ? ' +' . (count($s) - 1) : '') . "\n";
|
||
}
|
||
if (count($items) > 80) echo "… e altri " . num(count($items) - 80) . " articoli (tutti nel CSV)\n";
|
||
|
||
/* =========================================================
|
||
* 4. MESCOLE DEL PORTALE
|
||
* ========================================================= */
|
||
h('4. ARTICOLI CHE SOMIGLIANO ALLE MESCOLE DEL PORTALE');
|
||
$pdo = null;
|
||
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) {
|
||
echo "Database del portale non raggiungibile da qui: confronto saltato (lo faremo dopo, dal CSV).\n";
|
||
} else {
|
||
$names = [];
|
||
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) {
|
||
}
|
||
echo "Mescole nel portale: " . num($pdo->query("SELECT COUNT(*) FROM mescole")->fetchColumn()) . " · nomi confrontati: " . num(count($names)) . "\n\n";
|
||
|
||
$hit = 0;
|
||
$exact = 0;
|
||
foreach ($items as $it) {
|
||
$ni = norm($it['item']);
|
||
$nd = norm($it['desc']);
|
||
$match = null;
|
||
$how = '';
|
||
if (isset($names[$ni])) {
|
||
$match = $names[$ni];
|
||
$how = 'codice = ' . $match['via'];
|
||
$exact++;
|
||
} else {
|
||
foreach ($names as $n => $m) {
|
||
if (strlen($n) >= 4 && (strpos($ni, $n) !== false || strpos($nd, $n) !== false)) {
|
||
$match = $m;
|
||
$how = 'contenuto in ' . (strpos($ni, $n) !== false ? 'codice' : 'descrizione') . ' (' . $m['via'] . ')';
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!$match) continue;
|
||
$hit++;
|
||
if ($hit <= 60) {
|
||
echo pad($it['item'], 16) . pad($it['desc'], 32) . ' → ' . pad($match['name'], 22) . pad(implode('/', array_keys($it['uom'])), 6) .
|
||
pad($it['qty'] ? num($it['val'] / $it['qty'], 3) . ' €' : '-', 12, true) . ' ' . $how . "\n";
|
||
}
|
||
}
|
||
echo "\nArticoli collegabili a una mescola: " . num($hit) . " (di cui " . num($exact) . " con codice identico) su " . num(count($items)) . "\n";
|
||
}
|
||
|
||
/* =========================================================
|
||
* 5. ULTIME RIGHE
|
||
* ========================================================= */
|
||
h('5. ULTIME 30 RIGHE D\'ACQUISTO');
|
||
usort($last, fn($a, $b) => strcmp($b[0], $a[0]));
|
||
foreach (array_slice($last, 0, 30) as $l) {
|
||
echo pad($l[0], 11) . pad($l[1], 12) . pad($suppNames[$l[2]] ?? $l[2], 22) . pad($l[4], 14) . pad($l[5], 28) .
|
||
pad($l[6], 5) . pad(num($l[7], 1), 10, true) . pad(num($l[8], 3), 10, true) . pad(num($l[9], 2), 12, true) . "\n";
|
||
}
|
||
|
||
/* CSV */
|
||
$csv = sys_get_temp_dir() . '/mago_acquisti_articoli_' . date('Ymd_His') . '.csv';
|
||
$fh = fopen($csv, 'w');
|
||
fputcsv($fh, ['articolo', 'descrizione', 'um', 'righe', 'quantita', 'valore', 'prezzo_medio', 'ultimo_prezzo', 'ultima_data', 'fornitori'], ';');
|
||
foreach ($items as $it) {
|
||
fputcsv($fh, [
|
||
$it['item'],
|
||
$it['desc'],
|
||
implode('/', array_keys($it['uom'])),
|
||
$it['n'],
|
||
number_format($it['qty'], 3, ',', ''),
|
||
number_format($it['val'], 2, ',', ''),
|
||
$it['qty'] ? number_format($it['val'] / $it['qty'], 4, ',', '') : '',
|
||
number_format($it['last_price'], 4, ',', ''),
|
||
$it['last'],
|
||
implode(' | ', array_map(fn($s) => $suppNames[$s] ?? $s, array_keys($it['supp']))),
|
||
], ';');
|
||
}
|
||
fclose($fh);
|
||
h('FINE');
|
||
echo "CSV articoli: $csv\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";
|
||
}
|