update HR dan Payroll
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Migration_Finalize_flexible_payroll extends CI_Migration
|
||||
{
|
||||
private function column($table, $column, $definition)
|
||||
{
|
||||
if ($this->db->table_exists($table) && !$this->db->field_exists($column, $table)) {
|
||||
$this->db->query("ALTER TABLE `{$table}` ADD `{$column}` {$definition}");
|
||||
}
|
||||
}
|
||||
|
||||
private function indexExists($table, $index)
|
||||
{
|
||||
return (bool) $this->db->query("SHOW INDEX FROM `{$table}` WHERE Key_name=?", array($index))->row();
|
||||
}
|
||||
|
||||
private function index($table, $name, $columns, $unique = false)
|
||||
{
|
||||
if ($this->db->table_exists($table) && !$this->indexExists($table, $name)) {
|
||||
$this->db->query('ALTER TABLE `'.$table.'` ADD '.($unique ? 'UNIQUE ' : '').'KEY `'.$name.'` ('.$columns.')');
|
||||
}
|
||||
}
|
||||
|
||||
public function up()
|
||||
{
|
||||
foreach (array(
|
||||
'attendance_start_date'=>'DATE NULL AFTER `end_date`',
|
||||
'attendance_end_date'=>'DATE NULL AFTER `attendance_start_date`',
|
||||
'payment_date'=>'DATE NULL AFTER `attendance_end_date`',
|
||||
'notes'=>'TEXT NULL AFTER `payment_date`',
|
||||
'attendance_locked_by'=>'INT NULL AFTER `notes`',
|
||||
'attendance_locked_at'=>'DATETIME NULL AFTER `attendance_locked_by`',
|
||||
'calculated_by'=>'INT NULL AFTER `attendance_locked_at`',
|
||||
'calculated_at'=>'DATETIME NULL AFTER `calculated_by`',
|
||||
'calculation_version'=>'INT NOT NULL DEFAULT 0 AFTER `calculated_at`',
|
||||
'updated_at'=>'DATETIME NULL AFTER `created_at`'
|
||||
) as $column=>$definition) $this->column('k_payroll_periods', $column, $definition);
|
||||
|
||||
$this->db->query("UPDATE k_payroll_periods SET attendance_start_date=COALESCE(attendance_start_date,start_date), attendance_end_date=COALESCE(attendance_end_date,end_date), payment_date=COALESCE(payment_date,end_date) WHERE attendance_start_date IS NULL OR attendance_end_date IS NULL OR payment_date IS NULL");
|
||||
$this->index('k_payroll_periods', 'idx_payroll_period_range', '`company_id`,`attendance_start_date`,`attendance_end_date`,`workflow_status`');
|
||||
|
||||
foreach (array(
|
||||
'period_start_date'=>'DATE NULL AFTER `calculation_snapshot`',
|
||||
'period_end_date'=>'DATE NULL AFTER `period_start_date`',
|
||||
'structure_version'=>'INT NOT NULL DEFAULT 1 AFTER `period_end_date`'
|
||||
) as $column=>$definition) $this->column('k_payrolls', $column, $definition);
|
||||
$this->db->query("UPDATE k_payrolls x JOIN k_payroll_periods p ON p.id=x.payroll_period_id SET x.period_start_date=COALESCE(x.period_start_date,p.attendance_start_date,p.start_date),x.period_end_date=COALESCE(x.period_end_date,p.attendance_end_date,p.end_date)");
|
||||
$this->index('k_payrolls', 'idx_payroll_company_period_employee', '`company_id`,`payroll_period_id`,`employee_id`');
|
||||
|
||||
foreach (array(
|
||||
'company_id'=>'BIGINT UNSIGNED NULL FIRST',
|
||||
'calculation_method'=>"VARCHAR(40) NOT NULL DEFAULT 'fixed' AFTER `category`",
|
||||
'default_value'=>'DECIMAL(18,2) NULL AFTER `fixed_amount`',
|
||||
'effective_from'=>'DATE NULL AFTER `is_active`',
|
||||
'effective_to'=>'DATE NULL AFTER `effective_from`',
|
||||
'created_by'=>'INT NULL AFTER `effective_to`',
|
||||
'created_at'=>'DATETIME NULL AFTER `created_by`',
|
||||
'updated_at'=>'DATETIME NULL AFTER `created_at`'
|
||||
) as $column=>$definition) $this->column('payroll_components', $column, $definition);
|
||||
$firstCompany = $this->db->select_min('id', 'id')->get('companies')->row();
|
||||
$firstCompanyId = $firstCompany ? (int) $firstCompany->id : 1;
|
||||
$this->db->query("UPDATE payroll_components SET company_id=? WHERE company_id IS NULL", array($firstCompanyId));
|
||||
if ($this->indexExists('payroll_components', 'uq_payroll_component')) {
|
||||
$this->db->query('ALTER TABLE payroll_components DROP INDEX uq_payroll_component');
|
||||
}
|
||||
$this->index('payroll_components', 'uq_payroll_component_company', '`company_id`,`code`', true);
|
||||
$this->index('payroll_components', 'idx_payroll_component_effective', '`company_id`,`is_active`,`effective_from`,`effective_to`');
|
||||
$this->db->query("UPDATE payroll_components SET default_value=COALESCE(default_value,fixed_amount),created_at=COALESCE(created_at,NOW())");
|
||||
|
||||
foreach (array(
|
||||
'company_id'=>'BIGINT UNSIGNED NULL FIRST',
|
||||
'calculation_method'=>"VARCHAR(40) NOT NULL DEFAULT 'fixed' AFTER `component_id`",
|
||||
'formula_expression'=>'VARCHAR(500) NULL AFTER `amount_override`',
|
||||
'notes'=>'TEXT NULL AFTER `is_active`',
|
||||
'created_by'=>'INT NULL AFTER `notes`',
|
||||
'created_at'=>'DATETIME NULL AFTER `created_by`',
|
||||
'updated_at'=>'DATETIME NULL AFTER `created_at`'
|
||||
) as $column=>$definition) $this->column('employee_payroll_components', $column, $definition);
|
||||
$this->db->query("UPDATE employee_payroll_components x JOIN k_employees e ON e.id=x.employee_id SET x.company_id=e.company_id,x.created_at=COALESCE(x.created_at,NOW()) WHERE x.company_id IS NULL OR x.created_at IS NULL");
|
||||
$this->index('employee_payroll_components', 'idx_employee_component_scope', '`company_id`,`employee_id`,`component_id`,`is_active`');
|
||||
$this->index('employee_payroll_components', 'idx_employee_component_effective', '`company_id`,`effective_from`,`effective_to`');
|
||||
|
||||
$this->db->query("CREATE TABLE IF NOT EXISTS `position_payroll_components` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`company_id` BIGINT UNSIGNED NOT NULL,
|
||||
`position_id` BIGINT NOT NULL,
|
||||
`component_id` BIGINT UNSIGNED NOT NULL,
|
||||
`calculation_method` VARCHAR(40) NOT NULL DEFAULT 'fixed',
|
||||
`amount_override` DECIMAL(18,2) NULL,
|
||||
`formula_expression` VARCHAR(500) NULL,
|
||||
`effective_from` DATE NOT NULL,
|
||||
`effective_to` DATE NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`notes` TEXT NULL,
|
||||
`created_by` INT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_position_component_version` (`company_id`,`position_id`,`component_id`,`effective_from`),
|
||||
KEY `idx_position_component_effective` (`company_id`,`position_id`,`is_active`,`effective_from`,`effective_to`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
|
||||
$this->db->query("CREATE TABLE IF NOT EXISTS `payroll_attendance_rules` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`company_id` BIGINT UNSIGNED NOT NULL,
|
||||
`component_id` BIGINT UNSIGNED NULL,
|
||||
`rule_name` VARCHAR(120) NOT NULL,
|
||||
`condition_type` VARCHAR(40) NOT NULL,
|
||||
`calculation_method` VARCHAR(40) NOT NULL DEFAULT 'none',
|
||||
`tolerance_minutes` INT NOT NULL DEFAULT 0,
|
||||
`rate_value` DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
`maximum_amount` DECIMAL(18,2) NULL,
|
||||
`formula_expression` VARCHAR(500) NULL,
|
||||
`effective_from` DATE NOT NULL,
|
||||
`effective_to` DATE NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`notes` TEXT NULL,
|
||||
`created_by` INT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_attendance_rule_scope` (`company_id`,`condition_type`,`is_active`),
|
||||
KEY `idx_attendance_rule_effective` (`company_id`,`effective_from`,`effective_to`),
|
||||
KEY `idx_attendance_rule_component` (`component_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
|
||||
$this->db->query("CREATE TABLE IF NOT EXISTS `payroll_attendance_locks` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`company_id` BIGINT UNSIGNED NOT NULL,
|
||||
`payroll_period_id` BIGINT NOT NULL,
|
||||
`start_date` DATE NOT NULL,
|
||||
`end_date` DATE NOT NULL,
|
||||
`status` ENUM('open','locked') NOT NULL DEFAULT 'open',
|
||||
`locked_by` INT NULL,
|
||||
`locked_at` DATETIME NULL,
|
||||
`notes` TEXT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_payroll_attendance_lock_period` (`company_id`,`payroll_period_id`),
|
||||
KEY `idx_payroll_attendance_lock_range` (`company_id`,`status`,`start_date`,`end_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
|
||||
$this->db->query("CREATE TABLE IF NOT EXISTS `payroll_period_adjustments` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`company_id` BIGINT UNSIGNED NOT NULL,
|
||||
`payroll_period_id` BIGINT NOT NULL,
|
||||
`employee_id` BIGINT NOT NULL,
|
||||
`component_id` BIGINT UNSIGNED NOT NULL,
|
||||
`adjustment_type` ENUM('earning','deduction') NOT NULL,
|
||||
`amount` DECIMAL(18,2) NOT NULL,
|
||||
`notes` TEXT NOT NULL,
|
||||
`attachment_path` VARCHAR(255) NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_by` INT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_by` INT NULL,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_period_adjustment_scope` (`company_id`,`payroll_period_id`,`employee_id`,`is_active`),
|
||||
KEY `idx_period_adjustment_component` (`component_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
|
||||
foreach (array(
|
||||
'calculation_method'=>"VARCHAR(40) NOT NULL DEFAULT 'fixed' AFTER `component_type`",
|
||||
'source_snapshot'=>'LONGTEXT NULL AFTER `formula_snapshot`',
|
||||
'attendance_rule_snapshot'=>'LONGTEXT NULL AFTER `source_snapshot`'
|
||||
) as $column=>$definition) $this->column('payroll_calculation_lines', $column, $definition);
|
||||
$this->index('payroll_calculation_lines', 'idx_payroll_line_component', '`component_id`,`component_type`');
|
||||
|
||||
$this->column('payroll_payments', 'company_id', 'BIGINT UNSIGNED NULL FIRST');
|
||||
$this->db->query("UPDATE payroll_payments x JOIN k_payroll_periods p ON p.id=x.payroll_period_id SET x.company_id=p.company_id WHERE x.company_id IS NULL");
|
||||
$this->index('payroll_payments', 'idx_payroll_payment_company_status', '`company_id`,`status`,`payment_date`');
|
||||
|
||||
// Migrasi satu kali: gaji pokok lama hanya menjadi assignment bila karyawan belum punya struktur gaji.
|
||||
$this->db->query("INSERT INTO employee_payroll_components
|
||||
(company_id,employee_id,component_id,calculation_method,amount_override,effective_from,effective_to,is_active,notes,created_at)
|
||||
SELECT e.company_id,e.id,c.id,'fixed',GREATEST(COALESCE(e.basic_salary,0),0),COALESCE(e.join_date,'2000-01-01'),NULL,1,'Migrasi aman dari k_employees.basic_salary',NOW()
|
||||
FROM k_employees e
|
||||
JOIN payroll_components c ON c.company_id=e.company_id AND c.code='BASIC'
|
||||
WHERE e.company_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM employee_payroll_components x WHERE x.company_id=e.company_id AND x.employee_id=e.id)");
|
||||
|
||||
// Keterlambatan aman secara default: dicatat tetapi tidak otomatis memotong gaji.
|
||||
$this->db->query("INSERT INTO payroll_attendance_rules
|
||||
(company_id,component_id,rule_name,condition_type,calculation_method,tolerance_minutes,rate_value,effective_from,is_active,notes,created_at)
|
||||
SELECT c.id,NULL,'Keterlambatan tanpa potongan','late','none',0,0,'2000-01-01',1,'Default: keterlambatan tidak dipotong sampai aturan diubah.',NOW()
|
||||
FROM companies c
|
||||
WHERE NOT EXISTS (SELECT 1 FROM payroll_attendance_rules r WHERE r.company_id=c.id AND r.condition_type='late')");
|
||||
|
||||
// Trigger lama tetap dipertahankan sebagai kompatibilitas; rentang fleksibel ditambahkan.
|
||||
$rangeConditionNew = "EXISTS(SELECT 1 FROM payroll_attendance_locks l JOIN k_employees e ON e.id=NEW.employee_id WHERE l.company_id=e.company_id AND l.status='locked' AND NEW.attendance_date BETWEEN l.start_date AND l.end_date)";
|
||||
$rangeConditionOld = "EXISTS(SELECT 1 FROM payroll_attendance_locks l JOIN k_employees e ON e.id=OLD.employee_id WHERE l.company_id=e.company_id AND l.status='locked' AND OLD.attendance_date BETWEEN l.start_date AND l.end_date)";
|
||||
$this->db->query('DROP TRIGGER IF EXISTS trg_attendance_locked_insert');
|
||||
$this->db->query("CREATE TRIGGER trg_attendance_locked_insert BEFORE INSERT ON k_attendances FOR EACH ROW BEGIN IF {$rangeConditionNew} OR EXISTS(SELECT 1 FROM attendance_period_locks WHERE period=DATE_FORMAT(NEW.attendance_date,'%Y-%m') AND status='locked' AND branch_id IS NULL) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Attendance period is locked'; END IF; END");
|
||||
$this->db->query('DROP TRIGGER IF EXISTS trg_attendance_locked_update');
|
||||
$this->db->query("CREATE TRIGGER trg_attendance_locked_update BEFORE UPDATE ON k_attendances FOR EACH ROW BEGIN IF {$rangeConditionOld} OR EXISTS(SELECT 1 FROM attendance_period_locks WHERE period=DATE_FORMAT(OLD.attendance_date,'%Y-%m') AND status='locked' AND branch_id IS NULL) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Attendance period is locked'; END IF; END");
|
||||
$this->db->query('DROP TRIGGER IF EXISTS trg_attendance_locked_delete');
|
||||
$this->db->query("CREATE TRIGGER trg_attendance_locked_delete BEFORE DELETE ON k_attendances FOR EACH ROW BEGIN IF {$rangeConditionOld} OR EXISTS(SELECT 1 FROM attendance_period_locks WHERE period=DATE_FORMAT(OLD.attendance_date,'%Y-%m') AND status='locked' AND branch_id IS NULL) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Attendance period is locked'; END IF; END");
|
||||
|
||||
// Kolom lama sengaja tidak dihapus. k_employees.basic_salary hanya compatibility mirror/read-only fallback.
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
throw new RuntimeException('Migration payroll bersifat data-safe; gunakan backup untuk rollback struktur.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user