added fatturato

This commit is contained in:
2026-10-14 17:59:06 +02:00
parent b2a2f56383
commit 5e2ec11dbd
6 changed files with 655 additions and 12 deletions
+33
View File
@@ -59,6 +59,39 @@ class MagoSalesStats
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). */
public static function headerDue($a)
{
+244
View File
@@ -21,6 +21,8 @@ class MagoSync
const ENTITY_CUSTOMER = 'cust_supp';
const ENTITY_SUMMARY = 'sale_ord_summary';
const ENTITY_DETAILS = 'sale_ord_details';
const ENTITY_DOCS = 'sale_doc';
const ENTITY_DOC_SUMM = 'sale_doc_summary';
/** @var PDO */
private $pdo;
@@ -65,6 +67,15 @@ class MagoSync
$report['ordini'] = $this->syncSaleOrders($from, $limit, $full);
$report['righe'] = $this->syncSaleOrderDetails($from, $full, (bool)$limit);
$report['importi'] = $this->syncSaleOrderSummaries($from, $full, (bool)$limit);
// Documenti di vendita (fatture / note di credito) per il fatturato
if ($this->tableExists('mago_sale_doc')) {
$report['documenti'] = $this->syncSaleDocs($from, $limit, $full);
$report['importi_documenti'] = $this->syncSaleDocSummaries($from, $full, (bool)$limit);
} else {
$report['documenti'] = 'tabella mago_sale_doc mancante: eseguire la migrazione Phinx';
$this->log('Documenti: tabella mago_sale_doc mancante, salto (eseguire la migrazione)');
}
} catch (Throwable $e) {
$report['ok'] = false;
$report['message'] = $e->getMessage();
@@ -525,6 +536,239 @@ class MagoSync
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)
* ========================================================= */