upgradeDocuments(); $this->upgradeMasters(); $this->upgradeReservations(); $this->createHistoryTables(); $this->addPerformanceIndexes(); $this->backfillCompany(); } private function addColumn($table, $column, $definition) { // Do not use CI's schema cache during a long migration. A preceding // ALTER can otherwise leave table_exists/field_exists stale. if ($this->tableExists($table) && !$this->fieldExists($table, $column)) { $this->db->query("ALTER TABLE `{$table}` ADD `{$column}` {$definition}"); } } private function addIndex($table, $name, array $columns, $unique = false) { if (!$this->tableExists($table)) return; foreach ($columns as $column) if (!$this->fieldExists($table, $column)) return; $exists = $this->db->query( 'SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? AND index_name=? LIMIT 1', array($table, $name) )->row(); if (!$exists) { $quoted = '`'.implode('`,`', $columns).'`'; $this->db->query('ALTER TABLE `'.$table.'` ADD '.($unique ? 'UNIQUE ' : '').'KEY `'.$name.'` ('.$quoted.')'); } } private function tableExists($table) { return (bool) $this->db->query( 'SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=? LIMIT 1', array($table) )->row(); } private function fieldExists($table, $column) { return (bool) $this->db->query( 'SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name=? AND column_name=? LIMIT 1', array($table, $column) )->row(); } private function upgradeDocuments() { $this->addColumn('stock_documents', 'company_id', 'BIGINT UNSIGNED NULL AFTER `id`'); $this->addColumn('stock_documents', 'source_type', 'VARCHAR(50) NULL AFTER `reason`'); $this->addColumn('stock_documents', 'source_id', 'BIGINT UNSIGNED NULL AFTER `source_type`'); $this->addColumn('stock_documents', 'submitted_by', 'INT NULL AFTER `created_by`'); $this->addColumn('stock_documents', 'submitted_at', 'DATETIME NULL AFTER `submitted_by`'); $this->addColumn('stock_documents', 'rejected_by', 'INT NULL AFTER `approved_at`'); $this->addColumn('stock_documents', 'rejected_at', 'DATETIME NULL AFTER `rejected_by`'); $this->addColumn('stock_documents', 'rejection_reason', 'TEXT NULL AFTER `rejected_at`'); $this->addColumn('stock_documents', 'reversal_of_id', 'BIGINT UNSIGNED NULL AFTER `rejection_reason`'); $this->addColumn('stock_documents', 'reversed_by', 'INT NULL AFTER `posted_at`'); $this->addColumn('stock_documents', 'reversed_at', 'DATETIME NULL AFTER `reversed_by`'); $this->addColumn('stock_documents', 'updated_by', 'INT NULL AFTER `created_at`'); $this->addColumn('stock_documents', 'updated_at', 'DATETIME NULL AFTER `updated_by`'); $this->addColumn('stock_document_lines', 'line_no', 'INT NOT NULL DEFAULT 1 AFTER `stock_document_id`'); $this->addColumn('stock_document_lines', 'movement_direction', "ENUM('in','out') NULL AFTER `qty`"); $this->addColumn('stock_document_lines', 'source_line_type', 'VARCHAR(50) NULL AFTER `reason`'); $this->addColumn('stock_document_lines', 'source_line_id', 'BIGINT UNSIGNED NULL AFTER `source_line_type`'); } private function upgradeMasters() { $this->addColumn('warehouses', 'code', 'VARCHAR(30) NULL AFTER `company_id`'); $this->addColumn('warehouses', 'address', 'TEXT NULL AFTER `nama`'); $this->addColumn('warehouses', 'is_active', 'TINYINT(1) NOT NULL DEFAULT 1 AFTER `address`'); $this->addColumn('warehouse_bins', 'company_id', 'BIGINT UNSIGNED NULL AFTER `id`'); $this->addColumn('warehouse_bins', 'updated_at', 'DATETIME NULL AFTER `is_active`'); $this->addColumn('inventory_batches', 'company_id', 'BIGINT UNSIGNED NULL AFTER `id`'); $this->addColumn('inventory_batches', 'is_active', 'TINYINT(1) NOT NULL DEFAULT 1 AFTER `supplier_id`'); $this->addColumn('inventory_batches', 'updated_at', 'DATETIME NULL AFTER `created_at`'); $this->addColumn('kode_barang', 'company_id', 'BIGINT UNSIGNED NULL AFTER `id`'); $this->addColumn('kode_barang', 'is_active', 'TINYINT(1) NOT NULL DEFAULT 1'); } private function upgradeReservations() { $this->addColumn('stock_reservations', 'company_id', 'BIGINT UNSIGNED NULL AFTER `id`'); $this->addColumn('stock_reservations', 'notes', 'TEXT NULL AFTER `expires_at`'); $this->addColumn('stock_reservations', 'updated_by', 'INT NULL AFTER `created_by`'); $this->addColumn('stock_reservations', 'updated_at', 'DATETIME NULL AFTER `created_at`'); $this->addColumn('stock_reservations', 'completed_at', 'DATETIME NULL AFTER `updated_at`'); $this->addColumn('inventory_reconciliations', 'company_id', 'BIGINT UNSIGNED NULL AFTER `id`'); $this->addColumn('inventory_reconciliations', 'stock_log_value', 'DECIMAL(18,2) NULL AFTER `ledger_value`'); $this->addColumn('inventory_reconciliations', 'barcode_variance_count', 'INT NOT NULL DEFAULT 0 AFTER `difference`'); $this->addColumn('inventory_reconciliations', 'cache_variance_count', 'INT NOT NULL DEFAULT 0 AFTER `barcode_variance_count`'); } private function createHistoryTables() { $this->db->query("CREATE TABLE IF NOT EXISTS stock_document_status_history( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, stock_document_id BIGINT UNSIGNED NOT NULL, from_status VARCHAR(30) NULL, to_status VARCHAR(30) NOT NULL, notes TEXT NULL, user_id INT NULL, created_at DATETIME NOT NULL, KEY idx_stock_document_history(stock_document_id,created_at), CONSTRAINT fk_stock_document_history FOREIGN KEY(stock_document_id) REFERENCES stock_documents(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $this->db->query("CREATE TABLE IF NOT EXISTS stock_reservation_status_history( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, stock_reservation_id BIGINT UNSIGNED NOT NULL, from_status VARCHAR(30) NULL, to_status VARCHAR(30) NOT NULL, qty DECIMAL(18,4) NOT NULL DEFAULT 0, notes TEXT NULL, user_id INT NULL, created_at DATETIME NOT NULL, KEY idx_stock_reservation_history(stock_reservation_id,created_at), CONSTRAINT fk_stock_reservation_history FOREIGN KEY(stock_reservation_id) REFERENCES stock_reservations(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); } private function addPerformanceIndexes() { $this->addIndex('stock_documents', 'idx_stock_doc_company_action', array('company_id','status','document_date','id')); $this->addIndex('stock_documents', 'idx_stock_doc_source', array('company_id','source_type','source_id')); $this->addIndex('stock_documents', 'idx_stock_doc_reversal', array('reversal_of_id')); $this->addIndex('stock_document_lines', 'idx_stock_doc_line_item', array('stock_document_id','item_id','id')); $this->addIndex('stock_reservations', 'idx_stock_reservation_action', array('company_id','status','expires_at','id')); $this->addIndex('warehouse_bins', 'idx_warehouse_bin_active', array('company_id','warehouse_id','is_active','code')); $this->addIndex('inventory_batches', 'idx_inventory_batch_active', array('company_id','item_id','is_active','expiry_date')); $this->addIndex('item_barcodes', 'idx_item_barcode_lookup', array('item_id','warehouse_id','status','id')); $this->addIndex('stock_logs', 'idx_stock_log_item_wh_ref', array('item_id','warehouse_id','ref_type','ref_id')); $this->addIndex('transaction_attachments', 'idx_attachment_module_entity', array('module','entity_type','entity_id','id')); $this->addIndex('kode_barang', 'idx_item_code_company_active', array('company_id','is_active','kode_barang')); } private function backfillCompany() { $fallback = $this->db->select_min('id')->get('companies')->row(); $companyId = $fallback ? (int) $fallback->id : 1; $this->db->query("UPDATE warehouse_bins b JOIN warehouses w ON w.id=b.warehouse_id SET b.company_id=COALESCE(b.company_id,w.company_id,?) WHERE b.company_id IS NULL", array($companyId)); $this->db->query("UPDATE inventory_batches b JOIN items i ON i.id=b.item_id SET b.company_id=COALESCE(b.company_id,i.company_id,?) WHERE b.company_id IS NULL", array($companyId)); $this->db->query("UPDATE stock_reservations r JOIN items i ON i.id=r.item_id SET r.company_id=COALESCE(r.company_id,i.company_id,?) WHERE r.company_id IS NULL", array($companyId)); $this->db->query("UPDATE stock_documents d LEFT JOIN stock_document_lines l ON l.stock_document_id=d.id LEFT JOIN items i ON i.id=l.item_id SET d.company_id=COALESCE(d.company_id,i.company_id,?) WHERE d.company_id IS NULL", array($companyId)); $this->db->query("UPDATE inventory_reconciliations SET company_id=? WHERE company_id IS NULL", array($companyId)); $this->db->query("UPDATE kode_barang k LEFT JOIN items i ON i.kode_id=k.id SET k.company_id=COALESCE(k.company_id,i.company_id,?) WHERE k.company_id IS NULL", array($companyId)); $this->db->query("UPDATE warehouses SET code=CONCAT('GD-',LPAD(id,3,'0')) WHERE code IS NULL OR code='' "); } public function down() { throw new RuntimeException('Rollback finalisasi Persediaan wajib menggunakan backup database.'); } }