302 lines
13 KiB
PHP
302 lines
13 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Preventivo per matrice: costo della mescola, storico prezzi di vendita, preventivi salvati.
|
|
* Usa MagoMatriceStats (riconoscimento matrici, schede di lavoro, costo) e MagoMixPurchases (€/kg pagati).
|
|
*/
|
|
require_once __DIR__ . '/MagoMatriceStats.php';
|
|
|
|
class MagoQuote
|
|
{
|
|
/** Mesi di storico per prezzi e costo nel grafico */
|
|
const HISTORY_MONTHS = 24;
|
|
|
|
/** Ricerca matrici per la tendina (nome, con foto e indicazione della scheda di lavoro) */
|
|
public static function searchMatrici(PDO $pdo, string $q, int $limit = 30): array
|
|
{
|
|
$q = trim($q);
|
|
$p = [];
|
|
$w = '';
|
|
if ($q !== '') {
|
|
// cerco anche senza la M iniziale: "M00786S" trova "00786S"
|
|
$q2 = preg_replace('/^M(?=\d)/i', '', $q);
|
|
$w = "WHERE m.nome LIKE :q OR m.nome LIKE :q2";
|
|
$p['q'] = '%' . $q . '%';
|
|
$p['q2'] = '%' . $q2 . '%';
|
|
}
|
|
$st = $pdo->prepare("SELECT m.id, m.nome, m.photo,
|
|
(SELECT COUNT(*) FROM work_sheets ws JOIN work_sheet_mescole wm ON wm.worksheet_id = ws.id
|
|
WHERE ws.idmatrice = m.id AND wm.mix_weight_g_m > 0) AS n_gm
|
|
FROM matrice m $w
|
|
ORDER BY (m.nome LIKE :start) DESC, m.nome
|
|
LIMIT " . (int)$limit);
|
|
$p['start'] = ($q !== '' ? preg_replace('/^M(?=\d)/i', '', $q) : '') . '%';
|
|
$st->execute($p);
|
|
$out = [];
|
|
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
|
$out[] = ['id' => (int)$r['id'], 'text' => $r['nome'], 'photo' => MagoMatriceStats::photoUrl($r['photo']), 'has_gm' => (int)$r['n_gm'] > 0];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/** Valori Mago del campo Matrice/Misura che portano a questa matrice */
|
|
public static function rawsFor(PDO $pdo, int $id): array
|
|
{
|
|
$raws = [];
|
|
foreach ($pdo->query("SELECT DISTINCT matrice_misura FROM mago_sale_ord_detail WHERE matrice_misura <> ''")->fetchAll(PDO::FETCH_COLUMN) as $raw) {
|
|
if (MagoMatriceStats::resolve($pdo, $raw)['id'] === $id) $raws[] = $raw;
|
|
}
|
|
return $raws;
|
|
}
|
|
|
|
/**
|
|
* Tutti i dati per il preventivo.
|
|
* @param string $kind m | kg
|
|
*/
|
|
public static function data(PDO $pdo, int $id, string $kind, ?string $customer): array
|
|
{
|
|
$kind = $kind === 'kg' ? 'kg' : 'm';
|
|
$st = $pdo->prepare("SELECT id, nome, photo FROM matrice WHERE id = ?");
|
|
$st->execute([$id]);
|
|
$mat = $st->fetch(PDO::FETCH_ASSOC);
|
|
if (!$mat) return ['success' => false, 'message' => 'Matrice non trovata'];
|
|
|
|
$mi = MagoMatriceStats::mixInfo($pdo, [$id])[$id] ?? null;
|
|
$prices = MagoMatriceStats::mixPrices($pdo);
|
|
$cost = MagoMatriceStats::costOf($mi, $kind, $prices);
|
|
|
|
// mescole con tutti i dati per il calcolo modificabile nella pagina
|
|
$mix = [];
|
|
foreach ($mi['mix'] ?? [] as $x) {
|
|
$p = $prices[$x['id']] ?? null;
|
|
$mix[] = [
|
|
'id' => $x['id'],
|
|
'name' => $x['name'],
|
|
'out' => $x['out'],
|
|
'gm' => $x['gm'] !== null && $x['gm'] > 0 ? (float)$x['gm'] : null,
|
|
'eur_kg' => $p ? $p['price'] : null,
|
|
'last_price' => $p['last_price'] ?? null,
|
|
'last_date' => $p['last_date'] ?? null,
|
|
'old' => $p ? $p['old'] : null,
|
|
'supplier' => $p['last_supplier'] ?? null,
|
|
'check' => $p['check'] ?? null,
|
|
];
|
|
}
|
|
|
|
// costo mese per mese
|
|
$months = [];
|
|
for ($i = self::HISTORY_MONTHS - 1; $i >= 0; $i--) $months[] = date('Y-m', strtotime("first day of -$i months"));
|
|
$costMonths = [];
|
|
if ($cost['cost'] !== null) {
|
|
foreach ($months as $ym) {
|
|
$c = MagoMatriceStats::costOf($mi, $kind, $prices, $ym);
|
|
if ($c['cost'] !== null) $costMonths[$ym] = $c['cost'];
|
|
}
|
|
}
|
|
|
|
// vendite della matrice nell'unità scelta
|
|
$raws = self::rawsFor($pdo, $id);
|
|
$since = $months[0] . '-01';
|
|
$d12 = date('Y-m-d', strtotime('-365 days'));
|
|
$lines = [];
|
|
foreach ($raws ? MagoMatriceStats::fetchLines($pdo, $since, [], $raws) : [] as $l) {
|
|
if ($l['order_date'] < $since) continue;
|
|
if (MagoMatriceStats::uomKind($l['uom']) !== $kind) continue;
|
|
if ((float)$l['value'] <= 0 || (float)$l['qty'] <= 0) continue;
|
|
$lines[] = $l;
|
|
}
|
|
$names = MagoMatriceStats::customerNames($pdo, array_column($lines, 'customer'));
|
|
|
|
$all = ['m' => [], 'v12' => 0, 'q12' => 0, 'min' => null, 'max' => null, 'clients' => []];
|
|
$cus = ['m' => [], 'v12' => 0, 'q12' => 0, 'last_price' => null, 'last_date' => null, 'lines' => []];
|
|
foreach ($lines as $l) {
|
|
$q = (float)$l['qty'];
|
|
$v = (float)$l['value'];
|
|
$pr = $v / $q;
|
|
$ym = substr($l['order_date'], 0, 7);
|
|
$all['m'][$ym]['v'] = ($all['m'][$ym]['v'] ?? 0) + $v;
|
|
$all['m'][$ym]['q'] = ($all['m'][$ym]['q'] ?? 0) + $q;
|
|
if ($l['order_date'] > $d12) {
|
|
$all['v12'] += $v;
|
|
$all['q12'] += $q;
|
|
$all['min'] = $all['min'] === null ? $pr : min($all['min'], $pr);
|
|
$all['max'] = $all['max'] === null ? $pr : max($all['max'], $pr);
|
|
$c = &$all['clients'][$l['customer']];
|
|
$c['v'] = ($c['v'] ?? 0) + $v;
|
|
$c['q'] = ($c['q'] ?? 0) + $q;
|
|
unset($c);
|
|
}
|
|
if ($customer !== null && $l['customer'] === $customer) {
|
|
$cus['m'][$ym]['v'] = ($cus['m'][$ym]['v'] ?? 0) + $v;
|
|
$cus['m'][$ym]['q'] = ($cus['m'][$ym]['q'] ?? 0) + $q;
|
|
if ($l['order_date'] > $d12) {
|
|
$cus['v12'] += $v;
|
|
$cus['q12'] += $q;
|
|
}
|
|
$cus['last_price'] = $pr;
|
|
$cus['last_date'] = $l['order_date'];
|
|
$cus['lines'][] = ['date' => $l['order_date'], 'ord' => $l['internal_ord_no'], 'qty' => round($q, 3), 'price' => round($pr, 4)];
|
|
}
|
|
}
|
|
$avgM = function (array $m) {
|
|
$o = [];
|
|
ksort($m);
|
|
foreach ($m as $ym => $x) $o[$ym] = round($x['v'] / $x['q'], 4);
|
|
return $o;
|
|
};
|
|
$clients = [];
|
|
foreach ($all['clients'] as $code => $c) $clients[] = ['code' => $code, 'name' => $names[$code] ?? $code, 'price' => round($c['v'] / $c['q'], 4), 'qty' => round($c['q'], 1)];
|
|
usort($clients, fn($a, $b) => $b['qty'] <=> $a['qty']);
|
|
|
|
$avg12 = $all['q12'] > 0 ? $all['v12'] / $all['q12'] : null;
|
|
return [
|
|
'success' => true,
|
|
'matrice' => [
|
|
'id' => (int)$mat['id'],
|
|
'name' => $mat['nome'],
|
|
'photo' => MagoMatriceStats::photoUrl($mat['photo']),
|
|
'raws' => $raws,
|
|
'ws' => $mi['ws'] ?? null,
|
|
'mix_source' => $mi['source'] ?? 'nessuna'
|
|
],
|
|
'kind' => $kind,
|
|
'uom' => $kind === 'kg' ? 'KG' : 'MT',
|
|
'price_months' => MagoMixPurchases::PRICE_MONTHS,
|
|
'cost' => ['cost' => $cost['cost'], 'status' => $cost['status'], 'missing' => $cost['missing'], 'check' => $cost['check']],
|
|
'mix' => $mix,
|
|
'months' => $months,
|
|
'cost_months' => $costMonths,
|
|
'all' => [
|
|
'months' => $avgM($all['m']),
|
|
'avg12' => $avg12 !== null ? round($avg12, 4) : null,
|
|
'min12' => $all['min'] !== null ? round($all['min'], 4) : null,
|
|
'max12' => $all['max'] !== null ? round($all['max'], 4) : null,
|
|
'clients' => array_slice($clients, 0, 12),
|
|
'n_clients' => count($clients),
|
|
'markup' => ($avg12 && $cost['cost'] && $cost['status'] === 'ok') ? round(($avg12 / $cost['cost'] - 1) * 100, 0) : null,
|
|
],
|
|
'customer' => $customer === null ? null : [
|
|
'code' => $customer,
|
|
'name' => $names[$customer] ?? self::customerName($pdo, $customer),
|
|
'months' => $avgM($cus['m']),
|
|
'avg12' => $cus['q12'] > 0 ? round($cus['v12'] / $cus['q12'], 4) : null,
|
|
'last_price' => $cus['last_price'] !== null ? round($cus['last_price'], 4) : null,
|
|
'last_date' => $cus['last_date'],
|
|
'lines' => array_slice(array_reverse($cus['lines']), 0, 12),
|
|
],
|
|
'quotes' => self::quotes($pdo, $id, null),
|
|
];
|
|
}
|
|
|
|
public static function customerName(PDO $pdo, string $code): string
|
|
{
|
|
return MagoMatriceStats::customerNames($pdo, [$code])[$code] ?? $code;
|
|
}
|
|
|
|
/** Preventivi salvati di una matrice (tutti i clienti), più recenti prima */
|
|
public static function quotes(PDO $pdo, ?int $idmatrice, ?string $customer, int $limit = 50): array
|
|
{
|
|
$w = [];
|
|
$p = [];
|
|
if ($idmatrice) {
|
|
$w[] = 'q.idmatrice = :m';
|
|
$p['m'] = $idmatrice;
|
|
}
|
|
if ($customer) {
|
|
$w[] = 'q.customer_code = :c';
|
|
$p['c'] = $customer;
|
|
}
|
|
$user = "TRIM(CONCAT(IFNULL(u.first_name, ''), ' ', IFNULL(u.last_name, '')))";
|
|
try {
|
|
$st = $pdo->prepare("SELECT q.*, NULLIF($user, '') AS user_name, u.username
|
|
FROM sales_quotes q LEFT JOIN auth_users u ON u.id = q.user_id
|
|
" . ($w ? 'WHERE ' . implode(' AND ', $w) : '') . "
|
|
ORDER BY q.created_at DESC, q.id DESC LIMIT " . (int)$limit);
|
|
$st->execute($p);
|
|
return $st->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** Salva un preventivo. I numeri arrivano dalla pagina; il costo è ricalcolato dalle mescole inviate. */
|
|
public static function save(PDO $pdo, array $in, ?int $userId): array
|
|
{
|
|
$id = (int)($in['idmatrice'] ?? 0);
|
|
$st = $pdo->prepare("SELECT nome FROM matrice WHERE id = ?");
|
|
$st->execute([$id]);
|
|
$name = $st->fetchColumn();
|
|
if (!$name) return ['success' => false, 'message' => 'Matrice non trovata'];
|
|
$kind = ($in['kind'] ?? 'm') === 'kg' ? 'kg' : 'm';
|
|
$num = function ($v) {
|
|
if ($v === null || $v === '') return null;
|
|
return (float)str_replace(',', '.', (string)$v);
|
|
};
|
|
|
|
// mescole inviate dalla pagina (g/m e €/kg eventualmente corretti a mano)
|
|
$mix = json_decode((string)($in['mix'] ?? '[]'), true) ?: [];
|
|
$cost = 0.0;
|
|
$ok = 0;
|
|
$gmTot = 0.0;
|
|
foreach ($mix as $x) $gmTot += max(0, (float)($x['gm'] ?? 0));
|
|
foreach ($mix as &$x) {
|
|
$gm = $num($x['gm'] ?? null);
|
|
$eur = $num($x['eur_kg'] ?? null);
|
|
$x = ['id' => (int)($x['id'] ?? 0), 'name' => (string)($x['name'] ?? ''), 'gm' => $gm, 'eur_kg' => $eur, 'edited' => !empty($x['edited'])];
|
|
if ($eur === null) continue;
|
|
if ($kind === 'm') {
|
|
if ($gm) {
|
|
$cost += $gm / 1000 * $eur;
|
|
$ok++;
|
|
}
|
|
} else {
|
|
$share = $gmTot > 0 ? max(0, (float)$gm) / $gmTot : 1 / max(1, count($mix));
|
|
$cost += $share * $eur;
|
|
$ok++;
|
|
}
|
|
}
|
|
unset($x);
|
|
$costMix = $ok ? $cost : null;
|
|
$scrap = $num($in['scrap_pct'] ?? 0) ?? 0;
|
|
$other = $num($in['other_costs'] ?? 0) ?? 0;
|
|
$markup = $num($in['markup_pct'] ?? null);
|
|
$costTotal = $costMix !== null ? $costMix * (1 + $scrap / 100) + $other : ($other ?: null);
|
|
$suggested = ($costTotal !== null && $markup !== null) ? $costTotal * (1 + $markup / 100) : null;
|
|
$offered = $num($in['price_offered'] ?? null) ?? $suggested;
|
|
if ($offered === null) return ['success' => false, 'message' => 'Manca il prezzo: indica il ricarico o il prezzo proposto'];
|
|
|
|
$cust = trim((string)($in['customer'] ?? ''));
|
|
$st = $pdo->prepare("INSERT INTO sales_quotes
|
|
(created_at, user_id, idmatrice, matrice_name, customer_code, customer_name, uom, cost_mix, scrap_pct, other_costs, cost_total,
|
|
markup_pct, price_suggested, price_offered, ref_last_price, ref_avg_price, mix_detail, comment)
|
|
VALUES (NOW(), :u, :m, :mn, :cc, :cn, :uom, :cm, :sp, :oc, :ct, :mk, :ps, :po, :rl, :ra, :md, :cmt)");
|
|
$st->execute([
|
|
'u' => $userId ?: null,
|
|
'm' => $id,
|
|
'mn' => $name,
|
|
'cc' => $cust !== '' ? $cust : null,
|
|
'cn' => $cust !== '' ? self::customerName($pdo, $cust) : null,
|
|
'uom' => $kind === 'kg' ? 'KG' : 'MT',
|
|
'cm' => $costMix,
|
|
'sp' => $scrap,
|
|
'oc' => $other,
|
|
'ct' => $costTotal,
|
|
'mk' => $markup,
|
|
'ps' => $suggested,
|
|
'po' => $offered,
|
|
'rl' => $num($in['ref_last_price'] ?? null),
|
|
'ra' => $num($in['ref_avg_price'] ?? null),
|
|
'md' => json_encode($mix, JSON_UNESCAPED_UNICODE),
|
|
'cmt' => trim((string)($in['comment'] ?? '')) ?: null,
|
|
]);
|
|
return ['success' => true, 'id' => (int)$pdo->lastInsertId()];
|
|
}
|
|
|
|
public static function delete(PDO $pdo, int $id): bool
|
|
{
|
|
$st = $pdo->prepare("DELETE FROM sales_quotes WHERE id = ?");
|
|
$st->execute([$id]);
|
|
return $st->rowCount() > 0;
|
|
}
|
|
}
|