From 2a77402c60cfc4f9d86b2cd056dbb94098f5e88e Mon Sep 17 00:00:00 2001 From: Wian Drs Date: Thu, 17 Sep 2026 00:51:53 +0700 Subject: [PATCH] bank, laporan bank, budgeting badge --- application/controllers/Budgets.php | 5 +++- application/controllers/Companies.php | 2 +- application/controllers/Invoices.php | 11 +++++++- .../controllers/Professionalreports.php | 2 +- application/controllers/Sales.php | 7 +++++- application/libraries/NavigationService.php | 4 +-- .../libraries/ProfessionalReportService.php | 13 +++++++++- ...ce_visibility_to_company_bank_accounts.php | 25 +++++++++++++++++++ application/views/budgets/index.php | 8 +++--- application/views/companies/index.php | 6 ++++- .../views/professional_reports/index.php | 15 ++++++++--- 11 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 application/migrations/20260916000100_add_invoice_visibility_to_company_bank_accounts.php diff --git a/application/controllers/Budgets.php b/application/controllers/Budgets.php index a450803..d8620b6 100644 --- a/application/controllers/Budgets.php +++ b/application/controllers/Budgets.php @@ -11,7 +11,10 @@ class Budgets extends MY_Controller $listAccount=(int)$this->input->get('list_account_id'); $accounts=$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('kode_akun')->get('accounts')->result(); $budgetAccounts=$this->db->select('a.id,a.kode_akun,a.nama_akun,a.tipe')->distinct()->from('accounts a')->join('account_budget_entries e','e.account_id=a.id')->where(array('e.company_id'=>$company,'e.is_active'=>1))->order_by('a.kode_akun')->get()->result(); - $data=array('active_menu'=>'budgets','active_tab'=>$tab,'accounts'=>$accounts,'budget_accounts'=>$budgetAccounts,'budget_account_count'=>count($budgetAccounts),'entries'=>$this->budgetservice->entries($listAccount),'reminders'=>$this->budgetservice->currentReminders(),'list_account_id'=>$listAccount,'filters'=>$filters,'report_rows'=>$tab==='accounts'?$this->budgetservice->report($filters):array(),'can_create'=>$this->allowed('can_create'),'can_update'=>$this->allowed('can_update'),'can_delete'=>$this->allowed('can_delete'),'can_export'=>$this->allowed('can_export')); + $entries=$this->budgetservice->entries($listAccount);$reportRows=$tab==='accounts'?$this->budgetservice->report($filters):array(); + $listTotal=array_sum(array_map(function($entry){return(float)$entry->amount;},$entries)); + $accountTotal=array_sum(array_map(function($row){return(float)$row['budget'];},$reportRows)); + $data=array('active_menu'=>'budgets','active_tab'=>$tab,'accounts'=>$accounts,'budget_accounts'=>$budgetAccounts,'budget_account_count'=>count($budgetAccounts),'entries'=>$entries,'list_total_budget'=>$listTotal,'reminders'=>$this->budgetservice->currentReminders(),'list_account_id'=>$listAccount,'filters'=>$filters,'report_rows'=>$reportRows,'account_total_budget'=>$accountTotal,'can_create'=>$this->allowed('can_create'),'can_update'=>$this->allowed('can_update'),'can_delete'=>$this->allowed('can_delete'),'can_export'=>$this->allowed('can_export')); $this->load->view('partials/header',$data);$this->load->view('budgets/index',$data);$this->load->view('partials/footer'); } public function save_entry() diff --git a/application/controllers/Companies.php b/application/controllers/Companies.php index da72e02..36e76df 100644 --- a/application/controllers/Companies.php +++ b/application/controllers/Companies.php @@ -49,7 +49,7 @@ class Companies extends MY_Admin_Controller 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')); + $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,'show_on_invoice'=>$this->input->post('show_on_invoice')?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.'); diff --git a/application/controllers/Invoices.php b/application/controllers/Invoices.php index 30f6c1b..523818c 100644 --- a/application/controllers/Invoices.php +++ b/application/controllers/Invoices.php @@ -123,7 +123,16 @@ class Invoices extends MY_Controller } private function addPdfCompanySettings(array&$data) { - $data['pdf_company']=$this->db->get_where('companies',array('id'=>$this->company()))->row();$currency=!empty($data['invoice']->transaction_currency)?$data['invoice']->transaction_currency:($data['pdf_company']->base_currency??'IDR');$data['pdf_currency']=$this->db->get_where('currencies',array('code'=>$currency))->row();$data['pdf_bank_accounts']=$this->db->table_exists('company_bank_accounts')?$this->db->where(array('company_id'=>$this->company(),'is_active'=>1))->order_by('is_primary','DESC')->order_by('id','ASC')->get('company_bank_accounts')->result():array();$data['pdf_logo_data']='';$path=$data['pdf_company']->logo_path??'';if($path!==''&&is_file(FCPATH.$path)){$mime=function_exists('mime_content_type')?mime_content_type(FCPATH.$path):'image/png';$data['pdf_logo_data']='data:'.$mime.';base64,'.base64_encode(file_get_contents(FCPATH.$path));} + $data['pdf_company']=$this->db->get_where('companies',array('id'=>$this->company()))->row(); + $currency=!empty($data['invoice']->transaction_currency)?$data['invoice']->transaction_currency:($data['pdf_company']->base_currency??'IDR'); + $data['pdf_currency']=$this->db->get_where('currencies',array('code'=>$currency))->row(); + $data['pdf_bank_accounts']=array(); + if($this->db->table_exists('company_bank_accounts')){ + $this->db->where(array('company_id'=>$this->company(),'is_active'=>1)); + if($this->db->field_exists('show_on_invoice','company_bank_accounts'))$this->db->where('show_on_invoice',1); + $data['pdf_bank_accounts']=$this->db->order_by('is_primary','DESC')->order_by('id','ASC')->get('company_bank_accounts')->result(); + } + $data['pdf_logo_data']='';$path=$data['pdf_company']->logo_path??'';if($path!==''&&is_file(FCPATH.$path)){$mime=function_exists('mime_content_type')?mime_content_type(FCPATH.$path):'image/png';$data['pdf_logo_data']='data:'.$mime.';base64,'.base64_encode(file_get_contents(FCPATH.$path));} } private function mutation($permission,callable$callback,$message) { diff --git a/application/controllers/Professionalreports.php b/application/controllers/Professionalreports.php index 8f91329..744c166 100644 --- a/application/controllers/Professionalreports.php +++ b/application/controllers/Professionalreports.php @@ -4,7 +4,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;} + private function reportFilter($code,array$f){if(in_array($code,array('balance_sheet','bank_account_balances'),true))$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);$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'));} diff --git a/application/controllers/Sales.php b/application/controllers/Sales.php index fa0ef8c..02dea71 100644 --- a/application/controllers/Sales.php +++ b/application/controllers/Sales.php @@ -89,7 +89,12 @@ class Sales extends MY_Controller private function decodeRows(){$rows=json_decode((string)$this->input->post('rows'),true);if(!is_array($rows))throw new BusinessException('Detail barang tidak valid.');return$rows;} private function action($fn,$message){if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405);try{return json_response(true,$message,array('result'=>$fn()));}catch(Throwable$e){return business_exception_response($e);}} private function jsonDetail($fn){try{return json_response(true,'Detail ditemukan.',$fn());}catch(Throwable$e){return business_exception_response($e);}} - private function pdf($view,$data,$name){$company=(int)$this->companycontext->id();$data['pdf_company']=$this->db->get_where('companies',array('id'=>$company))->row();$currency=!empty($data['invoice']->transaction_currency)?$data['invoice']->transaction_currency:($data['pdf_company']->base_currency??'IDR');$data['pdf_currency']=$this->db->get_where('currencies',array('code'=>$currency))->row();$data['pdf_bank_accounts']=$this->db->table_exists('company_bank_accounts')?$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('is_primary','DESC')->order_by('id')->get('company_bank_accounts')->result():array();$data['pdf_logo_data']='';$path=$data['pdf_company']->logo_path??'';if($path!==''&&is_file(FCPATH.$path)){$mime=function_exists('mime_content_type')?mime_content_type(FCPATH.$path):'image/png';$data['pdf_logo_data']='data:'.$mime.';base64,'.base64_encode(file_get_contents(FCPATH.$path));}require_once FCPATH.'vendor/autoload.php';$html=$this->load->view($view,$data,true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','portrait');$pdf->render();$pdf->stream($name,array('Attachment'=>false));} + private function pdf($view,$data,$name) + { + $company=(int)$this->companycontext->id();$data['pdf_company']=$this->db->get_where('companies',array('id'=>$company))->row();$currency=!empty($data['invoice']->transaction_currency)?$data['invoice']->transaction_currency:($data['pdf_company']->base_currency??'IDR');$data['pdf_currency']=$this->db->get_where('currencies',array('code'=>$currency))->row();$data['pdf_bank_accounts']=array(); + if($this->db->table_exists('company_bank_accounts')){$this->db->where(array('company_id'=>$company,'is_active'=>1));if($this->db->field_exists('show_on_invoice','company_bank_accounts'))$this->db->where('show_on_invoice',1);$data['pdf_bank_accounts']=$this->db->order_by('is_primary','DESC')->order_by('id')->get('company_bank_accounts')->result();} + $data['pdf_logo_data']='';$path=$data['pdf_company']->logo_path??'';if($path!==''&&is_file(FCPATH.$path)){$mime=function_exists('mime_content_type')?mime_content_type(FCPATH.$path):'image/png';$data['pdf_logo_data']='data:'.$mime.';base64,'.base64_encode(file_get_contents(FCPATH.$path));}require_once FCPATH.'vendor/autoload.php';$html=$this->load->view($view,$data,true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','portrait');$pdf->render();$pdf->stream($name,array('Attachment'=>false)); + } private function uploads($field,$required=false) { if(empty($_FILES[$field])||empty($_FILES[$field]['name'])){if($required)throw new BusinessException('Bukti pembayaran gambar atau PDF wajib diunggah.');return array();}$files=$_FILES[$field];$names=is_array($files['name'])?$files['name']:array($files['name']);$saved=array();if(count($names)>10)throw new BusinessException('Maksimal 10 file.');$dir=FCPATH.'uploads/accounting/sales/';if(!is_dir($dir)&&!mkdir($dir,0755,true))throw new BusinessException('Folder lampiran tidak dapat dibuat.');$finfo=new finfo(FILEINFO_MIME_TYPE);$allowed=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','application/pdf'=>'pdf'); diff --git a/application/libraries/NavigationService.php b/application/libraries/NavigationService.php index 45702ab..539ec36 100644 --- a/application/libraries/NavigationService.php +++ b/application/libraries/NavigationService.php @@ -17,7 +17,7 @@ class NavigationService array('key'=>'fixed_assets','label'=>'Aset Tetap','icon'=>'bi-building-gear','url'=>'fixedassets','feature'=>'asset'), array('key'=>'accounting','label'=>'Accounting','icon'=>'bi-journal-text','feature'=>'jurnal','children'=>array(array('key'=>'jurnal','label'=>'Jurnal Umum','url'=>'jurnal'),array('key'=>'accounts','label'=>'Daftar Akun','url'=>'accounts'),array('key'=>'basejurnal','label'=>'Base Jurnal','url'=>'basejurnal'),array('key'=>'accounting_periods','label'=>'Periode Accounting','url'=>'accountingperiods'))), array('key'=>'professional_reports','label'=>'Laporan Profesional','icon'=>'bi-bar-chart','url'=>'professionalreports','feature'=>'reports'), - array('key'=>'budgets','label'=>'Budget','icon'=>'bi-bullseye','url'=>'budgets','feature'=>'budgets','badge'=>$this->budgetApprovalBadge(),'badge_class'=>'nav-badge-warning','badge_title'=>'Pengingat pembayaran budget yang perlu diperhatikan'), + array('key'=>'budgets','label'=>'Budget','icon'=>'bi-bullseye','url'=>'budgets','feature'=>'budgets','badge'=>$this->budgetDueBadge(),'badge_class'=>'nav-badge-danger','badge_title'=>'Budget jatuh tempo atau terlambat yang belum diselesaikan'), array('key'=>'hr','label'=>'HR & Payroll','icon'=>'bi-people','feature'=>array('hr_organization','hr_employees','hr_schedules','hr_attendance','hr_leave','payroll','employees'),'badge'=>$this->leaveApprovalBadge()+$this->manualAttendanceBadge(),'badge_class'=>'nav-badge-warning','badge_title'=>'Pengajuan HR menunggu tindakan','children'=>array(array('key'=>'employees','label'=>'Karyawan','url'=>$employeeUrl,'feature'=>array('hr_employees','hr_organization','employees'),'active_keys'=>array('employees','organization_master')),array('key'=>'attendance_monitoring','label'=>'Absen & Kehadiran','url'=>$attendanceUrl,'feature'=>array('hr_schedules','hr_attendance','hr_leave','employees'),'active_keys'=>array('generate_schedule','attendance_monitoring','attendance_monthly','shift','holidays','leave_requests'),'badge'=>$this->leaveApprovalBadge()+$this->manualAttendanceBadge(),'badge_class'=>'nav-badge-warning','badge_title'=>'Pengajuan absensi atau cuti menunggu tindakan'),array('key'=>'hr_payroll','label'=>'Payroll','url'=>'hrpayroll','feature'=>array('payroll','employees')))), array('key'=>'companies','label'=>'Company Setting','icon'=>'bi-buildings','url'=>'companies','feature'=>'companies','admin'=>true), array('key'=>'settings','label'=>'Pengaturan','icon'=>'bi-gear','feature'=>'setting','admin'=>true,'children'=>array(array('key'=>'users','label'=>'Pengguna','url'=>'users'),array('key'=>'roles','label'=>'Role & Permission','url'=>'roles'))) @@ -27,7 +27,7 @@ class NavigationService private function leaveApprovalBadge(){if($this->leaveBadge!==null)return$this->leaveBadge;if(!$this->CI->db->table_exists('k_leave_requests')||!$this->CI->db->field_exists('workflow_status','k_leave_requests'))return$this->leaveBadge=0;$this->CI->db->where('workflow_status','submitted');if($this->CI->db->field_exists('company_id','k_leave_requests'))$this->CI->db->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->leaveBadge=(int)$this->CI->db->count_all_results('k_leave_requests');} private function manualAttendanceBadge(){if($this->attendanceBadge!==null)return$this->attendanceBadge;if(!$this->CI->db->table_exists('manual_attendance_requests'))return$this->attendanceBadge=0;$this->CI->db->where('status','submitted')->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->attendanceBadge=(int)$this->CI->db->count_all_results('manual_attendance_requests');} private function salesBadge($name){if($this->salesBadges===null){$c=(int)$this->CI->session->userdata('company_id');$this->salesBadges=array('draft'=>0,'approval'=>0,'delivery'=>0,'receivable'=>0,'overdue'=>0,'return'=>0);if($this->CI->db->table_exists('invoices')){$where=$this->CI->db->field_exists('company_id','invoices')?' AND company_id='.(int)$c:'';$r=$this->CI->db->query("SELECT SUM(workflow_status='draft') draft_count,SUM(workflow_status='submitted') approval_count,SUM(workflow_status='posted' AND sisa_piutang>0) receivable_count,SUM(workflow_status='posted' AND sisa_piutang>0 AND jatuh_temporow();if($r){$this->salesBadges['draft']=(int)$r->draft_count;$this->salesBadges['approval']=(int)$r->approval_count;$this->salesBadges['receivable']=(int)$r->receivable_count;$this->salesBadges['overdue']=(int)$r->overdue_count;}}if($this->CI->db->table_exists('sales_deliveries')){$this->CI->db->where('status','draft');if($this->CI->db->field_exists('company_id','sales_deliveries'))$this->CI->db->where('company_id',$c);$this->salesBadges['delivery']=(int)$this->CI->db->count_all_results('sales_deliveries');}if($this->CI->db->table_exists('sales_returns')){$this->CI->db->where_in('status',array('draft','submitted','approved','received'));if($this->CI->db->field_exists('company_id','sales_returns'))$this->CI->db->where('company_id',$c);$this->salesBadges['return']=(int)$this->CI->db->count_all_results('sales_returns');}}if($name==='all')return$this->salesBadges['draft']+$this->salesBadges['approval']+$this->salesBadges['delivery']+$this->salesBadges['receivable']+$this->salesBadges['return'];return$this->salesBadges[$name]??0;} - private function budgetApprovalBadge(){if($this->budgetBadge!==null)return$this->budgetBadge;if(!$this->allowed('budgets'))return$this->budgetBadge=0;$company=(int)$this->CI->session->userdata('company_id');$total=0;if($this->CI->db->table_exists('budgets')&&(is_master_admin_user()||check_permission('budgets','can_approve'))){$this->CI->db->where('status','submitted');if($this->CI->db->field_exists('company_id','budgets'))$this->CI->db->where('company_id',$company);$total+=(int)$this->CI->db->count_all_results('budgets');}if($this->CI->db->table_exists('account_budget_entries')&&$this->CI->db->table_exists('account_budget_reminder_completions')){$period=date('Y-m');$row=$this->CI->db->query("SELECT COUNT(*) total FROM account_budget_entries e WHERE e.company_id=? AND e.is_active=1 AND e.reminder_enabled=1 AND ((e.entry_type='recurring' AND e.period<=?) OR (e.entry_type='override' AND e.period=?)) AND LEAST(e.reminder_day,DAY(LAST_DAY(CURDATE())))<=LEAST(DAY(LAST_DAY(CURDATE())),DAY(CURDATE())+7) AND NOT EXISTS(SELECT 1 FROM account_budget_reminder_completions c WHERE c.company_id=e.company_id AND c.budget_entry_id=e.id AND c.period=?)",array($company,$period,$period,$period))->row();$total+=(int)($row?$row->total:0);}return$this->budgetBadge=$total;} + private function budgetDueBadge(){if($this->budgetBadge!==null)return$this->budgetBadge;if(!$this->allowed('budgets')||!$this->CI->db->table_exists('account_budget_entries')||!$this->CI->db->table_exists('account_budget_reminder_completions'))return$this->budgetBadge=0;$company=(int)$this->CI->session->userdata('company_id');$period=date('Y-m');$row=$this->CI->db->query("SELECT COUNT(*) total FROM account_budget_entries e WHERE e.company_id=? AND e.is_active=1 AND e.reminder_enabled=1 AND ((e.entry_type='recurring' AND e.period<=?) OR (e.entry_type='override' AND e.period=?)) AND LEAST(e.reminder_day,DAY(LAST_DAY(CURDATE())))<=DAY(CURDATE()) AND NOT EXISTS(SELECT 1 FROM account_budget_reminder_completions c WHERE c.company_id=e.company_id AND c.budget_entry_id=e.id AND c.period=?)",array($company,$period,$period,$period))->row();return$this->budgetBadge=(int)($row?$row->total:0);} private function pendingItemBadge(){if($this->pendingBadge!==null)return$this->pendingBadge;if(!$this->CI->db->table_exists('items'))return$this->pendingBadge=0;$company=(int)$this->CI->session->userdata('company_id');if($this->CI->db->table_exists('item_barcodes'))return$this->pendingBadge=(int)$this->CI->db->query("SELECT COUNT(DISTINCT i.id) total FROM items i LEFT JOIN item_barcodes ib ON ib.item_id=i.id AND ib.status='pending' WHERE i.company_id=? AND EXISTS(SELECT 1 FROM goods_receipt_lines grl WHERE grl.item_id=i.id) AND(i.status='draft' OR ib.id IS NOT NULL)",array($company))->row()->total;return$this->pendingBadge=(int)$this->CI->db->where(array('status'=>'draft','company_id'=>$company))->count_all_results('items');} private function inventoryDocumentBadge(){if($this->inventoryDocumentBadge!==null)return$this->inventoryDocumentBadge;if(!$this->CI->db->table_exists('stock_documents'))return$this->inventoryDocumentBadge=0;$this->CI->db->where_in('status',array('submitted','approved'));if($this->CI->db->field_exists('company_id','stock_documents'))$this->CI->db->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->inventoryDocumentBadge=(int)$this->CI->db->count_all_results('stock_documents');} private function technicianActionBadge(){ diff --git a/application/libraries/ProfessionalReportService.php b/application/libraries/ProfessionalReportService.php index 0703918..b9bb0ca 100644 --- a/application/libraries/ProfessionalReportService.php +++ b/application/libraries/ProfessionalReportService.php @@ -50,6 +50,17 @@ class ProfessionalReportService 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 bankAccountBalances(array$f) + { + if(!$this->CI->db->table_exists('company_bank_accounts'))return array(); + $company=(int)$this->CI->companycontext->id(); + $invoiceColumn=$this->CI->db->field_exists('show_on_invoice','company_bank_accounts')?'b.show_on_invoice':'b.is_active AS show_on_invoice'; + $sql="SELECT b.id bank_account_id,b.bank_name,b.account_number,b.account_holder,b.product_name,b.account_type,b.currency,b.is_primary,b.is_active,{$invoiceColumn},b.opened_at,b.maturity_date,a.id gl_account_id,a.kode_akun,a.nama_akun,COALESCE(x.closing_balance,0) closing_balance,(SELECT COUNT(*) FROM company_bank_accounts bx WHERE bx.company_id=b.company_id AND bx.gl_account_id=b.gl_account_id AND b.gl_account_id IS NOT NULL) linked_account_count FROM company_bank_accounts b LEFT JOIN accounts a ON a.id=b.gl_account_id AND a.company_id=b.company_id LEFT JOIN(SELECT d.account_id,SUM(d.debit-d.kredit) closing_balance 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<=? GROUP BY d.account_id)x ON x.account_id=b.gl_account_id WHERE b.company_id=?"; + $bind=array($company,$f['to'],$company);if(!empty($f['account_id'])){$sql.=' AND b.gl_account_id=?';$bind[]=(int)$f['account_id'];}$sql.=' ORDER BY b.bank_name ASC,b.account_number ASC,b.id ASC'; + $rows=$this->CI->db->query($sql,$bind)->result_array(); + foreach($rows as&$row){$row['closing_balance']=(float)$row['closing_balance'];$row['account_type_label']=$row['account_type']==='deposit'?'Deposito':'Tabungan';$row['account_status']=$row['is_active']?'Aktif':'Nonaktif';$row['invoice_status']=$row['show_on_invoice']?'Ditampilkan':'Tidak ditampilkan';$row['mapping_status']=empty($row['gl_account_id'])?'Belum terhubung':((int)$row['linked_account_count']>1?'COA dipakai '.(int)$row['linked_account_count'].' rekening':'Terhubung');}unset($row); + return$rows; + } 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) { @@ -63,5 +74,5 @@ class ProfessionalReportService { 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.');} + 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==='bank_account_balances')return$this->bankAccountBalances($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.');} } diff --git a/application/migrations/20260916000100_add_invoice_visibility_to_company_bank_accounts.php b/application/migrations/20260916000100_add_invoice_visibility_to_company_bank_accounts.php new file mode 100644 index 0000000..aa5785b --- /dev/null +++ b/application/migrations/20260916000100_add_invoice_visibility_to_company_bank_accounts.php @@ -0,0 +1,25 @@ +db->table_exists('company_bank_accounts')) return; + + if (!$this->db->field_exists('show_on_invoice', 'company_bank_accounts')) { + $this->db->query("ALTER TABLE `company_bank_accounts` + ADD `show_on_invoice` TINYINT(1) NOT NULL DEFAULT 1 AFTER `is_primary`"); + } + + if (!$this->db->query("SHOW INDEX FROM company_bank_accounts WHERE Key_name='idx_company_bank_invoice'")->row()) { + $this->db->query("ALTER TABLE `company_bank_accounts` + ADD KEY `idx_company_bank_invoice` (`company_id`,`is_active`,`show_on_invoice`,`is_primary`)"); + } + } + + public function down() + { + throw new RuntimeException('Pilihan rekening pada PDF Invoice dipertahankan untuk histori dokumen.'); + } +} diff --git a/application/views/budgets/index.php b/application/views/budgets/index.php index 715db68..ed22d27 100644 --- a/application/views/budgets/index.php +++ b/application/views/budgets/index.php @@ -18,7 +18,7 @@ $periodLabel=function($period)use($monthNames){$parts=explode('-',(string)$perio
Daftar Budget

Setiap kebutuhan dibuat sebagai jadwal tersendiri. Beberapa jadwal boleh menggunakan akun yang sama dan semuanya dijumlahkan.

-
Reset
+
Reset
Total BudgetRp
@@ -34,7 +34,7 @@ $periodLabel=function($period)use($monthNames){$parts=explode('-',(string)$perio
Budget Per Akun

Hanya akun yang mempunyai budget yang ditampilkan. Budget berulang dihitung otomatis untuk setiap bulan dalam rentang.

-
+
Total BudgetRp
Budget / AkunPolaMulai / BulanNominal BudgetPengingatKeteranganDiperbaruiAksi
title)?>kode_akun.' - '.$entry->nama_akun.' · '.($accountTypes[$entry->tipe]??ucfirst($entry->tipe)))?>
@@ -53,9 +53,9 @@ $periodLabel=function($period)use($monthNames){$parts=explode('-',(string)$perio
AkunAkumulasi BudgetRealisasiKomitmen POTotal PemakaianSisa%Kondisi
Rp %
Belum ada budget pada periode iniBuat budget berulang atau budget khusus bulan terlebih dahulu.