update HR dan Payroll

This commit is contained in:
Wian Drs
2026-09-14 11:14:25 +07:00
parent ec94783db4
commit 63993ee76e
50 changed files with 2432 additions and 1473 deletions
+50
View File
@@ -0,0 +1,50 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
/** Pemeriksaan CLI terarah. Seluruh fixture selalu di-rollback. */
class Payrollcheck extends CI_Controller
{
public function exports()
{
if(!$this->input->is_cli_request())show_404();
$this->load->database();if(is_file(FCPATH.'vendor/autoload.php'))require_once FCPATH.'vendor/autoload.php';
$period=$this->db->order_by('id')->get('k_payroll_periods')->row();if(!$period){echo"SKIP: periode payroll belum tersedia.\n";return;}
$company=$this->db->get_where('companies',array('id'=>$period->company_id))->row();$rows=$this->db->select('x.*,e.employee_code,e.full_name,e.bank_name,e.bank_account_number')->from('k_payrolls x')->join('k_employees e','e.id=x.employee_id')->where('x.payroll_period_id',$period->id)->limit(3)->get()->result();
$html=$this->load->view('hr_payroll/register_pdf',array('period'=>$period,'rows'=>$rows,'company'=>$company),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();if(strlen($pdf->output())<500)throw new RuntimeException('Render PDF payroll gagal.');
$book=new PhpOffice\PhpSpreadsheet\Spreadsheet();$book->getActiveSheet()->fromArray(array(array('Kode','Nama','Neto'),array($rows?$rows[0]->employee_code:'-',$rows?$rows[0]->full_name:'-', $rows?(float)$rows[0]->total_salary:0)),null,'A1');$file=tempnam(sys_get_temp_dir(),'payroll-check-').'.xlsx';(new PhpOffice\PhpSpreadsheet\Writer\Xlsx($book))->save($file);if(!is_file($file)||filesize($file)<1000)throw new RuntimeException('Render XLSX payroll gagal.');unlink($file);echo"PASS: satu PDF register dan satu XLSX payroll berhasil dirender; file uji dihapus.\n";
}
public function index()
{
if(!$this->input->is_cli_request())show_404();
$this->load->database();$this->load->library('PayrollService');
$company=$this->db->order_by('id')->get('companies')->row();
if(!$company){echo"SKIP: perusahaan tidak tersedia.\n";return;}
$employee=$this->db->where(array('company_id'=>$company->id,'is_active'=>1,'salary_restricted'=>0))->order_by('id')->get('k_employees')->row();
$user=$this->db->order_by('id')->get('users')->row();
if(!$employee||!$user){echo"SKIP: karyawan biasa atau user tidak tersedia.\n";return;}
$access=$this->db->get_where('user_companies',array('company_id'=>$company->id))->row();if(!$access){echo"SKIP: akses user perusahaan tidak tersedia.\n";return;}$user=$this->db->get_where('users',array('id'=>$access->user_id))->row();$this->session->set_userdata(array('company_id'=>(int)$company->id,'user_id'=>(int)$user->id));
$this->db->trans_begin(true);
try{
$start='2098-08-14';$end='2098-09-13';$now=date('Y-m-d H:i:s');
$this->db->insert('k_payroll_periods',array('company_id'=>$company->id,'period_name'=>'[TEST ROLLBACK] Flexible payroll','start_date'=>$start,'end_date'=>$end,'attendance_start_date'=>$start,'attendance_end_date'=>$end,'payment_date'=>'2098-09-25','status'=>'draft','workflow_status'=>'draft','generated_at'=>$now));$periodId=(int)$this->db->insert_id();
$componentIds=array();foreach(array(array('SMOKE_EARN','[TEST] Pendapatan','earning'),array('SMOKE_DED','[TEST] Potongan Tetap','deduction'),array('SMOKE_ALPHA','[TEST] Potongan Alpha','deduction'),array('SMOKE_BONUS','[TEST] Bonus Periode','earning'))as$component){$this->db->insert('payroll_components',array('company_id'=>$company->id,'code'=>$component[0].'-'.bin2hex(random_bytes(2)),'name'=>$component[1],'component_type'=>$component[2],'category'=>'other','calculation_method'=>'fixed','default_value'=>0,'fixed_amount'=>0,'taxable'=>0,'prorated'=>0,'is_active'=>1,'effective_from'=>$start,'created_at'=>$now));$componentIds[]=(int)$this->db->insert_id();}
foreach(array(array($componentIds[0],100),array($componentIds[1],10))as$assignment)$this->db->insert('employee_payroll_components',array('company_id'=>$company->id,'employee_id'=>$employee->id,'component_id'=>$assignment[0],'calculation_method'=>'fixed','amount_override'=>$assignment[1],'effective_from'=>$start,'is_active'=>1,'notes'=>'[TEST ROLLBACK]','created_at'=>$now));
$this->db->insert('payroll_attendance_rules',array('company_id'=>$company->id,'component_id'=>$componentIds[2],'rule_name'=>'[TEST] Alpha per hari','condition_type'=>'alpha','calculation_method'=>'per_day','rate_value'=>25,'effective_from'=>$start,'is_active'=>1,'created_at'=>$now));
$this->db->insert('payroll_period_adjustments',array('company_id'=>$company->id,'payroll_period_id'=>$periodId,'employee_id'=>$employee->id,'component_id'=>$componentIds[3],'adjustment_type'=>'earning','amount'=>50,'notes'=>'[TEST ROLLBACK] Bonus','is_active'=>1,'created_by'=>$user->id,'created_at'=>$now));
$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->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;
if((int)$first->count!==(int)$second->count)throw new RuntimeException('Recalculate menggandakan atau menghilangkan baris.');
if(($found['[TEST] Pendapatan']??0)!==100.0||($found['[TEST] Potongan Tetap']??0)!==10.0||($found['[TEST] Potongan Alpha']??0)!==25.0||($found['[TEST] Bonus Periode']??0)!==50.0)throw new RuntimeException('Pendapatan, potongan, aturan alpha, atau bonus tidak sesuai.');
$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";
}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";
}
}