135 lines
19 KiB
PHP
135 lines
19 KiB
PHP
<?php
|
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
|
|
|
/** Halaman/API Invoice. Seluruh mutasi bisnis dipusatkan di SalesService. */
|
|
class Invoices extends MY_Controller
|
|
{
|
|
public function __construct(){parent::__construct();$this->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;$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)));
|
|
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)
|
|
{
|
|
$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){$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.');}
|
|
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);}
|
|
}
|