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=== brnwms 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_ 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_ 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, `zone` varchar(255) NOT NULL DEFAULT '', `aisle` varchar(255) NOT NULL DEFAULT '', `bin` varchar(255) NOT NULL DEFAULT '', 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'); // Put-away location of each returned line. The Customer Return form has always // asked for it and ReturnManager::confirmReturn() has always read it, but the // table had nowhere to keep it: the choice was dropped on save and every // confirmation then failed in WarehouseManager::occupyBin() with // "Bin -- does not exist". Same widths as td_stock's location columns. run($pdo, "ALTER TABLE `td_return_item` ADD COLUMN `zone` varchar(255) NOT NULL DEFAULT '' AFTER `stock_out_warehouse_id`", 'td_return_item.zone'); run($pdo, "ALTER TABLE `td_return_item` ADD COLUMN `aisle` varchar(255) NOT NULL DEFAULT '' AFTER `zone`", 'td_return_item.aisle'); run($pdo, "ALTER TABLE `td_return_item` ADD COLUMN `bin` varchar(255) NOT NULL DEFAULT '' AFTER `aisle`", 'td_return_item.bin'); 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'); // ── utf8mb4 ─────────────────────────────────────────────────────────────────── // Older tables were created as utf8mb3, which cannot hold 4-byte characters // (emoji, some CJK). With the connection on utf8mb4 a note containing one was // rejected by the server and the whole save rolled back. Convert whatever is // still utf8mb3, including the per-warehouse td_stock_ tables, to the // collation the existing utf8mb4 tables already use so joins keep working. foreach ([$db1, $db2] as $db_name) { try { $st = $pdo->prepare("SELECT TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE' AND TABLE_COLLATION LIKE 'utf8mb4\\_%' GROUP BY TABLE_COLLATION ORDER BY COUNT(*) DESC LIMIT 1"); $st->execute([$db2]); $target = $st->fetchColumn(); if (!$target) { $target = $pdo->query("SELECT COLLATION_NAME FROM information_schema.COLLATIONS WHERE CHARACTER_SET_NAME = 'utf8mb4' AND IS_DEFAULT = 'Yes' LIMIT 1")->fetchColumn() ?: 'utf8mb4_general_ci'; } if (!preg_match('/^utf8mb4_[a-z0-9_]+$/', $target)) $target = 'utf8mb4_general_ci'; $st = $pdo->prepare("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE' AND (TABLE_COLLATION LIKE 'utf8mb3\\_%' OR TABLE_COLLATION LIKE 'utf8\\_%') ORDER BY TABLE_NAME"); $st->execute([$db_name]); $legacy = $st->fetchAll(PDO::FETCH_COLUMN); if (!$legacy) { skip("utf8mb4: `{$db_name}` (already converted)"); } foreach ($legacy as $tbl) { run($pdo, "ALTER TABLE `{$db_name}`.`{$tbl}` CONVERT TO CHARACTER SET utf8mb4 COLLATE {$target}", "utf8mb4: {$db_name}.{$tbl}"); } run($pdo, "ALTER DATABASE `{$db_name}` CHARACTER SET utf8mb4 COLLATE {$target}", "utf8mb4: database `{$db_name}` default"); } catch (PDOException $e) { fail("utf8mb4 conversion of `{$db_name}`: " . $e->getMessage()); } } $pdo->exec("USE `{$db2}`"); // td_purchase_order totals were decimal(15,2) while its lines are decimal(18,4); // match the other document headers. foreach (['subtotal', 'discount', 'tax', 'shipping_fee', 'grand_total'] as $col) { run($pdo, "ALTER TABLE `{$db2}`.`td_purchase_order` MODIFY `{$col}` decimal(18,4) NOT NULL DEFAULT 0.0000", "td_purchase_order.{$col} decimal(18,4)"); } // ── 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);