428 lines
20 KiB
PHP
428 lines
20 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Acquisti mescole da Mago4 e prezzo €/kg per mescola.
|
|
*
|
|
* - sync(): copia in mago_mix_purchases le righe delle FATTURE fornitore con articolo MESCOLE (Mago in sola lettura)
|
|
* - la mescola si riconosce dalla DESCRIZIONE della riga, confrontata con i "nomi d'acquisto" della mescola
|
|
* (tabella mescole_supplier_names): un nome vale se tutte le sue parole sono nella descrizione.
|
|
* Es. il nome "ATES71" riconosce "ATES71 M100 X 9mm spessore". Vince il nome più specifico (più parole).
|
|
* - prices(): €/kg per mescola = media ponderata sui kg degli ultimi PRICE_MONTHS mesi; se non ci sono acquisti
|
|
* nel periodo si usa l'ultimo prezzo pagato (segnalato come "vecchio").
|
|
*/
|
|
class MagoMixPurchases
|
|
{
|
|
/** Articolo Mago delle mescole */
|
|
const ITEM = 'MESCOLE';
|
|
/** Tipo documento fattura fornitore (9830400 DDT, 9830402 nota di credito) */
|
|
const DOC_INVOICE = 9830401;
|
|
/** Mesi per la media del prezzo (modificabile) */
|
|
const PRICE_MONTHS = 3;
|
|
/** Sync incrementale: rilegge le fatture degli ultimi N giorni */
|
|
const SYNC_DAYS = 90;
|
|
|
|
/** Fornitori il cui prezzo va verificato (es. conto lavoro: il prezzo non comprende le materie prime) */
|
|
const CHECK_SUPPLIERS = ['D022' => 'Der-Gom'];
|
|
|
|
/** Righe che non sono una mescola da prezzare */
|
|
const EXCLUDE_RE = '/RECUPER|RITAGLI|\bMIX\b|\bREC\b|DA FILTRARE|RILAVORAR|RESO NON LAVORATO|NON STANDARD|SCADENZA|VARIE|ACCELERANTI|\bPROVA\b/i';
|
|
|
|
const SYN = ['IMP' => 'IMPASTO', 'ZIBO' => 'ZB', 'MESCOLE' => 'MESCOLA', 'TMDT' => 'TMTD', 'TTMD' => 'TMTD', 'BIANCI' => 'BIANCO', 'ESCO' => 'ECO', 'PAFX' => 'PAF'];
|
|
const DROP = ['MESCOLA', 'ART', 'STRISCIA', 'PICCOLA', 'SPESSORE', 'MM', 'LP', 'SP', 'PV', 'ZBO', 'FILTRATA', 'FILTRATO', 'RILAVORATE', 'RIPRESE'];
|
|
|
|
private static ?array $namesCache = null;
|
|
|
|
/* ============================================================
|
|
* NORMALIZZAZIONE
|
|
* ============================================================ */
|
|
|
|
/**
|
|
* Parole significative di un nome o descrizione: maiuscole, senza revisione, gradi Comet (M100/L080),
|
|
* codici (B5904), formato/imballo (STRISCIA…), punteggiatura; "ATES71" → ATES 71; "01" → 1.
|
|
*/
|
|
public static function tokens(string $s): array
|
|
{
|
|
$s = strtoupper($s);
|
|
$s = preg_replace('/\bREV\.?\s*\d+/', ' ', $s);
|
|
$s = preg_replace('/\b[ML]\d{3}\b/', ' ', $s);
|
|
$s = preg_replace('/\(B\d+\)?/', ' ', $s);
|
|
$s = preg_replace('/\b\w*(STRISC|TRISCIA|STRSC)\w*\b.*$/s', ' ', $s);
|
|
$s = preg_replace('/(?<=[A-Z])(?=\d)|(?<=\d)(?=[A-Z])/', ' ', $s);
|
|
$out = [];
|
|
foreach (preg_split('/[^A-Z0-9]+/', $s) as $t) {
|
|
if ($t === '') continue;
|
|
if (ctype_digit($t)) $t = (string)(int)$t;
|
|
$t = self::SYN[$t] ?? $t;
|
|
if (in_array($t, self::DROP, true)) continue;
|
|
$out[$t] = true;
|
|
}
|
|
return array_keys($out);
|
|
}
|
|
|
|
public static function isExcluded(string $desc): bool
|
|
{
|
|
return (bool)preg_match(self::EXCLUDE_RE, $desc);
|
|
}
|
|
|
|
public static function revision(string $desc): ?int
|
|
{
|
|
return preg_match('/REV\.?\s*(\d+)/i', $desc, $m) ? (int)$m[1] : null;
|
|
}
|
|
|
|
/* ============================================================
|
|
* NOMI D'ACQUISTO
|
|
* ============================================================ */
|
|
|
|
/** Tutti i nomi d'acquisto con le loro parole */
|
|
public static function names(PDO $pdo, bool $refresh = false): array
|
|
{
|
|
if (self::$namesCache !== null && !$refresh) return self::$namesCache;
|
|
$rows = $pdo->query("SELECT sn.id, sn.idmescola, sn.idsupplier, sn.supplier_mix_name, sn.is_preferred, sn.note,
|
|
m.nome AS mescola, m.nomeuscita, m.is_active, s.supplier_name, s.mago_code
|
|
FROM mescole_supplier_names sn
|
|
JOIN mescole m ON m.id = sn.idmescola
|
|
LEFT JOIN suppliers s ON s.idsupplier = sn.idsupplier")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($rows as &$r) $r['tokens'] = self::tokens((string)$r['supplier_mix_name']);
|
|
unset($r);
|
|
return self::$namesCache = array_values(array_filter($rows, fn($r) => $r['tokens']));
|
|
}
|
|
|
|
/**
|
|
* Mescola di una descrizione d'acquisto.
|
|
* @return array status: ok | ambiguous | none | excluded ; name (riga nomi) ; candidates (id mescola)
|
|
*/
|
|
public static function matchDescription(PDO $pdo, string $desc, ?string $supplierCode = null): array
|
|
{
|
|
if (self::isExcluded($desc)) return ['status' => 'excluded', 'name' => null, 'candidates' => []];
|
|
$T = array_flip(self::tokens($desc));
|
|
$hits = [];
|
|
foreach (self::names($pdo) as $n) {
|
|
// nome legato a un fornitore con codice Mago diverso da quello della fattura → non vale
|
|
if ($supplierCode !== null && $n['mago_code'] && strcasecmp($n['mago_code'], $supplierCode) !== 0) continue;
|
|
foreach ($n['tokens'] as $t) if (!isset($T[$t])) continue 2;
|
|
$hits[] = $n;
|
|
}
|
|
if (!$hits) return ['status' => 'none', 'name' => null, 'candidates' => []];
|
|
$max = max(array_map(fn($n) => count($n['tokens']), $hits));
|
|
$best = array_values(array_filter($hits, fn($n) => count($n['tokens']) === $max));
|
|
$ids = array_values(array_unique(array_map(fn($n) => (int)$n['idmescola'], $best)));
|
|
if (count($ids) > 1) return ['status' => 'ambiguous', 'name' => $best[0], 'candidates' => $ids];
|
|
return ['status' => 'ok', 'name' => $best[0], 'candidates' => $ids];
|
|
}
|
|
|
|
/** Stesso nome (stesse parole) su mescole diverse */
|
|
public static function duplicateNames(PDO $pdo): array
|
|
{
|
|
$by = [];
|
|
foreach (self::names($pdo) as $n) {
|
|
$k = implode(' ', $n['tokens']);
|
|
$by[$k][(int)$n['idmescola']] = $n['mescola'];
|
|
}
|
|
return array_filter($by, fn($m) => count($m) > 1);
|
|
}
|
|
|
|
/* ============================================================
|
|
* RIGHE D'ACQUISTO ABBINATE
|
|
* ============================================================ */
|
|
|
|
/** Righe locali (fatture, KG) con la mescola riconosciuta */
|
|
public static function lines(PDO $pdo, ?string $since = null): array
|
|
{
|
|
$p = [];
|
|
$w = "item = '" . self::ITEM . "' AND document_type = " . self::DOC_INVOICE;
|
|
if ($since) {
|
|
$w .= " AND doc_date >= :since";
|
|
$p['since'] = $since;
|
|
}
|
|
$st = $pdo->prepare("SELECT * FROM mago_mix_purchases WHERE $w ORDER BY doc_date, purchase_doc_id, line");
|
|
$st->execute($p);
|
|
$out = [];
|
|
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
|
$m = self::matchDescription($pdo, (string)$r['description'], $r['supplier_code']);
|
|
$r['match'] = $m['status'];
|
|
$r['idmescola'] = $m['status'] === 'ok' ? $m['candidates'][0] : null;
|
|
$r['candidates'] = $m['candidates'];
|
|
$r['qty'] = (float)$r['qty'];
|
|
$r['value'] = (float)$r['taxable_amount'] != 0 ? (float)$r['taxable_amount'] : (float)$r['qty'] * (float)$r['unit_value'];
|
|
$r['rev'] = self::revision((string)$r['description']);
|
|
$out[] = $r;
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* €/kg per mescola.
|
|
* @return array [idmescola => price, last_price, last_date, kg_window, kg_total, old (bool), months [ym => €/kg], suppliers [nome => €/kg],
|
|
* last_supplier, check (nome fornitore da verificare o null)]
|
|
*/
|
|
public static function prices(PDO $pdo, int $months = self::PRICE_MONTHS, ?array $lines = null): array
|
|
{
|
|
$months = max(1, $months);
|
|
$from = date('Y-m-d', strtotime("-$months months"));
|
|
$lines = $lines ?? self::lines($pdo);
|
|
$acc = [];
|
|
foreach ($lines as $l) {
|
|
if (!$l['idmescola'] || $l['qty'] <= 0 || $l['value'] <= 0 || strtoupper(trim((string)$l['uom'])) !== 'KG') continue;
|
|
$a = &$acc[$l['idmescola']];
|
|
$a['kg_total'] = ($a['kg_total'] ?? 0) + $l['qty'];
|
|
$a['val_total'] = ($a['val_total'] ?? 0) + $l['value'];
|
|
if ($l['doc_date'] >= $from) {
|
|
$a['kg_w'] = ($a['kg_w'] ?? 0) + $l['qty'];
|
|
$a['val_w'] = ($a['val_w'] ?? 0) + $l['value'];
|
|
$s = $l['supplier_name'] ?: $l['supplier_code'];
|
|
$a['sup'][$s]['kg'] = ($a['sup'][$s]['kg'] ?? 0) + $l['qty'];
|
|
$a['sup'][$s]['val'] = ($a['sup'][$s]['val'] ?? 0) + $l['value'];
|
|
}
|
|
if (($a['last_date'] ?? '') <= $l['doc_date']) {
|
|
$a['last_date'] = $l['doc_date'];
|
|
$a['last_price'] = $l['value'] / $l['qty'];
|
|
$a['last_sc'] = (string)$l['supplier_code'];
|
|
$a['last_sn'] = (string)($l['supplier_name'] ?: $l['supplier_code']);
|
|
}
|
|
$ym = substr((string)$l['doc_date'], 0, 7);
|
|
$a['m'][$ym]['kg'] = ($a['m'][$ym]['kg'] ?? 0) + $l['qty'];
|
|
$a['m'][$ym]['val'] = ($a['m'][$ym]['val'] ?? 0) + $l['value'];
|
|
unset($a);
|
|
}
|
|
$out = [];
|
|
foreach ($acc as $id => $a) {
|
|
$win = !empty($a['kg_w']);
|
|
$ms = [];
|
|
ksort($a['m']);
|
|
foreach ($a['m'] as $ym => $x) $ms[$ym] = round($x['val'] / $x['kg'], 4);
|
|
$sup = [];
|
|
foreach ($a['sup'] ?? [] as $s => $x) $sup[$s] = round($x['val'] / $x['kg'], 4);
|
|
$out[$id] = [
|
|
'price' => round($win ? $a['val_w'] / $a['kg_w'] : $a['last_price'], 4),
|
|
'last_price' => round($a['last_price'], 4),
|
|
'last_date' => $a['last_date'],
|
|
'kg_window' => round($a['kg_w'] ?? 0, 1),
|
|
'kg_total' => round($a['kg_total'], 1),
|
|
'old' => !$win,
|
|
'months' => $ms,
|
|
'suppliers' => $sup,
|
|
'last_supplier' => $a['last_sn'],
|
|
'check' => self::CHECK_SUPPLIERS[$a['last_sc']] ?? null,
|
|
];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/** Prezzo di una mescola in un mese: quello del mese, altrimenti l'ultimo dei mesi precedenti */
|
|
public static function priceAt(array $p, string $ym): ?float
|
|
{
|
|
$best = null;
|
|
foreach ($p['months'] as $m => $v) {
|
|
if ($m > $ym) break;
|
|
$best = $v;
|
|
}
|
|
return $best;
|
|
}
|
|
|
|
/** Descrizioni d'acquisto non abbinate (o ambigue), raggruppate per parole, ordinate per valore */
|
|
public static function unmatched(PDO $pdo, ?array $lines = null): array
|
|
{
|
|
$lines = $lines ?? self::lines($pdo);
|
|
$g = [];
|
|
foreach ($lines as $l) {
|
|
if (!in_array($l['match'], ['none', 'ambiguous'], true)) continue;
|
|
$k = implode(' ', self::tokens((string)$l['description']));
|
|
$x = &$g[$k];
|
|
$x['key'] = $k;
|
|
$x['description'] = $x['description'] ?? trim(preg_replace('/\s+/', ' ', (string)$l['description']));
|
|
$x['suggest'] = $x['suggest'] ?? self::suggestName((string)$l['description']);
|
|
$x['status'] = $l['match'];
|
|
$x['candidates'] = $l['candidates'];
|
|
$x['supplier_code'] = $l['supplier_code'];
|
|
$x['supplier_name'] = $l['supplier_name'];
|
|
$x['n'] = ($x['n'] ?? 0) + 1;
|
|
$x['kg'] = ($x['kg'] ?? 0) + $l['qty'];
|
|
$x['value'] = ($x['value'] ?? 0) + $l['value'];
|
|
$x['last_date'] = max($x['last_date'] ?? '', (string)$l['doc_date']);
|
|
unset($x);
|
|
}
|
|
foreach ($g as &$x) {
|
|
$x['price'] = $x['kg'] > 0 ? round($x['value'] / $x['kg'], 4) : null;
|
|
$x['kg'] = round($x['kg'], 1);
|
|
$x['value'] = round($x['value'], 2);
|
|
}
|
|
unset($x);
|
|
usort($g, fn($a, $b) => $b['value'] <=> $a['value']);
|
|
return $g;
|
|
}
|
|
|
|
/** Nome d'acquisto proposto da una descrizione: senza imballo, spessore, revisione, "MESCOLA" */
|
|
public static function suggestName(string $desc): string
|
|
{
|
|
$s = strtoupper(preg_replace('/\s+/', ' ', trim($desc)));
|
|
$s = preg_replace('/^MESCOL[AE]\s*/', '', $s);
|
|
$s = preg_replace('/^ART\.?\s*/', '', $s);
|
|
$s = preg_replace('/\s*\bREV\.?\s*\d+\b/', '', $s);
|
|
$s = preg_replace('/\s*\b\w*(STRISC|TRISCIA|STRSC)\w*\b.*$/', '', $s);
|
|
$s = preg_replace('/\s*\b(X\s*)?\d+\s*MM\b.*$/', '', $s);
|
|
$s = preg_replace('/\s*\b(PANI|FESTONI|IMPILATA|IN CASSONI|CONFEZIONE)\b.*$/', '', $s);
|
|
return trim($s, " -.,");
|
|
}
|
|
|
|
/** Righe e kg acquistati per ogni nome d'acquisto (per la modale della mescola) */
|
|
public static function nameStats(PDO $pdo, int $idmescola, ?array $lines = null): array
|
|
{
|
|
$lines = $lines ?? self::lines($pdo);
|
|
$out = [];
|
|
foreach ($lines as $l) {
|
|
if ($l['match'] !== 'ok' || (int)$l['idmescola'] !== $idmescola) continue;
|
|
$m = self::matchDescription($pdo, (string)$l['description'], $l['supplier_code']);
|
|
$nid = (int)$m['name']['id'];
|
|
$o = &$out[$nid];
|
|
$o['n'] = ($o['n'] ?? 0) + 1;
|
|
$o['kg'] = ($o['kg'] ?? 0) + $l['qty'];
|
|
$o['value'] = ($o['value'] ?? 0) + $l['value'];
|
|
if (($o['last_date'] ?? '') <= $l['doc_date']) {
|
|
$o['last_date'] = $l['doc_date'];
|
|
$o['last_price'] = $l['qty'] > 0 ? $l['value'] / $l['qty'] : null;
|
|
$o['last_desc'] = $l['description'];
|
|
$o['last_supplier'] = $l['supplier_name'];
|
|
}
|
|
unset($o);
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/* ============================================================
|
|
* SINCRONIZZAZIONE DA MAGO (sola lettura)
|
|
* ============================================================ */
|
|
|
|
/**
|
|
* Copia le righe MESCOLE delle fatture fornitore con data documento >= $from.
|
|
* Le righe del periodo che non esistono più in Mago vengono tolte.
|
|
* @return array letti, scritti, tolti, fornitori collegati
|
|
*/
|
|
public static function sync(PDO $pdo, MagoClient $mago, string $from, ?callable $log = null): array
|
|
{
|
|
$log = $log ?: function ($s) {};
|
|
$fields = [
|
|
'PurchaseDocId',
|
|
'Line',
|
|
'DocumentType',
|
|
'DocumentDate',
|
|
'DocNo',
|
|
'Supplier',
|
|
'Item',
|
|
'Description',
|
|
'UoM',
|
|
'Qty',
|
|
'UnitValue',
|
|
'TaxableAmount',
|
|
'Notes',
|
|
'PurchaseOrdNo'
|
|
];
|
|
$flt = [
|
|
['key' => 'Item', 'value' => self::ITEM, 'operator' => '='],
|
|
['key' => 'DocumentDate', 'value' => $from, 'operator' => '>='],
|
|
['key' => 'DocumentType', 'value' => (string)self::DOC_INVOICE, 'operator' => '='],
|
|
];
|
|
$rows = [];
|
|
foreach ($mago->iterate('MA_PurchaseDocDetail', ['fields' => $fields, 'filters' => $flt]) as $rec) $rows[] = $rec;
|
|
$log(count($rows) . " righe lette da Mago dal $from");
|
|
|
|
// nomi fornitori Mago
|
|
$names = [];
|
|
foreach (array_unique(array_map(fn($r) => (string)($r['Supplier'] ?? ''), $rows)) as $code) {
|
|
if ($code === '') continue;
|
|
try {
|
|
$r = $mago->getDataTable('MA_CustSupp', [
|
|
'fields' => ['CustSupp', 'CompanyName', 'CustSuppType'],
|
|
'filters' => [['key' => 'CustSupp', 'value' => $code, 'operator' => '=']],
|
|
'recordsPerPage' => 5
|
|
]);
|
|
foreach ($r['records'] as $c) {
|
|
$names[$code] = trim((string)$c['CompanyName']) ?: $code;
|
|
if ((string)$c['CustSuppType'] !== '3211264') break; // preferisco il record fornitore
|
|
}
|
|
} catch (Throwable $e) {
|
|
$names[$code] = $code;
|
|
}
|
|
}
|
|
|
|
$now = date('Y-m-d H:i:s');
|
|
$ins = $pdo->prepare("INSERT INTO mago_mix_purchases
|
|
(purchase_doc_id, line, document_type, doc_date, doc_no, supplier_code, supplier_name, item, description, uom, qty, unit_value, taxable_amount, notes, purchase_ord_no, synced_at)
|
|
VALUES (:id, :line, :type, :date, :no, :sc, :sn, :item, :desc, :uom, :qty, :uv, :ta, :notes, :pon, :now)
|
|
ON DUPLICATE KEY UPDATE document_type = VALUES(document_type), doc_date = VALUES(doc_date), doc_no = VALUES(doc_no),
|
|
supplier_code = VALUES(supplier_code), supplier_name = VALUES(supplier_name), item = VALUES(item), description = VALUES(description),
|
|
uom = VALUES(uom), qty = VALUES(qty), unit_value = VALUES(unit_value), taxable_amount = VALUES(taxable_amount),
|
|
notes = VALUES(notes), purchase_ord_no = VALUES(purchase_ord_no), synced_at = VALUES(synced_at)");
|
|
$seen = [];
|
|
$pdo->beginTransaction();
|
|
try {
|
|
foreach ($rows as $r) {
|
|
$id = (int)$r['PurchaseDocId'];
|
|
$line = (int)$r['Line'];
|
|
$seen["$id-$line"] = true;
|
|
$code = (string)($r['Supplier'] ?? '');
|
|
$ins->execute([
|
|
'id' => $id,
|
|
'line' => $line,
|
|
'type' => (int)$r['DocumentType'],
|
|
'date' => MagoClient::toMysqlDate($r['DocumentDate'] ?? null),
|
|
'no' => mb_substr((string)($r['DocNo'] ?? ''), 0, 40),
|
|
'sc' => $code,
|
|
'sn' => mb_substr($names[$code] ?? $code, 0, 150),
|
|
'item' => (string)($r['Item'] ?? ''),
|
|
'desc' => mb_substr(trim((string)($r['Description'] ?? '')), 0, 500),
|
|
'uom' => trim((string)($r['UoM'] ?? '')),
|
|
'qty' => MagoClient::toDecimal($r['Qty'] ?? 0),
|
|
'uv' => MagoClient::toDecimal($r['UnitValue'] ?? 0),
|
|
'ta' => MagoClient::toDecimal($r['TaxableAmount'] ?? 0),
|
|
'notes' => mb_substr(trim((string)($r['Notes'] ?? '')), 0, 500),
|
|
'pon' => mb_substr((string)($r['PurchaseOrdNo'] ?? ''), 0, 40),
|
|
'now' => $now,
|
|
]);
|
|
}
|
|
// righe del periodo non più presenti in Mago
|
|
$removed = 0;
|
|
$st = $pdo->prepare("SELECT purchase_doc_id, line FROM mago_mix_purchases WHERE doc_date >= ?");
|
|
$st->execute([$from]);
|
|
$del = $pdo->prepare("DELETE FROM mago_mix_purchases WHERE purchase_doc_id = ? AND line = ?");
|
|
foreach ($st->fetchAll(PDO::FETCH_NUM) as [$id, $line]) {
|
|
if (!isset($seen["$id-$line"])) {
|
|
$del->execute([$id, $line]);
|
|
$removed++;
|
|
}
|
|
}
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
$pdo->rollBack();
|
|
throw $e;
|
|
}
|
|
|
|
$linked = self::linkSuppliers($pdo, $names);
|
|
$log(count($rows) . " righe salvate, $removed tolte" . ($linked ? ", fornitori collegati: " . implode(', ', $linked) : ''));
|
|
return ['read' => count($rows), 'removed' => $removed, 'linked' => $linked];
|
|
}
|
|
|
|
/** Collega i fornitori del portale senza codice Mago, per nome (es. "DER-GOM" ↔ "DER-GOM S.R.L.") */
|
|
public static function linkSuppliers(PDO $pdo, array $magoNames): array
|
|
{
|
|
$norm = fn($s) => preg_replace('/[^A-Z0-9]/', '', strtoupper((string)$s));
|
|
$known = $pdo->query("SELECT mago_code FROM suppliers WHERE mago_code IS NOT NULL AND mago_code <> ''")->fetchAll(PDO::FETCH_COLUMN);
|
|
$free = $pdo->query("SELECT idsupplier, supplier_name FROM suppliers WHERE mago_code IS NULL OR mago_code = ''")->fetchAll(PDO::FETCH_ASSOC);
|
|
$upd = $pdo->prepare("UPDATE suppliers SET mago_code = ? WHERE idsupplier = ? AND (mago_code IS NULL OR mago_code = '')");
|
|
$done = [];
|
|
foreach ($magoNames as $code => $name) {
|
|
if (in_array($code, $known, true)) continue;
|
|
$nm = $norm($name);
|
|
foreach ($free as $i => $s) {
|
|
$ns = $norm($s['supplier_name']);
|
|
if (strlen($ns) >= 4 && strpos($nm, $ns) === 0) {
|
|
$upd->execute([$code, $s['idsupplier']]);
|
|
$done[] = $s['supplier_name'] . " = $code";
|
|
unset($free[$i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return $done;
|
|
}
|
|
}
|