hr dan payroll
This commit is contained in:
@@ -32,6 +32,7 @@ class Bukubesar extends MY_Admin_Controller {
|
||||
public function get_accounts()
|
||||
{
|
||||
$data = $this->dm->setTable('accounts')
|
||||
->where(array('company_id'=>(int)$this->companycontext->id(),'is_active'=>1,'allow_posting'=>1))
|
||||
->order_by('kode_akun','ASC')
|
||||
->get();
|
||||
|
||||
@@ -43,9 +44,26 @@ class Bukubesar extends MY_Admin_Controller {
|
||||
// =========================
|
||||
public function get_data()
|
||||
{
|
||||
$account_id = $this->input->get('account_id');
|
||||
$start = $this->input->get('start_date');
|
||||
$end = $this->input->get('end_date');
|
||||
$account_id = (int) $this->input->get('account_id');
|
||||
$start = (string) ($this->input->get('start_date') ?: date('Y-m-01'));
|
||||
$end = (string) ($this->input->get('end_date') ?: date('Y-m-d'));
|
||||
$companyId = (int) $this->companycontext->id();
|
||||
|
||||
if (!$account_id || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $start) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $end) || $end < $start) {
|
||||
return $this->output->set_content_type('application/json')->set_status_header(422)->set_output(json_encode(array('status'=>false,'message'=>'Akun dan periode Buku Besar tidak valid.')));
|
||||
}
|
||||
|
||||
$account = $this->db->get_where('accounts', array('id'=>$account_id,'company_id'=>$companyId))->row();
|
||||
if (!$account) {
|
||||
return $this->output->set_content_type('application/json')->set_status_header(404)->set_output(json_encode(array('status'=>false,'message'=>'Akun tidak ditemukan pada perusahaan aktif.')));
|
||||
}
|
||||
|
||||
$openingRow = $this->db->select('COALESCE(SUM(journal_details.debit),0) debit,COALESCE(SUM(journal_details.kredit),0) kredit', false)
|
||||
->from('journal_details')->join('journals','journals.id=journal_details.journal_id')
|
||||
->where(array('journals.company_id'=>$companyId,'journal_details.account_id'=>$account_id))
|
||||
->where_in('journals.status', array('posted','reversed'))->where('journals.tanggal <', $start)->get()->row();
|
||||
$normalDebit = in_array($account->tipe, array('asset','expense'), true);
|
||||
$opening = $normalDebit ? (float)$openingRow->debit-(float)$openingRow->kredit : (float)$openingRow->kredit-(float)$openingRow->debit;
|
||||
|
||||
$this->db->select('
|
||||
journals.tanggal,
|
||||
@@ -60,29 +78,26 @@ class Bukubesar extends MY_Admin_Controller {
|
||||
$this->db->join('journals','journals.id = journal_details.journal_id');
|
||||
$this->db->join('accounts','accounts.id = journal_details.account_id');
|
||||
$this->db->where_in('journals.status', array('posted', 'reversed'));
|
||||
|
||||
if ($account_id) {
|
||||
$this->db->where('journal_details.account_id', $account_id);
|
||||
}
|
||||
|
||||
if ($start && $end) {
|
||||
$this->db->where('journals.tanggal >=', $start);
|
||||
$this->db->where('journals.tanggal <=', $end);
|
||||
}
|
||||
$this->db->where('journals.company_id', $companyId);
|
||||
$this->db->where('journal_details.account_id', $account_id);
|
||||
$this->db->where('journals.tanggal >=', $start);
|
||||
$this->db->where('journals.tanggal <=', $end);
|
||||
// 🔥 FILTER DEBIT/KREDIT TIDAK BOLEH 0 SEMUA
|
||||
$this->db->group_start();
|
||||
$this->db->where('journal_details.debit >', 0);
|
||||
$this->db->or_where('journal_details.kredit >', 0);
|
||||
$this->db->group_end();
|
||||
|
||||
$this->db->order_by('journals.tanggal','ASC');
|
||||
$this->db->order_by('journals.tanggal','ASC')->order_by('journals.id','ASC')->order_by('journal_details.id','ASC');
|
||||
|
||||
$rows = $this->db->get()->result();
|
||||
|
||||
// =========================
|
||||
// HITUNG SALDO BERJALAN
|
||||
// =========================
|
||||
$saldo = 0;
|
||||
$saldo = $opening;
|
||||
$totalDebit = 0;
|
||||
$totalKredit = 0;
|
||||
$result = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
@@ -94,6 +109,9 @@ class Bukubesar extends MY_Admin_Controller {
|
||||
$saldo += ($r->kredit - $r->debit);
|
||||
}
|
||||
|
||||
$totalDebit += (float) $r->debit;
|
||||
$totalKredit += (float) $r->kredit;
|
||||
|
||||
$result[] = [
|
||||
'tanggal' => $r->tanggal,
|
||||
'no_ref' => $r->no_ref,
|
||||
@@ -104,6 +122,15 @@ class Bukubesar extends MY_Admin_Controller {
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($result);
|
||||
return $this->output->set_content_type('application/json')->set_output(json_encode(array(
|
||||
'status'=>true,
|
||||
'data'=>$result,
|
||||
'summary'=>array(
|
||||
'saldo_awal'=>round($opening,2),
|
||||
'total_debit'=>round($totalDebit,2),
|
||||
'total_kredit'=>round($totalKredit,2),
|
||||
'saldo_akhir'=>round($saldo,2),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Companies extends MY_Admin_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->load->database();$this->load->library(array('CompanyContext','MultiCurrencyService'));}
|
||||
public function __construct(){parent::__construct();$this->load->database();$this->load->library(array('CompanyContext','MultiCurrencyService','AuditService'));}
|
||||
|
||||
public function index()
|
||||
{
|
||||
@@ -13,7 +13,8 @@ class Companies extends MY_Admin_Controller
|
||||
$d['companies']=$this->db->order_by('name')->get('companies')->result();
|
||||
$d['currencies']=$this->db->order_by('code')->get('currencies')->result();
|
||||
$d['active_currencies']=$this->db->where('is_active',1)->order_by('code')->get('currencies')->result();
|
||||
$d['bank_accounts']=$this->db->table_exists('company_bank_accounts')?$this->db->where('company_id',$companyId)->order_by('is_primary','DESC')->order_by('id')->get('company_bank_accounts')->result():array();
|
||||
$d['bank_accounts']=$this->db->table_exists('company_bank_accounts')?$this->db->select('b.*,a.kode_akun,a.nama_akun')->from('company_bank_accounts b')->join('accounts a','a.id=b.gl_account_id AND a.company_id=b.company_id','left')->where('b.company_id',$companyId)->order_by('b.is_primary','DESC')->order_by('b.id')->get()->result():array();
|
||||
$d['bank_gl_accounts']=$this->db->where(array('company_id'=>$companyId,'tipe'=>'asset','is_active'=>1,'allow_posting'=>1))->order_by('kode_akun')->get('accounts')->result();
|
||||
$d['rates']=$this->db->select('r.*,c.name company_name')->from('exchange_rates r')->join('companies c','c.id=r.company_id')->where('r.company_id',$companyId)->order_by('rate_date','DESC')->limit(100)->get()->result();
|
||||
$this->load->view('partials/header',$d);$this->load->view('companies/index',$d);$this->load->view('partials/footer',$d);
|
||||
}
|
||||
@@ -39,12 +40,36 @@ class Companies extends MY_Admin_Controller
|
||||
|
||||
public function save_bank()
|
||||
{
|
||||
$this->postOnly();$company=(int)$this->companycontext->id();try{$id=(int)$this->input->post('id');$row=array('company_id'=>$company,'bank_name'=>trim((string)$this->input->post('bank_name',true)),'account_number'=>trim((string)$this->input->post('account_number',true)),'account_holder'=>trim((string)$this->input->post('account_holder',true)),'currency'=>strtoupper(trim((string)$this->input->post('currency',true))),'is_primary'=>$this->input->post('is_primary')?1:0,'is_active'=>1,'updated_at'=>date('Y-m-d H:i:s'));if($row['bank_name']===''||$row['account_number']===''||$row['account_holder']==='')throw new RuntimeException('Nama bank, nomor rekening, dan nama pemilik wajib diisi.');if($row['is_primary'])$this->db->where('company_id',$company)->update('company_bank_accounts',array('is_primary'=>0));if($id){if(!$this->db->get_where('company_bank_accounts',array('id'=>$id,'company_id'=>$company))->row())throw new RuntimeException('Rekening perusahaan tidak ditemukan.');$this->db->where(array('id'=>$id,'company_id'=>$company))->update('company_bank_accounts',$row);}else{$row['created_at']=date('Y-m-d H:i:s');$this->db->insert('company_bank_accounts',$row);}$this->session->set_flashdata('success','Rekening perusahaan berhasil disimpan.');}catch(Throwable$e){$this->session->set_flashdata('error',$e->getMessage());}redirect('companies');
|
||||
$this->postOnly();$company=(int)$this->companycontext->id();$transactionStarted=false;
|
||||
try{
|
||||
$id=(int)$this->input->post('id');$type=(string)$this->input->post('account_type',true);$opened=(string)$this->input->post('opened_at',true);$maturity=(string)$this->input->post('maturity_date',true);$interest=max(0,round((float)$this->input->post('interest_rate'),4));$gl=(int)$this->input->post('gl_account_id');
|
||||
if(!in_array($type,array('savings','deposit'),true))throw new RuntimeException('Jenis rekening tidak valid.');
|
||||
if(!$this->validDate($opened))throw new RuntimeException('Tanggal pembukaan rekening wajib diisi dengan benar.');
|
||||
if($type==='deposit'&&!$this->validDate($maturity))throw new RuntimeException('Tanggal jatuh tempo wajib untuk rekening Deposito.');
|
||||
if($type==='savings')$maturity=null;
|
||||
if($maturity!==null&&$maturity<$opened)throw new RuntimeException('Tanggal jatuh tempo tidak boleh sebelum tanggal pembukaan.');
|
||||
$coa=$this->db->get_where('accounts',array('id'=>$gl,'company_id'=>$company,'tipe'=>'asset','is_active'=>1,'allow_posting'=>1))->row();if(!$coa)throw new RuntimeException('Akun COA bank tidak valid atau bukan akun Aktiva aktif.');
|
||||
$row=array('company_id'=>$company,'bank_name'=>trim((string)$this->input->post('bank_name',true)),'account_number'=>trim((string)$this->input->post('account_number',true)),'account_holder'=>trim((string)$this->input->post('account_holder',true)),'product_name'=>trim((string)$this->input->post('product_name',true)),'account_type'=>$type,'interest_rate'=>$interest,'opened_at'=>$opened,'maturity_date'=>$maturity,'currency'=>strtoupper(trim((string)$this->input->post('currency',true))),'gl_account_id'=>$gl,'is_primary'=>$this->input->post('is_primary')?1:0,'updated_at'=>date('Y-m-d H:i:s'));
|
||||
if($row['bank_name']===''||$row['account_number']===''||$row['account_holder']===''||$row['product_name']==='')throw new RuntimeException('Nama bank, nomor rekening, pemilik, dan produk bank wajib diisi.');
|
||||
if(!$this->db->get_where('currencies',array('code'=>$row['currency'],'is_active'=>1))->row())throw new RuntimeException('Mata uang rekening tidak valid.');
|
||||
$this->db->where(array('company_id'=>$company,'account_number'=>$row['account_number']));if($id)$this->db->where('id !=',$id);if($this->db->count_all_results('company_bank_accounts'))throw new RuntimeException('Nomor rekening sudah terdaftar pada perusahaan ini.');
|
||||
$before=null;$this->db->trans_begin();$transactionStarted=true;
|
||||
if($row['is_primary'])$this->db->where('company_id',$company)->update('company_bank_accounts',array('is_primary'=>0,'updated_at'=>date('Y-m-d H:i:s')));
|
||||
if($id){$before=$this->db->get_where('company_bank_accounts',array('id'=>$id,'company_id'=>$company))->row();if(!$before)throw new RuntimeException('Rekening perusahaan tidak ditemukan.');$this->db->where(array('id'=>$id,'company_id'=>$company))->update('company_bank_accounts',$row);$bankId=$id;}else{$row['is_active']=1;$row['created_at']=date('Y-m-d H:i:s');$this->db->insert('company_bank_accounts',$row);$bankId=(int)$this->db->insert_id();}
|
||||
$after=$this->db->get_where('company_bank_accounts',array('id'=>$bankId,'company_id'=>$company))->row();$this->auditservice->record('company','company_bank_account',$bankId,$id?'update':'create',$before,$after,(int)$this->session->userdata('user_id'));if(!$this->db->trans_status())throw new RuntimeException('Rekening perusahaan gagal disimpan.');$this->db->trans_commit();$transactionStarted=false;$this->session->set_flashdata('success','Rekening perusahaan berhasil disimpan.');
|
||||
}catch(Throwable$e){if($transactionStarted)$this->db->trans_rollback();$this->session->set_flashdata('error',$e->getMessage());}
|
||||
redirect('companies#companyBanks');
|
||||
}
|
||||
|
||||
public function toggle_bank($id)
|
||||
{
|
||||
$this->postOnly();$company=(int)$this->companycontext->id();$bank=$this->db->get_where('company_bank_accounts',array('id'=>(int)$id,'company_id'=>$company))->row();if($bank)$this->db->where(array('id'=>$bank->id,'company_id'=>$company))->update('company_bank_accounts',array('is_active'=>$bank->is_active?0:1,'is_primary'=>0,'updated_at'=>date('Y-m-d H:i:s')));$this->session->set_flashdata('success','Status rekening perusahaan diperbarui.');redirect('companies');
|
||||
$this->postOnly();$company=(int)$this->companycontext->id();$this->db->trans_begin();
|
||||
try{$bank=$this->db->query('SELECT * FROM company_bank_accounts WHERE id=? AND company_id=? FOR UPDATE',array((int)$id,$company))->row();if(!$bank)throw new RuntimeException('Rekening perusahaan tidak ditemukan.');$update=array('is_active'=>$bank->is_active?0:1,'is_primary'=>0,'updated_at'=>date('Y-m-d H:i:s'));$this->db->where(array('id'=>$bank->id,'company_id'=>$company))->update('company_bank_accounts',$update);$after=$this->db->get_where('company_bank_accounts',array('id'=>$bank->id,'company_id'=>$company))->row();$this->auditservice->record('company','company_bank_account',$bank->id,'change_status',$bank,$after,(int)$this->session->userdata('user_id'));if(!$this->db->trans_status())throw new RuntimeException('Status rekening perusahaan gagal diperbarui.');$this->db->trans_commit();$this->session->set_flashdata('success','Status rekening perusahaan diperbarui.');}catch(Throwable$e){$this->db->trans_rollback();$this->session->set_flashdata('error',$e->getMessage());}redirect('companies#companyBanks');
|
||||
}
|
||||
|
||||
public function bank_export()
|
||||
{
|
||||
$company=(int)$this->companycontext->id();$this->db->select('b.bank_name,b.account_number,b.account_holder,b.product_name,b.account_type,b.interest_rate,b.opened_at,b.maturity_date,b.currency,b.is_active,a.kode_akun,a.nama_akun')->from('company_bank_accounts b')->join('accounts a','a.id=b.gl_account_id AND a.company_id=b.company_id','left')->where('b.company_id',$company);$bank=trim((string)$this->input->get('bank',true));$type=trim((string)$this->input->get('type',true));$status=$this->input->get('status');$from=trim((string)$this->input->get('maturity_from',true));$to=trim((string)$this->input->get('maturity_to',true));if($bank!=='')$this->db->like('b.bank_name',$bank);if(in_array($type,array('savings','deposit'),true))$this->db->where('b.account_type',$type);if($status==='0'||$status==='1')$this->db->where('b.is_active',(int)$status);if($this->validDate($from))$this->db->where('b.maturity_date >=',$from);if($this->validDate($to))$this->db->where('b.maturity_date <=',$to);$rows=$this->db->order_by('b.bank_name')->order_by('b.account_number')->get()->result_array();$this->output->set_content_type('text/csv')->set_header('Content-Disposition: attachment; filename="rekening-perusahaan-'.date('Ymd').'.csv"');$out=fopen('php://output','w');fputcsv($out,array('Nama Bank','Nomor Rekening','Pemilik','Produk','Jenis','Bunga (%)','Tanggal Pembukaan','Jatuh Tempo','Mata Uang','Status','Kode COA','Nama COA'));foreach($rows as$row){$row['account_type']=$row['account_type']==='deposit'?'Deposito':'Tabungan';$row['is_active']=$row['is_active']?'Aktif':'Nonaktif';fputcsv($out,array_values($row));}fclose($out);
|
||||
}
|
||||
|
||||
public function save_currency()
|
||||
@@ -64,4 +89,5 @@ class Companies extends MY_Admin_Controller
|
||||
if(empty($_FILES['logo'])||$_FILES['logo']['error']===UPLOAD_ERR_NO_FILE)return$current;if($_FILES['logo']['error']!==UPLOAD_ERR_OK)throw new RuntimeException('Logo perusahaan gagal diunggah.');if((int)$_FILES['logo']['size']>2*1024*1024)throw new RuntimeException('Ukuran logo maksimal 2 MB.');$mime=(new finfo(FILEINFO_MIME_TYPE))->file($_FILES['logo']['tmp_name']);$allowed=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp');if(!isset($allowed[$mime]))throw new RuntimeException('Logo hanya menerima JPG, PNG, atau WEBP.');$dir=FCPATH.'uploads/accounting/company/';if(!is_dir($dir)&&!mkdir($dir,0755,true))throw new RuntimeException('Folder logo perusahaan tidak dapat dibuat.');$relative='uploads/accounting/company/'.bin2hex(random_bytes(16)).'.'.$allowed[$mime];if(!move_uploaded_file($_FILES['logo']['tmp_name'],FCPATH.$relative))throw new RuntimeException('Logo perusahaan tidak dapat disimpan.');return$relative;
|
||||
}
|
||||
private function postOnly(){if(strtoupper($this->input->method())!=='POST')show_error('Metode tidak diizinkan.',405);}
|
||||
private function validDate($value){$date=DateTime::createFromFormat('Y-m-d',(string)$value);return$date&&$date->format('Y-m-d')===$value;}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,10 @@ class Dashboard extends MY_Controller
|
||||
|
||||
public function log_activity()
|
||||
{
|
||||
$data = array('active_menu' => 'dashboard');
|
||||
$data = array(
|
||||
'active_menu' => 'dashboard',
|
||||
'use_datatables' => true,
|
||||
);
|
||||
$this->load->view('partials/header', $data);
|
||||
$this->load->view('dashboard/activity_logs', $data);
|
||||
$this->load->view('partials/footer');
|
||||
|
||||
@@ -15,10 +15,17 @@ class Generatebuku extends MY_Controller
|
||||
|
||||
public function generatepdf($account_id, $start, $end)
|
||||
{
|
||||
if ($this->session->userdata('role') != 'Admin') {
|
||||
if (!is_master_admin_user() && $this->session->userdata('role') != 'Admin') {
|
||||
show_error('Akses ditolak', 403);
|
||||
}
|
||||
|
||||
$startDate = DateTime::createFromFormat('Y-m-d', (string) $start);
|
||||
$endDate = DateTime::createFromFormat('Y-m-d', (string) $end);
|
||||
if (!$startDate || $startDate->format('Y-m-d') !== $start
|
||||
|| !$endDate || $endDate->format('Y-m-d') !== $end || $end < $start) {
|
||||
show_error('Periode Buku Besar tidak valid.', 422);
|
||||
}
|
||||
|
||||
if (ob_get_length()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
@@ -28,8 +35,12 @@ class Generatebuku extends MY_Controller
|
||||
// =============================================================
|
||||
$akun = null;
|
||||
if (!empty($account_id)) {
|
||||
$akun = $this->db->get_where('accounts', ['id' => (int) $account_id])->row();
|
||||
$akun = $this->db->get_where('accounts', ['id' => (int) $account_id, 'company_id'=>(int)$this->companycontext->id()])->row();
|
||||
}
|
||||
if (!$akun) show_error('Akun Buku Besar tidak ditemukan pada perusahaan aktif.', 404);
|
||||
|
||||
$openingRow=$this->db->select('COALESCE(SUM(d.debit),0) debit,COALESCE(SUM(d.kredit),0) kredit',false)->from('journal_details d')->join('journals j','j.id=d.journal_id')->where(array('j.company_id'=>(int)$this->companycontext->id(),'d.account_id'=>(int)$account_id))->where_in('j.status',array('posted','reversed'))->where('j.tanggal <',$start)->get()->row();
|
||||
$saldoAwal=in_array($akun->tipe,array('asset','expense'),true)?(float)$openingRow->debit-(float)$openingRow->kredit:(float)$openingRow->kredit-(float)$openingRow->debit;
|
||||
|
||||
// =============================================================
|
||||
// QUERY DATA
|
||||
@@ -46,6 +57,7 @@ class Generatebuku extends MY_Controller
|
||||
$this->db->join('journals', 'journals.id = journal_details.journal_id');
|
||||
$this->db->join('accounts', 'accounts.id = journal_details.account_id');
|
||||
$this->db->where_in('journals.status', array('posted', 'reversed'));
|
||||
$this->db->where('journals.company_id', (int)$this->companycontext->id());
|
||||
|
||||
if (!empty($account_id)) {
|
||||
$this->db->where('journal_details.account_id', (int) $account_id);
|
||||
@@ -61,14 +73,14 @@ class Generatebuku extends MY_Controller
|
||||
$this->db->or_where('journal_details.kredit >', 0);
|
||||
$this->db->group_end();
|
||||
|
||||
$this->db->order_by('journals.tanggal', 'ASC');
|
||||
$this->db->order_by('journals.tanggal', 'ASC')->order_by('journals.id','ASC')->order_by('journal_details.id','ASC');
|
||||
|
||||
$rows = $this->db->get()->result();
|
||||
|
||||
// =============================================================
|
||||
// HITUNG SALDO BERJALAN
|
||||
// =============================================================
|
||||
$saldo = 0;
|
||||
$saldo = $saldoAwal;
|
||||
$result = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
@@ -94,7 +106,7 @@ class Generatebuku extends MY_Controller
|
||||
// =============================================================
|
||||
// DATA PERUSAHAAN
|
||||
// =============================================================
|
||||
$company = $this->db->get('pengaturan')->row();
|
||||
$company = $this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();
|
||||
|
||||
// =============================================================
|
||||
// BUILD STRING INFO LAPORAN
|
||||
@@ -117,16 +129,16 @@ class Generatebuku extends MY_Controller
|
||||
$pdf->SetMargins(10, 10, 10);
|
||||
|
||||
$pdf->setHeaderData(
|
||||
$company->nama_perusahaan,
|
||||
$company->alamat_perusahaan,
|
||||
FCPATH . 'uploads/img/logo-ljn.png'
|
||||
$company->name,
|
||||
$company->address,
|
||||
!empty($company->logo_path) ? FCPATH.$company->logo_path : ''
|
||||
);
|
||||
|
||||
$pdf->setInfoLaporan($namaAkun, $periodeStr);
|
||||
|
||||
$pdf->AddPage('P', 'A4');
|
||||
$pdf->BukuBesarTable($result);
|
||||
$pdf->BukuBesarTotal($total_debit, $total_kredit, $saldo);
|
||||
$pdf->BukuBesarTotal($saldoAwal, $total_debit, $total_kredit, $saldo);
|
||||
$pdf->Pembuat('Tito Ngudiana', 'Finance Department');
|
||||
|
||||
// =============================================================
|
||||
|
||||
@@ -12,13 +12,24 @@ class Hrpayroll extends MY_Controller
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}
|
||||
private function company(){return(int)$this->session->userdata('company_id');}
|
||||
private function allSalary(){return is_master_admin_user()||check_permission('payroll','can_manage_all_salary');}
|
||||
private function can($action){return is_master_admin_user()||check_permission('payroll',$action)||check_permission('employees',$action);}
|
||||
private function canManageAllowances(){return is_master_admin_user()||check_permission('payroll','can_manage_allowances');}
|
||||
private function can($action){return is_master_admin_user()||check_permission('payroll',$action);}
|
||||
private function go($ok,$message,$section='periods'){$this->session->set_flashdata($ok?'success':'error',$message);redirect('hrpayroll?section='.$section);}
|
||||
private function validDate($value){$date=DateTime::createFromFormat('Y-m-d',(string)$value);return$date&&$date->format('Y-m-d')===$value?$value:null;}
|
||||
private function jsonError(Throwable$e){return json_response(false,$e->getMessage(),array(),$e instanceof BusinessException?422:500);}
|
||||
private function requireAction($action,$message){if(!$this->can($action))throw new BusinessException($message);}
|
||||
private function requirePost(){if(strtoupper((string)$this->input->method())!=='POST')throw new BusinessException('Metode request tidak diizinkan.');}
|
||||
private function isAdditionalComponent($component){if(!$component)return false;$code=strtoupper((string)$component->code);if($code==='BASIC')return$component->component_type!=='earning';return$component->component_type==='employer_cost'||$component->component_type==='earning';}
|
||||
private function canManageComponent($component){return!$this->isAdditionalComponent($component)||$this->canManageAllowances();}
|
||||
private function assertManageComponent($component){if(!$this->canManageComponent($component))throw new BusinessException('Tunjangan, bonus, lembur, THR, dan pendapatan tambahan hanya dapat dikelola dengan permission Kelola Tunjangan & Bonus.');}
|
||||
private function invalidateDraftCalculations()
|
||||
{
|
||||
$this->db->where(array('company_id'=>$this->company(),'workflow_status'=>'draft'))->update('k_payroll_periods',array('calculated_at'=>null,'calculated_by'=>null,'calculation_hash'=>null,'updated_at'=>date('Y-m-d H:i:s')));
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
if(!$this->can('can_view'))show_error('Anda tidak memiliki izin melihat payroll.',403,'Akses ditolak');
|
||||
$section=$this->input->get('section',true)?:'periods';if(!in_array($section,array('periods','salaries','settings','adjustments'),true))$section='periods';
|
||||
$company=$this->company();$visible=$this->allSalary()?'1=1':'COALESCE(e.salary_restricted,0)=0';
|
||||
$periods=$this->db->query("SELECT p.*,COUNT(CASE WHEN {$visible} THEN x.id END) employee_count,COALESCE(SUM(CASE WHEN {$visible} THEN x.gross_salary ELSE 0 END),0) gross,COALESCE(SUM(CASE WHEN {$visible} THEN x.total_deductions ELSE 0 END),0) deductions,COALESCE(SUM(CASE WHEN {$visible} THEN x.total_salary ELSE 0 END),0) net,l.status attendance_lock_status FROM k_payroll_periods p LEFT JOIN k_payrolls x ON x.payroll_period_id=p.id LEFT JOIN k_employees e ON e.id=x.employee_id LEFT JOIN payroll_attendance_locks l ON l.payroll_period_id=p.id AND l.company_id=p.company_id WHERE p.company_id=? GROUP BY p.id,l.status ORDER BY COALESCE(p.attendance_start_date,p.start_date) DESC",array($company))->result();
|
||||
@@ -26,16 +37,19 @@ class Hrpayroll extends MY_Controller
|
||||
$employees=$this->db->select('e.id,e.employee_code,e.full_name,e.department_id,e.position_id,e.is_active,e.salary_restricted,d.department_name,p.position_name')->from('k_employees e')->join('k_departments d','d.id=e.department_id','left')->join('k_positions p','p.id=e.position_id','left')->where('e.company_id',$company)->order_by('e.full_name')->get()->result();
|
||||
if(!$this->allSalary())$employees=array_values(array_filter($employees,function($e){return empty($e->salary_restricted);}));
|
||||
$salaryRows=$this->salarySummaries($employees,$components);
|
||||
$positions=$this->db->select('p.id,p.code,p.position_name,p.department_id,d.department_name')->from('k_positions p')->join('k_departments d','d.id=p.department_id','left')->where('p.is_active',1)->order_by('p.position_name')->get()->result();
|
||||
$departments=$this->db->where('is_active',1)->order_by('department_name')->get('k_departments')->result();
|
||||
$positionAssignments=$this->db->select('x.*,p.position_name,c.code component_code,c.name component_name,c.component_type')->from('position_payroll_components x')->join('k_positions p','p.id=x.position_id')->join('payroll_components c','c.id=x.component_id')->where('x.company_id',$company)->order_by('p.position_name')->order_by('c.name')->get()->result();
|
||||
$rules=$this->db->select('r.*,c.code component_code,c.name component_name')->from('payroll_attendance_rules r')->join('payroll_components c','c.id=r.component_id','left')->where('r.company_id',$company)->order_by('r.condition_type')->order_by('r.effective_from','DESC')->get()->result();
|
||||
$adjustments=$this->db->select('a.*,p.period_name,p.workflow_status,e.employee_code,e.full_name,c.code component_code,c.name component_name')->from('payroll_period_adjustments a')->join('k_payroll_periods p','p.id=a.payroll_period_id')->join('k_employees e','e.id=a.employee_id')->join('payroll_components c','c.id=a.component_id')->where('a.company_id',$company)->where('a.is_active',1)->order_by('a.id','DESC')->get()->result();
|
||||
$positionQuery=$this->db->select('p.id,p.code,p.position_name,p.department_id,d.department_name')->from('k_positions p')->join('k_departments d','d.id=p.department_id','left')->where('p.is_active',1);if($this->db->field_exists('company_id','k_positions'))$positionQuery->where('p.company_id',$company);$positions=$positionQuery->order_by('p.position_name')->get()->result();
|
||||
$departmentQuery=$this->db->where('is_active',1);if($this->db->field_exists('company_id','k_departments'))$departmentQuery->where('company_id',$company);$departments=$departmentQuery->order_by('department_name')->get('k_departments')->result();
|
||||
$positionAssignments=$this->db->select('x.*,p.position_name,c.code component_code,c.name component_name,c.component_type')->from('position_payroll_components x')->join('k_positions p','p.id=x.position_id')->join('payroll_components c','c.id=x.component_id AND c.company_id=x.company_id')->where('x.company_id',$company)->order_by('p.position_name')->order_by('c.name')->get()->result();
|
||||
$rules=$this->db->select('r.*,c.code component_code,c.name component_name')->from('payroll_attendance_rules r')->join('payroll_components c','c.id=r.component_id AND c.company_id=r.company_id','left')->where('r.company_id',$company)->order_by('r.condition_type')->order_by('r.effective_from','DESC')->get()->result();
|
||||
$adjustments=$this->db->select('a.*,p.period_name,p.workflow_status,e.employee_code,e.full_name,c.code component_code,c.name component_name,c.component_type')->from('payroll_period_adjustments a')->join('k_payroll_periods p','p.id=a.payroll_period_id AND p.company_id=a.company_id')->join('k_employees e','e.id=a.employee_id AND e.company_id=a.company_id')->join('payroll_components c','c.id=a.component_id AND c.company_id=a.company_id')->where('a.company_id',$company)->where('a.is_active',1)->order_by('a.id','DESC')->get()->result();
|
||||
if(!$this->allSalary())$adjustments=array_values(array_filter($adjustments,function($row)use($employees){foreach($employees as$employee)if($employee->id==$row->employee_id)return true;return false;}));
|
||||
foreach($adjustments as$row)$row->can_manage=$this->canManageComponent($row);
|
||||
$cashQuery=$this->db->where('is_active',1);if($this->db->field_exists('company_id','cash_accounts'))$cashQuery->where('company_id',$company);$cash=$cashQuery->order_by('name')->get('cash_accounts')->result();
|
||||
$accounts=$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('kode_akun')->get('accounts')->result();
|
||||
$settings=$this->db->get_where('payroll_settings',array('company_id'=>$company))->row();
|
||||
$data=array('active_menu'=>'hr_payroll','payroll_section'=>$section,'periods'=>$periods,'components'=>$components,'employees'=>$employees,'salary_rows'=>$salaryRows,'positions'=>$positions,'departments'=>$departments,'position_assignments'=>$positionAssignments,'attendance_rules'=>$rules,'adjustments'=>$adjustments,'cash_accounts'=>$cash,'accounts'=>$accounts,'payroll_settings'=>$settings,'can_manage_all_salary'=>$this->allSalary(),'permissions'=>array('create'=>$this->can('can_create'),'update'=>$this->can('can_update'),'approve'=>$this->can('can_approve'),'post'=>$this->can('can_post'),'export'=>$this->can('can_export')));
|
||||
$manageableComponents=array_values(array_filter($components,function($component){return$this->canManageComponent($component);}));
|
||||
$adjustmentComponents=array_values(array_filter($manageableComponents,function($component){return$component->component_type==='deduction'||($this->canManageAllowances()&&$component->component_type==='earning'&&strtoupper((string)$component->code)!=='BASIC');}));
|
||||
$data=array('active_menu'=>'hr_payroll','payroll_section'=>$section,'periods'=>$periods,'components'=>$components,'manageable_components'=>$manageableComponents,'adjustment_components'=>$adjustmentComponents,'employees'=>$employees,'salary_rows'=>$salaryRows,'positions'=>$positions,'departments'=>$departments,'position_assignments'=>$positionAssignments,'attendance_rules'=>$rules,'adjustments'=>$adjustments,'cash_accounts'=>$cash,'accounts'=>$accounts,'payroll_settings'=>$settings,'can_manage_all_salary'=>$this->allSalary(),'can_manage_allowances'=>$this->canManageAllowances(),'permissions'=>array('create'=>$this->can('can_create'),'update'=>$this->can('can_update'),'approve'=>$this->can('can_approve'),'post'=>$this->can('can_post'),'export'=>$this->can('can_export')));
|
||||
$this->load->view('partials/header',$data);$this->load->view('hr_payroll/index',$data);$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
@@ -47,96 +61,96 @@ class Hrpayroll extends MY_Controller
|
||||
|
||||
public function create_period()
|
||||
{
|
||||
try{if(!$this->can('can_create'))throw new BusinessException('Anda tidak mempunyai permission membuat periode payroll.');$name=trim((string)$this->input->post('period_name',true));$start=$this->validDate($this->input->post('attendance_start_date'));$end=$this->validDate($this->input->post('attendance_end_date'));$payment=$this->validDate($this->input->post('payment_date'));if($name===''||!$start||!$end||!$payment)throw new BusinessException('Nama periode, rentang kehadiran, dan tanggal pembayaran wajib diisi.');if($end<$start)throw new BusinessException('Tanggal akhir kehadiran tidak boleh sebelum tanggal awal.');$overlap=(int)$this->db->query("SELECT COUNT(*) total FROM k_payroll_periods WHERE company_id=? AND workflow_status<>'cancelled' AND COALESCE(attendance_start_date,start_date)<=? AND COALESCE(attendance_end_date,end_date)>=?",array($this->company(),$end,$start))->row()->total;if($overlap)throw new BusinessException('Rentang kehadiran tumpang tindih dengan periode payroll lain.');$row=array('company_id'=>$this->company(),'period_name'=>$name,'start_date'=>$start,'end_date'=>$end,'attendance_start_date'=>$start,'attendance_end_date'=>$end,'payment_date'=>$payment,'notes'=>trim((string)$this->input->post('notes',true))?:null,'status'=>'draft','workflow_status'=>'draft','generated_at'=>date('Y-m-d H:i:s'));$this->db->insert('k_payroll_periods',$row);$id=(int)$this->db->insert_id();$this->auditservice->record('payroll','payroll_period',$id,'create',null,$row,$this->uid());$this->go(true,'Periode payroll berhasil dibuat.');}catch(Throwable$e){$this->go(false,$e->getMessage());}
|
||||
try{$this->requirePost();$this->requireAction('can_create','Anda tidak mempunyai permission membuat periode payroll.');$name=trim((string)$this->input->post('period_name',true));$start=$this->validDate($this->input->post('attendance_start_date'));$end=$this->validDate($this->input->post('attendance_end_date'));$payment=$this->validDate($this->input->post('payment_date'));if($name===''||!$start||!$end||!$payment)throw new BusinessException('Nama periode, rentang kehadiran, dan tanggal pembayaran wajib diisi.');if($end<$start)throw new BusinessException('Tanggal akhir kehadiran tidak boleh sebelum tanggal awal.');$overlap=(int)$this->db->query("SELECT COUNT(*) total FROM k_payroll_periods WHERE company_id=? AND workflow_status<>'cancelled' AND COALESCE(attendance_start_date,start_date)<=? AND COALESCE(attendance_end_date,end_date)>=?",array($this->company(),$end,$start))->row()->total;if($overlap)throw new BusinessException('Rentang kehadiran tumpang tindih dengan periode payroll lain.');$row=array('company_id'=>$this->company(),'period_name'=>$name,'start_date'=>$start,'end_date'=>$end,'attendance_start_date'=>$start,'attendance_end_date'=>$end,'payment_date'=>$payment,'notes'=>trim((string)$this->input->post('notes',true))?:null,'status'=>'draft','workflow_status'=>'draft','generated_at'=>date('Y-m-d H:i:s'));$this->db->insert('k_payroll_periods',$row);$id=(int)$this->db->insert_id();$this->auditservice->record('payroll','payroll_period',$id,'create',null,$row,$this->uid());$this->go(true,'Periode payroll berhasil dibuat.');}catch(Throwable$e){$this->go(false,$e->getMessage());}
|
||||
}
|
||||
|
||||
public function lock_attendance()
|
||||
{
|
||||
try{$id=(int)$this->input->post('period_id');$period=$this->period($id);if($period->workflow_status!=='draft')throw new BusinessException('Kehadiran hanya dapat dikunci ketika payroll masih Draft.');$start=$period->attendance_start_date?:$period->start_date;$end=$period->attendance_end_date?:$period->end_date;$pending=array();if($this->db->table_exists('manual_attendance_requests')){$n=$this->db->where('company_id',$this->company())->where('status','submitted')->where('attendance_date >=',$start)->where('attendance_date <=',$end)->count_all_results('manual_attendance_requests');if($n)$pending[]=$n.' absensi manual';}if($this->db->table_exists('k_leave_requests')){$n=$this->db->where('company_id',$this->company())->where('workflow_status','submitted')->where('start_date <=',$end)->where('end_date >=',$start)->count_all_results('k_leave_requests');if($n)$pending[]=$n.' cuti/izin';}if($this->db->table_exists('attendance_corrections')){$n=$this->db->from('attendance_corrections c')->join('k_attendances a','a.id=c.attendance_id')->join('k_employees e','e.id=a.employee_id')->where(array('e.company_id'=>$this->company(),'c.status'=>'submitted'))->where('a.attendance_date >=',$start)->where('a.attendance_date <=',$end)->count_all_results();if($n)$pending[]=$n.' koreksi absensi';}if($pending)throw new BusinessException('Periode belum dapat dikunci karena masih ada '.implode(', ',$pending).' menunggu approval.');$existing=$this->db->get_where('payroll_attendance_locks',array('company_id'=>$this->company(),'payroll_period_id'=>$id))->row();$row=array('company_id'=>$this->company(),'payroll_period_id'=>$id,'start_date'=>$start,'end_date'=>$end,'status'=>'locked','locked_by'=>$this->uid(),'locked_at'=>date('Y-m-d H:i:s'),'notes'=>trim((string)$this->input->post('notes',true))?:null);if($existing)$this->db->where('id',$existing->id)->update('payroll_attendance_locks',$row);else$this->db->insert('payroll_attendance_locks',$row);$this->db->where(array('id'=>$id,'company_id'=>$this->company()))->update('k_payroll_periods',array('attendance_locked_by'=>$this->uid(),'attendance_locked_at'=>date('Y-m-d H:i:s'),'updated_at'=>date('Y-m-d H:i:s')));$this->auditservice->record('payroll','attendance_lock',$id,'lock',null,$row,$this->uid());$this->go(true,'Kehadiran '.$start.' sampai '.$end.' berhasil dikunci.');}catch(Throwable$e){$this->go(false,$e->getMessage());}
|
||||
try{$this->requirePost();$this->requireAction('can_post','Anda tidak mempunyai permission mengunci kehadiran payroll.');$id=(int)$this->input->post('period_id');$period=$this->period($id);if($period->workflow_status!=='draft')throw new BusinessException('Kehadiran hanya dapat dikunci ketika payroll masih Draft.');$start=$period->attendance_start_date?:$period->start_date;$end=$period->attendance_end_date?:$period->end_date;$pending=array();if($this->db->table_exists('manual_attendance_requests')){$n=$this->db->where('company_id',$this->company())->where('status','submitted')->where('attendance_date >=',$start)->where('attendance_date <=',$end)->count_all_results('manual_attendance_requests');if($n)$pending[]=$n.' absensi manual';}if($this->db->table_exists('k_leave_requests')){$n=$this->db->where('company_id',$this->company())->where('workflow_status','submitted')->where('start_date <=',$end)->where('end_date >=',$start)->count_all_results('k_leave_requests');if($n)$pending[]=$n.' cuti/izin';}if($this->db->table_exists('attendance_corrections')){$n=$this->db->from('attendance_corrections c')->join('k_attendances a','a.id=c.attendance_id')->join('k_employees e','e.id=a.employee_id')->where(array('e.company_id'=>$this->company(),'c.status'=>'submitted'))->where('a.attendance_date >=',$start)->where('a.attendance_date <=',$end)->count_all_results();if($n)$pending[]=$n.' koreksi absensi';}if($pending)throw new BusinessException('Periode belum dapat dikunci karena masih ada '.implode(', ',$pending).' menunggu approval.');$existing=$this->db->get_where('payroll_attendance_locks',array('company_id'=>$this->company(),'payroll_period_id'=>$id))->row();$row=array('company_id'=>$this->company(),'payroll_period_id'=>$id,'start_date'=>$start,'end_date'=>$end,'status'=>'locked','locked_by'=>$this->uid(),'locked_at'=>date('Y-m-d H:i:s'),'notes'=>trim((string)$this->input->post('notes',true))?:null);$this->db->trans_begin();if($existing)$this->db->where('id',$existing->id)->update('payroll_attendance_locks',$row);else$this->db->insert('payroll_attendance_locks',$row);$this->db->where(array('id'=>$id,'company_id'=>$this->company()))->update('k_payroll_periods',array('attendance_locked_by'=>$this->uid(),'attendance_locked_at'=>date('Y-m-d H:i:s'),'calculated_at'=>null,'calculated_by'=>null,'calculation_hash'=>null,'updated_at'=>date('Y-m-d H:i:s')));$this->auditservice->record('payroll','attendance_lock',$id,'lock',null,$row,$this->uid());if($this->db->trans_status()===false)throw new RuntimeException('Kunci kehadiran gagal disimpan.');$this->db->trans_commit();$this->go(true,'Kehadiran '.$start.' sampai '.$end.' berhasil dikunci.');}catch(Throwable$e){if($this->db->trans_status()!==null)$this->db->trans_rollback();$this->go(false,$e->getMessage());}
|
||||
}
|
||||
|
||||
public function calculate(){try{$id=$this->payrollservice->calculate((int)$this->input->post('period_id'),$this->uid(),$this->allSalary(),$this->company());$this->go(true,'Draft payroll periode #'.$id.' berhasil dihitung.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function workflow($id,$action){try{$this->payrollservice->workflow($id,$action,$this->uid(),$this->company(),$this->allSalary());$this->go(true,'Tahap payroll berhasil diperbarui.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function post($id){try{$this->payrollservice->post($id,$this->input->post('date'),$this->uid(),$this->company(),$this->allSalary());$this->go(true,'Payroll berhasil diposting ke jurnal.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function pay($id){try{$this->payrollservice->pay($id,$this->input->post('date'),(int)$this->input->post('cash_account_id'),$this->uid(),$this->company(),$this->allSalary());$this->go(true,'Payroll berhasil dibayar.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function calculate(){try{$this->requirePost();$this->requireAction('can_create','Anda tidak mempunyai permission menghitung payroll.');$id=$this->payrollservice->calculate((int)$this->input->post('period_id'),$this->uid(),$this->allSalary(),$this->company());$this->go(true,'Draft payroll periode #'.$id.' berhasil dihitung.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function workflow($id,$action){try{$this->requirePost();$this->requireAction('can_approve','Anda tidak mempunyai permission approval payroll.');$this->payrollservice->workflow($id,$action,$this->uid(),$this->company(),$this->allSalary());$this->go(true,'Tahap payroll berhasil diperbarui.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function post($id){try{$this->requirePost();$this->requireAction('can_post','Anda tidak mempunyai permission posting payroll.');$date=$this->validDate($this->input->post('date'));if(!$date)throw new BusinessException('Tanggal posting tidak valid.');$this->payrollservice->post($id,$date,$this->uid(),$this->company(),$this->allSalary());$this->go(true,'Payroll berhasil diposting ke jurnal.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function pay($id){try{$this->requirePost();$this->requireAction('can_post','Anda tidak mempunyai permission membayar payroll.');$date=$this->validDate($this->input->post('date'));if(!$date)throw new BusinessException('Tanggal pembayaran tidak valid.');$this->payrollservice->pay($id,$date,(int)$this->input->post('cash_account_id'),$this->uid(),$this->company(),$this->allSalary());$this->go(true,'Payroll berhasil dibayar.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
|
||||
public function period_detail($id)
|
||||
{
|
||||
$period=$this->period($id,false);if(!$period)return json_response(false,'Periode payroll tidak ditemukan.',array(),404);$q=$this->db->select('p.id,p.basic_salary,p.gross_salary,p.total_deductions,p.total_salary,p.total_present,p.total_alpha,p.total_overtime_hours,e.employee_code,e.full_name,e.salary_restricted')->from('k_payrolls p')->join('k_employees e','e.id=p.employee_id')->where(array('p.payroll_period_id'=>$period->id,'p.company_id'=>$this->company()));if(!$this->allSalary())$q->where('e.salary_restricted',0);return json_response(true,'Rincian payroll.',array('period'=>$period,'rows'=>$q->order_by('e.full_name')->get()->result()));
|
||||
try{$this->requireAction('can_view','Anda tidak mempunyai permission melihat payroll.');$period=$this->period($id,false);if(!$period)return json_response(false,'Periode payroll tidak ditemukan.',array(),404);$q=$this->db->select('p.id,p.basic_salary,p.gross_salary,p.total_deductions,p.total_salary,p.total_present,p.total_alpha,p.total_overtime_hours,e.employee_code,e.full_name,e.salary_restricted')->from('k_payrolls p')->join('k_employees e','e.id=p.employee_id AND e.company_id=p.company_id')->where(array('p.payroll_period_id'=>$period->id,'p.company_id'=>$this->company()));if(!$this->allSalary())$q->where('e.salary_restricted',0);return json_response(true,'Rincian payroll.',array('period'=>$period,'rows'=>$q->order_by('e.full_name')->get()->result()));}catch(Throwable$e){return$this->jsonError($e);}
|
||||
}
|
||||
|
||||
public function salary_detail($employeeId)
|
||||
{
|
||||
try{$employee=$this->employee((int)$employeeId);if(!empty($employee->salary_restricted)&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');$lines=$this->db->select('x.*,c.code component_code,c.name component_name,c.component_type')->from('employee_payroll_components x')->join('payroll_components c','c.id=x.component_id')->where(array('x.company_id'=>$this->company(),'x.employee_id'=>$employee->id))->order_by('x.effective_from','DESC')->get()->result();return json_response(true,'Struktur gaji.',array('employee'=>$employee,'lines'=>$lines));}catch(Throwable$e){return$this->jsonError($e);}
|
||||
try{$this->requireAction('can_view','Anda tidak mempunyai permission melihat payroll.');$employee=$this->employee((int)$employeeId);if(!empty($employee->salary_restricted)&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');$lines=$this->db->select('x.*,c.code component_code,c.name component_name,c.component_type')->from('employee_payroll_components x')->join('payroll_components c','c.id=x.component_id AND c.company_id=x.company_id')->where(array('x.company_id'=>$this->company(),'x.employee_id'=>$employee->id))->order_by('x.effective_from','DESC')->get()->result();foreach($lines as$line)$line->can_manage=$this->can('can_update')&&$this->canManageComponent((object)array('component_type'=>$line->component_type,'code'=>$line->component_code));return json_response(true,'Struktur gaji.',array('employee'=>$employee,'lines'=>$lines));}catch(Throwable$e){return$this->jsonError($e);}
|
||||
}
|
||||
|
||||
public function save_salary_line()
|
||||
{
|
||||
try{if(!$this->can('can_update'))throw new BusinessException('Anda tidak mempunyai permission mengubah struktur gaji.');$employee=$this->employee((int)$this->input->post('employee_id'));if(!empty($employee->salary_restricted)&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');$row=$this->assignmentInput('employee',$employee->id);$duplicate=$this->db->get_where('employee_payroll_components',array('company_id'=>$this->company(),'employee_id'=>$employee->id,'component_id'=>$row['component_id'],'effective_from'=>$row['effective_from']))->row();if($duplicate)throw new BusinessException('Komponen dengan tanggal mulai yang sama sudah ada. Gunakan tanggal efektif baru agar histori tidak tertimpa.');$this->db->trans_begin();$previous=$this->db->where(array('company_id'=>$this->company(),'employee_id'=>$employee->id,'component_id'=>$row['component_id'],'is_active'=>1))->where('effective_from <',$row['effective_from'])->group_start()->where('effective_to IS NULL',null,false)->or_where('effective_to >=',$row['effective_from'])->group_end()->order_by('effective_from','DESC')->get('employee_payroll_components')->row();if($previous)$this->db->where('id',$previous->id)->update('employee_payroll_components',array('effective_to'=>date('Y-m-d',strtotime($row['effective_from'].' -1 day')),'updated_at'=>date('Y-m-d H:i:s')));$this->db->insert('employee_payroll_components',$row);$id=(int)$this->db->insert_id();if($row['component_id']===$this->basicComponentId())$this->db->where(array('id'=>$employee->id,'company_id'=>$this->company()))->update('k_employees',array('basic_salary'=>$row['amount_override']));$this->auditservice->record('payroll','employee_salary_component',$id,'create_version',$previous,$row,$this->uid());$this->db->trans_commit();return json_response(true,'Versi struktur gaji berhasil disimpan.');}catch(Throwable$e){$this->db->trans_rollback();return$this->jsonError($e);}
|
||||
try{$this->requirePost();$this->requireAction('can_update','Anda tidak mempunyai permission mengubah struktur gaji.');$employee=$this->employee((int)$this->input->post('employee_id'));if(!empty($employee->salary_restricted)&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');$row=$this->assignmentInput('employee',$employee->id);$duplicate=$this->db->get_where('employee_payroll_components',array('company_id'=>$this->company(),'employee_id'=>$employee->id,'component_id'=>$row['component_id'],'effective_from'=>$row['effective_from']))->row();if($duplicate)throw new BusinessException('Komponen dengan tanggal mulai yang sama sudah ada. Gunakan tanggal efektif baru agar histori tidak tertimpa.');$this->db->trans_begin();$previous=$this->db->where(array('company_id'=>$this->company(),'employee_id'=>$employee->id,'component_id'=>$row['component_id'],'is_active'=>1))->where('effective_from <',$row['effective_from'])->group_start()->where('effective_to IS NULL',null,false)->or_where('effective_to >=',$row['effective_from'])->group_end()->order_by('effective_from','DESC')->get('employee_payroll_components')->row();if($previous)$this->db->where('id',$previous->id)->update('employee_payroll_components',array('effective_to'=>date('Y-m-d',strtotime($row['effective_from'].' -1 day')),'updated_at'=>date('Y-m-d H:i:s')));$this->db->insert('employee_payroll_components',$row);$id=(int)$this->db->insert_id();if($row['component_id']===$this->basicComponentId())$this->db->where(array('id'=>$employee->id,'company_id'=>$this->company()))->update('k_employees',array('basic_salary'=>$row['amount_override']));$this->invalidateDraftCalculations();$this->auditservice->record('payroll','employee_salary_component',$id,'create_version',$previous,$row,$this->uid());if($this->db->trans_status()===false)throw new RuntimeException('Struktur gaji gagal disimpan.');$this->db->trans_commit();return json_response(true,'Versi struktur gaji berhasil disimpan. Draft payroll wajib dihitung ulang.');}catch(Throwable$e){$this->db->trans_rollback();return$this->jsonError($e);}
|
||||
}
|
||||
|
||||
public function deactivate_salary_line($id)
|
||||
{
|
||||
try{$line=$this->db->get_where('employee_payroll_components',array('id'=>(int)$id,'company_id'=>$this->company()))->row();if(!$line)throw new BusinessException('Komponen gaji tidak ditemukan.');$employee=$this->employee($line->employee_id);if(!empty($employee->salary_restricted)&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');$update=array('is_active'=>0,'effective_to'=>date('Y-m-d'),'updated_at'=>date('Y-m-d H:i:s'));$this->db->where('id',$line->id)->update('employee_payroll_components',$update);$this->auditservice->record('payroll','employee_salary_component',$line->id,'deactivate',$line,$update,$this->uid());return json_response(true,'Komponen dinonaktifkan tanpa menghapus histori.');}catch(Throwable$e){return$this->jsonError($e);}
|
||||
try{$this->requirePost();$this->requireAction('can_update','Anda tidak mempunyai permission mengubah struktur gaji.');$line=$this->db->select('x.*,c.code component_code,c.component_type')->from('employee_payroll_components x')->join('payroll_components c','c.id=x.component_id AND c.company_id=x.company_id')->where(array('x.id'=>(int)$id,'x.company_id'=>$this->company()))->get()->row();if(!$line)throw new BusinessException('Komponen gaji tidak ditemukan.');$this->assertManageComponent((object)array('component_type'=>$line->component_type,'code'=>$line->component_code));$employee=$this->employee($line->employee_id);if(!empty($employee->salary_restricted)&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');if(!(int)$line->is_active)throw new BusinessException('Komponen gaji sudah nonaktif.');$update=array('is_active'=>0,'effective_to'=>date('Y-m-d'),'updated_at'=>date('Y-m-d H:i:s'));$this->db->trans_begin();$this->db->where('id',$line->id)->update('employee_payroll_components',$update);$this->invalidateDraftCalculations();$this->auditservice->record('payroll','employee_salary_component',$line->id,'deactivate',$line,$update,$this->uid());if($this->db->trans_status()===false)throw new RuntimeException('Komponen gaji gagal dinonaktifkan.');$this->db->trans_commit();return json_response(true,'Komponen dinonaktifkan tanpa menghapus histori. Draft payroll wajib dihitung ulang.');}catch(Throwable$e){$this->db->trans_rollback();return$this->jsonError($e);}
|
||||
}
|
||||
|
||||
public function component()
|
||||
{
|
||||
try{if(!$this->can('can_update'))throw new BusinessException('Anda tidak mempunyai permission mengelola komponen.');$id=(int)$this->input->post('id');$code=strtoupper(trim((string)$this->input->post('code',true)));$name=trim((string)$this->input->post('name',true));$type=$this->input->post('component_type');$method=$this->method($this->input->post('calculation_method'));$value=$this->decimal($this->input->post('default_value'));if($code===''||$name===''||!in_array($type,array('earning','deduction','employer_cost'),true))throw new BusinessException('Kode, nama, dan jenis komponen wajib diisi.');$formula=$method==='formula'?trim((string)$this->input->post('formula_expression',true))?:null:null;if($method==='formula')$this->validateFormula($formula);$row=array('company_id'=>$this->company(),'code'=>$code,'name'=>$name,'component_type'=>$type,'category'=>$this->input->post('category')?:'other','calculation_method'=>$method,'default_value'=>$value,'fixed_amount'=>$value,'formula_expression'=>$formula,'taxable'=>$this->input->post('taxable')?1:0,'prorated'=>$this->input->post('prorated')?1:0,'expense_account_id'=>$this->input->post('expense_account_id')?:null,'liability_account_id'=>$this->input->post('liability_account_id')?:null,'is_active'=>$this->input->post('is_active')?1:0,'effective_from'=>$this->validDate($this->input->post('effective_from'))?:date('Y-m-d'),'effective_to'=>$this->validDate($this->input->post('effective_to'))?:null,'updated_at'=>date('Y-m-d H:i:s'));$duplicate=$this->db->where(array('company_id'=>$this->company(),'code'=>$code))->where('id !=',$id)->count_all_results('payroll_components');if($duplicate)throw new BusinessException('Kode komponen sudah digunakan.');if($id){$before=$this->db->get_where('payroll_components',array('id'=>$id,'company_id'=>$this->company()))->row();if(!$before)throw new BusinessException('Komponen tidak ditemukan.');$this->db->where('id',$id)->update('payroll_components',$row);}else{$row['created_by']=$this->uid();$row['created_at']=date('Y-m-d H:i:s');$this->db->insert('payroll_components',$row);$id=(int)$this->db->insert_id();$before=null;}$this->auditservice->record('payroll','payroll_component',$id,$before?'update':'create',$before,$row,$this->uid());$this->go(true,'Komponen payroll berhasil disimpan.','settings');}catch(Throwable$e){$this->go(false,$e->getMessage(),'settings');}
|
||||
try{$this->requirePost();$this->requireAction('can_update','Anda tidak mempunyai permission mengelola komponen.');$id=(int)$this->input->post('id');$before=$id?$this->db->get_where('payroll_components',array('id'=>$id,'company_id'=>$this->company()))->row():null;if($id&&!$before)throw new BusinessException('Komponen tidak ditemukan.');$code=strtoupper(trim((string)$this->input->post('code',true)));$name=trim((string)$this->input->post('name',true));$type=$this->input->post('component_type');$method=$this->method($this->input->post('calculation_method'));$value=$this->decimal($this->input->post('default_value'));if($code===''||$name===''||!in_array($type,array('earning','deduction','employer_cost'),true))throw new BusinessException('Kode, nama, dan jenis komponen wajib diisi.');$proposed=(object)array('code'=>$code,'component_type'=>$type);if(!$this->canManageAllowances()){if(!$before&&$type!=='deduction')throw new BusinessException('Tanpa permission Kelola Tunjangan & Bonus, komponen baru hanya boleh berupa potongan.');if($before){$this->assertManageComponent($before);$this->assertManageComponent($proposed);if(strtoupper((string)$before->code)==='BASIC'&&$code!=='BASIC')throw new BusinessException('Kode komponen Gaji Pokok tidak boleh diubah.');}}$formula=$method==='formula'?trim((string)$this->input->post('formula_expression',true))?:null:null;if($method==='formula')$this->validateFormula($formula);$row=array('company_id'=>$this->company(),'code'=>$code,'name'=>$name,'component_type'=>$type,'category'=>$this->input->post('category')?:'other','calculation_method'=>$method,'default_value'=>$value,'fixed_amount'=>$value,'formula_expression'=>$formula,'taxable'=>$this->input->post('taxable')?1:0,'prorated'=>$this->input->post('prorated')?1:0,'expense_account_id'=>$this->input->post('expense_account_id')?:null,'liability_account_id'=>$this->input->post('liability_account_id')?:null,'is_active'=>$this->input->post('is_active')?1:0,'effective_from'=>$this->validDate($this->input->post('effective_from'))?:date('Y-m-d'),'effective_to'=>$this->validDate($this->input->post('effective_to'))?:null,'updated_at'=>date('Y-m-d H:i:s'));$duplicate=$this->db->where(array('company_id'=>$this->company(),'code'=>$code))->where('id !=',$id)->count_all_results('payroll_components');if($duplicate)throw new BusinessException('Kode komponen sudah digunakan.');$this->db->trans_begin();if($id)$this->db->where(array('id'=>$id,'company_id'=>$this->company()))->update('payroll_components',$row);else{$row['created_by']=$this->uid();$row['created_at']=date('Y-m-d H:i:s');$this->db->insert('payroll_components',$row);$id=(int)$this->db->insert_id();}$this->invalidateDraftCalculations();$this->auditservice->record('payroll','payroll_component',$id,$before?'update':'create',$before,$row,$this->uid());if($this->db->trans_status()===false)throw new RuntimeException('Komponen payroll gagal disimpan.');$this->db->trans_commit();$this->go(true,'Komponen payroll berhasil disimpan. Draft payroll wajib dihitung ulang.','settings');}catch(Throwable$e){$this->db->trans_rollback();$this->go(false,$e->getMessage(),'settings');}
|
||||
}
|
||||
|
||||
public function save_position_line()
|
||||
{
|
||||
try{$positionId=(int)$this->input->post('position_id');if(!$positionId||!$this->db->where('id',$positionId)->count_all_results('k_positions'))throw new BusinessException('Posisi tidak ditemukan.');$row=$this->assignmentInput('position',$positionId);if($this->db->get_where('position_payroll_components',array('company_id'=>$this->company(),'position_id'=>$positionId,'component_id'=>$row['component_id'],'effective_from'=>$row['effective_from']))->row())throw new BusinessException('Template komponen dengan tanggal efektif yang sama sudah ada.');$this->db->insert('position_payroll_components',$row);$id=(int)$this->db->insert_id();$this->auditservice->record('payroll','position_salary_component',$id,'create',null,$row,$this->uid());$this->go(true,'Template posisi berhasil ditambahkan.','settings');}catch(Throwable$e){$this->go(false,$e->getMessage(),'settings');}
|
||||
try{$this->requirePost();$this->requireAction('can_update','Anda tidak mempunyai permission mengubah template posisi.');$positionId=(int)$this->input->post('position_id');$positionQuery=$this->db->where('id',$positionId);if($this->db->field_exists('company_id','k_positions'))$positionQuery->where('company_id',$this->company());if(!$positionId||!$positionQuery->count_all_results('k_positions'))throw new BusinessException('Posisi tidak ditemukan pada perusahaan aktif.');$row=$this->assignmentInput('position',$positionId);if($this->db->get_where('position_payroll_components',array('company_id'=>$this->company(),'position_id'=>$positionId,'component_id'=>$row['component_id'],'effective_from'=>$row['effective_from']))->row())throw new BusinessException('Template komponen dengan tanggal efektif yang sama sudah ada.');$this->db->trans_begin();$previous=$this->db->where(array('company_id'=>$this->company(),'position_id'=>$positionId,'component_id'=>$row['component_id'],'is_active'=>1))->where('effective_from <',$row['effective_from'])->group_start()->where('effective_to IS NULL',null,false)->or_where('effective_to >=',$row['effective_from'])->group_end()->order_by('effective_from','DESC')->get('position_payroll_components')->row();if($previous)$this->db->where(array('id'=>$previous->id,'company_id'=>$this->company()))->update('position_payroll_components',array('effective_to'=>date('Y-m-d',strtotime($row['effective_from'].' -1 day')),'updated_at'=>date('Y-m-d H:i:s')));$this->db->insert('position_payroll_components',$row);$id=(int)$this->db->insert_id();$this->invalidateDraftCalculations();$this->auditservice->record('payroll','position_salary_component',$id,'create_version',$previous,$row,$this->uid());if($this->db->trans_status()===false)throw new RuntimeException('Template posisi gagal disimpan.');$this->db->trans_commit();$this->go(true,'Versi template posisi berhasil ditambahkan. Draft payroll wajib dihitung ulang.','settings');}catch(Throwable$e){$this->db->trans_rollback();$this->go(false,$e->getMessage(),'settings');}
|
||||
}
|
||||
|
||||
public function save_attendance_rule()
|
||||
{
|
||||
try{$condition=$this->input->post('condition_type');$method=$this->attendanceMethod($this->input->post('calculation_method'));$start=$this->validDate($this->input->post('effective_from'));if(!in_array($condition,array('alpha','late','early_leave','unpaid','missing_checkin','missing_checkout'),true)||!$start)throw new BusinessException('Kondisi dan tanggal efektif aturan tidak valid.');$componentId=$this->input->post('component_id')?(int)$this->input->post('component_id'):null;if($method!=='none'&&!$componentId)throw new BusinessException('Komponen potongan wajib dipilih bila aturan menghasilkan potongan.');$formula=$method==='formula'?trim((string)$this->input->post('formula_expression',true))?:null:null;if($method==='formula')$this->validateFormula($formula);$row=array('company_id'=>$this->company(),'component_id'=>$componentId,'rule_name'=>trim((string)$this->input->post('rule_name',true)),'condition_type'=>$condition,'calculation_method'=>$method,'tolerance_minutes'=>max(0,(int)$this->input->post('tolerance_minutes')),'rate_value'=>$this->decimal($this->input->post('rate_value')),'maximum_amount'=>$this->input->post('maximum_amount')!==''?$this->decimal($this->input->post('maximum_amount')):null,'formula_expression'=>$formula,'effective_from'=>$start,'effective_to'=>$this->validDate($this->input->post('effective_to'))?:null,'is_active'=>$this->input->post('is_active')?1:0,'notes'=>trim((string)$this->input->post('notes',true))?:null,'created_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s'));if($row['rule_name']==='')throw new BusinessException('Nama aturan wajib diisi.');$this->db->insert('payroll_attendance_rules',$row);$id=(int)$this->db->insert_id();$this->auditservice->record('payroll','attendance_rule',$id,'create_version',null,$row,$this->uid());$this->go(true,'Versi aturan kehadiran berhasil disimpan.','settings');}catch(Throwable$e){$this->go(false,$e->getMessage(),'settings');}
|
||||
try{$this->requirePost();$this->requireAction('can_update','Anda tidak mempunyai permission mengubah aturan kehadiran payroll.');$condition=$this->input->post('condition_type');$method=$this->attendanceMethod($this->input->post('calculation_method'));$start=$this->validDate($this->input->post('effective_from'));if(!in_array($condition,array('alpha','late','early_leave','unpaid','missing_checkin','missing_checkout'),true)||!$start)throw new BusinessException('Kondisi dan tanggal efektif aturan tidak valid.');$componentId=$this->input->post('component_id')?(int)$this->input->post('component_id'):null;if($method!=='none'&&!$componentId)throw new BusinessException('Komponen potongan wajib dipilih bila aturan menghasilkan potongan.');if($componentId){$component=$this->componentRow($componentId);if($component->component_type!=='deduction')throw new BusinessException('Aturan kehadiran hanya boleh menggunakan komponen potongan.');}if($method==='none')$componentId=null;$formula=$method==='formula'?trim((string)$this->input->post('formula_expression',true))?:null:null;if($method==='formula')$this->validateFormula($formula);$row=array('company_id'=>$this->company(),'component_id'=>$componentId,'rule_name'=>trim((string)$this->input->post('rule_name',true)),'condition_type'=>$condition,'calculation_method'=>$method,'tolerance_minutes'=>max(0,(int)$this->input->post('tolerance_minutes')),'rate_value'=>$this->decimal($this->input->post('rate_value')),'maximum_amount'=>$this->input->post('maximum_amount')!==''?$this->decimal($this->input->post('maximum_amount')):null,'formula_expression'=>$formula,'effective_from'=>$start,'effective_to'=>$this->validDate($this->input->post('effective_to'))?:null,'is_active'=>$this->input->post('is_active')?1:0,'notes'=>trim((string)$this->input->post('notes',true))?:null,'created_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s'));if($row['rule_name']==='')throw new BusinessException('Nama aturan wajib diisi.');$this->db->trans_begin();$this->db->insert('payroll_attendance_rules',$row);$id=(int)$this->db->insert_id();$this->invalidateDraftCalculations();$this->auditservice->record('payroll','attendance_rule',$id,'create_version',null,$row,$this->uid());if($this->db->trans_status()===false)throw new RuntimeException('Aturan kehadiran gagal disimpan.');$this->db->trans_commit();$this->go(true,'Versi aturan kehadiran berhasil disimpan. Draft payroll wajib dihitung ulang.','settings');}catch(Throwable$e){$this->db->trans_rollback();$this->go(false,$e->getMessage(),'settings');}
|
||||
}
|
||||
|
||||
public function save_adjustment()
|
||||
{
|
||||
try{$period=$this->period((int)$this->input->post('payroll_period_id'),false);if($period->workflow_status!=='draft')throw new BusinessException('Penyesuaian hanya dapat dibuat pada payroll Draft.');$employee=$this->employee((int)$this->input->post('employee_id'));if(!empty($employee->salary_restricted)&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');$component=$this->componentRow((int)$this->input->post('component_id'));$type=$this->input->post('adjustment_type');if(!in_array($type,array('earning','deduction'),true))throw new BusinessException('Jenis penyesuaian tidak valid.');$amount=$this->decimal($this->input->post('amount'));$notes=trim((string)$this->input->post('notes',true));if($amount<=0||$notes==='')throw new BusinessException('Nominal harus lebih dari nol dan keterangan wajib diisi.');$row=array('company_id'=>$this->company(),'payroll_period_id'=>$period->id,'employee_id'=>$employee->id,'component_id'=>$component->id,'adjustment_type'=>$type,'amount'=>$amount,'notes'=>$notes,'is_active'=>1,'created_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s'));$this->db->insert('payroll_period_adjustments',$row);$id=(int)$this->db->insert_id();$this->auditservice->record('payroll','period_adjustment',$id,'create',null,$row,$this->uid());$this->go(true,'Penyesuaian periode berhasil ditambahkan.','adjustments');}catch(Throwable$e){$this->go(false,$e->getMessage(),'adjustments');}
|
||||
try{$this->requirePost();$this->requireAction('can_create','Anda tidak mempunyai permission membuat penyesuaian payroll.');$period=$this->period((int)$this->input->post('payroll_period_id'),false);if(!$period)throw new BusinessException('Periode payroll tidak ditemukan.');if($period->workflow_status!=='draft')throw new BusinessException('Penyesuaian hanya dapat dibuat pada payroll Draft.');$employee=$this->employee((int)$this->input->post('employee_id'));if(!empty($employee->salary_restricted)&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');$component=$this->componentRow((int)$this->input->post('component_id'));$type=$this->input->post('adjustment_type');if(!in_array($type,array('earning','deduction'),true))throw new BusinessException('Jenis penyesuaian tidak valid.');if($component->component_type!==$type)throw new BusinessException('Jenis penyesuaian harus sama dengan jenis komponen yang dipilih.');if($type==='earning'&&!$this->canManageAllowances())throw new BusinessException('Bonus dan pendapatan tambahan memerlukan permission Kelola Tunjangan & Bonus.');$amount=$this->decimal($this->input->post('amount'));$notes=trim((string)$this->input->post('notes',true));if($amount<=0||$notes==='')throw new BusinessException('Nominal harus lebih dari nol dan keterangan wajib diisi.');$row=array('company_id'=>$this->company(),'payroll_period_id'=>$period->id,'employee_id'=>$employee->id,'component_id'=>$component->id,'adjustment_type'=>$type,'amount'=>$amount,'notes'=>$notes,'is_active'=>1,'created_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s'));$this->db->trans_begin();$this->db->insert('payroll_period_adjustments',$row);$id=(int)$this->db->insert_id();$this->invalidateDraftCalculations();$this->auditservice->record('payroll','period_adjustment',$id,'create',null,$row,$this->uid());if($this->db->trans_status()===false)throw new RuntimeException('Penyesuaian payroll gagal disimpan.');$this->db->trans_commit();$this->go(true,'Penyesuaian periode berhasil ditambahkan. Draft payroll wajib dihitung ulang.','adjustments');}catch(Throwable$e){$this->db->trans_rollback();$this->go(false,$e->getMessage(),'adjustments');}
|
||||
}
|
||||
|
||||
public function deactivate_adjustment($id)
|
||||
{
|
||||
try{$row=$this->db->select('a.*,p.workflow_status,e.salary_restricted')->from('payroll_period_adjustments a')->join('k_payroll_periods p','p.id=a.payroll_period_id')->join('k_employees e','e.id=a.employee_id')->where(array('a.id'=>(int)$id,'a.company_id'=>$this->company()))->get()->row();if(!$row)throw new BusinessException('Penyesuaian tidak ditemukan.');if($row->workflow_status!=='draft')throw new BusinessException('Penyesuaian payroll yang bukan Draft tidak dapat diubah.');if($row->salary_restricted&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');$update=array('is_active'=>0,'updated_by'=>$this->uid(),'updated_at'=>date('Y-m-d H:i:s'));$this->db->where('id',$row->id)->update('payroll_period_adjustments',$update);$this->auditservice->record('payroll','period_adjustment',$row->id,'deactivate',$row,$update,$this->uid());$this->go(true,'Penyesuaian dinonaktifkan tanpa menghapus histori.','adjustments');}catch(Throwable$e){$this->go(false,$e->getMessage(),'adjustments');}
|
||||
try{$this->requirePost();$this->requireAction('can_update','Anda tidak mempunyai permission mengubah penyesuaian payroll.');$row=$this->db->select('a.*,p.workflow_status,e.salary_restricted,c.code component_code,c.component_type')->from('payroll_period_adjustments a')->join('k_payroll_periods p','p.id=a.payroll_period_id AND p.company_id=a.company_id')->join('k_employees e','e.id=a.employee_id AND e.company_id=a.company_id')->join('payroll_components c','c.id=a.component_id AND c.company_id=a.company_id')->where(array('a.id'=>(int)$id,'a.company_id'=>$this->company()))->get()->row();if(!$row)throw new BusinessException('Penyesuaian tidak ditemukan.');if($row->workflow_status!=='draft')throw new BusinessException('Penyesuaian payroll yang bukan Draft tidak dapat diubah.');if($row->salary_restricted&&!$this->allSalary())throw new BusinessException('Gaji karyawan ini bersifat terbatas.');$this->assertManageComponent((object)array('component_type'=>$row->component_type,'code'=>$row->component_code));$update=array('is_active'=>0,'updated_by'=>$this->uid(),'updated_at'=>date('Y-m-d H:i:s'));$this->db->trans_begin();$this->db->where(array('id'=>$row->id,'company_id'=>$this->company()))->update('payroll_period_adjustments',$update);$this->invalidateDraftCalculations();$this->auditservice->record('payroll','period_adjustment',$row->id,'deactivate',$row,$update,$this->uid());if($this->db->trans_status()===false)throw new RuntimeException('Penyesuaian payroll gagal dinonaktifkan.');$this->db->trans_commit();$this->go(true,'Penyesuaian dinonaktifkan tanpa menghapus histori. Draft payroll wajib dihitung ulang.','adjustments');}catch(Throwable$e){$this->db->trans_rollback();$this->go(false,$e->getMessage(),'adjustments');}
|
||||
}
|
||||
|
||||
public function save_settings()
|
||||
{
|
||||
try{if(!$this->can('can_update'))throw new BusinessException('Anda tidak mempunyai permission mengubah pengaturan payroll.');$day=$this->input->post('default_payment_day');$day=$day===''?null:(int)$day;if($day!==null&&($day<1||$day>31))throw new BusinessException('Tanggal pembayaran default harus 1 sampai 31.');$before=$this->db->get_where('payroll_settings',array('company_id'=>$this->company()))->row();$row=array('separation_of_duties'=>$this->input->post('separation_of_duties')?1:0,'default_payment_day'=>$day,'bank_export_note'=>trim((string)$this->input->post('bank_export_note',true))?:null,'updated_at'=>date('Y-m-d H:i:s'));if($before)$this->db->where('id',$before->id)->update('payroll_settings',$row);else{$row['company_id']=$this->company();$row['created_by']=$this->uid();$row['created_at']=date('Y-m-d H:i:s');$this->db->insert('payroll_settings',$row);}$this->auditservice->record('payroll','payroll_settings',$this->company(),'update',$before,$row,$this->uid());$this->go(true,'Pengaturan umum payroll berhasil disimpan.','settings');}catch(Throwable$e){$this->go(false,$e->getMessage(),'settings');}
|
||||
try{$this->requirePost();$this->requireAction('can_update','Anda tidak mempunyai permission mengubah pengaturan payroll.');if(!$this->canManageAllowances())throw new BusinessException('Pengaturan umum payroll hanya dapat diubah dengan permission Kelola Tunjangan & Bonus.');$day=$this->input->post('default_payment_day');$day=$day===''?null:(int)$day;if($day!==null&&($day<1||$day>31))throw new BusinessException('Tanggal pembayaran default harus 1 sampai 31.');$before=$this->db->get_where('payroll_settings',array('company_id'=>$this->company()))->row();$row=array('separation_of_duties'=>$this->input->post('separation_of_duties')?1:0,'default_payment_day'=>$day,'bank_export_note'=>trim((string)$this->input->post('bank_export_note',true))?:null,'updated_at'=>date('Y-m-d H:i:s'));$this->db->trans_begin();if($before)$this->db->where(array('id'=>$before->id,'company_id'=>$this->company()))->update('payroll_settings',$row);else{$row['company_id']=$this->company();$row['created_by']=$this->uid();$row['created_at']=date('Y-m-d H:i:s');$this->db->insert('payroll_settings',$row);}$this->auditservice->record('payroll','payroll_settings',$this->company(),'update',$before,$row,$this->uid());if($this->db->trans_status()===false)throw new RuntimeException('Pengaturan payroll gagal disimpan.');$this->db->trans_commit();$this->go(true,'Pengaturan umum payroll berhasil disimpan.','settings');}catch(Throwable$e){$this->db->trans_rollback();$this->go(false,$e->getMessage(),'settings');}
|
||||
}
|
||||
|
||||
private function assignmentInput($subject,$subjectId)
|
||||
{
|
||||
$component=$this->componentRow((int)$this->input->post('component_id'));$method=$this->method($this->input->post('calculation_method'));$amount=$this->decimal($this->input->post('amount_override'));$start=$this->validDate($this->input->post('effective_from'));$end=$this->validDate($this->input->post('effective_to'));if(!$start)throw new BusinessException('Tanggal mulai berlaku wajib diisi.');if($end&&$end<$start)throw new BusinessException('Tanggal selesai tidak boleh sebelum tanggal mulai.');$formula=$method==='formula'?trim((string)$this->input->post('formula_expression',true))?:null:null;if($method==='formula')$this->validateFormula($formula);$row=array('company_id'=>$this->company(),$subject.'_id'=>$subjectId,'component_id'=>$component->id,'calculation_method'=>$method,'amount_override'=>$amount,'formula_expression'=>$formula,'effective_from'=>$start,'effective_to'=>$end?:null,'is_active'=>1,'notes'=>trim((string)$this->input->post('notes',true))?:null,'created_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s'));return$row;
|
||||
$component=$this->componentRow((int)$this->input->post('component_id'));$this->assertManageComponent($component);if(!(int)$component->is_active)throw new BusinessException('Komponen payroll yang dipilih sudah nonaktif.');$method=$this->method($this->input->post('calculation_method'));$amount=$this->decimal($this->input->post('amount_override'));$start=$this->validDate($this->input->post('effective_from'));$end=$this->validDate($this->input->post('effective_to'));if(!$start)throw new BusinessException('Tanggal mulai berlaku wajib diisi.');if($end&&$end<$start)throw new BusinessException('Tanggal selesai tidak boleh sebelum tanggal mulai.');$formula=$method==='formula'?trim((string)$this->input->post('formula_expression',true))?:null:null;if($method==='formula')$this->validateFormula($formula);$row=array('company_id'=>$this->company(),$subject.'_id'=>$subjectId,'component_id'=>$component->id,'calculation_method'=>$method,'amount_override'=>$amount,'formula_expression'=>$formula,'effective_from'=>$start,'effective_to'=>$end?:null,'is_active'=>1,'notes'=>trim((string)$this->input->post('notes',true))?:null,'created_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s'));return$row;
|
||||
}
|
||||
private function method($method){$allowed=array('fixed','per_present_day','per_absent_day','per_minute','per_hour','percentage_basic','formula');if(!in_array($method,$allowed,true))throw new BusinessException('Metode perhitungan tidak valid.');return$method;}
|
||||
private function attendanceMethod($method){$allowed=array('none','fixed','per_minute','per_hour','per_day','percentage_daily','formula');if(!in_array($method,$allowed,true))throw new BusinessException('Metode potongan tidak valid.');return$method;}
|
||||
private function validateFormula($formula){if(!$formula)throw new BusinessException('Formula wajib diisi.');$variables=array('basic_salary'=>1,'calendar_days'=>1,'work_days'=>1,'present_days'=>1,'alpha_days'=>1,'unpaid_days'=>1,'unpaid_days_total'=>1,'late_minutes'=>1,'early_leave_minutes'=>1,'missing_checkin'=>1,'missing_checkout'=>1,'overtime_hours'=>1,'overtime_rate'=>1,'daily_rate'=>1,'condition_quantity'=>1,'rule_rate'=>1);$this->formulaservice->calculate($formula,$variables);}
|
||||
private function decimal($value){$number=(float)str_replace(array('. ',','),array('','.'),(string)$value);if($number<0)throw new BusinessException('Nominal atau tarif tidak boleh negatif.');return round($number,2);}
|
||||
private function employee($id){$row=$this->db->select('e.*,d.department_name,p.position_name')->from('k_employees e')->join('k_departments d','d.id=e.department_id','left')->join('k_positions p','p.id=e.position_id','left')->where(array('e.id'=>$id,'e.company_id'=>$this->company()))->get()->row();if(!$row)throw new BusinessException('Karyawan tidak ditemukan pada perusahaan aktif.');return$row;}
|
||||
private function componentRow($id){$row=$this->db->get_where('payroll_components',array('id'=>$id,'company_id'=>$this->company()))->row();if(!$row)throw new BusinessException('Komponen payroll tidak ditemukan.');return$row;}
|
||||
private function componentRow($id){$row=$this->db->get_where('payroll_components',array('id'=>$id,'company_id'=>$this->company()))->row();if(!$row)throw new BusinessException('Komponen payroll tidak ditemukan.');if(!(int)$row->is_active)throw new BusinessException('Komponen payroll yang dipilih sudah nonaktif.');return$row;}
|
||||
private function basicComponentId(){$row=$this->db->get_where('payroll_components',array('company_id'=>$this->company(),'code'=>'BASIC'))->row();return$row?(int)$row->id:0;}
|
||||
private function period($id,$throw=true){$row=$this->db->get_where('k_payroll_periods',array('id'=>(int)$id,'company_id'=>$this->company()))->row();if(!$row&&$throw)throw new BusinessException('Periode payroll tidak ditemukan.');return$row;}
|
||||
|
||||
public function slip($payrollId)
|
||||
{
|
||||
$payroll=$this->db->select('x.*,e.full_name,e.employee_code,e.salary_restricted,r.period_name,r.workflow_status,r.attendance_start_date,r.attendance_end_date,r.payment_date')->from('k_payrolls x')->join('k_employees e','e.id=x.employee_id')->join('k_payroll_periods r','r.id=x.payroll_period_id')->where(array('x.id'=>(int)$payrollId,'x.company_id'=>$this->company(),'r.company_id'=>$this->company()))->get()->row();if(!$payroll||!in_array($payroll->workflow_status,array('final','posted','paid'),true))show_error('Slip belum tersedia.',422);if($payroll->salary_restricted&&!$this->allSalary())show_error('Gaji ini bersifat terbatas.',403);$lines=$this->db->where('payroll_id',$payrollId)->get('payroll_calculation_lines')->result();$this->load->view('hr_payroll/slip',array('payroll'=>$payroll,'lines'=>$lines));
|
||||
if(!$this->can('can_view'))show_error('Anda tidak memiliki izin melihat slip payroll.',403,'Akses ditolak');$payroll=$this->db->select('x.*,e.full_name,e.employee_code,e.salary_restricted,r.period_name,r.workflow_status,r.attendance_start_date,r.attendance_end_date,r.payment_date')->from('k_payrolls x')->join('k_employees e','e.id=x.employee_id AND e.company_id=x.company_id')->join('k_payroll_periods r','r.id=x.payroll_period_id AND r.company_id=x.company_id')->where(array('x.id'=>(int)$payrollId,'x.company_id'=>$this->company()))->get()->row();if(!$payroll||!in_array($payroll->workflow_status,array('final','posted','paid'),true))show_error('Slip belum tersedia.',422);if($payroll->salary_restricted&&!$this->allSalary())show_error('Gaji ini bersifat terbatas.',403);$lines=$this->db->where('payroll_id',$payrollId)->get('payroll_calculation_lines')->result();$this->load->view('hr_payroll/slip',array('payroll'=>$payroll,'lines'=>$lines));
|
||||
}
|
||||
|
||||
public function export_pdf($id)
|
||||
{
|
||||
try{$period=$this->exportablePeriod($id);$rows=$this->payrollRows($id);$company=$this->db->get_where('companies',array('id'=>$this->company()))->row();$html=$this->load->view('hr_payroll/register_pdf',array('period'=>$period,'rows'=>$rows,'company'=>$company),true);$dompdf=new Dompdf\Dompdf();$dompdf->loadHtml($html);$dompdf->setPaper('A4','landscape');$dompdf->render();$dompdf->stream('payroll-'.$period->id.'.pdf',array('Attachment'=>true));}catch(Throwable$e){show_error($e->getMessage(),422);}
|
||||
try{$this->requireAction('can_export','Anda tidak mempunyai permission export payroll.');$period=$this->exportablePeriod($id);$rows=$this->payrollRows($id);$company=$this->db->get_where('companies',array('id'=>$this->company()))->row();$html=$this->load->view('hr_payroll/register_pdf',array('period'=>$period,'rows'=>$rows,'company'=>$company),true);$dompdf=new Dompdf\Dompdf();$dompdf->loadHtml($html);$dompdf->setPaper('A4','landscape');$dompdf->render();$dompdf->stream('payroll-'.$period->id.'.pdf',array('Attachment'=>true));}catch(Throwable$e){show_error($e->getMessage(),$e instanceof BusinessException?422:500);}
|
||||
}
|
||||
public function export_excel($id,$bank='general')
|
||||
{
|
||||
try{$period=$this->exportablePeriod($id);$bank=strtolower($bank);if(!in_array($bank,array('general','bri','bca'),true))throw new BusinessException('Format bank tidak tersedia.');$rows=$this->payrollRows($id,$bank==='general'?null:$bank);if(!$rows)throw new BusinessException('Tidak ada data payroll untuk format yang dipilih.');$spreadsheet=new PhpOffice\PhpSpreadsheet\Spreadsheet();$sheet=$spreadsheet->getActiveSheet();$sheet->setTitle($bank==='general'?'Payroll Register':strtoupper($bank).' Source');$headers=$bank==='general'?array('No','Kode Karyawan','Nama Karyawan','Bank','Nomor Rekening','Gaji Pokok','Bruto','Potongan','Neto','Tanggal Efektif'):array('No','Nomor Rekening','Nama Penerima','Nominal Neto','Berita Pembayaran','Kode Karyawan','Tanggal Efektif');$sheet->fromArray($headers,null,'A1');$line=2;foreach($rows as$i=>$row){$values=$bank==='general'?array($i+1,$row->employee_code,$row->full_name,$row->bank_name,$row->bank_account_number,(float)$row->basic_salary,(float)$row->gross_salary,(float)$row->total_deductions,(float)$row->total_salary,$period->payment_date):array($i+1,$row->bank_account_number,$row->bank_account_name?:$row->full_name,(float)$row->total_salary,'Gaji '.$period->period_name,$row->employee_code,$period->payment_date);$sheet->fromArray($values,null,'A'.$line++);$accountColumn=$bank==='general'?'E':'B';$sheet->setCellValueExplicit($accountColumn.($line-1),(string)$row->bank_account_number,PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);}$sheet->getStyle('A1:'.$sheet->getHighestColumn().'1')->getFont()->setBold(true);foreach(range('A',$sheet->getHighestColumn())as$column)$sheet->getColumnDimension($column)->setAutoSize(true);if($bank!=='general'){$guide=$spreadsheet->createSheet();$guide->setTitle('PETUNJUK');$guide->fromArray(array(array('File sumber '.strtoupper($bank)),array('Gunakan converter/checksum resmi portal bank perusahaan bila diwajibkan.'),array('Periksa nama, rekening, nominal, dan tanggal efektif sebelum otorisasi.')),null,'A1');$guide->getColumnDimension('A')->setWidth(100);}header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');header('Content-Disposition: attachment; filename="payroll-'.$bank.'-'.$period->id.'.xlsx"');header('Cache-Control: max-age=0');(new PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save('php://output');exit;}catch(Throwable$e){show_error($e->getMessage(),422);}
|
||||
try{$this->requireAction('can_export','Anda tidak mempunyai permission export payroll.');$period=$this->exportablePeriod($id);$bank=strtolower($bank);if(!in_array($bank,array('general','bri','bca'),true))throw new BusinessException('Format bank tidak tersedia.');$rows=$this->payrollRows($id,$bank==='general'?null:$bank);if(!$rows)throw new BusinessException('Tidak ada data payroll untuk format yang dipilih.');$spreadsheet=new PhpOffice\PhpSpreadsheet\Spreadsheet();$sheet=$spreadsheet->getActiveSheet();$sheet->setTitle($bank==='general'?'Payroll Register':strtoupper($bank).' Source');$headers=$bank==='general'?array('No','Kode Karyawan','Nama Karyawan','Bank','Nomor Rekening','Gaji Pokok','Bruto','Potongan','Neto','Tanggal Efektif'):array('No','Nomor Rekening','Nama Penerima','Nominal Neto','Berita Pembayaran','Kode Karyawan','Tanggal Efektif');$sheet->fromArray($headers,null,'A1');$line=2;foreach($rows as$i=>$row){$values=$bank==='general'?array($i+1,$row->employee_code,$row->full_name,$row->bank_name,$row->bank_account_number,(float)$row->basic_salary,(float)$row->gross_salary,(float)$row->total_deductions,(float)$row->total_salary,$period->payment_date):array($i+1,$row->bank_account_number,$row->bank_account_name?:$row->full_name,(float)$row->total_salary,'Gaji '.$period->period_name,$row->employee_code,$period->payment_date);$sheet->fromArray($values,null,'A'.$line++);$accountColumn=$bank==='general'?'E':'B';$sheet->setCellValueExplicit($accountColumn.($line-1),(string)$row->bank_account_number,PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);}$sheet->getStyle('A1:'.$sheet->getHighestColumn().'1')->getFont()->setBold(true);foreach(range('A',$sheet->getHighestColumn())as$column)$sheet->getColumnDimension($column)->setAutoSize(true);if($bank!=='general'){$guide=$spreadsheet->createSheet();$guide->setTitle('PETUNJUK');$guide->fromArray(array(array('File sumber '.strtoupper($bank)),array('Gunakan converter/checksum resmi portal bank perusahaan bila diwajibkan.'),array('Periksa nama, rekening, nominal, dan tanggal efektif sebelum otorisasi.')),null,'A1');$guide->getColumnDimension('A')->setWidth(100);}header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');header('Content-Disposition: attachment; filename="payroll-'.$bank.'-'.$period->id.'.xlsx"');header('Cache-Control: max-age=0');(new PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save('php://output');exit;}catch(Throwable$e){show_error($e->getMessage(),$e instanceof BusinessException?422:500);}
|
||||
}
|
||||
private function exportablePeriod($id){$period=$this->period($id);if(!in_array($period->workflow_status,array('final','posted','paid'),true))throw new BusinessException('Export tersedia setelah payroll berstatus Final.');if(!$this->allSalary()&&$this->hasRestrictedPeriod($id))throw new BusinessException('Export periode ini memuat gaji terbatas.');return$period;}
|
||||
private function hasRestrictedPeriod($id){return$this->db->from('k_payrolls x')->join('k_employees e','e.id=x.employee_id')->where(array('x.payroll_period_id'=>(int)$id,'x.company_id'=>$this->company(),'e.salary_restricted'=>1))->count_all_results()>0;}
|
||||
private function payrollRows($id,$bank=null){$q=$this->db->select('x.*,e.employee_code,e.full_name,e.bank_name,e.bank_account_number,e.bank_account_name,e.salary_restricted')->from('k_payrolls x')->join('k_employees e','e.id=x.employee_id')->where(array('x.payroll_period_id'=>(int)$id,'x.company_id'=>$this->company()));if(!$this->allSalary())$q->where('e.salary_restricted',0);if($bank){$q->group_start()->like('e.bank_name',$bank);if($bank==='bri')$q->or_like('e.bank_name','Rakyat Indonesia');if($bank==='bca')$q->or_like('e.bank_name','Central Asia');$q->group_end();}$rows=$q->order_by('e.full_name')->get()->result();if($bank){$missing=array();foreach($rows as$row)if(trim((string)$row->bank_account_number)===''||trim((string)$row->bank_account_name)==='')$missing[]=$row->full_name;if($missing)throw new BusinessException('Data rekening belum lengkap: '.implode(', ',$missing).'.');}return$rows;}
|
||||
private function hasRestrictedPeriod($id){return$this->db->from('k_payrolls x')->join('k_employees e','e.id=x.employee_id AND e.company_id=x.company_id')->where(array('x.payroll_period_id'=>(int)$id,'x.company_id'=>$this->company(),'e.salary_restricted'=>1))->count_all_results()>0;}
|
||||
private function payrollRows($id,$bank=null){$q=$this->db->select('x.*,e.employee_code,e.full_name,e.bank_name,e.bank_account_number,e.bank_account_name,e.salary_restricted')->from('k_payrolls x')->join('k_employees e','e.id=x.employee_id AND e.company_id=x.company_id')->where(array('x.payroll_period_id'=>(int)$id,'x.company_id'=>$this->company()));if(!$this->allSalary())$q->where('e.salary_restricted',0);if($bank){$q->group_start()->like('e.bank_name',$bank);if($bank==='bri')$q->or_like('e.bank_name','Rakyat Indonesia');if($bank==='bca')$q->or_like('e.bank_name','Central Asia');$q->group_end();}$rows=$q->order_by('e.full_name')->get()->result();if($bank){$missing=array();foreach($rows as$row)if(trim((string)$row->bank_account_number)===''||trim((string)$row->bank_account_name)==='')$missing[]=$row->full_name;if($missing)throw new BusinessException('Data rekening belum lengkap: '.implode(', ',$missing).'.');}return$rows;}
|
||||
}
|
||||
|
||||
@@ -65,9 +65,12 @@ class Invoices extends MY_Controller
|
||||
public function lookup_warehouses(){return$this->selectLookup('warehouses','nama',array('nama'),'Gudang ditemukan.');}
|
||||
public function lookup_barcodes()
|
||||
{
|
||||
$q=trim((string)$this->input->get('q'));$item=(int)$this->input->get('item_id');$warehouse=(int)$this->input->get('warehouse_id');$exact=(int)$this->input->get('exact')===1;$page=max(1,(int)$this->input->get('page'));$limit=20;
|
||||
$q=trim((string)$this->input->get('q'));$item=(int)$this->input->get('item_id');$warehouse=(int)$this->input->get('warehouse_id');$exact=(int)$this->input->get('exact')===1;$page=max(1,(int)$this->input->get('page'));$limit=20;$editingLine=(int)$this->input->get('editing_line_id');
|
||||
if(!$exact&&(!$item||!$warehouse))return json_response(true,'Pilih gudang dan barang terlebih dahulu.',array('results'=>array(),'pagination'=>array('more'=>false)));
|
||||
$this->db->select("b.id,CONCAT(b.barcode,IF(NULLIF(b.serial_number,'') IS NULL,'',CONCAT(' / ',b.serial_number)),' - tersedia ',FORMAT(b.qty_sisa-b.reserved_qty-COALESCE(b.return_reserved_qty,0),0,'id_ID')) text,b.barcode,b.serial_number,b.item_id,b.warehouse_id,(b.qty_sisa-b.reserved_qty-COALESCE(b.return_reserved_qty,0)) available,i.kode_detail,i.nama_barang,i.harga_jual,k.tracking_type,k.unit,w.nama warehouse_name",false)->from('item_barcodes b')->join('items i','i.id=b.item_id')->join('kode_barang k','k.id=i.kode_id','left')->join('warehouses w','w.id=b.warehouse_id')->where(array('i.company_id'=>$this->company(),'b.status'=>'available'))->where('(b.qty_sisa-b.reserved_qty-COALESCE(b.return_reserved_qty,0)) >',0,false)->where("(UPPER(COALESCE(k.tracking_type,'QTY'))<>'UNIT' OR NOT EXISTS(SELECT 1 FROM invoice_line_barcodes used JOIN invoice_details used_detail ON used_detail.id=used.invoice_detail_id AND used_detail.stock_posted_at IS NULL JOIN invoices used_invoice ON used_invoice.id=used.invoice_id WHERE used.barcode_id=b.id AND used_invoice.company_id=i.company_id AND used_invoice.deleted_at IS NULL AND COALESCE(used_invoice.status,'')<>'cancelled'))",null,false);if($item)$this->db->where('b.item_id',$item);if($warehouse)$this->db->where('b.warehouse_id',$warehouse);if($q!==''){$this->db->group_start();if($exact)$this->db->where('b.barcode',$q)->or_where('b.serial_number',$q);else$this->db->like('b.barcode',$q)->or_like('b.serial_number',$q);$this->db->group_end();}$rows=$this->db->order_by('b.id')->limit($limit+1,($page-1)*$limit)->get()->result();$more=count($rows)>$limit;if($more)array_pop($rows);return json_response(true,'Barcode ditemukan.',array('results'=>$rows,'pagination'=>array('more'=>$more)));
|
||||
if($editingLine){$owned=$this->db->select('d.id,inv.created_by')->from('invoice_details d')->join('invoices inv','inv.id=d.invoice_id AND inv.company_id=d.company_id')->where(array('d.id'=>$editingLine,'d.company_id'=>$this->company(),'inv.workflow_status'=>'draft'))->get()->row();if(!$owned||(!is_master_admin_user()&&(int)$owned->created_by!==$this->uid()))$editingLine=0;}
|
||||
$current="COALESCE((SELECT SUM(current_line.qty) FROM invoice_line_barcodes current_line WHERE current_line.invoice_detail_id=".$editingLine." AND current_line.company_id=i.company_id AND current_line.barcode_id=b.id),0)";
|
||||
$available="(b.qty_sisa-COALESCE(b.reserved_qty,0)-COALESCE(b.return_reserved_qty,0)+{$current})";
|
||||
$this->db->select("b.id,CONCAT(b.barcode,IF(NULLIF(b.serial_number,'') IS NULL,'',CONCAT(' / ',b.serial_number)),' - tersedia ',FORMAT({$available},0,'id_ID')) text,b.barcode,b.serial_number,b.item_id,b.warehouse_id,{$available} available,i.kode_detail,i.nama_barang,i.harga_jual,k.tracking_type,k.unit,w.nama warehouse_name",false)->from('item_barcodes b')->join('items i','i.id=b.item_id')->join('kode_barang k','k.id=i.kode_id','left')->join('warehouses w','w.id=b.warehouse_id')->where('i.company_id',$this->company())->where("(b.status='available' OR ({$editingLine}>0 AND {$current}>0))",null,false)->where("{$available} > 0",null,false)->where("(UPPER(COALESCE(k.tracking_type,'QTY'))<>'UNIT' OR NOT EXISTS(SELECT 1 FROM invoice_line_barcodes used JOIN invoice_details used_detail ON used_detail.id=used.invoice_detail_id AND used_detail.stock_posted_at IS NULL JOIN invoices used_invoice ON used_invoice.id=used.invoice_id WHERE used.barcode_id=b.id AND used_detail.id<>".$editingLine." AND used_invoice.company_id=i.company_id AND used_invoice.deleted_at IS NULL AND COALESCE(used_invoice.status,'')<>'cancelled'))",null,false);if($item)$this->db->where('b.item_id',$item);if($warehouse)$this->db->where('b.warehouse_id',$warehouse);if($q!==''){$this->db->group_start();if($exact)$this->db->where('b.barcode',$q)->or_where('b.serial_number',$q);else$this->db->like('b.barcode',$q)->or_like('b.serial_number',$q);$this->db->group_end();}$rows=$this->db->order_by('b.id')->limit($limit+1,($page-1)*$limit)->get()->result();$more=count($rows)>$limit;if($more)array_pop($rows);return json_response(true,'Barcode ditemukan.',array('results'=>$rows,'pagination'=>array('more'=>$more)));
|
||||
}
|
||||
private function selectLookup($table,$label,array$fields,$message)
|
||||
{
|
||||
@@ -104,7 +107,7 @@ class Invoices extends MY_Controller
|
||||
}, 'Semua item berhasil ditambahkan ke invoice.');
|
||||
}
|
||||
public function remove_line($invoiceId,$lineId){return$this->mutation('can_update',function()use($invoiceId,$lineId){return$this->salesservice->removeInvoiceLine($invoiceId,$lineId,$this->uid());},'Item berhasil dihapus, reservasi dilepas, dan jurnal awal dibalik.');}
|
||||
public function update_line($invoiceId,$lineId){return$this->mutation('can_update',function()use($invoiceId,$lineId){return$this->salesservice->updateNonInventoryLine($invoiceId,$lineId,$this->input->post(),$this->uid());},'Item nonbarang berhasil diperbarui.');}
|
||||
public function update_line($invoiceId,$lineId){return$this->mutation('can_update',function()use($invoiceId,$lineId){$row=$this->input->post();$bars=json_decode((string)$this->input->post('barcodes'),true);$row['barcodes']=is_array($bars)?$bars:array();return$this->salesservice->updateInvoiceLine($invoiceId,$lineId,$row,$this->uid());},'Item invoice berhasil diperbarui, reservasi dan jurnal telah disinkronkan.');}
|
||||
public function generate_delivery($invoiceId){return$this->mutation('can_update',function()use($invoiceId){return$this->salesservice->generateDelivery($invoiceId,$this->uid());},'Surat Jalan berhasil dibuat sebagai Draft.');}
|
||||
public function submit($id){return$this->mutation('can_submit',function()use($id){return$this->salesservice->submitInvoice($id,$this->uid());},'Invoice berhasil diajukan.');}
|
||||
public function approve($id){return$this->mutation('can_approve',function()use($id){return$this->salesservice->approveAndPostInvoice($id,$this->uid(),$this->roleId());},'Invoice disetujui dan otomatis diposting.');}
|
||||
|
||||
@@ -38,6 +38,9 @@ class Jurnal extends MY_Controller {
|
||||
$start = $_POST['start'];
|
||||
$length = $_POST['length'];
|
||||
$search = $_POST['search']['value'];
|
||||
$status = (string) $this->input->post('status', true);
|
||||
$allowedStatuses = array('draft', 'submitted', 'approved', 'posted', 'rejected', 'reversed');
|
||||
if (!in_array($status, $allowedStatuses, true)) $status = '';
|
||||
|
||||
// ================= TOTAL ALL
|
||||
$totalAll = $this->db->where('company_id',$this->companycontext->id())->count_all_results('journals');
|
||||
@@ -45,6 +48,10 @@ class Jurnal extends MY_Controller {
|
||||
// ================= QUERY FILTERED (HITUNG)
|
||||
$this->db->from('journals')->where('company_id',$this->companycontext->id());
|
||||
|
||||
if ($status !== '') {
|
||||
$this->db->where('status', $status);
|
||||
}
|
||||
|
||||
if(!empty($search)){
|
||||
$this->db->group_start();
|
||||
$this->db->like('no_ref', $search);
|
||||
@@ -63,6 +70,9 @@ class Jurnal extends MY_Controller {
|
||||
journals.keterangan,
|
||||
journals.status,
|
||||
journals.is_reversed,
|
||||
journals.ref_type,
|
||||
journals.created_by,
|
||||
journals.rejection_reason,
|
||||
COALESCE(SUM(journal_details.debit),0) as total_debit,
|
||||
COALESCE(SUM(journal_details.kredit),0) as total_kredit
|
||||
');
|
||||
@@ -70,6 +80,10 @@ class Jurnal extends MY_Controller {
|
||||
$this->db->join('journal_details','journal_details.journal_id = journals.id','LEFT');
|
||||
$this->db->where('journals.company_id',$this->companycontext->id());
|
||||
|
||||
if ($status !== '') {
|
||||
$this->db->where('journals.status', $status);
|
||||
}
|
||||
|
||||
if(!empty($search)){
|
||||
$this->db->group_start();
|
||||
$this->db->like('journals.no_ref', $search);
|
||||
@@ -92,12 +106,17 @@ class Jurnal extends MY_Controller {
|
||||
foreach ($query as $row) {
|
||||
$no++;
|
||||
|
||||
$isAdmin = $this->session->userdata('role') === 'Admin';
|
||||
$isAdmin = is_master_admin_user() || $this->session->userdata('role') === 'Admin';
|
||||
$isOwner = (int)$row->created_by === (int)$this->session->userdata('user_id');
|
||||
$aksi = '<button class="btn btn-sm btn-info btn-detail" data-id="'.$row->id.'">Detail</button> ';
|
||||
if ($row->status === 'draft') {
|
||||
$aksi .= '<button class="btn btn-sm btn-primary btn-submit" data-id="'.$row->id.'">Submit</button> ';
|
||||
if ($isAdmin) $aksi .= '<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Hapus</button> ';
|
||||
}
|
||||
if ($row->status === 'rejected' && ($isOwner || $isAdmin) && in_array((string)$row->ref_type, array('', 'manual_journal'), true)) {
|
||||
$aksi .= '<button class="btn btn-sm btn-warning btn-edit-rejected" data-id="'.$row->id.'">Edit & Koreksi</button> ';
|
||||
if ($isAdmin) $aksi .= '<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Hapus</button> ';
|
||||
}
|
||||
if ($isAdmin && $row->status === 'submitted') {
|
||||
$aksi .= '<button class="btn btn-sm btn-success btn-approve" data-id="'.$row->id.'">Approve</button> ';
|
||||
$aksi .= '<button class="btn btn-sm btn-outline-danger btn-reject" data-id="'.$row->id.'">Reject</button> ';
|
||||
@@ -181,6 +200,18 @@ class Jurnal extends MY_Controller {
|
||||
}
|
||||
}
|
||||
|
||||
public function update_rejected($id)
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
|
||||
try {
|
||||
$accounts=(array)$this->input->post('account_id');$debit=(array)$this->input->post('debit');$kredit=(array)$this->input->post('kredit');$lines=array();
|
||||
foreach($accounts as$index=>$accountId){$d=isset($debit[$index])?$this->parseNominal($debit[$index]):0;$k=isset($kredit[$index])?$this->parseNominal($kredit[$index]):0;if($d<=0&&$k<=0)continue;$lines[]=array('account_id'=>(int)$accountId,'debit'=>$d,'kredit'=>$k);}
|
||||
$journal=$this->journalservice->reviseRejected((int)$id,array('tanggal'=>(string)$this->input->post('tanggal',true),'keterangan'=>trim((string)$this->input->post('keterangan',true))),$lines,(int)$this->session->userdata('user_id'));
|
||||
log_activity('Jurnal','revise_rejected','Mengoreksi jurnal ditolak '.$journal->no_ref.' menjadi Draft.','success');
|
||||
return json_response(true,'Koreksi jurnal berhasil disimpan sebagai Draft dan dapat diajukan kembali.',array('journal_id'=>$journal->id));
|
||||
} catch (Throwable $exception) { return business_exception_response($exception); }
|
||||
}
|
||||
|
||||
/** Menerima nominal raw maupun berformat 20,000 / 20.000 / 20.000,50. */
|
||||
private function parseNominal($value)
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ class Professionalreports extends MY_Controller
|
||||
public function __construct(){parent::__construct();$this->requirePermission('reports');$this->load->database();$this->load->library(array('ProfessionalReportService','CompanyContext','JobQueueService'));}
|
||||
private function filter(){$to=$this->input->get_post('to')?:date('Y-m-d');return array('from'=>$this->input->get_post('from')?:date('Y-m-01',strtotime($to)),'to'=>$to,'account_id'=>(int)$this->input->get_post('account_id'));}
|
||||
private function reportFilter($code,array$f){if($code==='balance_sheet')$f['from']='1900-01-01';return$f;}
|
||||
public function index(){$code=$this->input->get('report')?:'trial_balance';$f=$this->filter();$reportFilters=$this->reportFilter($code,$f);$d=array('active_menu'=>'professional_reports','report'=>$code,'filters'=>$f,'rows'=>$this->professionalreportservice->run($code,$reportFilters),'profit_loss_rows'=>$code==='balance_sheet'?$this->professionalreportservice->run('profit_loss',$reportFilters):array());$d['accounts']=$this->db->where('company_id',$this->companycontext->id())->order_by('kode_akun')->get('accounts')->result();$d['company']=$this->db->get_where('companies',array('id'=>$this->companycontext->id()))->row();$this->load->view('partials/header',$d);$this->load->view('professional_reports/index',$d);$this->load->view('partials/footer');}
|
||||
public function index(){$code=$this->input->get('report')?:'trial_balance';$f=$this->filter();$reportFilters=$this->reportFilter($code,$f);$rows=$this->professionalreportservice->run($code,$reportFilters);$summary=array();if($code==='cash_flow')$summary=$this->professionalreportservice->cashFlowSummary($reportFilters,$rows);elseif($code==='general_ledger')$summary=$this->professionalreportservice->ledgerSummary($reportFilters);elseif($code==='receivable_reconciliation')$summary=$this->professionalreportservice->receivableSummary($reportFilters,$rows);$d=array('active_menu'=>'professional_reports','report'=>$code,'filters'=>$f,'rows'=>$rows,'report_summary'=>$summary,'profit_loss_rows'=>$code==='balance_sheet'?$this->professionalreportservice->run('profit_loss',$reportFilters):array());$d['accounts']=$this->db->where('company_id',$this->companycontext->id())->order_by('kode_akun')->get('accounts')->result();$d['company']=$this->db->get_where('companies',array('id'=>$this->companycontext->id()))->row();$this->load->view('partials/header',$d);$this->load->view('professional_reports/index',$d);$this->load->view('partials/footer');}
|
||||
public function export($code){$f=$this->reportFilter($code,$this->filter());$rows=$this->professionalreportservice->run($code,$f);$this->output->set_content_type('text/csv')->set_header('Content-Disposition: attachment; filename="'.$code.'-'.$f['to'].'.csv"');$h=fopen('php://output','w');if($rows){fputcsv($h,array_keys($rows[0]));foreach($rows as$r)fputcsv($h,$r);}fclose($h);}
|
||||
public function save_filter(){$this->db->insert('saved_report_filters',array('user_id'=>$this->session->userdata('user_id'),'report_code'=>$this->input->post('report_code'),'name'=>$this->input->post('name',true),'filter_json'=>json_encode($this->filter()),'created_at'=>date('Y-m-d H:i:s'),'company_id'=>$this->companycontext->id()));redirect('professionalreports?report='.$this->input->post('report_code'));}
|
||||
public function snapshot($code){$f=$this->reportFilter($code,$this->filter());$rows=$this->professionalreportservice->run($code,$f);$hash=hash('sha256',json_encode($f));if(!$this->db->get_where('report_snapshots',array('report_code'=>$code,'period_end'=>$f['to'],'filter_hash'=>$hash,'company_id'=>$this->companycontext->id()))->row())$this->db->insert('report_snapshots',array('report_code'=>$code,'period_end'=>$f['to'],'filter_hash'=>$hash,'payload'=>json_encode($rows),'created_by'=>$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s'),'company_id'=>$this->companycontext->id()));redirect('professionalreports?report='.$code.'&to='.$f['to']);}
|
||||
|
||||
@@ -29,7 +29,7 @@ class Roles extends MY_Admin_Controller
|
||||
if($existing&&!empty($existing->is_super_admin))return json_response(false,'Permission Master Admin bersifat penuh dan tidak dapat dibatasi.',array(),422);
|
||||
$duplicate=$this->db->where('nama_role',$name)->where('id !=',$id)->count_all_results('roles');if($duplicate)return json_response(false,'Nama role sudah digunakan.',array(),422);
|
||||
$posted=(array)$this->input->post('permissions');$permissions=array();
|
||||
foreach($this->features as$key=>$label){$allowedActions=array_keys($this->actions);if($key==='payroll')$allowedActions[]='can_manage_all_salary';$selected=array_values(array_intersect((array)(isset($posted[$key])?$posted[$key]:array()),$allowedActions));if($selected){if(!in_array('can_view',$selected,true))array_unshift($selected,'can_view');$permissions[$key]=$selected;}}
|
||||
foreach($this->features as$key=>$label){$allowedActions=array_keys($this->actions);if($key==='payroll')$allowedActions=array_merge($allowedActions,array('can_manage_all_salary','can_manage_allowances'));$selected=array_values(array_intersect((array)(isset($posted[$key])?$posted[$key]:array()),$allowedActions));if($key==='payroll'&&in_array('can_manage_allowances',$selected,true)&&!in_array('can_update',$selected,true))$selected[]='can_update';if($selected){if(!in_array('can_view',$selected,true))array_unshift($selected,'can_view');$permissions[$key]=$selected;}}
|
||||
if(!$permissions)return json_response(false,'Role wajib mempunyai minimal satu permission.',array(),422);
|
||||
$data=array('nama_role'=>$name,'description'=>$description,'permissions'=>json_encode($permissions),'is_active'=>$this->input->post('is_active')?1:0,'updated_at'=>date('Y-m-d H:i:s'));
|
||||
if($id)$this->db->where('id',$id)->update('roles',$data);else{$data['is_system']=0;$data['is_super_admin']=0;$data['created_at']=date('Y-m-d H:i:s');$this->db->insert('roles',$data);$id=$this->db->insert_id();}
|
||||
|
||||
@@ -480,7 +480,9 @@ class Teknisi extends MY_Controller {
|
||||
"customers" => $this->db->select('id,nama')->where('company_id',(int)$this->companycontext->id())->order_by('nama')->get('customers')->result(),
|
||||
"can_export" => is_master_admin_user() || check_permission('technician_equipment','can_export') || check_permission('items','can_export'),
|
||||
"equipment_permissions" => $this->equipmentPermissionFlags(),
|
||||
"equipment_dashboard" => $this->scopedEquipmentDashboard()
|
||||
"equipment_dashboard" => $this->scopedEquipmentDashboard(),
|
||||
"initial_filters" => $this->logFilters(),
|
||||
"active_panel" => "history"
|
||||
];
|
||||
|
||||
$this->load->view('partials/header', $data);
|
||||
|
||||
@@ -56,7 +56,7 @@ class MY_Controller extends CI_Controller
|
||||
'accountingperiods'=>'jurnal','professionalreports'=>'reports','budgets'=>'budgets',
|
||||
'taxes'=>'taxes','roles'=>'setting','users'=>'setting','employees'=>array('hr_employees','employees'),'generateschedule'=>array('hr_schedules','employees'),
|
||||
'attendancemonitoring'=>array('hr_attendance','employees'),'attendancemonthly'=>array('hr_attendance','employees'),'attendancmonthly'=>array('hr_attendance','employees'),
|
||||
'shifts'=>array('hr_schedules','employees'),'holidays'=>array('hr_schedules','employees'),'leaverequests'=>array('hr_leave','employees'),'hrpayroll'=>array('payroll','employees'),'payroll'=>array('payroll','employees'),'organizationmaster'=>array('hr_organization','employees')
|
||||
'shifts'=>array('hr_schedules','employees'),'holidays'=>array('hr_schedules','employees'),'leaverequests'=>array('hr_leave','employees'),'hrpayroll'=>'payroll','payroll'=>'payroll','organizationmaster'=>array('hr_organization','employees')
|
||||
);
|
||||
if (!isset($features[$class])) return;
|
||||
$action = 'can_view';
|
||||
|
||||
@@ -15,6 +15,7 @@ class JournalService
|
||||
$this->CI->load->library('TransactionService');
|
||||
$this->CI->load->library('FiscalPeriodService');
|
||||
$this->CI->load->library('JournalDescriptionService');
|
||||
$this->CI->load->library('AuditService');
|
||||
if ($this->CI->db->table_exists('companies')) $this->CI->load->library('CompanyContext');
|
||||
}
|
||||
|
||||
@@ -57,7 +58,50 @@ class JournalService
|
||||
return array('debit' => round($debit, 2), 'kredit' => round($credit, 2));
|
||||
}
|
||||
|
||||
private function validateAndNormalize(array $header, array $lines)
|
||||
public function reviseRejected($journalId, array $header, array $lines, $userId)
|
||||
{
|
||||
return $this->CI->transactionservice->run(function () use ($journalId, $header, $lines, $userId) {
|
||||
$journal = $this->CI->db->query(
|
||||
'SELECT * FROM journals WHERE id=? AND company_id=? FOR UPDATE',
|
||||
array((int)$journalId, (int)$this->CI->companycontext->id())
|
||||
)->row();
|
||||
if (!$journal) throw new BusinessException('Jurnal tidak ditemukan.');
|
||||
if ($journal->status !== 'rejected') throw new BusinessException('Hanya jurnal yang ditolak yang dapat dikoreksi.');
|
||||
if (!in_array((string)$journal->ref_type, array('', 'manual_journal'), true)) throw new BusinessException('Jurnal otomatis tidak dapat diedit melalui Jurnal Umum.');
|
||||
if ((int)$journal->created_by !== (int)$userId && !(function_exists('is_master_admin_user') && is_master_admin_user()) && $this->CI->session->userdata('role') !== 'Admin') throw new BusinessException('Hanya pembuat jurnal atau Admin yang dapat melakukan koreksi.');
|
||||
|
||||
$header['no_ref'] = $journal->no_ref;
|
||||
$header['ref_type'] = $journal->ref_type ?: 'manual_journal';
|
||||
$header['ref_id'] = $journal->ref_id;
|
||||
$header['created_by'] = $journal->created_by;
|
||||
$normalized = $this->validateAndNormalize($header, $lines, (int)$journal->id);
|
||||
$before = array('journal'=>$journal, 'lines'=>$this->CI->journalModel->lines($journal->id));
|
||||
$update = array(
|
||||
'tanggal'=>$normalized['header']['tanggal'],
|
||||
'keterangan'=>$normalized['header']['keterangan'],
|
||||
'accounting_period_id'=>$normalized['header']['accounting_period_id'],
|
||||
'status'=>'draft',
|
||||
'submitted_at'=>null,
|
||||
'submitted_by'=>null,
|
||||
'approved_at'=>null,
|
||||
'approved_by'=>null,
|
||||
'updated_by'=>(int)$userId,
|
||||
'updated_at'=>date('Y-m-d H:i:s'),
|
||||
);
|
||||
$this->CI->db->where('id', $journal->id)->update('journals', $update);
|
||||
$this->CI->db->where('journal_id', $journal->id)->delete('journal_details');
|
||||
$details=array();foreach($normalized['lines']as$line){$line['journal_id']=$journal->id;$details[]=$line;}
|
||||
$this->CI->journalModel->addLines($details);
|
||||
$notes='Jurnal dikoreksi dan dikembalikan menjadi Draft.';
|
||||
if(trim((string)$journal->rejection_reason)!=='')$notes.=' Alasan penolakan sebelumnya: '.$journal->rejection_reason;
|
||||
$this->CI->journalModel->addStatusHistory($journal->id,'rejected','draft',$userId,$notes);
|
||||
$after=array('journal'=>$this->CI->journalModel->find($journal->id),'lines'=>$this->CI->journalModel->lines($journal->id));
|
||||
$this->CI->auditservice->record('journal','journal',$journal->id,'revise_rejected',$before,$after,$userId);
|
||||
return $after['journal'];
|
||||
});
|
||||
}
|
||||
|
||||
private function validateAndNormalize(array $header, array $lines, $excludeJournalId = 0)
|
||||
{
|
||||
foreach (array('tanggal', 'no_ref', 'keterangan', 'created_by') as $required) {
|
||||
if (!isset($header[$required]) || trim((string) $header[$required]) === '') {
|
||||
@@ -69,7 +113,7 @@ class JournalService
|
||||
if (!$date || $date->format('Y-m-d') !== $header['tanggal']) {
|
||||
throw new BusinessException('Tanggal jurnal tidak valid.');
|
||||
}
|
||||
if ($this->CI->journalModel->referenceExists($header['no_ref'])) {
|
||||
if ($this->CI->journalModel->referenceExists($header['no_ref'], $excludeJournalId)) {
|
||||
throw new BusinessException('Nomor referensi jurnal sudah digunakan.');
|
||||
}
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ class PDF extends FPDF
|
||||
// BARIS TOTAL
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
public function BukuBesarTotal($totalDebit, $totalKredit, $saldo)
|
||||
public function BukuBesarTotal($saldoAwal, $totalDebit, $totalKredit, $saldoAkhir)
|
||||
{
|
||||
$startX = ($this->GetPageWidth() - array_sum($this->colWidths)) / 2;
|
||||
$labelWidth = $this->colWidths[0] + $this->colWidths[1] + $this->colWidths[2];
|
||||
@@ -253,10 +253,18 @@ class PDF extends FPDF
|
||||
$this->SetFillColor(255, 204, 102);
|
||||
$this->SetFont('Arial', 'B', 9);
|
||||
|
||||
$this->Cell($labelWidth, 8, 'TOTAL', 1, 0, 'C', true);
|
||||
$this->Cell($labelWidth, 8, 'TOTAL MUTASI', 1, 0, 'C', true);
|
||||
$this->Cell($this->colWidths[3], 8, number_format($totalDebit, 2, ',', '.'), 1, 0, 'R', true);
|
||||
$this->Cell($this->colWidths[4], 8, number_format($totalKredit, 2, ',', '.'), 1, 0, 'R', true);
|
||||
$this->Cell($this->colWidths[5], 8, number_format($saldo, 2, ',', '.'), 1, 1, 'R', true);
|
||||
$this->Cell($this->colWidths[5], 8, number_format($saldoAkhir, 2, ',', '.'), 1, 1, 'R', true);
|
||||
$this->SetX($startX);
|
||||
$this->SetFillColor(245, 245, 245);
|
||||
$this->Cell($labelWidth, 7, 'SALDO AWAL', 1, 0, 'R', true);
|
||||
$this->Cell($this->colWidths[3]+$this->colWidths[4]+$this->colWidths[5], 7, number_format($saldoAwal,2,',','.'), 1, 1, 'R', true);
|
||||
$this->SetX($startX);
|
||||
$this->SetFillColor(255, 204, 102);
|
||||
$this->Cell($labelWidth, 8, 'SALDO AKHIR', 1, 0, 'R', true);
|
||||
$this->Cell($this->colWidths[3]+$this->colWidths[4]+$this->colWidths[5], 8, number_format($saldoAkhir,2,',','.'), 1, 1, 'R', true);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@@ -27,8 +27,12 @@ class PayrollService
|
||||
$start=$period->attendance_start_date?:$period->start_date;$end=$period->attendance_end_date?:$period->end_date;
|
||||
if(!$db->get_where('payroll_attendance_locks',array('company_id'=>$companyId,'payroll_period_id'=>$period->id,'status'=>'locked'))->row())throw new BusinessException('Kehadiran periode ini harus dikunci sebelum payroll dihitung.');
|
||||
$employmentEmployees=$db
|
||||
->where(array('company_id'=>$companyId,'is_active'=>1))
|
||||
->where('company_id',$companyId)
|
||||
->where('join_date <=',$end)
|
||||
->group_start()
|
||||
->where('is_active',1)
|
||||
->or_where('resign_date >=',$start)
|
||||
->group_end()
|
||||
->group_start()
|
||||
->where('resign_date IS NULL',null,false)
|
||||
->or_where('resign_date','0000-00-00')
|
||||
@@ -37,9 +41,9 @@ class PayrollService
|
||||
->order_by('full_name')
|
||||
->get('k_employees')
|
||||
->result();
|
||||
if(!$canManageAll){foreach($employmentEmployees as$employee)if(!empty($employee->salary_restricted))throw new BusinessException('Periode memuat gaji terbatas. Perhitungan penuh harus dijalankan oleh pengguna dengan permission Manage All Gaji.');}
|
||||
$this->removeIneligibleDraftPayrolls($period->id,$companyId,$employmentEmployees);
|
||||
$employees=$employmentEmployees;
|
||||
if(!$canManageAll)$employees=array_values(array_filter($employees,function($employee){return empty($employee->salary_restricted);}));
|
||||
if(!$employees)throw new BusinessException('Tidak ada karyawan yang dapat dihitung dengan permission Anda.');
|
||||
foreach($employees as$employee)$this->calculateEmployee($period,$employee,$start,$end,$companyId,$uid);
|
||||
$hash=hash('sha256',json_encode($db->select('id,total_salary,version')->where(array('payroll_period_id'=>$period->id,'company_id'=>$companyId))->order_by('id')->get('k_payrolls')->result_array()));
|
||||
@@ -122,17 +126,17 @@ class PayrollService
|
||||
{
|
||||
$db=$this->CI->db;$db->trans_begin();try{$period=$this->period($id,$companyId);$before=$period;
|
||||
if($action==='revert'){if(!in_array($period->workflow_status,array('reviewed','approved'),true))throw new BusinessException('Hanya payroll Reviewed atau Approved yang dapat dikembalikan ke Draft.');$update=array('workflow_status'=>'draft','reviewed_by'=>null,'reviewed_at'=>null,'approved_by'=>null,'approved_at'=>null,'updated_at'=>date('Y-m-d H:i:s'));}
|
||||
else{$map=array('review'=>array('draft','reviewed'),'approve'=>array('reviewed','approved'),'finalize'=>array('approved','final'));if(!isset($map[$action])||$period->workflow_status!==$map[$action][0])throw new BusinessException('Tahap payroll tidak sesuai. Muat ulang halaman dan periksa status terbaru.');if(in_array($action,array('approve','finalize'),true)&&$this->hasRestrictedPayroll($id)&&!$canManageAll)throw new BusinessException('Periode memuat gaji terbatas dan memerlukan permission Manage All Gaji.');$settings=$db->get_where('payroll_settings',array('company_id'=>$companyId))->row();$separation=!$settings||(int)$settings->separation_of_duties===1;if($separation&&$action==='approve'&&(int)$period->reviewed_by===(int)$uid)throw new BusinessException('Reviewer dan approver harus pengguna yang berbeda.');if($separation&&$action==='finalize'&&((int)$period->reviewed_by===(int)$uid||(int)$period->approved_by===(int)$uid))throw new BusinessException('Finalizer harus berbeda dari reviewer dan approver.');$who=$action==='review'?'reviewed_by':($action==='approve'?'approved_by':'finalized_by');$when=$action==='review'?'reviewed_at':($action==='approve'?'approved_at':'finalized_at');$update=array('workflow_status'=>$map[$action][1],$who=>$uid,$when=>date('Y-m-d H:i:s'),'updated_at'=>date('Y-m-d H:i:s'));}
|
||||
else{$map=array('review'=>array('draft','reviewed'),'approve'=>array('reviewed','approved'),'finalize'=>array('approved','final'));if(!isset($map[$action])||$period->workflow_status!==$map[$action][0])throw new BusinessException('Tahap payroll tidak sesuai. Muat ulang halaman dan periksa status terbaru.');if($action==='review')$this->assertCalculationReady($period,$companyId);if(in_array($action,array('review','approve','finalize'),true)&&$this->hasRestrictedPayroll($id,$companyId)&&!$canManageAll)throw new BusinessException('Periode memuat gaji terbatas dan memerlukan permission Manage All Gaji.');$settings=$db->get_where('payroll_settings',array('company_id'=>$companyId))->row();$separation=!$settings||(int)$settings->separation_of_duties===1;if($separation&&$action==='approve'&&(int)$period->reviewed_by===(int)$uid)throw new BusinessException('Reviewer dan approver harus pengguna yang berbeda.');if($separation&&$action==='finalize'&&((int)$period->reviewed_by===(int)$uid||(int)$period->approved_by===(int)$uid))throw new BusinessException('Finalizer harus berbeda dari reviewer dan approver.');$who=$action==='review'?'reviewed_by':($action==='approve'?'approved_by':'finalized_by');$when=$action==='review'?'reviewed_at':($action==='approve'?'approved_at':'finalized_at');$update=array('workflow_status'=>$map[$action][1],$who=>$uid,$when=>date('Y-m-d H:i:s'),'updated_at'=>date('Y-m-d H:i:s'));}
|
||||
$db->where(array('id'=>$id,'company_id'=>$companyId))->update('k_payroll_periods',$update);$this->CI->auditservice->record('payroll','payroll_period',$id,$action,$before,$update,$uid);if($db->trans_status()===false)throw new RuntimeException('Workflow payroll gagal disimpan.');$db->trans_commit();
|
||||
}catch(Throwable$e){$db->trans_rollback();throw$e;}
|
||||
}
|
||||
|
||||
public function post($id,$date,$uid,$companyId,$canManageAll)
|
||||
{
|
||||
$db=$this->CI->db;$db->trans_begin();try{$period=$this->period($id,$companyId);if($period->workflow_status!=='final'||$period->journal_id)throw new BusinessException('Payroll belum Final atau sudah diposting.');if($this->hasRestrictedPayroll($id)&&!$canManageAll)throw new BusinessException('Posting payroll dengan gaji terbatas memerlukan permission Manage All Gaji.');$this->CI->fiscalperiodservice->assertOpen($date);
|
||||
$db=$this->CI->db;$db->trans_begin();try{$period=$this->period($id,$companyId);if($period->workflow_status!=='final'||$period->journal_id)throw new BusinessException('Payroll belum Final atau sudah diposting.');if($this->hasRestrictedPayroll($id,$companyId)&&!$canManageAll)throw new BusinessException('Posting payroll dengan gaji terbatas memerlukan permission Manage All Gaji.');$this->CI->fiscalperiodservice->assertOpen($date);
|
||||
$rows=$db->select('l.component_code,l.component_name,l.component_type,l.expense_account_id,l.liability_account_id,SUM(l.amount) amount',false)->from('payroll_calculation_lines l')->join('k_payrolls x','x.id=l.payroll_id')->where(array('x.payroll_period_id'=>$id,'x.company_id'=>$companyId))->group_by(array('l.component_code','l.component_name','l.component_type','l.expense_account_id','l.liability_account_id'))->get()->result();$net=(float)$db->select('COALESCE(SUM(total_salary),0) total',false)->get_where('k_payrolls',array('payroll_period_id'=>$id,'company_id'=>$companyId))->row()->total;$journal=array();$missing=array();
|
||||
foreach($rows as$row){try{$expense=$row->expense_account_id?(int)$row->expense_account_id:(int)$this->CI->accountmappingservice->get('payroll_expense');}catch(Throwable$e){$expense=0;}try{$liability=$row->liability_account_id?(int)$row->liability_account_id:(int)$this->CI->accountmappingservice->get($row->component_type==='deduction'?'payroll_tax_payable':'payroll_payable');}catch(Throwable$e){$liability=0;}if(in_array($row->component_type,array('earning','employer_cost'),true)&&!$expense)$missing[]=$row->component_name.' (akun beban)';if(in_array($row->component_type,array('deduction','employer_cost'),true)&&!$liability)$missing[]=$row->component_name.' (akun kewajiban)';if($row->component_type==='earning')$this->addJournal($journal,$expense,(float)$row->amount,0);elseif($row->component_type==='deduction')$this->addJournal($journal,$liability,0,(float)$row->amount);else{$this->addJournal($journal,$expense,(float)$row->amount,0);$this->addJournal($journal,$liability,0,(float)$row->amount);}}
|
||||
try{$payable=(int)$this->CI->accountmappingservice->get('payroll_payable');}catch(Throwable$e){$payable=0;}if(!$payable)$missing[]='Hutang Gaji (payroll_payable)';else$this->addJournal($journal,$payable,0,$net);if($missing)throw new BusinessException('Mapping akun payroll belum lengkap: '.implode(', ',array_unique($missing)).'.');$debit=0;$credit=0;foreach($journal as$line){$debit+=$line['debit'];$credit+=$line['kredit'];}if(abs($debit-$credit)>.01)throw new BusinessException('Jurnal payroll tidak balance. Periksa mapping komponen.');$count=$db->where(array('payroll_period_id'=>$id,'company_id'=>$companyId))->count_all_results('k_payrolls');$journalId=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>'PAYROLL-'.$period->id,'keterangan'=>'Payroll '.$period->period_name.' untuk '.$count.' karyawan','ref_type'=>'payroll','ref_id'=>$id,'created_by'=>$uid),array_values($journal),false,true);$db->where(array('id'=>$id,'company_id'=>$companyId))->update('k_payroll_periods',array('workflow_status'=>'posted','journal_id'=>$journalId,'updated_at'=>date('Y-m-d H:i:s')));$this->CI->auditservice->record('payroll','payroll_period',$id,'post',null,array('journal_id'=>$journalId),$uid);$db->trans_commit();return$journalId;
|
||||
if(!$rows||$net<=0)throw new BusinessException('Hasil payroll kosong atau total neto tidak valid. Hitung ulang payroll sebelum posting.');try{$payable=(int)$this->CI->accountmappingservice->get('payroll_payable');}catch(Throwable$e){$payable=0;}if(!$payable)$missing[]='Hutang Gaji (payroll_payable)';else$this->addJournal($journal,$payable,0,$net);if($missing)throw new BusinessException('Mapping akun payroll belum lengkap: '.implode(', ',array_unique($missing)).'.');$debit=0;$credit=0;foreach($journal as$line){$debit+=$line['debit'];$credit+=$line['kredit'];}if(abs($debit-$credit)>.01)throw new BusinessException('Jurnal payroll tidak balance. Periksa mapping komponen.');$count=$db->where(array('payroll_period_id'=>$id,'company_id'=>$companyId))->count_all_results('k_payrolls');$journalId=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>'PAYROLL-'.$period->id,'keterangan'=>'Payroll '.$period->period_name.' untuk '.$count.' karyawan','ref_type'=>'payroll','ref_id'=>$id,'created_by'=>$uid),array_values($journal),false,true);$db->where(array('id'=>$id,'company_id'=>$companyId))->update('k_payroll_periods',array('workflow_status'=>'posted','journal_id'=>$journalId,'updated_at'=>date('Y-m-d H:i:s')));$this->CI->auditservice->record('payroll','payroll_period',$id,'post',null,array('journal_id'=>$journalId),$uid);if($db->trans_status()===false)throw new RuntimeException('Posting payroll gagal disimpan.');$db->trans_commit();return$journalId;
|
||||
}catch(Throwable$e){$db->trans_rollback();throw$e;}
|
||||
}
|
||||
|
||||
@@ -140,9 +144,18 @@ class PayrollService
|
||||
|
||||
public function pay($id,$date,$cashAccountId,$uid,$companyId,$canManageAll)
|
||||
{
|
||||
$db=$this->CI->db;$db->trans_begin();try{$period=$this->period($id,$companyId);if($period->workflow_status!=='posted')throw new BusinessException('Payroll belum diposting atau sudah dibayar.');if($this->hasRestrictedPayroll($id)&&!$canManageAll)throw new BusinessException('Pembayaran payroll dengan gaji terbatas memerlukan permission Manage All Gaji.');if($db->get_where('payroll_payments',array('company_id'=>$companyId,'payroll_period_id'=>$id))->row())throw new BusinessException('Pembayaran payroll sudah tersedia.');$cashQuery=$db->where(array('id'=>$cashAccountId,'is_active'=>1));if($db->field_exists('company_id','cash_accounts'))$cashQuery->where('company_id',$companyId);$cash=$cashQuery->get('cash_accounts')->row();if(!$cash)throw new BusinessException('Rekening pembayaran aktif tidak ditemukan.');$this->CI->fiscalperiodservice->assertOpen($date);$net=(float)$db->select('COALESCE(SUM(total_salary),0) total',false)->get_where('k_payrolls',array('payroll_period_id'=>$id,'company_id'=>$companyId))->row()->total;if($net<=0)throw new BusinessException('Total pembayaran payroll tidak valid.');$number='PRP-'.$companyId.'-'.date('YmdHis');$journal=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>$number,'keterangan'=>'Pembayaran payroll '.$period->period_name.' melalui '.$cash->name,'ref_type'=>'payroll_payment','ref_id'=>$id,'created_by'=>$uid),array(array('account_id'=>$this->CI->accountmappingservice->get('payroll_payable'),'debit'=>$net,'kredit'=>0),array('account_id'=>$cash->gl_account_id,'debit'=>0,'kredit'=>$net)),false,true);$db->insert('payroll_payments',array('company_id'=>$companyId,'payroll_period_id'=>$id,'payment_no'=>$number,'payment_date'=>$date,'cash_account_id'=>$cashAccountId,'total_amount'=>$net,'status'=>'posted','journal_id'=>$journal,'created_by'=>$uid,'approved_by'=>$uid,'posted_at'=>date('Y-m-d H:i:s')));$paymentId=(int)$db->insert_id();$db->where(array('id'=>$id,'company_id'=>$companyId))->update('k_payroll_periods',array('workflow_status'=>'paid','payment_transaction_id'=>$paymentId,'updated_at'=>date('Y-m-d H:i:s')));$this->CI->auditservice->record('payroll','payroll_period',$id,'pay',null,array('payment_id'=>$paymentId,'journal_id'=>$journal),$uid);$db->trans_commit();return$journal;
|
||||
$db=$this->CI->db;$db->trans_begin();try{$period=$this->period($id,$companyId);if($period->workflow_status!=='posted')throw new BusinessException('Payroll belum diposting atau sudah dibayar.');if($this->hasRestrictedPayroll($id,$companyId)&&!$canManageAll)throw new BusinessException('Pembayaran payroll dengan gaji terbatas memerlukan permission Manage All Gaji.');if($db->get_where('payroll_payments',array('company_id'=>$companyId,'payroll_period_id'=>$id))->row())throw new BusinessException('Pembayaran payroll sudah tersedia.');$cashQuery=$db->where(array('id'=>$cashAccountId,'is_active'=>1));if($db->field_exists('company_id','cash_accounts'))$cashQuery->where('company_id',$companyId);$cash=$cashQuery->get('cash_accounts')->row();if(!$cash)throw new BusinessException('Rekening pembayaran aktif tidak ditemukan.');if(empty($cash->gl_account_id))throw new BusinessException('Rekening pembayaran belum terhubung ke akun buku besar.');$accountQuery=$db->where(array('id'=>(int)$cash->gl_account_id,'company_id'=>$companyId,'is_active'=>1));if($db->field_exists('allow_posting','accounts'))$accountQuery->where('allow_posting',1);if(!$accountQuery->count_all_results('accounts'))throw new BusinessException('Akun buku besar pada rekening pembayaran tidak aktif, tidak dapat diposting, atau berbeda perusahaan.');$this->CI->fiscalperiodservice->assertOpen($date);$net=(float)$db->select('COALESCE(SUM(total_salary),0) total',false)->get_where('k_payrolls',array('payroll_period_id'=>$id,'company_id'=>$companyId))->row()->total;if($net<=0)throw new BusinessException('Total pembayaran payroll tidak valid.');$number='PRP-'.$companyId.'-'.date('YmdHis').'-'.strtoupper(bin2hex(random_bytes(2)));$journal=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>$number,'keterangan'=>'Pembayaran payroll '.$period->period_name.' melalui '.$cash->name,'ref_type'=>'payroll_payment','ref_id'=>$id,'created_by'=>$uid),array(array('account_id'=>$this->CI->accountmappingservice->get('payroll_payable'),'debit'=>$net,'kredit'=>0),array('account_id'=>$cash->gl_account_id,'debit'=>0,'kredit'=>$net)),false,true);$db->insert('payroll_payments',array('company_id'=>$companyId,'payroll_period_id'=>$id,'payment_no'=>$number,'payment_date'=>$date,'cash_account_id'=>$cashAccountId,'total_amount'=>$net,'status'=>'posted','journal_id'=>$journal,'created_by'=>$uid,'approved_by'=>$uid,'posted_at'=>date('Y-m-d H:i:s')));$paymentId=(int)$db->insert_id();$db->where(array('id'=>$id,'company_id'=>$companyId))->update('k_payroll_periods',array('workflow_status'=>'paid','payment_transaction_id'=>$paymentId,'updated_at'=>date('Y-m-d H:i:s')));$this->CI->auditservice->record('payroll','payroll_period',$id,'pay',null,array('payment_id'=>$paymentId,'journal_id'=>$journal),$uid);if($db->trans_status()===false)throw new RuntimeException('Pembayaran payroll gagal disimpan.');$db->trans_commit();return$journal;
|
||||
}catch(Throwable$e){$db->trans_rollback();throw$e;}
|
||||
}
|
||||
|
||||
private function hasRestrictedPayroll($periodId){return$this->CI->db->from('k_payrolls p')->join('k_employees e','e.id=p.employee_id')->where('p.payroll_period_id',(int)$periodId)->where('e.salary_restricted',1)->count_all_results()>0;}
|
||||
private function assertCalculationReady($period,$companyId)
|
||||
{
|
||||
if(empty($period->calculated_at)||empty($period->calculation_hash))throw new BusinessException('Payroll belum dihitung atau berubah setelah perhitungan. Silakan Hitung Ulang.');
|
||||
$actual=(int)$this->CI->db->where(array('company_id'=>$companyId,'payroll_period_id'=>$period->id))->count_all_results('k_payrolls');
|
||||
if($actual<1)throw new BusinessException('Rincian payroll masih kosong. Silakan Hitung Ulang.');
|
||||
$current=hash('sha256',json_encode($this->CI->db->select('id,total_salary,version')->where(array('payroll_period_id'=>$period->id,'company_id'=>$companyId))->order_by('id')->get('k_payrolls')->result_array()));
|
||||
if(!hash_equals((string)$period->calculation_hash,$current))throw new BusinessException('Hasil payroll berubah setelah perhitungan. Silakan Hitung Ulang sebelum review.');
|
||||
}
|
||||
|
||||
private function hasRestrictedPayroll($periodId,$companyId){return$this->CI->db->from('k_payrolls p')->join('k_employees e','e.id=p.employee_id AND e.company_id=p.company_id')->where(array('p.payroll_period_id'=>(int)$periodId,'p.company_id'=>(int)$companyId,'e.salary_restricted'=>1))->count_all_results()>0;}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,61 @@ class ProfessionalReportService
|
||||
private function filters(array$f){return array(' j.company_id=? AND j.status IN(\'posted\',\'reversed\') AND j.tanggal BETWEEN ? AND ?',array($this->CI->companycontext->id(),$f['from'],$f['to']));}
|
||||
public function trialBalance(array$f){list($w,$b)=$this->filters($f);return$this->CI->db->query("SELECT a.id account_id,a.kode_akun,a.nama_akun,a.tipe,COALESCE(SUM(d.debit),0) debit,COALESCE(SUM(d.kredit),0) credit,COALESCE(SUM(d.debit-d.kredit),0) balance FROM accounts a LEFT JOIN journal_details d ON d.account_id=a.id LEFT JOIN journals j ON j.id=d.journal_id AND j.company_id=a.company_id WHERE a.company_id=? AND (j.id IS NULL OR ($w)) GROUP BY a.id ORDER BY a.priority ASC,a.kode_akun ASC,a.id ASC",array_merge(array($this->CI->companycontext->id()),$b))->result_array();}
|
||||
public function statement($type,array$f,$priorityOrder=true){list($w,$b)=$this->filters($f);$types=$type==='profit_loss'?array('revenue','expense'):array('asset','liability','equity');$marks=implode(',',array_fill(0,count($types),'?'));$company=$this->CI->companycontext->id();$order=$priorityOrder?'a.priority ASC,a.kode_akun ASC,a.id ASC':'a.kode_akun ASC';$sql="SELECT a.id account_id,a.kode_akun,a.nama_akun,a.tipe,COALESCE(x.amount,0) amount FROM accounts a LEFT JOIN (SELECT d.account_id,SUM(IF(ac.tipe IN('asset','expense'),d.debit-d.kredit,d.kredit-d.debit)) amount FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts ac ON ac.id=d.account_id WHERE $w GROUP BY d.account_id)x ON x.account_id=a.id WHERE a.company_id=? AND a.tipe IN($marks) ORDER BY $order";return$this->CI->db->query($sql,array_merge($b,array($company),$types))->result_array();}
|
||||
public function ledger(array$f){list($w,$b)=$this->filters($f);$sql="SELECT j.id journal_id,j.tanggal,j.no_ref,j.keterangan,j.ref_type,j.ref_id,a.kode_akun,a.nama_akun,d.debit,d.kredit FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE $w";if(!empty($f['account_id'])){$sql.=' AND d.account_id=?';$b[]=(int)$f['account_id'];}$sql.=' ORDER BY j.tanggal,j.id,d.id';return$this->CI->db->query($sql,$b)->result_array();}
|
||||
public function cashFlow(array$f){list($w,$b)=$this->filters($f);return$this->CI->db->query("SELECT CASE WHEN j.ref_type IN('invoice','customer_payment','supplier_invoice','supplier_payment','payroll','payroll_payment') THEN 'operating' WHEN j.ref_type LIKE 'asset_%' THEN 'investing' ELSE 'financing_or_other' END section,SUM(IF(a.sub_tipe='kas',d.debit-d.kredit,0)) amount FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE $w GROUP BY section",$b)->result_array();}
|
||||
public function ledger(array$f)
|
||||
{
|
||||
list($w,$b)=$this->filters($f);
|
||||
$sql="SELECT d.id detail_id,j.id journal_id,j.tanggal,j.no_ref,j.keterangan,j.ref_type,j.ref_id,a.id account_id,a.kode_akun,a.nama_akun,a.tipe,d.debit,d.kredit FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE $w";
|
||||
if(!empty($f['account_id'])){$sql.=' AND d.account_id=?';$b[]=(int)$f['account_id'];}
|
||||
$sql.=' AND (d.debit<>0 OR d.kredit<>0) ORDER BY a.priority ASC,a.kode_akun ASC,j.tanggal ASC,j.id ASC,d.id ASC';
|
||||
$rows=$this->CI->db->query($sql,$b)->result_array();
|
||||
$opening=$this->ledgerOpening($f);
|
||||
$running=array();
|
||||
foreach($rows as&$row){$id=(int)$row['account_id'];if(!isset($running[$id]))$running[$id]=(float)($opening[$id]??0);$normal=in_array($row['tipe'],array('asset','expense'),true)?1:-1;$row['saldo_awal']=(float)($opening[$id]??0);$running[$id]+=$normal*((float)$row['debit']-(float)$row['kredit']);$row['saldo_berjalan']=round($running[$id],2);}
|
||||
unset($row);return$rows;
|
||||
}
|
||||
public function ledgerSummary(array$f)
|
||||
{
|
||||
$company=(int)$this->CI->companycontext->id();$opening=$this->ledgerOpening($f);
|
||||
$sql="SELECT a.id account_id,a.kode_akun,a.nama_akun,a.tipe,a.priority,COALESCE(SUM(CASE WHEN j.id IS NOT NULL THEN d.debit ELSE 0 END),0) total_debit,COALESCE(SUM(CASE WHEN j.id IS NOT NULL THEN d.kredit ELSE 0 END),0) total_kredit FROM accounts a LEFT JOIN journal_details d ON d.account_id=a.id LEFT JOIN journals j ON j.id=d.journal_id AND j.company_id=? AND j.status IN('posted','reversed') AND j.tanggal BETWEEN ? AND ? WHERE a.company_id=?";
|
||||
$bind=array($company,$f['from'],$f['to'],$company);if(!empty($f['account_id'])){$sql.=' AND a.id=?';$bind[]=(int)$f['account_id'];}$sql.=' GROUP BY a.id ORDER BY a.priority ASC,a.kode_akun ASC';
|
||||
$rows=$this->CI->db->query($sql,$bind)->result_array();$result=array();foreach($rows as$row){$id=(int)$row['account_id'];$row['saldo_awal']=(float)($opening[$id]??0);$normal=in_array($row['tipe'],array('asset','expense'),true)?1:-1;$row['saldo_akhir']=round($row['saldo_awal']+$normal*((float)$row['total_debit']-(float)$row['total_kredit']),2);if(abs($row['saldo_awal'])>.009||abs((float)$row['total_debit'])>.009||abs((float)$row['total_kredit'])>.009)$result[]=$row;}return$result;
|
||||
}
|
||||
private function ledgerOpening(array$f)
|
||||
{
|
||||
$company=(int)$this->CI->companycontext->id();$sql="SELECT d.account_id,a.tipe,SUM(d.debit) debit,SUM(d.kredit) kredit FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE j.company_id=? AND j.status IN('posted','reversed') AND j.tanggal<?";$bind=array($company,$f['from']);if(!empty($f['account_id'])){$sql.=' AND d.account_id=?';$bind[]=(int)$f['account_id'];}$sql.=' GROUP BY d.account_id,a.tipe';$out=array();foreach($this->CI->db->query($sql,$bind)->result_array()as$row){$normal=in_array($row['tipe'],array('asset','expense'),true)?1:-1;$out[(int)$row['account_id']]=$normal*((float)$row['debit']-(float)$row['kredit']);}return$out;
|
||||
}
|
||||
public function cashFlow(array$f)
|
||||
{
|
||||
$company=(int)$this->CI->companycontext->id();$cashWhere="(a.sub_tipe='kas' OR EXISTS(SELECT 1 FROM cash_accounts ca WHERE ca.gl_account_id=a.id AND (ca.company_id=? OR ca.company_id IS NULL)))";
|
||||
$opening=$this->cashOpening($f);
|
||||
$sql="SELECT d.id detail_id,j.id journal_id,j.tanggal,j.no_ref,j.keterangan,j.ref_type,j.ref_id,a.kode_akun,a.nama_akun,d.debit kas_masuk,d.kredit kas_keluar FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE j.company_id=? AND j.status IN('posted','reversed') AND j.tanggal BETWEEN ? AND ? AND $cashWhere AND (d.debit<>0 OR d.kredit<>0)";$bind=array($company,$f['from'],$f['to'],$company);if(!empty($f['account_id'])){$sql.=' AND a.id=?';$bind[]=(int)$f['account_id'];}$sql.=' ORDER BY j.tanggal,j.id,d.id';$rows=$this->CI->db->query($sql,$bind)->result_array();$running=$opening;
|
||||
foreach($rows as&$row){$row['section']=$this->cashSection((string)$row['ref_type']);$row['aktivitas']=$this->cashSectionLabel($row['section']);$row['akun']=$row['kode_akun'].' - '.$row['nama_akun'];$running+=(float)$row['kas_masuk']-(float)$row['kas_keluar'];$row['saldo_berjalan']=round($running,2);}unset($row);return$rows;
|
||||
}
|
||||
public function cashFlowSummary(array$f,array$rows=null)
|
||||
{
|
||||
if($rows===null)$rows=$this->cashFlow($f);$opening=$this->cashOpening($f);$summary=array('operating'=>0,'investing'=>0,'financing'=>0,'opening_balance'=>$opening,'increase_decrease'=>0,'closing_balance'=>$opening,'cash_in'=>0,'cash_out'=>0);foreach($rows as$row){$net=(float)$row['kas_masuk']-(float)$row['kas_keluar'];$summary[$row['section']]+=$net;$summary['cash_in']+=(float)$row['kas_masuk'];$summary['cash_out']+=(float)$row['kas_keluar'];}$summary['increase_decrease']=$summary['operating']+$summary['investing']+$summary['financing'];$summary['closing_balance']=$opening+$summary['increase_decrease'];return$summary;
|
||||
}
|
||||
private function cashOpening(array$f){$company=(int)$this->CI->companycontext->id();$cashWhere="(a.sub_tipe='kas' OR EXISTS(SELECT 1 FROM cash_accounts ca WHERE ca.gl_account_id=a.id AND (ca.company_id=? OR ca.company_id IS NULL)))";$sql="SELECT COALESCE(SUM(d.debit-d.kredit),0) amount FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE j.company_id=? AND j.status IN('posted','reversed') AND j.tanggal<? AND $cashWhere";$bind=array($company,$f['from'],$company);if(!empty($f['account_id'])){$sql.=' AND a.id=?';$bind[]=(int)$f['account_id'];}return(float)$this->CI->db->query($sql,$bind)->row()->amount;}
|
||||
private function cashSection($refType){if(preg_match('/asset|fixed_asset|capitalization|depreciation/i',$refType))return'investing';if(preg_match('/capital|equity|dividend|loan|financing|customer_saving|deposit/i',$refType))return'financing';return'operating';}
|
||||
private function cashSectionLabel($section){return array('operating'=>'Aktivitas Operasi','investing'=>'Aktivitas Investasi','financing'=>'Aktivitas Pendanaan')[$section]??'Aktivitas Operasi';}
|
||||
public function equity(array$f){list($w,$b)=$this->filters($f);return$this->CI->db->query("SELECT a.kode_akun,a.nama_akun,SUM(d.kredit-d.debit) amount FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE $w AND a.tipe='equity' GROUP BY a.id ORDER BY a.kode_akun",$b)->result_array();}
|
||||
public function actualBudget(array$f){$this->CI->load->library('BudgetService');$source=$this->CI->budgetservice->accountReport(array('from'=>substr($f['from'],0,7),'to'=>substr($f['to'],0,7),'account_id'=>(int)($f['account_id']??0)));$rows=array();foreach($source as$r)$rows[]=array('kode_akun'=>$r['kode_akun'],'nama_akun'=>$r['nama_akun'],'actual'=>$r['actual'],'budget'=>$r['budget'],'variance'=>$r['actual']-$r['budget']);return$rows;}
|
||||
public function comparative(array$f){$current=$this->statement('profit_loss',$f,false);$pm=array_merge($f,array('from'=>date('Y-m-d',strtotime($f['from'].' -1 month')),'to'=>date('Y-m-d',strtotime($f['to'].' -1 month'))));$py=array_merge($f,array('from'=>date('Y-m-d',strtotime($f['from'].' -1 year')),'to'=>date('Y-m-d',strtotime($f['to'].' -1 year'))));$idx=array();foreach($this->statement('profit_loss',$pm,false)as$r)$idx[$r['account_id']]['previous_month']=$r['amount'];foreach($this->statement('profit_loss',$py,false)as$r)$idx[$r['account_id']]['previous_year']=$r['amount'];foreach($current as&$r){$r['previous_month']=isset($idx[$r['account_id']]['previous_month'])?$idx[$r['account_id']]['previous_month']:0;$r['previous_year']=isset($idx[$r['account_id']]['previous_year'])?$idx[$r['account_id']]['previous_year']:0;$r['variance_month']=$r['amount']-$r['previous_month'];$r['variance_year']=$r['amount']-$r['previous_year'];}return$current;}
|
||||
public function purchaseReturns(array$f){return$this->CI->db->select('r.id record_id,r.return_no,r.return_date,s.supplier_code,s.name supplier,po.po_no,r.problem_category,r.requested_resolution,r.status,r.amount,r.journal_id')->from('purchase_returns r')->join('suppliers s','s.id=r.supplier_id')->join('purchase_orders po','po.id=r.purchase_order_id','left')->where('r.company_id',$this->CI->companycontext->id())->where('r.return_date >=',$f['from'])->where('r.return_date <=',$f['to'])->order_by('r.return_date')->order_by('r.id')->get()->result_array();}
|
||||
public function supplierRefunds(array$f,$outstanding=false){$this->CI->db->select('r.id record_id,r.refund_no,r.claim_date,r.expected_date,s.supplier_code,s.name supplier,po.po_no,si.internal_no invoice_supplier,r.source_type,r.status,r.claim_amount,r.received_amount,r.balance,r.entitlement_journal_id journal_id')->from('supplier_refund_claims r')->join('suppliers s','s.id=r.supplier_id')->join('purchase_orders po','po.id=r.purchase_order_id','left')->join('supplier_invoices si','si.id=r.supplier_invoice_id','left')->where('r.company_id',$this->CI->companycontext->id())->where('r.claim_date >=',$f['from'])->where('r.claim_date <=',$f['to']);if($outstanding)$this->CI->db->where('r.balance >',0)->where_not_in('r.status',array('draft','rejected','cancelled','reversed','reconciled'));return$this->CI->db->order_by('r.claim_date')->order_by('r.id')->get()->result_array();}
|
||||
public function supplierDebitNotes(array$f){return$this->CI->db->select('d.id record_id,d.debit_note_no,d.note_date,s.supplier_code,s.name supplier,po.po_no,si.internal_no invoice_supplier,d.correction_type,d.status,d.base_amount,d.tax_amount,d.amount,d.journal_id')->from('supplier_debit_notes d')->join('suppliers s','s.id=d.supplier_id')->join('purchase_orders po','po.id=d.purchase_order_id','left')->join('supplier_invoices si','si.id=d.supplier_invoice_id','left')->where('d.company_id',$this->CI->companycontext->id())->where('d.note_date >=',$f['from'])->where('d.note_date <=',$f['to'])->order_by('d.note_date')->order_by('d.id')->get()->result_array();}
|
||||
public function salesReport($code,array$f){$c=$this->CI->companycontext->id();if($code==='sales_summary')return$this->CI->db->select('i.id record_id,i.no_invoice,i.tanggal,c.nama customer,i.invoice_type,i.recognition_policy,i.status,i.total,i.total_bayar,i.sisa_piutang')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->order_by('i.tanggal')->get()->result_array();if($code==='sales_by_customer')return$this->CI->db->select('c.id customer_id,c.nama customer,COUNT(i.id) invoice_count,SUM(i.total) sales,SUM(i.total_bayar) paid,SUM(i.sisa_piutang) balance',false)->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->group_by('c.id')->order_by('sales','DESC')->get()->result_array();if($code==='sales_by_item')return$this->CI->db->select('d.items_id item_id,it.kode_detail AS kode_barang,it.nama_barang,SUM(d.qty) qty,SUM(d.net_amount) net_sales,SUM(d.tax_amount) tax,SUM(d.subtotal) total',false)->from('invoice_details d')->join('invoices i','i.id=d.invoice_id')->join('items it','it.id=d.items_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->group_by('d.items_id')->order_by('net_sales','DESC')->get()->result_array();if($code==='sales_by_barcode')return$this->CI->db->select('i.id record_id,i.no_invoice,i.tanggal,it.kode_detail AS kode_barang,it.nama_barang,b.barcode,b.serial_number,lb.qty,sd.delivery_no')->from('invoice_line_barcodes lb')->join('invoices i','i.id=lb.invoice_id')->join('item_barcodes b','b.id=lb.barcode_id')->join('items it','it.id=b.item_id')->join('sales_deliveries sd','sd.id=lb.sales_delivery_id','left')->where('lb.company_id',$c)->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->order_by('i.tanggal')->get()->result_array();if($code==='running_invoices')return$this->CI->db->select('i.id record_id,i.no_invoice,c.nama customer,i.period_start,i.period_end,i.delivery_status,i.total,i.workflow_status')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.invoice_type'=>'running','i.workflow_status'=>'draft'))->order_by('i.period_end')->get()->result_array();if($code==='sales_returns')return$this->CI->db->select('r.id record_id,r.return_no,r.return_date,c.nama customer,i.no_invoice,d.delivery_no,r.resolution,r.status,SUM(l.net_amount+l.tax_amount) amount',false)->from('sales_returns r')->join('customers c','c.id=r.customer_id')->join('invoices i','i.id=r.invoice_id','left')->join('sales_deliveries d','d.id=r.sales_delivery_id','left')->join('sales_return_lines l','l.sales_return_id=r.id','left')->where('r.company_id',$c)->where('r.return_date >=',$f['from'])->where('r.return_date <=',$f['to'])->group_by('r.id')->order_by('r.return_date')->get()->result_array();if($code==='revenue_recognition')return$this->CI->db->select('i.id record_id,i.no_invoice,c.nama customer,i.recognition_policy,i.subtotal_before_tax invoice_net,i.recognized_revenue,i.subtotal_before_tax-i.recognized_revenue deferred_revenue,i.recognized_cogs')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->order_by('i.tanggal')->get()->result_array();if($code==='unbilled_deliveries')return$this->CI->db->select('d.id record_id,d.delivery_no,d.delivery_date,c.nama customer,d.status')->from('sales_deliveries d')->join('customers c','c.id=d.customer_id')->where('d.company_id',$c)->where('d.invoice_id IS NULL',null,false)->where('d.delivery_date >=',$f['from'])->where('d.delivery_date <=',$f['to'])->get()->result_array();if($code==='receivable_reconciliation'){$ar=$this->CI->load->library('AccountMappingService')?:null;$account=$this->CI->accountmappingservice->get('accounts_receivable');$sub=(float)$this->CI->db->select('COALESCE(SUM(sisa_piutang),0) v',false)->where(array('company_id'=>$c,'workflow_status'=>'posted'))->get('invoices')->row()->v;$gl=(float)$this->CI->db->select('COALESCE(SUM(d.debit-d.kredit),0) v',false)->from('journal_details d')->join('journals j','j.id=d.journal_id')->where(array('j.company_id'=>$c,'j.status'=>'posted','d.account_id'=>$account))->get()->row()->v;return array(array('subledger_piutang'=>$sub,'buku_besar_piutang'=>$gl,'selisih'=>$sub-$gl,'status'=>abs($sub-$gl)<.01?'Balance':'Perlu direkonsiliasi'));}throw new BusinessException('Jenis laporan penjualan tidak valid.');}
|
||||
public function salesReport($code,array$f){$c=$this->CI->companycontext->id();if($code==='revenue_recognition')return$this->revenueRecognition($f);if($code==='receivable_reconciliation')return$this->receivableReconciliation($f);if($code==='sales_summary')return$this->CI->db->select('i.id record_id,i.no_invoice,i.tanggal,c.nama customer,i.invoice_type,i.recognition_policy,i.status,i.total,i.total_bayar,i.sisa_piutang')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->order_by('i.tanggal')->get()->result_array();if($code==='sales_by_customer')return$this->CI->db->select('c.id customer_id,c.nama customer,COUNT(i.id) invoice_count,SUM(i.total) sales,SUM(i.total_bayar) paid,SUM(i.sisa_piutang) balance',false)->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->group_by('c.id')->order_by('sales','DESC')->get()->result_array();if($code==='sales_by_item')return$this->CI->db->select('d.items_id item_id,it.kode_detail AS kode_barang,it.nama_barang,SUM(d.qty) qty,SUM(d.net_amount) net_sales,SUM(d.tax_amount) tax,SUM(d.subtotal) total',false)->from('invoice_details d')->join('invoices i','i.id=d.invoice_id')->join('items it','it.id=d.items_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->group_by('d.items_id')->order_by('net_sales','DESC')->get()->result_array();if($code==='sales_by_barcode')return$this->CI->db->select('i.id record_id,i.no_invoice,i.tanggal,it.kode_detail AS kode_barang,it.nama_barang,b.barcode,b.serial_number,lb.qty,sd.delivery_no')->from('invoice_line_barcodes lb')->join('invoices i','i.id=lb.invoice_id')->join('item_barcodes b','b.id=lb.barcode_id')->join('items it','it.id=b.item_id')->join('sales_deliveries sd','sd.id=lb.sales_delivery_id','left')->where('lb.company_id',$c)->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->order_by('i.tanggal')->get()->result_array();if($code==='running_invoices')return$this->CI->db->select('i.id record_id,i.no_invoice,c.nama customer,i.period_start,i.period_end,i.delivery_status,i.total,i.workflow_status')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.invoice_type'=>'running','i.workflow_status'=>'draft'))->order_by('i.period_end')->get()->result_array();if($code==='sales_returns')return$this->CI->db->select('r.id record_id,r.return_no,r.return_date,c.nama customer,i.no_invoice,d.delivery_no,r.resolution,r.status,SUM(l.net_amount+l.tax_amount) amount',false)->from('sales_returns r')->join('customers c','c.id=r.customer_id')->join('invoices i','i.id=r.invoice_id','left')->join('sales_deliveries d','d.id=r.sales_delivery_id','left')->join('sales_return_lines l','l.sales_return_id=r.id','left')->where('r.company_id',$c)->where('r.return_date >=',$f['from'])->where('r.return_date <=',$f['to'])->group_by('r.id')->order_by('r.return_date')->get()->result_array();if($code==='unbilled_deliveries')return$this->CI->db->select('d.id record_id,d.delivery_no,d.delivery_date,c.nama customer,d.status')->from('sales_deliveries d')->join('customers c','c.id=d.customer_id')->where('d.company_id',$c)->where('d.invoice_id IS NULL',null,false)->where('d.delivery_date >=',$f['from'])->where('d.delivery_date <=',$f['to'])->get()->result_array();throw new BusinessException('Jenis laporan penjualan tidak valid.');}
|
||||
private function revenueRecognition(array$f)
|
||||
{
|
||||
$company=(int)$this->CI->companycontext->id();$sql="SELECT i.id record_id,i.no_invoice,i.tanggal,c.nama customer,CASE WHEN i.recognition_policy='accrual' THEN 'Diakui saat posted' ELSE 'Proporsional setelah dibayar' END recognition_policy,COALESCE(i.subtotal_before_tax,0) invoice_net,COALESCE(i.recognized_revenue,0) recognized_revenue,GREATEST(COALESCE(i.subtotal_before_tax,0)-COALESCE(i.recognized_revenue,0),0) deferred_revenue,COALESCE(i.recognized_cogs,0) recognized_cogs FROM invoices i JOIN customers c ON c.id=i.customer_id AND c.company_id=i.company_id WHERE i.company_id=? AND i.workflow_status='posted' AND i.tanggal BETWEEN ? AND ? AND i.deleted_at IS NULL ORDER BY i.tanggal,i.id";return$this->CI->db->query($sql,array($company,$f['from'],$f['to']))->result_array();
|
||||
}
|
||||
private function receivableReconciliation(array$f)
|
||||
{
|
||||
$company=(int)$this->CI->companycontext->id();$to=$f['to'];$sql="SELECT i.id record_id,c.nama customer,i.no_invoice,i.tanggal invoice_date,i.jatuh_tempo due_date,i.total receivable_amount,COALESCE(p.paid,0) payments,COALESCE(n.credit_amount,0) credit_notes,COALESCE(w.writeoff_amount,0) writeoffs,GREATEST(i.total-COALESCE(p.paid,0)-COALESCE(n.credit_amount,0)-COALESCE(w.writeoff_amount,0),0) remaining_receivable,CASE WHEN GREATEST(i.total-COALESCE(p.paid,0)-COALESCE(n.credit_amount,0)-COALESCE(w.writeoff_amount,0),0)<=0.01 THEN 'Lunas' WHEN COALESCE(p.paid,0)+COALESCE(n.credit_amount,0)+COALESCE(w.writeoff_amount,0)>0 THEN 'Partial' WHEN i.jatuh_tempo<? THEN 'Jatuh Tempo' ELSE 'Belum Lunas' END status,CONCAT_WS(' | ',NULLIF(p.payment_refs,''),NULLIF(n.credit_refs,''),NULLIF(w.writeoff_refs,'')) transaction_references,0 reconciliation_adjustment FROM invoices i JOIN customers c ON c.id=i.customer_id AND c.company_id=i.company_id LEFT JOIN(SELECT pa.invoice_id,SUM(pa.amount) paid,GROUP_CONCAT(DISTINCT p.payment_no ORDER BY p.tanggal,p.id SEPARATOR ', ') payment_refs FROM payment_allocations pa JOIN payments p ON p.id=pa.payment_id WHERE p.status='posted' AND p.tanggal<=? GROUP BY pa.invoice_id)p ON p.invoice_id=i.id LEFT JOIN(SELECT invoice_id,SUM(amount) credit_amount,GROUP_CONCAT(credit_note_no ORDER BY credit_date,id SEPARATOR ', ') credit_refs FROM credit_notes WHERE status='posted' AND credit_date<=? GROUP BY invoice_id)n ON n.invoice_id=i.id LEFT JOIN(SELECT invoice_id,SUM(amount) writeoff_amount,GROUP_CONCAT(writeoff_no ORDER BY writeoff_date,id SEPARATOR ', ') writeoff_refs FROM receivable_writeoffs WHERE status IN('approved','posted') AND writeoff_date<=? GROUP BY invoice_id)w ON w.invoice_id=i.id WHERE i.company_id=? AND i.workflow_status='posted' AND i.tanggal<=? AND i.deleted_at IS NULL ORDER BY c.nama,i.tanggal,i.id";$rows=$this->CI->db->query($sql,array($to,$to,$to,$to,$company,$to))->result_array();$summary=$this->receivableSummary($f,$rows);if(abs($summary['difference'])>.01)$rows[]=array('record_id'=>0,'customer'=>'Saldo GL belum terpetakan','no_invoice'=>'REKONSILIASI-GL','invoice_date'=>$to,'due_date'=>null,'receivable_amount'=>0,'payments'=>0,'credit_notes'=>0,'writeoffs'=>0,'remaining_receivable'=>$summary['difference'],'status'=>'Perlu ditelusuri','transaction_references'=>'Selisih saldo Buku Besar terhadap subledger invoice sampai '.$to,'reconciliation_adjustment'=>1);return$rows;
|
||||
}
|
||||
public function receivableSummary(array$f,array$rows=null)
|
||||
{
|
||||
if($rows===null)$rows=$this->receivableReconciliation($f);$subledger=0;foreach($rows as$row)if(empty($row['reconciliation_adjustment']))$subledger+=(float)$row['remaining_receivable'];$this->CI->load->library('AccountMappingService');$account=(int)$this->CI->accountmappingservice->get('accounts_receivable');$gl=(float)$this->CI->db->query("SELECT COALESCE(SUM(d.debit-d.kredit),0) amount FROM journal_details d JOIN journals j ON j.id=d.journal_id WHERE j.company_id=? AND j.status IN('posted','reversed') AND j.tanggal<=? AND d.account_id=?",array((int)$this->CI->companycontext->id(),$f['to'],$account))->row()->amount;return array('subledger'=>$subledger,'general_ledger'=>$gl,'difference'=>round($gl-$subledger,2),'balanced'=>abs($gl-$subledger)<.01);
|
||||
}
|
||||
public function run($code,array$f){if($code==='trial_balance')return$this->trialBalance($f);if(in_array($code,array('profit_loss','balance_sheet'),true))return$this->statement($code,$f);if(in_array($code,array('general_ledger','journal_register','account_detail'),true))return$this->ledger($f);if($code==='cash_flow')return$this->cashFlow($f);if($code==='changes_equity')return$this->equity($f);if($code==='actual_budget')return$this->actualBudget($f);if($code==='comparative')return$this->comparative($f);if($code==='purchase_returns')return$this->purchaseReturns($f);if($code==='supplier_refunds')return$this->supplierRefunds($f);if($code==='outstanding_supplier_refunds')return$this->supplierRefunds($f,true);if($code==='supplier_debit_notes')return$this->supplierDebitNotes($f);if(in_array($code,array('sales_summary','sales_by_customer','sales_by_item','sales_by_barcode','running_invoices','sales_returns','revenue_recognition','unbilled_deliveries','receivable_reconciliation'),true))return$this->salesReport($code,$f);throw new BusinessException('Jenis laporan tidak valid.');}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,17 @@ class PurchaseService
|
||||
return array($amount,$accountId);
|
||||
}
|
||||
|
||||
private function assertPurchasePaymentSchema()
|
||||
{
|
||||
$missing=array();
|
||||
foreach(array('bank_charge_amount','bank_charge_account_id','total_cash_out')as$column)if(!$this->CI->db->field_exists($column,'supplier_payments'))$missing[]=$column;
|
||||
if(!$this->CI->db->table_exists('purchase_payment_sources'))$missing[]='purchase_payment_sources';
|
||||
if($missing)throw new BusinessException('Struktur database pembayaran pembelian belum diperbarui ('.implode(', ',$missing).'). Jalankan: php index.php migrate latest');
|
||||
}
|
||||
|
||||
public function prepayOrderWithBankCharge($poId,$date,array$sources,$reference,$notes,$user,$bankCharge=0,$bankChargeAccount=0)
|
||||
{
|
||||
$this->assertPurchasePaymentSchema();
|
||||
return$this->CI->transactionservice->run(function()use($poId,$date,$sources,$reference,$notes,$user,$bankCharge,$bankChargeAccount){
|
||||
$po=$this->CI->db->query('SELECT * FROM purchase_orders WHERE id=? FOR UPDATE',array((int)$poId))->row();
|
||||
if(!$po||!in_array($po->status,array('approved','partially_received'),true))throw new BusinessException('PO belum disetujui atau sudah selesai.');
|
||||
@@ -57,6 +66,7 @@ class PurchaseService
|
||||
|
||||
public function settleSupplierInvoiceMultiWithBankCharge($id,array$sources,$paymentDate,$reference,$user,$bankCharge=0,$bankChargeAccount=0)
|
||||
{
|
||||
$this->assertPurchasePaymentSchema();
|
||||
$bill=$this->CI->db->get_where('supplier_invoices',array('id'=>(int)$id))->row();if(!$bill)throw new BusinessException('Invoice supplier tidak ditemukan.');
|
||||
$po=$this->CI->db->select('po.*')->from('supplier_invoice_lines sil')->join('purchase_order_lines pol','pol.id=sil.po_line_id')->join('purchase_orders po','po.id=pol.purchase_order_id')->where('sil.supplier_invoice_id',$id)->limit(1)->get()->row();
|
||||
$alreadyPaid=$po&&in_array($po->payment_timing,array('prepaid','on_receipt'),true)&&$po->paid_amount>=$bill->total-.001;
|
||||
@@ -88,6 +98,7 @@ class PurchaseService
|
||||
|
||||
public function paymentMultiWithBankCharge($supplierId,$date,array$sources,array$allocations,$allowAdvance,$user,$reference,$bankCharge=0,$bankChargeAccount=0)
|
||||
{
|
||||
$this->assertPurchasePaymentSchema();
|
||||
return$this->CI->transactionservice->run(function()use($supplierId,$date,$sources,$allocations,$allowAdvance,$user,$reference,$bankCharge,$bankChargeAccount){
|
||||
$supplier=$this->CI->db->get_where('suppliers',array('id'=>(int)$supplierId))->row();if(!$supplier)throw new BusinessException('Supplier tidak ditemukan.');
|
||||
list($valid,$gross)=$this->purchasePaymentSources($sources);list($charge,$chargeAccount)=$this->purchaseBankCharge($bankCharge,$bankChargeAccount);
|
||||
|
||||
@@ -295,7 +295,10 @@ class SalesService
|
||||
$invoice=$this->invoiceForEditor($invoiceId);$start=max(0,(int)$start);$length=max(1,min(100,(int)$length));$search=trim((string)$search);
|
||||
$base=function()use($invoiceId,$search){$this->CI->db->from('invoice_details d')->join('items it','it.id=d.items_id','left')->join('accounts a','a.id=d.account_id','left')->join('accounts ca','ca.id=d.counter_account_id','left')->where(array('d.invoice_id'=>(int)$invoiceId,'d.company_id'=>$this->company()));if($search!=='')$this->CI->db->group_start()->like('d.nama_item',$search)->or_like('d.specification',$search)->or_like('d.keterangan',$search)->or_like('it.kode_detail',$search)->or_like('a.nama_akun',$search)->or_like('ca.nama_akun',$search)->group_end();};
|
||||
$base();$filtered=(int)$this->CI->db->count_all_results();$total=(int)$this->CI->db->where(array('invoice_id'=>(int)$invoiceId,'company_id'=>$this->company()))->count_all_results('invoice_details');
|
||||
$base();$rows=$this->CI->db->select("d.*,it.kode_detail item_code,a.kode_akun,a.nama_akun,ca.kode_akun counter_kode_akun,ca.nama_akun counter_nama_akun,(SELECT GROUP_CONCAT(CONCAT(ib.barcode,IF(NULLIF(ib.serial_number,'') IS NULL,'',CONCAT(' / ',ib.serial_number))) ORDER BY ilb.id SEPARATOR '||') FROM invoice_line_barcodes ilb JOIN item_barcodes ib ON ib.id=ilb.barcode_id WHERE ilb.invoice_detail_id=d.id) barcode_labels",false)->order_by('d.service_date','ASC')->order_by('d.id','ASC')->limit($length,$start)->get()->result();
|
||||
$base();$rows=$this->CI->db->select("d.*,it.kode_detail item_code,it.harga_jual item_sale_price,kb.tracking_type,kb.unit item_unit,w.nama warehouse_name,a.kode_akun,a.nama_akun,ca.kode_akun counter_kode_akun,ca.nama_akun counter_nama_akun,(SELECT GROUP_CONCAT(CONCAT(ib.barcode,IF(NULLIF(ib.serial_number,'') IS NULL,'',CONCAT(' / ',ib.serial_number))) ORDER BY ilb.id SEPARATOR '||') FROM invoice_line_barcodes ilb JOIN item_barcodes ib ON ib.id=ilb.barcode_id WHERE ilb.invoice_detail_id=d.id) barcode_labels",false)->join('kode_barang kb','kb.id=it.kode_id','left')->join('warehouses w','w.id=d.warehouse_id','left')->order_by('d.service_date','ASC')->order_by('d.id','ASC')->limit($length,$start)->get()->result();
|
||||
$lineIds=array_map(function($row){return(int)$row->id;},$rows);$allocations=array();
|
||||
if($lineIds){$bars=$this->CI->db->select('ilb.invoice_detail_id,ilb.barcode_id id,ib.barcode,ib.serial_number,ilb.qty,(ib.qty_sisa-COALESCE(ib.reserved_qty,0)-COALESCE(ib.return_reserved_qty,0)+ilb.qty) available,kb.tracking_type',false)->from('invoice_line_barcodes ilb')->join('item_barcodes ib','ib.id=ilb.barcode_id')->join('items it','it.id=ib.item_id')->join('kode_barang kb','kb.id=it.kode_id','left')->where('ilb.company_id',$this->company())->where_in('ilb.invoice_detail_id',$lineIds)->order_by('ilb.id')->get()->result();foreach($bars as$bar)$allocations[(int)$bar->invoice_detail_id][]=$bar;}
|
||||
foreach($rows as$row)$row->barcode_allocations=$allocations[(int)$row->id]??array();
|
||||
return array('invoice'=>$invoice,'rows'=>$rows,'recordsTotal'=>$total,'recordsFiltered'=>$filtered,'summary'=>array('line_count'=>$total,'subtotal'=>(float)$invoice->subtotal_before_tax,'discount'=>(float)$invoice->discount_total,'tax'=>(float)$invoice->tax_total,'total'=>(float)$invoice->total));
|
||||
}
|
||||
public function updateInvoiceHeader($id,array$data,$user)
|
||||
@@ -326,26 +329,51 @@ class SalesService
|
||||
$this->CI->db->where(array('invoice_detail_id'=>(int)$lineId,'company_id'=>$this->company()))->delete('invoice_line_barcodes');
|
||||
}
|
||||
|
||||
public function updateNonInventoryLine($invoiceId,$lineId,array$data,$user)
|
||||
public function updateInvoiceLine($invoiceId,$lineId,array$data,$user)
|
||||
{
|
||||
return $this->CI->transactionservice->run(function()use($invoiceId,$lineId,$data,$user){
|
||||
$invoice=$this->lockInvoice($invoiceId,'draft');$this->assertDraftOwner($invoice,$user);
|
||||
$line=$this->CI->db->query('SELECT * FROM invoice_details WHERE id=? AND invoice_id=? AND company_id=? FOR UPDATE',array((int)$lineId,$invoice->id,$this->company()))->row();
|
||||
if(!$line||!in_array($line->line_type,array('service','savings'),true))throw new BusinessException('Hanya item Jasa/Nonbarang atau Tabungan pada Draft yang dapat diedit.');
|
||||
$type=$line->line_type;$description=trim((string)($data['description']??''));if($description==='')throw new BusinessException('Deskripsi item wajib diisi.');
|
||||
if(!$line||!in_array($line->line_type,array('inventory','service','savings'),true))throw new BusinessException('Item invoice ini tidak dapat diedit.');
|
||||
$type=$line->line_type;if(isset($data['line_type'])&&$data['line_type']!==''&&$data['line_type']!==$type)throw new BusinessException('Jenis item tidak dapat diubah saat edit. Hapus lalu tambahkan kembali bila jenis transaksi berubah.');
|
||||
if($type==='inventory')$this->assertInventoryInvoiceLineEditable($line);
|
||||
$description=$type==='inventory'?'':trim((string)($data['description']??''));if($type!=='inventory'&&$description==='')throw new BusinessException('Deskripsi item wajib diisi.');
|
||||
$account=$this->tenant('accounts',$data['account_id']??0,$type==='savings'?'Akun simpanan customer':'Akun pendapatan');
|
||||
if(isset($account->is_active)&&!(int)$account->is_active||isset($account->allow_posting)&&!(int)$account->allow_posting)throw new BusinessException('Akun item tidak aktif atau tidak dapat diposting.');
|
||||
$counter=null;if($type==='service'&&(string)$account->tipe!=='revenue')throw new BusinessException('Jasa harus menggunakan akun pendapatan.');
|
||||
$mayUseHidden=function_exists('is_master_admin_user')&&is_master_admin_user();if((isset($account->is_active)&&(int)$account->is_active!==1)||(isset($account->allow_posting)&&(int)$account->allow_posting!==1)||(!$mayUseHidden&&isset($account->is_hidden)&&(int)$account->is_hidden===1))throw new BusinessException('Akun item tidak aktif atau tidak dapat diposting.');
|
||||
$counter=null;if(in_array($type,array('inventory','service'),true)&&(string)$account->tipe!=='revenue')throw new BusinessException('Barang dan jasa harus menggunakan akun pendapatan.');
|
||||
if($type==='savings'&&(string)$account->tipe!=='liability')throw new BusinessException('Tabungan harus menggunakan akun Passiva/Kewajiban.');
|
||||
$qty=$type==='savings'?1:$this->qty($data['qty']??0);$price=$this->money($data['unit_price']??0);$discount=$type==='savings'?0:max(0,$this->money($data['discount_amount']??0));if($discount>$qty*$price)throw new BusinessException('Diskon tidak boleh melebihi nilai bruto item.');
|
||||
$tax=$type==='service'?max(0,(float)str_replace(',','.',(string)($data['tax_rate']??0))):0;$net=max(0,$qty*$price-$discount);$taxAmount=round($net*$tax/100,2);$service=$data['service_date']??$line->service_date;if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',(string)$service))throw new BusinessException('Tanggal transaksi item tidak valid.');
|
||||
$update=$this->columns('invoice_details',array('service_date'=>$service,'tanggal'=>$service,'account_id'=>$account->id,'counter_account_id'=>$counter?$counter->id:null,'nama_item'=>$description,'unit'=>trim((string)($data['unit']??$line->unit)),'qty'=>$qty,'harga'=>$price,'discount_amount'=>$discount,'tax_rate'=>$tax,'tax_amount'=>$taxAmount,'net_amount'=>$net,'subtotal'=>$net+$taxAmount,'subtotal_asli'=>$net+$taxAmount,'keterangan'=>trim((string)($data['notes']??''))));
|
||||
$item=null;$warehouse=null;$kb=null;if($type==='inventory'){$item=$this->tenant('items',$data['item_id']??0,'Barang');$warehouse=$this->tenant('warehouses',$data['warehouse_id']??0,'Gudang');if($item->status!=='active')throw new BusinessException('Barang '.$item->nama_barang.' belum aktif.');$kb=$this->CI->db->get_where('kode_barang',array('id'=>$item->kode_id))->row();}
|
||||
$qty=$type==='savings'?1:$this->qty($data['qty']??0);$price=$this->money($data['unit_price']??($item?$item->harga_jual:0));$discount=$type==='savings'?0:max(0,$this->money($data['discount_amount']??0));if($discount>$qty*$price)throw new BusinessException('Diskon tidak boleh melebihi nilai bruto item.');
|
||||
$tax=$type==='savings'?0:max(0,(float)str_replace(',','.',(string)($data['tax_rate']??0)));$net=max(0,$qty*$price-$discount);$taxAmount=round($net*$tax/100,2);$service=$data['service_date']??$line->service_date;if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',(string)$service))throw new BusinessException('Tanggal transaksi item tidak valid.');
|
||||
$requestedBarcodes=(array)($data['barcodes']??array());if($type==='inventory'&&!$requestedBarcodes)throw new BusinessException('Pilih minimal satu barcode untuk barang gudang.');
|
||||
$beforeBars=$this->CI->db->where(array('invoice_detail_id'=>$line->id,'company_id'=>$this->company()))->order_by('id')->get('invoice_line_barcodes')->result_array();$before=array('line'=>$line,'barcodes'=>$beforeBars);
|
||||
$lineJournalMode=true;$otherLines=$this->CI->db->select('id,line_type,subtotal,total_hpp_barang')->where(array('invoice_id'=>$invoice->id,'company_id'=>$this->company()))->where('id !=',$line->id)->get('invoice_details')->result();foreach($otherLines as$other)if($other->line_type!=='savings'&&((float)$other->subtotal>0||(float)$other->total_hpp_barang>0)){$otherJournal=$this->initialLineJournal($other->id);if(!$otherJournal||$otherJournal->is_reversed||$otherJournal->status!=='posted'){$lineJournalMode=false;break;}}
|
||||
$this->reverseInvoiceLineInitialJournal($line,$user,'Item Draft invoice '.$invoice->no_invoice.' diedit');
|
||||
$this->CI->db->where(array('id'=>$line->id,'invoice_id'=>$invoice->id,'company_id'=>$this->company()))->update('invoice_details',$update);$updated=$this->CI->db->get_where('invoice_details',array('id'=>$line->id,'company_id'=>$this->company()))->row();$customer=$this->tenant('customers',$invoice->customer_id,'Customer');if($lineJournalMode)$this->postInvoiceLineInitialJournal($invoice,$line->id,$updated,null,array(),$customer,$user);$this->refreshInvoiceTotals($invoice->id);$this->CI->auditservice->record('sales','invoice_detail',$line->id,'update',$line,$update,$user);return$line->id;
|
||||
$cleanBarcodes=array();if($type==='inventory'){$this->detachDraftDeliveryLine($line);$this->releaseInvoiceLineBarcodes($line->id);$seen=array();$barcodeQty=0;foreach($requestedBarcodes as$requested){$barcodeId=(int)($requested['id']??$requested['barcode_id']??0);if(!$barcodeId||isset($seen[$barcodeId]))throw new BusinessException('Barcode tidak boleh dipilih dua kali pada satu item.');$seen[$barcodeId]=true;$barcodeQtyValue=$this->qty($requested['qty']??1);$barcode=$this->CI->db->query("SELECT * FROM item_barcodes WHERE id=? AND item_id=? AND warehouse_id=? AND status IN('available','reserved') FOR UPDATE",array($barcodeId,$item->id,$warehouse->id))->row();if(!$barcode)throw new BusinessException('Barcode barang '.$item->nama_barang.' tidak tersedia pada gudang terpilih.');if($this->CI->db->where(array('invoice_id'=>$invoice->id,'barcode_id'=>$barcodeId))->count_all_results('invoice_line_barcodes'))throw new BusinessException('Barcode '.$barcode->barcode.' sudah dipakai item lain pada invoice ini.');if($kb&&strtoupper((string)$kb->tracking_type)==='UNIT'){$used=(int)$this->CI->db->select('COUNT(*) total',false)->from('invoice_line_barcodes used')->join('invoice_details used_detail','used_detail.id=used.invoice_detail_id AND used_detail.stock_posted_at IS NULL')->join('invoices used_invoice','used_invoice.id=used.invoice_id')->where(array('used.barcode_id'=>$barcode->id,'used_invoice.company_id'=>$this->company()))->where('used_invoice.deleted_at IS NULL',null,false)->where("COALESCE(used_invoice.status,'') <> 'cancelled'",null,false)->get()->row()->total;if($used>0)throw new BusinessException('Barcode/Serial '.$barcode->barcode.' sedang digunakan invoice lain.');if(abs($barcodeQtyValue-1)>.0001)throw new BusinessException('Barcode UNIT harus memiliki qty 1.');}$available=(float)$barcode->qty_sisa-(float)$barcode->reserved_qty-(float)($barcode->return_reserved_qty??0);if($barcodeQtyValue>$available+.0001)throw new BusinessException('Qty barcode '.$barcode->barcode.' melebihi stok tersedia.');$reservedAfter=(float)$barcode->reserved_qty+$barcodeQtyValue;$barcodeUpdate=array('reserved_qty'=>$reservedAfter,'version'=>(int)$barcode->version+1);if($kb&&strtoupper((string)$kb->tracking_type)==='UNIT')$barcodeUpdate['status']='reserved';$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',$barcodeUpdate);if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode sedang diproses pengguna lain. Silakan ulangi.');$barcode->reserved_qty=$reservedAfter;$cleanBarcodes[]=array('row'=>$barcode,'qty'=>$barcodeQtyValue);$barcodeQty+=$barcodeQtyValue;}if(abs($barcodeQty-$qty)>.0001)throw new BusinessException('Total qty barcode harus sama dengan qty barang '.$item->nama_barang.'.');}
|
||||
$update=$this->columns('invoice_details',array('service_date'=>$service,'tanggal'=>$service,'account_id'=>$account->id,'counter_account_id'=>$counter?$counter->id:null,'items_id'=>$item?$item->id:null,'warehouse_id'=>$warehouse?$warehouse->id:null,'delivery_line_id'=>null,'nama_item'=>$type==='inventory'?$item->nama_barang:$description,'unit'=>$kb?$kb->unit:trim((string)($data['unit']??$line->unit)),'qty'=>$qty,'harga'=>$price,'discount_amount'=>$discount,'tax_rate'=>$tax,'tax_amount'=>$taxAmount,'net_amount'=>$net,'subtotal'=>$net+$taxAmount,'subtotal_asli'=>$net+$taxAmount,'total_hpp_barang'=>$item?round($qty*(float)$item->harga_beli,2):0,'keterangan'=>trim((string)($data['notes']??''))));
|
||||
$this->CI->db->where(array('id'=>$line->id,'invoice_id'=>$invoice->id,'company_id'=>$this->company()))->update('invoice_details',$update);if($type==='inventory')foreach($cleanBarcodes as$bar)$this->CI->db->insert('invoice_line_barcodes',array('company_id'=>$this->company(),'invoice_id'=>$invoice->id,'invoice_detail_id'=>$line->id,'sales_delivery_id'=>null,'sales_delivery_line_id'=>null,'barcode_id'=>$bar['row']->id,'qty'=>$bar['qty'],'created_at'=>date('Y-m-d H:i:s')));
|
||||
$updated=$this->CI->db->get_where('invoice_details',array('id'=>$line->id,'company_id'=>$this->company()))->row();$customer=$this->tenant('customers',$invoice->customer_id,'Customer');if($lineJournalMode)$this->postInvoiceLineInitialJournal($invoice,$line->id,$updated,$item,$cleanBarcodes,$customer,$user);$this->refreshInvoiceTotals($invoice->id);$this->updateInvoiceDeliveryStatus($invoice->id);$afterBars=$this->CI->db->where(array('invoice_detail_id'=>$line->id,'company_id'=>$this->company()))->order_by('id')->get('invoice_line_barcodes')->result_array();$this->CI->auditservice->record('sales','invoice_detail',$line->id,'update',$before,array('line'=>$updated,'barcodes'=>$afterBars),$user);return$line->id;
|
||||
});
|
||||
}
|
||||
|
||||
/** Kompatibilitas pemanggil lama; seluruh validasi sekarang terpusat. */
|
||||
public function updateNonInventoryLine($invoiceId,$lineId,array$data,$user){return$this->updateInvoiceLine($invoiceId,$lineId,$data,$user);}
|
||||
|
||||
private function assertInventoryInvoiceLineEditable($line)
|
||||
{
|
||||
if(!empty($line->stock_posted_at)||!empty($line->stock_log_id))throw new BusinessException('Barang sudah memiliki mutasi stok dan tidak dapat diedit. Gunakan retur atau koreksi persediaan.');
|
||||
$deliveryLineId=(int)$line->delivery_line_id;if($deliveryLineId){$delivery=$this->CI->db->select('l.stock_posted_at,d.status')->from('sales_delivery_lines l')->join('sales_deliveries d','d.id=l.sales_delivery_id')->where(array('l.id'=>$deliveryLineId,'d.company_id'=>$this->company()))->get()->row();if(!$delivery||$delivery->status!=='draft'||!empty($delivery->stock_posted_at))throw new BusinessException('Surat Jalan barang sudah diproses. Gunakan retur/koreksi agar stok dan audit trail tetap benar.');}
|
||||
$stockCount=(int)$this->CI->db->query("SELECT COUNT(*) total FROM stock_logs WHERE ((ref_type='invoice_details' AND ref_id=?) OR (? > 0 AND ref_type='sales_delivery' AND ref_id=?))",array((int)$line->id,$deliveryLineId,$deliveryLineId))->row()->total;
|
||||
$movementCount=(int)$this->CI->db->query("SELECT COUNT(*) total FROM item_movements WHERE ((reference_type IN('invoice','invoice_details') AND reference_id=?) OR (? > 0 AND reference_type='sales_delivery' AND reference_id=?))",array((int)$line->id,$deliveryLineId,$deliveryLineId))->row()->total;
|
||||
$ledgerCount=(int)$this->CI->db->query("SELECT COUNT(*) total FROM inventory_ledger WHERE ((document_type IN('invoice','invoice_details') AND document_line_id=?) OR (? > 0 AND document_type='sales_delivery' AND document_line_id=?))",array((int)$line->id,$deliveryLineId,$deliveryLineId))->row()->total;
|
||||
if($stockCount||$movementCount||$ledgerCount)throw new BusinessException('Barang sudah terhubung dengan stock log atau item movement. Edit dihentikan; gunakan retur/koreksi persediaan.');
|
||||
}
|
||||
|
||||
private function detachDraftDeliveryLine($line)
|
||||
{
|
||||
if(!(int)$line->delivery_line_id)return;$deliveryLine=$this->CI->db->select('l.sales_delivery_id,l.stock_posted_at,d.status')->from('sales_delivery_lines l')->join('sales_deliveries d','d.id=l.sales_delivery_id')->where(array('l.id'=>(int)$line->delivery_line_id,'d.company_id'=>$this->company()))->get()->row();if(!$deliveryLine||$deliveryLine->status!=='draft'||!empty($deliveryLine->stock_posted_at))throw new BusinessException('Item tidak dapat dilepas dari Surat Jalan yang sudah diproses.');$this->CI->db->where('sales_delivery_line_id',(int)$line->delivery_line_id)->delete('sales_delivery_barcodes');$this->CI->db->where('id',(int)$line->delivery_line_id)->delete('sales_delivery_lines');if(!$this->CI->db->where('sales_delivery_id',(int)$deliveryLine->sales_delivery_id)->count_all_results('sales_delivery_lines'))$this->CI->db->where(array('id'=>(int)$deliveryLine->sales_delivery_id,'company_id'=>$this->company()))->update('sales_deliveries',array('status'=>'cancelled'));
|
||||
}
|
||||
|
||||
public function updateDelivery($id,array$data,array$attachments,$user)
|
||||
{
|
||||
return$this->CI->transactionservice->run(function()use($id,$data,$attachments,$user){$d=$this->CI->db->query('SELECT * FROM sales_deliveries WHERE id=? AND company_id=? FOR UPDATE',array((int)$id,$this->company()))->row();if(!$d||$d->status!=='draft')throw new BusinessException('Hanya Surat Jalan Draft yang dapat dilengkapi.');$date=$data['delivery_date']??$d->delivery_date;if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date))throw new BusinessException('Tanggal pengiriman tidak valid.');$update=array('delivery_date'=>$date,'address'=>trim((string)($data['address']??'')),'driver_name'=>trim((string)($data['driver_name']??'')),'vehicle_no'=>trim((string)($data['vehicle_no']??'')),'courier'=>trim((string)($data['courier']??'')),'tracking_no'=>trim((string)($data['tracking_no']??'')),'recipient_name'=>trim((string)($data['recipient_name']??'')),'notes'=>trim((string)($data['notes']??'')),'version'=>(int)$d->version+1);$this->CI->db->where('id',$d->id)->update('sales_deliveries',$update);foreach($attachments as$a)$this->insertAttachment('sales_delivery',$d->id,$a,$user);$this->CI->auditservice->record('sales','sales_delivery',$d->id,'update',$d,$update,$user);return$d->id;});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Migration_Expand_company_bank_accounts extends CI_Migration
|
||||
{
|
||||
private function column($name, $definition)
|
||||
{
|
||||
if (!$this->db->field_exists($name, 'company_bank_accounts')) {
|
||||
$this->db->query("ALTER TABLE `company_bank_accounts` ADD `{$name}` {$definition}");
|
||||
}
|
||||
}
|
||||
|
||||
public function up()
|
||||
{
|
||||
if (!$this->db->table_exists('company_bank_accounts')) return;
|
||||
|
||||
$this->column('product_name', "VARCHAR(120) NULL AFTER `account_holder`");
|
||||
$this->column('account_type', "ENUM('savings','deposit') NOT NULL DEFAULT 'savings' AFTER `product_name`");
|
||||
$this->column('interest_rate', "DECIMAL(7,4) NOT NULL DEFAULT 0 AFTER `account_type`");
|
||||
$this->column('opened_at', "DATE NULL AFTER `interest_rate`");
|
||||
$this->column('maturity_date', "DATE NULL AFTER `opened_at`");
|
||||
$this->column('gl_account_id', "INT NULL AFTER `currency`");
|
||||
|
||||
if ($this->db->table_exists('cash_accounts')) {
|
||||
$this->db->query("UPDATE company_bank_accounts b
|
||||
JOIN cash_accounts ca ON ca.bank_account_no=b.account_number
|
||||
AND (ca.company_id=b.company_id OR ca.company_id IS NULL)
|
||||
JOIN accounts a ON a.id=ca.gl_account_id AND a.company_id=b.company_id
|
||||
SET b.gl_account_id=ca.gl_account_id
|
||||
WHERE b.gl_account_id IS NULL");
|
||||
}
|
||||
|
||||
if (!$this->db->query("SHOW INDEX FROM company_bank_accounts WHERE Key_name='idx_company_bank_report'")->row()) {
|
||||
$this->db->query("ALTER TABLE company_bank_accounts ADD KEY idx_company_bank_report(company_id,account_type,is_active,maturity_date)");
|
||||
}
|
||||
if (!$this->db->query("SHOW INDEX FROM company_bank_accounts WHERE Key_name='idx_company_bank_gl'")->row()) {
|
||||
$this->db->query("ALTER TABLE company_bank_accounts ADD KEY idx_company_bank_gl(company_id,gl_account_id)");
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
throw new RuntimeException('Metadata rekening perusahaan dipertahankan untuk audit dan dokumen lama.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Memulihkan skema pembayaran pembelian pada database yang metadata
|
||||
* migrasinya sudah maju, tetapi kolom biaya bank belum ikut terpasang.
|
||||
*/
|
||||
class Migration_Repair_purchase_payment_bank_charge_schema extends CI_Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (!$this->db->table_exists('supplier_payments')) {
|
||||
throw new RuntimeException('Tabel supplier_payments belum tersedia. Jalankan migrasi pembelian sebelumnya terlebih dahulu.');
|
||||
}
|
||||
|
||||
if (!$this->db->field_exists('bank_charge_amount', 'supplier_payments')) {
|
||||
$this->db->query('ALTER TABLE supplier_payments ADD bank_charge_amount DECIMAL(18,2) NOT NULL DEFAULT 0 AFTER amount');
|
||||
}
|
||||
if (!$this->db->field_exists('bank_charge_account_id', 'supplier_payments')) {
|
||||
$this->db->query('ALTER TABLE supplier_payments ADD bank_charge_account_id INT NULL AFTER bank_charge_amount');
|
||||
}
|
||||
if (!$this->db->field_exists('total_cash_out', 'supplier_payments')) {
|
||||
$this->db->query('ALTER TABLE supplier_payments ADD total_cash_out DECIMAL(18,2) NOT NULL DEFAULT 0 AFTER bank_charge_account_id');
|
||||
}
|
||||
|
||||
// Pembayaran lama tidak mempunyai biaya bank, sehingga total kas
|
||||
// keluar sama dengan nilai pokok pembayaran yang sudah tersimpan.
|
||||
$this->db->query('UPDATE supplier_payments SET bank_charge_amount=0 WHERE bank_charge_amount IS NULL');
|
||||
$this->db->query('UPDATE supplier_payments SET total_cash_out=amount+bank_charge_amount WHERE total_cash_out IS NULL OR total_cash_out=0');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
throw new RuntimeException('Kolom pembayaran pembelian dipertahankan untuk menjaga histori dan audit transaksi.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Memulihkan ENUM supplier_invoices.status pada database hasil clone yang
|
||||
* versi migrasinya sudah maju, tetapi definisi kolomnya masih versi lama.
|
||||
*/
|
||||
class Migration_Repair_supplier_invoice_status_enum extends CI_Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (!$this->db->table_exists('supplier_invoices')) {
|
||||
throw new RuntimeException('Tabel supplier_invoices belum tersedia. Jalankan migrasi pembelian sebelumnya terlebih dahulu.');
|
||||
}
|
||||
|
||||
// Gunakan ENUM transisi supaya seluruh nilai legacy tetap valid saat
|
||||
// dikonversi ke workflow supplier invoice yang berlaku sekarang.
|
||||
$this->db->query("ALTER TABLE supplier_invoices MODIFY status ENUM('draft','submitted','approved','verified','posted','partial','paid','cancelled','reversed') NOT NULL DEFAULT 'verified'");
|
||||
$this->db->query("UPDATE supplier_invoices SET status='verified' WHERE status IN('draft','submitted','approved')");
|
||||
$this->db->query("UPDATE supplier_invoices SET status=IF(COALESCE(balance,0)<=0,'paid','partial') WHERE status='posted'");
|
||||
$this->db->query("ALTER TABLE supplier_invoices MODIFY status ENUM('verified','partial','paid','cancelled','reversed') NOT NULL DEFAULT 'verified'");
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
throw new RuntimeException('Status supplier invoice dipertahankan untuk menjaga histori dan workflow pembayaran.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Memperbaiki akun sistem yang dibuat setelah fondasi multi-company sehingga
|
||||
* company_id-nya masih NULL dan ditolak saat jurnal divalidasi.
|
||||
*/
|
||||
class Migration_Repair_system_account_company_scope extends CI_Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (!$this->db->table_exists('companies')) return;
|
||||
|
||||
$company = $this->db->where('is_active', 1)->order_by('id')->get('companies')->row();
|
||||
if (!$company) {
|
||||
throw new RuntimeException('Perusahaan aktif tidak ditemukan untuk memperbaiki scope akun sistem.');
|
||||
}
|
||||
|
||||
$companyId = (int) $company->id;
|
||||
if ($this->db->table_exists('accounts') && $this->db->field_exists('company_id', 'accounts')) {
|
||||
$this->db->where('company_id IS NULL OR company_id=0', null, false)
|
||||
->update('accounts', array('company_id'=>$companyId));
|
||||
}
|
||||
|
||||
if ($this->db->table_exists('system_account_mappings') && $this->db->field_exists('company_id', 'system_account_mappings')) {
|
||||
$this->db->query(
|
||||
'UPDATE system_account_mappings m '
|
||||
.'JOIN accounts a ON a.id=m.account_id '
|
||||
.'SET m.company_id=COALESCE(NULLIF(a.company_id,0),?) '
|
||||
.'WHERE m.company_id IS NULL OR m.company_id=0',
|
||||
array($companyId)
|
||||
);
|
||||
}
|
||||
|
||||
$mapping = $this->db->select('m.account_id,a.tipe,a.is_active,a.allow_posting,a.company_id')
|
||||
->from('system_account_mappings m')
|
||||
->join('accounts a', 'a.id=m.account_id')
|
||||
->where(array('m.mapping_key'=>'goods_in_transit','m.is_active'=>1))
|
||||
->get()->row();
|
||||
if (!$mapping || $mapping->tipe !== 'asset' || !(int)$mapping->is_active || !(int)$mapping->allow_posting || !(int)$mapping->company_id) {
|
||||
throw new RuntimeException('Mapping Barang Dalam Perjalanan belum menunjuk akun Aktiva aktif milik perusahaan.');
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
throw new RuntimeException('Scope perusahaan pada akun sistem dipertahankan untuk integritas jurnal.');
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,15 @@ class AccountModel extends CI_Model
|
||||
return array();
|
||||
}
|
||||
|
||||
$rows = $this->db
|
||||
->select('id')
|
||||
$this->db->select('id')
|
||||
->where_in('id', array_values(array_unique($ids)))
|
||||
->where('is_active', 1)
|
||||
->where('allow_posting', 1)
|
||||
->get('accounts')
|
||||
->result_array();
|
||||
->where('allow_posting', 1);
|
||||
if ($this->db->field_exists('company_id', 'accounts')) {
|
||||
$companyId = isset($this->companycontext) ? (int)$this->companycontext->id() : (int)$this->session->userdata('company_id');
|
||||
$this->db->where('company_id', $companyId);
|
||||
}
|
||||
$rows = $this->db->get('accounts')->result_array();
|
||||
return array_map('intval', array_column($rows, 'id'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ class JournalModel extends CI_Model
|
||||
}
|
||||
}
|
||||
|
||||
public function referenceExists($reference)
|
||||
public function referenceExists($reference, $excludeId = 0)
|
||||
{
|
||||
return $this->db->where('no_ref', $reference)->count_all_results('journals') > 0;
|
||||
$this->db->where('no_ref', $reference);
|
||||
if ((int) $excludeId > 0) $this->db->where('id !=', (int) $excludeId);
|
||||
return $this->db->count_all_results('journals') > 0;
|
||||
}
|
||||
|
||||
public function sourceExists($type, $id)
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
|
||||
<div class="col-md-2">
|
||||
<label>Dari</label>
|
||||
<input type="date" id="start_date" class="form-control">
|
||||
<input type="date" id="start_date" class="form-control" value="<?=date('Y-m-01')?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label>Sampai</label>
|
||||
<input type="date" id="end_date" class="form-control">
|
||||
<input type="date" id="end_date" class="form-control" value="<?=date('Y-m-d')?>">
|
||||
</div>
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3" id="ledgerSummary">
|
||||
<div class="col-6 col-lg-3"><div class="card card-body"><small class="text-muted">Saldo Awal</small><strong data-summary="saldo_awal">0</strong></div></div>
|
||||
<div class="col-6 col-lg-3"><div class="card card-body"><small class="text-muted">Total Debit</small><strong data-summary="total_debit">0</strong></div></div>
|
||||
<div class="col-6 col-lg-3"><div class="card card-body"><small class="text-muted">Total Kredit</small><strong data-summary="total_kredit">0</strong></div></div>
|
||||
<div class="col-6 col-lg-3"><div class="card card-body"><small class="text-muted">Saldo Akhir</small><strong data-summary="saldo_akhir">0</strong></div></div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="tableLedger" class="table modern-table align-middle w-100">
|
||||
<thead>
|
||||
@@ -62,6 +69,7 @@ $(function(){
|
||||
opt += `<option value="${a.id}">${a.kode_akun} - ${a.nama_akun}</option>`;
|
||||
});
|
||||
$('#account_id').html(opt);
|
||||
if(res.length) loadData();
|
||||
},'json');
|
||||
|
||||
function loadData(){
|
||||
@@ -78,7 +86,7 @@ $(function(){
|
||||
|
||||
let html = '';
|
||||
|
||||
res.forEach(r=>{
|
||||
(res.data || []).forEach(r=>{
|
||||
html += `
|
||||
<tr>
|
||||
<td>${r.tanggal}</td>
|
||||
@@ -92,8 +100,13 @@ $(function(){
|
||||
});
|
||||
|
||||
$('#tableLedger tbody').html(html);
|
||||
Object.keys(res.summary || {}).forEach(function(key){
|
||||
$('[data-summary="'+key+'"]').text(Number(res.summary[key] || 0).toLocaleString('id-ID'));
|
||||
});
|
||||
|
||||
},'json');
|
||||
},'json').fail(function(xhr){
|
||||
Swal.fire('Error', xhr.responseJSON?.message || 'Data Buku Besar gagal dimuat.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +131,7 @@ $(function(){
|
||||
+ String(now.getDate()).padStart(2, '0');
|
||||
|
||||
const account_id = $('#account_id').val();
|
||||
const start_date = $('#start_date').val() || '0000-00-00';
|
||||
const start_date = $('#start_date').val() || '1900-01-01';
|
||||
const end_date = $('#end_date').val() || today;
|
||||
|
||||
let btn = $(this);
|
||||
@@ -153,4 +166,4 @@ $(function(){
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php $csrfName=$this->security->get_csrf_token_name();$csrfHash=$this->security->get_csrf_hash(); ?>
|
||||
<link rel="stylesheet" href="<?=base_url('assets/css/company-settings.css').'?v='.filemtime(FCPATH.'assets/css/company-settings.css')?>">
|
||||
<section class="company-settings-workspace">
|
||||
<header class="company-settings-hero"><div><span>ADMINISTRASI PERUSAHAAN</span><h3>Company Setting</h3><p>Kelola identitas dokumen, rekening pembayaran, perusahaan, dan mata uang dari satu tempat.</p></div><div class="active-company-chip"><small>Perusahaan aktif</small><b><?=html_escape($active_company->company_code.' - '.$active_company->name)?></b></div></header>
|
||||
<header class="company-settings-hero"><div><span>ADMINISTRASI PERUSAHAAN</span><h3 class="text-light">Company Setting</h3><p>Kelola identitas dokumen, rekening pembayaran, perusahaan, dan mata uang dari satu tempat.</p></div><div class="active-company-chip"><small>Perusahaan aktif</small><b><?=html_escape($active_company->company_code.' - '.$active_company->name)?></b></div></header>
|
||||
<?php if($this->session->flashdata('success')):?><div class="alert alert-success"><?=html_escape($this->session->flashdata('success'))?></div><?php endif?>
|
||||
<?php if($this->session->flashdata('error')):?><div class="alert alert-danger"><?=html_escape($this->session->flashdata('error'))?></div><?php endif?>
|
||||
|
||||
@@ -25,3 +25,26 @@
|
||||
$('.edit-currency').on('click',function(){var r=$(this).data('row'),f=document.getElementById('currencyForm');f.elements.code.value=r.code;f.elements.code.readOnly=true;f.elements.name.value=r.name;f.elements.symbol.value=r.symbol||'';f.elements.decimal_places.value=r.decimal_places;f.elements.is_active.checked=Number(r.is_active)===1;$('#cancelCurrencyEdit').removeClass('d-none');f.scrollIntoView({behavior:'smooth',block:'center'});});$('#cancelCurrencyEdit').on('click',resetCurrency);
|
||||
})(jQuery);
|
||||
</script>
|
||||
<script>
|
||||
(function($){
|
||||
var form=document.getElementById('bankAccountForm');if(!form)return;
|
||||
$('[data-bs-target="#companyBanks"]').html('<i class="bi bi-bank"></i> Akun Bank Perusahaan');
|
||||
var glAccounts=<?=json_encode(array_map(function($a){return array('id'=>$a->id,'text'=>$a->kode_akun.' - '.$a->nama_akun);},$bank_gl_accounts),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)?>;
|
||||
var coaOptions='<option value="">Pilih akun COA</option>'+glAccounts.map(function(a){return '<option value="'+a.id+'">'+$('<div>').text(a.text).html()+'</option>';}).join('');
|
||||
$(form).find('.row.g-3').first().before(
|
||||
'<div class="mb-3"><label class="form-label">Jenis Produk / Jasa Bank</label><input class="form-control" name="product_name" placeholder="Contoh: BritAma Bisnis atau Deposito Berjangka" required></div>'+
|
||||
'<div class="row g-3 mb-3"><div class="col-md-6"><label class="form-label">Jenis Rekening</label><select class="form-select" name="account_type" required><option value="savings">Tabungan</option><option value="deposit">Deposito</option></select></div><div class="col-md-6"><label class="form-label">Bunga Bank (%)</label><input class="form-control" type="number" min="0" step="0.0001" name="interest_rate" value="0"></div><div class="col-md-6"><label class="form-label">Tanggal Pembukaan</label><input class="form-control" type="date" name="opened_at" required></div><div class="col-md-6 maturity-field d-none"><label class="form-label">Tanggal Jatuh Tempo</label><input class="form-control" type="date" name="maturity_date"></div><div class="col-12"><label class="form-label">Akun COA Terhubung</label><select class="form-select select-search" name="gl_account_id" required>'+coaOptions+'</select><small class="text-muted">Gunakan akun Aktiva kas/bank yang menampung transaksi rekening ini.</small></div></div>'
|
||||
);
|
||||
function accountTypeChanged(){var deposit=form.elements.account_type.value==='deposit';$(form).find('.maturity-field').toggleClass('d-none',!deposit);form.elements.maturity_date.required=deposit;if(!deposit)form.elements.maturity_date.value='';}
|
||||
$(form.elements.account_type).on('change',accountTypeChanged);accountTypeChanged();
|
||||
$('#cancelBankEdit').on('click',function(){$(form.elements.gl_account_id).val('').trigger('change');accountTypeChanged();});
|
||||
$('.edit-bank').on('click',function(){var r=$(this).data('row');form.elements.product_name.value=r.product_name||'';form.elements.account_type.value=r.account_type||'savings';form.elements.interest_rate.value=r.interest_rate||0;form.elements.opened_at.value=r.opened_at||'';form.elements.maturity_date.value=r.maturity_date||'';form.elements.gl_account_id.value=r.gl_account_id||'';$(form.elements.gl_account_id).trigger('change');accountTypeChanged();});
|
||||
|
||||
var table=$('#companyBanks table').first(),head=table.find('thead tr');head.html('<th>Bank / Rekening</th><th>Produk</th><th>Mata Uang</th><th>Akun COA</th><th>Status</th><th>Aksi</th>');
|
||||
table.find('tbody tr').each(function(){var tr=$(this),button=tr.find('.edit-bank');if(!button.length){tr.children('td').attr('colspan',6);return;}var r=button.data('row'),cells=tr.children('td'),product=$('<td>').html('<b>'+(r.account_type==='deposit'?'Deposito':'Tabungan')+'</b><small>'+$('<div>').text(r.product_name||'-').html()+' · buka '+(r.opened_at||'-')+(r.maturity_date?' · jatuh tempo '+r.maturity_date:'')+(Number(r.interest_rate)>0?' · bunga '+Number(r.interest_rate).toLocaleString('id-ID')+'%':'')+'</small>'),coa=$('<td>').html(r.kode_akun?'<b>'+$('<div>').text(r.kode_akun).html()+'</b><small>'+$('<div>').text(r.nama_akun||'').html()+'</small>':'<span class="text-warning">Belum terhubung</span>');cells.eq(0).after(product);cells.eq(1).after(coa);tr.data('bankRow',r);});
|
||||
var filters=$('<div class="row g-2 mb-3 bank-report-filter"><div class="col-md-3"><input class="form-control" id="bankFilterName" placeholder="Cari nama bank"></div><div class="col-md-2"><select class="form-select" id="bankFilterType"><option value="">Semua jenis</option><option value="savings">Tabungan</option><option value="deposit">Deposito</option></select></div><div class="col-md-2"><select class="form-select" id="bankFilterStatus"><option value="">Semua status</option><option value="1">Aktif</option><option value="0">Nonaktif</option></select></div><div class="col-md-2"><input type="date" class="form-control" id="bankFilterFrom" title="Jatuh tempo dari"></div><div class="col-md-2"><input type="date" class="form-control" id="bankFilterTo" title="Jatuh tempo sampai"></div><div class="col-md-1"><a class="btn btn-success w-100" id="bankExport" href="<?=base_url('companies/bank_export')?>" title="Export CSV"><i class="bi bi-file-earmark-spreadsheet"></i></a></div></div>');table.before(filters);
|
||||
function filterBanks(){var name=$('#bankFilterName').val().toLowerCase(),type=$('#bankFilterType').val(),status=$('#bankFilterStatus').val(),from=$('#bankFilterFrom').val(),to=$('#bankFilterTo').val();table.find('tbody tr').each(function(){var tr=$(this),r=tr.data('bankRow');if(!r)return;var match=(!name||String(r.bank_name||'').toLowerCase().includes(name))&&(!type||r.account_type===type)&&(status===''||String(Number(r.is_active))===status)&&(!from||(r.maturity_date&&r.maturity_date>=from))&&(!to||(r.maturity_date&&r.maturity_date<=to));tr.toggle(match);});$('#bankExport').attr('href','<?=base_url('companies/bank_export')?>?'+$.param({bank:$('#bankFilterName').val(),type:type,status:status,maturity_from:from,maturity_to:to}));}
|
||||
$('.bank-report-filter').on('input change','input,select',filterBanks);
|
||||
if(location.hash==='#companyBanks'){var trigger=document.querySelector('[data-bs-target="#companyBanks"]');if(trigger)bootstrap.Tab.getOrCreateInstance(trigger).show();}
|
||||
})(jQuery);
|
||||
</script>
|
||||
|
||||
@@ -30,9 +30,19 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
|
||||
let table = $('#tableActivity').DataTable({
|
||||
$(function () {
|
||||
|
||||
if (!$.fn || typeof $.fn.DataTable !== 'function') {
|
||||
$('#tableActivity tbody').remove();
|
||||
$('#tableActivity').append(
|
||||
'<tbody><tr><td colspan="7" class="text-center text-danger py-4">' +
|
||||
'Komponen tabel gagal dimuat. Silakan muat ulang halaman.' +
|
||||
'</td></tr></tbody>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let table = $('#tableActivity').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
ajax: {
|
||||
@@ -99,4 +109,4 @@ $(function () {
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -8,6 +8,7 @@ $statusLabels=array('draft'=>'Draft','reviewed'=>'Sudah Direview','approved'=>'D
|
||||
?>
|
||||
<?php foreach(array('success','error')as$flash):if($this->session->flashdata($flash)):?><div data-app-toast data-icon="<?=$flash==='success'?'success':'error'?>" data-message="<?=html_escape($this->session->flashdata($flash))?>"></div><?php endif;endforeach?>
|
||||
<?php if(!$can_manage_all_salary):?><div class="alert alert-warning"><i class="bi bi-lock-fill me-1"></i> Tampilan ini tidak memuat nilai maupun total gaji terbatas. Akses tersebut memerlukan permission <b>Manage All Gaji</b>.</div><?php endif?>
|
||||
<?php if(!$can_manage_allowances&&in_array($payroll_section,array('salaries','settings','adjustments'),true)):?><div class="alert alert-info"><i class="bi bi-shield-lock-fill me-1"></i> Akses pengelolaan dibatasi pada <b>Gaji Pokok dan Potongan</b>. Tunjangan, bonus, lembur, THR, dan pendapatan tambahan memerlukan permission <b>Kelola Tunjangan & Bonus</b>.</div><?php endif?>
|
||||
|
||||
<?php if($payroll_section==='periods'):?>
|
||||
<div class="hr-workflow-strip" aria-label="Tahapan payroll"><span><b>1</b>Buat Periode</span><span><b>2</b>Lock Kehadiran</span><span><b>3</b>Hitung Draft</span><span><b>4</b>Review HR</span><span><b>5</b>Approve & Final</span><span><b>6</b>Posting Jurnal</span><span><b>7</b>Pembayaran</span></div>
|
||||
@@ -32,23 +33,31 @@ $statusLabels=array('draft'=>'Draft','reviewed'=>'Sudah Direview','approved'=>'D
|
||||
<ul class="nav nav-pills payroll-setting-tabs mb-3" role="tablist"><li class="nav-item"><button class="nav-link active" data-bs-toggle="pill" data-bs-target="#componentsTab">Master Komponen</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="pill" data-bs-target="#positionsTab">Template Posisi</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="pill" data-bs-target="#attendanceRulesTab">Aturan Kehadiran</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="pill" data-bs-target="#generalTab">Pengaturan Umum</button></li></ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="componentsTab"><section class="card hr-panel"><div class="card-header bg-white"><b>Master Komponen</b><small class="d-block text-muted">Komponen tidak otomatis berlaku untuk semua karyawan.</small></div><?php if($permissions['update']):?><div class="card-body border-bottom"><form method="post" action="<?=base_url('hrpayroll/component')?>" class="row g-2"><?=$csrf?><div class="col-md-2"><label class="form-label">Kode</label><input class="form-control" name="code" required></div><div class="col-md-3"><label class="form-label">Nama</label><input class="form-control" name="name" required></div><div class="col-md-2"><label class="form-label">Jenis</label><select class="form-select" name="component_type"><option value="earning">Pendapatan</option><option value="deduction">Potongan</option><option value="employer_cost">Beban perusahaan</option></select></div><div class="col-md-2"><label class="form-label">Kategori</label><select class="form-select" name="category"><option value="basic">Gaji Pokok</option><option value="allowance">Tunjangan</option><option value="overtime">Lembur</option><option value="unpaid_leave">Cuti tak dibayar</option><option value="loan">Pinjaman</option><option value="bpjs">BPJS</option><option value="tax">Pajak</option><option value="thr">THR</option><option value="bonus">Bonus</option><option value="other">Lainnya</option></select></div><div class="col-md-3"><label class="form-label">Metode</label><select class="form-select" name="calculation_method"><?php foreach($methodLabels as$key=>$label):?><option value="<?=$key?>"><?=html_escape($label)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nilai Default</label><input type="number" min="0" step="0.01" class="form-control" name="default_value" value="0"></div><div class="col-md-3"><label class="form-label">Formula</label><input class="form-control" name="formula_expression" placeholder="present_days * 25000"></div><div class="col-md-2"><label class="form-label">Akun Beban</label><select class="form-select searchable" name="expense_account_id"><option value="">Mapping umum</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>"><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Akun Kewajiban</label><select class="form-select searchable" name="liability_account_id"><option value="">Mapping umum</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>"><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>"></div><div class="col-md-1 d-flex align-items-end"><label class="form-check mb-2"><input class="form-check-input" type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-md-2 d-flex align-items-end justify-content-end"><button class="btn btn-primary">Simpan Komponen</button></div></form></div><?php endif?><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Kode</th><th>Komponen</th><th>Jenis</th><th>Metode</th><th class="text-end">Default</th><th>Mapping</th><th>Status</th></tr></thead><tbody><?php foreach($components as$c):?><tr><td><b><?=html_escape($c->code)?></b></td><td><?=html_escape($c->name)?></td><td><?=html_escape($c->component_type)?></td><td><?=html_escape($methodLabels[$c->calculation_method]??$c->calculation_method)?></td><td class="text-end">Rp <?=number_format($c->default_value??$c->fixed_amount,2,',','.')?></td><td><?=html_escape(($c->expense_account_id?:'mapping umum').' / '.($c->liability_account_id?:'mapping umum'))?></td><td><span class="badge <?=$c->is_active?'bg-success':'bg-secondary'?>"><?=$c->is_active?'Aktif':'Nonaktif'?></span></td></tr><?php endforeach?></tbody></table></div></section></div>
|
||||
<div class="tab-pane fade" id="positionsTab"><section class="card hr-panel"><div class="card-header bg-white"><b>Template Posisi</b><small class="d-block text-muted">Override karyawan tetap memiliki prioritas di atas template posisi.</small></div><?php if($permissions['update']):?><div class="card-body border-bottom"><form method="post" action="<?=base_url('hrpayroll/save_position_line')?>" class="row g-2"><?=$csrf?><div class="col-md-3"><label class="form-label">Posisi</label><select class="form-select searchable" name="position_id" required><option value="">Pilih posisi</option><?php foreach($positions as$p):?><option value="<?=$p->id?>"><?=html_escape($p->position_name)?></option><?php endforeach?></select></div><div class="col-md-3"><label class="form-label">Komponen</label><select class="form-select searchable" name="component_id" required><option value="">Pilih komponen</option><?php foreach($components as$c):?><option value="<?=$c->id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Metode</label><select class="form-select" name="calculation_method"><?php foreach($methodLabels as$key=>$label):?><option value="<?=$key?>"><?=html_escape($label)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nilai/Tarif</label><input type="number" min="0" step="0.01" class="form-control" name="amount_override" value="0"></div><div class="col-md-2"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>" required></div><div class="col-md-10"><input class="form-control" name="notes" placeholder="Keterangan template, opsional"></div><div class="col-md-2"><button class="btn btn-primary w-100">Tambah Template</button></div></form></div><?php endif?><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Posisi</th><th>Komponen</th><th>Metode</th><th class="text-end">Nilai</th><th>Efektif</th><th>Status</th></tr></thead><tbody><?php if(!$position_assignments):?><tr><td colspan="6" class="text-center text-muted py-4">Belum ada template posisi.</td></tr><?php endif;foreach($position_assignments as$x):?><tr><td><?=html_escape($x->position_name)?></td><td><?=html_escape($x->component_code.' - '.$x->component_name)?></td><td><?=html_escape($methodLabels[$x->calculation_method]??$x->calculation_method)?></td><td class="text-end">Rp <?=number_format($x->amount_override,2,',','.')?></td><td><?=html_escape($x->effective_from)?><?=!empty($x->effective_to)?' s.d. '.html_escape($x->effective_to):''?></td><td><span class="badge <?=$x->is_active?'bg-success':'bg-secondary'?>"><?=$x->is_active?'Aktif':'Nonaktif'?></span></td></tr><?php endforeach?></tbody></table></div></section></div>
|
||||
<div class="tab-pane fade" id="positionsTab"><section class="card hr-panel"><div class="card-header bg-white"><b>Template Posisi</b><small class="d-block text-muted">Override karyawan tetap memiliki prioritas di atas template posisi.</small></div><?php if($permissions['update']):?><div class="card-body border-bottom"><form method="post" action="<?=base_url('hrpayroll/save_position_line')?>" class="row g-2"><?=$csrf?><div class="col-md-3"><label class="form-label">Posisi</label><select class="form-select searchable" name="position_id" required><option value="">Pilih posisi</option><?php foreach($positions as$p):?><option value="<?=$p->id?>"><?=html_escape($p->position_name)?></option><?php endforeach?></select></div><div class="col-md-3"><label class="form-label">Komponen</label><select class="form-select searchable" name="component_id" required><option value="">Pilih komponen</option><?php foreach($manageable_components as$c):?><option value="<?=$c->id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Metode</label><select class="form-select" name="calculation_method"><?php foreach($methodLabels as$key=>$label):?><option value="<?=$key?>"><?=html_escape($label)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nilai/Tarif</label><input type="number" min="0" step="0.01" class="form-control" name="amount_override" value="0"></div><div class="col-md-2"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>" required></div><div class="col-md-10"><input class="form-control" name="notes" placeholder="Keterangan template, opsional"></div><div class="col-md-2"><button class="btn btn-primary w-100">Tambah Template</button></div></form></div><?php endif?><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Posisi</th><th>Komponen</th><th>Metode</th><th class="text-end">Nilai</th><th>Efektif</th><th>Status</th></tr></thead><tbody><?php if(!$position_assignments):?><tr><td colspan="6" class="text-center text-muted py-4">Belum ada template posisi.</td></tr><?php endif;foreach($position_assignments as$x):?><tr><td><?=html_escape($x->position_name)?></td><td><?=html_escape($x->component_code.' - '.$x->component_name)?></td><td><?=html_escape($methodLabels[$x->calculation_method]??$x->calculation_method)?></td><td class="text-end">Rp <?=number_format($x->amount_override,2,',','.')?></td><td><?=html_escape($x->effective_from)?><?=!empty($x->effective_to)?' s.d. '.html_escape($x->effective_to):''?></td><td><span class="badge <?=$x->is_active?'bg-success':'bg-secondary'?>"><?=$x->is_active?'Aktif':'Nonaktif'?></span></td></tr><?php endforeach?></tbody></table></div></section></div>
|
||||
<div class="tab-pane fade" id="attendanceRulesTab"><section class="card hr-panel"><div class="card-header bg-white"><b>Aturan Kehadiran</b><small class="d-block text-muted">Terlambat secara default tidak dipotong. Cuti tahunan, sakit, izin, dan hari libur tidak dianggap alpa.</small></div><?php if($permissions['update']):?><div class="card-body border-bottom"><form method="post" action="<?=base_url('hrpayroll/save_attendance_rule')?>" class="row g-2"><?=$csrf?><div class="col-md-3"><label class="form-label">Nama Aturan</label><input class="form-control" name="rule_name" required></div><div class="col-md-2"><label class="form-label">Kondisi</label><select class="form-select" name="condition_type"><option value="alpha">Alpa</option><option value="late">Terlambat</option><option value="early_leave">Pulang cepat</option><option value="unpaid">Cuti tidak dibayar</option><option value="missing_checkin">Tidak check-in</option><option value="missing_checkout">Tidak check-out</option></select></div><div class="col-md-2"><label class="form-label">Metode Potongan</label><select class="form-select" name="calculation_method"><option value="none">Tidak dipotong</option><option value="fixed">Nominal tetap</option><option value="per_minute">Per menit</option><option value="per_hour">Per jam</option><option value="per_day">Per hari</option><option value="percentage_daily">Persentase tarif harian</option><option value="formula">Formula</option></select></div><div class="col-md-2"><label class="form-label">Komponen Potongan</label><select class="form-select searchable" name="component_id"><option value="">Tidak ada</option><?php foreach($components as$c):if($c->component_type!=='deduction')continue;?><option value="<?=$c->id?>"><?=html_escape($c->name)?></option><?php endforeach?></select></div><div class="col-md-1"><label class="form-label">Toleransi</label><input type="number" min="0" class="form-control" name="tolerance_minutes" value="0"></div><div class="col-md-2"><label class="form-label">Tarif</label><input type="number" min="0" step="0.01" class="form-control" name="rate_value" value="0"></div><div class="col-md-2"><label class="form-label">Maksimum</label><input type="number" min="0" step="0.01" class="form-control" name="maximum_amount"></div><div class="col-md-3"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>" required></div><div class="col-md-4"><label class="form-label">Formula / Keterangan</label><input class="form-control" name="formula_expression" placeholder="condition_quantity * rule_rate"></div><div class="col-md-1 d-flex align-items-end"><label class="form-check mb-2"><input class="form-check-input" type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-md-2 d-flex align-items-end"><button class="btn btn-primary w-100">Simpan Versi</button></div></form></div><?php endif?><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Aturan</th><th>Kondisi</th><th>Metode</th><th>Toleransi</th><th class="text-end">Tarif</th><th>Komponen</th><th>Efektif</th></tr></thead><tbody><?php foreach($attendance_rules as$r):?><tr><td><?=html_escape($r->rule_name)?></td><td><?=html_escape($r->condition_type)?></td><td><?=html_escape($r->calculation_method)?></td><td><?=number_format($r->tolerance_minutes)?> menit</td><td class="text-end"><?=number_format($r->rate_value,2,',','.')?></td><td><?=html_escape($r->component_name?:'Tidak memotong')?></td><td><?=html_escape($r->effective_from)?></td></tr><?php endforeach?></tbody></table></div></section></div>
|
||||
<div class="tab-pane fade" id="generalTab"><section class="card hr-panel"><div class="card-header bg-white"><b>Pengaturan Umum Payroll</b><small class="d-block text-muted">Berlaku untuk perusahaan aktif dan tidak mengubah histori payroll lama.</small></div><div class="card-body"><div class="row g-3 mb-4"><div class="col-md-4"><div class="payroll-info-box"><b>Sumber Kehadiran</b><span>Rentang tanggal pada periode payroll</span></div></div><div class="col-md-4"><div class="payroll-info-box"><b>Prioritas Komponen</b><span>Override karyawan → template posisi → nilai default</span></div></div><div class="col-md-4"><div class="payroll-info-box"><b>Pengamanan</b><span>Snapshot, separation of duties, idempotency jurnal</span></div></div></div><?php if($permissions['update']):?><form method="post" action="<?=base_url('hrpayroll/save_settings')?>" class="row g-3"><?=$csrf?><div class="col-md-4"><label class="form-label">Pemisahan Tugas</label><label class="form-check form-switch"><input class="form-check-input" type="checkbox" name="separation_of_duties" value="1" <?=!$payroll_settings||$payroll_settings->separation_of_duties?'checked':''?>> Reviewer, approver, dan finalizer harus berbeda</label></div><div class="col-md-3"><label class="form-label">Tanggal Bayar Default</label><input type="number" min="1" max="31" class="form-control" name="default_payment_day" value="<?=html_escape($payroll_settings->default_payment_day??'')?>" placeholder="1–31"></div><div class="col-md-5"><label class="form-label">Catatan Export Bank</label><div class="input-group"><input class="form-control" name="bank_export_note" value="<?=html_escape($payroll_settings->bank_export_note??'')?>" placeholder="Instruksi internal sebelum upload"><button class="btn btn-primary">Simpan</button></div></div></form><?php endif?><p class="text-muted small mt-3 mb-0">Akun jurnal utama dikelola melalui System Account Mapping. Akun khusus dapat dipilih pada masing-masing komponen.</p></div></section></div>
|
||||
</div>
|
||||
|
||||
<?php else:?>
|
||||
<?php if($permissions['create']):?><section class="card hr-panel mb-3"><div class="card-header bg-white"><b>Tambah Penyesuaian Satu Periode</b><small class="d-block text-muted">Tidak membuat jurnal saat disimpan dan tidak berulang ke periode berikutnya.</small></div><div class="card-body"><form method="post" action="<?=base_url('hrpayroll/save_adjustment')?>" class="row g-2"><?=$csrf?><div class="col-md-3"><label class="form-label">Periode Draft</label><select class="form-select searchable" name="payroll_period_id" required><option value="">Pilih periode</option><?php foreach($periods as$p):if($p->workflow_status!=='draft')continue;?><option value="<?=$p->id?>"><?=html_escape($p->period_name)?></option><?php endforeach?></select></div><div class="col-md-3"><label class="form-label">Karyawan</label><select class="form-select searchable" name="employee_id" required><option value="">Pilih karyawan</option><?php foreach($employees as$e):?><option value="<?=$e->id?>"><?=html_escape($e->employee_code.' - '.$e->full_name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Jenis</label><select class="form-select" name="adjustment_type"><option value="earning">Pendapatan</option><option value="deduction">Potongan</option></select></div><div class="col-md-2"><label class="form-label">Komponen</label><select class="form-select searchable" name="component_id" required><option value="">Pilih komponen</option><?php foreach($components as$c):if($c->component_type==='employer_cost')continue;?><option value="<?=$c->id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nominal</label><input type="number" min="0.01" step="0.01" class="form-control" name="amount" required></div><div class="col-md-10"><label class="form-label">Keterangan</label><input class="form-control" name="notes" placeholder="Alasan bonus, rapel, koreksi, atau potongan" required></div><div class="col-md-2 d-flex align-items-end"><button class="btn btn-primary w-100">Simpan</button></div></form></div></section><?php endif?>
|
||||
<section class="card hr-panel"><div class="card-header bg-white"><b>Daftar Penyesuaian</b></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Periode</th><th>Karyawan</th><th>Komponen</th><th>Jenis</th><th class="text-end">Nominal</th><th>Keterangan</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php if(!$adjustments):?><tr><td colspan="8" class="text-center text-muted py-5">Belum ada penyesuaian periode.</td></tr><?php endif;foreach($adjustments as$a):?><tr><td><?=html_escape($a->period_name)?></td><td><b><?=html_escape($a->full_name)?></b><small class="d-block text-muted"><?=html_escape($a->employee_code)?></small></td><td><?=html_escape($a->component_name)?></td><td><span class="badge <?=$a->adjustment_type==='earning'?'bg-success':'bg-warning text-dark'?>"><?=html_escape($a->adjustment_type==='earning'?'Pendapatan':'Potongan')?></span></td><td class="text-end">Rp <?=number_format($a->amount,2,',','.')?></td><td><?=html_escape($a->notes)?></td><td><?=html_escape($statusLabels[$a->workflow_status]??$a->workflow_status)?></td><td><?php if($permissions['update']&&$a->workflow_status==='draft'):?><form method="post" action="<?=base_url('hrpayroll/deactivate_adjustment/'.$a->id)?>" class="js-confirm"><?=$csrf?><button class="btn btn-sm btn-outline-danger">Nonaktifkan</button></form><?php else:?>-<?php endif?></td></tr><?php endforeach?></tbody></table></div></section>
|
||||
<?php if($permissions['create']):?><section class="card hr-panel mb-3"><div class="card-header bg-white"><b>Tambah Penyesuaian Satu Periode</b><small class="d-block text-muted">Tidak membuat jurnal saat disimpan dan tidak berulang ke periode berikutnya.</small></div><div class="card-body"><form method="post" action="<?=base_url('hrpayroll/save_adjustment')?>" class="row g-2"><?=$csrf?><div class="col-md-3"><label class="form-label">Periode Draft</label><select class="form-select searchable" name="payroll_period_id" required><option value="">Pilih periode</option><?php foreach($periods as$p):if($p->workflow_status!=='draft')continue;?><option value="<?=$p->id?>"><?=html_escape($p->period_name)?></option><?php endforeach?></select></div><div class="col-md-3"><label class="form-label">Karyawan</label><select class="form-select searchable" name="employee_id" required><option value="">Pilih karyawan</option><?php foreach($employees as$e):?><option value="<?=$e->id?>"><?=html_escape($e->employee_code.' - '.$e->full_name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Jenis</label><select class="form-select" name="adjustment_type"><?php if($can_manage_allowances):?><option value="earning">Pendapatan / Bonus</option><?php endif?><option value="deduction">Potongan</option></select></div><div class="col-md-2"><label class="form-label">Komponen</label><select class="form-select searchable" name="component_id" required><option value="">Pilih komponen</option><?php foreach($adjustment_components as$c):?><option value="<?=$c->id?>" data-type="<?=html_escape($c->component_type)?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nominal</label><input type="number" min="0.01" step="0.01" class="form-control" name="amount" required></div><div class="col-md-10"><label class="form-label">Keterangan</label><input class="form-control" name="notes" placeholder="Alasan bonus, rapel, koreksi, atau potongan" required></div><div class="col-md-2 d-flex align-items-end"><button class="btn btn-primary w-100">Simpan</button></div></form></div></section><?php endif?>
|
||||
<section class="card hr-panel"><div class="card-header bg-white"><b>Daftar Penyesuaian</b></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Periode</th><th>Karyawan</th><th>Komponen</th><th>Jenis</th><th class="text-end">Nominal</th><th>Keterangan</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php if(!$adjustments):?><tr><td colspan="8" class="text-center text-muted py-5">Belum ada penyesuaian periode.</td></tr><?php endif;foreach($adjustments as$a):?><tr><td><?=html_escape($a->period_name)?></td><td><b><?=html_escape($a->full_name)?></b><small class="d-block text-muted"><?=html_escape($a->employee_code)?></small></td><td><?=html_escape($a->component_name)?></td><td><span class="badge <?=$a->adjustment_type==='earning'?'bg-success':'bg-warning text-dark'?>"><?=html_escape($a->adjustment_type==='earning'?'Pendapatan':'Potongan')?></span></td><td class="text-end">Rp <?=number_format($a->amount,2,',','.')?></td><td><?=html_escape($a->notes)?></td><td><?=html_escape($statusLabels[$a->workflow_status]??$a->workflow_status)?></td><td><?php if($permissions['update']&&!empty($a->can_manage)&&$a->workflow_status==='draft'):?><form method="post" action="<?=base_url('hrpayroll/deactivate_adjustment/'.$a->id)?>" class="js-confirm"><?=$csrf?><button class="btn btn-sm btn-outline-danger">Nonaktifkan</button></form><?php else:?>-<?php endif?></td></tr><?php endforeach?></tbody></table></div></section>
|
||||
<?php endif?>
|
||||
|
||||
<div class="modal fade" id="payrollDetailModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">RINCIAN PERIODE</small><h5 class="modal-title" id="payrollDetailTitle">Payroll</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="table-responsive"><table class="table align-middle"><thead><tr><th>Karyawan</th><th class="text-end">Hadir</th><th class="text-end">Alpa</th><th class="text-end">Lembur</th><th class="text-end">Bruto</th><th class="text-end">Potongan</th><th class="text-end">Neto</th><th>Slip</th></tr></thead><tbody id="payrollDetailRows"></tbody></table></div></div></div></div></div>
|
||||
<div class="modal fade" id="salaryModal" tabindex="-1" data-bs-backdrop="static"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">STRUKTUR GAJI EFEKTIF</small><h5 class="modal-title" id="salaryModalTitle">Karyawan</h5><small id="salaryModalMeta" class="text-muted"></small></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="table-responsive mb-4"><table class="table align-middle"><thead><tr><th>Komponen</th><th>Jenis</th><th>Metode</th><th class="text-end">Nilai/Tarif</th><th>Efektif</th><th>Status</th><th>Aksi</th></tr></thead><tbody id="salaryLineRows"></tbody></table></div><?php if($permissions['update']):?><form id="salaryLineForm" data-ajax="true" class="row g-2"><?=$csrf?><input type="hidden" name="employee_id" id="salaryEmployeeId"><div class="col-md-3"><label class="form-label">Komponen</label><select class="form-select" name="component_id" required><option value="">Pilih komponen</option><?php foreach($components as$c):?><option value="<?=$c->id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Metode</label><select class="form-select" name="calculation_method"><?php foreach($methodLabels as$key=>$label):?><option value="<?=$key?>"><?=html_escape($label)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nilai/Tarif</label><input type="number" min="0" step="0.01" class="form-control" name="amount_override" value="0" required></div><div class="col-md-2"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>" required></div><div class="col-md-2"><label class="form-label">Selesai (opsional)</label><input type="date" class="form-control" name="effective_to"></div><div class="col-md-1 d-flex align-items-end"><button class="btn btn-primary w-100">Tambah</button></div><div class="col-md-6"><input class="form-control" name="formula_expression" placeholder="Formula bila metode Formula"></div><div class="col-md-6"><input class="form-control" name="notes" placeholder="Keterangan perubahan"></div></form><?php endif?></div></div></div></div>
|
||||
<div class="modal fade" id="salaryModal" tabindex="-1" data-bs-backdrop="static"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">STRUKTUR GAJI EFEKTIF</small><h5 class="modal-title" id="salaryModalTitle">Karyawan</h5><small id="salaryModalMeta" class="text-muted"></small></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="table-responsive mb-4"><table class="table align-middle"><thead><tr><th>Komponen</th><th>Jenis</th><th>Metode</th><th class="text-end">Nilai/Tarif</th><th>Efektif</th><th>Status</th><th>Aksi</th></tr></thead><tbody id="salaryLineRows"></tbody></table></div><?php if($permissions['update']):?><form id="salaryLineForm" data-ajax="true" class="row g-2"><?=$csrf?><input type="hidden" name="employee_id" id="salaryEmployeeId"><div class="col-md-3"><label class="form-label">Komponen</label><select class="form-select" name="component_id" required><option value="">Pilih komponen</option><?php foreach($manageable_components as$c):?><option value="<?=$c->id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Metode</label><select class="form-select" name="calculation_method"><?php foreach($methodLabels as$key=>$label):?><option value="<?=$key?>"><?=html_escape($label)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nilai/Tarif</label><input type="number" min="0" step="0.01" class="form-control" name="amount_override" value="0" required></div><div class="col-md-2"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>" required></div><div class="col-md-2"><label class="form-label">Selesai (opsional)</label><input type="date" class="form-control" name="effective_to"></div><div class="col-md-1 d-flex align-items-end"><button type="submit" class="btn btn-primary w-100">Tambah</button></div><div class="col-md-6"><input class="form-control" name="formula_expression" placeholder="Formula bila metode Formula"></div><div class="col-md-6"><input class="form-control" name="notes" placeholder="Keterangan perubahan"></div></form><?php endif?></div></div></div></div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
const esc=v=>$('<div>').text(v??'').html(),money=v=>'Rp '+Number(v||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2});
|
||||
$('.searchable').select2({width:'100%'});
|
||||
<?php if(!$can_manage_allowances):?>
|
||||
$('#componentsTab select[name="component_type"] option').filter(function(){return this.value!=='deduction'}).remove();
|
||||
$('#componentsTab select[name="category"] option').filter(function(){return !['unpaid_leave','loan','bpjs','tax','other'].includes(this.value)}).remove();
|
||||
$('#generalTab form').remove();
|
||||
<?php endif?>
|
||||
const adjustmentForm=$('form[action$="hrpayroll/save_adjustment"]');
|
||||
adjustmentForm.find('select[name="component_id"]').on('change',function(){const type=$(this).find(':selected').data('type');if(type)adjustmentForm.find('select[name="adjustment_type"]').val(type).trigger('change.select2')});
|
||||
$(document).ajaxSuccess(function(_event,_xhr,settings,response){if(!String(settings.url||'').includes('hrpayroll/salary_detail/'))return;const lines=response?.data?.lines||response?.lines||[];$('#salaryLineRows tr').each(function(index){if(lines[index]&&!lines[index].can_manage)$(this).find('.deactivate-salary').remove()})});
|
||||
if($('#salaryTable').length){const table=$('#salaryTable').DataTable({pageLength:25,order:[[0,'asc']],language:{search:'Cari:',lengthMenu:'Tampilkan _MENU_ data',info:'_START_–_END_ dari _TOTAL_',paginate:{previous:'Sebelumnya',next:'Berikutnya'}}});$('#salaryDepartmentFilter').on('change',function(){table.column(1).search(this.value).draw()});$('#salaryPositionFilter').on('change',function(){table.column(2).search(this.value).draw()});$('#salaryStatusFilter').on('change',function(){table.column(3).search(this.value).draw()})}
|
||||
const detailModal=bootstrap.Modal.getOrCreateInstance('#payrollDetailModal');$('.payroll-detail').on('click',function(){$.getJSON('<?=base_url('hrpayroll/period_detail/')?>'+this.dataset.id).done(r=>{if(!r.status)return AppAlert.toast(r.message,'error');$('#payrollDetailTitle').text(r.period.period_name);$('#payrollDetailRows').html((r.rows||[]).map(x=>`<tr><td><b>${esc(x.full_name)}</b><small class="d-block text-muted">${esc(x.employee_code)}${x.salary_restricted==1?' · Gaji terbatas':''}</small></td><td class="text-end">${x.total_present}</td><td class="text-end">${x.total_alpha}</td><td class="text-end">${x.total_overtime_hours}</td><td class="text-end">${money(x.gross_salary)}</td><td class="text-end">${money(x.total_deductions)}</td><td class="text-end fw-bold">${money(x.total_salary)}</td><td>${['final','posted','paid'].includes(r.period.workflow_status)?`<a class="btn btn-sm btn-outline-primary" target="_blank" href="<?=base_url('hrpayroll/slip/')?>${x.id}">Slip</a>`:'-'}</td></tr>`).join('')||'<tr><td colspan="8" class="text-center text-muted">Belum ada rincian.</td></tr>');detailModal.show()}).fail(x=>AppAlert.toast(x.responseJSON?.message||'Rincian gagal dimuat.','error'))});
|
||||
const salaryModal=bootstrap.Modal.getOrCreateInstance('#salaryModal');let currentEmployee=0;function loadSalary(id){currentEmployee=id;$('#salaryLineRows').html('<tr><td colspan="7" class="text-center py-4"><span class="spinner-border spinner-border-sm"></span> Memuat…</td></tr>');salaryModal.show();$.getJSON('<?=base_url('hrpayroll/salary_detail/')?>'+id).done(r=>{if(!r.status){salaryModal.hide();return AppAlert.toast(r.message,'error')}$('#salaryEmployeeId').val(id);$('#salaryModalTitle').text(r.data?.employee?.full_name||r.employee?.full_name||'Karyawan');const employee=r.data?.employee||r.employee,lines=r.data?.lines||r.lines||[];$('#salaryModalMeta').text((employee?.employee_code||'')+' · '+(employee?.department_name||'-')+' · '+(employee?.position_name||'-'));$('#salaryLineRows').html(lines.map(x=>`<tr><td><b>${esc(x.component_code)}</b><small class="d-block text-muted">${esc(x.component_name)}</small></td><td>${esc(x.component_type)}</td><td>${esc(x.calculation_method)}</td><td class="text-end">${money(x.amount_override)}</td><td>${esc(x.effective_from)}${x.effective_to?' s.d. '+esc(x.effective_to):''}</td><td><span class="badge ${x.is_active==1?'bg-success':'bg-secondary'}">${x.is_active==1?'Aktif':'Selesai'}</span></td><td>${x.is_active==1?`<button class="btn btn-sm btn-outline-danger deactivate-salary" data-id="${x.id}">Nonaktifkan</button>`:'-'}</td></tr>`).join('')||'<tr><td colspan="7" class="text-center text-muted py-4">Belum ada komponen gaji.</td></tr>')}).fail(x=>{salaryModal.hide();AppAlert.toast(x.responseJSON?.message||'Struktur gaji gagal dimuat.','error')})}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
)as$m):?><div class="col-6 col-xl-2"><div class="metric-card"><span class="metric-icon text-<?=$m[3]?>"><i class="bi <?=$m[2]?>"></i></span><strong><?=$m[1]?></strong><small><?=$m[0]?></small></div></div><?php endforeach?>
|
||||
</div>
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-xl-7"><section class="inventory-card h-100"><div class="inventory-card-head"><div><h5>Pekerjaan Gudang</h5><p>Barang yang sudah diterima tetapi belum siap digunakan atau dijual.</p></div><a href="<?=base_url('items/draft_items')?>" class="btn btn-sm btn-outline-primary">Buka Barang Pending</a></div>
|
||||
<div class="col-xl-6"><section class="inventory-card h-100"><div class="inventory-card-head"><div><h5>Pekerjaan Gudang</h5><p>Barang yang sudah diterima tetapi belum siap digunakan atau dijual.</p></div><a href="<?=base_url('items/draft_items')?>" class="btn btn-sm btn-outline-primary">Buka Barang Pending</a></div>
|
||||
<?php if(!$action_items):?><div class="inventory-empty"><i class="bi bi-check-circle"></i><strong>Tidak ada barang pending</strong><span>Semua penerimaan sudah ditangani.</span></div><?php else:?><div class="inventory-list"><?php foreach($action_items as$i):?><a href="<?=base_url('items/draft_items?focus='.$i->id)?>"><span class="item-avatar"><i class="bi bi-box"></i></span><span class="flex-grow-1"><strong><?=html_escape($i->kode_detail.' - '.$i->nama_barang)?></strong><small><?=number_format($i->ready_barcodes,0,',','.')?> siap dari <?=number_format($i->pending_barcodes,0,',','.')?> barcode pending</small></span><i class="bi bi-chevron-right"></i></a><?php endforeach?></div><?php endif?>
|
||||
</section></div>
|
||||
<div class="col-xl-5"><section class="inventory-card h-100" id="scanner"><div class="inventory-card-head"><div><h5>Scan Barcode / Serial</h5><p>Lihat posisi, status, saldo, dan riwayat unit.</p></div></div><div class="p-3"><div class="input-group input-group-lg"><span class="input-group-text"><i class="bi bi-upc-scan"></i></span><input id="scanCode" class="form-control" placeholder="Scan atau ketik kode"><button id="scanButton" class="btn btn-primary">Periksa</button></div><div id="scanResult" class="scan-result mt-3"><div class="inventory-empty py-4"><i class="bi bi-qr-code-scan"></i><span>Hasil scan tampil di sini.</span></div></div></div></section></div>
|
||||
<div class="col-xl-6"><section class="inventory-card h-100" id="scanner"><div class="inventory-card-head"><div><h5>Scan Barcode / Serial</h5><p>Lihat posisi, status, saldo, dan riwayat unit.</p></div></div><div class="p-3"><div class="input-group input-group-lg"><span class="input-group-text"><i class="bi bi-upc-scan"></i></span><input id="scanCode" class="form-control" placeholder="Scan atau ketik kode"><button id="scanButton" class="btn btn-primary">Periksa</button></div><div id="scanResult" class="scan-result mt-3"><div class="inventory-empty py-4"><i class="bi bi-qr-code-scan"></i><span>Hasil scan tampil di sini.</span></div></div></div></section></div>
|
||||
</div>
|
||||
<section class="inventory-card mt-3"><div class="inventory-card-head"><div><h5>Dokumen Mutasi Terbaru</h5><p>Jejak dokumen stok dari seluruh gudang.</p></div><a href="<?=base_url('inventoryprofessional/operations')?>" class="btn btn-sm btn-outline-primary">Lihat Semua</a></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Dokumen</th><th>Jenis</th><th>Tanggal</th><th>Lokasi</th><th>Status</th><th>Pembuat</th></tr></thead><tbody><?php foreach($documents as$d):?><tr><td><a href="<?=base_url('inventoryprofessional/operations?focus='.$d->id)?>" class="fw-semibold"><?=html_escape($d->document_no)?></a></td><td><?=html_escape(ucwords(str_replace('_',' ',$d->document_type)))?></td><td><?=date('d/m/Y',strtotime($d->document_date))?></td><td><?=html_escape(($d->from_warehouse?:'-').' → '.($d->to_warehouse?:'-'))?></td><td><span class="status-chip status-<?=$d->status?>"><?=html_escape(strtoupper($d->status))?></span></td><td><?=html_escape($d->creator_name?:'-')?></td></tr><?php endforeach?></tbody></table></div></section>
|
||||
<script>
|
||||
|
||||
@@ -8,7 +8,7 @@ $(function(){
|
||||
const csrf=()=>({[APP_CSRF.name]:APP_CSRF.value}),esc=v=>$('<div>').text(v==null?'-':v).html(),money=v=>'Rp '+Number(v||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2});
|
||||
const table=$('#tableItems').DataTable({responsive:true,processing:true,serverSide:true,searchDelay:350,ajax:{url:'<?=base_url('items/get_data')?>',data:d=>{d.s='draft';d.server=1}},pageLength:25,language:{search:'Cari:',processing:'Memuat data...',lengthMenu:'Tampilkan _MENU_',info:'_START_–_END_ dari _TOTAL_',paginate:{previous:'Sebelumnya',next:'Berikutnya'}},initComplete:function(){let f=new URLSearchParams(location.search).get('focus');if(f)setTimeout(()=>$('.btn-detail[data-id="'+f+'"]').first().click(),300)}});
|
||||
$(document).on('blur','.pending-sale-price',function(){let n=String(this.value).replace(/\./g,'').replace(',','.');if(Number(n)>0)this.value=Number(n).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2})});
|
||||
$(document).on('click','.btn-updatePosting',function(){let id=$(this).data('id'),input=$('.pending-sale-price[data-id="'+id+'"]'),sale=Number(String(input.val()).replace(/\./g,'').replace(',','.')),buy=Number(input.data('buy')||0);if(!sale)return AppAlert.toast('Harga jual satuan wajib diisi.','warning');if(sale<buy)return AppAlert.toast('Harga jual tidak boleh lebih rendah dari harga beli.','error');let b=$(this);AppAlert.confirm({title:'Posting barang siap digunakan?',text:'Hanya barcode yang serialnya sudah lengkap yang akan diaktifkan.',confirmButtonText:'Posting Barang'}).then(r=>{if(!r.isConfirmed)return;b.prop('disabled',true);$.post('<?=base_url('items/posting')?>',Object.assign({id:id,harga_jual:sale},csrf())).done(x=>{if(!x.status)return AppAlert.toast(x.message,'error');table.ajax.reload(null,false);AppAlert.toast(x.message,'success')}).fail(()=>AppAlert.toast('Posting barang gagal.','error')).always(()=>b.prop('disabled',false))})});
|
||||
$(document).on('click','.btn-updatePosting',function(){let id=$(this).data('id'),input=$('.pending-sale-price[data-id="'+id+'"]'),sale=Number(String(input.val()).replace(/\./g,'').replace(',','.')),buy=Number(input.data('buy')||0);if(!sale)return AppAlert.toast('Harga jual satuan wajib diisi.','warning');if(sale<buy)return AppAlert.toast('Harga jual tidak boleh lebih rendah dari harga beli.','error');let b=$(this);AppAlert.confirm({title:'Posting barang siap digunakan?',text:'Hanya barcode yang serialnya sudah lengkap yang akan diaktifkan.',confirmButtonText:'Posting Barang'}).then(r=>{if(!r.isConfirmed)return;b.prop('disabled',true);$.ajax({url:'<?=base_url('items/posting')?>',type:'POST',data:Object.assign({id:id,harga_jual:sale},csrf()),dataType:'json'}).done(x=>{let message=x&&x.message?String(x.message):'Respons posting barang tidak valid.';if(!x||!x.status)return AppAlert.toast(message,'error');table.ajax.reload(null,false);AppAlert.toast(message,'success')}).fail(xhr=>{let message=xhr.responseJSON&&xhr.responseJSON.message?xhr.responseJSON.message:'Posting barang gagal.';AppAlert.toast(String(message),'error')}).always(()=>b.prop('disabled',false))})});
|
||||
$(document).on('click','.btn-detail',function(){let id=$(this).data('id');$('#pendingDetailBody').html('<div class="inventory-loading"><span class="spinner-border spinner-border-sm"></span> Memuat detail…</div>');bootstrap.Modal.getOrCreateInstance('#pendingDetailModal').show();$.get('<?=base_url('items/detail')?>/'+id).done(r=>{let rows=(r.logs||[]).map(x=>'<tr><td>'+esc(x.created_at)+'</td><td>'+esc(x.gudang)+'</td><td>'+esc(x.tipe)+'</td><td class="text-end">'+Number(x.qty||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</td><td>'+esc(x.keterangan)+'</td></tr>').join('');$('#pendingDetailBody').html('<div class="document-meta"><span>Kode<strong>'+esc(r.item.kode_detail)+'</strong></span><span>Barang<strong>'+esc(r.item.nama_barang)+'</strong></span><span>Harga Beli<strong>'+money(r.item.harga_beli)+'</strong></span><span>Harga Jual<strong>'+money(r.item.harga_jual)+'</strong></span><span>Stok Diterima<strong>'+Number(r.item.stok||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</strong></span><span>Status<strong><span class="status-chip status-submitted">PENDING</span></strong></span></div><div class="table-responsive mt-3"><table class="table inventory-table"><thead><tr><th>Tanggal</th><th>Gudang</th><th>Arah</th><th class="text-end">Qty</th><th>Keterangan</th></tr></thead><tbody>'+rows+'</tbody></table></div><button type="button" class="btn btn-sm btn-outline-danger stock-pdf" data-id="'+id+'"><i class="bi bi-file-earmark-pdf"></i> PDF Audit</button>')})});
|
||||
$(document).on('click','.stock-pdf',function(){$('#barcodePrintModal .modal-title').text('PDF Kartu Stok');$('#barcodePrintModal iframe').attr('src','<?=base_url('items/stock_log_pdf')?>/'+$(this).data('id'));bootstrap.Modal.getOrCreateInstance(document.getElementById('barcodePrintModal')).show()});
|
||||
$(document).on('click','.btn-print',function(){let id=$(this).data('id');AppAlert.prompt({title:'Preview Barcode',html:'<label class="form-label d-block text-start">Jumlah kolom</label><select id="printColumn" class="form-select mb-3"><option value="2">2</option><option value="3" selected>3</option><option value="4">4</option></select><label class="form-label d-block text-start">Salinan</label><input id="printCopies" type="number" min="1" max="20" value="1" class="form-control">',preConfirm:()=>({column:$('#printColumn').val(),duplicat:$('#printCopies').val()}),confirmButtonText:'Preview'}).then(r=>{if(!r.isConfirmed)return;$('#barcodePrintModal .modal-title').text('Preview Barcode');$('#barcodePrintModal iframe').attr('src','<?=base_url('items/print_barcode')?>/'+id+'?column='+r.value.column+'&duplicat='+r.value.duplicat+'&status=all');bootstrap.Modal.getOrCreateInstance(document.getElementById('barcodePrintModal')).show()})});
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
<h5>Jurnal Umum</h5>
|
||||
<button class="btn btn-warning btn-add">Tambah Jurnal</button>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<label for="journalStatusFilter" class="form-label">Status</label>
|
||||
<select id="journalStatusFilter" class="form-select">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="submitted">Diajukan</option>
|
||||
<option value="approved">Disetujui</option>
|
||||
<option value="posted">Terposting</option>
|
||||
<option value="rejected">Ditolak</option>
|
||||
<option value="reversed">Reversal</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--<form id="filterForm" class="row mb-3">-->
|
||||
|
||||
@@ -63,7 +78,7 @@
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header bg-warning text-white">
|
||||
<h5>Tambah Jurnal</h5>
|
||||
<h5 id="journalModalTitle">Tambah Jurnal</h5>
|
||||
<button class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
@@ -140,7 +155,8 @@
|
||||
<strong>No Ref:</strong> <span id="d_noref"></span><br>
|
||||
<strong>Keterangan:</strong> <span id="d_keterangan"></span><br>
|
||||
<strong>Dibuat oleh:</strong> <span id="d_createdby"></span><br>
|
||||
<strong>Status:</strong> <span id="d_status"></span>
|
||||
<strong>Status:</strong> <span id="d_status"></span>
|
||||
<div id="d_rejection" class="alert alert-danger mt-3 d-none"></div>
|
||||
</div>
|
||||
|
||||
<table class="table table-bordered">
|
||||
@@ -183,6 +199,7 @@ $(function(){
|
||||
|
||||
let accounts = [];
|
||||
let mode = 'manual';
|
||||
let editJournalId = 0;
|
||||
|
||||
// ================= LOAD ACCOUNTS
|
||||
function loadAccounts(){
|
||||
@@ -374,6 +391,27 @@ $('.btn-add').click(async function(){
|
||||
$('#formTotalKredit').text('0');
|
||||
|
||||
$('#modeManual').click(); // default jurnal manual dengan dua akun
|
||||
editJournalId = 0;
|
||||
$('#journalModalTitle').text('Tambah Jurnal');
|
||||
$('#btnSimpan').text('Simpan');
|
||||
});
|
||||
|
||||
$(document).on('click','.btn-edit-rejected',async function(){
|
||||
const id=$(this).data('id');
|
||||
await loadAccounts();
|
||||
await loadBaseJurnal();
|
||||
$('#modeManual').click();
|
||||
$.get("<?= base_url('jurnal/detail/'); ?>"+id,function(res){
|
||||
editJournalId=id;
|
||||
$('#journalModalTitle').text('Koreksi Jurnal Ditolak');
|
||||
$('#btnSimpan').text('Simpan Koreksi');
|
||||
$('#tanggal').val(res.header.tanggal);
|
||||
$('#keterangan').val(res.header.keterangan);
|
||||
$('#tableDetail tbody').empty();
|
||||
(res.detail||[]).forEach(function(row){addRow({account_id:row.account_id,debit:Number(row.debit)>0?row.debit:'',kredit:Number(row.kredit)>0?row.kredit:''});});
|
||||
hitungTotalForm();
|
||||
$('#modalJurnal').modal('show');
|
||||
},'json').fail(function(xhr){Swal.fire('Error',xhr.responseJSON?.message||'Jurnal tidak dapat dimuat.','error');});
|
||||
});
|
||||
|
||||
$('#btnSimpan').click(function(){
|
||||
@@ -426,7 +464,10 @@ $('#btnSimpan').click(function(){
|
||||
return;
|
||||
}
|
||||
// lanjut simpan
|
||||
$.post("<?= base_url('jurnal/save'); ?>", {
|
||||
const saveUrl=editJournalId
|
||||
? "<?= base_url('jurnal/update_rejected/'); ?>"+editJournalId
|
||||
: "<?= base_url('jurnal/save'); ?>";
|
||||
$.post(saveUrl, {
|
||||
tanggal: $('#tanggal').val(),
|
||||
keterangan: $('#keterangan').val(),
|
||||
account_id: account_id,
|
||||
@@ -436,6 +477,7 @@ $('#btnSimpan').click(function(){
|
||||
|
||||
if(res.status){
|
||||
$('#modalJurnal').modal('hide');
|
||||
editJournalId=0;
|
||||
$('#tableJurnal').DataTable().ajax.reload(null,false);
|
||||
Swal.fire('Sukses',res.message,'success');
|
||||
} else {
|
||||
@@ -454,7 +496,10 @@ let table = $('#tableJurnal').DataTable({
|
||||
order:[[1,'desc']],
|
||||
ajax:{
|
||||
url:"<?= base_url('jurnal/get_data'); ?>",
|
||||
type:"POST"
|
||||
type:"POST",
|
||||
data:function(d){
|
||||
d.status = $('#journalStatusFilter').val();
|
||||
}
|
||||
},
|
||||
columnDefs:[
|
||||
{ targets:[4,5], className:'text-end' }
|
||||
@@ -476,6 +521,10 @@ let table = $('#tableJurnal').DataTable({
|
||||
}
|
||||
});
|
||||
|
||||
$('#journalStatusFilter').on('change', function(){
|
||||
table.ajax.reload(null, true);
|
||||
});
|
||||
|
||||
// DELETE
|
||||
$(document).on('click','.btn-delete',function(){
|
||||
let id = $(this).data('id');
|
||||
@@ -520,10 +569,7 @@ let table = $('#tableJurnal').DataTable({
|
||||
});
|
||||
|
||||
// DETAIL
|
||||
$(document).on('click','.btn-detail',function(){
|
||||
|
||||
let id = $(this).data('id');
|
||||
|
||||
function openJournalDetail(id){
|
||||
$.get("<?= base_url('jurnal/detail/'); ?>" + id, function(res){
|
||||
|
||||
$('#d_tanggal').text(res.header.tanggal);
|
||||
@@ -531,6 +577,7 @@ $(document).on('click','.btn-detail',function(){
|
||||
$('#d_keterangan').text(res.header.keterangan);
|
||||
$('#d_createdby').text(res.user ? res.user.nama : '-');
|
||||
$('#d_status').text(String(res.header.status || '').toUpperCase());
|
||||
if(res.header.rejection_reason){$('#d_rejection').removeClass('d-none').html('<strong>Alasan penolakan sebelumnya</strong><br>'+String(res.header.rejection_reason).replace(/[&<>'"]/g,function(c){return{'&':'&','<':'<','>':'>',"'":''','"':'"'}[c];}));}else{$('#d_rejection').addClass('d-none').empty();}
|
||||
let history=(res.history||[]).map(h=>`${h.from_status||'-'} → ${h.to_status} oleh ${h.changed_by_name||'-'} (${h.changed_at})${h.notes?' — '+h.notes:''}`).join('<br>');
|
||||
$('#statusHistory').html(history || 'Belum ada perubahan status.');
|
||||
|
||||
@@ -558,9 +605,11 @@ $(document).on('click','.btn-detail',function(){
|
||||
|
||||
$('#modalDetail').modal('show');
|
||||
|
||||
},'json');
|
||||
},'json').fail(function(xhr){Swal.fire('Error',xhr.responseJSON?.message||'Detail jurnal tidak dapat dimuat.','error');});
|
||||
}
|
||||
|
||||
});
|
||||
$(document).on('click','.btn-detail',function(){openJournalDetail($(this).data('id'));});
|
||||
$(function(){const focus=Number(new URLSearchParams(window.location.search).get('focus')||0);if(focus>0)openJournalDetail(focus);});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@@ -10,6 +10,7 @@ $CI->config->load('ui_assets',true);
|
||||
$uiAssets=$CI->config->item('ui_assets');
|
||||
$dataPages=isset($uiAssets['ui_data_pages'])?$uiAssets['ui_data_pages']:array();
|
||||
$selectPages=isset($uiAssets['ui_select_pages'])?$uiAssets['ui_select_pages']:array();
|
||||
$loadDataTables=!empty($use_datatables)||in_array($active_menu,$dataPages,true);
|
||||
$userName=$this->session->userdata('nama')?:$this->session->userdata('username')?:'Pengguna';
|
||||
$userRole=$this->session->userdata('role')?:'User';
|
||||
$userPhoto=$this->session->userdata('profile_photo');
|
||||
@@ -19,11 +20,11 @@ $userPhoto=$this->session->userdata('profile_photo');
|
||||
<title><?=html_escape($page_title)?> · Accounting</title>
|
||||
<link rel="manifest" href="<?=base_url('manifest.webmanifest')?>"><link rel="icon" href="<?=base_url('assets/img/pwa-192.png')?>"><link rel="apple-touch-icon" href="<?=base_url('assets/img/pwa-192.png')?>">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" onerror="this.onerror=null;this.href='<?=base_url('assets/vendor/bootstrap.min.css')?>'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||
<?php if(in_array($active_menu,$dataPages,true)):?><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/css/dataTables.bootstrap5.min.css"><?php endif?>
|
||||
<?php if($loadDataTables):?><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/css/dataTables.bootstrap5.min.css"><?php endif?>
|
||||
<?php if(in_array($active_menu,$selectPages,true)):?><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css"><?php endif?>
|
||||
<link rel="stylesheet" href="<?=base_url('assets/css/style-custom.css').'?v='.filemtime(FCPATH.'assets/css/style-custom.css')?>"><link rel="stylesheet" href="<?=base_url('assets/css/app-final.css').'?v='.filemtime(FCPATH.'assets/css/app-final.css')?>"><?php if($active_menu==='profile'):?><link rel="stylesheet" href="<?=base_url('assets/css/profile.css').'?v='.filemtime(FCPATH.'assets/css/profile.css')?>"><?php endif?><?php if($active_menu==='leave_requests'):?><link rel="stylesheet" href="<?=base_url('assets/css/leave-requests.css').'?v='.filemtime(FCPATH.'assets/css/leave-requests.css')?>"><?php endif?><?php if(in_array($active_menu,array('organization_master','employees','generate_schedule','attendance_monitoring','attendance_monthly','shift','holidays','leave_requests','hr_payroll'),true)):?><link rel="stylesheet" href="<?=base_url('assets/css/hr-payroll.css').'?v='.filemtime(FCPATH.'assets/css/hr-payroll.css')?>"><?php endif?>
|
||||
<script src="<?=base_url('assets/vendor/jquery.min.js')?>"></script>
|
||||
<?php if(in_array($active_menu,$dataPages,true)):?><script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.8/js/jquery.dataTables.min.js"></script><script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/js/dataTables.bootstrap5.min.js"></script><script src="https://cdn.jsdelivr.net/npm/datatables.net-responsive@2.5.0/js/dataTables.responsive.min.js"></script><?php endif?>
|
||||
<?php if($loadDataTables):?><script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.8/js/jquery.dataTables.min.js"></script><script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/js/dataTables.bootstrap5.min.js"></script><script src="https://cdn.jsdelivr.net/npm/datatables.net-responsive@2.5.0/js/dataTables.responsive.min.js"></script><?php endif?>
|
||||
<?php if(in_array($active_menu,$selectPages,true)):?><script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.min.js"></script><?php endif?>
|
||||
</head><body class="app-v2" data-page="<?=html_escape($active_menu)?>">
|
||||
<a class="visually-hidden-focusable skip-link" href="#mainContent">Lewati ke konten</a>
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
$inventory_title='Log Peralatan Teknisi';
|
||||
$inventory_subtitle='Telusuri barang yang dibawa, dipasang pada customer, atau dikembalikan oleh teknisi.';
|
||||
$perm=$equipment_permissions??array();
|
||||
$active_panel='history';
|
||||
$active_panel=$active_panel??'history';
|
||||
$initialFilters=$initial_filters??array();
|
||||
$initialStatus=(string)($initialFilters['status']??'');
|
||||
$initialCondition=(string)($initialFilters['condition']??'');
|
||||
$this->load->view('inventory_professional/_module_nav',get_defined_vars());
|
||||
?>
|
||||
<link rel="stylesheet" href="<?=base_url('assets/css/technician-equipment.css')?>?v=<?=filemtime(FCPATH.'assets/css/technician-equipment.css')?>">
|
||||
<div class="tech-shell">
|
||||
<?php $this->load->view('peralatan_teknisi/_module_nav',get_defined_vars()); ?>
|
||||
<section class="inventory-card">
|
||||
<div class="inventory-card-head"><div><h5>Filter Riwayat Peralatan</h5><p>Tanggal mencakup tanggal dibawa, dipasang, atau dikembalikan.</p></div><a href="<?=base_url('teknisi')?>" class="btn btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i>Kembali</a></div>
|
||||
@@ -14,14 +18,15 @@ $this->load->view('inventory_professional/_module_nav',get_defined_vars());
|
||||
<div class="col-sm-6 col-xl-2"><label class="form-label">Sampai Tanggal</label><input type="date" class="form-control" name="date_to"></div>
|
||||
<div class="col-md-6 col-xl-2"><label class="form-label">Customer</label><select class="form-select select-search" name="customer_id"><option value="">Semua customer</option><?php foreach($customers as$customer):?><option value="<?=$customer->id?>"><?=html_escape($customer->nama)?></option><?php endforeach?></select></div>
|
||||
<div class="col-md-6 col-xl-2"><label class="form-label">Karyawan / Teknisi</label><select class="form-select select-search" name="user_id"><option value="">Semua teknisi</option><?php foreach($teknisi_list as$employee):?><option value="<?=$employee->id?>"><?=html_escape($employee->employee_code.' - '.$employee->full_name)?></option><?php endforeach?></select></div>
|
||||
<div class="col-md-4 col-xl-2"><label class="form-label">Status</label><select class="form-select" name="status"><option value="">Semua</option><option value="carried">Dibawa</option><option value="installed">Dipasang</option><option value="removed">Dilepas</option><option value="returned">Dikembalikan</option></select></div>
|
||||
<div class="col-md-4 col-xl-2"><label class="form-label">Kondisi</label><select class="form-select" name="condition"><option value="">Semua</option><option value="good">Baik</option><option value="inspection_required">Perlu Diperiksa</option><option value="repair_required">Perlu Perbaikan</option><option value="under_repair">Dalam Perbaikan</option><option value="damaged">Rusak</option><option value="lost">Hilang</option><option value="scrapped">Dihapuskan</option></select></div>
|
||||
<div class="col-md-4 col-xl-2"><label class="form-label">Status</label><select class="form-select" name="status"><option value="">Semua</option><option value="carried" <?=$initialStatus==='carried'?'selected':''?>>Dibawa</option><option value="installed" <?=$initialStatus==='installed'?'selected':''?>>Dipasang</option><option value="removed" <?=$initialStatus==='removed'?'selected':''?>>Dilepas</option><option value="returned" <?=$initialStatus==='returned'?'selected':''?>>Dikembalikan</option></select></div>
|
||||
<div class="col-md-4 col-xl-2"><label class="form-label">Kondisi</label><select class="form-select" name="condition"><option value="">Semua</option><option value="good" <?=$initialCondition==='good'?'selected':''?>>Baik</option><option value="inspection_required" <?=$initialCondition==='inspection_required'?'selected':''?>>Perlu Diperiksa</option><option value="repair_required" <?=$initialCondition==='repair_required'?'selected':''?>>Perlu Perbaikan</option><option value="under_repair" <?=$initialCondition==='under_repair'?'selected':''?>>Dalam Perbaikan</option><option value="damaged" <?=$initialCondition==='damaged'?'selected':''?>>Rusak</option><option value="lost" <?=$initialCondition==='lost'?'selected':''?>>Hilang</option><option value="scrapped" <?=$initialCondition==='scrapped'?'selected':''?>>Dihapuskan</option></select></div>
|
||||
<div class="col-md-4 col-xl-2"><label class="form-label">Kepemilikan</label><select class="form-select" name="ownership"><option value="">Semua</option><option value="company">Perusahaan</option><option value="customer">Customer</option><option value="transferred_non_inventory">Non-Persediaan Diserahkan</option><option value="unknown">Belum Diketahui</option></select></div>
|
||||
<div class="col-md-8 col-xl-4"><label class="form-label">Barang / Barcode / Serial</label><input class="form-control" name="query" placeholder="Cari identitas barang"></div>
|
||||
<div class="col-md-4 col-xl-2 d-flex gap-2"><button type="submit" class="btn btn-primary flex-grow-1"><i class="bi bi-funnel me-1"></i>Tampilkan</button><?php if($can_export):?><button type="button" id="technicianLogPdf" class="btn btn-outline-danger" title="Export PDF"><i class="bi bi-file-earmark-pdf"></i></button><?php endif?></div>
|
||||
</form>
|
||||
<div class="table-responsive"><table class="table align-middle mb-0 w-100" id="tblLogBarang"><thead><tr><th>No</th><th>Dokumen</th><th>Teknisi</th><th>Barang</th><th>Barcode / SN</th><th>Pemilik</th><th>Qty</th><th>Customer</th><th>Posisi</th><th>Status</th><th>Kondisi</th><th>Keterangan</th><th>Aksi</th></tr></thead><tbody></tbody></table></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="modal fade" id="technicianLogDetailModal" data-bs-backdrop="static" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Detail Dokumen Peralatan</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"></div><div class="modal-footer"><button class="btn btn-light" data-bs-dismiss="modal">Tutup</button></div></div></div></div>
|
||||
<?php if($can_export):?><div class="modal fade" id="technicianLogPdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content" style="height:min(90vh,900px)"><div class="modal-header"><h5 class="modal-title">PDF Log Peralatan Teknisi</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="PDF Log Peralatan Teknisi" style="width:100%;height:100%;border:0"></iframe></div></div></div></div><?php endif?>
|
||||
<script>
|
||||
|
||||
@@ -10,8 +10,8 @@ function tech_qty($value){$n=(float)$value;return abs($n-round($n))<.0001?number
|
||||
|
||||
<?php if($panel==='overview'):?>
|
||||
<div class="tech-summary">
|
||||
<?php foreach(array('carried'=>array('Dibawa Teknisi','bi-person-bag'),'installed'=>array('Terpasang','bi-house-check'),'inspection'=>array('Menunggu Periksa','bi-clipboard-pulse'),'repair'=>array('Dalam Perbaikan','bi-wrench-adjustable'),'damaged'=>array('Rusak','bi-exclamation-octagon'),'lost'=>array('Hilang','bi-question-diamond')) as$key=>$meta):?>
|
||||
<div class="tech-stat"><small><i class="bi <?=$meta[1]?>"></i> <?=$meta[0]?></small><strong data-summary="<?=$key?>"><?=tech_qty($dash['summary'][$key]??0)?></strong></div>
|
||||
<?php foreach(array('carried'=>array('Dibawa Teknisi','bi-person-bag','status','carried'),'installed'=>array('Terpasang','bi-house-check','status','installed'),'inspection'=>array('Menunggu Periksa','bi-clipboard-pulse','condition','inspection_required'),'repair'=>array('Dalam Perbaikan','bi-wrench-adjustable','condition','under_repair'),'damaged'=>array('Rusak','bi-exclamation-octagon','condition','damaged'),'lost'=>array('Hilang','bi-question-diamond','condition','lost')) as$key=>$meta):?>
|
||||
<a class="tech-stat tech-stat-<?=$key?>" href="<?=base_url('teknisi/log_barang').'?'.http_build_query(array($meta[2]=>$meta[3]))?>" title="Lihat riwayat <?=html_escape(strtolower($meta[0]))?>"><small><i class="bi <?=$meta[1]?>"></i> <?=$meta[0]?></small><strong data-summary="<?=$key?>"><?=tech_qty($dash['summary'][$key]??0)?></strong><span class="tech-stat-link"><i class="bi bi-arrow-right"></i></span></a>
|
||||
<?php endforeach?>
|
||||
</div>
|
||||
<section class="tech-card" id="scanner">
|
||||
|
||||
@@ -33,9 +33,22 @@ $accountCode=function($value){return preg_replace('/(?:[\.,]0+)$/','',(string)$v
|
||||
<div class="col-lg-6"><div class="card h-100"><div class="card-header bg-danger text-white"><b>BEBAN</b></div><div class="card-body p-0"><table class="table mb-0"><tbody><?php foreach($expenses as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' · '.$r['nama_akun'])?></td><td class="text-end"><?=$money($r['amount'])?></td></tr><?php endforeach?></tbody><tfoot><tr class="table-danger"><th>TOTAL BEBAN</th><th class="text-end">Rp <?=$money($totalExpense)?></th></tr></tfoot></table></div></div></div>
|
||||
</div>
|
||||
<div class="card mt-3"><div class="card-body d-flex justify-content-between align-items-center"><h5 class="mb-0"><?=$netProfit>=0?'LABA BERSIH':'RUGI BERSIH'?></h5><h4 class="mb-0 <?=$netProfit>=0?'text-success':'text-danger'?>">Rp <?=$money(abs($netProfit))?></h4></div></div>
|
||||
<?php elseif($report==='cash_flow'): ?>
|
||||
<div class="row g-2 mb-3">
|
||||
<?php foreach(array('opening_balance'=>'Saldo Awal Kas','operating'=>'Aktivitas Operasi','investing'=>'Aktivitas Investasi','financing'=>'Aktivitas Pendanaan','increase_decrease'=>'Kenaikan / Penurunan Kas','closing_balance'=>'Saldo Akhir Kas')as$key=>$label):?>
|
||||
<div class="col-md-4 col-xl-2"><div class="card h-100"><div class="card-body"><small class="text-muted"><?=$label?></small><strong class="d-block mt-1 <?=((float)($report_summary[$key]??0))<0?'text-danger':''?>">Rp <?=$money($report_summary[$key]??0)?></strong></div></div></div>
|
||||
<?php endforeach?>
|
||||
</div>
|
||||
<div class="card"><div class="card-header"><b>Detail Transaksi Pembentuk Arus Kas</b><small class="d-block text-muted">Saldo berjalan mencakup seluruh akun kas/bank yang dipilih.</small></div><div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Tanggal</th><th>Referensi</th><th>Keterangan</th><th>Akun</th><th>Aktivitas</th><th class="text-end">Kas Masuk</th><th class="text-end">Kas Keluar</th><th class="text-end">Saldo</th></tr></thead><tbody><?php foreach($rows as$r):?><tr><td><?=html_escape($r['tanggal'])?></td><td><a href="<?=base_url('jurnal?focus='.$r['journal_id'])?>"><?=html_escape($r['no_ref']?:'#'.$r['journal_id'])?></a></td><td><?=html_escape($r['keterangan'])?></td><td><?=html_escape($r['akun'])?></td><td><?=html_escape($r['aktivitas'])?></td><td class="text-end text-success"><?=$money($r['kas_masuk'])?></td><td class="text-end text-danger"><?=$money($r['kas_keluar'])?></td><td class="text-end fw-semibold"><?=$money($r['saldo_berjalan'])?></td></tr><?php endforeach?><?php if(!$rows):?><tr><td colspan="8" class="text-center text-muted py-4">Tidak ada transaksi kas pada periode ini.</td></tr><?php endif?></tbody><tfoot><tr><th colspan="5">Total Mutasi</th><th class="text-end"><?=$money($report_summary['cash_in']??0)?></th><th class="text-end"><?=$money($report_summary['cash_out']??0)?></th><th class="text-end"><?=$money($report_summary['closing_balance']??0)?></th></tr></tfoot></table></div></div>
|
||||
<?php elseif($report==='general_ledger'): ?>
|
||||
<div class="card mb-3"><div class="card-header"><b>Ringkasan Saldo per Akun</b><small class="d-block text-muted">Saldo awal berasal dari seluruh jurnal sebelum periode, bukan dari pagination tampilan.</small></div><div class="table-responsive"><table class="table table-sm mb-0"><thead><tr><th>Akun</th><th class="text-end">Saldo Awal</th><th class="text-end">Total Debit</th><th class="text-end">Total Kredit</th><th class="text-end">Saldo Akhir</th></tr></thead><tbody><?php foreach($report_summary as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' - '.$r['nama_akun'])?></td><td class="text-end"><?=$money($r['saldo_awal'])?></td><td class="text-end"><?=$money($r['total_debit'])?></td><td class="text-end"><?=$money($r['total_kredit'])?></td><td class="text-end fw-bold"><?=$money($r['saldo_akhir'])?></td></tr><?php endforeach?><?php if(!$report_summary):?><tr><td colspan="5" class="text-center text-muted py-4">Tidak ada saldo atau mutasi akun.</td></tr><?php endif?></tbody></table></div></div>
|
||||
<div class="card"><div class="card-header"><b>Rincian Buku Besar</b></div><div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Akun</th><th>Tanggal</th><th>Referensi</th><th>Keterangan</th><th class="text-end">Debit</th><th class="text-end">Kredit</th><th class="text-end">Saldo Berjalan</th></tr></thead><tbody><?php foreach($rows as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' - '.$r['nama_akun'])?></td><td><?=html_escape($r['tanggal'])?></td><td><a href="<?=base_url('jurnal?focus='.$r['journal_id'])?>"><?=html_escape($r['no_ref'])?></a></td><td><?=html_escape($r['keterangan'])?></td><td class="text-end"><?=$money($r['debit'])?></td><td class="text-end"><?=$money($r['kredit'])?></td><td class="text-end fw-semibold"><?=$money($r['saldo_berjalan'])?></td></tr><?php endforeach?><?php if(!$rows):?><tr><td colspan="7" class="text-center text-muted py-4">Tidak ada transaksi pada periode ini.</td></tr><?php endif?></tbody></table></div></div>
|
||||
<?php elseif($report==='receivable_reconciliation'): ?>
|
||||
<div class="row g-2 mb-3"><div class="col-md-4"><div class="card h-100"><div class="card-body"><small class="text-muted">Subledger Invoice</small><strong class="d-block">Rp <?=$money($report_summary['subledger']??0)?></strong></div></div></div><div class="col-md-4"><div class="card h-100"><div class="card-body"><small class="text-muted">Saldo Buku Besar Piutang</small><strong class="d-block">Rp <?=$money($report_summary['general_ledger']??0)?></strong></div></div></div><div class="col-md-4"><div class="card h-100 border-<?=$report_summary['balanced']?'success':'warning'?>"><div class="card-body"><small class="text-muted">Selisih Belum Terpetakan</small><strong class="d-block <?=$report_summary['balanced']?'text-success':'text-warning'?>">Rp <?=$money($report_summary['difference']??0)?></strong></div></div></div></div>
|
||||
<div class="card"><div class="card-header"><b>Piutang per Invoice sampai <?=html_escape($filters['to'])?></b><small class="d-block text-muted">Pembayaran, credit note, dan write-off ditelusuri sampai tanggal laporan. Selisih data lama tetap ditampilkan untuk kebutuhan koreksi.</small></div><div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Customer</th><th>Invoice</th><th>Tanggal</th><th>Jatuh Tempo</th><th class="text-end">Nilai Piutang</th><th class="text-end">Pembayaran</th><th class="text-end">Credit Note</th><th class="text-end">Write-off</th><th class="text-end">Sisa</th><th>Status</th><th>Referensi</th></tr></thead><tbody><?php foreach($rows as$r):?><tr class="<?=!empty($r['reconciliation_adjustment'])?'table-warning':''?>"><td><?=html_escape($r['customer'])?></td><td><?php if(!empty($r['record_id'])):?><a href="<?=base_url('invoices/detail/'.$r['record_id'])?>"><?=html_escape($r['no_invoice'])?></a><?php else:?><?=html_escape($r['no_invoice'])?><?php endif?></td><td><?=html_escape($r['invoice_date']?:'-')?></td><td><?=html_escape($r['due_date']?:'-')?></td><td class="text-end"><?=$money($r['receivable_amount'])?></td><td class="text-end"><?=$money($r['payments'])?></td><td class="text-end"><?=$money($r['credit_notes'])?></td><td class="text-end"><?=$money($r['writeoffs'])?></td><td class="text-end fw-bold"><?=$money($r['remaining_receivable'])?></td><td><?=html_escape($r['status'])?></td><td><?=html_escape($r['transaction_references']?:'-')?></td></tr><?php endforeach?></tbody><tfoot><tr><th colspan="8">Total setelah rekonsiliasi</th><th class="text-end">Rp <?=$money($report_summary['general_ledger']??0)?></th><th colspan="2"></th></tr></tfoot></table></div></div>
|
||||
<?php else: ?>
|
||||
<?php $visibleColumns=$rows?array_values(array_filter(array_keys($rows[0]),function($key){return$key!=='record_id';})):array();$documentTypes=array('return_no'=>'purchase_return','refund_no'=>'supplier_refund','debit_note_no'=>'supplier_debit_note'); ?>
|
||||
<div class="card"><div class="table-responsive"><table class="table table-sm mb-0"><thead><?php if($rows):?><tr><?php foreach($visibleColumns as$h):?><th><?=html_escape(ucwords(str_replace('_',' ',$h)))?></th><?php endforeach?></tr><?php endif?></thead><tbody><?php foreach($rows as$r):?><tr><?php foreach($visibleColumns as$k):$v=$r[$k];?><td class="<?=is_numeric($v)&&!preg_match('/(^id$|_id$|kode_akun$)/',$k)?'text-end':''?>"><?php if($k==='journal_id'&&$v):?><a href="<?=base_url('jurnal/detail/'.$v)?>">#<?=$v?></a><?php elseif(isset($documentTypes[$k])&&!empty($r['record_id'])):?><a href="<?=base_url('purchases/returns?action=history&focus_type='.$documentTypes[$k].'&focus='.$r['record_id'])?>"><?=html_escape($v)?></a><?php elseif($k==='kode_akun'):?><?=html_escape($accountCode($v))?><?php elseif(is_numeric($v)&&!preg_match('/(^id$|_id$|kode_akun$)/',$k)):?><?=$money($v)?><?php else:?><?=html_escape($v===null?'-':$v)?><?php endif?></td><?php endforeach?></tr><?php endforeach?></tbody></table></div></div>
|
||||
<div class="card"><div class="table-responsive"><table class="table table-sm mb-0"><thead><?php if($rows):?><tr><?php foreach($visibleColumns as$h):?><th><?=html_escape(ucwords(str_replace('_',' ',$h)))?></th><?php endforeach?></tr><?php endif?></thead><tbody><?php foreach($rows as$r):?><tr><?php foreach($visibleColumns as$k):$v=$r[$k];?><td class="<?=is_numeric($v)&&!preg_match('/(^id$|_id$|kode_akun$)/',$k)?'text-end':''?>"><?php if($k==='journal_id'&&$v):?><a href="<?=base_url('jurnal?focus='.$v)?>">#<?=$v?></a><?php elseif(isset($documentTypes[$k])&&!empty($r['record_id'])):?><a href="<?=base_url('purchases/returns?action=history&focus_type='.$documentTypes[$k].'&focus='.$r['record_id'])?>"><?=html_escape($v)?></a><?php elseif($k==='kode_akun'):?><?=html_escape($accountCode($v))?><?php elseif(is_numeric($v)&&!preg_match('/(^id$|_id$|kode_akun$)/',$k)):?><?=$money($v)?><?php else:?><?=html_escape($v===null?'-':$v)?><?php endif?></td><?php endforeach?></tr><?php endforeach?></tbody></table></div></div>
|
||||
<?php endif; ?>
|
||||
<div class="mt-3"><b>Subledger:</b> <a href="<?=base_url('receivables')?>">Aging Piutang</a> · <a href="<?=base_url('payables')?>">Aging Hutang</a> · <a href="<?=base_url('inventoryprofessional')?>">Persediaan</a> · <a href="<?=base_url('fixedassets')?>">Aset</a> · <a href="<?=base_url('hrpayroll')?>">Payroll</a> · <a href="<?=base_url('purchases/returns')?>">Retur & Refund</a></div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<?php foreach($roles as$r):?><button type="button" class="list-group-item list-group-item-action role-item" data-id="<?=$r->id?>"><div class="d-flex justify-content-between"><b><?=html_escape($r->nama_role)?></b><span><?php if($r->is_super_admin):?><span class="badge bg-danger">Master Admin</span><?php elseif($r->is_system):?><span class="badge bg-secondary">Bawaan</span><?php else:?><span class="badge bg-info text-dark">Custom</span><?php endif?></span></div><small class="text-muted"><?=html_escape($r->description?:'Belum ada uraian tanggung jawab')?></small></button><?php endforeach?>
|
||||
</div></div></div>
|
||||
<div class="col-lg-8"><div class="card"><div class="card-header" id="formTitle">Pilih role untuk melihat permission</div><div class="card-body"><form id="roleForm"><input type="hidden" name="id" id="roleId"><div class="row g-2 mb-3"><div class="col-md-4"><label class="form-label">Nama Role</label><input class="form-control" name="name" id="roleName" required></div><div class="col-md-6"><label class="form-label">Tanggung Jawab Role</label><input class="form-control" name="description" id="roleDescription" required placeholder="Jelaskan pekerjaan yang ditangani"></div><div class="col-md-2 d-flex align-items-end"><label class="form-check mb-2"><input class="form-check-input" type="checkbox" name="is_active" id="roleActive" value="1"> Aktif</label></div></div>
|
||||
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Modul</th><?php foreach($actions as$label):?><th class="text-center"><?=$label?></th><?php endforeach?></tr></thead><tbody><?php foreach($features as$key=>$label):?><tr><td><b><?=$label?></b><?php if($key==='payroll'):?><div class="form-check mt-2"><input class="form-check-input permission-box" type="checkbox" name="permissions[payroll][]" value="can_manage_all_salary" id="manageAllSalary" data-feature="payroll" data-action="can_manage_all_salary"><label class="form-check-label small text-danger" for="manageAllSalary">Manage All Gaji (termasuk gaji terbatas)</label></div><?php endif?></td><?php foreach($actions as$action=>$actionLabel):?><td class="text-center"><input class="form-check-input permission-box" type="checkbox" name="permissions[<?=$key?>][]" value="<?=$action?>" data-feature="<?=$key?>" data-action="<?=$action?>"></td><?php endforeach?></tr><?php endforeach?></tbody></table></div>
|
||||
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Modul</th><?php foreach($actions as$label):?><th class="text-center"><?=$label?></th><?php endforeach?></tr></thead><tbody><?php foreach($features as$key=>$label):?><tr><td><b><?=$label?></b><?php if($key==='payroll'):?><div class="form-check mt-2"><input class="form-check-input permission-box" type="checkbox" name="permissions[payroll][]" value="can_manage_all_salary" id="manageAllSalary" data-feature="payroll" data-action="can_manage_all_salary"><label class="form-check-label small text-danger" for="manageAllSalary">Manage All Gaji (termasuk gaji terbatas)</label></div><div class="form-check mt-2"><input class="form-check-input permission-box" type="checkbox" name="permissions[payroll][]" value="can_manage_allowances" id="managePayrollAllowances" data-feature="payroll" data-action="can_manage_allowances"><label class="form-check-label small text-warning-emphasis" for="managePayrollAllowances">Kelola Tunjangan & Bonus</label><small class="d-block text-muted">Tanpa izin ini, user hanya dapat mengatur Gaji Pokok dan Potongan.</small></div><?php endif?></td><?php foreach($actions as$action=>$actionLabel):?><td class="text-center"><input class="form-check-input permission-box" type="checkbox" name="permissions[<?=$key?>][]" value="<?=$action?>" data-feature="<?=$key?>" data-action="<?=$action?>"></td><?php endforeach?></tr><?php endforeach?></tbody></table></div>
|
||||
<div class="d-flex justify-content-between mt-3"><button type="button" class="btn btn-outline-danger" id="deleteRole" hidden>Hapus Role Custom</button><button class="btn btn-success ms-auto" id="saveRole">Simpan Permission</button></div>
|
||||
</form></div></div></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user