load->library('SalesService');} private function company(){return(int)$this->companycontext->id();} private function uid(){return(int)$this->session->userdata('user_id');} private function roleId(){return(int)$this->session->userdata('role_id');} private function allowed($action){return is_master_admin_user()||check_permission('invoices',$action);} private function permissions(){return array('view'=>$this->allowed('can_view'),'create'=>$this->allowed('can_create'),'update'=>$this->allowed('can_update'),'delete'=>$this->allowed('can_delete'),'submit'=>$this->allowed('can_submit')||$this->allowed('can_create'),'approve'=>$this->allowed('can_approve'),'post'=>$this->allowed('can_post'),'export'=>$this->allowed('can_export'));} public function index(){$this->page('invoices/index',array('active_menu'=>'sales_invoice','summary'=>$this->salesservice->dashboard(),'permissions'=>$this->permissions()));} public function draft(){redirect('invoices?workflow_status=draft');} public function paid(){redirect('invoices?payment_status=paid');} public function create() { $this->requirePermission('invoices','can_create'); $this->page('invoices/editor',array('active_menu'=>'sales_invoice','mode'=>'create','invoice'=>null,'permissions'=>$this->permissions(),'source_order'=>(int)$this->input->get('source_order'))); } public function edit($id) { $this->requirePermission('invoices','can_update'); try{$invoice=$this->salesservice->invoiceForEditor($id);$this->guardView($invoice);if($invoice->workflow_status!=='draft')redirect('invoices/detail/'.$invoice->id);$this->page('invoices/editor',array('active_menu'=>'sales_invoice','mode'=>'edit','invoice'=>$invoice,'permissions'=>$this->permissions(),'source_order'=>0));}catch(Throwable$e){show_error($e->getMessage(),404,'Invoice tidak ditemukan');} } public function detail($id) { try{$invoice=$this->salesservice->invoiceForEditor($id);$this->guardView($invoice);$this->page('invoices/editor',array('active_menu'=>'sales_invoice','mode'=>'detail','invoice'=>$invoice,'permissions'=>$this->permissions(),'source_order'=>0));}catch(Throwable$e){show_error($e->getMessage(),404,'Invoice tidak ditemukan');} } public function data() { $draw=(int)$this->input->get_post('draw');$start=max(0,(int)$this->input->get_post('start'));$length=max(10,min(100,(int)($this->input->get_post('length')?:25)));$search=$this->input->get_post('search');$term=is_array($search)?trim((string)($search['value']??'')):trim((string)$search); $this->invoiceQuery(false,'');$recordsTotal=(int)$this->db->count_all_results();$this->invoiceQuery(true,$term);$recordsFiltered=(int)$this->db->count_all_results();$this->invoiceQuery(true,$term); $rows=$this->db->select("i.id,i.no_invoice,i.tanggal,i.jatuh_tempo,i.recognition_policy,i.total,i.total_bayar,i.sisa_piutang,i.status,i.workflow_status,i.delivery_status,i.created_by,c.nama customer_name,u.nama creator_name,(CASE WHEN EXISTS(SELECT 1 FROM invoice_details ds WHERE ds.invoice_id=i.id AND ds.company_id=i.company_id AND ds.line_type='savings') AND EXISTS(SELECT 1 FROM invoice_details dx WHERE dx.invoice_id=i.id AND dx.company_id=i.company_id AND dx.line_type IN('inventory','service')) THEN 'mixed' WHEN EXISTS(SELECT 1 FROM invoice_details ds WHERE ds.invoice_id=i.id AND ds.company_id=i.company_id AND ds.line_type='savings') THEN 'savings' ELSE (SELECT dx.line_type FROM invoice_details dx WHERE dx.invoice_id=i.id AND dx.company_id=i.company_id ORDER BY dx.id LIMIT 1) END) invoice_purpose,(SELECT COUNT(*) FROM invoice_details dx WHERE dx.invoice_id=i.id AND dx.company_id=i.company_id AND dx.line_type='inventory') inventory_line_count,(SELECT COUNT(*) FROM invoice_details dx WHERE dx.invoice_id=i.id AND dx.company_id=i.company_id AND dx.line_type='inventory' AND dx.delivery_line_id IS NULL) pending_delivery_count",false)->order_by('i.id','DESC')->limit($length,$start)->get()->result(); return json_response(true,'Daftar invoice ditemukan.',array('draw'=>$draw,'recordsTotal'=>$recordsTotal,'recordsFiltered'=>$recordsFiltered,'data'=>$rows)); } private function invoiceQuery($filters,$term) { $this->db->from('invoices i')->join('customers c','c.id=i.customer_id')->join('users u','u.id=i.created_by','left')->where('i.company_id',$this->company())->where('i.deleted_at IS NULL',null,false); if(!is_master_admin_user())$this->db->group_start()->where('i.workflow_status !=','draft')->or_where('i.created_by',$this->uid())->group_end(); if(!$filters)return;$workflow=trim((string)$this->input->get_post('workflow_status'));$payment=trim((string)$this->input->get_post('payment_status'));$dueStatus=trim((string)$this->input->get_post('due_status'));$from=trim((string)$this->input->get_post('date_from'));$to=trim((string)$this->input->get_post('date_to')); if($workflow!=='')$this->db->where('i.workflow_status',$workflow);if($payment!=='')$this->db->where('i.status',$payment);if($dueStatus==='overdue')$this->db->where('i.jatuh_tempo <',date('Y-m-d'))->where('i.sisa_piutang >',0);elseif($dueStatus==='not_due')$this->db->where('i.jatuh_tempo >=',date('Y-m-d'))->where('i.sisa_piutang >',0);if($from!=='')$this->db->where('i.tanggal >=',$from);if($to!=='')$this->db->where('i.tanggal <=',$to);if($term!=='')$this->db->group_start()->like('i.no_invoice',$term)->or_like('c.nama',$term)->group_end(); } public function lines($id) { try{$invoice=$this->salesservice->invoiceForEditor($id);$this->guardView($invoice);$search=$this->input->get_post('search');$term=is_array($search)?($search['value']??''):$search;$result=$this->salesservice->invoiceLinesPage($id,$this->input->get_post('start'),$this->input->get_post('length')?:25,$term);return json_response(true,'Item invoice ditemukan.',array('draw'=>(int)$this->input->get_post('draw'),'recordsTotal'=>$result['recordsTotal'],'recordsFiltered'=>$result['recordsFiltered'],'data'=>$result['rows'],'summary'=>$result['summary'],'invoice'=>$result['invoice']));}catch(Throwable$e){return business_exception_response($e);} } public function lookup_customers(){return$this->selectLookup('customers','nama',array('nama','alamat'),'Customer ditemukan.');} public function lookup_accounts() { $q=trim((string)$this->input->get('q'));$scope=trim((string)$this->input->get('scope'));$types=array('revenue');$message='Akun pendapatan ditemukan.';if($scope==='asset'){$types=array('asset');$message='Akun Aktiva ditemukan.';}elseif($scope==='liability'){$types=array('liability');$message='Akun Passiva ditemukan.';}elseif($scope==='settlement'){$types=array('asset','liability');$message='Akun pelunasan ditemukan.';} $page=max(1,(int)$this->input->get('page'));$limit=20;$this->db->select("id,CONCAT(kode_akun,' - ',nama_akun) text,tipe",false)->from('accounts')->where('company_id',$this->company())->where_in('tipe',$types)->where(array('is_active'=>1,'allow_posting'=>1));if(!is_master_admin_user())$this->db->where('is_hidden',0);if($q!=='')$this->db->group_start()->like('kode_akun',$q)->or_like('nama_akun',$q)->group_end();$rows=$this->db->order_by('kode_akun')->limit($limit+1,($page-1)*$limit)->get()->result();$more=count($rows)>$limit;if($more)array_pop($rows);return json_response(true,$message,array('results'=>$rows,'pagination'=>array('more'=>$more))); } public function lookup_items() { $q=trim((string)$this->input->get('q'));$page=max(1,(int)$this->input->get('page'));$limit=20; $available="(SELECT ib.item_id,SUM(CASE WHEN ib.status IN('available','reserved') THEN GREATEST(ib.qty_sisa-COALESCE(ib.reserved_qty,0)-COALESCE(ib.return_reserved_qty,0),0) ELSE 0 END) available_stock FROM item_barcodes ib GROUP BY ib.item_id) av"; $this->db->select("i.id,CONCAT(i.kode_detail,' - ',i.nama_barang,' - tersedia ',FORMAT(COALESCE(av.available_stock,0),0,'id_ID')) text,i.kode_detail,i.nama_barang,i.harga_jual,COALESCE(av.available_stock,0) stok,k.tracking_type,k.unit",false)->from('items i')->join('kode_barang k','k.id=i.kode_id','left')->join($available,'av.item_id=i.id','inner',false)->where(array('i.company_id'=>$this->company(),'i.status'=>'active'))->where('av.available_stock >',0);if($q!=='')$this->db->group_start()->like('i.kode_detail',$q)->or_like('i.nama_barang',$q)->group_end();$rows=$this->db->order_by('i.nama_barang')->limit($limit+1,($page-1)*$limit)->get()->result();$more=count($rows)>$limit;if($more)array_pop($rows);return json_response(true,'Barang ditemukan.',array('results'=>$rows,'pagination'=>array('more'=>$more))); } 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; 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))); } private function selectLookup($table,$label,array$fields,$message) { $q=trim((string)$this->input->get('q'));$page=max(1,(int)$this->input->get('page'));$limit=20;$this->db->select('id,'.$label.' text,'.implode(',',$fields))->from($table)->where('company_id',$this->company());if($this->db->field_exists('is_active',$table))$this->db->where('is_active',1);if($q!=='')$this->db->like($label,$q);$rows=$this->db->order_by($label)->limit($limit+1,($page-1)*$limit)->get()->result();$more=count($rows)>$limit;if($more)array_pop($rows);return json_response(true,$message,array('results'=>$rows,'pagination'=>array('more'=>$more))); } public function create_header(){return$this->mutation('can_create',function(){return$this->salesservice->createInvoiceHeader($this->input->post(),$this->uid());},'Header invoice berhasil disimpan sebagai Draft.');} public function update_header($id){return$this->mutation('can_update',function()use($id){return$this->salesservice->updateInvoiceHeader($id,$this->input->post(),$this->uid());},'Aturan invoice berhasil diperbarui.');} public function add_line($id){return$this->mutation('can_update',function()use($id){$row=$this->input->post();$bars=json_decode((string)$this->input->post('barcodes'),true);$row['barcodes']=is_array($bars)?$bars:array();return$this->salesservice->appendInvoiceLine($id,$row,$this->uid());},'Item berhasil ditambahkan ke invoice.');} public function add_lines($id) { return $this->mutation('can_update', function() use ($id) { $rows=json_decode((string)$this->input->post('rows'),true); if(!is_array($rows)||!$rows)throw new BusinessException('Tambahkan minimal satu barang, jasa, atau tabungan.'); if(count($rows)>100)throw new BusinessException('Maksimal 100 item dalam satu kali penambahan.'); $serviceDate=trim((string)$this->input->post('service_date')); $batchKey=trim((string)$this->input->post('idempotency_key')); foreach($rows as $index=>&$row){ if(!is_array($row))throw new BusinessException('Format item ke-'.($index+1).' tidak valid.'); $row['service_date']=$serviceDate; $row['specification']=''; $row['barcodes']=isset($row['barcodes'])&&is_array($row['barcodes'])?$row['barcodes']:array(); $row['idempotency_key']=$batchKey!==''?$batchKey.'-'.($index+1):''; } unset($row); return $this->salesservice->appendInvoiceLinesBatch( $id, $rows, $this->input->post('batch_discount'), $this->uid() ); }, '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 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.');} public function reject($id){return$this->mutation('can_approve',function()use($id){return$this->salesservice->rejectInvoice($id,$this->input->post('reason',true),$this->uid(),$this->roleId());},'Invoice ditolak dan dikembalikan ke Draft.');} public function delete_draft($id){return$this->mutation('can_delete',function()use($id){return$this->salesservice->deleteDraftInvoice($id,$this->uid(),is_master_admin_user());},'Draft invoice dibatalkan, stok/barcode tersedia kembali, dan jurnal awal telah dibalik.');} public function pdf($id) { $this->requirePermission('invoices','can_export');$this->guardView($this->salesservice->invoiceForEditor($id));$data=$this->salesservice->invoiceDetail($id);$includePrevious=(int)$this->input->get('include_previous')===1;$data['previous_invoices']=$includePrevious?$this->salesservice->previousOutstandingInvoices($id):array();$this->addPdfCompanySettings($data);require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('sales/invoice_pdf',$data,true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','portrait');$pdf->render();$pdf->stream('Invoice-'.$data['invoice']->no_invoice.'.pdf',array('Attachment'=>false)); } public function pdf_options($id) { $this->requirePermission('invoices','can_export');try{$this->guardView($this->salesservice->invoiceForEditor($id));$rows=$this->salesservice->previousOutstandingInvoices($id);$total=0;foreach($rows as$row)$total+=(float)$row->sisa_piutang;return json_response(true,'Pilihan PDF ditemukan.',array('count'=>count($rows),'total'=>$total));}catch(Throwable$e){return business_exception_response($e);} } 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));} } private function mutation($permission,callable$callback,$message) { if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405);if(!$this->allowed($permission)&&!($permission==='can_submit'&&$this->allowed('can_create')))return json_response(false,'Anda tidak memiliki izin untuk tindakan ini.',array(),403);try{return json_response(true,$message,array('result'=>$callback()));}catch(Throwable$e){return business_exception_response($e);} } private function guardView($invoice){if($invoice->workflow_status==='draft'&&!is_master_admin_user()&&(int)$invoice->created_by!==$this->uid())throw new BusinessException('Draft invoice hanya dapat dilihat oleh pembuatnya.');} private function page($view,array$data){$this->load->view('partials/header',$data);$this->load->view($view,$data);$this->load->view('partials/footer',$data);} }