169 lines
10 KiB
PHP
169 lines
10 KiB
PHP
<?php
|
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
|
|
|
/**
|
|
* Finalisasi fondasi Persediaan.
|
|
*
|
|
* Migration bersifat additive. Data transaksi dan ledger lama tidak dihapus
|
|
* agar seluruh perubahan tetap dapat diaudit ketika diterapkan di production.
|
|
*/
|
|
class Migration_Finalize_professional_inventory extends CI_Migration
|
|
{
|
|
public function up()
|
|
{
|
|
$this->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.');
|
|
}
|
|
}
|