Demo Data Population
This commit is contained in:
+663
@@ -0,0 +1,663 @@
|
||||
<?php
|
||||
/**
|
||||
* Demo Data Seeder
|
||||
* Run once after setup.php on a fresh install: php demo_seed.php
|
||||
*
|
||||
* What it does:
|
||||
* 1. Reads DB credentials from app/config.php (same as setup.php)
|
||||
* 2. Creates a demo company + owner user (bypasses email verification —
|
||||
* inserted directly with status='active' so you can log in immediately)
|
||||
* 3. Creates 2 warehouses with bin ranges
|
||||
* 4. Creates product categories + products
|
||||
* 5. Creates contact types + contacts (customers/suppliers)
|
||||
* 6. Receives opening stock for every product into Main Warehouse, approved
|
||||
* (so dashboard/report balances are non-zero from the first login)
|
||||
* 7. Receives a few later restock/replenishment batches (additional stock-in,
|
||||
* separate from the opening-stock batch) into fresh bins in Main Warehouse
|
||||
* and Bangna Distribution Center
|
||||
* 8. Ships a few products out to customers (stock-out)
|
||||
* 9. Transfers a few products from Main Warehouse to Bangna Distribution
|
||||
* Center (stock-transfer)
|
||||
*
|
||||
* Together, steps 6-9 give the Stock In / Stock Out / Stock Transfer list
|
||||
* pages several realistic entries each, instead of just one opening-balance batch.
|
||||
*
|
||||
* All writes go through the same Manager classes the app itself uses
|
||||
* (WarehouseManager, ProductManager, ContactManager, StockManager), so the
|
||||
* data is guaranteed to match what the UI/API would have produced.
|
||||
*
|
||||
* Safe to re-run: every insert is guarded by an existence check.
|
||||
*
|
||||
* No company_smtp row is created for the demo company, which means login
|
||||
* skips the OTP email step entirely (see login_otp.php) — you can log in
|
||||
* with just username + password.
|
||||
*/
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
exit('Run via CLI only: php demo_seed.php');
|
||||
}
|
||||
|
||||
$_SESSION = []; // Manager classes read $_SESSION['login_user_id'] for audit logs; CLI has no session.
|
||||
|
||||
require __DIR__ . '/app/config.php';
|
||||
require __DIR__ . '/app/dbconn.php';
|
||||
require __DIR__ . '/app/assets/utils/db_helpers.php';
|
||||
require __DIR__ . '/app/assets/utils/classes/WarehouseManager.php';
|
||||
require __DIR__ . '/app/assets/utils/classes/ProductManager.php';
|
||||
require __DIR__ . '/app/assets/utils/classes/ContactManager.php';
|
||||
require __DIR__ . '/app/assets/utils/classes/StockManager.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 findProductDef(array $product_defs, string $sku): array
|
||||
{
|
||||
foreach ($product_defs as $p) {
|
||||
if ($p['sku'] === $sku) return $p;
|
||||
}
|
||||
throw new Exception("demo_seed.php: unknown SKU '{$sku}' referenced by a later seeding step.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first unoccupied bin in a warehouse (simple-mode: zone = aisle = bin).
|
||||
* Queried fresh on every call so it stays correct as bins fill up / free up
|
||||
* across the run — safe to call repeatedly even against a warehouse that
|
||||
* already has unrelated stock in it.
|
||||
*/
|
||||
function findFreeBin(PDO $pdo2, int $company_id, int $warehouse_id): string
|
||||
{
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT bin FROM md_bin
|
||||
WHERE company_id = :c AND warehouse = :w AND product_sku IS NULL
|
||||
ORDER BY id ASC
|
||||
LIMIT 1"
|
||||
);
|
||||
$sth->execute([':c' => $company_id, ':w' => $warehouse_id]);
|
||||
$bin = $sth->fetchColumn();
|
||||
if ($bin === false) {
|
||||
throw new Exception("demo_seed.php: no free bin available in warehouse #{$warehouse_id}.");
|
||||
}
|
||||
return $bin;
|
||||
}
|
||||
|
||||
echo "\n\033[1m=== mn3wms Demo Data Seeder ===\033[0m\n\n";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 1. Owner user + company (pdo1 = wms auth/identity db)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
echo "--- Owner account & company ---\n";
|
||||
|
||||
$demo_username = 'admin';
|
||||
$demo_email = 'thanakorn.inbox@gmail.com';
|
||||
$demo_password = 'Demo@12345';
|
||||
$demo_channel = 'demo';
|
||||
|
||||
$sth = $pdo1->prepare("SELECT user_id FROM user WHERE username = :u OR email = :e LIMIT 1");
|
||||
$sth->execute([':u' => $demo_username, ':e' => $demo_email]);
|
||||
$owner_user_id = (int)($sth->fetchColumn() ?: 0);
|
||||
|
||||
if ($owner_user_id) {
|
||||
skip("User '{$demo_username}' already exists (user_id={$owner_user_id})");
|
||||
} else {
|
||||
$hashed = password_hash($demo_password, PASSWORD_BCRYPT);
|
||||
$sth = $pdo1->prepare("
|
||||
INSERT INTO user (username, name, surname, email, password, status, profile_picture, license, app_access)
|
||||
VALUES (:username, :name, :surname, :email, :password, 'active', '', 'owner', 'all')
|
||||
");
|
||||
$sth->execute([
|
||||
':username' => $demo_username,
|
||||
':name' => 'Thanakorn',
|
||||
':surname' => 'Admin',
|
||||
':email' => $demo_email,
|
||||
':password' => $hashed,
|
||||
]);
|
||||
$owner_user_id = (int)$pdo1->lastInsertId();
|
||||
ok("Created user '{$demo_username}' (user_id={$owner_user_id}), password: {$demo_password}");
|
||||
}
|
||||
|
||||
$sth = $pdo1->prepare("SELECT company_id FROM company_list WHERE channel_name = :c LIMIT 1");
|
||||
$sth->execute([':c' => $demo_channel]);
|
||||
$company_id = (int)($sth->fetchColumn() ?: 0);
|
||||
|
||||
if ($company_id) {
|
||||
skip("Company channel '{$demo_channel}' already exists (company_id={$company_id})");
|
||||
} else {
|
||||
$sth = $pdo1->prepare("
|
||||
INSERT INTO company_list (channel_name, company_name, company_name2, branch, branch_no, email, phone, fx)
|
||||
VALUES (:channel_name, :company_name, :company_name2, :branch, :branch_no, :email, :phone, 'thb')
|
||||
");
|
||||
$sth->execute([
|
||||
':channel_name' => $demo_channel,
|
||||
':company_name' => 'Demo Trading Co., Ltd.',
|
||||
':company_name2' => 'Demo Trading',
|
||||
':branch' => 'สำนักงานใหญ่',
|
||||
':branch_no' => '00000',
|
||||
':email' => $demo_email,
|
||||
':phone' => '021234567',
|
||||
]);
|
||||
$company_id = (int)$pdo1->lastInsertId();
|
||||
ok("Created company 'Demo Trading Co., Ltd.' (company_id={$company_id})");
|
||||
}
|
||||
|
||||
$sth = $pdo1->prepare("SELECT 1 FROM company_map_user WHERE company_id = :c AND user_id = :u LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':u' => $owner_user_id]);
|
||||
if ($sth->fetchColumn()) {
|
||||
skip("company_map_user link already exists");
|
||||
} else {
|
||||
$pdo1->prepare("
|
||||
INSERT INTO company_map_user (company_id, user_id, role, app_access, created_at)
|
||||
VALUES (:company_id, :user_id, 'owner', 'all', NOW())
|
||||
")->execute([':company_id' => $company_id, ':user_id' => $owner_user_id]);
|
||||
ok("Linked user to company as owner");
|
||||
}
|
||||
|
||||
$pdo1->prepare("UPDATE user SET default_company = :c WHERE user_id = :u AND (default_company IS NULL OR default_company = 0)")
|
||||
->execute([':c' => $company_id, ':u' => $owner_user_id]);
|
||||
|
||||
// No company_smtp row is inserted on purpose — login_otp.php skips the OTP
|
||||
// step entirely when a company has no SMTP configured.
|
||||
|
||||
$logging = ['user_id' => $owner_user_id, 'dt' => date('Y-m-d H:i:s'), 'login' => null, 'action' => 'seed'];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 2. Warehouses + storage/bins (pdo2 = wms2 business db)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
echo "\n--- Warehouses ---\n";
|
||||
|
||||
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||
|
||||
$warehouse_defs = [
|
||||
['name' => 'Main Warehouse', 'location' => '123 Sukhumvit Rd, Bangkok', 'bin_to' => 30],
|
||||
['name' => 'Bangna Distribution Center', 'location' => '456 Bangna-Trad Rd, Samut Prakan', 'bin_to' => 15],
|
||||
];
|
||||
|
||||
$warehouse_ids = []; // name => id
|
||||
|
||||
foreach ($warehouse_defs as $wh) {
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_warehouse WHERE company_id = :c AND warehouse_name = :n LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':n' => $wh['name']]);
|
||||
$id = (int)($sth->fetchColumn() ?: 0);
|
||||
|
||||
if ($id) {
|
||||
skip("Warehouse '{$wh['name']}' already exists (id={$id})");
|
||||
} else {
|
||||
dbTransaction($pdo2, function ($pdo2) use ($whMgmt, $wh, $company_id, $owner_user_id, $logging, &$id) {
|
||||
$id = $whMgmt->saveWarehouse([
|
||||
'id' => 0,
|
||||
'warehouse_name' => $wh['name'],
|
||||
'location' => $wh['location'],
|
||||
'manager' => $owner_user_id,
|
||||
'description' => 'Demo warehouse',
|
||||
'status' => 1,
|
||||
], $logging);
|
||||
});
|
||||
$whMgmt->createStockTableForWarehouse($id);
|
||||
ok("Created warehouse '{$wh['name']}' (id={$id}) + td_stock_{$id} table");
|
||||
}
|
||||
$warehouse_ids[$wh['name']] = $id;
|
||||
|
||||
// Simple-mode storage: one bin range "A-1".."A-N" (zone=aisle=bin=same value)
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_storage WHERE company_id = :c AND warehouse = :w LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':w' => $id]);
|
||||
if ($sth->fetchColumn()) {
|
||||
skip("Storage range for '{$wh['name']}' already exists");
|
||||
} else {
|
||||
dbTransaction($pdo2, function ($pdo2) use ($whMgmt, $id, $wh, $logging) {
|
||||
$whMgmt->saveStorage([
|
||||
'id' => 0,
|
||||
'warehouse' => (string)$id,
|
||||
'bin_from' => 'A-1',
|
||||
'bin_to' => 'A-' . $wh['bin_to'],
|
||||
'description' => 'Demo storage range',
|
||||
'status' => 1,
|
||||
], $logging, false);
|
||||
});
|
||||
ok("Created storage range A-1..A-{$wh['bin_to']} for '{$wh['name']}' ({$wh['bin_to']} bins)");
|
||||
}
|
||||
}
|
||||
|
||||
$main_wh_id = $warehouse_ids['Main Warehouse'];
|
||||
$bangna_wh_id = $warehouse_ids['Bangna Distribution Center'];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 3. Product categories + products
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
echo "\n--- Products ---\n";
|
||||
|
||||
$prodMgmt = new ProductManager($pdo2, $company_id);
|
||||
|
||||
$category_defs = [
|
||||
'electronics' => 'Electronics',
|
||||
'office-supplies' => 'Office Supplies',
|
||||
'beverages' => 'Beverages',
|
||||
'packaging-materials' => 'Packaging Materials',
|
||||
];
|
||||
|
||||
$category_ids = []; // slug => id
|
||||
|
||||
foreach ($category_defs as $slug => $name) {
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_product_category WHERE company_id = :c AND slug = :s LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':s' => $slug]);
|
||||
$id = (int)($sth->fetchColumn() ?: 0);
|
||||
|
||||
if ($id) {
|
||||
skip("Category '{$name}' already exists (id={$id})");
|
||||
} else {
|
||||
dbTransaction($pdo2, function ($pdo2) use ($prodMgmt, $name, $slug, $logging) {
|
||||
$prodMgmt->saveCategory([
|
||||
'id' => 0,
|
||||
'category' => $name,
|
||||
'slug' => $slug,
|
||||
'description' => '',
|
||||
'status' => 1,
|
||||
], $logging);
|
||||
});
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_product_category WHERE company_id = :c AND slug = :s LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':s' => $slug]);
|
||||
$id = (int)$sth->fetchColumn();
|
||||
ok("Created category '{$name}' (id={$id})");
|
||||
}
|
||||
$category_ids[$slug] = $id;
|
||||
}
|
||||
|
||||
$product_defs = [
|
||||
['sku' => 'EL-001', 'name' => 'Wireless Mouse', 'cat' => 'electronics', 'uom' => 'pcs', 'price' => 590, 'cost' => 350, 'min' => 10, 'reorder' => 20, 'qty' => 120],
|
||||
['sku' => 'EL-002', 'name' => 'USB-C Cable 1m', 'cat' => 'electronics', 'uom' => 'pcs', 'price' => 190, 'cost' => 90, 'min' => 20, 'reorder' => 40, 'qty' => 250],
|
||||
['sku' => 'EL-003', 'name' => 'Bluetooth Speaker', 'cat' => 'electronics', 'uom' => 'pcs', 'price' => 1290, 'cost' => 780, 'min' => 5, 'reorder' => 10, 'qty' => 45],
|
||||
['sku' => 'EL-004', 'name' => 'Power Bank 10000mAh', 'cat' => 'electronics', 'uom' => 'pcs', 'price' => 890, 'cost' => 520, 'min' => 8, 'reorder' => 15, 'qty' => 60],
|
||||
['sku' => 'OF-001', 'name' => 'A4 Paper Ream', 'cat' => 'office-supplies', 'uom' => 'ream', 'price' => 120, 'cost' => 75, 'min' => 30, 'reorder' => 60, 'qty' => 300],
|
||||
['sku' => 'OF-002', 'name' => 'Ballpoint Pen (Box of 12)', 'cat' => 'office-supplies', 'uom' => 'box', 'price' => 85, 'cost' => 45, 'min' => 15, 'reorder' => 30, 'qty' => 150],
|
||||
['sku' => 'OF-003', 'name' => 'Stapler', 'cat' => 'office-supplies', 'uom' => 'pcs', 'price' => 65, 'cost' => 35, 'min' => 10, 'reorder' => 20, 'qty' => 80],
|
||||
['sku' => 'OF-004', 'name' => 'Sticky Notes Pack', 'cat' => 'office-supplies', 'uom' => 'pack', 'price' => 45, 'cost' => 22, 'min' => 20, 'reorder' => 40, 'qty' => 200],
|
||||
['sku' => 'BV-001', 'name' => 'Bottled Water 600ml (Pack of 12)', 'cat' => 'beverages', 'uom' => 'pack', 'price' => 72, 'cost' => 48, 'min' => 25, 'reorder' => 50, 'qty' => 180],
|
||||
['sku' => 'BV-002', 'name' => 'Instant Coffee 3-in-1 (Box of 30)', 'cat' => 'beverages', 'uom' => 'box', 'price' => 145, 'cost' => 98, 'min' => 15, 'reorder' => 30, 'qty' => 90],
|
||||
['sku' => 'BV-003', 'name' => 'Green Tea Bags (Box of 25)', 'cat' => 'beverages', 'uom' => 'box', 'price' => 89, 'cost' => 55, 'min' => 12, 'reorder' => 24, 'qty' => 70],
|
||||
['sku' => 'PK-001', 'name' => 'Corrugated Box Medium', 'cat' => 'packaging-materials', 'uom' => 'pcs', 'price' => 18, 'cost' => 9, 'min' => 100, 'reorder' => 200, 'qty' => 500],
|
||||
['sku' => 'PK-002', 'name' => 'Bubble Wrap Roll', 'cat' => 'packaging-materials', 'uom' => 'roll', 'price' => 250, 'cost' => 160, 'min' => 10, 'reorder' => 20, 'qty' => 40],
|
||||
['sku' => 'PK-003', 'name' => 'Packing Tape', 'cat' => 'packaging-materials', 'uom' => 'pcs', 'price' => 35, 'cost' => 18, 'min' => 40, 'reorder' => 80, 'qty' => 220],
|
||||
];
|
||||
|
||||
foreach ($product_defs as $p) {
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_product WHERE company_id = :c AND sku = :s LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':s' => $p['sku']]);
|
||||
if ($sth->fetchColumn()) {
|
||||
skip("Product '{$p['sku']}' already exists");
|
||||
continue;
|
||||
}
|
||||
dbTransaction($pdo2, function ($pdo2) use ($prodMgmt, $p, $category_ids, $logging) {
|
||||
$prodMgmt->saveProduct([
|
||||
'id' => 0,
|
||||
'product_name' => $p['name'],
|
||||
'sku' => $p['sku'],
|
||||
'barcode' => '',
|
||||
'uom' => $p['uom'],
|
||||
'price' => $p['price'],
|
||||
'cost_price' => $p['cost'],
|
||||
'min_stock' => $p['min'],
|
||||
'reorder_point' => $p['reorder'],
|
||||
'category' => $category_ids[$p['cat']],
|
||||
'description' => '',
|
||||
'status' => 1,
|
||||
], $logging, '');
|
||||
});
|
||||
ok("Created product {$p['sku']} — {$p['name']}");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 4. Contact types + contacts
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
echo "\n--- Contacts ---\n";
|
||||
|
||||
$contactMgmt = new ContactManager($pdo2, $company_id);
|
||||
|
||||
$contact_type_defs = ['Customer', 'Supplier'];
|
||||
$contact_type_ids = [];
|
||||
|
||||
foreach ($contact_type_defs as $name) {
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_contact_type WHERE company_id = :c AND contact_type = :n LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':n' => $name]);
|
||||
$id = (int)($sth->fetchColumn() ?: 0);
|
||||
|
||||
if ($id) {
|
||||
skip("Contact type '{$name}' already exists (id={$id})");
|
||||
} else {
|
||||
dbTransaction($pdo2, function ($pdo2) use ($contactMgmt, $name, $logging) {
|
||||
$contactMgmt->saveContactType([
|
||||
'id' => 0,
|
||||
'contact_type' => $name,
|
||||
'description' => '',
|
||||
'status' => 1,
|
||||
], $logging);
|
||||
});
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_contact_type WHERE company_id = :c AND contact_type = :n LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':n' => $name]);
|
||||
$id = (int)$sth->fetchColumn();
|
||||
ok("Created contact type '{$name}' (id={$id})");
|
||||
}
|
||||
$contact_type_ids[$name] = $id;
|
||||
}
|
||||
|
||||
$contact_defs = [
|
||||
['name' => 'Siam Retail Co., Ltd.', 'type' => 'Customer', 'tax' => '0105561012341'],
|
||||
['name' => 'Bangkok Mart', 'type' => 'Customer', 'tax' => '0105561012342'],
|
||||
['name' => 'Northern Distribution Partners', 'type' => 'Customer', 'tax' => '0105561012343'],
|
||||
['name' => 'Chao Phraya Trading', 'type' => 'Customer', 'tax' => '0105561012344'],
|
||||
['name' => 'Global Parts Supply Co., Ltd.', 'type' => 'Supplier', 'tax' => '0105561098761'],
|
||||
['name' => 'Thai Packaging Industries', 'type' => 'Supplier', 'tax' => '0105561098762'],
|
||||
['name' => 'Asia Electronics Wholesale', 'type' => 'Supplier', 'tax' => '0105561098763'],
|
||||
];
|
||||
|
||||
$supplier_ids = [];
|
||||
$customer_ids = [];
|
||||
|
||||
foreach ($contact_defs as $c) {
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_contact WHERE company_id = :c AND contact_name = :n LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':n' => $c['name']]);
|
||||
$id = (int)($sth->fetchColumn() ?: 0);
|
||||
|
||||
if ($id) {
|
||||
skip("Contact '{$c['name']}' already exists (id={$id})");
|
||||
} else {
|
||||
dbTransaction($pdo2, function ($pdo2) use ($contactMgmt, $c, $contact_type_ids, $logging) {
|
||||
$contactMgmt->saveContact([
|
||||
'id' => 0,
|
||||
'contact_name' => $c['name'],
|
||||
'tax_id' => $c['tax'],
|
||||
'organization' => $c['name'],
|
||||
'branch' => 'สำนักงานใหญ่',
|
||||
'contact_type' => $contact_type_ids[$c['type']],
|
||||
'billing_address' => '99 Business Rd, Bangkok 10110, Thailand',
|
||||
'shipping_location' => 'Head Office',
|
||||
'shipping_address' => '99 Business Rd, Bangkok 10110, Thailand',
|
||||
'remark' => '',
|
||||
'status' => 1,
|
||||
], $logging, '');
|
||||
});
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_contact WHERE company_id = :c AND contact_name = :n LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':n' => $c['name']]);
|
||||
$id = (int)$sth->fetchColumn();
|
||||
ok("Created contact '{$c['name']}' ({$c['type']}, id={$id})");
|
||||
}
|
||||
if ($c['type'] === 'Supplier') {
|
||||
$supplier_ids[] = $id;
|
||||
} else {
|
||||
$customer_ids[] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 5. Opening stock — receive every product into Main Warehouse bins, approved
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
echo "\n--- Opening stock (Main Warehouse) ---\n";
|
||||
|
||||
$stockMgmt = new StockManager($pdo2, $company_id);
|
||||
|
||||
$sth = $pdo2->prepare("SELECT COUNT(*) FROM `td_stock_{$main_wh_id}` WHERE company_id = :c AND type = 'in'");
|
||||
$sth->execute([':c' => $company_id]);
|
||||
if ((int)$sth->fetchColumn() > 0) {
|
||||
skip("Opening stock already exists in Main Warehouse — skipping stock-in step");
|
||||
} else {
|
||||
$bin_index = 1;
|
||||
foreach ($product_defs as $p) {
|
||||
$bin = 'A-' . $bin_index;
|
||||
$bin_index++;
|
||||
|
||||
$supplier_id = $supplier_ids[array_rand($supplier_ids)];
|
||||
$uuid = bin2hex(random_bytes(16));
|
||||
|
||||
dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $main_wh_id, $p, $bin, $supplier_id, $logging, $uuid) {
|
||||
$stock_id = $stockMgmt->saveStockIn([
|
||||
'id' => 0,
|
||||
'warehouse' => $main_wh_id,
|
||||
'product_sku' => $p['sku'],
|
||||
'quantity' => $p['qty'],
|
||||
'price' => $p['cost'],
|
||||
'zone' => $bin,
|
||||
'aisle' => $bin,
|
||||
'bin' => $bin,
|
||||
'contact_id' => $supplier_id,
|
||||
'description' => 'Opening stock (demo seed)',
|
||||
], $logging, $uuid);
|
||||
|
||||
$stockMgmt->approveStock($stock_id, $main_wh_id, 'in', $whMgmt);
|
||||
});
|
||||
|
||||
ok("Received {$p['qty']} {$p['uom']} of {$p['sku']} into bin {$bin}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 6. Restock — later replenishment receipts from suppliers, into fresh bins
|
||||
// (separate batch from opening stock, so the Stock In list shows more than
|
||||
// one receiving event). Bin is resolved dynamically (first free bin in the
|
||||
// target warehouse) rather than hardcoded, so this stays correct regardless
|
||||
// of which bins earlier runs / other seed steps have already taken.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
echo "\n--- Restock (additional stock-in) ---\n";
|
||||
|
||||
$restock_marker = 'Restock (demo seed)';
|
||||
|
||||
$restock_defs = [
|
||||
['warehouse' => $main_wh_id, 'warehouse_name' => 'Main Warehouse', 'sku' => 'EL-001', 'qty' => 80],
|
||||
['warehouse' => $main_wh_id, 'warehouse_name' => 'Main Warehouse', 'sku' => 'OF-001', 'qty' => 200],
|
||||
['warehouse' => $bangna_wh_id, 'warehouse_name' => 'Bangna Distribution Center', 'sku' => 'BV-001', 'qty' => 120],
|
||||
['warehouse' => $main_wh_id, 'warehouse_name' => 'Main Warehouse', 'sku' => 'EL-002', 'qty' => 100],
|
||||
['warehouse' => $main_wh_id, 'warehouse_name' => 'Main Warehouse', 'sku' => 'PK-001', 'qty' => 150],
|
||||
['warehouse' => $bangna_wh_id, 'warehouse_name' => 'Bangna Distribution Center', 'sku' => 'BV-002', 'qty' => 60],
|
||||
['warehouse' => $bangna_wh_id, 'warehouse_name' => 'Bangna Distribution Center', 'sku' => 'OF-002', 'qty' => 90],
|
||||
];
|
||||
|
||||
foreach ($restock_defs as $r) {
|
||||
$p = findProductDef($product_defs, $r['sku']);
|
||||
$warehouse_id = $r['warehouse'];
|
||||
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT COUNT(*) FROM `td_stock_{$warehouse_id}`
|
||||
WHERE company_id = :c AND type = 'in' AND description = :d AND product_sku = :s"
|
||||
);
|
||||
$sth->execute([':c' => $company_id, ':d' => $restock_marker, ':s' => $r['sku']]);
|
||||
if ((int)$sth->fetchColumn() > 0) {
|
||||
skip("Restock of {$r['sku']} into '{$r['warehouse_name']}' already exists");
|
||||
continue;
|
||||
}
|
||||
|
||||
$bin = findFreeBin($pdo2, $company_id, $warehouse_id);
|
||||
$supplier_id = $supplier_ids[array_rand($supplier_ids)];
|
||||
$uuid = bin2hex(random_bytes(16));
|
||||
|
||||
dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $warehouse_id, $p, $r, $bin, $supplier_id, $logging, $uuid, $restock_marker) {
|
||||
$stock_id = $stockMgmt->saveStockIn([
|
||||
'id' => 0,
|
||||
'warehouse' => $warehouse_id,
|
||||
'product_sku' => $r['sku'],
|
||||
'quantity' => $r['qty'],
|
||||
'price' => $p['cost'],
|
||||
'zone' => $bin,
|
||||
'aisle' => $bin,
|
||||
'bin' => $bin,
|
||||
'contact_id' => $supplier_id,
|
||||
'description' => $restock_marker,
|
||||
], $logging, $uuid);
|
||||
|
||||
$stockMgmt->approveStock($stock_id, $warehouse_id, 'in', $whMgmt);
|
||||
});
|
||||
|
||||
ok("Received {$r['qty']} {$p['uom']} of {$r['sku']} into bin {$bin} ({$r['warehouse_name']})");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 7. Stock-out — ship product out to customers.
|
||||
//
|
||||
// saveStockOut() always takes the *entire* quantity sitting in the source
|
||||
// bin (there is no partial-out parameter), so to produce a stock-out of a
|
||||
// specific, controlled quantity we first receive a small dedicated batch
|
||||
// into a fresh bin, approve it, then immediately move that whole batch out
|
||||
// again. This leaves the opening-stock / restock balances untouched while
|
||||
// still creating a standalone, realistic outbound movement.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
echo "\n--- Stock-out (Main Warehouse) ---\n";
|
||||
|
||||
$dispatch_in_marker = 'Batch for dispatch (demo seed)';
|
||||
$dispatch_out_marker = 'Direct dispatch (demo seed)';
|
||||
|
||||
$stockout_defs = [
|
||||
['sku' => 'PK-003', 'qty' => 25, 'customer' => 'Siam Retail Co., Ltd.'],
|
||||
['sku' => 'OF-003', 'qty' => 15, 'customer' => 'Bangkok Mart'],
|
||||
['sku' => 'EL-004', 'qty' => 10, 'customer' => 'Chao Phraya Trading'],
|
||||
['sku' => 'BV-002', 'qty' => 20, 'customer' => 'Northern Distribution Partners'],
|
||||
['sku' => 'PK-001', 'qty' => 40, 'customer' => 'Siam Retail Co., Ltd.'],
|
||||
['sku' => 'EL-002', 'qty' => 30, 'customer' => 'Bangkok Mart'],
|
||||
];
|
||||
|
||||
foreach ($stockout_defs as $so) {
|
||||
$p = findProductDef($product_defs, $so['sku']);
|
||||
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT COUNT(*) FROM `td_stock_{$main_wh_id}`
|
||||
WHERE company_id = :c AND type = 'out' AND description = :d AND product_sku = :s"
|
||||
);
|
||||
$sth->execute([':c' => $company_id, ':d' => $dispatch_out_marker, ':s' => $so['sku']]);
|
||||
if ((int)$sth->fetchColumn() > 0) {
|
||||
skip("Dispatch of {$so['sku']} already exists");
|
||||
continue;
|
||||
}
|
||||
|
||||
$sth = $pdo2->prepare("SELECT id FROM md_contact WHERE company_id = :c AND contact_name = :n LIMIT 1");
|
||||
$sth->execute([':c' => $company_id, ':n' => $so['customer']]);
|
||||
$customer_id = (int)$sth->fetchColumn();
|
||||
|
||||
$sku = $so['sku'];
|
||||
$qty = $so['qty'];
|
||||
$cost = $p['cost'];
|
||||
$uom = $p['uom'];
|
||||
|
||||
dbTransaction($pdo2, function ($pdo2) use (
|
||||
$stockMgmt, $whMgmt, $company_id, $main_wh_id, $sku, $qty, $cost, $customer_id,
|
||||
$logging, $dispatch_in_marker, $dispatch_out_marker
|
||||
) {
|
||||
$bin = findFreeBin($pdo2, $company_id, $main_wh_id);
|
||||
|
||||
$in_uuid = bin2hex(random_bytes(16));
|
||||
$stock_id = $stockMgmt->saveStockIn([
|
||||
'id' => 0,
|
||||
'warehouse' => $main_wh_id,
|
||||
'product_sku' => $sku,
|
||||
'quantity' => $qty,
|
||||
'price' => $cost,
|
||||
'zone' => $bin,
|
||||
'aisle' => $bin,
|
||||
'bin' => $bin,
|
||||
'contact_id' => $customer_id,
|
||||
'description' => $dispatch_in_marker,
|
||||
], $logging, $in_uuid);
|
||||
$stockMgmt->approveStock($stock_id, $main_wh_id, 'in', $whMgmt);
|
||||
|
||||
$out_uuid = bin2hex(random_bytes(16));
|
||||
$out_id = $stockMgmt->saveStockOut([
|
||||
'id' => 0,
|
||||
'warehouse' => $main_wh_id,
|
||||
'product_sku' => $sku,
|
||||
'zone' => $bin,
|
||||
'aisle' => $bin,
|
||||
'bin' => $bin,
|
||||
'contact_id' => $customer_id,
|
||||
'description' => $dispatch_out_marker,
|
||||
], $logging, $out_uuid);
|
||||
$stockMgmt->approveStock($out_id, $main_wh_id, 'out', $whMgmt);
|
||||
});
|
||||
|
||||
ok("Shipped {$qty} {$uom} of {$sku} to {$so['customer']}");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 8. Stock-transfer — move product from Main Warehouse to Bangna Distribution
|
||||
// Center. Same batch-then-move approach as stock-out above: receive a
|
||||
// dedicated small batch into a fresh Main Warehouse bin, then transfer that
|
||||
// whole batch to a fresh Bangna bin, so opening-stock / restock balances
|
||||
// in Main Warehouse are left untouched.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
echo "\n--- Stock-transfer (Main Warehouse -> Bangna Distribution Center) ---\n";
|
||||
|
||||
$transfer_in_marker = 'Batch for transfer (demo seed)';
|
||||
$transfer_out_marker = 'Transfer to Bangna (demo seed)';
|
||||
|
||||
$transfer_defs = [
|
||||
['sku' => 'BV-001', 'qty' => 30],
|
||||
['sku' => 'PK-002', 'qty' => 12],
|
||||
['sku' => 'OF-004', 'qty' => 20],
|
||||
['sku' => 'EL-003', 'qty' => 15],
|
||||
['sku' => 'OF-002', 'qty' => 25],
|
||||
['sku' => 'PK-001', 'qty' => 50],
|
||||
];
|
||||
|
||||
foreach ($transfer_defs as $t) {
|
||||
$p = findProductDef($product_defs, $t['sku']);
|
||||
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT COUNT(*) FROM `td_stock_{$main_wh_id}`
|
||||
WHERE company_id = :c AND type = 'transfer' AND description = :d
|
||||
AND product_sku = :s AND `out` > 0"
|
||||
);
|
||||
$sth->execute([':c' => $company_id, ':d' => $transfer_out_marker, ':s' => $t['sku']]);
|
||||
if ((int)$sth->fetchColumn() > 0) {
|
||||
skip("Transfer of {$t['sku']} to Bangna Distribution Center already exists");
|
||||
continue;
|
||||
}
|
||||
|
||||
$sku = $t['sku'];
|
||||
$qty = $t['qty'];
|
||||
$cost = $p['cost'];
|
||||
$uom = $p['uom'];
|
||||
|
||||
dbTransaction($pdo2, function ($pdo2) use (
|
||||
$stockMgmt, $whMgmt, $company_id, $main_wh_id, $bangna_wh_id, $sku, $qty, $cost,
|
||||
$logging, $transfer_in_marker, $transfer_out_marker
|
||||
) {
|
||||
$from_bin = findFreeBin($pdo2, $company_id, $main_wh_id);
|
||||
|
||||
$in_uuid = bin2hex(random_bytes(16));
|
||||
$stock_id = $stockMgmt->saveStockIn([
|
||||
'id' => 0,
|
||||
'warehouse' => $main_wh_id,
|
||||
'product_sku' => $sku,
|
||||
'quantity' => $qty,
|
||||
'price' => $cost,
|
||||
'zone' => $from_bin,
|
||||
'aisle' => $from_bin,
|
||||
'bin' => $from_bin,
|
||||
'contact_id' => 0,
|
||||
'description' => $transfer_in_marker,
|
||||
], $logging, $in_uuid);
|
||||
$stockMgmt->approveStock($stock_id, $main_wh_id, 'in', $whMgmt);
|
||||
|
||||
$to_bin = findFreeBin($pdo2, $company_id, $bangna_wh_id);
|
||||
|
||||
$transfer_uuid = bin2hex(random_bytes(16));
|
||||
$transfer_id = $stockMgmt->saveStockTransfer([
|
||||
'id' => 0,
|
||||
'warehouse_from' => $main_wh_id,
|
||||
'zone_from' => $from_bin,
|
||||
'aisle_from' => $from_bin,
|
||||
'bin_from' => $from_bin,
|
||||
'warehouse_to' => $bangna_wh_id,
|
||||
'zone_to' => $to_bin,
|
||||
'aisle_to' => $to_bin,
|
||||
'bin_to' => $to_bin,
|
||||
'product_sku' => $sku,
|
||||
'contact_id' => 0,
|
||||
'description' => $transfer_out_marker,
|
||||
], $logging, $transfer_uuid);
|
||||
$stockMgmt->approveStock($transfer_id, $main_wh_id, 'transfer', $whMgmt);
|
||||
});
|
||||
|
||||
ok("Transferred {$qty} {$uom} of {$sku} from Main Warehouse to Bangna Distribution Center");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
echo "\n\033[1m=== Done ===\033[0m\n\n";
|
||||
echo "Log in at /login/index.php with:\n";
|
||||
echo " Username: {$demo_username}\n";
|
||||
echo " Password: {$demo_password}\n";
|
||||
echo " (No OTP step — the demo company has no SMTP configured.)\n\n";
|
||||
Reference in New Issue
Block a user