added fatturato
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Documenti di vendita da MA_SaleDoc + importi da MA_SaleDocSummary (Mago4).
|
||||||
|
* Servono per il FATTURATO per data documento (fatture, fatture d'acconto, note di credito).
|
||||||
|
* Si salvano tutti i tipi (anche DDT): quali contano come fatturato si decide in MagoSalesStats.
|
||||||
|
*/
|
||||||
|
final class CreateMagoSaleDoc extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$this->table('mago_sale_doc', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['sale_doc_id'],
|
||||||
|
'comment' => 'Copia locale MA_SaleDoc + MA_SaleDocSummary (Mago4) - sola lettura',
|
||||||
|
])
|
||||||
|
->addColumn('sale_doc_id', 'integer', ['signed' => false])
|
||||||
|
->addColumn('document_type', 'integer', ['default' => 0, 'comment' => '3407874 fattura, 3407883 acconto, 3407876 nota credito, 3407873 DDT...'])
|
||||||
|
->addColumn('doc_no', 'string', ['limit' => 30, 'default' => ''])
|
||||||
|
->addColumn('document_date', 'date', ['null' => true])
|
||||||
|
->addColumn('posting_date', 'date', ['null' => true])
|
||||||
|
->addColumn('customer', 'string', ['limit' => 20, 'default' => ''])
|
||||||
|
->addColumn('invoicing_customer', 'string', ['limit' => 20, 'default' => ''])
|
||||||
|
->addColumn('salesperson', 'string', ['limit' => 20, 'default' => ''])
|
||||||
|
->addColumn('area', 'string', ['limit' => 20, 'default' => ''])
|
||||||
|
->addColumn('currency', 'string', ['limit' => 8, 'default' => ''])
|
||||||
|
->addColumn('tax_journal', 'string', ['limit' => 20, 'default' => '', 'comment' => 'Registro IVA: vuoto per DDT/proforma'])
|
||||||
|
->addColumn('inv_rsn', 'string', ['limit' => 20, 'default' => ''])
|
||||||
|
->addColumn('ei_document_type', 'integer', ['default' => 0])
|
||||||
|
->addColumn('issued', 'boolean', ['default' => false])
|
||||||
|
->addColumn('cancelled', 'boolean', ['default' => false])
|
||||||
|
->addColumn('posted_to_accounting', 'boolean', ['default' => false])
|
||||||
|
->addColumn('taxable_amount', 'decimal', ['precision' => 15, 'scale' => 2, 'null' => true])
|
||||||
|
->addColumn('tax_amount', 'decimal', ['precision' => 15, 'scale' => 2, 'null' => true])
|
||||||
|
->addColumn('total_amount', 'decimal', ['precision' => 15, 'scale' => 2, 'null' => true])
|
||||||
|
->addColumn('summary_tb_modified', 'datetime', ['null' => true])
|
||||||
|
->addColumn('tb_created', 'datetime', ['null' => true])
|
||||||
|
->addColumn('tb_modified', 'datetime', ['null' => true])
|
||||||
|
->addColumn('synced_at', 'datetime', ['null' => true])
|
||||||
|
->addIndex(['document_date'])
|
||||||
|
->addIndex(['document_type', 'document_date'])
|
||||||
|
->addIndex(['customer'])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,39 @@ class MagoSalesStats
|
|||||||
return "DATE_ADD($z, INTERVAL (" . (int)self::ZIBO_WEEK_END_DAYS . " - WEEKDAY($z)) DAY)";
|
return "DATE_ADD($z, INTERVAL (" . (int)self::ZIBO_WEEK_END_DAYS . " - WEEKDAY($z)) DAY)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FATTURATO: tipi di documento di vendita che contano (verificati sui dati reali il 14/10/2026)
|
||||||
|
* e segno con cui contano. Esclusi: DDT 3407873, DDT non valorizzati 3407881,
|
||||||
|
* proforma 3407888, movimenti deposito 3407890.
|
||||||
|
*/
|
||||||
|
const INVOICE_TYPES = [
|
||||||
|
3407874 => 1, // Fattura (immediata / differita) serie A ...
|
||||||
|
3407883 => 1, // Fattura d'acconto (TD02) serie B
|
||||||
|
3407876 => -1, // Nota di credito (TD04) → in negativo serie D ...
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Vista fatture/note di credito (alias i) con imponibile già col segno giusto (revenue). */
|
||||||
|
public static function invoiceFrom()
|
||||||
|
{
|
||||||
|
$types = implode(',', array_map('intval', array_keys(self::INVOICE_TYPES)));
|
||||||
|
$sign = 'CASE d.document_type';
|
||||||
|
foreach (self::INVOICE_TYPES as $t => $sg) $sign .= ' WHEN ' . (int)$t . ' THEN ' . ((int)$sg < 0 ? -1 : 1);
|
||||||
|
$sign .= ' ELSE 0 END';
|
||||||
|
|
||||||
|
return "(
|
||||||
|
SELECT d.sale_doc_id, d.document_type, d.doc_no, d.document_date,
|
||||||
|
d.customer, d.salesperson, d.area,
|
||||||
|
COALESCE(NULLIF(c.company_name, ''), d.customer) AS company_name,
|
||||||
|
($sign) * COALESCE(d.taxable_amount, 0) AS revenue,
|
||||||
|
($sign) < 0 AS is_credit_note,
|
||||||
|
d.summary_tb_modified IS NULL AS no_amount
|
||||||
|
FROM mago_sale_doc d
|
||||||
|
LEFT JOIN mago_cust_supp c
|
||||||
|
ON c.cust_supp_type = " . self::CUSTOMER_TYPE . " AND c.cust_supp = d.customer
|
||||||
|
WHERE d.document_type IN ($types) AND d.cancelled = 0
|
||||||
|
) i";
|
||||||
|
}
|
||||||
|
|
||||||
/** Scadenza di testata (usata solo per ordini senza righe). */
|
/** Scadenza di testata (usata solo per ordini senza righe). */
|
||||||
public static function headerDue($a)
|
public static function headerDue($a)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class MagoSync
|
|||||||
const ENTITY_CUSTOMER = 'cust_supp';
|
const ENTITY_CUSTOMER = 'cust_supp';
|
||||||
const ENTITY_SUMMARY = 'sale_ord_summary';
|
const ENTITY_SUMMARY = 'sale_ord_summary';
|
||||||
const ENTITY_DETAILS = 'sale_ord_details';
|
const ENTITY_DETAILS = 'sale_ord_details';
|
||||||
|
const ENTITY_DOCS = 'sale_doc';
|
||||||
|
const ENTITY_DOC_SUMM = 'sale_doc_summary';
|
||||||
|
|
||||||
/** @var PDO */
|
/** @var PDO */
|
||||||
private $pdo;
|
private $pdo;
|
||||||
@@ -65,6 +67,15 @@ class MagoSync
|
|||||||
$report['ordini'] = $this->syncSaleOrders($from, $limit, $full);
|
$report['ordini'] = $this->syncSaleOrders($from, $limit, $full);
|
||||||
$report['righe'] = $this->syncSaleOrderDetails($from, $full, (bool)$limit);
|
$report['righe'] = $this->syncSaleOrderDetails($from, $full, (bool)$limit);
|
||||||
$report['importi'] = $this->syncSaleOrderSummaries($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)');
|
||||||
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$report['ok'] = false;
|
$report['ok'] = false;
|
||||||
$report['message'] = $e->getMessage();
|
$report['message'] = $e->getMessage();
|
||||||
@@ -525,6 +536,239 @@ class MagoSync
|
|||||||
return ['records' => $read, 'aggiornati' => $updated, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
|
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)
|
* ANAGRAFICA CLIENTI (per decodificare i codici cliente)
|
||||||
* ========================================================= */
|
* ========================================================= */
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mago_probe_saledoc2_cli.php - SOLO LETTURA. Riepilogo dei documenti di vendita PER TIPO
|
||||||
|
* (n. documenti, imponibile totale, registro IVA, causale...) per capire quali sono fatture
|
||||||
|
* e quali note di credito. Non scrive nulla nel database.
|
||||||
|
*
|
||||||
|
* Uso: php mago_probe_saledoc2_cli.php --from=2026-01-01 > probe_saledoc2.txt 2>&1
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Solo da riga di comando');
|
||||||
|
}
|
||||||
|
|
||||||
|
set_time_limit(0);
|
||||||
|
|
||||||
|
$dirs = [__DIR__, dirname(__DIR__), dirname(__DIR__, 2), dirname(__DIR__, 3)];
|
||||||
|
foreach ($dirs as $d) {
|
||||||
|
if (file_exists($d . '/vendor/autoload.php')) {
|
||||||
|
require_once $d . '/vendor/autoload.php';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (class_exists('Dotenv\Dotenv')) {
|
||||||
|
foreach ($dirs as $d) {
|
||||||
|
if (file_exists($d . '/.env')) {
|
||||||
|
Dotenv\Dotenv::createImmutable($d)->safeLoad();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require_once __DIR__ . '/class/MagoClient.php';
|
||||||
|
|
||||||
|
$opts = getopt('', ['from::']);
|
||||||
|
$from = $opts['from'] ?? date('Y') . '-01-01';
|
||||||
|
|
||||||
|
$mago = MagoClient::fromEnv();
|
||||||
|
try {
|
||||||
|
$mago->login();
|
||||||
|
|
||||||
|
// 1) tutte le testate del periodo, solo i campi utili
|
||||||
|
$fields = [
|
||||||
|
'SaleDocId',
|
||||||
|
'DocumentType',
|
||||||
|
'DocNo',
|
||||||
|
'DocumentDate',
|
||||||
|
'CustSupp',
|
||||||
|
'TaxJournal',
|
||||||
|
'InvoicingTaxJournal',
|
||||||
|
'InvRsn',
|
||||||
|
'ShippingReason',
|
||||||
|
'EIDocumentType',
|
||||||
|
'InvoiceFollows',
|
||||||
|
'Issued',
|
||||||
|
'Cancelled',
|
||||||
|
'PostedToAccounting',
|
||||||
|
'CorrectionDocument',
|
||||||
|
'StubBook'
|
||||||
|
];
|
||||||
|
$docs = [];
|
||||||
|
$minId = PHP_INT_MAX;
|
||||||
|
foreach (
|
||||||
|
$mago->iterate('MA_SaleDoc', [
|
||||||
|
'filters' => [['key' => 'DocumentDate', 'value' => $from, 'operator' => '>=']],
|
||||||
|
'fields' => $fields,
|
||||||
|
]) as $r
|
||||||
|
) {
|
||||||
|
$id = (int)$r['SaleDocId'];
|
||||||
|
$docs[$id] = $r;
|
||||||
|
$minId = min($minId, $id);
|
||||||
|
}
|
||||||
|
echo "Documenti dal $from: " . count($docs) . PHP_EOL;
|
||||||
|
|
||||||
|
// 2) importi: leggo i totali con SaleDocId >= il più piccolo del periodo
|
||||||
|
$tax = [];
|
||||||
|
if ($docs) {
|
||||||
|
foreach (
|
||||||
|
$mago->iterate('MA_SaleDocSummary', [
|
||||||
|
'filters' => [['key' => 'SaleDocId', 'value' => (string)$minId, 'operator' => '>=']],
|
||||||
|
'fields' => ['SaleDocId', 'TaxableAmount', 'TotalAmount'],
|
||||||
|
]) as $r
|
||||||
|
) {
|
||||||
|
$tax[(int)$r['SaleDocId']] = (float)$r['TaxableAmount'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "Totali letti: " . count($tax) . PHP_EOL;
|
||||||
|
|
||||||
|
// 3) riepilogo per tipo
|
||||||
|
$sum = [];
|
||||||
|
$add = function (&$arr, $v) {
|
||||||
|
$v = (string)$v;
|
||||||
|
if ($v === '') $v = '(vuoto)';
|
||||||
|
$arr[$v] = ($arr[$v] ?? 0) + 1;
|
||||||
|
};
|
||||||
|
foreach ($docs as $id => $r) {
|
||||||
|
$t = (string)$r['DocumentType'];
|
||||||
|
if (!isset($sum[$t])) $sum[$t] = [
|
||||||
|
'documenti' => 0,
|
||||||
|
'imponibile' => 0.0,
|
||||||
|
'senza_importo' => 0,
|
||||||
|
'annullati' => 0,
|
||||||
|
'in_contabilita' => 0,
|
||||||
|
'TaxJournal' => [],
|
||||||
|
'InvRsn' => [],
|
||||||
|
'ShippingReason' => [],
|
||||||
|
'EIDocumentType' => [],
|
||||||
|
'StubBook' => [],
|
||||||
|
'InvoiceFollows' => [],
|
||||||
|
'esempi' => []
|
||||||
|
];
|
||||||
|
$s = &$sum[$t];
|
||||||
|
$s['documenti']++;
|
||||||
|
if (isset($tax[$id])) $s['imponibile'] += $tax[$id];
|
||||||
|
else $s['senza_importo']++;
|
||||||
|
if (!empty($r['Cancelled'])) $s['annullati']++;
|
||||||
|
if (!empty($r['PostedToAccounting'])) $s['in_contabilita']++;
|
||||||
|
$add($s['TaxJournal'], $r['TaxJournal'] ?? '');
|
||||||
|
$add($s['InvRsn'], $r['InvRsn'] ?? '');
|
||||||
|
$add($s['ShippingReason'], $r['ShippingReason'] ?? '');
|
||||||
|
$add($s['EIDocumentType'], $r['EIDocumentType'] ?? '');
|
||||||
|
$add($s['StubBook'], $r['StubBook'] ?? '');
|
||||||
|
$add($s['InvoiceFollows'], !empty($r['InvoiceFollows']) ? 'si' : 'no');
|
||||||
|
if (count($s['esempi']) < 4) $s['esempi'][] = $r['DocNo'] . ' del ' . substr((string)$r['DocumentDate'], 0, 10) . ' cliente ' . $r['CustSupp'] . ' imponibile ' . ($tax[$id] ?? '?');
|
||||||
|
unset($s);
|
||||||
|
}
|
||||||
|
ksort($sum);
|
||||||
|
foreach ($sum as &$s) {
|
||||||
|
$s['imponibile'] = round($s['imponibile'], 2);
|
||||||
|
foreach (['TaxJournal', 'InvRsn', 'ShippingReason', 'EIDocumentType', 'StubBook', 'InvoiceFollows'] as $k) arsort($s[$k]);
|
||||||
|
}
|
||||||
|
unset($s);
|
||||||
|
|
||||||
|
echo PHP_EOL . '=== Riepilogo per DocumentType ===' . PHP_EOL;
|
||||||
|
echo json_encode($sum, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
fwrite(STDERR, PHP_EOL . 'ERRORE: ' . $e->getMessage() . PHP_EOL);
|
||||||
|
} finally {
|
||||||
|
$mago->logoff();
|
||||||
|
echo PHP_EOL . 'Logoff eseguito.' . PHP_EOL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mago_probe_saledoc_cli.php - SOLO LETTURA. Esplora i documenti di vendita (fatture) in Mago
|
||||||
|
* per preparare i grafici del fatturato. Non scrive nulla nel database.
|
||||||
|
*
|
||||||
|
* Uso: php mago_probe_saledoc_cli.php
|
||||||
|
* php mago_probe_saledoc_cli.php --from=2025-01-01
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Solo da riga di comando');
|
||||||
|
}
|
||||||
|
|
||||||
|
set_time_limit(0);
|
||||||
|
|
||||||
|
$dirs = [__DIR__, dirname(__DIR__), dirname(__DIR__, 2), dirname(__DIR__, 3)];
|
||||||
|
foreach ($dirs as $d) {
|
||||||
|
if (file_exists($d . '/vendor/autoload.php')) {
|
||||||
|
require_once $d . '/vendor/autoload.php';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (class_exists('Dotenv\Dotenv')) {
|
||||||
|
foreach ($dirs as $d) {
|
||||||
|
if (file_exists($d . '/.env')) {
|
||||||
|
Dotenv\Dotenv::createImmutable($d)->safeLoad();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require_once __DIR__ . '/class/MagoClient.php';
|
||||||
|
|
||||||
|
$opts = getopt('', ['from::']);
|
||||||
|
$from = $opts['from'] ?? date('Y') . '-01-01';
|
||||||
|
|
||||||
|
$out = function ($title, $data = null) {
|
||||||
|
echo PHP_EOL . '=== ' . $title . ' ===' . PHP_EOL;
|
||||||
|
if ($data !== null) echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||||
|
};
|
||||||
|
|
||||||
|
$mago = MagoClient::fromEnv();
|
||||||
|
try {
|
||||||
|
$mago->login();
|
||||||
|
$f = [['key' => 'DocumentDate', 'value' => $from, 'operator' => '>=']];
|
||||||
|
|
||||||
|
// 1) testata documento: 2 record completi (tutti i campi)
|
||||||
|
$r = $mago->getDataTable('MA_SaleDoc', ['filters' => $f, 'recordsPerPage' => 2]);
|
||||||
|
$out("MA_SaleDoc dal $from: totale documenti = " . $r['total'] . ' (2 esempi completi)', $r['records']);
|
||||||
|
|
||||||
|
// 2) quanti documenti per TIPO (fattura, DDT, nota di credito...): mi serve per capire i codici
|
||||||
|
$types = [];
|
||||||
|
$examples = [];
|
||||||
|
foreach ($mago->iterate('MA_SaleDoc', ['filters' => $f, 'fields' => ['SaleDocId', 'DocumentType', 'DocNo', 'DocumentDate']]) as $rec) {
|
||||||
|
$t = (string)($rec['DocumentType'] ?? '?');
|
||||||
|
$types[$t] = ($types[$t] ?? 0) + 1;
|
||||||
|
if (!isset($examples[$t])) $examples[$t] = ($rec['DocNo'] ?? '') . ' del ' . substr((string)($rec['DocumentDate'] ?? ''), 0, 10) . ' (SaleDocId ' . ($rec['SaleDocId'] ?? '') . ')';
|
||||||
|
}
|
||||||
|
ksort($types);
|
||||||
|
$rows = [];
|
||||||
|
foreach ($types as $t => $n) $rows[] = ['DocumentType' => $t, 'documenti' => $n, 'esempio' => $examples[$t]];
|
||||||
|
$out('Documenti per DocumentType', $rows);
|
||||||
|
|
||||||
|
// 3) totali: 2 record completi
|
||||||
|
$r = $mago->getDataTable('MA_SaleDocSummary', ['recordsPerPage' => 2]);
|
||||||
|
$out('MA_SaleDocSummary (2 esempi completi)', $r['records']);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
fwrite(STDERR, PHP_EOL . 'ERRORE: ' . $e->getMessage() . PHP_EOL);
|
||||||
|
} finally {
|
||||||
|
$mago->logoff();
|
||||||
|
echo PHP_EOL . 'Logoff eseguito.' . PHP_EOL;
|
||||||
|
}
|
||||||
@@ -73,6 +73,8 @@ if ($action === 'data') {
|
|||||||
|
|
||||||
$monthly = [];
|
$monthly = [];
|
||||||
$customers = [];
|
$customers = [];
|
||||||
|
$yearIn = '1=0';
|
||||||
|
$yp = $params;
|
||||||
|
|
||||||
if ($years) {
|
if ($years) {
|
||||||
$ph = [];
|
$ph = [];
|
||||||
@@ -110,8 +112,41 @@ if ($action === 'data') {
|
|||||||
$customers = array_values($customers);
|
$customers = array_values($customers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- FATTURATO per data documento (fatture + acconti - note di credito) ---
|
||||||
|
$invoiced = ['available' => false, 'summary' => [], 'monthly' => []];
|
||||||
|
try {
|
||||||
|
$iFrom = MagoSalesStats::invoiceFrom();
|
||||||
|
$iWhere = preg_replace('/\bx\./', 'i.', $where); // stessi filtri cliente/agente/area
|
||||||
|
$iYtd = $ytd ? " AND DATE_FORMAT(i.document_date, '%m-%d') <= DATE_FORMAT(CURDATE(), '%m-%d')" : '';
|
||||||
|
|
||||||
|
$invoiced['summary'] = MagoSalesStats::fetchAllQ($pdo, "
|
||||||
|
SELECT YEAR(i.document_date) AS y,
|
||||||
|
COALESCE(SUM(i.revenue), 0) AS value,
|
||||||
|
SUM(i.is_credit_note = 0) AS docs,
|
||||||
|
SUM(i.is_credit_note = 1) AS credit_notes,
|
||||||
|
COALESCE(SUM(CASE WHEN i.is_credit_note = 1 THEN -i.revenue END), 0) AS credit_value,
|
||||||
|
SUM(i.no_amount) AS missing_amounts
|
||||||
|
FROM $iFrom
|
||||||
|
WHERE $iWhere AND i.document_date IS NOT NULL $iYtd
|
||||||
|
GROUP BY y ORDER BY y
|
||||||
|
", $params);
|
||||||
|
|
||||||
|
if ($years) {
|
||||||
|
$invoiced['monthly'] = MagoSalesStats::fetchAllQ($pdo, "
|
||||||
|
SELECT YEAR(i.document_date) AS y, MONTH(i.document_date) AS m, SUM(i.revenue) AS v
|
||||||
|
FROM $iFrom
|
||||||
|
WHERE $iWhere AND " . str_replace('x.order_date', 'i.document_date', $yearIn) . " $iYtd
|
||||||
|
GROUP BY y, m ORDER BY y, m
|
||||||
|
", $yp);
|
||||||
|
}
|
||||||
|
$invoiced['available'] = (bool)$pdo->query("SELECT 1 FROM mago_sale_doc LIMIT 1")->fetchColumn();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$invoiced['error'] = 'Fatturato non disponibile: eseguire la migrazione e la sincronizzazione';
|
||||||
|
}
|
||||||
|
|
||||||
MagoSalesStats::jsonOut([
|
MagoSalesStats::jsonOut([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
'invoiced' => $invoiced,
|
||||||
'metric' => $metric,
|
'metric' => $metric,
|
||||||
'ytd' => $ytd,
|
'ytd' => $ytd,
|
||||||
'today' => date('Y-m-d'),
|
'today' => date('Y-m-d'),
|
||||||
@@ -444,6 +479,26 @@ if ($action === 'data') {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.year-card .inv {
|
||||||
|
margin-top: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: .85rem;
|
||||||
|
color: var(--c-muted);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-card .inv b {
|
||||||
|
color: var(--c-ink);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
.year-card .deltas {
|
.year-card .deltas {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: .88rem;
|
font-size: .88rem;
|
||||||
@@ -659,6 +714,25 @@ if ($action === 'data') {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- FATTURATO (per data fattura) -->
|
||||||
|
<div class="row g-3 mb-3" id="invRow">
|
||||||
|
<div class="col-xl-6">
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-title">Fatturato per mese</div>
|
||||||
|
<div class="chart-hint">Imponibile fatture + acconti − note di credito, per data fattura</div>
|
||||||
|
<div id="chInvMonthly" style="min-height:340px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6">
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-title">Fatturato progressivo dell'anno</div>
|
||||||
|
<div class="chart-hint">Somma da gennaio del fatturato, anno su anno</div>
|
||||||
|
<div id="chInvCumul" style="min-height:340px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="warn-strip mb-3" id="invMissing" style="display:none;"></div>
|
||||||
|
|
||||||
<!-- CLIENTI -->
|
<!-- CLIENTI -->
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
|
||||||
@@ -871,6 +945,7 @@ if ($action === 'data') {
|
|||||||
renderCards(d);
|
renderCards(d);
|
||||||
renderMonthly(d);
|
renderMonthly(d);
|
||||||
renderCumul(d);
|
renderCumul(d);
|
||||||
|
renderInvoiced(d);
|
||||||
renderCustomers(d);
|
renderCustomers(d);
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
$('#content').css('opacity', 1);
|
$('#content').css('opacity', 1);
|
||||||
@@ -899,6 +974,19 @@ if ($action === 'data') {
|
|||||||
$('#missingAmounts').toggle(missing > 0).html(missing > 0 ?
|
$('#missingAmounts').toggle(missing > 0).html(missing > 0 ?
|
||||||
`<i class="fa-solid fa-circle-exclamation me-1"></i> ${fmtNum(missing)} ordini negli anni scelti non hanno ancora gli importi: lancia una sincronizzazione.` : '');
|
`<i class="fa-solid fa-circle-exclamation me-1"></i> ${fmtNum(missing)} ordini negli anni scelti non hanno ancora gli importi: lancia una sincronizzazione.` : '');
|
||||||
|
|
||||||
|
// riga fatturato nella carta anno (se i documenti sono sincronizzati)
|
||||||
|
const inv = {};
|
||||||
|
((d.invoiced && d.invoiced.summary) || []).forEach(r => inv[+r.y] = r);
|
||||||
|
const invLine = y => {
|
||||||
|
if (!d.invoiced || !d.invoiced.available) return '';
|
||||||
|
const r = inv[y],
|
||||||
|
p = inv[y - 1];
|
||||||
|
const v = r ? +r.value : 0;
|
||||||
|
const dl = p ? deltaSpan(pct(v, +p.value), `vs ${y - 1}`) : '';
|
||||||
|
const cn = r && +r.credit_notes ? ` title="${fmtNum(r.docs)} fatture, ${fmtNum(r.credit_notes)} note di credito (−${esc(fmtEur(r.credit_value, 2))})"` : (r ? ` title="${fmtNum(r.docs)} fatture"` : '');
|
||||||
|
return `<div class="inv"${cn}><span>Fatturato</span><b>${fmtEurShort(v)}</b>${dl}</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
$('#yearCards').html(d.years.slice().reverse().map(y => {
|
$('#yearCards').html(d.years.slice().reverse().map(y => {
|
||||||
const r = byYear[y] || {
|
const r = byYear[y] || {
|
||||||
orders: 0,
|
orders: 0,
|
||||||
@@ -924,16 +1012,17 @@ if ($action === 'data') {
|
|||||||
<div><span>Medio</span><b>${fmtEurShort(avg)}</b></div>
|
<div><span>Medio</span><b>${fmtEurShort(avg)}</b></div>
|
||||||
<div><span>Clienti</span><b>${fmtNum(r.customers)}</b></div>
|
<div><span>Clienti</span><b>${fmtNum(r.customers)}</b></div>
|
||||||
</div>
|
</div>
|
||||||
|
${invLine(y)}
|
||||||
<div class="deltas">${deltas}</div>
|
<div class="deltas">${deltas}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join(''));
|
}).join(''));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- GRAFICI ---------------- */
|
/* ---------------- GRAFICI ---------------- */
|
||||||
function seriesByYear(d) {
|
function seriesByYear(d, rows) {
|
||||||
const map = {};
|
const map = {};
|
||||||
d.years.forEach(y => map[y] = Array(12).fill(0));
|
d.years.forEach(y => map[y] = Array(12).fill(0));
|
||||||
d.monthly.forEach(r => {
|
(rows || d.monthly).forEach(r => {
|
||||||
if (map[+r.y]) map[+r.y][+r.m - 1] = +r.v;
|
if (map[+r.y]) map[+r.y][+r.m - 1] = +r.v;
|
||||||
});
|
});
|
||||||
// mesi futuri: vuoti (non zero) per l'anno in corso, e per tutti se "a pari data"
|
// mesi futuri: vuoti (non zero) per l'anno in corso, e per tutti se "a pari data"
|
||||||
@@ -964,9 +1053,9 @@ if ($action === 'data') {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderMonthly(d) {
|
function renderMonthly(d, key, el, rows, fmtV, fmtA) {
|
||||||
const map = seriesByYear(d);
|
const map = seriesByYear(d, rows);
|
||||||
upsert('monthly', 'chMonthly', {
|
upsert(key || 'monthly', el || 'chMonthly', {
|
||||||
chart: {
|
chart: {
|
||||||
...base,
|
...base,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -989,14 +1078,14 @@ if ($action === 'data') {
|
|||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
labels: {
|
labels: {
|
||||||
formatter: fmtAxis
|
formatter: fmtA || fmtAxis
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
shared: true,
|
shared: true,
|
||||||
intersect: false,
|
intersect: false,
|
||||||
y: {
|
y: {
|
||||||
formatter: fmtMetric
|
formatter: fmtV || fmtMetric
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
@@ -1009,14 +1098,14 @@ if ($action === 'data') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCumul(d) {
|
function renderCumul(d, key, el, rows, fmtV, fmtA) {
|
||||||
const map = seriesByYear(d);
|
const map = seriesByYear(d, rows);
|
||||||
const cum = {};
|
const cum = {};
|
||||||
d.years.forEach(y => {
|
d.years.forEach(y => {
|
||||||
let s = 0;
|
let s = 0;
|
||||||
cum[y] = map[y].map(v => v === null ? null : (s += v));
|
cum[y] = map[y].map(v => v === null ? null : (s += v));
|
||||||
});
|
});
|
||||||
upsert('cumul', 'chCumul', {
|
upsert(key || 'cumul', el || 'chCumul', {
|
||||||
chart: {
|
chart: {
|
||||||
...base,
|
...base,
|
||||||
type: 'area',
|
type: 'area',
|
||||||
@@ -1046,14 +1135,14 @@ if ($action === 'data') {
|
|||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
labels: {
|
labels: {
|
||||||
formatter: fmtAxis
|
formatter: fmtA || fmtAxis
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
shared: true,
|
shared: true,
|
||||||
intersect: false,
|
intersect: false,
|
||||||
y: {
|
y: {
|
||||||
formatter: fmtMetric
|
formatter: fmtV || fmtMetric
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
@@ -1066,6 +1155,20 @@ if ($action === 'data') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fatturato: sempre in € (indipendente dal selettore N° ordini / Valore)
|
||||||
|
function renderInvoiced(d) {
|
||||||
|
const inv = d.invoiced || {};
|
||||||
|
$('#invRow').toggle(!!inv.available);
|
||||||
|
const miss = (inv.summary || []).filter(r => d.years.includes(+r.y)).reduce((a, r) => a + (+r.missing_amounts || 0), 0);
|
||||||
|
$('#invMissing').toggle(!inv.available || miss > 0).html(!inv.available ?
|
||||||
|
'<i class="fa-solid fa-circle-info me-1"></i> Fatturato non ancora disponibile: eseguire la migrazione <b>mago_sale_doc</b> e una sincronizzazione.' :
|
||||||
|
`<i class="fa-solid fa-circle-exclamation me-1"></i> ${fmtNum(miss)} fatture negli anni scelti non hanno ancora l'importo: lancia una sincronizzazione.`);
|
||||||
|
if (!inv.available) return;
|
||||||
|
const fv = v => v === null || v === undefined ? '—' : fmtEur(v);
|
||||||
|
renderMonthly(d, 'invMonthly', 'chInvMonthly', inv.monthly || [], fv, fmtEurShort);
|
||||||
|
renderCumul(d, 'invCumul', 'chInvCumul', inv.monthly || [], fv, fmtEurShort);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- CLIENTI ---------------- */
|
/* ---------------- CLIENTI ---------------- */
|
||||||
function classify(c, last, prev) {
|
function classify(c, last, prev) {
|
||||||
const a = c.v[last] || 0,
|
const a = c.v[last] || 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user