added stat matrici
This commit is contained in:
@@ -0,0 +1,845 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Statistiche vendite per MATRICE.
|
||||
* - prezzo di vendita per unità (€/m, €/kg) nel tempo e per cliente
|
||||
* - consumo mescole stimato dalla scheda di lavoro (g/m per mescola)
|
||||
* Fonte: righe ordine Mago (mago_sale_ord_detail), solo lettura. Prezzo = imponibile riga ÷ quantità.
|
||||
*/
|
||||
require_once __DIR__ . '/MagoSalesStats.php';
|
||||
|
||||
class MagoMatriceStats
|
||||
{
|
||||
/** Righe con quantità <= MIN_QTY escluse (servizi: quantità 0 o 1) */
|
||||
const MIN_QTY = 1;
|
||||
/** Articoli esclusi (spese, attrezzature) */
|
||||
const EXCLUDED_ITEM_PREFIXES = ['SPES', 'SPATT'];
|
||||
/** Prima data considerata */
|
||||
const SINCE = '2020-01-01';
|
||||
|
||||
const UOM_METERS = ['MT', 'M', 'ML', 'MTL'];
|
||||
const UOM_KG = ['KG', 'KGS'];
|
||||
const UOM_PIECES = ['N.', 'N', 'NR', 'NR.', 'PZ', 'PZ.'];
|
||||
|
||||
/** Tipi di collegamento che contano come "matrice in anagrafica" */
|
||||
const LINKED_TYPES = ['esatta', 'revisione', 'mappa'];
|
||||
|
||||
/** Esito del riconoscimento (campo type) */
|
||||
const TYPE_LABELS = [
|
||||
'esatta' => 'Matrice',
|
||||
'revisione' => 'Altra revisione',
|
||||
'mappa' => 'Abbinata a mano',
|
||||
'da_creare' => 'Non in anagrafica',
|
||||
'profilo' => 'Profilo',
|
||||
'altro' => 'Non riconosciuta',
|
||||
];
|
||||
|
||||
/** Tipo dalla lettera iniziale di Matrice/Misura (campo cat) */
|
||||
const CAT_LABELS = [
|
||||
'M' => 'Matrice',
|
||||
'T' => 'Tubo',
|
||||
'R' => 'Rettangolo',
|
||||
'D' => 'Diametro',
|
||||
'Q' => 'Quadrato',
|
||||
'S' => 'Profilo S',
|
||||
'X' => 'Non riconosciuta',
|
||||
];
|
||||
|
||||
private static ?array $idx = null;
|
||||
private static array $cache = [];
|
||||
|
||||
/* ============================================================
|
||||
* UTILITY
|
||||
* ============================================================ */
|
||||
|
||||
public static function norm($s): string
|
||||
{
|
||||
return strtoupper(preg_replace('/\s+/u', ' ', trim((string)$s)));
|
||||
}
|
||||
|
||||
/** m = metri, kg = chili, pz = pezzi, altro */
|
||||
public static function uomKind($u): string
|
||||
{
|
||||
$u = self::norm($u);
|
||||
if (in_array($u, self::UOM_METERS, true)) return 'm';
|
||||
if (in_array($u, self::UOM_KG, true)) return 'kg';
|
||||
if (in_array($u, self::UOM_PIECES, true)) return 'pz';
|
||||
return 'altro';
|
||||
}
|
||||
|
||||
public static function photoUrl($p): ?string
|
||||
{
|
||||
$p = trim((string)$p);
|
||||
if ($p === '') return null;
|
||||
if (preg_match('~^(https?:)?//~i', $p) || strpos($p, '/') !== false) return $p;
|
||||
return 'photos/matrici/' . $p;
|
||||
}
|
||||
|
||||
private static function inList(string $prefix, array $values, array &$params): string
|
||||
{
|
||||
$ph = [];
|
||||
foreach (array_values($values) as $i => $v) {
|
||||
$ph[] = ":{$prefix}{$i}";
|
||||
$params["{$prefix}{$i}"] = $v;
|
||||
}
|
||||
return implode(',', $ph);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* RICONOSCIMENTO MATRICE (stesse regole della coda produzione:
|
||||
* mappa manuale → codice senza M, con e senza revisione → non in anagrafica / profilo T-R-D-Q-S / altro)
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* Codice matrice normalizzato: senza la M iniziale e separato dalla revisione.
|
||||
* "M00407P-1" → ['00407P', '1'] · "00786S" → ['00786S', '']
|
||||
*/
|
||||
public static function splitCode(string $s): array
|
||||
{
|
||||
$s = strtoupper(trim($s));
|
||||
$s = preg_replace('/^M\s?(?=\d)/', '', $s);
|
||||
$s = preg_replace('/\s+/', '', $s);
|
||||
if (preg_match('/^(.+?)-(\d+)$/', $s, $m)) return [$m[1], $m[2]];
|
||||
return [$s, ''];
|
||||
}
|
||||
|
||||
public static function index(PDO $pdo): array
|
||||
{
|
||||
if (self::$idx !== null) return self::$idx;
|
||||
$base = []; // codice senza M e senza revisione => [revisione => id]
|
||||
$info = [];
|
||||
foreach ($pdo->query("SELECT id, nome, photo FROM matrice ORDER BY id") as $r) {
|
||||
$id = (int)$r['id'];
|
||||
$info[$id] = ['name' => (string)$r['nome'], 'photo' => self::photoUrl($r['photo'])];
|
||||
$parts = preg_split('/\s+/u', trim((string)$r['nome']));
|
||||
$first = $parts[0] ?? '';
|
||||
if ($first === '') continue;
|
||||
[$code, $rev] = self::splitCode($first);
|
||||
if ($code === '') continue;
|
||||
$base[$code][$rev] = $id;
|
||||
}
|
||||
$map = [];
|
||||
try {
|
||||
foreach ($pdo->query("SELECT codice_mago, idmatrice FROM matrice_mago_map WHERE idmatrice IS NOT NULL") as $r) {
|
||||
$map[self::norm($r['codice_mago'])] = (int)$r['idmatrice'];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
return self::$idx = ['base' => $base, 'info' => $info, 'map' => $map];
|
||||
}
|
||||
|
||||
/**
|
||||
* Matrice dell'anagrafica per codice (senza M) e revisione, provando con e senza revisione.
|
||||
* - stessa revisione, oppure revisione assente da una delle due parti → esatta
|
||||
* - revisioni diverse da entrambe le parti → revisione (avviso)
|
||||
* @return array|null [id, type]
|
||||
*/
|
||||
private static function findMatrix(array $ix, string $code, string $rev): ?array
|
||||
{
|
||||
$revs = $ix['base'][$code] ?? null;
|
||||
if (!$revs) return null;
|
||||
if (isset($revs[$rev])) return [$revs[$rev], 'esatta'];
|
||||
if ($rev !== '' && isset($revs[''])) return [$revs[''], 'esatta'];
|
||||
$keys = array_keys($revs);
|
||||
usort($keys, fn($a, $b) => (int)$b <=> (int)$a); // revisione più alta
|
||||
return [$revs[$keys[0]], $rev === '' ? 'esatta' : 'revisione'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array key, id, cat (M/T/R/D/Q/S/X), type (esito), label, code
|
||||
* key: m{id} (matrice in anagrafica) | c:{codice} (non in anagrafica) | p:{valore} (profilo) | x:{valore}
|
||||
*/
|
||||
public static function resolve(PDO $pdo, $raw): array
|
||||
{
|
||||
$n = self::norm($raw);
|
||||
if (isset(self::$cache[$n])) return self::$cache[$n];
|
||||
$ix = self::index($pdo);
|
||||
$linked = function (int $id, string $type, string $code) use ($ix, $n) {
|
||||
return ['key' => 'm' . $id, 'id' => $id, 'cat' => 'M', 'type' => $type, 'label' => $ix['info'][$id]['name'] ?? $n, 'code' => $code];
|
||||
};
|
||||
|
||||
if (isset($ix['map'][$n]) && isset($ix['info'][$ix['map'][$n]])) {
|
||||
$res = $linked($ix['map'][$n], 'mappa', $n);
|
||||
} elseif (preg_match('/(?<![A-Z])M\s?(\d{3,6}[A-Z]{0,2})(?:\s?-\s?(\d+))?/', $n, $m)) {
|
||||
// codice matrice anche in mezzo al campo (Q8X8M00936P, S5X5,5 M00456S): si cerca senza la M
|
||||
$code = $m[1];
|
||||
$rev = $m[2] ?? '';
|
||||
$label = $code . ($rev !== '' ? '-' . $rev : '');
|
||||
$f = self::findMatrix($ix, $code, $rev);
|
||||
if ($f) {
|
||||
$res = $linked($f[0], $f[1], $label);
|
||||
} else {
|
||||
$res = ['key' => 'c:' . $code, 'id' => null, 'cat' => 'M', 'type' => 'da_creare', 'label' => $label, 'code' => $label];
|
||||
}
|
||||
} elseif (preg_match('/^([DRTQS])\s?\d/', $n, $m)) {
|
||||
$res = ['key' => 'p:' . $n, 'id' => null, 'cat' => $m[1], 'type' => 'profilo', 'label' => $n, 'code' => $n];
|
||||
} else {
|
||||
$res = ['key' => 'x:' . $n, 'id' => null, 'cat' => 'X', 'type' => 'altro', 'label' => $n, 'code' => $n];
|
||||
}
|
||||
return self::$cache[$n] = $res;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* MESCOLE
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* Per ogni matrice: ultima scheda di lavoro (prima le attive, poi la più recente)
|
||||
* con le mescole e i loro g/m. Senza scheda: mescole dell'anagrafica matrice (senza g/m).
|
||||
*/
|
||||
public static function mixInfo(PDO $pdo, array $ids): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
|
||||
$out = [];
|
||||
foreach (array_chunk($ids, 500) as $chunk) {
|
||||
$in = implode(',', $chunk);
|
||||
$ws = [];
|
||||
$rows = $pdo->query("SELECT id, idmatrice, worksheet_number, worksheet_date, revision_code, worksheet_status
|
||||
FROM work_sheets WHERE idmatrice IN ($in)
|
||||
ORDER BY idmatrice, (worksheet_status = 'active') DESC, worksheet_date DESC, id DESC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as $r) {
|
||||
$m = (int)$r['idmatrice'];
|
||||
if (!isset($ws[$m])) $ws[$m] = $r;
|
||||
}
|
||||
|
||||
$mix = [];
|
||||
if ($ws) {
|
||||
$wsIds = implode(',', array_map(fn($r) => (int)$r['id'], $ws));
|
||||
foreach (
|
||||
$pdo->query("SELECT w.worksheet_id, w.idmescola, w.mix_position, w.mix_weight_g_m, m.nome, m.nomeuscita
|
||||
FROM work_sheet_mescole w JOIN mescole m ON m.id = w.idmescola
|
||||
WHERE w.worksheet_id IN ($wsIds)
|
||||
ORDER BY w.worksheet_id, w.mix_position") as $r
|
||||
) {
|
||||
$mix[(int)$r['worksheet_id']][] = [
|
||||
'id' => (int)$r['idmescola'],
|
||||
'name' => (string)$r['nome'],
|
||||
'out' => (string)($r['nomeuscita'] ?? ''),
|
||||
'pos' => (int)$r['mix_position'],
|
||||
'gm' => $r['mix_weight_g_m'] !== null ? (float)$r['mix_weight_g_m'] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$ana = [];
|
||||
foreach (
|
||||
$pdo->query("SELECT mm.idmatrice, m.id, m.nome, m.nomeuscita
|
||||
FROM matrici_mescole mm JOIN mescole m ON m.id = mm.idmescola
|
||||
WHERE mm.idmatrice IN ($in) ORDER BY m.nome") as $r
|
||||
) {
|
||||
$ana[(int)$r['idmatrice']][] = ['id' => (int)$r['id'], 'name' => (string)$r['nome'], 'out' => (string)($r['nomeuscita'] ?? ''), 'pos' => 0, 'gm' => null];
|
||||
}
|
||||
|
||||
foreach ($chunk as $id) {
|
||||
$w = $ws[$id] ?? null;
|
||||
$list = $w ? ($mix[(int)$w['id']] ?? []) : [];
|
||||
$src = $list ? 'scheda' : 'nessuna';
|
||||
if (!$list && !empty($ana[$id])) {
|
||||
$list = $ana[$id];
|
||||
$src = 'anagrafica';
|
||||
}
|
||||
$gmTot = 0.0;
|
||||
$missing = 0;
|
||||
foreach ($list as $x) {
|
||||
if ($x['gm'] !== null && $x['gm'] > 0) $gmTot += $x['gm'];
|
||||
else $missing++;
|
||||
}
|
||||
$out[$id] = [
|
||||
'ws' => $w ? [
|
||||
'id' => (int)$w['id'],
|
||||
'number' => $w['worksheet_number'],
|
||||
'date' => $w['worksheet_date'],
|
||||
'rev' => $w['revision_code'],
|
||||
'active' => $w['worksheet_status'] === 'active',
|
||||
] : null,
|
||||
'mix' => $list,
|
||||
'source' => $src,
|
||||
'gm_total' => $gmTot,
|
||||
'gm_missing' => $missing,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kg di mescola per una quantità venduta.
|
||||
* - Metri: kg = metri × g/m ÷ 1000, mescola per mescola.
|
||||
* - Chili: i kg si dividono tra le mescole in proporzione ai g/m; senza g/m in parti uguali (2 → 50/50, 3 → 33%).
|
||||
* - Pezzi: in attesa della conversione pezzi → metri.
|
||||
* status: ok | gm_part (g/m mancante su qualche mescola) | gm (nessun g/m) | none (nessuna mescola) | pz | altro
|
||||
*/
|
||||
public static function mixKg(?array $info, string $kind, float $qty): array
|
||||
{
|
||||
if ($kind === 'pz') return ['status' => 'pz', 'kg' => []];
|
||||
if ($kind === 'altro') return ['status' => 'altro', 'kg' => []];
|
||||
if (!$info || !$info['mix']) return ['status' => 'none', 'kg' => []];
|
||||
|
||||
$kg = [];
|
||||
if ($kind === 'm') {
|
||||
if ($info['gm_total'] <= 0) return ['status' => 'gm', 'kg' => []];
|
||||
foreach ($info['mix'] as $x) {
|
||||
if ($x['gm'] > 0) $kg[$x['id']] = ($kg[$x['id']] ?? 0) + $qty * $x['gm'] / 1000;
|
||||
}
|
||||
return ['status' => $info['gm_missing'] ? 'gm_part' : 'ok', 'kg' => $kg];
|
||||
}
|
||||
|
||||
$n = count($info['mix']);
|
||||
foreach ($info['mix'] as $x) {
|
||||
$share = $info['gm_total'] > 0 ? max(0.0, (float)$x['gm']) / $info['gm_total'] : 1 / $n;
|
||||
$kg[$x['id']] = ($kg[$x['id']] ?? 0) + $qty * $share;
|
||||
}
|
||||
return ['status' => ($info['gm_total'] > 0 && $info['gm_missing']) ? 'gm_part' : 'ok', 'kg' => $kg];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* DATI
|
||||
* ============================================================ */
|
||||
|
||||
/** Righe ordine con Matrice/Misura: dal giorno $lo oppure ancora da consegnare */
|
||||
public static function fetchLines(PDO $pdo, string $lo, array $customers = [], ?array $raws = null, ?string $uom = null): array
|
||||
{
|
||||
if ($raws !== null && !$raws) return [];
|
||||
$from = MagoSalesStats::baseFrom();
|
||||
$p = ['lo' => $lo];
|
||||
$w = [
|
||||
"x.status <> 'annullato'",
|
||||
"d.cancelled = 0",
|
||||
"d.matrice_misura <> ''",
|
||||
"d.qty > " . (float)self::MIN_QTY,
|
||||
"(x.order_date >= :lo OR (d.delivered = 0 AND x.status <> 'evaso'))",
|
||||
];
|
||||
foreach (self::EXCLUDED_ITEM_PREFIXES as $i => $pre) {
|
||||
$w[] = "d.item NOT LIKE :ex$i";
|
||||
$p["ex$i"] = $pre . '%';
|
||||
}
|
||||
if ($customers) $w[] = "x.customer IN (" . self::inList('cu', $customers, $p) . ")";
|
||||
if ($raws !== null) $w[] = "d.matrice_misura IN (" . self::inList('mm', $raws, $p) . ")";
|
||||
if ($uom !== null) {
|
||||
$w[] = "UPPER(TRIM(d.uom)) = :uom";
|
||||
$p['uom'] = $uom;
|
||||
}
|
||||
|
||||
$sql = "SELECT d.sale_ord_id, d.sub_id, d.matrice_misura, d.uom, d.item, d.description, d.qty, d.delivered_qty, d.delivered,
|
||||
CASE WHEN d.taxable_amount <> 0 THEN d.taxable_amount ELSE d.qty * d.unit_value - d.discount_amount END AS value,
|
||||
x.order_date, x.customer, x.internal_ord_no, x.status
|
||||
FROM mago_sale_ord_detail d
|
||||
JOIN $from ON x.sale_ord_id = d.sale_ord_id
|
||||
WHERE " . implode(' AND ', $w) . "
|
||||
ORDER BY x.order_date, d.sale_ord_id, d.sub_id";
|
||||
return MagoSalesStats::fetchAllQ($pdo, $sql, $p);
|
||||
}
|
||||
|
||||
public static function customerNames(PDO $pdo, array $codes): array
|
||||
{
|
||||
$codes = array_values(array_unique(array_filter($codes, fn($c) => $c !== '')));
|
||||
$out = [];
|
||||
foreach (array_chunk($codes, 500) as $chunk) {
|
||||
$p = [];
|
||||
$in = self::inList('c', $chunk, $p);
|
||||
$rows = MagoSalesStats::fetchAllQ($pdo, "SELECT cust_supp AS code, company_name AS name FROM mago_cust_supp
|
||||
WHERE cust_supp_type = " . MagoSalesStats::CUSTOMER_TYPE . " AND cust_supp IN ($in)", $p);
|
||||
foreach ($rows as $r) $out[$r['code']] = $r['name'] ?: $r['code'];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function lineIsOpen(array $l): float
|
||||
{
|
||||
if ((int)$l['delivered'] !== 0 || $l['status'] === 'evaso') return 0.0;
|
||||
return max(0.0, (float)$l['qty'] - (float)$l['delivered_qty']);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* CLASSIFICA MATRICI + KPI + MESCOLE
|
||||
* ============================================================ */
|
||||
|
||||
public static function buildList(PDO $pdo, string $from, string $to, array $customers = []): array
|
||||
{
|
||||
$d12 = date('Y-m-d', strtotime('-365 days'));
|
||||
$d24 = date('Y-m-d', strtotime('-730 days'));
|
||||
$lines = self::fetchLines($pdo, min($from, $d24), $customers);
|
||||
|
||||
$ids = [];
|
||||
foreach ($lines as $i => $l) {
|
||||
$lines[$i]['_r'] = self::resolve($pdo, $l['matrice_misura']);
|
||||
if ($lines[$i]['_r']['id']) $ids[] = $lines[$i]['_r']['id'];
|
||||
}
|
||||
$mix = self::mixInfo($pdo, $ids);
|
||||
$ix = self::index($pdo);
|
||||
|
||||
$spark = [];
|
||||
for ($i = 11; $i >= 0; $i--) $spark[] = date('Y-m', strtotime("first day of -$i months"));
|
||||
$sparkPos = array_flip($spark);
|
||||
|
||||
$agg = [];
|
||||
$mixTot = [];
|
||||
$mixMonth = [];
|
||||
$mixNames = [];
|
||||
$kpiClients = [];
|
||||
$kpi = [
|
||||
'value' => 0,
|
||||
'lines' => 0,
|
||||
'qty_m' => 0,
|
||||
'val_m' => 0,
|
||||
'pq_m' => 0,
|
||||
'qty_kg' => 0,
|
||||
'val_kg' => 0,
|
||||
'pq_kg' => 0,
|
||||
'pz_lines' => 0,
|
||||
'pz_value' => 0,
|
||||
'pz_qty' => 0,
|
||||
'kg_mix' => 0,
|
||||
'kg_open' => 0,
|
||||
'open_value' => 0,
|
||||
'linked_value' => 0,
|
||||
'profile_value' => 0,
|
||||
'unlinked_value' => 0,
|
||||
'gm_missing_lines' => 0,
|
||||
'gm_missing_value' => 0,
|
||||
'matrici' => 0,
|
||||
];
|
||||
$linkedIds = [];
|
||||
|
||||
foreach ($lines as $l) {
|
||||
$r = $l['_r'];
|
||||
$uom = self::norm($l['uom']);
|
||||
$kind = self::uomKind($uom);
|
||||
$key = $r['key'] . '|' . $uom;
|
||||
$qty = (float)$l['qty'];
|
||||
$val = (float)$l['value'];
|
||||
$date = (string)$l['order_date'];
|
||||
$ym = substr($date, 0, 7);
|
||||
$inP = $date >= $from && $date <= $to;
|
||||
$res = self::lineIsOpen($l);
|
||||
$mi = $r['id'] ? ($mix[$r['id']] ?? null) : null;
|
||||
|
||||
if (!isset($agg[$key])) {
|
||||
$agg[$key] = [
|
||||
'key' => $key,
|
||||
'rkey' => $r['key'],
|
||||
'id' => $r['id'],
|
||||
'cat' => $r['cat'],
|
||||
'type' => $r['type'],
|
||||
'name' => $r['label'],
|
||||
'code' => $r['code'],
|
||||
'photo' => $r['id'] ? ($ix['info'][$r['id']]['photo'] ?? null) : null,
|
||||
'uom' => $uom,
|
||||
'kind' => $kind,
|
||||
'raws' => [],
|
||||
'n' => 0,
|
||||
'qty' => 0,
|
||||
'value' => 0,
|
||||
'pq' => 0,
|
||||
'orders' => [],
|
||||
'clients' => [],
|
||||
'pmin' => null,
|
||||
'pmax' => null,
|
||||
'last_date' => '',
|
||||
'last_price' => null,
|
||||
'v12' => 0,
|
||||
'q12' => 0,
|
||||
'vp' => 0,
|
||||
'qp' => 0,
|
||||
'sv' => array_fill(0, 12, 0),
|
||||
'sq' => array_fill(0, 12, 0),
|
||||
'kg' => 0,
|
||||
'open_qty' => 0,
|
||||
'open_value' => 0,
|
||||
'open_kg' => 0,
|
||||
'mix_status' => self::mixKg($mi, $kind, 1)['status'],
|
||||
'mix_n' => $mi ? count($mi['mix']) : 0,
|
||||
'gm_total' => $mi ? $mi['gm_total'] : 0,
|
||||
];
|
||||
}
|
||||
$a = &$agg[$key];
|
||||
$a['raws'][self::norm($l['matrice_misura'])] = 1;
|
||||
|
||||
if ($inP) {
|
||||
$a['n']++;
|
||||
$a['qty'] += $qty;
|
||||
$a['value'] += $val;
|
||||
$a['orders'][$l['sale_ord_id']] = 1;
|
||||
$a['clients'][$l['customer']] = 1;
|
||||
$kpiClients[$l['customer']] = 1;
|
||||
if ($val > 0) {
|
||||
$p = $val / $qty;
|
||||
$a['pq'] += $qty;
|
||||
$a['pmin'] = $a['pmin'] === null ? $p : min($a['pmin'], $p);
|
||||
$a['pmax'] = $a['pmax'] === null ? $p : max($a['pmax'], $p);
|
||||
if ($date >= $a['last_date']) {
|
||||
$a['last_date'] = $date;
|
||||
$a['last_price'] = $p;
|
||||
}
|
||||
}
|
||||
$mk = self::mixKg($mi, $kind, $qty);
|
||||
foreach ($mk['kg'] as $mid => $kg) {
|
||||
$a['kg'] += $kg;
|
||||
$kpi['kg_mix'] += $kg;
|
||||
$mixTot[$mid]['kg'] = ($mixTot[$mid]['kg'] ?? 0) + $kg;
|
||||
$mixTot[$mid]['mat'][$r['id']] = 1;
|
||||
$mixMonth[$mid][$ym] = ($mixMonth[$mid][$ym] ?? 0) + $kg;
|
||||
}
|
||||
|
||||
$kpi['value'] += $val;
|
||||
$kpi['lines']++;
|
||||
if (in_array($r['type'], self::LINKED_TYPES, true)) {
|
||||
$kpi['linked_value'] += $val;
|
||||
$linkedIds[$r['id']] = 1;
|
||||
} elseif ($r['type'] === 'profilo') {
|
||||
$kpi['profile_value'] += $val;
|
||||
} else {
|
||||
$kpi['unlinked_value'] += $val;
|
||||
}
|
||||
if ($kind === 'm') {
|
||||
$kpi['qty_m'] += $qty;
|
||||
if ($val > 0) {
|
||||
$kpi['val_m'] += $val;
|
||||
$kpi['pq_m'] += $qty;
|
||||
}
|
||||
} elseif ($kind === 'kg') {
|
||||
$kpi['qty_kg'] += $qty;
|
||||
if ($val > 0) {
|
||||
$kpi['val_kg'] += $val;
|
||||
$kpi['pq_kg'] += $qty;
|
||||
}
|
||||
} elseif ($kind === 'pz') {
|
||||
$kpi['pz_lines']++;
|
||||
$kpi['pz_value'] += $val;
|
||||
$kpi['pz_qty'] += $qty;
|
||||
}
|
||||
if (in_array($mk['status'], ['gm', 'gm_part', 'none'], true) && $r['id']) {
|
||||
$kpi['gm_missing_lines']++;
|
||||
$kpi['gm_missing_value'] += $val;
|
||||
}
|
||||
}
|
||||
|
||||
if ($val > 0) {
|
||||
if ($date > $d12) {
|
||||
$a['v12'] += $val;
|
||||
$a['q12'] += $qty;
|
||||
} elseif ($date > $d24) {
|
||||
$a['vp'] += $val;
|
||||
$a['qp'] += $qty;
|
||||
}
|
||||
if (isset($sparkPos[$ym])) {
|
||||
$a['sv'][$sparkPos[$ym]] += $val;
|
||||
$a['sq'][$sparkPos[$ym]] += $qty;
|
||||
}
|
||||
}
|
||||
|
||||
if ($res > 0) {
|
||||
$ov = $qty > 0 ? $val * $res / $qty : 0;
|
||||
$a['open_qty'] += $res;
|
||||
$a['open_value'] += $ov;
|
||||
$kpi['open_value'] += $ov;
|
||||
foreach (self::mixKg($mi, $kind, $res)['kg'] as $mid => $kg) {
|
||||
$a['open_kg'] += $kg;
|
||||
$kpi['kg_open'] += $kg;
|
||||
$mixTot[$mid]['kg_open'] = ($mixTot[$mid]['kg_open'] ?? 0) + $kg;
|
||||
$mixTot[$mid]['mat'][$r['id']] = 1;
|
||||
}
|
||||
}
|
||||
unset($a);
|
||||
|
||||
if ($mi) foreach ($mi['mix'] as $x) $mixNames[$x['id']] = $x['name'];
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($agg as $a) {
|
||||
if ($a['n'] === 0 && $a['open_qty'] <= 0) continue;
|
||||
$price = $a['pq'] > 0 ? $a['value'] / $a['pq'] : null;
|
||||
$p12 = $a['q12'] > 0 ? $a['v12'] / $a['q12'] : null;
|
||||
$pp = $a['qp'] > 0 ? $a['vp'] / $a['qp'] : null;
|
||||
$sp = [];
|
||||
for ($i = 0; $i < 12; $i++) $sp[] = $a['sq'][$i] > 0 ? round($a['sv'][$i] / $a['sq'][$i], 4) : null;
|
||||
$rows[] = [
|
||||
'key' => $a['key'],
|
||||
'id' => $a['id'],
|
||||
'cat' => $a['cat'],
|
||||
'type' => $a['type'],
|
||||
'name' => $a['name'],
|
||||
'code' => $a['code'],
|
||||
'photo' => $a['photo'],
|
||||
'uom' => $a['uom'],
|
||||
'kind' => $a['kind'],
|
||||
'raws' => array_keys($a['raws']),
|
||||
'n' => $a['n'],
|
||||
'orders' => count($a['orders']),
|
||||
'clients' => count($a['clients']),
|
||||
'qty' => round($a['qty'], 3),
|
||||
'value' => round($a['value'], 2),
|
||||
'price' => $price !== null ? round($price, 4) : null,
|
||||
'pmin' => $a['pmin'] !== null ? round($a['pmin'], 4) : null,
|
||||
'pmax' => $a['pmax'] !== null ? round($a['pmax'], 4) : null,
|
||||
'spread' => ($a['pmin'] > 0 && $a['pmax'] !== null) ? round(($a['pmax'] - $a['pmin']) / $a['pmin'] * 100, 1) : null,
|
||||
'last_date' => $a['last_date'] ?: null,
|
||||
'last_price' => $a['last_price'] !== null ? round($a['last_price'], 4) : null,
|
||||
'price12' => $p12 !== null ? round($p12, 4) : null,
|
||||
'pricep' => $pp !== null ? round($pp, 4) : null,
|
||||
'var' => ($p12 !== null && $pp) ? round(($p12 - $pp) / $pp * 100, 1) : null,
|
||||
'spark' => $sp,
|
||||
'kg' => round($a['kg'], 1),
|
||||
'open_qty' => round($a['open_qty'], 3),
|
||||
'open_value' => round($a['open_value'], 2),
|
||||
'open_kg' => round($a['open_kg'], 1),
|
||||
'mix_status' => $a['mix_status'],
|
||||
'mix_n' => $a['mix_n'],
|
||||
'gm_total' => $a['gm_total'],
|
||||
];
|
||||
}
|
||||
usort($rows, fn($x, $y) => $y['value'] <=> $x['value']);
|
||||
|
||||
$mescole = [];
|
||||
foreach ($mixTot as $mid => $m) {
|
||||
$mescole[] = [
|
||||
'id' => $mid,
|
||||
'name' => $mixNames[$mid] ?? ('#' . $mid),
|
||||
'kg' => round($m['kg'] ?? 0, 1),
|
||||
'kg_open' => round($m['kg_open'] ?? 0, 1),
|
||||
'matrici' => count($m['mat'] ?? []),
|
||||
'months' => array_map(fn($v) => round($v, 1), $mixMonth[$mid] ?? []),
|
||||
];
|
||||
}
|
||||
usort($mescole, fn($x, $y) => $y['kg'] <=> $x['kg']);
|
||||
|
||||
$kpi['matrici'] = count($linkedIds);
|
||||
$kpi['clients'] = count($kpiClients);
|
||||
foreach ($kpi as $k => $v) if (is_float($v)) $kpi[$k] = round($v, 2);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'today' => date('Y-m-d'),
|
||||
'spark_months' => $spark,
|
||||
'kpi' => $kpi,
|
||||
'rows' => $rows,
|
||||
'mescole' => $mescole,
|
||||
'types' => self::TYPE_LABELS,
|
||||
'cats' => self::CAT_LABELS,
|
||||
];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* DETTAGLIO DI UNA MATRICE (in una unità di misura)
|
||||
* ============================================================ */
|
||||
|
||||
public static function buildDetail(PDO $pdo, string $key, string $from, string $to, array $customers = []): array
|
||||
{
|
||||
$pos = strrpos($key, '|');
|
||||
if ($pos === false) return ['success' => false, 'message' => 'Matrice non indicata'];
|
||||
$rkey = substr($key, 0, $pos);
|
||||
$uom = substr($key, $pos + 1);
|
||||
|
||||
// valori Mago che portano a questa matrice
|
||||
$raws = [];
|
||||
$first = null;
|
||||
foreach ($pdo->query("SELECT DISTINCT matrice_misura FROM mago_sale_ord_detail WHERE matrice_misura <> ''")->fetchAll(PDO::FETCH_COLUMN) as $raw) {
|
||||
$r = self::resolve($pdo, $raw);
|
||||
if ($r['key'] === $rkey) {
|
||||
$raws[] = $raw;
|
||||
if (!$first) $first = $r;
|
||||
}
|
||||
}
|
||||
if (!$raws) return ['success' => false, 'message' => 'Nessuna riga d\'ordine per questa matrice'];
|
||||
|
||||
$kind = self::uomKind($uom);
|
||||
$lines = self::fetchLines($pdo, $from, $customers, $raws, $uom);
|
||||
$ix = self::index($pdo);
|
||||
$mi = $first['id'] ? (self::mixInfo($pdo, [$first['id']])[$first['id']] ?? null) : null;
|
||||
|
||||
$prodLines = [];
|
||||
if ($first['id']) {
|
||||
$st = $pdo->prepare("SELECT pl.name, pl.color FROM matrici_lines ml JOIN production_lines pl ON pl.id = ml.idlinea WHERE ml.idmatrice = ? ORDER BY pl.line_number");
|
||||
$st->execute([$first['id']]);
|
||||
$prodLines = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$names = self::customerNames($pdo, array_column($lines, 'customer'));
|
||||
$cl = [];
|
||||
$months = [];
|
||||
$out = [];
|
||||
$mixKgP = [];
|
||||
$mixKgO = [];
|
||||
$tot = ['n' => 0, 'qty' => 0, 'value' => 0, 'pq' => 0, 'kg' => 0, 'open_qty' => 0, 'open_kg' => 0, 'pmin' => null, 'pmax' => null];
|
||||
$d12 = date('Y-m-d', strtotime('-365 days'));
|
||||
$d24 = date('Y-m-d', strtotime('-730 days'));
|
||||
|
||||
foreach ($lines as $l) {
|
||||
$qty = (float)$l['qty'];
|
||||
$val = (float)$l['value'];
|
||||
$date = (string)$l['order_date'];
|
||||
$inP = $date >= $from && $date <= $to;
|
||||
$res = self::lineIsOpen($l);
|
||||
$price = ($val > 0 && $qty > 0) ? $val / $qty : null;
|
||||
$c = $l['customer'];
|
||||
|
||||
if ($res > 0) {
|
||||
$tot['open_qty'] += $res;
|
||||
foreach (self::mixKg($mi, $kind, $res)['kg'] as $mid => $kg) {
|
||||
$mixKgO[$mid] = ($mixKgO[$mid] ?? 0) + $kg;
|
||||
$tot['open_kg'] += $kg;
|
||||
}
|
||||
}
|
||||
if (!$inP) continue;
|
||||
|
||||
$kgLine = 0;
|
||||
foreach (self::mixKg($mi, $kind, $qty)['kg'] as $mid => $kg) {
|
||||
$mixKgP[$mid] = ($mixKgP[$mid] ?? 0) + $kg;
|
||||
$kgLine += $kg;
|
||||
}
|
||||
$tot['n']++;
|
||||
$tot['qty'] += $qty;
|
||||
$tot['value'] += $val;
|
||||
$tot['kg'] += $kgLine;
|
||||
|
||||
if (!isset($cl[$c])) {
|
||||
$cl[$c] = [
|
||||
'code' => $c,
|
||||
'name' => $names[$c] ?? $c,
|
||||
'n' => 0,
|
||||
'qty' => 0,
|
||||
'value' => 0,
|
||||
'pq' => 0,
|
||||
'first_date' => null,
|
||||
'first_price' => null,
|
||||
'last_date' => null,
|
||||
'last_price' => null,
|
||||
'v12' => 0,
|
||||
'q12' => 0,
|
||||
'vp' => 0,
|
||||
'qp' => 0,
|
||||
'open_qty' => 0
|
||||
];
|
||||
}
|
||||
$x = &$cl[$c];
|
||||
$x['n']++;
|
||||
$x['qty'] += $qty;
|
||||
$x['value'] += $val;
|
||||
$x['open_qty'] += $res;
|
||||
if ($price !== null) {
|
||||
$tot['pq'] += $qty;
|
||||
$tot['pmin'] = $tot['pmin'] === null ? $price : min($tot['pmin'], $price);
|
||||
$tot['pmax'] = $tot['pmax'] === null ? $price : max($tot['pmax'], $price);
|
||||
$x['pq'] += $qty;
|
||||
if ($x['first_date'] === null) {
|
||||
$x['first_date'] = $date;
|
||||
$x['first_price'] = $price;
|
||||
}
|
||||
$x['last_date'] = $date;
|
||||
$x['last_price'] = $price;
|
||||
if ($date > $d12) {
|
||||
$x['v12'] += $val;
|
||||
$x['q12'] += $qty;
|
||||
} elseif ($date > $d24) {
|
||||
$x['vp'] += $val;
|
||||
$x['qp'] += $qty;
|
||||
}
|
||||
$ym = substr($date, 0, 7);
|
||||
$months[$ym]['v'] = ($months[$ym]['v'] ?? 0) + $val;
|
||||
$months[$ym]['q'] = ($months[$ym]['q'] ?? 0) + $qty;
|
||||
}
|
||||
unset($x);
|
||||
|
||||
$out[] = [
|
||||
'date' => $date,
|
||||
'ord' => $l['internal_ord_no'],
|
||||
'customer' => $c,
|
||||
'cname' => $names[$c] ?? $c,
|
||||
'item' => $l['item'],
|
||||
'description' => $l['description'],
|
||||
'raw' => $l['matrice_misura'],
|
||||
'qty' => round($qty, 3),
|
||||
'value' => round($val, 2),
|
||||
'price' => $price !== null ? round($price, 4) : null,
|
||||
'open' => round($res, 3),
|
||||
'kg' => round($kgLine, 1),
|
||||
];
|
||||
}
|
||||
|
||||
$avg = $tot['pq'] > 0 ? $tot['value'] / $tot['pq'] : null;
|
||||
$clients = [];
|
||||
foreach ($cl as $x) {
|
||||
$p = $x['pq'] > 0 ? $x['value'] / $x['pq'] : null;
|
||||
$p12 = $x['q12'] > 0 ? $x['v12'] / $x['q12'] : null;
|
||||
$pp = $x['qp'] > 0 ? $x['vp'] / $x['qp'] : null;
|
||||
$clients[] = [
|
||||
'code' => $x['code'],
|
||||
'name' => $x['name'],
|
||||
'n' => $x['n'],
|
||||
'qty' => round($x['qty'], 3),
|
||||
'value' => round($x['value'], 2),
|
||||
'price' => $p !== null ? round($p, 4) : null,
|
||||
'dev' => ($p !== null && $avg) ? round(($p - $avg) / $avg * 100, 1) : null,
|
||||
'first_date' => $x['first_date'],
|
||||
'first_price' => $x['first_price'] !== null ? round($x['first_price'], 4) : null,
|
||||
'last_date' => $x['last_date'],
|
||||
'last_price' => $x['last_price'] !== null ? round($x['last_price'], 4) : null,
|
||||
'var' => ($p12 !== null && $pp) ? round(($p12 - $pp) / $pp * 100, 1) : null,
|
||||
'open_qty' => round($x['open_qty'], 3),
|
||||
];
|
||||
}
|
||||
usort($clients, fn($a, $b) => $b['value'] <=> $a['value']);
|
||||
|
||||
ksort($months);
|
||||
$mrows = [];
|
||||
foreach ($months as $ym => $m) $mrows[] = ['ym' => $ym, 'price' => $m['q'] > 0 ? round($m['v'] / $m['q'], 4) : null, 'qty' => round($m['q'], 3)];
|
||||
|
||||
$mixRows = [];
|
||||
if ($mi) {
|
||||
foreach ($mi['mix'] as $x) {
|
||||
$share = $mi['gm_total'] > 0 ? ($x['gm'] > 0 ? $x['gm'] / $mi['gm_total'] : 0) : (count($mi['mix']) ? 1 / count($mi['mix']) : 0);
|
||||
$mixRows[] = [
|
||||
'id' => $x['id'],
|
||||
'name' => $x['name'],
|
||||
'out' => $x['out'],
|
||||
'gm' => $x['gm'],
|
||||
'share' => round($share * 100, 1),
|
||||
'kg' => round($mixKgP[$x['id']] ?? 0, 1),
|
||||
'kg_open' => round($mixKgO[$x['id']] ?? 0, 1),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$status = self::mixKg($mi, $kind, 1)['status'];
|
||||
return [
|
||||
'success' => true,
|
||||
'key' => $key,
|
||||
'head' => [
|
||||
'id' => $first['id'],
|
||||
'cat' => $first['cat'],
|
||||
'cat_label' => self::CAT_LABELS[$first['cat']] ?? $first['cat'],
|
||||
'type' => $first['type'],
|
||||
'type_label' => self::TYPE_LABELS[$first['type']] ?? $first['type'],
|
||||
'name' => $first['label'],
|
||||
'code' => $first['code'],
|
||||
'photo' => $first['id'] ? ($ix['info'][$first['id']]['photo'] ?? null) : null,
|
||||
'uom' => $uom,
|
||||
'kind' => $kind,
|
||||
'raws' => array_values(array_unique(array_map([self::class, 'norm'], $raws))),
|
||||
'lines' => $prodLines,
|
||||
'ws' => $mi['ws'] ?? null,
|
||||
'mix_source' => $mi['source'] ?? 'nessuna',
|
||||
'gm_total' => $mi['gm_total'] ?? 0,
|
||||
'mix_status' => $status,
|
||||
],
|
||||
'tot' => [
|
||||
'n' => $tot['n'],
|
||||
'qty' => round($tot['qty'], 3),
|
||||
'value' => round($tot['value'], 2),
|
||||
'price' => $avg !== null ? round($avg, 4) : null,
|
||||
'pmin' => $tot['pmin'] !== null ? round($tot['pmin'], 4) : null,
|
||||
'pmax' => $tot['pmax'] !== null ? round($tot['pmax'], 4) : null,
|
||||
'kg' => round($tot['kg'], 1),
|
||||
'open_qty' => round($tot['open_qty'], 3),
|
||||
'open_kg' => round($tot['open_kg'], 1),
|
||||
'clients' => count($clients),
|
||||
],
|
||||
'clients' => $clients,
|
||||
'months' => $mrows,
|
||||
'mix' => $mixRows,
|
||||
'lines' => array_reverse($out),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -413,10 +413,107 @@
|
||||
<li><b>Da consegnare</b>: righe aperte con quantità mancante e date (ZIBO e cliente). Righe molto vecchie spesso sono da chiudere in Mago.</li>
|
||||
<li><b>Ordini</b>: gli ordini del cliente; clic sul numero per aprirlo.</li>
|
||||
<li><b>Articoli</b>: cosa compra, quanto, e il <b>prezzo medio</b> degli ultimi 12 mesi rispetto ai 12 precedenti: utile per verificare gli aumenti di listino.</li>
|
||||
<li><b>Matrici</b>: le matrici che ordina, con il prezzo per metro o per kg e la variazione sull'anno prima. Clic sul nome per confrontarlo con gli altri clienti.</li>
|
||||
<li><b>Fatture</b>: fatture, acconti e note di credito degli ultimi 3 anni.</li>
|
||||
</ul>
|
||||
<p class="sh-note">Ogni tabella ha il suo pulsante Excel.</p>`,
|
||||
},
|
||||
|
||||
/* ===================== MATRICI ===================== */
|
||||
"page.matrici": {
|
||||
t: "Matrici: come si legge",
|
||||
b: `<p>A che prezzo vendiamo <b>ogni matrice</b>, nell'unità in cui la vendiamo: <b>€/metro</b> se a metri, <b>€/kg</b> se a chili. E quanta <b>mescola</b> consuma.</p>
|
||||
<ul>
|
||||
<li>I dati vengono dalle <b>righe degli ordini</b>: prezzo = imponibile della riga ÷ quantità.</li>
|
||||
<li>Esclusi: righe con quantità 0 o 1 (servizi), spese, ordini annullati e "listino".</li>
|
||||
<li><b>Clienti</b> in alto: scegline uno per vedere <b>cosa ordina</b> e a che prezzo.</li>
|
||||
<li>Clic su una matrice (tabella, bolla o elenco) per <b>prezzi nel tempo cliente per cliente</b>, mescole e righe d'ordine.</li>
|
||||
</ul>
|
||||
<p class="sh-note">Le mescole sono una stima dall'ultima scheda di lavoro, senza scarto. Le righe a pezzi aspettano la conversione pezzi → metri.</p>`,
|
||||
},
|
||||
"mx.types": {
|
||||
t: "Tipo",
|
||||
b: `<p>Il tipo viene dalla lettera iniziale del campo <b>Matrice/Misura</b> di Mago:</p>
|
||||
<ul>
|
||||
<li><span class="sh-chip" style="background:#ede9fe;color:#5b21b6">Matrice</span> M + codice (anche in mezzo, es. Q8X8M00936P). Si cerca in anagrafica <b>senza la M</b>, con e senza revisione.</li>
|
||||
<li><span class="sh-chip" style="background:#e0f2fe;color:#075985">Tubo</span> T + misura (es. T 10X15)</li>
|
||||
<li><span class="sh-chip" style="background:#dcfce7;color:#166534">Rettangolo</span> R + misura (es. R 110X2)</li>
|
||||
<li><span class="sh-chip" style="background:#fce7f3;color:#9d174d">Diametro</span> D + misura (es. D 3,3)</li>
|
||||
<li><b>Altri</b>: Q quadrato, S, valori non riconosciuti.</li>
|
||||
</ul>
|
||||
<p>Avvisi accanto alle matrici: <span class="sh-chip sh-orange">non in anagrafica</span> codice che non esiste in anagrafica · <span class="sh-chip sh-orange">altra revisione</span> revisione diversa da quella in anagrafica · <span class="sh-chip sh-orange">abbinata a mano</span> da "Ricorda corrispondenza".</p>
|
||||
<p class="sh-note"><b>Da sistemare</b> raccoglie matrici non in anagrafica, altre revisioni e valori non riconosciuti.</p>`,
|
||||
},
|
||||
"mx.k.value": {
|
||||
t: "Venduto con matrice",
|
||||
b: `<p>Imponibile delle righe d'ordine del periodo che hanno il campo <b>Matrice/Misura</b> compilato, con il numero di matrici dell'anagrafica e di clienti.</p>`,
|
||||
},
|
||||
"mx.k.qty": {
|
||||
t: "Venduto a metri / a chili",
|
||||
b: `<p>Quantità venduta nel periodo e <b>prezzo medio</b> (imponibile ÷ quantità) su tutte le righe in quell'unità.</p>
|
||||
<p class="sh-note">Clic sul box per vedere solo le matrici vendute in quell'unità.</p>`,
|
||||
},
|
||||
"mx.k.mix": {
|
||||
t: "Mescola consumata",
|
||||
b: `<p>Kg di mescola stimati per le righe vendute nel periodo.</p>
|
||||
<p><b>Ancora da produrre</b>: kg necessari per le righe ancora da consegnare (qualsiasi data). È la base per gli acquisti di mescola.</p>`,
|
||||
},
|
||||
"mx.k.pz": {
|
||||
t: "Venduto a pezzi",
|
||||
b: `<p>Righe vendute a pezzi (UM N.). Il prezzo al pezzo c'è, ma la mescola non si può stimare finché non c'è la conversione pezzi → metri.</p>`,
|
||||
},
|
||||
"mx.k.cover": {
|
||||
t: "Copertura anagrafica",
|
||||
b: `<p>Quanta parte del venduto è collegata a una <b>matrice dell'anagrafica</b> (verde), quanta è tubi, rettangoli, diametri e altri profili (viola), quanta resta da sistemare (arancio).</p>
|
||||
<p class="sh-note">Cosa fare: clic sul box per la lista "Da sistemare" e completare l'anagrafica matrici o le corrispondenze.</p>`,
|
||||
},
|
||||
"mx.map": {
|
||||
t: "Mappa dei prezzi",
|
||||
b: `<p>Ogni bolla è una matrice venduta nel periodo (le prime 150 per valore).</p>
|
||||
<ul>
|
||||
<li><b>Verso destra</b>: più quantità venduta (scala logaritmica).</li>
|
||||
<li><b>Verso l'alto</b>: prezzo medio più alto.</li>
|
||||
<li><b>Più grande</b>: più fatturato.</li>
|
||||
</ul>
|
||||
<p class="sh-note">Le bolle in basso a destra sono matrici molto vendute a prezzo basso: le prime da guardare per un aumento.</p>`,
|
||||
},
|
||||
"mx.moves": {
|
||||
t: "Prezzi in movimento",
|
||||
b: `<p>Prezzo medio degli <b>ultimi 12 mesi</b> rispetto ai <b>12 mesi precedenti</b>, per le matrici vendute in entrambi i periodi.</p>
|
||||
<p class="sh-note">Serve a verificare che gli aumenti di listino siano stati applicati e a scoprire cali non voluti.</p>`,
|
||||
},
|
||||
"mx.table": {
|
||||
t: "Classifica matrici",
|
||||
b: `<ul>
|
||||
<li><b>Prezzo medio</b>: imponibile ÷ quantità del periodo.</li>
|
||||
<li><b>Forbice</b>: prezzo minimo e massimo praticato. Barra <span class="sh-chip sh-green">verde</span> fino al 10% di differenza, <span class="sh-chip sh-amber">gialla</span> fino al 25%, <span class="sh-chip sh-red">rossa</span> oltre.</li>
|
||||
<li><b>12 mesi</b>: prezzo mese per mese e variazione sui 12 mesi precedenti.</li>
|
||||
<li><b>Mescola</b>: kg stimati, oppure il motivo per cui mancano (<span class="sh-chip sh-amber">a pezzi</span>, <span class="sh-chip sh-red">g/m mancante</span>).</li>
|
||||
</ul>
|
||||
<p class="sh-note">La stessa matrice venduta a metri e a chili compare su due righe.</p>`,
|
||||
},
|
||||
"mx.mix": {
|
||||
t: "Mescole consumate per mese",
|
||||
b: `<p>Kg di mescola stimati dalle vendite, mese per mese.</p>
|
||||
<ul>
|
||||
<li>Si usa l'<b>ultima scheda di lavoro</b> della matrice (prima quelle attive).</li>
|
||||
<li>Righe a metri: kg = metri × g/m di ciascuna mescola ÷ 1000.</li>
|
||||
<li>Righe a chili: i kg si dividono tra le mescole in proporzione ai g/m; senza g/m in parti uguali (2 mescole 50/50, 3 mescole 33%).</li>
|
||||
</ul>
|
||||
<p class="sh-note">Per ora senza scarto.</p>`,
|
||||
},
|
||||
"mx.mixneed": {
|
||||
t: "Fabbisogno mescole",
|
||||
b: `<p><b>Venduto</b>: kg di mescola delle righe ordinate nel periodo.</p>
|
||||
<p><b>Da produrre</b>: kg che servono per le righe <b>ancora da consegnare</b>, qualsiasi sia la data dell'ordine.</p>
|
||||
<p class="sh-note">Quando avremo il costo delle mescole, da qui si calcolerà anche il costo grezzo.</p>`,
|
||||
},
|
||||
"mx.dprice": {
|
||||
t: "Prezzo nel tempo",
|
||||
b: `<p>Ogni punto è una <b>riga d'ordine</b>: altezza = prezzo, colore = cliente (i primi 8 per valore, gli altri insieme).</p>
|
||||
<p>La linea scura è il <b>prezzo medio del mese</b>. Punti sotto la linea = clienti che pagano meno della media.</p>
|
||||
<p class="sh-note">Nella tabella Clienti, "Rispetto alla media" mostra di quanto ognuno paga più o meno.</p>`,
|
||||
},
|
||||
};
|
||||
|
||||
/* ===================== STILE ===================== */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user