Files
wms-app/setup.php
T
2026-08-03 11:17:41 +07:00

1236 lines
55 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 '',
`bin_from` varchar(255) NOT NULL DEFAULT '',
`bin_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_bin` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`td_stock_id` int(11) DEFAULT NULL,
`warehouse` int(11) NOT NULL,
`storage_id` int(11) NOT NULL DEFAULT 0,
`zone` varchar(10) NOT NULL,
`aisle` varchar(10) NOT NULL,
`bin` varchar(10) NOT NULL,
`product_sku` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_bin_location` (`company_id`,`warehouse`,`zone`,`aisle`,`bin`),
KEY `idx_storage` (`storage_id`),
KEY `idx_sku` (`company_id`,`product_sku`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'md_bin');
// Column made nullable — WarehouseManager::releaseBin() does `SET td_stock_id =
// NULL` to mark a bin empty (mirroring product_sku on the same row), but the
// column was NOT NULL DEFAULT 0. Under strict SQL mode this made releaseBin()
// fail with "Column 'td_stock_id' cannot be null" on every supplier return,
// every fully-depleted stock-out, and every stock transfer. Every read site
// (getBinStock(), occupyBin()) already treats 0 and NULL as equivalent "empty"
// (`if (!$td_stock_id)`), so this is a safe, behavior-preserving widening.
run($pdo, "ALTER TABLE `md_bin` MODIFY `td_stock_id` int(11) DEFAULT NULL", 'md_bin.td_stock_id nullable');
run($pdo, "
CREATE TABLE IF NOT EXISTS `td_bin_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(10) NOT NULL,
`md_bin_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_bin` (`company_id`,`md_bin_id`),
KEY `idx_user` (`company_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_bin_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,
`bin` 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`,`bin`)
) 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(64) 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 '',
`bin` 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)');
// Column widened for db_auth.php's 32-char $uuid (bin2hex(random_bytes(16))) plus
// manager-appended suffixes (e.g. "_ret_0", "_srn_0", "_3") — varchar(20) was too
// narrow and caused "Data too long for column 'uuid'" on every real stock movement
// (PO receipt, order confirm, return confirm, stock transfer) under strict SQL mode.
run($pdo, "ALTER TABLE `td_stock` MODIFY `uuid` varchar(64) NOT NULL", 'td_stock.uuid width');
// Per-warehouse td_stock_<id> tables are cloned from td_stock at warehouse-creation
// time (WarehouseManager::createStockTableForWarehouse()) and are NOT otherwise
// tracked by this script — widen uuid on every one that already exists so
// previously-created warehouses get the same fix. New warehouses get it for free
// from the template above.
$stockTables = $pdo->query("SHOW TABLES LIKE 'td\\_stock\\_%'")->fetchAll(PDO::FETCH_COLUMN);
foreach ($stockTables as $t) {
run($pdo, "ALTER TABLE `{$t}` MODIFY `uuid` varchar(64) NOT NULL", "{$t}.uuid width");
}
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');
run($pdo, "
CREATE TABLE IF NOT EXISTS `etl_gl_summary` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(11) unsigned NOT NULL DEFAULT 0,
`acc_code` int(11) NOT NULL DEFAULT 0,
`period` varchar(7) NOT NULL,
`debit` decimal(18,4) NOT NULL DEFAULT 0.0000,
`credit` decimal(18,4) NOT NULL DEFAULT 0.0000,
`count` int(11) NOT NULL DEFAULT 0,
`source_updated_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `company_id_acc_code_period` (`company_id`,`acc_code`,`period`),
KEY `company_id` (`company_id`),
KEY `period` (`period`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'etl_gl_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 '',
`formula_id` int(11) DEFAULT NULL,
`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');
// Column added for ReturnManager::saveFormula() / custom credit-note GL formula
// selection — was missing from the original table (unlike td_invoice/td_receipt/
// td_payment, which all have formula_id). Without it, ReturnManager::saveReturn()
// fails on every insert.
run($pdo, "ALTER TABLE `td_return` ADD COLUMN `formula_id` int(11) DEFAULT NULL AFTER `notes`", 'td_return.formula_id');
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 '',
`bin` 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($pdo, "
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($pdo, "
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);