Files
accounting_dev_v2/application/migrations/20260831001000_create_receivable_subledger.php
T
2026-09-11 16:03:00 +07:00

55 lines
9.8 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Migration_Create_receivable_subledger extends CI_Migration
{
private function column($table,$column,$sql){if(!$this->db->field_exists($column,$table))$this->db->query("ALTER TABLE `$table` ADD `$column` $sql");}
public function up()
{
$dup=$this->db->query('SELECT no_invoice FROM invoices GROUP BY no_invoice HAVING COUNT(*)>1 LIMIT 1')->row();
if($dup)throw new RuntimeException('Nomor invoice duplikat: '.$dup->no_invoice.'. Rekonsiliasi sebelum Tahap 5.');
$this->column('customers','credit_limit','DECIMAL(18,2) NOT NULL DEFAULT 0');
$this->column('customers','payment_term_days','INT NOT NULL DEFAULT 30');
$this->column('customers','is_credit_blocked','TINYINT(1) NOT NULL DEFAULT 0');
$this->column('customers','tax_number','VARCHAR(50) NULL');
$this->column('invoices','workflow_status',"ENUM('draft','submitted','approved','posted','cancelled','reversed') NOT NULL DEFAULT 'draft'");
$this->column('invoices','subtotal_before_tax','DECIMAL(18,2) NOT NULL DEFAULT 0');
$this->column('invoices','tax_total','DECIMAL(18,2) NOT NULL DEFAULT 0');
$this->column('invoices','discount_total','DECIMAL(18,2) NOT NULL DEFAULT 0');
$this->column('invoices','currency','CHAR(3) NOT NULL DEFAULT \'IDR\'');
$this->column('invoices','posted_at','DATETIME NULL'); $this->column('invoices','posted_by','INT NULL');
$this->column('invoices','cancelled_at','DATETIME NULL'); $this->column('invoices','cancelled_by','INT NULL');
$this->column('invoices','reversal_journal_id','INT NULL'); $this->column('invoices','version','INT NOT NULL DEFAULT 1');
$this->column('invoice_details','tax_rate','DECIMAL(7,4) NOT NULL DEFAULT 0');
$this->column('invoice_details','tax_amount','DECIMAL(18,2) NOT NULL DEFAULT 0');
$this->column('invoice_details','discount_amount','DECIMAL(18,2) NOT NULL DEFAULT 0');
$this->column('invoice_details','stock_posted_at','DATETIME NULL');
$this->column('invoice_details','stock_log_id','INT NULL');
$indexes=$this->db->query("SHOW INDEX FROM invoices WHERE Key_name='uq_invoices_no_invoice'")->result();
if(!$indexes)$this->db->query('ALTER TABLE invoices ADD UNIQUE KEY uq_invoices_no_invoice(no_invoice)');
$this->db->query("CREATE TABLE IF NOT EXISTS sales_documents(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,document_type ENUM('quotation','sales_order') NOT NULL,document_no VARCHAR(60) NOT NULL,customer_id INT NOT NULL,document_date DATE NOT NULL,valid_until DATE NULL,status ENUM('draft','sent','accepted','rejected','cancelled','converted') NOT NULL DEFAULT 'draft',subtotal DECIMAL(18,2) NOT NULL DEFAULT 0,tax_total DECIMAL(18,2) NOT NULL DEFAULT 0,total DECIMAL(18,2) NOT NULL DEFAULT 0,notes TEXT NULL,converted_invoice_id INT NULL,created_by INT NULL,created_at DATETIME NOT NULL,updated_at DATETIME NULL,UNIQUE KEY uq_sales_document_no(document_no),KEY idx_sales_customer(customer_id,status)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$this->db->query("CREATE TABLE IF NOT EXISTS sales_document_lines(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,sales_document_id BIGINT UNSIGNED NOT NULL,item_id INT NULL,description VARCHAR(255) NOT NULL,qty DECIMAL(18,4) NOT NULL,unit_price DECIMAL(18,2) NOT NULL,discount_amount DECIMAL(18,2) NOT NULL DEFAULT 0,tax_rate DECIMAL(7,4) NOT NULL DEFAULT 0,tax_amount DECIMAL(18,2) NOT NULL DEFAULT 0,line_total DECIMAL(18,2) NOT NULL,CONSTRAINT fk_sales_line_document FOREIGN KEY(sales_document_id) REFERENCES sales_documents(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$this->column('payments','payment_no','VARCHAR(60) NULL'); $this->column('payments','customer_id','INT NULL');
$this->column('payments','status',"ENUM('draft','posted','cancelled','reversed') NOT NULL DEFAULT 'posted'");
$this->column('payments','reference_no','VARCHAR(100) NULL'); $this->column('payments','notes','TEXT NULL');
$this->column('payments','created_by','INT NULL'); $this->column('payments','reversal_journal_id','INT NULL');
$this->db->query("UPDATE payments p JOIN invoices i ON i.id=p.invoice_id SET p.customer_id=i.customer_id WHERE p.customer_id IS NULL");
$this->db->query("UPDATE payments SET payment_no=CONCAT('LEG-PAY-',LPAD(id,8,'0')) WHERE payment_no IS NULL OR payment_no=''");
$idx=$this->db->query("SHOW INDEX FROM payments WHERE Key_name='uq_payments_payment_no'")->result();if(!$idx)$this->db->query('ALTER TABLE payments ADD UNIQUE KEY uq_payments_payment_no(payment_no)');
$this->db->query("CREATE TABLE IF NOT EXISTS payment_allocations(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,payment_id INT NOT NULL,invoice_id INT NOT NULL,amount DECIMAL(18,2) NOT NULL,created_at DATETIME NOT NULL,UNIQUE KEY uq_payment_invoice(payment_id,invoice_id),KEY idx_allocation_invoice(invoice_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$this->db->query("INSERT IGNORE INTO payment_allocations(payment_id,invoice_id,amount,created_at) SELECT id,invoice_id,jumlah,created_at FROM payments WHERE invoice_id IS NOT NULL AND jumlah>0");
$this->db->query("CREATE TABLE IF NOT EXISTS customer_advances(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,advance_no VARCHAR(60) NOT NULL,customer_id INT NOT NULL,payment_id INT NULL,original_amount DECIMAL(18,2) NOT NULL,available_amount DECIMAL(18,2) NOT NULL,status ENUM('available','partially_used','used','refunded') NOT NULL DEFAULT 'available',journal_id INT NULL,created_at DATETIME NOT NULL,UNIQUE KEY uq_advance_no(advance_no),KEY idx_advance_customer(customer_id,status)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$this->db->query("CREATE TABLE IF NOT EXISTS credit_notes(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,credit_note_no VARCHAR(60) NOT NULL,invoice_id INT NOT NULL,customer_id INT NOT NULL,credit_date DATE NOT NULL,amount DECIMAL(18,2) NOT NULL,reason TEXT NOT NULL,status ENUM('draft','approved','posted','cancelled','reversed') NOT NULL DEFAULT 'draft',journal_id INT NULL,reversal_journal_id INT NULL,created_by INT NULL,created_at DATETIME NOT NULL,UNIQUE KEY uq_credit_note_no(credit_note_no),KEY idx_credit_invoice(invoice_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$this->db->query("CREATE TABLE IF NOT EXISTS customer_refunds(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,refund_no VARCHAR(60) NOT NULL,customer_id INT NOT NULL,advance_id BIGINT UNSIGNED NULL,credit_note_id BIGINT UNSIGNED NULL,refund_date DATE NOT NULL,amount DECIMAL(18,2) NOT NULL,bank_account_id INT NOT NULL,reason TEXT NOT NULL,status ENUM('draft','posted','cancelled','reversed') NOT NULL DEFAULT 'draft',journal_id INT NULL,created_by INT NULL,created_at DATETIME NOT NULL,UNIQUE KEY uq_refund_no(refund_no)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$this->db->query("CREATE TABLE IF NOT EXISTS receivable_writeoffs(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,writeoff_no VARCHAR(60) NOT NULL,invoice_id INT NOT NULL,writeoff_date DATE NOT NULL,amount DECIMAL(18,2) NOT NULL,reason TEXT NOT NULL,status ENUM('draft','approved','posted','reversed') NOT NULL DEFAULT 'draft',journal_id INT NULL,created_by INT NULL,created_at DATETIME NOT NULL,UNIQUE KEY uq_writeoff_no(writeoff_no)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$this->db->query("CREATE TABLE IF NOT EXISTS receivable_reminders(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,invoice_id INT NOT NULL,channel ENUM('email','whatsapp','phone','manual') NOT NULL,recipient VARCHAR(150) NULL,message TEXT NULL,status ENUM('queued','sent','failed','cancelled') NOT NULL DEFAULT 'queued',scheduled_at DATETIME NULL,sent_at DATETIME NULL,created_by INT NULL,created_at DATETIME NOT NULL,KEY idx_reminder_queue(status,scheduled_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$this->db->query("CREATE TABLE IF NOT EXISTS transaction_attachments(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,module VARCHAR(50) NOT NULL,entity_type VARCHAR(50) NOT NULL,entity_id BIGINT UNSIGNED NOT NULL,original_name VARCHAR(255) NOT NULL,stored_name VARCHAR(255) NOT NULL,mime_type VARCHAR(100) NOT NULL,file_size BIGINT UNSIGNED NOT NULL,sha256 CHAR(64) NOT NULL,uploaded_by INT NULL,created_at DATETIME NOT NULL,KEY idx_attachment_entity(entity_type,entity_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$this->db->query("UPDATE invoices SET workflow_status=CASE WHEN status='draft' THEN 'draft' ELSE 'posted' END, subtotal_before_tax=total, posted_at=CASE WHEN status<>'draft' THEN created_at ELSE NULL END WHERE workflow_status='draft'");
$this->db->query("UPDATE invoice_details d SET stock_posted_at=COALESCE((SELECT MIN(s.created_at) FROM stock_logs s WHERE s.ref_type='invoice_details' AND s.ref_id=d.id),stock_posted_at),stock_log_id=COALESCE((SELECT MIN(s.id) FROM stock_logs s WHERE s.ref_type='invoice_details' AND s.ref_id=d.id),stock_log_id)");
$this->db->query("INSERT IGNORE INTO approval_workflows(code,module,entity_type,name,require_separation,is_active,created_at) VALUES('INVOICE_DEFAULT','invoice','invoice','Persetujuan Invoice',1,1,NOW()),('CREDIT_NOTE_DEFAULT','receivable','credit_note','Persetujuan Credit Note',1,1,NOW()),('WRITEOFF_DEFAULT','receivable','writeoff','Persetujuan Write-off Piutang',1,1,NOW())");
$admin=$this->db->where('nama_role','Admin')->get('roles')->row();if($admin){foreach(array('INVOICE_DEFAULT','CREDIT_NOTE_DEFAULT','WRITEOFF_DEFAULT') as $code){$w=$this->db->get_where('approval_workflows',array('code'=>$code))->row();if($w&&!$this->db->where('workflow_id',$w->id)->count_all_results('approval_workflow_steps'))$this->db->insert('approval_workflow_steps',array('workflow_id'=>$w->id,'step_order'=>1,'name'=>'Persetujuan Admin','approver_role_id'=>$admin->id));}}
}
public function down(){throw new RuntimeException('Migration Tahap 5 tidak mendukung down otomatis karena memuat backfill subledger. Gunakan backup.');}
}