Files
wms-app/setup.php
T
Thanakorn SandClaude Sonnet 4.6 b4b1f5cbec Security hardening: invited user onboarding flow (C1–N7)
- C1: verify.php now filters license='owner' — invite tokens no longer accepted
- C1: onboarding API rejects non-owner sessions
- C2: Existing-user invite requires explicit acceptance via accept_invite.php
- C2: New accept_invite.php page and API engine added
- C2: inviteUser() generates token + expiry for existing users; resendInvite() handles active users
- C3: session_regenerate_id(true) before writing invite session keys on both invite pages
- C4: invited_onboarding API wraps activation in transaction with SELECT FOR UPDATE; rowCount check added; SQLSTATE 23000 caught cleanly
- C5: inviteUser() and resendInvite() two-table writes wrapped in transactions
- M2: removeUser() wrapped in transaction with FOR UPDATE; clears default_company on active user removal
- M4: Logged-in user guard added to invited_onboarding.php and accept_invite.php
- M5: manage_users.php uses $server_url instead of HTTP_HOST for invite URLs
- M6: Username regex enforces 3-32 chars; reserved name blocklist added
- N5: searchUsers() changed from LIKE fuzzy search to exact email match only
- N7: resendInvite() rate-limited to once per 60s via invite_resent_at column
- Schema: company_map_user gains invite_expires_at and invite_resent_at columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:18:40 +07:00

