fixed stats
This commit is contained in:
@@ -9,6 +9,60 @@ class MagoSalesStats
|
||||
{
|
||||
const CUSTOMER_TYPE = 3211264;
|
||||
|
||||
/*
|
||||
* MAPPATURA DATE DI CONSEGNA (righe e testata usano gli stessi nomi colonna)
|
||||
*
|
||||
* Data ZIBO = data comunicata da Zibo al cliente → è la SCADENZA che conta (ritardo in rosso)
|
||||
* Data cliente = data desiderata dal cliente → riferimento (ritardo in arancione)
|
||||
*
|
||||
* Per invertirle basta scambiare i due valori qui sotto.
|
||||
*/
|
||||
const ZIBO_DATE_COL = 'confirmed_delivery_date'; // Mago: ConfirmedDeliveryDate
|
||||
const CLIENT_DATE_COL = 'expected_delivery_date'; // Mago: ExpectedDeliveryDate
|
||||
|
||||
/** Data ZIBO "valida" di una riga/testata: in Mago, se non compilata, resta uguale alla data ordine. */
|
||||
public static function ziboDate($a)
|
||||
{
|
||||
$c = $a . '.' . self::ZIBO_DATE_COL;
|
||||
return "CASE WHEN $c > $a.order_date THEN $c END";
|
||||
}
|
||||
|
||||
public static function clientDate($a)
|
||||
{
|
||||
$c = $a . '.' . self::CLIENT_DATE_COL;
|
||||
return "CASE WHEN $c > $a.order_date THEN $c END";
|
||||
}
|
||||
|
||||
/** Scadenza effettiva di una riga: data ZIBO, se manca la data cliente. */
|
||||
public static function lineDue($a)
|
||||
{
|
||||
return "COALESCE(" . self::ziboDate($a) . ", " . self::clientDate($a) . ")";
|
||||
}
|
||||
|
||||
/** Aggregati per ordine calcolati dalle righe (alias l). */
|
||||
public static function lineAggregates()
|
||||
{
|
||||
$open = "(d.delivered = 0 AND d.cancelled = 0)";
|
||||
$due = self::lineDue('d');
|
||||
return "(
|
||||
SELECT d.sale_ord_id,
|
||||
COUNT(*) AS lines_total,
|
||||
SUM($open) AS open_lines,
|
||||
MIN(CASE WHEN $open THEN " . self::ziboDate('d') . " END) AS next_zibo,
|
||||
MIN(CASE WHEN $open THEN " . self::clientDate('d') . " END) AS next_client,
|
||||
MIN(" . self::ziboDate('d') . ") AS first_zibo,
|
||||
MIN(" . self::clientDate('d') . ") AS first_client,
|
||||
MIN(CASE WHEN $open THEN $due END) AS next_due,
|
||||
MIN($due) AS first_due,
|
||||
COUNT(DISTINCT CASE WHEN $open THEN $due END) AS open_dates,
|
||||
SUM($open AND $due < CURDATE()) AS late_lines,
|
||||
SUM($open AND " . self::clientDate('d') . " < CURDATE()) AS late_lines_client
|
||||
FROM mago_sale_ord_detail d
|
||||
WHERE d.item <> '' OR d.qty <> 0
|
||||
GROUP BY d.sale_ord_id
|
||||
) l";
|
||||
}
|
||||
|
||||
/** Risposta JSON e fine script (pulisce l'output di headscript). */
|
||||
public static function jsonOut(array $data)
|
||||
{
|
||||
@@ -36,8 +90,13 @@ class MagoSalesStats
|
||||
}
|
||||
|
||||
/**
|
||||
* Vista base: ordini + nome cliente + stato calcolato + data consegna di riferimento.
|
||||
* Le statistiche filtrano su questa vista (alias x).
|
||||
* Vista base: ordini + nome cliente + stato + date di consegna (alias x).
|
||||
*
|
||||
* zibo_date = prima data ZIBO tra le righe ancora da consegnare (se tutte consegnate: la prima in assoluto);
|
||||
* senza righe: data ZIBO di testata
|
||||
* client_date = stessa logica con la data cliente
|
||||
* due_date = scadenza dell'ordine: la più vicina tra le righe aperte (riga: data ZIBO, se manca data cliente)
|
||||
* late_lines / open_lines = righe aperte in ritardo / righe aperte (per il "ritardo parziale")
|
||||
*/
|
||||
public static function baseFrom()
|
||||
{
|
||||
@@ -45,13 +104,19 @@ class MagoSalesStats
|
||||
SELECT
|
||||
o.*,
|
||||
COALESCE(NULLIF(c.company_name, ''), o.customer) AS company_name,
|
||||
-- Data consegna di riferimento: prevista; la 'confermata' solo se successiva all'ordine
|
||||
-- (in Mago spesso coincide con la data ordine e farebbe risultare tutto in ritardo)
|
||||
COALESCE(l.next_zibo, l.first_zibo, " . self::ziboDate('o') . ") AS zibo_date,
|
||||
COALESCE(l.next_client, l.first_client, " . self::clientDate('o') . ") AS client_date,
|
||||
-- scadenza = la più vicina tra le righe aperte, dove ogni riga scade alla sua data ZIBO
|
||||
-- (o alla data cliente se la data ZIBO non è compilata)
|
||||
COALESCE(
|
||||
o.expected_delivery_date,
|
||||
CASE WHEN o.confirmed_delivery_date > o.order_date THEN o.confirmed_delivery_date END,
|
||||
o.compulsory_delivery_date
|
||||
l.next_due, l.first_due,
|
||||
" . self::ziboDate('o') . ", " . self::clientDate('o') . ", o.compulsory_delivery_date
|
||||
) AS due_date,
|
||||
COALESCE(l.lines_total, 0) AS lines_total,
|
||||
COALESCE(l.open_lines, 0) AS open_lines,
|
||||
COALESCE(l.open_dates, 0) AS open_dates,
|
||||
COALESCE(l.late_lines, 0) AS late_lines,
|
||||
COALESCE(l.late_lines_client, 0) AS late_lines_client,
|
||||
CASE
|
||||
WHEN o.cancelled = 1 THEN 'annullato'
|
||||
WHEN o.delivered = 1 THEN 'evaso'
|
||||
@@ -61,9 +126,36 @@ class MagoSalesStats
|
||||
FROM mago_sale_ord o
|
||||
LEFT JOIN mago_cust_supp c
|
||||
ON c.cust_supp_type = " . self::CUSTOMER_TYPE . " AND c.cust_supp = o.customer
|
||||
LEFT JOIN " . self::lineAggregates() . " ON l.sale_ord_id = o.sale_ord_id
|
||||
) x";
|
||||
}
|
||||
|
||||
/**
|
||||
* Vista righe aperte (alias r) per la pagina Consegne: riga + ordine + date effettive.
|
||||
*/
|
||||
public static function openLinesFrom()
|
||||
{
|
||||
return "(
|
||||
SELECT
|
||||
d.sale_ord_id, d.sub_id, d.line, d.position, d.item, d.description, d.uom,
|
||||
d.qty, d.delivered_qty,
|
||||
GREATEST(d.qty - d.delivered_qty, 0) AS residual_qty,
|
||||
CASE WHEN d.qty > 0 THEN ROUND(d.taxable_amount * GREATEST(d.qty - d.delivered_qty, 0) / d.qty, 2)
|
||||
ELSE d.taxable_amount END AS residual_value,
|
||||
d.taxable_amount,
|
||||
" . self::ziboDate('d') . " AS zibo_date,
|
||||
" . self::clientDate('d') . " AS client_date,
|
||||
" . self::lineDue('d') . " AS due_date,
|
||||
x.internal_ord_no, x.order_date, x.customer, x.company_name,
|
||||
x.salesperson, x.area, x.status, x.your_reference, x.external_ord_no
|
||||
FROM mago_sale_ord_detail d
|
||||
JOIN " . self::baseFrom() . " ON x.sale_ord_id = d.sale_ord_id
|
||||
WHERE d.delivered = 0 AND d.cancelled = 0
|
||||
AND x.status IN ('aperto','bloccato')
|
||||
AND (d.item <> '' OR d.qty <> 0)
|
||||
) r";
|
||||
}
|
||||
|
||||
/**
|
||||
* WHERE + parametri dai filtri in querystring.
|
||||
* $override sostituisce from/to (stringa vuota = nessun filtro data).
|
||||
@@ -108,6 +200,7 @@ class MagoSalesStats
|
||||
'annullato' => "x.status = 'annullato'",
|
||||
'bloccato' => "x.status = 'bloccato'",
|
||||
'ritardo' => "(x.status IN ('aperto','bloccato') AND x.due_date < CURDATE())",
|
||||
'ritardo_cliente' => "(x.status IN ('aperto','bloccato') AND x.client_date < CURDATE())",
|
||||
'da_fatturare' => "(x.delivered = 1 AND x.invoiced = 0 AND x.cancelled = 0)",
|
||||
'fatturato' => "x.invoiced = 1",
|
||||
];
|
||||
|
||||
@@ -20,6 +20,7 @@ class MagoSync
|
||||
const ENTITY_ORDERS = 'sale_ord';
|
||||
const ENTITY_CUSTOMER = 'cust_supp';
|
||||
const ENTITY_SUMMARY = 'sale_ord_summary';
|
||||
const ENTITY_DETAILS = 'sale_ord_details';
|
||||
|
||||
/** @var PDO */
|
||||
private $pdo;
|
||||
@@ -62,6 +63,7 @@ class MagoSync
|
||||
$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);
|
||||
} catch (Throwable $e) {
|
||||
$report['ok'] = false;
|
||||
@@ -240,6 +242,192 @@ class MagoSync
|
||||
];
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* 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, 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, :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),
|
||||
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),
|
||||
'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)
|
||||
* ========================================================= */
|
||||
|
||||
Reference in New Issue
Block a user