fixed stats
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Righe ordini cliente da MA_SaleOrdDetails (Mago4) e date di consegna
|
||||
* calcolate dalle righe sulla testata.
|
||||
*/
|
||||
final class CreateMagoSaleOrdDetail extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('mago_sale_ord_detail', [
|
||||
'id' => false,
|
||||
'primary_key' => ['sale_ord_id', 'sub_id'],
|
||||
'comment' => 'Copia locale MA_SaleOrdDetails (Mago4) - sola lettura',
|
||||
])
|
||||
->addColumn('sale_ord_id', 'integer', ['signed' => false])
|
||||
->addColumn('sub_id', 'integer')
|
||||
->addColumn('line', 'integer', ['default' => 0])
|
||||
->addColumn('position', 'integer', ['default' => 0])
|
||||
->addColumn('line_type', 'integer', ['default' => 0])
|
||||
->addColumn('item', 'string', ['limit' => 30, 'default' => ''])
|
||||
->addColumn('description', 'string', ['limit' => 255, 'default' => ''])
|
||||
->addColumn('uom', 'string', ['limit' => 10, 'default' => ''])
|
||||
->addColumn('qty', 'decimal', ['precision' => 15, 'scale' => 3, 'default' => 0])
|
||||
->addColumn('delivered_qty', 'decimal', ['precision' => 15, 'scale' => 3, 'default' => 0])
|
||||
->addColumn('invoiced_qty', 'decimal', ['precision' => 15, 'scale' => 3, 'default' => 0])
|
||||
->addColumn('unit_value', 'decimal', ['precision' => 15, 'scale' => 5, 'default' => 0])
|
||||
->addColumn('discount_amount', 'decimal', ['precision' => 15, 'scale' => 2, 'default' => 0])
|
||||
->addColumn('taxable_amount', 'decimal', ['precision' => 15, 'scale' => 2, 'default' => 0])
|
||||
->addColumn('total_amount', 'decimal', ['precision' => 15, 'scale' => 2, 'default' => 0])
|
||||
->addColumn('expected_delivery_date', 'date', ['null' => true])
|
||||
->addColumn('confirmed_delivery_date', 'date', ['null' => true])
|
||||
->addColumn('delivered', 'boolean', ['default' => false])
|
||||
->addColumn('invoiced', 'boolean', ['default' => false])
|
||||
->addColumn('cancelled', 'boolean', ['default' => false])
|
||||
->addColumn('customer', 'string', ['limit' => 20, 'default' => ''])
|
||||
->addColumn('order_date', 'date', ['null' => true])
|
||||
->addColumn('internal_ord_no', 'string', ['limit' => 30, 'default' => ''])
|
||||
->addColumn('notes', 'text', ['null' => true])
|
||||
->addColumn('tb_modified', 'datetime', ['null' => true])
|
||||
->addColumn('synced_at', 'datetime', ['null' => true])
|
||||
->addIndex(['item'])
|
||||
->addIndex(['order_date'])
|
||||
->addIndex(['expected_delivery_date'])
|
||||
->addIndex(['customer'])
|
||||
->create();
|
||||
|
||||
// Date di consegna ricavate dalle righe (la data in testata spesso resta = data ordine)
|
||||
$this->table('mago_sale_ord')
|
||||
->addColumn('line_expected_date', 'date', ['null' => true, 'after' => 'compulsory_delivery_date', 'comment' => 'Prima data prevista tra le righe aperte'])
|
||||
->addColumn('line_confirmed_date', 'date', ['null' => true, 'after' => 'line_expected_date', 'comment' => 'Prima data confermata tra le righe aperte'])
|
||||
->addColumn('lines_count', 'integer', ['default' => 0, 'after' => 'line_confirmed_date'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
* ========================================================= */
|
||||
|
||||
@@ -235,6 +235,11 @@
|
||||
<i class='bx bx-radio-circle'></i>Confronto anni
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="sales_orders_deliveries.php">
|
||||
<i class='bx bx-radio-circle'></i>Consegne
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -73,6 +73,8 @@ if ($action === 'data') {
|
||||
SUM(x.status = 'annullato') AS cancelled,
|
||||
SUM(x.status = 'bloccato') AS blocked,
|
||||
SUM(x.status IN ('aperto','bloccato') AND x.due_date < CURDATE()) AS late,
|
||||
SUM(x.status IN ('aperto','bloccato') AND x.client_date < CURDATE()) AS late_client,
|
||||
SUM(x.status IN ('aperto','bloccato') AND x.late_lines > 0 AND x.late_lines < x.open_lines) AS late_partial,
|
||||
SUM(x.delivered = 1 AND x.invoiced = 0 AND x.cancelled = 0) AS to_invoice,
|
||||
COUNT(DISTINCT x.customer) AS customers,
|
||||
AVG(CASE WHEN x.status <> 'annullato' AND x.due_date IS NOT NULL AND x.due_date >= x.order_date
|
||||
@@ -160,12 +162,30 @@ if ($action === 'data') {
|
||||
GROUP BY bucket ORDER BY bucket
|
||||
", $params);
|
||||
|
||||
// --- Top articoli (dalle righe, annullati esclusi) ---
|
||||
$topItems = MagoSalesStats::fetchAllQ($pdo, "
|
||||
SELECT d.item AS code,
|
||||
MAX(d.description) AS name,
|
||||
MAX(d.uom) AS uom,
|
||||
SUM(d.qty) AS qty,
|
||||
SUM(d.taxable_amount) AS value,
|
||||
COUNT(DISTINCT d.sale_ord_id) AS orders
|
||||
FROM mago_sale_ord_detail d
|
||||
JOIN $from ON x.sale_ord_id = d.sale_ord_id
|
||||
WHERE $where AND d.item <> '' AND d.cancelled = 0 AND x.status <> 'annullato'
|
||||
GROUP BY d.item
|
||||
ORDER BY " . ($metric === 'value' ? 'value' : 'orders') . " DESC
|
||||
LIMIT 10
|
||||
", $params);
|
||||
|
||||
// --- Elenco ordini ---
|
||||
$rows = MagoSalesStats::fetchAllQ($pdo, "
|
||||
SELECT x.sale_ord_id, x.internal_ord_no, x.external_ord_no,
|
||||
x.order_date, x.due_date, x.customer, x.company_name,
|
||||
x.salesperson, x.area, x.status, x.invoiced, x.your_reference,
|
||||
x.taxable_amount, x.total_amount,
|
||||
x.zibo_date, x.client_date, x.open_lines, x.late_lines, x.late_lines_client, x.open_dates,
|
||||
(x.status IN ('aperto','bloccato') AND x.client_date < CURDATE()) AS is_late_client,
|
||||
(x.summary_tb_modified IS NULL) AS no_amount,
|
||||
(x.status IN ('aperto','bloccato') AND x.due_date < CURDATE()) AS is_late
|
||||
FROM $from
|
||||
@@ -183,6 +203,7 @@ if ($action === 'data') {
|
||||
'by_area' => $byArea,
|
||||
'by_salesperson' => $bySalesperson,
|
||||
'due_buckets' => $dueBuckets,
|
||||
'top_items' => $topItems,
|
||||
'rows' => $rows,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
@@ -201,8 +222,22 @@ if ($action === 'order') {
|
||||
$raw = json_decode((string)$o['raw_json'], true) ?: [];
|
||||
unset($o['raw_json']);
|
||||
|
||||
$stL = $pdo->prepare("
|
||||
SELECT d.line, d.item, d.description, d.uom, d.qty, d.delivered_qty, d.invoiced_qty, d.unit_value,
|
||||
GREATEST(d.qty - d.delivered_qty, 0) AS residual_qty,
|
||||
d.taxable_amount, d.delivered, d.invoiced, d.cancelled, d.notes,
|
||||
" . MagoSalesStats::ziboDate('d') . " AS zibo_date,
|
||||
" . MagoSalesStats::clientDate('d') . " AS client_date
|
||||
FROM mago_sale_ord_detail d
|
||||
WHERE d.sale_ord_id = ?
|
||||
ORDER BY d.position, d.line, d.sub_id
|
||||
");
|
||||
$stL->execute([$id]);
|
||||
$lines = $stL->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
MagoSalesStats::jsonOut([
|
||||
'success' => true,
|
||||
'lines' => $lines,
|
||||
'order' => $o,
|
||||
'extra' => [
|
||||
'Cliente fatturazione' => $raw['InvoicingCustomer'] ?? '',
|
||||
@@ -654,6 +689,83 @@ if ($action === 'order') {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* date di consegna: ZIBO (scadenza) rossa se superata, cliente (desiderata) arancione */
|
||||
.dt-zibo {
|
||||
font-weight: 700;
|
||||
color: var(--c-ink);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dt-zibo.late {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.dt-client {
|
||||
font-size: .82rem;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dt-client.late {
|
||||
color: #ea580c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dt-tag {
|
||||
display: inline-block;
|
||||
font-size: .68rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: .3px;
|
||||
text-transform: uppercase;
|
||||
margin-right: 4px;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.dt-fallback {
|
||||
font-size: .72rem;
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.more-dates {
|
||||
display: inline-block;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
font-size: .7rem;
|
||||
font-weight: 800;
|
||||
border-radius: 999px;
|
||||
padding: 1px 7px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.partial-badge {
|
||||
background: #fff1e6;
|
||||
color: #c2410c;
|
||||
font-weight: 800;
|
||||
font-size: .75rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.late-badge,
|
||||
.partial-badge,
|
||||
.late-client-badge {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.late-client-badge {
|
||||
background: #fff7ed;
|
||||
color: #ea580c;
|
||||
font-weight: 800;
|
||||
font-size: .72rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.inv-badge {
|
||||
background: #efe9fb;
|
||||
color: var(--c-invoice);
|
||||
@@ -771,6 +883,7 @@ if ($action === 'order') {
|
||||
<div class="stats-tabs">
|
||||
<a href="sales_orders_stats.php" class="active"><i class="fa-solid fa-list-check me-1"></i> Andamento ordini</a>
|
||||
<a href="sales_orders_years.php"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
|
||||
<a href="sales_orders_deliveries.php"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
|
||||
</div>
|
||||
|
||||
<!-- FILTRI -->
|
||||
@@ -816,7 +929,8 @@ if ($action === 'order') {
|
||||
<label>Stato</label>
|
||||
<div class="chip-row" id="statusChips">
|
||||
<button type="button" class="chip" data-status="aperto" style="--chip-c:var(--c-open);">Aperti</button>
|
||||
<button type="button" class="chip" data-status="ritardo" style="--chip-c:var(--c-late);">In ritardo</button>
|
||||
<button type="button" class="chip" data-status="ritardo" style="--chip-c:var(--c-late);">In ritardo (ZIBO)</button>
|
||||
<button type="button" class="chip" data-status="ritardo_cliente" style="--chip-c:#ea580c;">Oltre data cliente</button>
|
||||
<button type="button" class="chip" data-status="bloccato" style="--chip-c:var(--c-block);">Bloccati</button>
|
||||
<button type="button" class="chip" data-status="evaso" style="--chip-c:var(--c-done);">Evasi</button>
|
||||
<button type="button" class="chip" data-status="da_fatturare" style="--chip-c:var(--c-invoice);">Da fatturare</button>
|
||||
@@ -876,7 +990,7 @@ if ($action === 'order') {
|
||||
<div class="kpi-icon"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
||||
<div class="kpi-label">In ritardo</div>
|
||||
<div class="kpi-value" id="kLate">—</div>
|
||||
<div class="kpi-sub">consegna prevista superata</div>
|
||||
<div class="kpi-sub" id="kLateSub">data ZIBO superata</div>
|
||||
</div>
|
||||
<div class="kpi" data-status="evaso" style="--k:var(--c-done);">
|
||||
<div class="kpi-icon"><i class="fa-solid fa-truck"></i></div>
|
||||
@@ -947,6 +1061,16 @@ if ($action === 'order') {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title" id="ttlItems">Top 10 articoli per numero ordini</div>
|
||||
<div class="chart-hint">Dalle righe ordine · passa sopra una barra per quantità e valore</div>
|
||||
<div id="chItems" style="min-height:380px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="chart-card">
|
||||
@@ -976,7 +1100,7 @@ if ($action === 'order') {
|
||||
<th>Data</th>
|
||||
<th>Cliente</th>
|
||||
<th>Rif. cliente</th>
|
||||
<th>Consegna prev.</th>
|
||||
<th>Consegna <span class="small fw-normal">(ZIBO / cliente)</span></th>
|
||||
<th class="text-end">Imponibile</th>
|
||||
<th>Stato</th>
|
||||
<th>Fatt.</th>
|
||||
@@ -1198,7 +1322,11 @@ if ($action === 'order') {
|
||||
if (f.statuses.length === 1) $(`.kpi[data-status="${f.statuses[0]}"]`).addClass('selected');
|
||||
}
|
||||
|
||||
// aperta da link (?q=numero ordine): non sovrascrivo i filtri salvati dell'utente
|
||||
const URL_Q = new URLSearchParams(location.search).get('q');
|
||||
|
||||
function saveFilters(f) {
|
||||
if (URL_Q) return;
|
||||
try {
|
||||
localStorage.setItem(STORE_KEY, JSON.stringify(Object.assign({}, f, {
|
||||
metric: metric
|
||||
@@ -1319,7 +1447,13 @@ if ($action === 'order') {
|
||||
renderDue(d.due_buckets);
|
||||
renderSimpleBar('chArea', d.by_area, '#0ea5e9', 'areas');
|
||||
renderSimpleBar('chSalesperson', d.by_salesperson, '#6f42c1', 'salespeople');
|
||||
renderItems(d.top_items || []);
|
||||
renderTable(d.rows);
|
||||
if (window.__autoOpen) {
|
||||
const hit = d.rows.find(r => r.internal_ord_no === window.__autoOpen) || (d.rows.length === 1 ? d.rows[0] : null);
|
||||
window.__autoOpen = null;
|
||||
if (hit) openOrder(hit.sale_ord_id);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
$('#statsContent').css('opacity', 1);
|
||||
@@ -1336,6 +1470,7 @@ if ($action === 'order') {
|
||||
$('#ttlDue').text(v ? 'Scadenze ordini aperti (valore)' : 'Scadenze ordini aperti');
|
||||
$('#ttlArea').text(v ? 'Valore per area' : 'Ordini per area');
|
||||
$('#ttlSalesperson').text(v ? 'Valore per agente' : 'Ordini per agente');
|
||||
$('#ttlItems').text(v ? 'Top 10 articoli per valore ordinato' : 'Top 10 articoli per numero ordini');
|
||||
}
|
||||
|
||||
function renderKpi(k) {
|
||||
@@ -1343,6 +1478,9 @@ if ($action === 'order') {
|
||||
$('#kOrders').text(fmtNum(orders));
|
||||
$('#kOpen').text(fmtNum(k.open_orders));
|
||||
$('#kLate').text(fmtNum(k.late));
|
||||
$('#kLateSub').html(`data ZIBO superata` +
|
||||
(+k.late_partial ? ` · ${fmtNum(k.late_partial)} parziali` : '') +
|
||||
`<br><span style="color:#ea580c;font-weight:700;">${fmtNum(k.late_client)}</span> oltre data cliente`);
|
||||
$('#kDelivered').text(fmtNum(k.delivered));
|
||||
$('#kToInvoice').text(fmtNum(k.to_invoice));
|
||||
$('#kCustomers').text(fmtNum(k.customers));
|
||||
@@ -1695,16 +1833,113 @@ if ($action === 'order') {
|
||||
});
|
||||
}
|
||||
|
||||
function renderItems(rows) {
|
||||
const v = metric === 'value';
|
||||
const label = r => {
|
||||
const n = (r.name || '').trim();
|
||||
const t = n ? `${r.code} · ${n}` : r.code;
|
||||
return t.length > 60 ? t.substring(0, 58) + '…' : t;
|
||||
};
|
||||
upsertChart('items', 'chItems', {
|
||||
chart: {
|
||||
...baseChart,
|
||||
type: 'bar',
|
||||
height: 380
|
||||
},
|
||||
series: [{
|
||||
name: v ? 'Valore' : 'Ordini',
|
||||
data: rows.map(r => v ? +r.value : +r.orders)
|
||||
}],
|
||||
colors: ['#0f766e'],
|
||||
xaxis: {
|
||||
categories: rows.map(label),
|
||||
labels: {
|
||||
formatter: fmtMetricAxis
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
maxWidth: 380
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
borderRadius: 4,
|
||||
barHeight: '65%'
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
grid: {
|
||||
borderColor: '#eef2f7'
|
||||
},
|
||||
tooltip: {
|
||||
custom: ({
|
||||
dataPointIndex
|
||||
}) => {
|
||||
const r = rows[dataPointIndex];
|
||||
if (!r) return '';
|
||||
return `<div style="padding:8px 12px;font-size:.85rem;">
|
||||
<div style="font-weight:800;">${esc(r.code)}</div>
|
||||
<div style="color:#64748b;max-width:320px;white-space:normal;">${esc(r.name)}</div>
|
||||
<div style="margin-top:6px;">Ordini: <b>${fmtNum(r.orders)}</b></div>
|
||||
<div>Quantità: <b>${fmtNum(Math.round(r.qty))} ${esc(r.uom)}</b></div>
|
||||
<div>Valore: <b>${fmtEur(r.value)}</b></div>
|
||||
</div>`;
|
||||
}
|
||||
},
|
||||
noData: {
|
||||
text: 'Nessuna riga articolo nel periodo'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// cella consegna: data ZIBO (rossa se superata) + data cliente (arancione se superata)
|
||||
function dateCell(zibo, client, opts) {
|
||||
opts = opts || {};
|
||||
const today = iso(new Date());
|
||||
const open = opts.open !== false;
|
||||
const zLate = open && zibo && zibo < today;
|
||||
const cLate = open && client && client < today;
|
||||
let html = '';
|
||||
if (zibo) {
|
||||
html += `<div class="dt-zibo${zLate ? ' late' : ''}"><span class="dt-tag">ZIBO</span>${fmtDate(zibo)}${opts.more ? `<span class="more-dates" title="Le righe aperte hanno date di consegna diverse">+${opts.more} date</span>` : ''}</div>`;
|
||||
} else {
|
||||
html += `<div class="dt-fallback">data ZIBO non indicata</div>`;
|
||||
}
|
||||
if (client) {
|
||||
html += `<div class="dt-client${cLate ? ' late' : ''}"><span class="dt-tag">Cliente</span>${fmtDate(client)}</div>`;
|
||||
}
|
||||
return html || '—';
|
||||
}
|
||||
|
||||
function lateBadges(r) {
|
||||
if (r.status !== 'aperto' && r.status !== 'bloccato') return '';
|
||||
const lateL = +r.late_lines,
|
||||
openL = +r.open_lines;
|
||||
if (+r.is_late) {
|
||||
return lateL > 0 && openL > lateL ?
|
||||
`<span class="partial-badge" title="Righe aperte in ritardo sulla data ZIBO">PARZIALE ${lateL}/${openL} righe</span>` :
|
||||
'<span class="late-badge">IN RITARDO</span>';
|
||||
}
|
||||
return +r.is_late_client ? '<span class="late-client-badge" title="Data ZIBO non ancora superata">OLTRE DATA CLIENTE</span>' : '';
|
||||
}
|
||||
|
||||
function renderTable(rows) {
|
||||
const data = rows.map(r => [
|
||||
`<b>${esc(r.internal_ord_no)}</b>${r.external_ord_no ? `<div class="small text-muted">${esc(r.external_ord_no)}</div>` : ''}`,
|
||||
`<span data-order="${esc(r.order_date || '')}">${fmtDate(r.order_date)}</span>`,
|
||||
`<div class="fw-semibold">${esc(r.company_name)}</div><div class="small text-muted">${esc(r.customer)}</div>`,
|
||||
esc(r.your_reference),
|
||||
`<span data-order="${esc(r.due_date || '')}">${fmtDate(r.due_date) || '—'}</span>`,
|
||||
`<span data-order="${esc(r.due_date || '')}">${dateCell(r.zibo_date, r.client_date, {
|
||||
open: r.status === 'aperto' || r.status === 'bloccato',
|
||||
more: +r.open_dates > 1 ? (+r.open_dates - 1) : 0
|
||||
})}</span>`,
|
||||
+r.no_amount ? '<span data-order="-1" class="text-muted">n.d.</span>' :
|
||||
`<span data-order="${+r.taxable_amount}" class="amount${r.status === 'annullato' ? ' text-muted text-decoration-line-through' : ''}">${fmtEur(r.taxable_amount, 2)}</span>`,
|
||||
`<span class="st-badge st-${esc(r.status)}">${STATUS_LABEL[r.status] || esc(r.status)}</span>${+r.is_late ? '<span class="late-badge">IN RITARDO</span>' : ''}`,
|
||||
`<span class="st-badge st-${esc(r.status)}">${STATUS_LABEL[r.status] || esc(r.status)}</span>` + lateBadges(r),
|
||||
+r.invoiced ? '<span class="inv-badge">Sì</span>' : '<span class="text-muted">—</span>',
|
||||
esc(r.salesperson),
|
||||
esc(r.area),
|
||||
@@ -1756,7 +1991,7 @@ if ($action === 'order') {
|
||||
columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
format: {
|
||||
// importo esportato come numero, il resto come testo
|
||||
body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('<div>').html(String(d).replace(/<(div|span)/g, ' <$1')).text().replace(/\s+/g, ' ').trim()
|
||||
body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('<div>').html(String(d).replace(/<(div|span)/g, ' <$1').replace(/<\/span>/g, '</span> ')).text().replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1769,7 +2004,7 @@ if ($action === 'order') {
|
||||
columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
format: {
|
||||
// importo esportato come numero, il resto come testo
|
||||
body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('<div>').html(String(d).replace(/<(div|span)/g, ' <$1')).text().replace(/\s+/g, ' ').trim()
|
||||
body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('<div>').html(String(d).replace(/<(div|span)/g, ' <$1').replace(/<\/span>/g, '</span> ')).text().replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1825,19 +2060,58 @@ if ($action === 'order') {
|
||||
</div>` : '<div class="warn-strip mb-3">Importi non ancora letti da Mago.</div>'}
|
||||
<div class="ro-grid mb-3">
|
||||
<div class="l">Data ordine</div><div>${fmtDate(o.order_date) || '—'}</div>
|
||||
<div class="l">Consegna prevista</div><div>${fmtDate(o.expected_delivery_date) || '—'}</div>
|
||||
<div class="l">Consegna confermata</div><div>${fmtDate(o.confirmed_delivery_date) || '—'}</div>
|
||||
<div class="l">Consegna</div><div>${dateCell(o.zibo_date, o.client_date, {open: o.status === 'aperto' || o.status === 'bloccato'})}${+o.open_lines ? ` <span class="small text-muted">prima riga da consegnare · ${fmtNum(o.open_lines)} righe aperte${+o.late_lines ? `, <b style="color:#dc3545;">${fmtNum(o.late_lines)} in ritardo</b>` : ''}</span>` : ''}</div>
|
||||
<div class="l">N° ordine cliente</div><div>${esc(o.external_ord_no) || '—'}</div>
|
||||
<div class="l">Agente</div><div>${esc(o.salesperson) || '—'}</div>
|
||||
<div class="l">Area</div><div>${esc(o.area) || '—'}</div>
|
||||
${extra}
|
||||
</div>
|
||||
${o.notes ? `<div class="p-3 rounded" style="background:#f8fafc;border:1px solid #e2e8f0;white-space:pre-wrap;"><b>Note:</b><br>${esc(o.notes)}</div>` : ''}
|
||||
<div class="mt-3 small text-muted"><i class="fa-solid fa-circle-info me-1"></i>Le righe articolo non sono disponibili: la tabella righe ordine non è esposta dalle API di Mago.</div>
|
||||
${renderLines(d.lines || [])}
|
||||
${o.notes ? `<div class="p-3 rounded mt-3" style="background:#f8fafc;border:1px solid #e2e8f0;white-space:pre-wrap;"><b>Note:</b><br>${esc(o.notes)}</div>` : ''}
|
||||
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
function renderLines(lines) {
|
||||
if (!lines.length) {
|
||||
return '<div class="warn-strip">Righe non ancora sincronizzate: premi <b>Aggiorna da Mago</b>.</div>';
|
||||
}
|
||||
const today = iso(new Date());
|
||||
const rows = lines.map(l => {
|
||||
const open = !+l.delivered && !+l.cancelled;
|
||||
const due = l.zibo_date || l.client_date;
|
||||
const late = open && due && due < today;
|
||||
const days = late ? Math.round((new Date(today) - new Date(due)) / 86400000) : 0;
|
||||
const st = +l.cancelled ? '<span class="st-badge st-annullato">Annullata</span>' :
|
||||
(+l.delivered ? '<span class="st-badge st-evaso">Consegnata</span>' :
|
||||
'<span class="st-badge st-aperto">Aperta</span>' + (late ? `<span class="late-badge">+${days} gg</span>` : ''));
|
||||
const qtyTxt = n => fmtNum(Math.round(+n * 100) / 100);
|
||||
return `<tr>
|
||||
<td><b>${esc(l.item)}</b><div class="small text-muted" style="white-space:normal;max-width:320px;">${esc(l.description)}</div></td>
|
||||
<td class="text-end amount">${qtyTxt(l.qty)} ${esc(l.uom)}</td>
|
||||
<td class="text-end amount">${qtyTxt(l.delivered_qty)}</td>
|
||||
<td class="text-end amount">${open ? '<b>' + qtyTxt(l.residual_qty) + '</b>' : '<span class="text-muted">—</span>'}</td>
|
||||
<td>${dateCell(l.zibo_date, l.client_date, {open: open})}</td>
|
||||
<td class="text-end amount">${fmtEur(l.taxable_amount, 2)}</td>
|
||||
<td>${st}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="fw-bold mb-2" style="color:var(--c-ink);">Righe ordine (${lines.length})</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle" style="font-size:.9rem;">
|
||||
<thead style="background:#f1f5f9;">
|
||||
<tr>
|
||||
<th>Articolo</th><th class="text-end">Qtà</th><th class="text-end">Consegnata</th><th class="text-end">Residuo</th>
|
||||
<th>Consegna (ZIBO / cliente)</th><th class="text-end">Imponibile</th><th>Stato</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ---------------- SINCRONIZZAZIONE ---------------- */
|
||||
function runSync(mode) {
|
||||
const labels = {
|
||||
@@ -1875,6 +2149,7 @@ if ($action === 'order') {
|
||||
icon: 'success',
|
||||
title: 'Dati aggiornati',
|
||||
html: `Ordini letti: <b>${fmtNum(o.records)}</b>` +
|
||||
(d.righe ? `<br>Righe lette: <b>${fmtNum(d.righe.records)}</b>` : '') +
|
||||
(d.importi ? `<br>Totali letti: <b>${fmtNum(d.importi.records)}</b>` : '') +
|
||||
(c.records !== undefined ? `<br>Clienti aggiornati: <b>${fmtNum(c.records)}</b>` : '') +
|
||||
(d.logoff !== 'OK' ? `<div class="small text-muted mt-2">Logoff Mago: ${esc(d.logoff)}</div>` : ''),
|
||||
@@ -1958,8 +2233,16 @@ if ($action === 'order') {
|
||||
loadFilterOptions(false).then(d => {
|
||||
window.__totalOrders = d && d.sync ? d.sync.total_orders : 0;
|
||||
|
||||
const saved = restoreFilters();
|
||||
const saved = URL_Q ? null : restoreFilters();
|
||||
if (saved && saved.metric === 'value') metric = 'value';
|
||||
if (URL_Q) {
|
||||
// tutto lo storico, solo l'ordine cercato; se è uno solo apro il dettaglio
|
||||
$('#fFrom').val((d && d.range && d.range.min_date) || '2020-01-01');
|
||||
$('#fTo').val(iso(new Date()));
|
||||
$('#periodChips .chip').removeClass('active');
|
||||
$('#fSearch').val(URL_Q);
|
||||
window.__autoOpen = URL_Q;
|
||||
}
|
||||
if (saved && saved.from) {
|
||||
$('#fFrom').val(saved.from);
|
||||
$('#fTo').val(saved.to);
|
||||
@@ -1968,7 +2251,7 @@ if ($action === 'order') {
|
||||
$('#fAreas').val(saved.areas || []).trigger('change.select2');
|
||||
(saved.statuses || []).forEach(s => $(`#statusChips .chip[data-status="${s}"]`).addClass('active'));
|
||||
$('#fSearch').val(saved.q || '');
|
||||
} else {
|
||||
} else if (!URL_Q) {
|
||||
setPeriod('y' + new Date().getFullYear(), true);
|
||||
}
|
||||
loadData();
|
||||
|
||||
@@ -584,6 +584,7 @@ if ($action === 'data') {
|
||||
<div class="stats-tabs">
|
||||
<a href="sales_orders_stats.php"><i class="fa-solid fa-list-check me-1"></i> Andamento ordini</a>
|
||||
<a href="sales_orders_years.php" class="active"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
|
||||
<a href="sales_orders_deliveries.php"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
|
||||
</div>
|
||||
|
||||
<!-- FILTRI -->
|
||||
|
||||
Reference in New Issue
Block a user