added book to bill
This commit is contained in:
@@ -70,6 +70,124 @@ class MagoSalesStats
|
||||
3407876 => -1, // Nota di credito (TD04) → in negativo serie D ...
|
||||
];
|
||||
|
||||
/**
|
||||
* CLIENTI DA RICHIAMARE: clienti "regolari" (almeno DORMANT_MIN_ORDERS giorni d'ordine negli ultimi
|
||||
* DORMANT_WINDOW_YEARS anni) in ritardo rispetto al loro ritmo abituale (mediana dei giorni tra due ordini).
|
||||
* ritardo = giorni dall'ultimo ordine / ritmo abituale
|
||||
*/
|
||||
const DORMANT_WINDOW_YEARS = 3;
|
||||
const DORMANT_MIN_ORDERS = 4;
|
||||
const DORMANT_MIN_DAYS = 30; // sotto i 30 giorni dall'ultimo ordine nessun allarme
|
||||
const DORMANT_WATCH = 1.5; // da 1,5 volte il ritmo: attenzione
|
||||
const DORMANT_SLEEP = 2.0; // da 2 volte: dormiente
|
||||
const DORMANT_LOST = 4.0; // da 4 volte (o oltre 1 anno): probabilmente perso
|
||||
|
||||
/**
|
||||
* Calcola la lista clienti da richiamare a partire dagli ordini (code, name, order_date, v, salesperson, area, ord_no).
|
||||
*/
|
||||
public static function dormantCustomers(array $rows)
|
||||
{
|
||||
$by = [];
|
||||
foreach ($rows as $r) {
|
||||
$c = $r['code'];
|
||||
if (!isset($by[$c])) $by[$c] = ['code' => $c, 'name' => $r['name'], 'dates' => [], 'orders' => []];
|
||||
$by[$c]['dates'][$r['order_date']] = true;
|
||||
$by[$c]['orders'][] = $r;
|
||||
}
|
||||
|
||||
$today = new DateTime('today');
|
||||
$out = [];
|
||||
foreach ($by as $c) {
|
||||
$dates = array_keys($c['dates']);
|
||||
sort($dates);
|
||||
$n = count($dates);
|
||||
if ($n < self::DORMANT_MIN_ORDERS) continue;
|
||||
|
||||
$gaps = [];
|
||||
for ($k = 1; $k < $n; $k++) {
|
||||
$gaps[] = (int)(new DateTime($dates[$k - 1]))->diff(new DateTime($dates[$k]))->days;
|
||||
}
|
||||
sort($gaps);
|
||||
$m = count($gaps);
|
||||
$median = $m % 2 ? $gaps[intdiv($m, 2)] : ($gaps[$m / 2 - 1] + $gaps[$m / 2]) / 2;
|
||||
$rhythm = max(7, (float)$median);
|
||||
|
||||
$last = end($dates);
|
||||
$since = (int)(new DateTime($last))->diff($today)->days;
|
||||
if ($since < self::DORMANT_MIN_DAYS) continue;
|
||||
|
||||
$ratio = $since / $rhythm;
|
||||
if ($ratio >= self::DORMANT_LOST || $since >= 365) $level = 'lost';
|
||||
elseif ($ratio >= self::DORMANT_SLEEP) $level = 'dormant';
|
||||
elseif ($ratio >= self::DORMANT_WATCH) $level = 'watch';
|
||||
else continue;
|
||||
|
||||
// ordinato nei 12 mesi fino all'ultimo ordine = quanto vale il cliente "a regime"
|
||||
$from12 = (new DateTime($last))->modify('-365 days')->format('Y-m-d');
|
||||
$v12 = 0.0;
|
||||
$lastOrder = null;
|
||||
foreach ($c['orders'] as $o) {
|
||||
if ($o['order_date'] > $from12) $v12 += (float)$o['v'];
|
||||
if (
|
||||
!$lastOrder || $o['order_date'] > $lastOrder['order_date'] ||
|
||||
($o['order_date'] === $lastOrder['order_date'] && $o['ord_no'] > $lastOrder['ord_no'])
|
||||
) $lastOrder = $o;
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'code' => $c['code'],
|
||||
'name' => $c['name'],
|
||||
'salesperson' => $lastOrder['salesperson'],
|
||||
'area' => $lastOrder['area'],
|
||||
'last_date' => $last,
|
||||
'last_no' => $lastOrder['ord_no'],
|
||||
'days_since' => $since,
|
||||
'rhythm' => (int)round($rhythm),
|
||||
'ratio' => round($ratio, 1),
|
||||
'orders' => $n,
|
||||
'value12' => round($v12, 2),
|
||||
'level' => $level,
|
||||
];
|
||||
}
|
||||
usort($out, function ($a, $b) {
|
||||
return $b['value12'] <=> $a['value12'];
|
||||
});
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Previsione di fine anno: valore dell'anno in corso ad oggi diviso la quota che, negli anni
|
||||
* precedenti (max 3), si era raggiunta alla stessa data. $rows: [y, full, ytd].
|
||||
*/
|
||||
public static function yearForecast(array $rows, $curYear)
|
||||
{
|
||||
$cur = null;
|
||||
$shares = [];
|
||||
$prevFull = null;
|
||||
foreach ($rows as $r) {
|
||||
$y = (int)$r['y'];
|
||||
if ($y === $curYear) $cur = (float)$r['ytd'];
|
||||
elseif ($y >= $curYear - 3 && $y < $curYear && (float)$r['full'] > 0 && (float)$r['ytd'] > 0) {
|
||||
$shares[$y] = (float)$r['ytd'] / (float)$r['full'];
|
||||
}
|
||||
if ($y === $curYear - 1) $prevFull = (float)$r['full'];
|
||||
}
|
||||
if ($cur === null || !$shares) {
|
||||
return ['ytd' => $cur, 'forecast' => null, 'prev_full' => $prevFull];
|
||||
}
|
||||
$avg = array_sum($shares) / count($shares);
|
||||
ksort($shares);
|
||||
return [
|
||||
'ytd' => round($cur, 2),
|
||||
'forecast' => round($cur / $avg, 2),
|
||||
'low' => round($cur / max($shares), 2),
|
||||
'high' => round($cur / min($shares), 2),
|
||||
'prev_full' => $prevFull,
|
||||
'years' => array_keys($shares),
|
||||
'share' => round($avg * 100, 1),
|
||||
];
|
||||
}
|
||||
|
||||
/** Etichette dei tipi documento (tabella documenti). */
|
||||
const INVOICE_TYPE_LABELS = [
|
||||
3407874 => 'Fattura',
|
||||
|
||||
Reference in New Issue
Block a user