1186 lines
52 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Production Setup Script
* Run once on a fresh server via CLI: php setup.php
*
* What it does:
* 1. Reads DB credentials from app/config.php
* 2. Creates wms + wms2 databases if they don't exist
* 3. Creates all tables (CREATE TABLE IF NOT EXISTS — safe to re-run)
* 4. Prints a status report
*
* Dynamic td_stock_<warehouse_id> tables are created automatically by the app
* the first time a warehouse is used; they are NOT listed here.
*/
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit('Run via CLI only: php setup.php');
}
// ── Load config ───────────────────────────────────────────────────────────────
$config = __DIR__ . '/app/config.php';
if (!file_exists($config)) {
exit("ERROR: app/config.php not found. Configure it before running setup.\n");
}
require $config;
$host = $db_server ?? 'localhost';
$user = $db_user ?? 'root';
$pass = $db_pass ?? '';
$db1 = $db_database ?? 'wms';
$db2 = $db_database2 ?? 'wms2';
// ── Helpers ───────────────────────────────────────────────────────────────────
$ok_count = 0;
$skip_count = 0;
$err_count = 0;
function info(string $msg): void { echo "\033[36m •\033[0m {$msg}\n"; }
function ok(string $msg): void { global $ok_count; $ok_count++; echo "\033[32m ✓\033[0m {$msg}\n"; }
function skip(string $msg): void { global $skip_count; $skip_count++; echo "\033[33m –\033[0m {$msg}\n"; }
function fail(string $msg): void { global $err_count; $err_count++; echo "\033[31m ✗\033[0m {$msg}\n"; }
function run(PDO $pdo, string $sql, string $label): void {
try {
$pdo->exec($sql);
ok($label);
} catch (PDOException $e) {
if (str_contains($e->getMessage(), 'already exists') ||
str_contains($e->getMessage(), 'Duplicate column name')) {
skip($label . ' (already exists)');
} else {
fail($label . ': ' . $e->getMessage());
}
}
}
// ── Connect (no DB selected) ──────────────────────────────────────────────────
echo "\n\033[1m=== mn3wms Production Setup ===\033[0m\n\n";
echo "Host : {$host}\n";
echo "DB1 : {$db1} (auth / identity)\n";
echo "DB2 : {$db2} (business data)\n\n";
try {
$pdo = new PDO("mysql:host={$host};charset=utf8", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
ok("Connected to MySQL as {$user}@{$host}");
} catch (PDOException $e) {
fail("Cannot connect: " . $e->getMessage());
exit(1);
}
// ── Create databases ──────────────────────────────────────────────────────────
echo "\n--- Databases ---\n";
run($pdo, "CREATE DATABASE IF NOT EXISTS `{$db1}` CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci", "Database `{$db1}`");
run($pdo, "CREATE DATABASE IF NOT EXISTS `{$db2}` CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci", "Database `{$db2}`");
// ─────────────────────────────────────────────────────────────────────────────
// WMS (auth / identity) tables
// ─────────────────────────────────────────────────────────────────────────────
echo "\n--- {$db1} tables ---\n";
$pdo->exec("USE `{$db1}`");
run($pdo, "
CREATE TABLE IF NOT EXISTS `user` (
`user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`status` varchar(40) NOT NULL DEFAULT 'not activated',
`name` tinytext NOT NULL,
`surname` tinytext NOT NULL,
`address` tinytext DEFAULT NULL,
`country` varchar(50) NOT NULL DEFAULT '',
`profile_picture` tinytext NOT NULL,
`default_company` int(11) DEFAULT 0,
`language` varchar(3) NOT NULL DEFAULT 'th',
`regis_date` timestamp NOT NULL DEFAULT current_timestamp(),
`license` enum('owner','user') NOT NULL DEFAULT 'owner',
`app_access` enum('all','wms','accounting') NOT NULL DEFAULT 'all',
`active` varchar(255) NOT NULL DEFAULT 'yes',
`phone` varchar(50) NOT NULL DEFAULT '',
`verify_token` varchar(64) DEFAULT NULL,
`verify_expires_at` datetime DEFAULT NULL,
`login_attempts` int(11) NOT NULL DEFAULT 0,
`locked_until` datetime DEFAULT NULL,
`session_token` varchar(64) DEFAULT NULL,
`session_token_at` datetime DEFAULT NULL,
`session_last_seen` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`),
KEY `idx_user_verify_token` (`verify_token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'user');
run($pdo, "
CREATE TABLE IF NOT EXISTS `company_list` (
`company_id` int(11) NOT NULL AUTO_INCREMENT,
`channel_name` varchar(100) NOT NULL DEFAULT '' COMMENT 'Unique slug used for login routing',
`company_name` varchar(255) NOT NULL DEFAULT '',
`company_name2` varchar(255) NOT NULL DEFAULT '' COMMENT 'Secondary / English name',
`company_logo` varchar(255) NOT NULL DEFAULT '',
`company_seal` varchar(255) NOT NULL DEFAULT '',
`branch` varchar(255) NOT NULL DEFAULT 'สำนักงานใหญ่',
`branch_no` varchar(50) NOT NULL DEFAULT '',
`fiscal_year` varchar(10) NOT NULL DEFAULT '',
`fx` varchar(10) NOT NULL DEFAULT 'thb',
`address` text NOT NULL DEFAULT '',
`address2` text NOT NULL DEFAULT '',
`tax_id` varchar(50) NOT NULL DEFAULT '',
`prompt_pay` varchar(50) NOT NULL DEFAULT '',
`entrepreneur` varchar(100) NOT NULL DEFAULT '',
`email` varchar(255) NOT NULL DEFAULT '',
`phone` varchar(50) NOT NULL DEFAULT '',
`fax` varchar(50) NOT NULL DEFAULT '',
`website` varchar(255) NOT NULL DEFAULT '',
`facebook_page` varchar(255) NOT NULL DEFAULT '',
`package` varchar(50) NOT NULL DEFAULT 'starter',
PRIMARY KEY (`company_id`),
UNIQUE KEY `channel_name` (`channel_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'company_list');
run($pdo, "
CREATE TABLE IF NOT EXISTS `company_map_user` (
`map_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`role` varchar(15) NOT NULL DEFAULT 'user',
`app_access` varchar(15) NOT NULL DEFAULT 'wms',
`invite_token` varchar(64) DEFAULT NULL,
`invite_expires_at` datetime DEFAULT NULL,
`invite_resent_at` datetime DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`map_id`),
KEY `user_id` (`user_id`),
KEY `company_id` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'company_map_user');
// Column added for explicit-consent invite flow (existing users)
run($pdo, "ALTER TABLE `company_map_user` ADD COLUMN `invite_expires_at` DATETIME DEFAULT NULL AFTER `invite_token`", 'company_map_user.invite_expires_at');
run($pdo, "ALTER TABLE `company_map_user` ADD COLUMN `invite_resent_at` DATETIME DEFAULT NULL AFTER `invite_expires_at`", 'company_map_user.invite_resent_at');
run($pdo, "
CREATE TABLE IF NOT EXISTS `company_setting` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`setting_key` varchar(64) NOT NULL,
`value` varchar(255) NOT NULL DEFAULT '',
`updated_at` datetime DEFAULT NULL,
`log` mediumtext DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `company_key` (`company_id`,`setting_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'company_setting');
run($pdo, "
CREATE TABLE IF NOT EXISTS `company_smtp` (
`smtp_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(10) unsigned NOT NULL,
`server` varchar(255) NOT NULL,
`port` varchar(10) NOT NULL DEFAULT '587',
`username` varchar(255) NOT NULL,
`password` text NOT NULL,
`from_name` varchar(255) NOT NULL DEFAULT '',
`from_email` varchar(255) NOT NULL DEFAULT '',
`encryption` enum('tls','ssl','none') NOT NULL DEFAULT 'tls',
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`smtp_id`),
UNIQUE KEY `uq_company_smtp` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'company_smtp');
run($pdo, "
CREATE TABLE IF NOT EXISTS `company_usage` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`day_date` date NOT NULL,
`daily_count` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_company_day` (`company_id`,`day_date`),
KEY `idx_company_id` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'company_usage');
run($pdo, "
CREATE TABLE IF NOT EXISTS `schema_migrations` (
`version` varchar(64) NOT NULL,
`applied_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'schema_migrations');
run($pdo, "
CREATE TABLE IF NOT EXISTS `whitelist` (
`list_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cookie` varchar(32) NOT NULL,
`status` int(11) NOT NULL DEFAULT 1 COMMENT '0=block 1=wait 2=allow',
`ip` varchar(20) NOT NULL,
`dt` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`list_id`),
UNIQUE KEY `cookie` (`cookie`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'whitelist');
// ─────────────────────────────────────────────────────────────────────────────
// WMS2 (business data) tables
// ─────────────────────────────────────────────────────────────────────────────
echo "\n--- {$db2} tables ---\n";
$pdo->exec("USE `{$db2}`");
// ── Master data ───────────────────────────────────────────────────────────────
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_warehouse` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`warehouse_name` varchar(255) NOT NULL DEFAULT '',
`location` text NOT NULL DEFAULT '',
`manager` int(11) NOT NULL DEFAULT 0,
`description` text NOT NULL DEFAULT '',
`status` int(1) NOT NULL DEFAULT 1,
`log` mediumtext NOT NULL DEFAULT '{}',
PRIMARY KEY (`id`),
UNIQUE KEY `company_id_warehouse_name` (`company_id`,`warehouse_name`),
KEY `company_id` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_warehouse');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_storage` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`warehouse` varchar(255) NOT NULL DEFAULT '',
`zone` text NOT NULL DEFAULT '',
`aisle_from` varchar(255) NOT NULL DEFAULT '',
`aisle_to` varchar(255) NOT NULL DEFAULT '',
`rack_from` varchar(255) NOT NULL DEFAULT '',
`rack_to` varchar(255) NOT NULL DEFAULT '',
`description` text NOT NULL DEFAULT '',
`status` int(1) NOT NULL DEFAULT 1,
`log` mediumtext NOT NULL DEFAULT '{}',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_storage');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_rack` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`td_stock_id` int(11) NOT NULL DEFAULT 0,
`warehouse` int(11) NOT NULL,
`storage_id` int(11) NOT NULL DEFAULT 0,
`zone` varchar(10) NOT NULL,
`aisle` varchar(10) NOT NULL,
`rack` varchar(10) NOT NULL,
`product_sku` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_rack_location` (`company_id`,`warehouse`,`zone`,`aisle`,`rack`),
KEY `idx_storage` (`storage_id`),
KEY `idx_sku` (`company_id`,`product_sku`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'md_rack');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_rack_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(10) NOT NULL,
`md_rack_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL,
`dt` datetime NOT NULL DEFAULT current_timestamp(),
`login` datetime DEFAULT NULL,
`action` enum('occupy','release','transfer_in','transfer_out') NOT NULL,
`product_sku` varchar(100) DEFAULT NULL,
`td_stock_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_rack` (`company_id`,`md_rack_id`),
KEY `idx_user` (`company_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_rack_log');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_product_category` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`category` varchar(255) NOT NULL DEFAULT '',
`slug` varchar(255) NOT NULL DEFAULT '',
`description` text NOT NULL DEFAULT '',
`status` int(1) NOT NULL DEFAULT 1,
`log` mediumtext NOT NULL DEFAULT '{}',
PRIMARY KEY (`id`),
UNIQUE KEY `company_id_product_id` (`company_id`,`category`),
KEY `company_id` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_product_category');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_product` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`product_name` varchar(255) NOT NULL DEFAULT '',
`sku` varchar(255) NOT NULL DEFAULT '',
`barcode` varchar(64) DEFAULT NULL,
`price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`cost_price` decimal(15,2) NOT NULL DEFAULT 0.00,
`tax_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`sales_account_code` varchar(20) DEFAULT NULL,
`purchase_account_code` varchar(20) DEFAULT NULL,
`uom` varchar(20) NOT NULL DEFAULT 'pcs',
`min_stock` decimal(18,4) NOT NULL DEFAULT 0.0000,
`reorder_point` decimal(18,4) NOT NULL DEFAULT 0.0000,
`category` int(11) NOT NULL DEFAULT 0,
`product_image` text NOT NULL DEFAULT '',
`description` text NOT NULL DEFAULT '',
`status` int(1) NOT NULL DEFAULT 1,
`log` mediumtext NOT NULL DEFAULT '{}',
PRIMARY KEY (`id`),
UNIQUE KEY `company_id_product_id` (`company_id`,`product_name`),
KEY `company_id` (`company_id`),
KEY `idx_product_barcode` (`company_id`,`barcode`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_product');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_lot` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(10) unsigned NOT NULL,
`product_sku` varchar(100) NOT NULL,
`lot_number` varchar(100) NOT NULL,
`expiry_date` date NOT NULL,
`description` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `company_id_product_sku_lot_number` (`company_id`,`product_sku`,`lot_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_lot');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_contact_type` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`contact_type` varchar(255) NOT NULL DEFAULT '',
`description` text NOT NULL DEFAULT '',
`status` int(1) NOT NULL DEFAULT 1,
`log` mediumtext NOT NULL DEFAULT '{}',
PRIMARY KEY (`id`),
UNIQUE KEY `company_id_product_id` (`company_id`,`contact_type`),
KEY `company_id` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_contact_type');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_contact` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`contact_name` varchar(255) NOT NULL,
`tax_id` varchar(255) NOT NULL,
`organization` varchar(255) NOT NULL,
`branch` varchar(255) NOT NULL,
`contact_type` int(11) unsigned NOT NULL,
`billing_address` text NOT NULL,
`shipping_location` varchar(255) NOT NULL,
`shipping_address` text NOT NULL,
`remark` text NOT NULL,
`contact_image` mediumtext NOT NULL,
`status` int(1) unsigned NOT NULL DEFAULT 1,
`log` mediumtext NOT NULL DEFAULT '{}',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_contact');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_barcode` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(10) unsigned NOT NULL,
`barcode_type` varchar(20) NOT NULL,
`barcode` varchar(320) NOT NULL,
`product_sku` varchar(100) DEFAULT NULL,
`lot_number` varchar(100) DEFAULT NULL,
`serial_number` varchar(100) DEFAULT NULL,
`warehouse_id` int(10) unsigned DEFAULT NULL,
`zone` varchar(100) DEFAULT NULL,
`aisle` varchar(100) DEFAULT NULL,
`rack` varchar(100) DEFAULT NULL,
`print_count` int(10) unsigned NOT NULL DEFAULT 0,
`last_printed_dt` datetime DEFAULT NULL,
`created_dt` datetime NOT NULL DEFAULT current_timestamp(),
`status` tinyint(4) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `company_id_barcode` (`company_id`,`barcode`),
KEY `company_id_type` (`company_id`,`barcode_type`),
KEY `company_id_product_lot` (`company_id`,`product_sku`,`lot_number`),
KEY `company_id_location` (`company_id`,`warehouse_id`,`zone`,`aisle`,`rack`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_barcode');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_sku_barcode_label` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(10) unsigned NOT NULL,
`product_sku` varchar(100) NOT NULL,
`lot_number` varchar(100) NOT NULL,
`serial_number` varchar(100) NOT NULL,
`barcode` varchar(320) NOT NULL,
`created_dt` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `company_id_barcode` (`company_id`,`barcode`),
KEY `company_id_product_lot` (`company_id`,`product_sku`,`lot_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_sku_barcode_label');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_lock_operation` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`operation_type` varchar(50) NOT NULL DEFAULT '',
`user_id` int(11) NOT NULL DEFAULT 0,
`locked_at` datetime NOT NULL,
`expires_at` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `op` (`company_id`,`operation_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_lock_operation');
// ── Accounting master ─────────────────────────────────────────────────────────
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`dept_code` varchar(20) NOT NULL,
`dept_name` varchar(255) NOT NULL,
`description` text DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_company_dept` (`company_id`,`dept_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_department');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`account_code` varchar(20) NOT NULL,
`account_name` varchar(255) NOT NULL,
`account_type` enum('asset','liability','equity','revenue','expense') NOT NULL,
`account_category` varchar(50) NOT NULL DEFAULT '',
`parent_code` varchar(20) DEFAULT NULL,
`is_posting` tinyint(1) NOT NULL DEFAULT 1,
`status` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_company_account` (`company_id`,`account_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_account');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_account_formula` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`formula_name` varchar(100) NOT NULL,
`document_type` varchar(50) NOT NULL,
`description` text DEFAULT NULL,
`is_default` tinyint(1) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 1,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_company_doc` (`company_id`,`document_type`),
KEY `idx_status` (`company_id`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_account_formula');
run($pdo, "
CREATE TABLE IF NOT EXISTS `md_account_formula_item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`formula_id` int(11) NOT NULL,
`drcr` enum('D','C') NOT NULL,
`account_code` varchar(20) NOT NULL,
`amount_key` enum('grand_total','total','tax','amount') NOT NULL,
`description` varchar(255) DEFAULT NULL,
`sort_order` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_formula` (`company_id`,`formula_id`),
KEY `idx_account` (`company_id`,`account_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_account_formula_item');
// ── Stock (template + base) ───────────────────────────────────────────────────
// td_stock_<warehouse_id> tables are created automatically by the app on first use.
// This base table matches the template used by WarehouseManager::createStockTableForWarehouse().
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_stock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`ref_id` int(11) unsigned NOT NULL DEFAULT 0,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(20) NOT NULL,
`type` varchar(20) NOT NULL,
`ref_warehouse` varchar(20) NOT NULL DEFAULT '',
`date` datetime DEFAULT NULL,
`product_sku` varchar(255) NOT NULL DEFAULT '0',
`lot_number` varchar(255) NOT NULL DEFAULT '',
`serial_number` varchar(255) NOT NULL DEFAULT '',
`source` varchar(255) NOT NULL DEFAULT '',
`source_id` int(11) NOT NULL DEFAULT 0,
`in` decimal(18,4) NOT NULL DEFAULT 0.0000,
`out` decimal(18,4) NOT NULL DEFAULT 0.0000,
`price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`zone` varchar(255) NOT NULL DEFAULT '',
`aisle` varchar(255) NOT NULL DEFAULT '',
`rack` varchar(255) NOT NULL DEFAULT '',
`contact_id` int(11) NOT NULL DEFAULT 0,
`description` text NOT NULL DEFAULT '',
`status` int(3) NOT NULL DEFAULT 0,
`log` mediumtext NOT NULL DEFAULT '{}',
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `product_id` (`product_sku`),
KEY `updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_stock (base template)');
run($pdo, "
CREATE TABLE IF NOT EXISTS `etl_stock_summary` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) unsigned NOT NULL DEFAULT 0,
`warehouse_id` int(11) unsigned NOT NULL DEFAULT 0,
`product_sku` varchar(255) NOT NULL DEFAULT '',
`month` varchar(10) NOT NULL,
`total_in` decimal(18,4) NOT NULL DEFAULT 0.0000,
`total_out` decimal(18,4) NOT NULL DEFAULT 0.0000,
`source_updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `company_id_warehouse_id_product_sku_month` (`company_id`,`warehouse_id`,`product_sku`,`month`),
KEY `company_id` (`company_id`),
KEY `product_id` (`warehouse_id`),
KEY `source_updated_at` (`source_updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'etl_stock_summary');
// ── Transactions ──────────────────────────────────────────────────────────────
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_gl` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`source_type` varchar(50) NOT NULL DEFAULT '',
`source_id` int(11) NOT NULL DEFAULT 0,
`reference` varchar(100) NOT NULL DEFAULT '',
`description` varchar(255) NOT NULL DEFAULT '',
`journal_date` date DEFAULT NULL,
`formula_id` int(11) NOT NULL DEFAULT 0,
`period` varchar(7) NOT NULL DEFAULT '',
`current_version` tinyint(4) NOT NULL DEFAULT 1,
`history` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '[]' CHECK (json_valid(`history`)),
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `period` (`period`),
KEY `formula_id` (`formula_id`),
KEY `source` (`company_id`,`source_type`,`source_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_gl');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_gl_item` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`gl_id` int(10) unsigned NOT NULL,
`account_code` varchar(20) NOT NULL DEFAULT '',
`department_id` int(11) NOT NULL DEFAULT 0,
`debit` decimal(18,4) NOT NULL DEFAULT 0.0000,
`credit` decimal(18,4) NOT NULL DEFAULT 0.0000,
`description` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `gl_id` (`gl_id`),
KEY `company_id` (`company_id`),
KEY `account_code` (`account_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_gl_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_batch_action` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`type` varchar(50) NOT NULL DEFAULT '',
`doc_type` varchar(50) NOT NULL DEFAULT '',
`total` int(11) NOT NULL DEFAULT 0,
`succeeded` int(11) NOT NULL DEFAULT 0,
`failed` int(11) NOT NULL DEFAULT 0,
`failed_ids` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '[]' CHECK (json_valid(`failed_ids`)),
`user_id` int(11) NOT NULL DEFAULT 0,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `type` (`type`),
KEY `doc_type` (`doc_type`),
KEY `user_id` (`user_id`),
KEY `created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_batch_action');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_quotation` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`quotation_number` varchar(50) NOT NULL DEFAULT '',
`contact_id` int(11) NOT NULL DEFAULT 0,
`department_id` int(11) NOT NULL DEFAULT 0,
`quotation_date` date DEFAULT NULL,
`valid_until` date DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`subtotal` decimal(18,4) NOT NULL DEFAULT 0.0000,
`discount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax` decimal(18,4) NOT NULL DEFAULT 0.0000,
`grand_total` decimal(18,4) NOT NULL DEFAULT 0.0000,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `quotation_number` (`quotation_number`),
KEY `status` (`status`),
KEY `contact_id` (`contact_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_quotation');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_quotation_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`quotation_id` int(11) unsigned NOT NULL DEFAULT 0,
`item_id` int(11) unsigned NOT NULL DEFAULT 0,
`product_sku` varchar(100) NOT NULL DEFAULT '',
`product_name` varchar(255) NOT NULL DEFAULT '',
`quantity` decimal(18,4) NOT NULL DEFAULT 0.0000,
`unit_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`total_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`converted_qty` decimal(18,4) NOT NULL DEFAULT 0.0000,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_qt_item` (`quotation_id`,`item_id`),
KEY `idx_quotation` (`company_id`,`quotation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_quotation_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`source_id` int(11) NOT NULL DEFAULT 0,
`source` varchar(20) NOT NULL DEFAULT '',
`order_number` varchar(50) NOT NULL DEFAULT '',
`contact_id` int(11) NOT NULL DEFAULT 0,
`department_id` int(11) NOT NULL DEFAULT 0,
`order_date` date DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`subtotal` decimal(18,4) NOT NULL DEFAULT 0.0000,
`discount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_adjustment` decimal(8,2) NOT NULL DEFAULT 0.00,
`shipping_fee` decimal(18,4) NOT NULL DEFAULT 0.0000,
`shipping_tracking_number` varchar(100) DEFAULT NULL,
`grand_total` decimal(18,4) NOT NULL DEFAULT 0.0000,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `contact_id` (`contact_id`),
KEY `order_number` (`order_number`),
KEY `status` (`status`),
KEY `order_date` (`order_date`),
KEY `quotation_id` (`source_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_order');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_order_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`order_id` int(11) unsigned NOT NULL DEFAULT 0,
`item_id` int(11) unsigned NOT NULL DEFAULT 0,
`product_sku` varchar(100) NOT NULL DEFAULT '',
`product_name` varchar(255) NOT NULL DEFAULT '',
`quantity` decimal(18,4) NOT NULL DEFAULT 0.0000,
`unit_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`total_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`warehouse_id` int(11) NOT NULL DEFAULT 0,
`stock_out_id` int(11) NOT NULL DEFAULT 0,
`lot_number` varchar(100) NOT NULL DEFAULT '',
`invoiced_qty` decimal(18,4) NOT NULL DEFAULT 0.0000,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_order_item` (`order_id`,`item_id`),
KEY `idx_order` (`company_id`,`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_order_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_invoice` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`source_id` int(11) NOT NULL DEFAULT 0,
`source` varchar(20) NOT NULL DEFAULT '',
`doc_type` varchar(20) NOT NULL DEFAULT 'invoice',
`invoice_number` varchar(50) NOT NULL DEFAULT '',
`ref_invoice_id` int(11) unsigned NOT NULL DEFAULT 0,
`order_id` int(11) unsigned NOT NULL DEFAULT 0,
`contact_id` int(11) NOT NULL DEFAULT 0,
`department_id` int(11) NOT NULL DEFAULT 0,
`issued_date` date DEFAULT NULL,
`due_date` date DEFAULT NULL,
`subtotal` decimal(18,4) NOT NULL DEFAULT 0.0000,
`discount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_adjustment` decimal(8,2) NOT NULL DEFAULT 0.00,
`shipping_fee` decimal(18,4) NOT NULL DEFAULT 0.0000,
`grand_total` decimal(18,4) NOT NULL DEFAULT 0.0000,
`formula_id` int(11) DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `order_id` (`order_id`),
KEY `contact_id` (`contact_id`),
KEY `invoice_number` (`invoice_number`),
KEY `ref_invoice_id` (`ref_invoice_id`),
KEY `doc_type` (`doc_type`),
KEY `status` (`status`),
KEY `due_date` (`due_date`),
KEY `source_id` (`source_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_invoice');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_invoice_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`invoice_id` int(11) unsigned NOT NULL DEFAULT 0,
`item_id` int(11) unsigned NOT NULL DEFAULT 0,
`product_sku` varchar(100) NOT NULL DEFAULT '',
`product_name` varchar(255) NOT NULL DEFAULT '',
`quantity` decimal(18,4) NOT NULL DEFAULT 0.0000,
`unit_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`total_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_invoice_item` (`invoice_id`,`item_id`),
KEY `idx_invoice` (`company_id`,`invoice_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_invoice_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_return` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`return_number` varchar(50) NOT NULL DEFAULT '',
`order_id` int(11) unsigned NOT NULL DEFAULT 0,
`invoice_id` int(11) unsigned NOT NULL DEFAULT 0,
`contact_id` int(11) NOT NULL DEFAULT 0,
`department_id` int(11) NOT NULL DEFAULT 0,
`return_date` date DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`reason` text NOT NULL DEFAULT '',
`refund_amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_adjustment` decimal(8,2) NOT NULL DEFAULT 0.00,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `order_id` (`order_id`),
KEY `invoice_id` (`invoice_id`),
KEY `contact_id` (`contact_id`),
KEY `return_number` (`return_number`),
KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_return');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_return_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`return_id` int(11) unsigned NOT NULL DEFAULT 0,
`item_id` int(11) unsigned NOT NULL DEFAULT 0,
`product_sku` varchar(100) NOT NULL DEFAULT '',
`product_name` varchar(255) NOT NULL DEFAULT '',
`quantity` decimal(18,4) NOT NULL DEFAULT 0.0000,
`unit_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`total_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`warehouse_id` int(11) NOT NULL DEFAULT 0,
`stock_out_id` int(11) NOT NULL DEFAULT 0,
`stock_out_warehouse_id` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_return_item` (`return_id`,`item_id`),
KEY `idx_return` (`company_id`,`return_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_return_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_purchase_request` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`request_number` varchar(50) NOT NULL DEFAULT '',
`contact_id` int(11) NOT NULL DEFAULT 0,
`department_id` int(11) NOT NULL DEFAULT 0,
`request_date` date DEFAULT NULL,
`required_date` date DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`subtotal` decimal(18,4) NOT NULL DEFAULT 0.0000,
`discount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax` decimal(18,4) NOT NULL DEFAULT 0.0000,
`shipping_fee` decimal(18,4) NOT NULL DEFAULT 0.0000,
`grand_total` decimal(18,4) NOT NULL DEFAULT 0.0000,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `request_number` (`request_number`),
KEY `status` (`status`),
KEY `contact_id` (`contact_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_purchase_request');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_purchase_request_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`request_id` int(11) unsigned NOT NULL DEFAULT 0,
`item_id` int(11) unsigned NOT NULL DEFAULT 0,
`product_sku` varchar(100) NOT NULL DEFAULT '',
`product_name` varchar(255) NOT NULL DEFAULT '',
`quantity` decimal(18,4) NOT NULL DEFAULT 0.0000,
`unit_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`total_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`converted_qty` decimal(18,4) NOT NULL DEFAULT 0.0000,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_pr_item` (`request_id`,`item_id`),
KEY `idx_pr_item` (`company_id`,`request_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_purchase_request_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_purchase_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`uuid` varchar(64) NOT NULL,
`source_id` int(11) NOT NULL DEFAULT 0,
`source` varchar(32) NOT NULL DEFAULT '',
`po_number` varchar(32) NOT NULL,
`contact_id` int(11) DEFAULT NULL,
`department_id` int(11) NOT NULL DEFAULT 0,
`po_date` date NOT NULL,
`expected_date` date DEFAULT NULL,
`closed_at` datetime DEFAULT NULL,
`warehouse_id` int(11) NOT NULL DEFAULT 0,
`status` tinyint(4) NOT NULL DEFAULT 0,
`subtotal` decimal(15,2) NOT NULL DEFAULT 0.00,
`discount` decimal(15,2) NOT NULL DEFAULT 0.00,
`tax` decimal(15,2) NOT NULL DEFAULT 0.00,
`tax_adjustment` decimal(8,2) NOT NULL DEFAULT 0.00,
`shipping_fee` decimal(15,2) NOT NULL DEFAULT 0.00,
`grand_total` decimal(15,2) NOT NULL DEFAULT 0.00,
`notes` text DEFAULT NULL,
`log` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`log`)),
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_po_company` (`company_id`),
KEY `idx_po_status` (`company_id`,`status`),
KEY `idx_po_contact` (`company_id`,`contact_id`),
KEY `idx_po_date` (`company_id`,`po_date`),
KEY `idx_po_source` (`company_id`,`source`,`source_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_purchase_order');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_purchase_order_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`order_id` int(11) unsigned NOT NULL DEFAULT 0,
`item_id` int(11) unsigned NOT NULL DEFAULT 0,
`product_sku` varchar(100) NOT NULL DEFAULT '',
`product_name` varchar(255) NOT NULL DEFAULT '',
`quantity` decimal(18,4) NOT NULL DEFAULT 0.0000,
`unit_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`total_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`received_qty` decimal(18,4) NOT NULL DEFAULT 0.0000,
`stock_in_id` int(11) NOT NULL DEFAULT 0,
`invoiced_qty` decimal(18,4) NOT NULL DEFAULT 0.0000,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_po_item` (`order_id`,`item_id`),
KEY `idx_order` (`company_id`,`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_purchase_order_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_supplier_return` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`return_number` varchar(32) NOT NULL DEFAULT '',
`po_id` int(11) unsigned NOT NULL DEFAULT 0,
`invoice_id` int(11) unsigned NOT NULL DEFAULT 0,
`contact_id` int(11) NOT NULL DEFAULT 0,
`department_id` int(11) NOT NULL DEFAULT 0,
`return_date` date NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`reason` text NOT NULL DEFAULT '',
`refund_amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_adjustment` decimal(8,2) NOT NULL DEFAULT 0.00,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
`shipping_tracking_number` varchar(100) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `idx_company_id` (`company_id`),
KEY `idx_po_id` (`po_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_supplier_return');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_supplier_return_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`return_id` int(11) unsigned NOT NULL DEFAULT 0,
`item_id` int(11) unsigned NOT NULL DEFAULT 0,
`product_sku` varchar(100) NOT NULL DEFAULT '',
`product_name` varchar(255) NOT NULL DEFAULT '',
`quantity` decimal(18,4) NOT NULL DEFAULT 0.0000,
`unit_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`total_price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`tax_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`warehouse_id` int(11) NOT NULL DEFAULT 0,
`stock_in_id` int(11) NOT NULL DEFAULT 0,
`zone` varchar(50) NOT NULL DEFAULT '',
`aisle` varchar(50) NOT NULL DEFAULT '',
`rack` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_sr_item` (`return_id`,`item_id`),
KEY `idx_return` (`company_id`,`return_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_supplier_return_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_receipt_billing` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`billing_number` varchar(50) NOT NULL DEFAULT '',
`contact_id` int(11) NOT NULL DEFAULT 0,
`billing_date` date DEFAULT NULL,
`amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`status` tinyint(1) NOT NULL DEFAULT 1,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `contact_id` (`contact_id`),
KEY `billing_number` (`billing_number`),
KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_receipt_billing');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_receipt_billing_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`billing_id` int(11) unsigned NOT NULL DEFAULT 0,
`invoice_id` int(11) unsigned NOT NULL DEFAULT 0,
`doc_type` varchar(20) NOT NULL DEFAULT '',
`amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `billing_id` (`billing_id`),
KEY `invoice_id` (`invoice_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_receipt_billing_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_receipt` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`receipt_number` varchar(50) NOT NULL DEFAULT '',
`contact_id` int(11) NOT NULL DEFAULT 0,
`department_id` int(11) NOT NULL DEFAULT 0,
`receipt_billing_id` int(11) unsigned NOT NULL DEFAULT 0,
`receipt_date` date DEFAULT NULL,
`payment_method` varchar(50) NOT NULL DEFAULT '',
`amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`formula_id` int(11) DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 1,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `contact_id` (`contact_id`),
KEY `receipt_number` (`receipt_number`),
KEY `status` (`status`),
KEY `receipt_billing_id` (`receipt_billing_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_receipt');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_receipt_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`receipt_id` int(11) unsigned NOT NULL DEFAULT 0,
`invoice_id` int(11) unsigned NOT NULL DEFAULT 0,
`doc_type` varchar(20) NOT NULL DEFAULT '',
`department_id` int(11) NOT NULL DEFAULT 0,
`amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `receipt_id` (`receipt_id`),
KEY `invoice_id` (`invoice_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_receipt_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_payment_billing` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`billing_number` varchar(50) NOT NULL DEFAULT '',
`contact_id` int(11) NOT NULL DEFAULT 0,
`billing_date` date DEFAULT NULL,
`amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`status` tinyint(1) NOT NULL DEFAULT 1,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `contact_id` (`contact_id`),
KEY `billing_number` (`billing_number`),
KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_payment_billing');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_payment_billing_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`billing_id` int(11) unsigned NOT NULL DEFAULT 0,
`invoice_id` int(11) unsigned NOT NULL DEFAULT 0,
`doc_type` varchar(20) NOT NULL DEFAULT '',
`amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `billing_id` (`billing_id`),
KEY `invoice_id` (`invoice_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_payment_billing_item');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_payment` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`uuid` varchar(32) NOT NULL DEFAULT '',
`payment_number` varchar(50) NOT NULL DEFAULT '',
`contact_id` int(11) NOT NULL DEFAULT 0,
`department_id` int(11) NOT NULL DEFAULT 0,
`payment_billing_id` int(11) unsigned NOT NULL DEFAULT 0,
`payment_date` date DEFAULT NULL,
`payment_method` varchar(50) NOT NULL DEFAULT '',
`amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
`formula_id` int(11) DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 1,
`notes` text NOT NULL DEFAULT '',
`log` mediumtext NOT NULL DEFAULT '{}',
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `contact_id` (`contact_id`),
KEY `payment_billing_id` (`payment_billing_id`),
KEY `payment_number` (`payment_number`),
KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_payment');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_payment_item` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`payment_id` int(11) unsigned NOT NULL DEFAULT 0,
`invoice_id` int(11) unsigned NOT NULL DEFAULT 0,
`doc_type` varchar(20) NOT NULL DEFAULT '',
`department_id` int(11) NOT NULL DEFAULT 0,
`amount` decimal(18,4) NOT NULL DEFAULT 0.0000,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `payment_id` (`payment_id`),
KEY `invoice_id` (`invoice_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'td_payment_item');
run_ddl("
CREATE TABLE IF NOT EXISTS `document_types` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`doc_type_key` varchar(50) NOT NULL DEFAULT '',
`name` varchar(100) NOT NULL DEFAULT '',
`prefix` varchar(20) NOT NULL DEFAULT '',
`format_type` enum('YY','YYMM','YYMMDD','none') NOT NULL DEFAULT 'YYMM',
`sequence_digits` tinyint(2) unsigned NOT NULL DEFAULT 5,
`allow_manual` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_company_doctype_prefix` (`company_id`, `doc_type_key`, `prefix`),
KEY `company_id` (`company_id`),
KEY `doc_type_key` (`doc_type_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'document_types');
run_ddl("
CREATE TABLE IF NOT EXISTS `document_number_sequences` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL DEFAULT 0,
`prefix` varchar(20) NOT NULL DEFAULT '',
`format_type` enum('YY','YYMM','YYMMDD','none') NOT NULL DEFAULT 'YYMM',
`year` smallint(4) unsigned NOT NULL DEFAULT 0,
`month` tinyint(2) unsigned NOT NULL DEFAULT 0,
`day` tinyint(2) unsigned NOT NULL DEFAULT 0,
`last_sequence` int(11) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_scope` (`company_id`, `prefix`, `format_type`, `year`, `month`, `day`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'document_number_sequences');
// ── Summary ───────────────────────────────────────────────────────────────────
$total = $ok_count + $skip_count + $err_count;
echo "\n\033[1m=== Done ===\033[0m\n";
printf(" Created : %d\n", $ok_count);
printf(" Skipped : %d\n", $skip_count);
printf(" Errors : %d\n", $err_count);
echo PHP_EOL;
if ($err_count > 0) {
echo "\033[31mSetup completed with errors. Review the output above.\033[0m\n\n";
exit(1);
}
echo "\033[32mSetup complete. Both databases are ready.\033[0m\n";
echo "Next step: visit /login/index.php and register the first owner account.\n\n";
exit(0);