Store the put-away location on return lines (new td_return_item columns, needs setup.php), split auto credit notes into net and VAT with the parent's department, drop the write to a td_quotation column that does not exist, and have the demo seed confirm its customer return.
793 lines
42 KiB
PHP
793 lines
42 KiB
PHP
<?php
|
||
/**
|
||
* Demo Transaction Data Seeder
|
||
* Run once after demo_seed.php: php demo_seed_transactions.php
|
||
*
|
||
* Extends the base demo data (demo_seed.php: company, warehouses, products,
|
||
* contacts, opening stock) with transactional data covering the rest of the
|
||
* app's features (restock / dispatch / transfer movements are seeded by demo_seed.php):
|
||
*
|
||
* 4. Chart of accounts, departments, GL posting formulas, product account mapping
|
||
* 5. Sales cycle: quotation -> sales order -> invoice -> GL post
|
||
* 6. AR: receipt billing -> receipt -> GL post
|
||
* 7. Purchasing cycle: purchase request -> PO -> receive -> purchase invoice -> GL post
|
||
* 8. AP: payment billing -> payment -> GL post
|
||
* 9. Supplier return (confirmed, restocks reversed)
|
||
* 10. Customer return (draft only — see note below)
|
||
* 11. Barcode labels for a handful of products
|
||
*
|
||
* Every write goes through the same Manager classes + engine-file patterns
|
||
* the app itself uses (dbTransaction wrapping, GlManager posting exactly as
|
||
* order/api/engine/issue_invoice.php and finance/api/engine/manage_receipt.php
|
||
* do it), so the data matches what the real UI would have produced.
|
||
*
|
||
* Customer returns: the return is saved with a put-away bin and then
|
||
* confirmed, the same two steps the Customer Return page performs. (It used to
|
||
* be left in draft: td_return_item had no zone/aisle/bin columns, so the
|
||
* location was lost on save and confirmReturn() could not restock. setup.php
|
||
* now adds them.)
|
||
*
|
||
* Safe to re-run: every insert is guarded by an existence check.
|
||
*/
|
||
|
||
if (PHP_SAPI !== 'cli') {
|
||
http_response_code(403);
|
||
exit('Run via CLI only: php demo_seed_transactions.php');
|
||
}
|
||
|
||
$_SESSION = [];
|
||
|
||
require_once __DIR__ . '/app/config.php';
|
||
require_once __DIR__ . '/app/dbconn.php';
|
||
require_once __DIR__ . '/app/assets/utils/db_helpers.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/WarehouseManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/StockManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/ProductManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/QuotationManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/OrderManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/InvoiceManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/ReturnManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/PurchaseRequestManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/PurchaseOrderManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/SupplierReturnManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/ReceiptBillingManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/ReceiptManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/PaymentBillingManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/PaymentManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes/BarcodeManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/ChartOfAccounts.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/DepartmentManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/AccountFormulaManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/PostingWindowGuard.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/GlManager.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/posting/BasePosting.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/posting/InvoicePosting.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/posting/PurchaseInvoicePosting.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/posting/ReceiptPosting.php';
|
||
require_once __DIR__ . '/app/assets/utils/classes_ac/posting/PaymentPosting.php';
|
||
|
||
function info(string $msg): void { echo "\033[36m •\033[0m {$msg}\n"; }
|
||
function ok(string $msg): void { echo "\033[32m ✓\033[0m {$msg}\n"; }
|
||
function skip(string $msg): void { echo "\033[33m –\033[0m {$msg}\n"; }
|
||
function warn(string $msg): void { echo "\033[31m !\033[0m {$msg}\n"; }
|
||
|
||
echo "\n\033[1m=== brnwms Demo Transaction Seeder ===\033[0m\n\n";
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 0. Resolve company / owner / warehouses / contacts seeded by demo_seed.php
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
$sth = $pdo1->prepare("SELECT company_id FROM company_list WHERE channel_name = 'demo' LIMIT 1");
|
||
$sth->execute();
|
||
$company_id = (int)($sth->fetchColumn() ?: 0);
|
||
if (!$company_id) exit("ERROR: demo company not found — run demo_seed.php first.\n");
|
||
|
||
$sth = $pdo1->prepare("SELECT user_id FROM user WHERE username = 'admin' LIMIT 1");
|
||
$sth->execute();
|
||
$owner_user_id = (int)($sth->fetchColumn() ?: 0);
|
||
if (!$owner_user_id) exit("ERROR: demo owner user not found — run demo_seed.php first.\n");
|
||
|
||
// Resolve by name: ids depend on what else exists in the database.
|
||
$sth = $pdo2->prepare("SELECT id, warehouse_name FROM md_warehouse WHERE company_id = :c ORDER BY id");
|
||
$sth->execute([':c' => $company_id]);
|
||
$warehouses = $sth->fetchAll(PDO::FETCH_KEY_PAIR); // id => name
|
||
$main_wh = (int)(array_search('Main Warehouse', $warehouses, true) ?: 0);
|
||
$bangna_wh = (int)(array_search('Bangna Distribution Center', $warehouses, true) ?: 0);
|
||
if (!$main_wh || !$bangna_wh) exit("ERROR: expected Main Warehouse and Bangna Distribution Center — run demo_seed.php first.\n");
|
||
|
||
// md_contact.contact_type is an md_contact_type id, so match on the type name.
|
||
$sth = $pdo2->prepare(
|
||
"SELECT c.id, c.contact_name, t.contact_type AS type_name
|
||
FROM md_contact c
|
||
JOIN md_contact_type t ON t.company_id = c.company_id AND t.id = c.contact_type
|
||
WHERE c.company_id = :c
|
||
ORDER BY c.id"
|
||
);
|
||
$sth->execute([':c' => $company_id]);
|
||
$contacts = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||
$customer_ids = array_values(array_column(array_filter($contacts, fn($c) => $c['type_name'] === 'Customer'), 'id'));
|
||
$supplier_ids = array_values(array_column(array_filter($contacts, fn($c) => $c['type_name'] === 'Supplier'), 'id'));
|
||
if (count($customer_ids) < 4 || count($supplier_ids) < 3) exit("ERROR: expected 4 customers and 3 suppliers — run demo_seed.php first.\n");
|
||
|
||
$sth = $pdo2->prepare("SELECT sku, product_name, uom, cost_price, price FROM md_product WHERE company_id = :c ORDER BY id");
|
||
$sth->execute([':c' => $company_id]);
|
||
$products = $sth->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_UNIQUE);
|
||
if (count($products) < 14) exit("ERROR: expected 14 products — run demo_seed.php first.\n");
|
||
|
||
$logging = ['user_id' => $owner_user_id, 'dt' => date('Y-m-d H:i:s'), 'login' => null, 'action' => 'seed_transactions'];
|
||
|
||
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 4. Chart of accounts + departments + GL posting formulas
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
echo "\n--- Chart of accounts ---\n";
|
||
|
||
$coa = new ChartOfAccounts($pdo2, $company_id);
|
||
|
||
$account_defs = [
|
||
['code' => '1000', 'name' => 'Cash and Bank', 'type' => 'asset'],
|
||
['code' => '1150', 'name' => 'Input VAT', 'type' => 'asset'],
|
||
['code' => '1200', 'name' => 'Accounts Receivable', 'type' => 'asset'],
|
||
['code' => '2000', 'name' => 'Accounts Payable', 'type' => 'liability'],
|
||
['code' => '2100', 'name' => 'Output VAT', 'type' => 'liability'],
|
||
['code' => '4000', 'name' => 'Sales Revenue', 'type' => 'revenue'],
|
||
['code' => '5000', 'name' => 'Cost of Goods Sold', 'type' => 'expense'],
|
||
];
|
||
|
||
$account_ids = [];
|
||
foreach ($account_defs as $a) {
|
||
$existing = $coa->getByCode($a['code']);
|
||
if ($existing) {
|
||
skip("Account {$a['code']} — {$a['name']} already exists");
|
||
$account_ids[$a['code']] = (int)$existing['id'];
|
||
continue;
|
||
}
|
||
$id = $coa->create([
|
||
'account_code' => $a['code'], 'account_name' => $a['name'], 'account_type' => $a['type'],
|
||
'account_category' => '', 'parent_code' => null, 'is_posting' => 1, 'status' => 1,
|
||
]);
|
||
$account_ids[$a['code']] = $id;
|
||
ok("Created account {$a['code']} — {$a['name']} ({$a['type']})");
|
||
}
|
||
|
||
echo "\n--- Departments ---\n";
|
||
|
||
$deptMgmt = new DepartmentManager($pdo2, $company_id);
|
||
$dept_defs = [
|
||
['code' => 'SALES', 'name' => 'Sales'],
|
||
['code' => 'WH', 'name' => 'Warehouse Operations'],
|
||
];
|
||
$dept_ids = [];
|
||
foreach ($dept_defs as $d) {
|
||
$sth = $pdo2->prepare("SELECT id FROM md_department WHERE company_id = :c AND dept_code = :code LIMIT 1");
|
||
$sth->execute([':c' => $company_id, ':code' => $d['code']]);
|
||
$id = (int)($sth->fetchColumn() ?: 0);
|
||
if ($id) {
|
||
skip("Department {$d['code']} already exists (id={$id})");
|
||
} else {
|
||
$id = $deptMgmt->create(['dept_code' => $d['code'], 'dept_name' => $d['name'], 'description' => '', 'status' => 1]);
|
||
ok("Created department {$d['code']} — {$d['name']} (id={$id})");
|
||
}
|
||
$dept_ids[$d['code']] = $id;
|
||
}
|
||
|
||
echo "\n--- GL posting formulas ---\n";
|
||
|
||
$formulaMgmt = new AccountFormulaManager($pdo2, $company_id);
|
||
|
||
// document_type => [formula_name, items[ [drcr, account_code, amount_key, description] ]]
|
||
$formula_defs = [
|
||
'invoice' => [
|
||
'name' => 'Standard Sales Invoice',
|
||
'items' => [
|
||
['D', '1200', 'grand_total', 'Accounts Receivable'],
|
||
['C', '4000', 'total', 'Sales Revenue'],
|
||
['C', '2100', 'tax', 'Output VAT'],
|
||
],
|
||
],
|
||
'purchase_invoice' => [
|
||
'name' => 'Standard Purchase Invoice',
|
||
'items' => [
|
||
['D', '5000', 'total', 'Cost of Goods Sold'],
|
||
['D', '1150', 'tax', 'Input VAT'],
|
||
['C', '2000', 'grand_total', 'Accounts Payable'],
|
||
],
|
||
],
|
||
'receipt' => [
|
||
'name' => 'Standard Receipt',
|
||
'items' => [
|
||
['D', '1000', 'amount', 'Cash and Bank'],
|
||
['C', '1200', 'amount', 'Accounts Receivable'],
|
||
],
|
||
],
|
||
'payment' => [
|
||
'name' => 'Standard Payment',
|
||
'items' => [
|
||
['D', '2000', 'amount', 'Accounts Payable'],
|
||
['C', '1000', 'amount', 'Cash and Bank'],
|
||
],
|
||
],
|
||
];
|
||
|
||
$formula_ids = [];
|
||
foreach ($formula_defs as $doc_type => $def) {
|
||
$sth = $pdo2->prepare(
|
||
"SELECT id FROM md_account_formula WHERE company_id = :c AND document_type = :t AND is_default = 1 LIMIT 1"
|
||
);
|
||
$sth->execute([':c' => $company_id, ':t' => $doc_type]);
|
||
$id = (int)($sth->fetchColumn() ?: 0);
|
||
if ($id) {
|
||
skip("Default formula for '{$doc_type}' already exists (id={$id})");
|
||
} else {
|
||
$items = array_map(fn($i) => [
|
||
'drcr' => $i[0], 'account_code' => $i[1], 'amount_key' => $i[2], 'description' => $i[3],
|
||
], $def['items']);
|
||
$id = $formulaMgmt->save([
|
||
'id' => 0, 'formula_name' => $def['name'], 'document_type' => $doc_type,
|
||
'description' => '', 'is_default' => 1, 'status' => 1, 'items' => $items,
|
||
]);
|
||
ok("Created default GL formula for '{$doc_type}': {$def['name']} (id={$id})");
|
||
}
|
||
$formula_ids[$doc_type] = $id;
|
||
}
|
||
|
||
echo "\n--- Product account mapping ---\n";
|
||
|
||
// The sales and purchase formulas split 'total' per product, so Batch GL Entries
|
||
// refuses to post until every product has sales/purchase accounts
|
||
// (Account Formulas > Product Accounts, accounting/api/engine/product_account_mapping.php).
|
||
$sth = $pdo2->prepare("SELECT id, sku, sales_account_code, purchase_account_code FROM md_product WHERE company_id = :c ORDER BY id");
|
||
$sth->execute([':c' => $company_id]);
|
||
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $prod) {
|
||
if ($prod['sales_account_code'] && $prod['purchase_account_code']) {
|
||
skip("Product {$prod['sku']} already mapped");
|
||
continue;
|
||
}
|
||
$sales_code = $prod['sales_account_code'] ?: '4000';
|
||
$purchase_code = $prod['purchase_account_code'] ?: '5000';
|
||
dbTransaction($pdo2, fn($pdo) => (new ProductManager($pdo, $company_id))
|
||
->updateAccountMapping((int)$prod['id'], $sales_code, $purchase_code, $logging));
|
||
ok("Mapped {$prod['sku']}: sales {$sales_code}, purchase {$purchase_code}");
|
||
}
|
||
|
||
/** Post (or replace) GL for a document, mirroring order/api/engine/issue_invoice.php exactly. */
|
||
function postGl(PDO $pdo1, PDO $pdo2, int $company_id, string $doc_type, int $doc_id, string $posting_class): array {
|
||
require_once __DIR__ . "/app/assets/utils/classes_ac/posting/{$posting_class}.php";
|
||
$posting = new $posting_class($pdo2, $company_id);
|
||
$built = $posting->build($doc_id, null);
|
||
$guard = new PostingWindowGuard($pdo1, $company_id);
|
||
$gl = new GlManager($pdo2, $company_id, $guard);
|
||
$meta = ['journal_date' => $built['doc_date'] ?? null];
|
||
$existing = $gl->getBySource($doc_type, $doc_id);
|
||
|
||
$pdo2->beginTransaction();
|
||
if ($existing) {
|
||
$gl->replace($doc_type, $doc_id, $built['formula_id'], $built['period'], $built['lines'], $meta);
|
||
$pdo2->commit();
|
||
return ['action' => 'replaced', 'lines' => count($built['lines'])];
|
||
}
|
||
$gl->post($doc_type, $doc_id, $built['formula_id'], $built['period'], $built['lines'], $meta);
|
||
$pdo2->commit();
|
||
return ['action' => 'posted', 'lines' => count($built['lines'])];
|
||
}
|
||
|
||
/** Build a quotation/order/PR/PO line item with 7% VAT from a product SKU + qty. */
|
||
function buildLineItem(array $products, string $sku, float $qty): array {
|
||
$p = $products[$sku];
|
||
$unit_price = (float)$p['price'];
|
||
$total_price = round($qty * $unit_price, 4);
|
||
$tax_rate = 7.0;
|
||
$tax_amount = round($total_price * $tax_rate / 100, 4);
|
||
return [
|
||
'product_sku' => $sku,
|
||
'product_name' => $p['product_name'], // documents show the product name, as the UI's product search fills it
|
||
'quantity' => $qty,
|
||
'unit_price' => $unit_price,
|
||
'total_price' => $total_price,
|
||
'tax_amount' => $tax_amount,
|
||
'tax_rate' => $tax_rate,
|
||
];
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 5. Sales cycle: quotation -> sales order -> invoice -> GL post
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
echo "\n--- Sales cycle (quotation -> order -> invoice) ---\n";
|
||
|
||
$quotMgmt = new QuotationManager($pdo2, $company_id);
|
||
$orderMgmt = new OrderManager($pdo2, $company_id);
|
||
$invMgmt = new InvoiceManager($pdo2, $company_id);
|
||
|
||
// The link between a quotation and its order is source='quotation' /
|
||
// source_id=$qid on td_order, which QuotationManager::getById() joins on —
|
||
// that's what this script relies on, both to link and to look the link back up.
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_quotation WHERE company_id = :c AND notes = 'Demo seed sales cycle' LIMIT 1");
|
||
$sth->execute([':c' => $company_id]);
|
||
$sales_quotation_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($sales_quotation_id) {
|
||
skip("Demo sales-cycle quotation already exists (id={$sales_quotation_id})");
|
||
} else {
|
||
$sales_items = [
|
||
buildLineItem($products, 'EL-002', 15),
|
||
buildLineItem($products, 'OF-001', 25),
|
||
];
|
||
|
||
$sales_quotation_id = dbTransaction($pdo2, function ($pdo2) use ($quotMgmt, $customer_ids, $dept_ids, $sales_items, $logging) {
|
||
$qid = $quotMgmt->save([
|
||
'id' => 0, 'contact_id' => $customer_ids[0], 'department_id' => $dept_ids['SALES'],
|
||
'quotation_date' => date('Y-m-d'), 'valid_until' => date('Y-m-d', strtotime('+30 days')),
|
||
'items' => $sales_items, 'discount' => 0, 'notes' => 'Demo seed sales cycle',
|
||
], $logging);
|
||
$quotMgmt->updateStatus($qid, 'send', $logging);
|
||
$quotMgmt->updateStatus($qid, 'accept', $logging);
|
||
return $qid;
|
||
});
|
||
ok("Created + accepted quotation (id={$sales_quotation_id})");
|
||
}
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_order WHERE company_id = :c AND source = 'quotation' AND source_id = :qid LIMIT 1");
|
||
$sth->execute([':c' => $company_id, ':qid' => $sales_quotation_id]);
|
||
$sales_order_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($sales_order_id) {
|
||
skip("Order converted from demo quotation already exists (id={$sales_order_id})");
|
||
} else {
|
||
// Re-derive line items from the quotation itself (works whether the
|
||
// quotation was just created above or already existed from a prior run).
|
||
$sth = $pdo2->prepare(
|
||
"SELECT item_id, product_sku, product_name, quantity, unit_price, total_price, tax_amount, tax_rate
|
||
FROM td_quotation_item WHERE quotation_id = :qid AND company_id = :c ORDER BY item_id"
|
||
);
|
||
$sth->execute([':qid' => $sales_quotation_id, ':c' => $company_id]);
|
||
$order_items = array_map(fn($it) => $it + ['warehouse_id' => $main_wh], $sth->fetchAll(PDO::FETCH_ASSOC));
|
||
|
||
$sales_order_id = dbTransaction($pdo2, function ($pdo2) use ($orderMgmt, $quotMgmt, $customer_ids, $dept_ids, $order_items, $sales_quotation_id, $logging) {
|
||
// Insert with source='quotation' -> starts as status=-2 (pending warehouse assignment)
|
||
$oid = $orderMgmt->saveOrder([
|
||
'id' => 0, 'source' => 'quotation', 'source_id' => $sales_quotation_id,
|
||
'contact_id' => $customer_ids[0], 'department_id' => $dept_ids['SALES'],
|
||
'order_date' => date('Y-m-d'), 'items' => $order_items, 'discount' => 0,
|
||
'shipping_fee' => 0, 'notes' => 'Demo seed sales cycle',
|
||
], $logging);
|
||
// Re-save with warehouse_id already set on every item -> auto-promotes -2 to draft (0)
|
||
$orderMgmt->saveOrder([
|
||
'id' => $oid, 'contact_id' => $customer_ids[0], 'department_id' => $dept_ids['SALES'],
|
||
'order_date' => date('Y-m-d'), 'items' => $order_items, 'discount' => 0,
|
||
'shipping_fee' => 0, 'notes' => 'Demo seed sales cycle',
|
||
], $logging);
|
||
|
||
$quotMgmt->incrementConvertedQty($sales_quotation_id, array_map(
|
||
fn($it) => ['item_id' => $it['item_id'], 'quantity' => $it['quantity']], $order_items
|
||
));
|
||
|
||
$orderMgmt->confirmOrder($oid, bin2hex(random_bytes(16)), $logging, true); // auto-approve stock-out
|
||
return $oid;
|
||
});
|
||
ok("Created + confirmed sales order from quotation (id={$sales_order_id}), stock-out auto-approved");
|
||
}
|
||
|
||
$sth = $pdo2->prepare("SELECT id, status FROM td_invoice WHERE company_id = :c AND order_id = :oid AND doc_type = 'invoice' LIMIT 1");
|
||
$sth->execute([':c' => $company_id, ':oid' => $sales_order_id]);
|
||
$inv_row = $sth->fetch(PDO::FETCH_ASSOC);
|
||
|
||
if ($inv_row) {
|
||
skip("Invoice for sales order already exists (id={$inv_row['id']})");
|
||
$sales_invoice_id = (int)$inv_row['id'];
|
||
} else {
|
||
$sales_invoice_id = dbTransaction($pdo2, function ($pdo2) use ($invMgmt, $sales_order_id, $logging) {
|
||
$iid = $invMgmt->createFromOrder($sales_order_id, $logging);
|
||
$invMgmt->issueInvoice($iid, $logging, date('Y-m-d', strtotime('+30 days')));
|
||
return $iid;
|
||
});
|
||
ok("Created + issued invoice from sales order (id={$sales_invoice_id})");
|
||
}
|
||
|
||
$existing_gl = (new GlManager($pdo2, $company_id))->getBySource('invoice', $sales_invoice_id);
|
||
if ($existing_gl) {
|
||
skip("GL already posted for invoice #{$sales_invoice_id}");
|
||
} else {
|
||
$res = postGl($pdo1, $pdo2, $company_id, 'invoice', $sales_invoice_id, 'InvoicePosting');
|
||
ok("GL {$res['action']} for invoice #{$sales_invoice_id} ({$res['lines']} lines)");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 6. AR: receipt billing -> receipt -> GL post
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
echo "\n--- AR (receipt billing -> receipt) ---\n";
|
||
|
||
$receiptBillingMgmt = new ReceiptBillingManager($pdo2, $company_id);
|
||
$receiptMgmt = new ReceiptManager($pdo2, $company_id);
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_receipt_billing WHERE company_id = :c AND notes = 'Demo seed AR' LIMIT 1");
|
||
$sth->execute([':c' => $company_id]);
|
||
$receipt_billing_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($receipt_billing_id) {
|
||
skip("Demo receipt billing already exists (id={$receipt_billing_id})");
|
||
} else {
|
||
$sth = $pdo2->prepare("SELECT grand_total FROM td_invoice WHERE id = :id AND company_id = :c LIMIT 1");
|
||
$sth->execute([':id' => $sales_invoice_id, ':c' => $company_id]);
|
||
$invoice_total = (float)$sth->fetchColumn();
|
||
|
||
$receipt_billing_id = dbTransaction($pdo2, function ($pdo2) use ($receiptBillingMgmt, $customer_ids, $sales_invoice_id, $invoice_total, $logging) {
|
||
return $receiptBillingMgmt->createBilling([
|
||
'contact_id' => $customer_ids[0], 'billing_date' => date('Y-m-d'), 'notes' => 'Demo seed AR',
|
||
'allocations' => [['invoice_id' => $sales_invoice_id, 'amount' => $invoice_total]],
|
||
], $logging);
|
||
});
|
||
ok("Created receipt billing for invoice #{$sales_invoice_id} (id={$receipt_billing_id}), amount " . number_format($invoice_total, 2));
|
||
}
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_receipt WHERE company_id = :c AND receipt_billing_id = :bid LIMIT 1");
|
||
$sth->execute([':c' => $company_id, ':bid' => $receipt_billing_id]);
|
||
$receipt_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($receipt_id) {
|
||
skip("Receipt for billing #{$receipt_billing_id} already exists (id={$receipt_id})");
|
||
} else {
|
||
$sth = $pdo2->prepare("SELECT contact_id, amount FROM td_receipt_billing WHERE id = :id LIMIT 1");
|
||
$sth->execute([':id' => $receipt_billing_id]);
|
||
$billing = $sth->fetch(PDO::FETCH_ASSOC);
|
||
|
||
$receipt_id = dbTransaction($pdo2, function ($pdo2) use ($receiptMgmt, $billing, $receipt_billing_id, $logging) {
|
||
return $receiptMgmt->createReceipt([
|
||
'receipt_billing_id' => $receipt_billing_id, 'receipt_date' => date('Y-m-d'),
|
||
'payment_method' => 'bank_transfer', 'amount' => $billing['amount'], 'notes' => 'Demo seed AR',
|
||
], $logging);
|
||
});
|
||
ok("Created receipt #{$receipt_id} for billing #{$receipt_billing_id}, amount " . number_format($billing['amount'], 2));
|
||
}
|
||
|
||
$existing_gl = (new GlManager($pdo2, $company_id))->getBySource('receipt', $receipt_id);
|
||
if ($existing_gl) {
|
||
skip("GL already posted for receipt #{$receipt_id}");
|
||
} else {
|
||
$res = postGl($pdo1, $pdo2, $company_id, 'receipt', $receipt_id, 'ReceiptPosting');
|
||
ok("GL {$res['action']} for receipt #{$receipt_id} ({$res['lines']} lines)");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 7. Purchasing cycle: purchase request -> PO -> receive -> purchase invoice -> GL post
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
echo "\n--- Purchasing cycle (request -> PO -> receipt -> invoice) ---\n";
|
||
|
||
$prMgmt = new PurchaseRequestManager($pdo2, $company_id);
|
||
$poMgmt = new PurchaseOrderManager($pdo2, $company_id);
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_purchase_request WHERE company_id = :c AND notes = 'Demo seed purchasing cycle' LIMIT 1");
|
||
$sth->execute([':c' => $company_id]);
|
||
$pr_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($pr_id) {
|
||
skip("Demo purchase request already exists (id={$pr_id})");
|
||
} else {
|
||
$pr_items = [
|
||
buildLineItem($products, 'EL-003', 20),
|
||
buildLineItem($products, 'PK-001', 100),
|
||
];
|
||
$pr_id = dbTransaction($pdo2, function ($pdo2) use ($prMgmt, $supplier_ids, $dept_ids, $pr_items, $logging) {
|
||
$rid = $prMgmt->save([
|
||
'id' => 0, 'contact_id' => $supplier_ids[0], 'department_id' => $dept_ids['WH'],
|
||
'request_date' => date('Y-m-d'), 'required_date' => date('Y-m-d', strtotime('+14 days')),
|
||
'items' => $pr_items, 'discount' => 0, 'shipping_fee' => 0, 'notes' => 'Demo seed purchasing cycle',
|
||
], $logging);
|
||
$prMgmt->updateStatus($rid, 'submit', $logging);
|
||
$prMgmt->updateStatus($rid, 'approve', $logging);
|
||
return $rid;
|
||
});
|
||
ok("Created + approved purchase request (id={$pr_id})");
|
||
}
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_purchase_order WHERE company_id = :c AND source = 'purchase_request' AND source_id = :rid LIMIT 1");
|
||
$sth->execute([':c' => $company_id, ':rid' => $pr_id]);
|
||
$po_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($po_id) {
|
||
skip("PO converted from demo purchase request already exists (id={$po_id})");
|
||
} else {
|
||
$sth = $pdo2->prepare(
|
||
"SELECT item_id, product_sku, product_name, quantity, unit_price, total_price, tax_amount, tax_rate
|
||
FROM td_purchase_request_item WHERE request_id = :rid AND company_id = :c ORDER BY item_id"
|
||
);
|
||
$sth->execute([':rid' => $pr_id, ':c' => $company_id]);
|
||
$po_items = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||
|
||
$po_id = dbTransaction($pdo2, function ($pdo2) use ($poMgmt, $prMgmt, $supplier_ids, $dept_ids, $po_items, $pr_id, $main_wh, $logging) {
|
||
$pid = $poMgmt->savePo([
|
||
'id' => 0, 'source' => 'purchase_request', 'source_id' => $pr_id,
|
||
'contact_id' => $supplier_ids[0], 'department_id' => $dept_ids['WH'],
|
||
'po_date' => date('Y-m-d'), 'expected_date' => date('Y-m-d', strtotime('+7 days')),
|
||
'warehouse_id' => $main_wh, 'items' => $po_items, 'discount' => 0,
|
||
'shipping_fee' => 0, 'notes' => 'Demo seed purchasing cycle',
|
||
], $logging);
|
||
// Insert with source set -> status=-2 (pending). Re-save with warehouse_id
|
||
// present -> auto-promotes to draft (0), same two-step dance as saveOrder().
|
||
$poMgmt->savePo([
|
||
'id' => $pid, 'contact_id' => $supplier_ids[0], 'department_id' => $dept_ids['WH'],
|
||
'po_date' => date('Y-m-d'), 'expected_date' => date('Y-m-d', strtotime('+7 days')),
|
||
'warehouse_id' => $main_wh, 'items' => $po_items, 'discount' => 0,
|
||
'shipping_fee' => 0, 'notes' => 'Demo seed purchasing cycle',
|
||
], $logging);
|
||
$poMgmt->confirmPo($pid, $logging);
|
||
|
||
$prMgmt->incrementConvertedQty($pr_id, array_map(
|
||
fn($it) => ['item_id' => $it['item_id'], 'quantity' => $it['quantity']], $po_items
|
||
));
|
||
return $pid;
|
||
});
|
||
ok("Created + confirmed PO from purchase request (id={$po_id})");
|
||
}
|
||
|
||
$sth = $pdo2->prepare(
|
||
"SELECT COUNT(*) FROM td_purchase_order_item WHERE order_id = :pid AND company_id = :c AND received_qty > 0"
|
||
);
|
||
$sth->execute([':pid' => $po_id, ':c' => $company_id]);
|
||
if ((int)$sth->fetchColumn() > 0) {
|
||
skip("PO #{$po_id} already received");
|
||
} else {
|
||
$sth = $pdo2->prepare(
|
||
"SELECT item_id, product_sku, unit_price, quantity FROM td_purchase_order_item
|
||
WHERE order_id = :pid AND company_id = :c ORDER BY item_id"
|
||
);
|
||
$sth->execute([':pid' => $po_id, ':c' => $company_id]);
|
||
$po_line_items = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||
|
||
// Grab as many free bins as line items in one query — none of these are
|
||
// occupied until receivePo() runs, so a single up-front list (rather than
|
||
// repeated nextFreeBin() calls, which would all see the same still-empty
|
||
// bin and collide) is what keeps each line's location distinct.
|
||
$sth = $pdo2->prepare(
|
||
"SELECT bin FROM md_bin WHERE company_id = :c AND warehouse = :w AND product_sku IS NULL
|
||
ORDER BY CAST(SUBSTRING(bin, 3) AS UNSIGNED) ASC"
|
||
);
|
||
$sth->execute([':c' => $company_id, ':w' => $main_wh]);
|
||
$free_bins = array_slice($sth->fetchAll(PDO::FETCH_COLUMN), 0, count($po_line_items));
|
||
if (count($free_bins) < count($po_line_items)) {
|
||
throw new Exception("Not enough free bins in {$warehouses[$main_wh]} to receive PO #{$po_id}.");
|
||
}
|
||
|
||
$receive_items = [];
|
||
foreach ($po_line_items as $i => $it) {
|
||
$bin = $free_bins[$i];
|
||
$receive_items[] = [
|
||
'item_id' => (int)$it['item_id'], 'product_sku' => $it['product_sku'],
|
||
'warehouse_id' => $main_wh, 'quantity' => (float)$it['quantity'],
|
||
'unit_price' => (float)$it['unit_price'],
|
||
'zone' => $bin, 'aisle' => $bin, 'bin' => $bin,
|
||
'contact_id' => $supplier_ids[0],
|
||
];
|
||
}
|
||
|
||
$po_uuid = bin2hex(random_bytes(16));
|
||
dbTransaction($pdo2, function ($pdo2) use ($poMgmt, $whMgmt, $po_id, $receive_items, $po_uuid, $logging) {
|
||
$poMgmt->receivePo($po_id, $receive_items, $po_uuid, $logging, true); // auto-approve stock-in
|
||
});
|
||
ok("Received PO #{$po_id} — " . count($receive_items) . " line(s) into {$warehouses[$main_wh]}, stock-in auto-approved");
|
||
}
|
||
|
||
$sth = $pdo2->prepare(
|
||
"SELECT id FROM td_invoice WHERE company_id = :c AND source = 'po' AND source_id = :pid AND doc_type = 'purchase_invoice' LIMIT 1"
|
||
);
|
||
$sth->execute([':c' => $company_id, ':pid' => $po_id]);
|
||
$purchase_invoice_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($purchase_invoice_id) {
|
||
skip("Purchase invoice for PO already exists (id={$purchase_invoice_id})");
|
||
} else {
|
||
$purchase_invoice_id = dbTransaction($pdo2, function ($pdo2) use ($invMgmt, $po_id, $logging) {
|
||
$iid = $invMgmt->createFromPo($po_id, $logging);
|
||
$invMgmt->issueInvoice($iid, $logging, date('Y-m-d', strtotime('+30 days')));
|
||
return $iid;
|
||
});
|
||
ok("Created + issued purchase invoice from PO (id={$purchase_invoice_id})");
|
||
}
|
||
|
||
$existing_gl = (new GlManager($pdo2, $company_id))->getBySource('purchase_invoice', $purchase_invoice_id);
|
||
if ($existing_gl) {
|
||
skip("GL already posted for purchase invoice #{$purchase_invoice_id}");
|
||
} else {
|
||
$res = postGl($pdo1, $pdo2, $company_id, 'purchase_invoice', $purchase_invoice_id, 'PurchaseInvoicePosting');
|
||
ok("GL {$res['action']} for purchase invoice #{$purchase_invoice_id} ({$res['lines']} lines)");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 8. AP: payment billing -> payment -> GL post
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
echo "\n--- AP (payment billing -> payment) ---\n";
|
||
|
||
$paymentBillingMgmt = new PaymentBillingManager($pdo2, $company_id);
|
||
$paymentMgmt = new PaymentManager($pdo2, $company_id);
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_payment_billing WHERE company_id = :c AND notes = 'Demo seed AP' LIMIT 1");
|
||
$sth->execute([':c' => $company_id]);
|
||
$payment_billing_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($payment_billing_id) {
|
||
skip("Demo payment billing already exists (id={$payment_billing_id})");
|
||
} else {
|
||
$sth = $pdo2->prepare("SELECT grand_total FROM td_invoice WHERE id = :id AND company_id = :c LIMIT 1");
|
||
$sth->execute([':id' => $purchase_invoice_id, ':c' => $company_id]);
|
||
$po_invoice_total = (float)$sth->fetchColumn();
|
||
|
||
$payment_billing_id = dbTransaction($pdo2, function ($pdo2) use ($paymentBillingMgmt, $supplier_ids, $purchase_invoice_id, $po_invoice_total, $logging) {
|
||
return $paymentBillingMgmt->createBilling([
|
||
'contact_id' => $supplier_ids[0], 'billing_date' => date('Y-m-d'), 'notes' => 'Demo seed AP',
|
||
'allocations' => [['invoice_id' => $purchase_invoice_id, 'amount' => $po_invoice_total]],
|
||
], $logging);
|
||
});
|
||
ok("Created payment billing for purchase invoice #{$purchase_invoice_id} (id={$payment_billing_id}), amount " . number_format($po_invoice_total, 2));
|
||
}
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_payment WHERE company_id = :c AND payment_billing_id = :bid LIMIT 1");
|
||
$sth->execute([':c' => $company_id, ':bid' => $payment_billing_id]);
|
||
$payment_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($payment_id) {
|
||
skip("Payment for billing #{$payment_billing_id} already exists (id={$payment_id})");
|
||
} else {
|
||
$sth = $pdo2->prepare("SELECT contact_id, amount FROM td_payment_billing WHERE id = :id LIMIT 1");
|
||
$sth->execute([':id' => $payment_billing_id]);
|
||
$pbilling = $sth->fetch(PDO::FETCH_ASSOC);
|
||
|
||
$payment_id = dbTransaction($pdo2, function ($pdo2) use ($paymentMgmt, $pbilling, $payment_billing_id, $logging) {
|
||
return $paymentMgmt->createPayment([
|
||
'payment_billing_id' => $payment_billing_id, 'payment_date' => date('Y-m-d'),
|
||
'payment_method' => 'bank_transfer', 'amount' => $pbilling['amount'], 'notes' => 'Demo seed AP',
|
||
], $logging);
|
||
});
|
||
ok("Created payment #{$payment_id} for billing #{$payment_billing_id}, amount " . number_format($pbilling['amount'], 2));
|
||
}
|
||
|
||
$existing_gl = (new GlManager($pdo2, $company_id))->getBySource('payment', $payment_id);
|
||
if ($existing_gl) {
|
||
skip("GL already posted for payment #{$payment_id}");
|
||
} else {
|
||
$res = postGl($pdo1, $pdo2, $company_id, 'payment', $payment_id, 'PaymentPosting');
|
||
ok("GL {$res['action']} for payment #{$payment_id} ({$res['lines']} lines)");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 9. Supplier return — return the full PK-001 line from the demo PO
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
echo "\n--- Supplier return ---\n";
|
||
|
||
$supReturnMgmt = new SupplierReturnManager($pdo2, $company_id);
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_supplier_return WHERE company_id = :c AND po_id = :pid LIMIT 1");
|
||
$sth->execute([':c' => $company_id, ':pid' => $po_id]);
|
||
$supplier_return_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($supplier_return_id) {
|
||
skip("Demo supplier return already exists (id={$supplier_return_id})");
|
||
} else {
|
||
// td_purchase_order_item has no warehouse_id column — the PO is single-warehouse
|
||
// (td_purchase_order.warehouse_id), which is $main_wh here (set when the PO was created).
|
||
$sth = $pdo2->prepare(
|
||
"SELECT item_id, product_sku, product_name, quantity, unit_price, total_price, tax_amount, tax_rate,
|
||
stock_in_id
|
||
FROM td_purchase_order_item
|
||
WHERE order_id = :pid AND company_id = :c AND product_sku = 'PK-001' LIMIT 1"
|
||
);
|
||
$sth->execute([':pid' => $po_id, ':c' => $company_id]);
|
||
$po_line = $sth->fetch(PDO::FETCH_ASSOC);
|
||
|
||
$sth = $pdo2->prepare("SELECT zone, aisle, bin FROM `td_stock_{$main_wh}` WHERE id = :id AND company_id = :c LIMIT 1");
|
||
$sth->execute([':id' => $po_line['stock_in_id'], ':c' => $company_id]);
|
||
$bin_row = $sth->fetch(PDO::FETCH_ASSOC);
|
||
|
||
$return_item = [
|
||
'product_sku' => $po_line['product_sku'], 'product_name' => $po_line['product_name'],
|
||
'quantity' => $po_line['quantity'], 'unit_price' => $po_line['unit_price'],
|
||
'total_price' => $po_line['total_price'], 'tax_amount' => $po_line['tax_amount'],
|
||
'tax_rate' => $po_line['tax_rate'], 'warehouse_id' => $main_wh,
|
||
'stock_in_id' => $po_line['stock_in_id'],
|
||
'zone' => $bin_row['zone'], 'aisle' => $bin_row['aisle'], 'bin' => $bin_row['bin'],
|
||
];
|
||
|
||
$supplier_return_id = dbTransaction($pdo2, function ($pdo2) use ($supReturnMgmt, $whMgmt, $supplier_ids, $dept_ids, $return_item, $po_id, $logging) {
|
||
$rid = $supReturnMgmt->saveReturn([
|
||
'id' => 0, 'po_id' => $po_id, 'contact_id' => $supplier_ids[0], 'department_id' => $dept_ids['WH'],
|
||
'return_date' => date('Y-m-d'), 'reason' => 'Damaged on arrival (demo seed)',
|
||
'items' => [$return_item], 'tax_adjustment' => 0, 'notes' => 'Demo seed supplier return',
|
||
], $logging);
|
||
$supReturnMgmt->confirmReturn($rid, bin2hex(random_bytes(16)), $logging, $whMgmt, true); // auto-approve
|
||
return $rid;
|
||
});
|
||
ok("Created + confirmed supplier return for {$return_item['quantity']} x {$return_item['product_sku']} (id={$supplier_return_id})");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 10. Customer return — saved with a put-away bin, then confirmed (restocked).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
echo "\n--- Customer return ---\n";
|
||
|
||
$returnMgmt = new ReturnManager($pdo2, $company_id);
|
||
|
||
$sth = $pdo2->prepare("SELECT id FROM td_return WHERE company_id = :c AND notes = 'Demo seed customer return' LIMIT 1");
|
||
$sth->execute([':c' => $company_id]);
|
||
$customer_return_id = (int)($sth->fetchColumn() ?: 0);
|
||
|
||
if ($customer_return_id) {
|
||
skip("Demo customer return already exists (id={$customer_return_id})");
|
||
} else {
|
||
// Return 5 units of EL-002 against the demo sales order/invoice.
|
||
$sth = $pdo2->prepare(
|
||
"SELECT item_id, product_sku, product_name, unit_price, tax_rate, warehouse_id, stock_out_id
|
||
FROM td_order_item WHERE order_id = :oid AND company_id = :c AND product_sku = 'EL-002' LIMIT 1"
|
||
);
|
||
$sth->execute([':oid' => $sales_order_id, ':c' => $company_id]);
|
||
$order_line = $sth->fetch(PDO::FETCH_ASSOC);
|
||
|
||
$ret_qty = 5.0;
|
||
$ret_total = round($ret_qty * (float)$order_line['unit_price'], 4);
|
||
$ret_tax = round($ret_total * (float)$order_line['tax_rate'] / 100, 4);
|
||
|
||
// Returned goods go back into a free bin of the warehouse they shipped from.
|
||
$sth = $pdo2->prepare(
|
||
"SELECT bin FROM md_bin WHERE company_id = :c AND warehouse = :w AND product_sku IS NULL
|
||
ORDER BY CAST(SUBSTRING(bin, 3) AS UNSIGNED) ASC LIMIT 1"
|
||
);
|
||
$sth->execute([':c' => $company_id, ':w' => (int)$order_line['warehouse_id']]);
|
||
$return_bin = (string)($sth->fetchColumn() ?: '');
|
||
if ($return_bin === '') {
|
||
throw new Exception("No free bin in warehouse #{$order_line['warehouse_id']} for the demo customer return.");
|
||
}
|
||
|
||
$customer_return_id = dbTransaction($pdo2, function ($pdo2) use ($returnMgmt, $customer_ids, $order_line, $ret_qty, $ret_total, $ret_tax, $sales_order_id, $sales_invoice_id, $logging, $return_bin) {
|
||
return $returnMgmt->saveReturn([
|
||
'id' => 0, 'order_id' => $sales_order_id, 'invoice_id' => $sales_invoice_id,
|
||
'contact_id' => $customer_ids[0], 'return_date' => date('Y-m-d'),
|
||
'reason' => 'Customer changed mind (demo seed)', 'tax_adjustment' => 0,
|
||
'notes' => 'Demo seed customer return',
|
||
'items' => [[
|
||
'product_sku' => $order_line['product_sku'], 'product_name' => $order_line['product_name'],
|
||
'quantity' => $ret_qty, 'unit_price' => $order_line['unit_price'],
|
||
'total_price' => $ret_total, 'tax_amount' => $ret_tax, 'tax_rate' => $order_line['tax_rate'],
|
||
'warehouse_id' => $order_line['warehouse_id'], 'stock_out_id' => $order_line['stock_out_id'],
|
||
'stock_out_warehouse_id' => $order_line['warehouse_id'],
|
||
// simple location mode: zone and aisle mirror the bin
|
||
'zone' => $return_bin, 'aisle' => $return_bin, 'bin' => $return_bin,
|
||
]],
|
||
], $logging);
|
||
});
|
||
|
||
// Confirm exactly as order/api/engine/confirm_return.php does: restock,
|
||
// auto-approve the stock-in, and raise the credit note.
|
||
dbTransaction($pdo2, function ($pdo2) use ($company_id, $customer_return_id, $logging) {
|
||
$ret = new ReturnManager($pdo2, $company_id);
|
||
$ret->confirmReturn(
|
||
$customer_return_id, bin2hex(random_bytes(16)), $logging,
|
||
new WarehouseManager($pdo2, $company_id), new InvoiceManager($pdo2, $company_id),
|
||
true, true
|
||
);
|
||
});
|
||
ok("Created + confirmed customer return for {$ret_qty} x {$order_line['product_sku']} into bin {$return_bin} (id={$customer_return_id})");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 11. Barcode labels for a handful of products
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
echo "\n--- Barcode labels ---\n";
|
||
|
||
$barcodeMgmt = new BarcodeManager($pdo2, $company_id);
|
||
$label_skus = ['EL-001', 'EL-002', 'OF-001', 'BV-001', 'PK-001'];
|
||
|
||
foreach ($label_skus as $sku) {
|
||
$sth = $pdo2->prepare(
|
||
"SELECT COUNT(*) FROM md_barcode WHERE company_id = :c AND barcode_type = 'sku' AND product_sku = :sku"
|
||
);
|
||
$sth->execute([':c' => $company_id, ':sku' => $sku]);
|
||
if ((int)$sth->fetchColumn() > 0) {
|
||
skip("SKU label for {$sku} already exists");
|
||
continue;
|
||
}
|
||
$label = dbTransaction($pdo2, function ($pdo2) use ($barcodeMgmt, $sku) {
|
||
return $barcodeMgmt->createSkuLabel($sku, '', '', true);
|
||
});
|
||
ok("Created SKU label for {$sku}: {$label['barcode']}");
|
||
}
|
||
|
||
echo "\n\033[1m=== Done ===\033[0m\n\n";
|