908 lines
43 KiB
PHP
908 lines
43 KiB
PHP
<?php
|
|
|
|
/**
|
|
* MagoSync - sincronizza dati Mago4 → tabelle MySQL locali (Mago in SOLA LETTURA).
|
|
*
|
|
* $sync = new MagoSync($pdo, MagoClient::fromEnv());
|
|
* $report = $sync->run(['limit' => 50]); // prova con pochi ordini
|
|
* $report = $sync->run(); // incrementale (TbModified)
|
|
* $report = $sync->run(['full' => true]); // tutto dal 2026-01-01
|
|
*
|
|
* Il logoff da Mago viene fatto sempre, anche in caso di errore.
|
|
*/
|
|
|
|
require_once __DIR__ . '/MagoClient.php';
|
|
|
|
class MagoSync
|
|
{
|
|
const CUSTOMER_TYPE = 3211264; // CustSuppType "Cliente" (verificato sui dati)
|
|
const DEFAULT_FROM = '2020-01-01'; // storico dal 2020 per confronto anni
|
|
const ENTITY_ORDERS = 'sale_ord';
|
|
const ENTITY_CUSTOMER = 'cust_supp';
|
|
const ENTITY_SUMMARY = 'sale_ord_summary';
|
|
const ENTITY_DETAILS = 'sale_ord_details';
|
|
const ENTITY_DOCS = 'sale_doc';
|
|
const ENTITY_DOC_SUMM = 'sale_doc_summary';
|
|
|
|
/** @var PDO */
|
|
private $pdo;
|
|
/** @var MagoClient */
|
|
private $mago;
|
|
/** @var callable|null */
|
|
private $logger;
|
|
|
|
public function __construct(PDO $pdo, MagoClient $mago, ?callable $logger = null)
|
|
{
|
|
$this->pdo = $pdo;
|
|
$this->mago = $mago;
|
|
$this->logger = $logger;
|
|
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
}
|
|
|
|
/**
|
|
* Opzioni:
|
|
* limit int|null numero massimo di ordini (modalità prova)
|
|
* full bool ignora l'incrementale e rilegge tutto dal 'from'
|
|
* from string data minima ordini (default 2026-01-01)
|
|
* skip_customers bool non sincronizzare l'anagrafica clienti
|
|
*/
|
|
public function run(array $opts = [])
|
|
{
|
|
$limit = isset($opts['limit']) && (int)$opts['limit'] > 0 ? (int)$opts['limit'] : null;
|
|
$full = !empty($opts['full']);
|
|
$from = !empty($opts['from']) ? $opts['from'] : self::DEFAULT_FROM;
|
|
|
|
$report = ['ok' => true, 'limit' => $limit, 'full' => $full, 'from' => $from];
|
|
|
|
if (!$this->acquireLock()) {
|
|
return ['ok' => false, 'message' => 'Sincronizzazione già in corso, riprova tra poco.'];
|
|
}
|
|
|
|
try {
|
|
$this->mago->login();
|
|
|
|
if (empty($opts['skip_customers'])) {
|
|
$report['clienti'] = $this->syncCustomers($full);
|
|
}
|
|
$report['ordini'] = $this->syncSaleOrders($from, $limit, $full);
|
|
$report['righe'] = $this->syncSaleOrderDetails($from, $full, (bool)$limit);
|
|
$report['importi'] = $this->syncSaleOrderSummaries($from, $full, (bool)$limit);
|
|
|
|
// Documenti di vendita (fatture / note di credito) per il fatturato
|
|
if ($this->tableExists('mago_sale_doc')) {
|
|
$report['documenti'] = $this->syncSaleDocs($from, $limit, $full);
|
|
$report['importi_documenti'] = $this->syncSaleDocSummaries($from, $full, (bool)$limit);
|
|
} else {
|
|
$report['documenti'] = 'tabella mago_sale_doc mancante: eseguire la migrazione Phinx';
|
|
$this->log('Documenti: tabella mago_sale_doc mancante, salto (eseguire la migrazione)');
|
|
}
|
|
|
|
// Coda "Nuovi da Mago" per la programmazione produzione (solo dati locali, nessuna chiamata a Mago)
|
|
if ($this->tableExists('mago_prod_queue')) {
|
|
require_once __DIR__ . '/MagoProduction.php';
|
|
$q = MagoProduction::refreshQueue($this->pdo);
|
|
$report['coda_produzione'] = $q;
|
|
$this->log("Coda produzione: {$q['nuove']} nuove, {$q['aggiornate']} aggiornate, {$q['chiuse']} chiuse, {$q['modificate']} modificate dopo l'import");
|
|
}
|
|
} catch (Throwable $e) {
|
|
$report['ok'] = false;
|
|
$report['message'] = $e->getMessage();
|
|
$this->log('ERRORE: ' . $e->getMessage());
|
|
} finally {
|
|
$logoffOk = $this->mago->logoff();
|
|
$report['logoff'] = $logoffOk ? 'OK' : ('non riuscito: ' . $this->mago->getLastLogoffError());
|
|
$this->releaseLock();
|
|
}
|
|
|
|
return $report;
|
|
}
|
|
|
|
/* =========================================================
|
|
* ORDINI CLIENTE
|
|
* ========================================================= */
|
|
|
|
public function syncSaleOrders($from, $limit = null, $full = false)
|
|
{
|
|
$t0 = microtime(true);
|
|
$state = $this->getState(self::ENTITY_ORDERS);
|
|
|
|
$filters = [['key' => 'OrderDate', 'value' => $from . 'T00:00:00', 'operator' => '>=']];
|
|
|
|
// Incrementale: solo i record modificati dall'ultima sincronizzazione completa
|
|
$incremental = !$full && !$limit && !empty($state['last_tb_modified']);
|
|
if ($incremental) {
|
|
$filters[] = [
|
|
'key' => 'TbModified',
|
|
'value' => date('Y-m-d\TH:i:s', strtotime($state['last_tb_modified'])),
|
|
'operator' => '>=',
|
|
];
|
|
}
|
|
|
|
$this->log('Ordini: ' . ($incremental ? 'incrementale da ' . $state['last_tb_modified'] : 'completa dal ' . $from) . ($limit ? " (prova: max $limit)" : ''));
|
|
|
|
$sql = "INSERT INTO mago_sale_ord (
|
|
sale_ord_id, internal_ord_no, external_ord_no, order_date,
|
|
expected_delivery_date, confirmed_delivery_date, compulsory_delivery_date,
|
|
customer, invoicing_customer, salesperson, area_manager, area,
|
|
payment, currency, our_reference, your_reference,
|
|
transport, port, package, storage, job,
|
|
delivered, invoiced, cancelled, is_blocked, printed, archived, open_order,
|
|
priority, notes, tb_created, tb_modified, raw_json, synced_at
|
|
) VALUES (
|
|
:sale_ord_id, :internal_ord_no, :external_ord_no, :order_date,
|
|
:expected_delivery_date, :confirmed_delivery_date, :compulsory_delivery_date,
|
|
:customer, :invoicing_customer, :salesperson, :area_manager, :area,
|
|
:payment, :currency, :our_reference, :your_reference,
|
|
:transport, :port, :package, :storage, :job,
|
|
:delivered, :invoiced, :cancelled, :is_blocked, :printed, :archived, :open_order,
|
|
:priority, :notes, :tb_created, :tb_modified, :raw_json, NOW()
|
|
)
|
|
ON DUPLICATE KEY UPDATE
|
|
internal_ord_no = VALUES(internal_ord_no),
|
|
external_ord_no = VALUES(external_ord_no),
|
|
order_date = VALUES(order_date),
|
|
expected_delivery_date = VALUES(expected_delivery_date),
|
|
confirmed_delivery_date = VALUES(confirmed_delivery_date),
|
|
compulsory_delivery_date = VALUES(compulsory_delivery_date),
|
|
customer = VALUES(customer),
|
|
invoicing_customer = VALUES(invoicing_customer),
|
|
salesperson = VALUES(salesperson),
|
|
area_manager = VALUES(area_manager),
|
|
area = VALUES(area),
|
|
payment = VALUES(payment),
|
|
currency = VALUES(currency),
|
|
our_reference = VALUES(our_reference),
|
|
your_reference = VALUES(your_reference),
|
|
transport = VALUES(transport),
|
|
port = VALUES(port),
|
|
package = VALUES(package),
|
|
storage = VALUES(storage),
|
|
job = VALUES(job),
|
|
delivered = VALUES(delivered),
|
|
invoiced = VALUES(invoiced),
|
|
cancelled = VALUES(cancelled),
|
|
is_blocked = VALUES(is_blocked),
|
|
printed = VALUES(printed),
|
|
archived = VALUES(archived),
|
|
open_order = VALUES(open_order),
|
|
priority = VALUES(priority),
|
|
notes = VALUES(notes),
|
|
tb_created = VALUES(tb_created),
|
|
tb_modified = VALUES(tb_modified),
|
|
raw_json = VALUES(raw_json),
|
|
synced_at = NOW()";
|
|
|
|
$stmt = $this->pdo->prepare($sql);
|
|
|
|
$count = 0;
|
|
$maxModified = $state['last_tb_modified'] ?? null;
|
|
|
|
$this->pdo->beginTransaction();
|
|
try {
|
|
foreach ($this->mago->iterate('MA_SaleOrd', ['filters' => $filters]) as $r) {
|
|
$stmt->execute($this->mapSaleOrd($r));
|
|
$count++;
|
|
|
|
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
|
|
if ($mod && (!$maxModified || $mod > $maxModified)) {
|
|
$maxModified = $mod;
|
|
}
|
|
|
|
if ($count % 200 === 0) {
|
|
$this->pdo->commit();
|
|
$this->pdo->beginTransaction();
|
|
$this->log(" ordini salvati: $count");
|
|
}
|
|
|
|
if ($limit && $count >= $limit) break;
|
|
}
|
|
$this->pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
|
|
$this->saveState(self::ENTITY_ORDERS, null, 'errore', $e->getMessage(), $count, $t0, false);
|
|
throw $e;
|
|
}
|
|
|
|
// In modalità prova NON avanzo il punto di ripresa (i record non arrivano in ordine di modifica)
|
|
$this->saveState(
|
|
self::ENTITY_ORDERS,
|
|
$limit ? null : $maxModified,
|
|
'ok',
|
|
($limit ? "Prova: $count ordini" : ($incremental ? "Incrementale: $count ordini" : "Completa: $count ordini")),
|
|
$count,
|
|
$t0,
|
|
!$limit
|
|
);
|
|
|
|
$this->log("Ordini sincronizzati: $count");
|
|
return ['records' => $count, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
|
|
}
|
|
|
|
private function mapSaleOrd(array $r)
|
|
{
|
|
$s = function ($k, $len = 255) use ($r) {
|
|
return mb_substr(trim((string)($r[$k] ?? '')), 0, $len);
|
|
};
|
|
|
|
return [
|
|
'sale_ord_id' => (int)($r['SaleOrdId'] ?? 0),
|
|
'internal_ord_no' => $s('InternalOrdNo', 30),
|
|
'external_ord_no' => $s('ExternalOrdNo', 60),
|
|
'order_date' => MagoClient::toMysqlDate($r['OrderDate'] ?? null),
|
|
'expected_delivery_date' => MagoClient::toMysqlDate($r['ExpectedDeliveryDate'] ?? null),
|
|
'confirmed_delivery_date' => MagoClient::toMysqlDate($r['ConfirmedDeliveryDate'] ?? null),
|
|
'compulsory_delivery_date' => MagoClient::toMysqlDate($r['CompulsoryDeliveryDate'] ?? null),
|
|
'customer' => $s('Customer', 20),
|
|
'invoicing_customer' => $s('InvoicingCustomer', 20),
|
|
'salesperson' => $s('Salesperson', 20),
|
|
'area_manager' => $s('AreaManager', 20),
|
|
'area' => $s('Area', 20),
|
|
'payment' => $s('Payment', 20),
|
|
'currency' => $s('Currency', 8),
|
|
'our_reference' => $s('OurReference', 150),
|
|
'your_reference' => $s('YourReference', 150),
|
|
'transport' => $s('Transport', 20),
|
|
'port' => $s('Port', 20),
|
|
'package' => $s('Package', 40),
|
|
'storage' => $s('StoragePhase1', 30),
|
|
'job' => $s('Job', 30),
|
|
'delivered' => MagoClient::toBool($r['Delivered'] ?? false),
|
|
'invoiced' => MagoClient::toBool($r['Invoiced'] ?? false),
|
|
'cancelled' => MagoClient::toBool($r['Cancelled'] ?? false),
|
|
'is_blocked' => MagoClient::toBool($r['IsBlocked'] ?? false),
|
|
'printed' => MagoClient::toBool($r['Printed'] ?? false),
|
|
'archived' => MagoClient::toBool($r['Archived'] ?? false),
|
|
'open_order' => MagoClient::toBool($r['OpenOrder'] ?? false),
|
|
'priority' => (int)($r['Priority'] ?? 0),
|
|
'notes' => trim((string)($r['Notes'] ?? '')) ?: null,
|
|
'tb_created' => MagoClient::toMysqlDate($r['TbCreated'] ?? null, true),
|
|
'tb_modified' => MagoClient::toMysqlDate($r['TbModified'] ?? null, true),
|
|
'raw_json' => json_encode($r, JSON_UNESCAPED_UNICODE),
|
|
];
|
|
}
|
|
|
|
/* =========================================================
|
|
* RIGHE ORDINE (MA_SaleOrdDetails → mago_sale_ord_detail)
|
|
* ========================================================= */
|
|
|
|
/**
|
|
* Le righe hanno OrderDate, quindi si filtrano come le testate.
|
|
* Vengono salvate solo le righe di ordini già presenti in locale.
|
|
* Nelle letture complete le righe degli ordini del periodo vengono
|
|
* prima cancellate, così spariscono anche le righe eliminate in Mago.
|
|
*/
|
|
public function syncSaleOrderDetails($from, $full = false, $testMode = false)
|
|
{
|
|
$t0 = microtime(true);
|
|
$state = $this->getState(self::ENTITY_DETAILS);
|
|
|
|
// ordini locali del periodo (per scartare righe di ordini non sincronizzati)
|
|
$st = $this->pdo->prepare("SELECT sale_ord_id FROM mago_sale_ord WHERE order_date >= ?");
|
|
$st->execute([$from]);
|
|
$localIds = array_flip(array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN)));
|
|
if (!$localIds) {
|
|
return ['records' => 0, 'salvate' => 0, 'incrementale' => false, 'ms' => $this->ms($t0)];
|
|
}
|
|
|
|
$incremental = !$full && !$testMode && !empty($state['last_tb_modified']);
|
|
|
|
$filters = [['key' => 'OrderDate', 'value' => $from . 'T00:00:00', 'operator' => '>=']];
|
|
if ($incremental) {
|
|
$filters[] = [
|
|
'key' => 'TbModified',
|
|
'value' => date('Y-m-d\TH:i:s', strtotime($state['last_tb_modified'])),
|
|
'operator' => '>=',
|
|
];
|
|
}
|
|
if ($testMode) {
|
|
// prova: leggo solo le righe nel periodo degli ordini importati
|
|
$range = $this->pdo->query("SELECT MIN(order_date), MAX(order_date) FROM mago_sale_ord WHERE order_date >= " . $this->pdo->quote($from))->fetch(PDO::FETCH_NUM);
|
|
$filters = [
|
|
['key' => 'OrderDate', 'value' => $range[0] . 'T00:00:00', 'operator' => '>='],
|
|
['key' => 'OrderDate', 'value' => $range[1] . 'T00:00:00', 'operator' => '<='],
|
|
];
|
|
}
|
|
|
|
$this->log('Righe: ' . ($incremental ? 'incrementale da ' . $state['last_tb_modified'] : 'completa dal ' . $from));
|
|
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO mago_sale_ord_detail (
|
|
sale_ord_id, sub_id, line, position, line_type, item, description, uom,
|
|
qty, delivered_qty, invoiced_qty, unit_value, discount_amount, taxable_amount, total_amount,
|
|
expected_delivery_date, confirmed_delivery_date, delivered, invoiced, cancelled,
|
|
customer, order_date, internal_ord_no, notes, matrice_misura, cod_art_cli, tb_modified, synced_at
|
|
) VALUES (
|
|
:sale_ord_id, :sub_id, :line, :position, :line_type, :item, :description, :uom,
|
|
:qty, :delivered_qty, :invoiced_qty, :unit_value, :discount_amount, :taxable_amount, :total_amount,
|
|
:expected, :confirmed, :delivered, :invoiced, :cancelled,
|
|
:customer, :order_date, :internal_ord_no, :notes, :matrice_misura, :cod_art_cli, :tb_modified, NOW()
|
|
)
|
|
ON DUPLICATE KEY UPDATE
|
|
line = VALUES(line), position = VALUES(position), line_type = VALUES(line_type),
|
|
item = VALUES(item), description = VALUES(description), uom = VALUES(uom),
|
|
qty = VALUES(qty), delivered_qty = VALUES(delivered_qty), invoiced_qty = VALUES(invoiced_qty),
|
|
unit_value = VALUES(unit_value), discount_amount = VALUES(discount_amount),
|
|
taxable_amount = VALUES(taxable_amount), total_amount = VALUES(total_amount),
|
|
expected_delivery_date = VALUES(expected_delivery_date),
|
|
confirmed_delivery_date = VALUES(confirmed_delivery_date),
|
|
delivered = VALUES(delivered), invoiced = VALUES(invoiced), cancelled = VALUES(cancelled),
|
|
customer = VALUES(customer), order_date = VALUES(order_date),
|
|
internal_ord_no = VALUES(internal_ord_no), notes = VALUES(notes),
|
|
matrice_misura = VALUES(matrice_misura), cod_art_cli = VALUES(cod_art_cli),
|
|
tb_modified = VALUES(tb_modified), synced_at = NOW()
|
|
");
|
|
|
|
$read = 0;
|
|
$saved = 0;
|
|
$maxModified = $state['last_tb_modified'] ?? null;
|
|
|
|
$this->pdo->beginTransaction();
|
|
try {
|
|
if (!$incremental) {
|
|
// lettura completa: riparto pulito per gli ordini del periodo
|
|
$this->pdo->prepare("
|
|
DELETE d FROM mago_sale_ord_detail d
|
|
JOIN mago_sale_ord o ON o.sale_ord_id = d.sale_ord_id
|
|
WHERE o.order_date >= ?
|
|
")->execute([$from]);
|
|
}
|
|
|
|
foreach ($this->mago->iterate('MA_SaleOrdDetails', ['filters' => $filters]) as $r) {
|
|
$read++;
|
|
$id = (int)($r['SaleOrdId'] ?? 0);
|
|
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
|
|
if ($mod && (!$maxModified || $mod > $maxModified)) $maxModified = $mod;
|
|
|
|
if (!isset($localIds[$id])) continue;
|
|
|
|
$s = function ($k, $len) use ($r) {
|
|
return mb_substr(trim((string)($r[$k] ?? '')), 0, $len);
|
|
};
|
|
$d = function ($k, $dec = 2) use ($r) {
|
|
return round(MagoClient::toDecimal($r[$k] ?? 0), $dec);
|
|
};
|
|
|
|
$stmt->execute([
|
|
'sale_ord_id' => $id,
|
|
'sub_id' => (int)($r['SubId'] ?? $r['Line'] ?? 0),
|
|
'line' => (int)($r['Line'] ?? 0),
|
|
'position' => (int)($r['Position'] ?? 0),
|
|
'line_type' => (int)($r['LineType'] ?? 0),
|
|
'item' => $s('Item', 30),
|
|
'description' => $s('Description', 255),
|
|
'uom' => $s('UoM', 10),
|
|
'qty' => $d('Qty', 3),
|
|
'delivered_qty' => $d('DeliveredQty', 3),
|
|
'invoiced_qty' => $d('InvoicedQty', 3),
|
|
'unit_value' => $d('UnitValue', 5),
|
|
'discount_amount' => $d('DiscountAmount'),
|
|
'taxable_amount' => $d('TaxableAmount'),
|
|
'total_amount' => $d('TotalAmount'),
|
|
'expected' => MagoClient::toMysqlDate($r['ExpectedDeliveryDate'] ?? null),
|
|
'confirmed' => MagoClient::toMysqlDate($r['ConfirmedDeliveryDate'] ?? null),
|
|
'delivered' => MagoClient::toBool($r['Delivered'] ?? false),
|
|
'invoiced' => MagoClient::toBool($r['Invoiced'] ?? false),
|
|
'cancelled' => MagoClient::toBool($r['Cancelled'] ?? false),
|
|
'customer' => $s('Customer', 20),
|
|
'order_date' => MagoClient::toMysqlDate($r['OrderDate'] ?? null),
|
|
'internal_ord_no' => $s('InternalOrdNo', 30),
|
|
'matrice_misura' => $s('Department', 30), // colonna "Matrice/Misura" in Mago (es. M00786S, D 3,3)
|
|
'cod_art_cli' => $s('ReferenceDocNo', 60), // colonna "Cod.Art.Cli" in Mago
|
|
'notes' => trim((string)($r['Notes'] ?? '')) ?: null,
|
|
'tb_modified' => $mod,
|
|
]);
|
|
$saved++;
|
|
|
|
if ($read % 200 === 0) {
|
|
$this->pdo->commit();
|
|
$this->pdo->beginTransaction();
|
|
$this->log(" righe lette: $read");
|
|
}
|
|
}
|
|
|
|
$this->refreshLineDates($from);
|
|
$this->pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
|
|
$this->saveState(self::ENTITY_DETAILS, null, 'errore', $e->getMessage(), $read, $t0, false);
|
|
if (!$incremental) {
|
|
// lettura completa interrotta: al prossimo giro si riparte da capo
|
|
$this->pdo->prepare("UPDATE mago_sync_state SET last_tb_modified = NULL WHERE entity = ?")
|
|
->execute([self::ENTITY_DETAILS]);
|
|
}
|
|
throw $e;
|
|
}
|
|
|
|
$this->saveState(self::ENTITY_DETAILS, $testMode ? null : $maxModified, 'ok', "Lette $read righe, salvate $saved", $read, $t0, !$testMode);
|
|
$this->log("Righe: lette $read, salvate $saved");
|
|
|
|
return ['records' => $read, 'salvate' => $saved, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
|
|
}
|
|
|
|
/**
|
|
* Riporta sulla testata la prima data di consegna delle righe ancora da consegnare
|
|
* (se tutte consegnate: la prima data tra tutte le righe).
|
|
* La data confermata vale solo se successiva alla data ordine.
|
|
*/
|
|
private function refreshLineDates($from)
|
|
{
|
|
$this->pdo->prepare("
|
|
UPDATE mago_sale_ord o
|
|
LEFT JOIN (
|
|
SELECT sale_ord_id,
|
|
COALESCE(
|
|
MIN(CASE WHEN delivered = 0 AND cancelled = 0 THEN expected_delivery_date END),
|
|
MIN(expected_delivery_date)
|
|
) AS exp_date,
|
|
COALESCE(
|
|
MIN(CASE WHEN delivered = 0 AND cancelled = 0 AND confirmed_delivery_date > order_date THEN confirmed_delivery_date END),
|
|
MIN(CASE WHEN confirmed_delivery_date > order_date THEN confirmed_delivery_date END)
|
|
) AS conf_date,
|
|
COUNT(*) AS n
|
|
FROM mago_sale_ord_detail
|
|
WHERE item <> '' OR qty <> 0
|
|
GROUP BY sale_ord_id
|
|
) d ON d.sale_ord_id = o.sale_ord_id
|
|
SET o.line_expected_date = d.exp_date,
|
|
o.line_confirmed_date = d.conf_date,
|
|
o.lines_count = COALESCE(d.n, 0)
|
|
WHERE o.order_date >= ?
|
|
")->execute([$from]);
|
|
}
|
|
|
|
/* =========================================================
|
|
* IMPORTI (MA_SaleOrdSummary → colonne importo di mago_sale_ord)
|
|
* ========================================================= */
|
|
|
|
/**
|
|
* MA_SaleOrdSummary non ha la data ordine: filtro per TbModified, che è
|
|
* l'unico filtro data verificato. Parto dalla creazione del primo ordine
|
|
* locale del periodo (i totali nascono insieme all'ordine) e aggiorno
|
|
* solo gli ordini già presenti in locale.
|
|
*/
|
|
public function syncSaleOrderSummaries($from, $full = false, $testMode = false)
|
|
{
|
|
$t0 = microtime(true);
|
|
$state = $this->getState(self::ENTITY_SUMMARY);
|
|
|
|
$info = $this->pdo->query(
|
|
"
|
|
SELECT MIN(COALESCE(tb_created, order_date)) AS first_created,
|
|
SUM(summary_tb_modified IS NULL) AS missing,
|
|
COUNT(*) AS n
|
|
FROM mago_sale_ord
|
|
WHERE order_date >= " . $this->pdo->quote($from)
|
|
)->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (empty($info['n'])) {
|
|
return ['records' => 0, 'aggiornati' => 0, 'incrementale' => false, 'ms' => $this->ms($t0)];
|
|
}
|
|
|
|
// Incrementale solo se tutti gli ordini locali hanno già i totali
|
|
$incremental = !$full && !$testMode && !empty($state['last_tb_modified']) && (int)$info['missing'] === 0;
|
|
$since = $incremental
|
|
? $state['last_tb_modified']
|
|
: date('Y-m-d 00:00:00', strtotime($info['first_created'] . ' -1 day'));
|
|
|
|
$this->log('Importi: ' . ($incremental ? 'incrementale' : 'completa') . ' da ' . $since);
|
|
|
|
$filters = [['key' => 'TbModified', 'value' => date('Y-m-d\TH:i:s', strtotime($since)), 'operator' => '>=']];
|
|
|
|
$stmt = $this->pdo->prepare("
|
|
UPDATE mago_sale_ord SET
|
|
taxable_amount = :taxable,
|
|
tax_amount = :tax,
|
|
total_amount = :total,
|
|
goods_amount = :goods,
|
|
discounts = :discounts,
|
|
charges = :charges,
|
|
summary_tb_modified = :mod
|
|
WHERE sale_ord_id = :id
|
|
");
|
|
|
|
$read = 0;
|
|
$updated = 0;
|
|
$maxModified = $state['last_tb_modified'] ?? null;
|
|
|
|
$this->pdo->beginTransaction();
|
|
try {
|
|
foreach ($this->mago->iterate('MA_SaleOrdSummary', ['filters' => $filters]) as $r) {
|
|
$read++;
|
|
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
|
|
$d = function ($k) use ($r) {
|
|
return round(MagoClient::toDecimal($r[$k] ?? 0), 2);
|
|
};
|
|
|
|
$stmt->execute([
|
|
'taxable' => $d('TaxableAmount'),
|
|
'tax' => $d('TaxAmount'),
|
|
'total' => $d('TotalAmount'),
|
|
'goods' => $d('GoodsAmount'),
|
|
'discounts' => $d('Discounts'),
|
|
'charges' => $d('ShippingCharges') + $d('PackagingCharges') + $d('CollectionCharges')
|
|
+ $d('StampsCharges') + $d('AdditionalCharges') + $d('InsuranceCharges'),
|
|
'mod' => $mod ?: date('Y-m-d H:i:s'),
|
|
'id' => (int)($r['SaleOrdId'] ?? 0),
|
|
]);
|
|
$updated += $stmt->rowCount() > 0 ? 1 : 0;
|
|
|
|
if ($mod && (!$maxModified || $mod > $maxModified)) $maxModified = $mod;
|
|
|
|
if ($read % 200 === 0) {
|
|
$this->pdo->commit();
|
|
$this->pdo->beginTransaction();
|
|
$this->log(" totali letti: $read");
|
|
}
|
|
}
|
|
$this->pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
|
|
$this->saveState(self::ENTITY_SUMMARY, null, 'errore', $e->getMessage(), $read, $t0, false);
|
|
throw $e;
|
|
}
|
|
|
|
$this->saveState(self::ENTITY_SUMMARY, $testMode ? null : $maxModified, 'ok', "Letti $read totali, $updated ordini aggiornati", $read, $t0, !$testMode);
|
|
$this->log("Importi: letti $read, ordini aggiornati $updated");
|
|
|
|
return ['records' => $read, 'aggiornati' => $updated, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
|
|
}
|
|
|
|
/* =========================================================
|
|
* DOCUMENTI DI VENDITA (MA_SaleDoc → mago_sale_doc) per il fatturato
|
|
* ========================================================= */
|
|
|
|
/**
|
|
* Testate documenti (tutti i tipi: fatture, note di credito, DDT...), filtrate per DocumentDate.
|
|
* Lettura completa: alla fine cancella in locale i documenti del periodo non più presenti in Mago.
|
|
*/
|
|
public function syncSaleDocs($from, $limit = null, $full = false)
|
|
{
|
|
$t0 = microtime(true);
|
|
$state = $this->getState(self::ENTITY_DOCS);
|
|
|
|
$filters = [['key' => 'DocumentDate', 'value' => $from . 'T00:00:00', 'operator' => '>=']];
|
|
$incremental = !$full && !$limit && !empty($state['last_tb_modified']);
|
|
if ($incremental) {
|
|
$filters[] = [
|
|
'key' => 'TbModified',
|
|
'value' => date('Y-m-d\TH:i:s', strtotime($state['last_tb_modified'])),
|
|
'operator' => '>=',
|
|
];
|
|
}
|
|
|
|
// solo i campi che servono (la testata ha ~200 campi: così è molto più veloce)
|
|
$fields = [
|
|
'SaleDocId',
|
|
'DocumentType',
|
|
'DocNo',
|
|
'DocumentDate',
|
|
'PostingDate',
|
|
'CustSupp',
|
|
'InvoicingCustomer',
|
|
'Salesperson',
|
|
'Area',
|
|
'Currency',
|
|
'TaxJournal',
|
|
'InvRsn',
|
|
'EIDocumentType',
|
|
'Issued',
|
|
'Cancelled',
|
|
'PostedToAccounting',
|
|
'TbCreated',
|
|
'TbModified'
|
|
];
|
|
|
|
$this->log('Documenti: ' . ($incremental ? 'incrementale da ' . $state['last_tb_modified'] : 'completa dal ' . $from) . ($limit ? " (prova: max $limit)" : ''));
|
|
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO mago_sale_doc (
|
|
sale_doc_id, document_type, doc_no, document_date, posting_date, customer, invoicing_customer,
|
|
salesperson, area, currency, tax_journal, inv_rsn, ei_document_type,
|
|
issued, cancelled, posted_to_accounting, tb_created, tb_modified, synced_at
|
|
) VALUES (
|
|
:id, :type, :doc_no, :doc_date, :posting_date, :customer, :inv_customer,
|
|
:salesperson, :area, :currency, :tax_journal, :inv_rsn, :ei_type,
|
|
:issued, :cancelled, :posted, :tb_created, :tb_modified, NOW()
|
|
)
|
|
ON DUPLICATE KEY UPDATE
|
|
document_type = VALUES(document_type), doc_no = VALUES(doc_no),
|
|
document_date = VALUES(document_date), posting_date = VALUES(posting_date),
|
|
customer = VALUES(customer), invoicing_customer = VALUES(invoicing_customer),
|
|
salesperson = VALUES(salesperson), area = VALUES(area), currency = VALUES(currency),
|
|
tax_journal = VALUES(tax_journal), inv_rsn = VALUES(inv_rsn), ei_document_type = VALUES(ei_document_type),
|
|
issued = VALUES(issued), cancelled = VALUES(cancelled), posted_to_accounting = VALUES(posted_to_accounting),
|
|
tb_created = VALUES(tb_created), tb_modified = VALUES(tb_modified), synced_at = NOW()
|
|
");
|
|
|
|
$count = 0;
|
|
$seen = [];
|
|
$maxModified = $state['last_tb_modified'] ?? null;
|
|
|
|
$this->pdo->beginTransaction();
|
|
try {
|
|
foreach ($this->mago->iterate('MA_SaleDoc', ['filters' => $filters, 'fields' => $fields]) as $r) {
|
|
$s = function ($k, $len) use ($r) {
|
|
return mb_substr(trim((string)($r[$k] ?? '')), 0, $len);
|
|
};
|
|
$id = (int)($r['SaleDocId'] ?? 0);
|
|
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
|
|
|
|
$stmt->execute([
|
|
'id' => $id,
|
|
'type' => (int)($r['DocumentType'] ?? 0),
|
|
'doc_no' => $s('DocNo', 30),
|
|
'doc_date' => MagoClient::toMysqlDate($r['DocumentDate'] ?? null),
|
|
'posting_date' => MagoClient::toMysqlDate($r['PostingDate'] ?? null),
|
|
'customer' => $s('CustSupp', 20),
|
|
'inv_customer' => $s('InvoicingCustomer', 20),
|
|
'salesperson' => $s('Salesperson', 20),
|
|
'area' => $s('Area', 20),
|
|
'currency' => $s('Currency', 8),
|
|
'tax_journal' => $s('TaxJournal', 20),
|
|
'inv_rsn' => $s('InvRsn', 20),
|
|
'ei_type' => (int)($r['EIDocumentType'] ?? 0),
|
|
'issued' => MagoClient::toBool($r['Issued'] ?? false),
|
|
'cancelled' => MagoClient::toBool($r['Cancelled'] ?? false),
|
|
'posted' => MagoClient::toBool($r['PostedToAccounting'] ?? false),
|
|
'tb_created' => MagoClient::toMysqlDate($r['TbCreated'] ?? null, true),
|
|
'tb_modified' => $mod,
|
|
]);
|
|
$count++;
|
|
$seen[$id] = true;
|
|
if ($mod && (!$maxModified || $mod > $maxModified)) $maxModified = $mod;
|
|
|
|
if ($count % 200 === 0) {
|
|
$this->pdo->commit();
|
|
$this->pdo->beginTransaction();
|
|
$this->log(" documenti salvati: $count");
|
|
}
|
|
if ($limit && $count >= $limit) break;
|
|
}
|
|
|
|
// lettura completa riuscita: tolgo i documenti del periodo cancellati in Mago
|
|
$deleted = 0;
|
|
if (!$incremental && !$limit) {
|
|
$st = $this->pdo->prepare("SELECT sale_doc_id FROM mago_sale_doc WHERE document_date >= ?");
|
|
$st->execute([$from]);
|
|
$del = $this->pdo->prepare("DELETE FROM mago_sale_doc WHERE sale_doc_id = ?");
|
|
foreach ($st->fetchAll(PDO::FETCH_COLUMN) as $id) {
|
|
if (!isset($seen[(int)$id])) {
|
|
$del->execute([$id]);
|
|
$deleted++;
|
|
}
|
|
}
|
|
}
|
|
$this->pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
|
|
$this->saveState(self::ENTITY_DOCS, null, 'errore', $e->getMessage(), $count, $t0, false);
|
|
throw $e;
|
|
}
|
|
|
|
$msg = ($limit ? "Prova: $count documenti" : ($incremental ? "Incrementale: $count documenti" : "Completa: $count documenti")) . ($deleted ? ", $deleted cancellati" : '');
|
|
$this->saveState(self::ENTITY_DOCS, $limit ? null : $maxModified, 'ok', $msg, $count, $t0, !$limit);
|
|
$this->log("Documenti sincronizzati: $count" . ($deleted ? " ($deleted non più presenti in Mago, cancellati)" : ''));
|
|
|
|
return ['records' => $count, 'cancellati' => $deleted, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
|
|
}
|
|
|
|
/**
|
|
* Importi dei documenti (MA_SaleDocSummary). Come per gli ordini si filtra per TbModified
|
|
* partendo dalla creazione del primo documento senza importo (o di tutti, in lettura completa).
|
|
*/
|
|
public function syncSaleDocSummaries($from, $full = false, $testMode = false)
|
|
{
|
|
$t0 = microtime(true);
|
|
$state = $this->getState(self::ENTITY_DOC_SUMM);
|
|
|
|
$info = $this->pdo->query(
|
|
"
|
|
SELECT MIN(COALESCE(tb_created, document_date)) AS first_created,
|
|
MIN(CASE WHEN summary_tb_modified IS NULL THEN COALESCE(tb_created, document_date) END) AS first_missing,
|
|
COUNT(*) AS n
|
|
FROM mago_sale_doc
|
|
WHERE document_date >= " . $this->pdo->quote($from)
|
|
)->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (empty($info['n'])) {
|
|
return ['records' => 0, 'aggiornati' => 0, 'incrementale' => false, 'ms' => $this->ms($t0)];
|
|
}
|
|
|
|
// Incrementale: dal punto di ripresa, ma se ci sono documenti nuovi senza importo parto dalla loro creazione
|
|
$incremental = !$full && !$testMode && !empty($state['last_tb_modified']);
|
|
if ($incremental) {
|
|
$since = $state['last_tb_modified'];
|
|
if (!empty($info['first_missing'])) {
|
|
$miss = date('Y-m-d 00:00:00', strtotime($info['first_missing'] . ' -1 day'));
|
|
if ($miss < $since) $since = $miss;
|
|
}
|
|
} else {
|
|
$since = date('Y-m-d 00:00:00', strtotime($info['first_created'] . ' -1 day'));
|
|
}
|
|
|
|
$this->log('Importi documenti: ' . ($incremental ? 'incrementale' : 'completa') . ' da ' . $since);
|
|
|
|
$filters = [['key' => 'TbModified', 'value' => date('Y-m-d\TH:i:s', strtotime($since)), 'operator' => '>=']];
|
|
|
|
$stmt = $this->pdo->prepare("
|
|
UPDATE mago_sale_doc SET
|
|
taxable_amount = :taxable, tax_amount = :tax, total_amount = :total, summary_tb_modified = :mod
|
|
WHERE sale_doc_id = :id
|
|
");
|
|
|
|
$read = 0;
|
|
$updated = 0;
|
|
$maxModified = $state['last_tb_modified'] ?? null;
|
|
|
|
$this->pdo->beginTransaction();
|
|
try {
|
|
foreach (
|
|
$this->mago->iterate('MA_SaleDocSummary', [
|
|
'filters' => $filters,
|
|
'fields' => ['SaleDocId', 'TaxableAmount', 'TaxAmount', 'TotalAmount', 'TbModified'],
|
|
]) as $r
|
|
) {
|
|
$read++;
|
|
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
|
|
$stmt->execute([
|
|
'taxable' => round(MagoClient::toDecimal($r['TaxableAmount'] ?? 0), 2),
|
|
'tax' => round(MagoClient::toDecimal($r['TaxAmount'] ?? 0), 2),
|
|
'total' => round(MagoClient::toDecimal($r['TotalAmount'] ?? 0), 2),
|
|
'mod' => $mod ?: date('Y-m-d H:i:s'),
|
|
'id' => (int)($r['SaleDocId'] ?? 0),
|
|
]);
|
|
$updated += $stmt->rowCount() > 0 ? 1 : 0;
|
|
if ($mod && (!$maxModified || $mod > $maxModified)) $maxModified = $mod;
|
|
|
|
if ($read % 200 === 0) {
|
|
$this->pdo->commit();
|
|
$this->pdo->beginTransaction();
|
|
$this->log(" totali documenti letti: $read");
|
|
}
|
|
}
|
|
$this->pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
|
|
$this->saveState(self::ENTITY_DOC_SUMM, null, 'errore', $e->getMessage(), $read, $t0, false);
|
|
throw $e;
|
|
}
|
|
|
|
$this->saveState(self::ENTITY_DOC_SUMM, $testMode ? null : $maxModified, 'ok', "Letti $read totali, $updated documenti aggiornati", $read, $t0, !$testMode);
|
|
$this->log("Importi documenti: letti $read, documenti aggiornati $updated");
|
|
|
|
return ['records' => $read, 'aggiornati' => $updated, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
|
|
}
|
|
|
|
private function tableExists($name)
|
|
{
|
|
$st = $this->pdo->prepare("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?");
|
|
$st->execute([$name]);
|
|
return (int)$st->fetchColumn() > 0;
|
|
}
|
|
|
|
/* =========================================================
|
|
* ANAGRAFICA CLIENTI (per decodificare i codici cliente)
|
|
* ========================================================= */
|
|
|
|
public function syncCustomers($full = false)
|
|
{
|
|
$t0 = microtime(true);
|
|
$state = $this->getState(self::ENTITY_CUSTOMER);
|
|
|
|
$filters = [['key' => 'CustSuppType', 'value' => (string)self::CUSTOMER_TYPE, 'operator' => '=']];
|
|
$incremental = !$full && !empty($state['last_tb_modified']);
|
|
if ($incremental) {
|
|
$filters[] = [
|
|
'key' => 'TbModified',
|
|
'value' => date('Y-m-d\TH:i:s', strtotime($state['last_tb_modified'])),
|
|
'operator' => '>=',
|
|
];
|
|
}
|
|
|
|
$this->log('Clienti: ' . ($incremental ? 'incrementale' : 'completa'));
|
|
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO mago_cust_supp
|
|
(cust_supp_type, cust_supp, company_name, city, county, region, country, iso_country_code, disabled, tb_modified, synced_at)
|
|
VALUES
|
|
(:t, :c, :name, :city, :county, :region, :country, :iso, :disabled, :mod, NOW())
|
|
ON DUPLICATE KEY UPDATE
|
|
company_name = VALUES(company_name), city = VALUES(city), county = VALUES(county),
|
|
region = VALUES(region), country = VALUES(country), iso_country_code = VALUES(iso_country_code),
|
|
disabled = VALUES(disabled), tb_modified = VALUES(tb_modified), synced_at = NOW()
|
|
");
|
|
|
|
$count = 0;
|
|
$maxModified = $state['last_tb_modified'] ?? null;
|
|
|
|
$this->pdo->beginTransaction();
|
|
try {
|
|
foreach ($this->mago->iterate('MA_CustSupp', ['filters' => $filters]) as $r) {
|
|
$mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
|
|
$stmt->execute([
|
|
't' => (int)($r['CustSuppType'] ?? self::CUSTOMER_TYPE),
|
|
'c' => mb_substr(trim((string)($r['CustSupp'] ?? '')), 0, 20),
|
|
'name' => mb_substr(trim((string)($r['CompanyName'] ?? '')), 0, 200),
|
|
'city' => mb_substr(trim((string)($r['City'] ?? '')), 0, 100),
|
|
'county' => mb_substr(trim((string)($r['County'] ?? '')), 0, 10),
|
|
'region' => mb_substr(trim((string)($r['Region'] ?? '')), 0, 60),
|
|
'country' => mb_substr(trim((string)($r['Country'] ?? '')), 0, 60),
|
|
'iso' => mb_substr(trim((string)($r['ISOCountryCode'] ?? '')), 0, 4),
|
|
'disabled' => MagoClient::toBool($r['Disabled'] ?? false),
|
|
'mod' => $mod,
|
|
]);
|
|
$count++;
|
|
if ($mod && (!$maxModified || $mod > $maxModified)) $maxModified = $mod;
|
|
}
|
|
$this->pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($this->pdo->inTransaction()) $this->pdo->rollBack();
|
|
$this->saveState(self::ENTITY_CUSTOMER, null, 'errore', $e->getMessage(), $count, $t0, false);
|
|
throw $e;
|
|
}
|
|
|
|
$this->saveState(self::ENTITY_CUSTOMER, $maxModified, 'ok', "$count clienti", $count, $t0, true);
|
|
$this->log("Clienti sincronizzati: $count");
|
|
|
|
return ['records' => $count, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
|
|
}
|
|
|
|
/* =========================================================
|
|
* STATO / LOCK / LOG
|
|
* ========================================================= */
|
|
|
|
private function getState($entity)
|
|
{
|
|
$st = $this->pdo->prepare("SELECT * FROM mago_sync_state WHERE entity = ?");
|
|
$st->execute([$entity]);
|
|
return $st->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
}
|
|
|
|
private function saveState($entity, $lastModified, $status, $message, $records, $t0, $updateCursor)
|
|
{
|
|
$sql = "INSERT INTO mago_sync_state (entity, last_tb_modified, last_run_at, last_status, last_message, records_last_run, duration_ms)
|
|
VALUES (:e, :m, NOW(), :s, :msg, :r, :d)
|
|
ON DUPLICATE KEY UPDATE
|
|
" . ($updateCursor ? "last_tb_modified = COALESCE(VALUES(last_tb_modified), last_tb_modified)," : "") . "
|
|
last_run_at = NOW(),
|
|
last_status = VALUES(last_status),
|
|
last_message = VALUES(last_message),
|
|
records_last_run = VALUES(records_last_run),
|
|
duration_ms = VALUES(duration_ms)";
|
|
|
|
$this->pdo->prepare($sql)->execute([
|
|
'e' => $entity,
|
|
'm' => $updateCursor ? $lastModified : null,
|
|
's' => $status,
|
|
'msg' => mb_substr((string)$message, 0, 2000),
|
|
'r' => (int)$records,
|
|
'd' => (int)round((microtime(true) - $t0) * 1000),
|
|
]);
|
|
}
|
|
|
|
private function acquireLock()
|
|
{
|
|
return (int)$this->pdo->query("SELECT GET_LOCK('mago_sync', 0)")->fetchColumn() === 1;
|
|
}
|
|
|
|
private function releaseLock()
|
|
{
|
|
try {
|
|
$this->pdo->query("SELECT RELEASE_LOCK('mago_sync')");
|
|
} catch (Throwable $e) {
|
|
}
|
|
}
|
|
|
|
private function ms($t0)
|
|
{
|
|
return (int)round((microtime(true) - $t0) * 1000);
|
|
}
|
|
|
|
private function log($msg)
|
|
{
|
|
if ($this->logger) {
|
|
call_user_func($this->logger, '[' . date('H:i:s') . '] ' . $msg);
|
|
}
|
|
}
|
|
}
|