465 lines
21 KiB
PHP
465 lines
21 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';
|
|
|
|
/** @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['importi'] = $this->syncSaleOrderSummaries($from, $full, (bool)$limit);
|
|
} 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),
|
|
];
|
|
}
|
|
|
|
/* =========================================================
|
|
* 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)];
|
|
}
|
|
|
|
/* =========================================================
|
|
* 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);
|
|
}
|
|
}
|
|
}
|