hr dan payroll
This commit is contained in:
@@ -4,6 +4,29 @@ defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
/** Pemeriksaan CLI terarah. Seluruh fixture selalu di-rollback. */
|
||||
class Payrollcheck extends CI_Controller
|
||||
{
|
||||
public function audit()
|
||||
{
|
||||
if(!$this->input->is_cli_request())show_404();
|
||||
$this->load->database();
|
||||
$checks=array(
|
||||
'Baris payroll lintas company / orphan'=>"SELECT COUNT(*) total FROM k_payrolls x LEFT JOIN k_payroll_periods p ON p.id=x.payroll_period_id LEFT JOIN k_employees e ON e.id=x.employee_id WHERE p.id IS NULL OR e.id IS NULL OR p.company_id<>x.company_id OR e.company_id<>x.company_id",
|
||||
'Rincian kalkulasi tanpa header payroll'=>"SELECT COUNT(*) total FROM payroll_calculation_lines l LEFT JOIN k_payrolls x ON x.id=l.payroll_id WHERE x.id IS NULL",
|
||||
'Penyesuaian dengan relasi/jenis tidak konsisten'=>"SELECT COUNT(*) total FROM payroll_period_adjustments a LEFT JOIN k_payroll_periods p ON p.id=a.payroll_period_id LEFT JOIN k_employees e ON e.id=a.employee_id LEFT JOIN payroll_components c ON c.id=a.component_id WHERE p.id IS NULL OR e.id IS NULL OR c.id IS NULL OR p.company_id<>a.company_id OR e.company_id<>a.company_id OR c.company_id<>a.company_id OR a.adjustment_type<>c.component_type",
|
||||
'Periode lanjut proses tanpa hasil hitung'=>"SELECT COUNT(*) total FROM k_payroll_periods WHERE workflow_status IN('reviewed','approved','final','posted','paid') AND (calculated_at IS NULL OR calculation_hash IS NULL)",
|
||||
'Periode posted/paid tanpa jurnal'=>"SELECT COUNT(*) total FROM k_payroll_periods p LEFT JOIN journals j ON j.id=p.journal_id WHERE p.workflow_status IN('posted','paid') AND j.id IS NULL",
|
||||
'Periode paid tanpa pembayaran'=>"SELECT COUNT(*) total FROM k_payroll_periods p LEFT JOIN payroll_payments y ON y.id=p.payment_transaction_id AND y.company_id=p.company_id WHERE p.workflow_status='paid' AND y.id IS NULL",
|
||||
'Rentang assignment karyawan tidak valid'=>"SELECT COUNT(*) total FROM employee_payroll_components WHERE effective_to IS NOT NULL AND effective_to<effective_from",
|
||||
'Rentang template posisi tidak valid'=>"SELECT COUNT(*) total FROM position_payroll_components WHERE effective_to IS NOT NULL AND effective_to<effective_from",
|
||||
'Assignment karyawan aktif saling tumpang tindih'=>"SELECT COUNT(*) total FROM employee_payroll_components a JOIN employee_payroll_components b ON b.id>a.id AND b.company_id=a.company_id AND b.employee_id=a.employee_id AND b.component_id=a.component_id AND b.is_active=1 WHERE a.is_active=1 AND a.effective_from<=COALESCE(b.effective_to,'9999-12-31') AND b.effective_from<=COALESCE(a.effective_to,'9999-12-31')",
|
||||
'Template posisi aktif saling tumpang tindih'=>"SELECT COUNT(*) total FROM position_payroll_components a JOIN position_payroll_components b ON b.id>a.id AND b.company_id=a.company_id AND b.position_id=a.position_id AND b.component_id=a.component_id AND b.is_active=1 WHERE a.is_active=1 AND a.effective_from<=COALESCE(b.effective_to,'9999-12-31') AND b.effective_from<=COALESCE(a.effective_to,'9999-12-31')",
|
||||
'Assignment karyawan lintas company'=>"SELECT COUNT(*) total FROM employee_payroll_components x JOIN k_employees e ON e.id=x.employee_id JOIN payroll_components c ON c.id=x.component_id WHERE e.company_id<>x.company_id OR c.company_id<>x.company_id",
|
||||
'Template posisi lintas company'=>"SELECT COUNT(*) total FROM position_payroll_components x JOIN payroll_components c ON c.id=x.component_id WHERE c.company_id<>x.company_id",
|
||||
'Jurnal payroll tidak balance'=>"SELECT COUNT(*) total FROM (SELECT j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE j.ref_type IN('payroll','payroll_payment') GROUP BY j.id HAVING ABS(SUM(d.debit-d.kredit))>0.01) q"
|
||||
);
|
||||
$issues=0;foreach($checks as$name=>$sql){$count=(int)$this->db->query($sql)->row()->total;echo($count?'ISSUE':'PASS').': '.$name.' = '.$count.PHP_EOL;$issues+=$count;}
|
||||
if($issues){fwrite(STDERR,'FAIL: ditemukan '.$issues.' inkonsistensi payroll. Periksa baris ISSUE sebelum proses berikutnya.'.PHP_EOL);exit(1);}echo"PASS: audit integritas payroll tidak menemukan inkonsistensi.\n";
|
||||
}
|
||||
|
||||
public function exports()
|
||||
{
|
||||
if(!$this->input->is_cli_request())show_404();
|
||||
@@ -35,6 +58,7 @@ class Payrollcheck extends CI_Controller
|
||||
$this->db->insert('k_attendances',array('employee_id'=>$employee->id,'attendance_date'=>'2098-09-01','attendance_status'=>'alpha','late_minutes'=>0,'early_leave_minutes'=>0));
|
||||
$this->db->insert('k_attendances',array('employee_id'=>$employee->id,'attendance_date'=>'2098-09-02','attendance_status'=>'late','late_minutes'=>30,'early_leave_minutes'=>0));
|
||||
$this->db->insert('payroll_attendance_locks',array('company_id'=>$company->id,'payroll_period_id'=>$periodId,'start_date'=>$start,'end_date'=>$end,'status'=>'locked','locked_by'=>$user->id,'locked_at'=>$now));
|
||||
$this->db->where('id',$employee->id)->update('k_employees',array('salary_restricted'=>1));$restrictedBlocked=false;try{$this->payrollservice->calculate($periodId,$user->id,false,$company->id);}catch(Throwable$expected){$restrictedBlocked=true;}if(!$restrictedBlocked)throw new RuntimeException('Perhitungan gaji terbatas tidak diblokir untuk user tanpa Manage All Gaji.');$this->db->where('id',$employee->id)->update('k_employees',array('salary_restricted'=>0,'is_active'=>0,'resign_date'=>'2098-09-01'));
|
||||
$this->payrollservice->calculate($periodId,$user->id,true,$company->id);$first=$this->db->select('COUNT(*) count')->from('payroll_calculation_lines l')->join('k_payrolls p','p.id=l.payroll_id')->where('p.payroll_period_id',$periodId)->get()->row();
|
||||
$this->payrollservice->calculate($periodId,$user->id,true,$company->id);$second=$this->db->select('COUNT(*) count')->from('payroll_calculation_lines l')->join('k_payrolls p','p.id=l.payroll_id')->where('p.payroll_period_id',$periodId)->get()->row();
|
||||
$codes=$this->db->select('c.name,l.amount')->from('payroll_calculation_lines l')->join('k_payrolls p','p.id=l.payroll_id')->join('payroll_components c','c.id=l.component_id')->where('p.payroll_period_id',$periodId)->where_in('c.id',$componentIds)->get()->result();$found=array();foreach($codes as$row)$found[$row->name]=(float)$row->amount;
|
||||
@@ -43,7 +67,7 @@ class Payrollcheck extends CI_Controller
|
||||
$snapshot=$this->db->select('calculation_snapshot')->get_where('k_payrolls',array('payroll_period_id'=>$periodId,'employee_id'=>$employee->id))->row();if(!$snapshot||strpos($snapshot->calculation_snapshot,$start)===false||strpos($snapshot->calculation_snapshot,$end)===false)throw new RuntimeException('Snapshot rentang fleksibel tidak tersimpan.');
|
||||
$late=$this->db->where(array('company_id'=>$company->id,'condition_type'=>'late','calculation_method'=>'none'))->count_all_results('payroll_attendance_rules');if(!$late)throw new RuntimeException('Default terlambat tanpa potongan tidak tersedia.');
|
||||
$beforeJournal=(int)$this->db->count_all('journals');$this->db->where('id',$periodId)->update('k_payroll_periods',array('workflow_status'=>'final'));if((int)$this->db->count_all('journals')!==$beforeJournal)throw new RuntimeException('Finalisasi membuat jurnal sebelum Posting.');$journalId=$this->payrollservice->post($periodId,'2026-09-13',$user->id,$company->id,true);$balance=$this->db->select('SUM(debit-kredit) difference',false)->get_where('journal_details',array('journal_id'=>$journalId))->row();if(!$balance||abs((float)$balance->difference)>.01)throw new RuntimeException('Jurnal payroll tidak balance.');$doubleBlocked=false;try{$this->payrollservice->post($periodId,'2026-09-13',$user->id,$company->id,true);}catch(Throwable$expected){$doubleBlocked=true;}if(!$doubleBlocked)throw new RuntimeException('Double posting tidak diblokir.');
|
||||
echo"PASS: periode 14 Agustus–13 September, assignment, alpha, bonus, snapshot, recalculate, jurnal balance, dan idempotency posting.\n";
|
||||
$cashQuery=$this->db->select('c.*')->from('cash_accounts c')->join('accounts a','a.id=c.gl_account_id')->where(array('c.is_active'=>1,'a.is_active'=>1,'a.allow_posting'=>1,'a.company_id'=>$company->id));if($this->db->field_exists('company_id','cash_accounts'))$cashQuery->where('c.company_id',$company->id);$cash=$cashQuery->order_by('c.id')->get()->row();if($cash){$paymentJournal=$this->payrollservice->pay($periodId,'2026-09-13',$cash->id,$user->id,$company->id,true);$paid=$this->db->get_where('k_payroll_periods',array('id'=>$periodId,'workflow_status'=>'paid'))->row();$payment=$this->db->get_where('payroll_payments',array('company_id'=>$company->id,'payroll_period_id'=>$periodId))->row();$paymentBalance=$this->db->select('SUM(debit-kredit) difference',false)->get_where('journal_details',array('journal_id'=>$paymentJournal))->row();if(!$paid||!$payment||!$paymentBalance||abs((float)$paymentBalance->difference)>.01)throw new RuntimeException('Pembayaran payroll atau jurnal pembayarannya tidak konsisten.');$doublePayBlocked=false;try{$this->payrollservice->pay($periodId,'2026-09-13',$cash->id,$user->id,$company->id,true);}catch(Throwable$expected){$doublePayBlocked=true;}if(!$doublePayBlocked)throw new RuntimeException('Double payment tidak diblokir.');echo"PASS: periode fleksibel, kalkulasi, posting, pembayaran, jurnal balance, dan idempotency tervalidasi.\n";}else echo"PASS: kalkulasi dan posting tervalidasi. SKIP pembayaran: Kas/Bank dengan akun GL aktif belum tersedia.\n";
|
||||
}catch(Throwable$e){$this->db->trans_rollback();fwrite(STDERR,'FAIL: '.$e->getMessage().PHP_EOL);exit(1);}
|
||||
$this->db->trans_rollback();echo"ROLLBACK: seluruh fixture payroll dibatalkan.\n";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user