update di penjualan invoice

This commit is contained in:
Wian Drs
2026-09-12 01:45:03 +07:00
parent e43bda8d6b
commit ec94783db4
22 changed files with 776 additions and 44 deletions
+3 -2
View File
@@ -7,6 +7,7 @@ class Customers extends MY_Controller {
{
parent::__construct();
$this->load->model('DynamicModel', 'dm');
$this->load->library('SalesService');
if (!$this->session->userdata('logged_in')) {
redirect('auth');
@@ -15,9 +16,9 @@ class Customers extends MY_Controller {
public function index()
{
$data = ["active_menu" => "customers"];
$data = ["active_menu" => "customers", "summary" => $this->salesservice->dashboard()];
$this->load->view('partials/header', $data);
$this->load->view('customers/layout');
$this->load->view('customers/layout', $data);
$this->load->view('partials/footer');
}
+5 -3
View File
@@ -58,14 +58,16 @@ class Invoices extends MY_Controller
}
public function lookup_items()
{
$q=trim((string)$this->input->get('q'));$page=max(1,(int)$this->input->get('page'));$limit=20;$this->db->select("i.id,CONCAT(i.kode_detail,' - ',i.nama_barang) text,i.kode_detail,i.nama_barang,i.harga_jual,i.stok,k.tracking_type,k.unit",false)->from('items i')->join('kode_barang k','k.id=i.kode_id','left')->where(array('i.company_id'=>$this->company(),'i.status'=>'active','i.stok >'=>'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)));
$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 ',ROUND(b.qty_sisa-b.reserved_qty-COALESCE(b.return_reserved_qty,0),4)) 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);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)));
$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)
{
@@ -107,7 +109,7 @@ class Invoices extends MY_Controller
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 dan seluruh reservasi dilepas.');}
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));
+7
View File
@@ -693,6 +693,13 @@ class Items extends MY_Controller {
return json_response(false, 'Barang tidak ditemukan.', array(), 404);
}
if ($item->status === 'active') {
$availability = $this->db->query("SELECT COUNT(*) barcode_count,COALESCE(SUM(CASE WHEN status IN('available','reserved') THEN GREATEST(qty_sisa-COALESCE(reserved_qty,0)-COALESCE(return_reserved_qty,0),0) ELSE 0 END),0) available_stock,COALESCE(SUM(COALESCE(reserved_qty,0)+COALESCE(return_reserved_qty,0)),0) reserved_stock FROM item_barcodes WHERE item_id=?", array((int)$item->id))->row();
$item->physical_stock = (float)$item->stok;
$item->available_stock = $availability && (int)$availability->barcode_count > 0 ? (float)$availability->available_stock : (float)$item->stok;
$item->reserved_stock = $availability ? (float)$availability->reserved_stock : 0;
}
$logs = $this->db
->select('stock_logs.*, warehouses.nama as gudang')
->join('warehouses','warehouses.id = stock_logs.warehouse_id','left')
+1 -1
View File
@@ -12,7 +12,7 @@ class Receivables extends MY_Controller
{
$focusId=(int)$this->input->get('invoice_id');$focus=null;
if($focusId)$focus=$this->db->select('i.id,i.customer_id,i.no_invoice,i.sisa_piutang,c.nama customer_name')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.id'=>$focusId,'i.company_id'=>$this->company(),'i.workflow_status'=>'posted'))->where('i.sisa_piutang >',0)->get()->row();
$data=array('active_menu'=>'receivables','aging'=>$this->receivableservice->aging($this->input->get('as_of')?:date('Y-m-d')),'focus_invoice'=>$focus,'can_post'=>is_master_admin_user()||check_permission('invoices','can_post'));
$data=array('active_menu'=>'receivables','aging'=>$this->receivableservice->aging($this->input->get('as_of')?:date('Y-m-d')),'focus_invoice'=>$focus,'can_post'=>is_master_admin_user()||check_permission('invoices','can_post'),'summary'=>$this->salesservice->dashboard());
$this->load->view('partials/header',$data);$this->load->view('receivables/index',$data);$this->load->view('partials/footer',$data);
}
+28 -1
View File
@@ -57,7 +57,34 @@ class Sales extends MY_Controller
public function return_pdf($id){$data=$this->salesservice->returnDetail($id);$this->pdf('sales/return_pdf',$data,'Retur-'.$data['return']->return_no.'.pdf');}
public function receipt_pdf($id)
{
$company=(int)$this->companycontext->id();$data['payment']=$this->db->select('p.*,c.nama customer_name,c.alamat')->from('payments p')->join('customers c','c.id=p.customer_id')->where(array('p.id'=>(int)$id,'p.company_id'=>$company))->get()->row();if(!$data['payment'])show_404();$data['allocations']=$this->db->select('a.*,i.no_invoice')->from('payment_allocations a')->join('invoices i','i.id=a.invoice_id')->where(array('a.payment_id'=>(int)$id,'i.company_id'=>$company))->get()->result();$data['sources']=$this->db->select('s.*,a.kode_akun,a.nama_akun')->from('payment_sources s')->join('accounts a','a.id=s.account_id')->where(array('s.payment_id'=>(int)$id,'s.company_id'=>$company))->get()->result();$this->pdf('sales/receipt_pdf',$data,'Bukti-Penerimaan-'.$data['payment']->payment_no.'.pdf');
$company=(int)$this->companycontext->id();
$data['payment']=$this->db
->select('p.*,c.nama customer_name,c.alamat customer_address,c.telp customer_phone,c.email customer_email,u.nama creator_name')
->from('payments p')
->join('customers c','c.id=p.customer_id')
->join('users u','u.id=p.created_by','left')
->where(array('p.id'=>(int)$id,'p.company_id'=>$company))
->get()
->row();
if(!$data['payment'])show_404();
$data['allocations']=$this->db
->select('a.*,i.no_invoice,i.tanggal invoice_date,i.jatuh_tempo')
->from('payment_allocations a')
->join('invoices i','i.id=a.invoice_id')
->where(array('a.payment_id'=>(int)$id,'i.company_id'=>$company))
->order_by('i.tanggal','ASC')
->order_by('i.id','ASC')
->get()
->result();
$data['sources']=$this->db
->select('s.*,a.kode_akun,a.nama_akun')
->from('payment_sources s')
->join('accounts a','a.id=s.account_id')
->where(array('s.payment_id'=>(int)$id,'s.company_id'=>$company))
->order_by('s.id','ASC')
->get()
->result();
$this->pdf('sales/receipt_pdf',$data,'Bukti-Penerimaan-'.$data['payment']->payment_no.'.pdf');
}
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);}}
+28 -6
View File
@@ -184,7 +184,9 @@ class SalesService
if(!$bid||isset($seenBarcodes[$bid]))throw new BusinessException('Barcode tidak boleh dipilih dua kali pada satu baris.');
$seenBarcodes[$bid]=true;
if($this->CI->db->where(array('invoice_id'=>$invoice->id,'barcode_id'=>$bid))->count_all_results('invoice_line_barcodes'))throw new BusinessException('Barcode sudah dipakai pada item lain di invoice ini.');
$bq=$this->qty($b['qty']??1);$barcode=$this->CI->db->query("SELECT * FROM item_barcodes WHERE id=? AND item_id=? AND warehouse_id=? AND status='available' FOR UPDATE",array($bid,$item->id,$warehouse->id))->row();if(!$barcode)throw new BusinessException('Barcode barang '.$item->nama_barang.' tidak tersedia di gudang terpilih.');$available=(float)$barcode->qty_sisa-(float)$barcode->reserved_qty-(float)($barcode->return_reserved_qty??0);if($bq>$available+.0001)throw new BusinessException('Qty barcode '.$barcode->barcode.' melebihi saldo tersedia.');$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',array('reserved_qty'=>(float)$barcode->reserved_qty+$bq,'version'=>(int)$barcode->version+1));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode sedang dipilih pengguna lain. Silakan ulangi.');$barcode->reserved_qty=(float)$barcode->reserved_qty+$bq;$barcode->version=(int)$barcode->version+1;$barcodeQty+=$bq;$cleanBarcodes[]=array('row'=>$barcode,'qty'=>$bq);
$bq=$this->qty($b['qty']??1);$barcode=$this->CI->db->query("SELECT * FROM item_barcodes WHERE id=? AND item_id=? AND warehouse_id=? AND status='available' FOR UPDATE",array($bid,$item->id,$warehouse->id))->row();if(!$barcode)throw new BusinessException('Barcode barang '.$item->nama_barang.' tidak tersedia di gudang terpilih.');
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.' sudah digunakan pada invoice lain.');}
$available=(float)$barcode->qty_sisa-(float)$barcode->reserved_qty-(float)($barcode->return_reserved_qty??0);if($bq>$available+.0001)throw new BusinessException('Qty barcode '.$barcode->barcode.' melebihi saldo tersedia.');$reservedAfter=(float)$barcode->reserved_qty+$bq;$reserveUpdate=array('reserved_qty'=>$reservedAfter,'version'=>(int)$barcode->version+1);if($kb&&strtoupper((string)$kb->tracking_type)==='UNIT')$reserveUpdate['status']='reserved';$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',$reserveUpdate);if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode sedang dipilih pengguna lain. Silakan ulangi.');$barcode->reserved_qty=$reservedAfter;$barcode->version=(int)$barcode->version+1;$barcodeQty+=$bq;$cleanBarcodes[]=array('row'=>$barcode,'qty'=>$bq);
}
if(abs($barcodeQty-$q)>.0001)throw new BusinessException('Total qty barcode harus sama dengan qty barang '.$item->nama_barang.'.');if($kb&&$kb->tracking_type==='UNIT')foreach($cleanBarcodes as$b)if(abs($b['qty']-1)>.0001)throw new BusinessException('Barcode UNIT harus memiliki qty 1.');
}
@@ -315,7 +317,12 @@ class SalesService
private function releaseInvoiceLineBarcodes($lineId)
{
$bars=$this->CI->db->where(array('invoice_detail_id'=>(int)$lineId,'company_id'=>$this->company()))->get('invoice_line_barcodes')->result();
foreach($bars as$b){$barcode=$this->CI->db->query('SELECT * FROM item_barcodes WHERE id=? FOR UPDATE',array($b->barcode_id))->row();if($barcode)$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',array('reserved_qty'=>max(0,(float)$barcode->reserved_qty-(float)$b->qty),'status'=>'available','version'=>(int)$barcode->version+1));}
foreach($bars as$b){
$barcode=$this->CI->db->query('SELECT ib.* FROM item_barcodes ib JOIN items i ON i.id=ib.item_id WHERE ib.id=? AND i.company_id=? FOR UPDATE',array($b->barcode_id,$this->company()))->row();
if(!$barcode)throw new BusinessException('Barcode reservasi invoice tidak ditemukan. Pembatalan dihentikan agar stok dan jurnal tetap sinkron.');
$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',array('reserved_qty'=>max(0,(float)$barcode->reserved_qty-(float)$b->qty),'status'=>'available','version'=>(int)$barcode->version+1));
if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode sedang berubah pada proses lain. Silakan ulangi pembatalan invoice.');
}
$this->CI->db->where(array('invoice_detail_id'=>(int)$lineId,'company_id'=>$this->company()))->delete('invoice_line_barcodes');
}
@@ -353,7 +360,7 @@ class SalesService
$totalCost=0;$inventoryReclass=0;
foreach($lines as$l){
if($l->stock_posted_at)continue;$bars=$this->CI->db->where('sales_delivery_line_id',$l->id)->get('sales_delivery_barcodes')->result();$allocated=0;
foreach($bars as$b){$barcode=$this->CI->db->query('SELECT * FROM item_barcodes WHERE id=? AND item_id=? AND warehouse_id=? FOR UPDATE',array($b->barcode_id,$l->item_id,$d->warehouse_id))->row();if(!$barcode||$barcode->status!=='available'||(float)$barcode->qty_sisa<(float)$b->qty-.0001||(float)$barcode->reserved_qty<(float)$b->qty-.0001)throw new BusinessException('Barcode pada Surat Jalan sudah tidak tersedia atau reservasinya tidak valid.');$left=(float)$barcode->qty_sisa-(float)$b->qty;$reserved=max(0,(float)$barcode->reserved_qty-(float)$b->qty);$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',array('qty_sisa'=>$left,'reserved_qty'=>$reserved,'status'=>$left<=.0001?'sold_out':'available','version'=>(int)$barcode->version+1));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode sedang diproses pengguna lain. Silakan ulangi.');$this->CI->db->where('id',$b->id)->update('sales_delivery_barcodes',array('status'=>'delivered'));$allocated+=(float)$b->qty;}
foreach($bars as$b){$barcode=$this->CI->db->query('SELECT * FROM item_barcodes WHERE id=? AND item_id=? AND warehouse_id=? FOR UPDATE',array($b->barcode_id,$l->item_id,$d->warehouse_id))->row();if(!$barcode||!in_array($barcode->status,array('available','reserved'),true)||(float)$barcode->qty_sisa<(float)$b->qty-.0001||(float)$barcode->reserved_qty<(float)$b->qty-.0001)throw new BusinessException('Barcode pada Surat Jalan sudah tidak tersedia atau reservasinya tidak valid.');$left=(float)$barcode->qty_sisa-(float)$b->qty;$reserved=max(0,(float)$barcode->reserved_qty-(float)$b->qty);$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',array('qty_sisa'=>$left,'reserved_qty'=>$reserved,'status'=>$left<=.0001?'sold_out':($reserved>.0001?'reserved':'available'),'version'=>(int)$barcode->version+1));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode sedang diproses pengguna lain. Silakan ulangi.');$this->CI->db->where('id',$b->id)->update('sales_delivery_barcodes',array('status'=>'delivered'));$allocated+=(float)$b->qty;}
if(abs($allocated-(float)$l->qty)>.0001)throw new BusinessException('Alokasi barcode Surat Jalan tidak sama dengan qty barang.');
$detailBefore=$this->CI->db->get_where('invoice_details',array('id'=>$l->invoice_detail_id,'company_id'=>$this->company()))->row();$initialJournal=$detailBefore?$this->initialLineJournal($detailBefore->id):null;$hadInitialJournal=$initialJournal&&!$initialJournal->is_reversed&&$initialJournal->status==='posted';$previousCost=$detailBefore?(float)$detailBefore->total_hpp_barang:0;
$cost=$this->consumeCost($l->item_id,$d->warehouse_id,$l->qty);$totalCost+=$cost;$inventoryReclass+=$hadInitialJournal?($cost-$previousCost):$cost;$key='SALES-DELIVERY-LINE-'.$l->id;$desc='[AUTO] Surat Jalan '.$d->delivery_no.' | '.$l->description.' | Qty '.number_format($l->qty,0,',','.');
@@ -378,7 +385,7 @@ class SalesService
public function submitInvoice($id,$user)
{
return$this->CI->transactionservice->run(function()use($id,$user){$i=$this->lockInvoice($id,'draft');$this->assertDraftOwner($i,$user);$open=(int)$this->CI->db->where(array('invoice_id'=>$i->id,'company_id'=>$this->company(),'status'=>'draft'))->count_all_results('sales_deliveries');if($open)throw new BusinessException('Seluruh Surat Jalan Draft harus diposting sebelum invoice diajukan.');$this->refreshInvoiceTotals($i->id);$this->updateInvoiceDeliveryStatus($i->id);$i=$this->lockInvoice($id,'draft');if((float)$i->total<=0)throw new BusinessException('Invoice belum mempunyai nilai.');if($i->delivery_status!=='delivered')throw new BusinessException('Pengiriman barang pada invoice belum selesai.');$this->CI->approvalservice->create('invoice','invoice',$i->id,$i->total,$user,array('invoice_no'=>$i->no_invoice,'customer_id'=>$i->customer_id));$this->CI->db->where('id',$i->id)->update('invoices',array('workflow_status'=>'submitted','finalized_at'=>date('Y-m-d H:i:s'),'finalized_by'=>$user,'submitted_at'=>date('Y-m-d H:i:s'),'submitted_by'=>$user));$this->CI->auditservice->record('sales','invoice',$i->id,'submit',$i,array('status'=>'submitted'),$user);return$i->id;});
return$this->CI->transactionservice->run(function()use($id,$user){$i=$this->lockInvoice($id,'draft');$this->assertDraftOwner($i,$user);$this->refreshInvoiceTotals($i->id);$this->updateInvoiceDeliveryStatus($i->id);$i=$this->lockInvoice($id,'draft');if((float)$i->total<=0)throw new BusinessException('Invoice belum mempunyai nilai.');$this->CI->approvalservice->create('invoice','invoice',$i->id,$i->total,$user,array('invoice_no'=>$i->no_invoice,'customer_id'=>$i->customer_id));$this->CI->db->where('id',$i->id)->update('invoices',array('workflow_status'=>'submitted','finalized_at'=>date('Y-m-d H:i:s'),'finalized_by'=>$user,'submitted_at'=>date('Y-m-d H:i:s'),'submitted_by'=>$user));$this->CI->auditservice->record('sales','invoice',$i->id,'submit',$i,array('status'=>'submitted','delivery_status'=>$i->delivery_status,'delivery_optional'=>true),$user);return$i->id;});
}
public function rejectInvoice($id,$reason,$user,$roleId)
{
@@ -390,7 +397,7 @@ class SalesService
}
private function postInvoiceInternal($id,$user)
{
$i=$this->lockInvoice($id,'approved');$this->CI->fiscalperiodservice->assertOpen($i->tanggal);if($i->journal_id)throw new BusinessException('Invoice sudah pernah diposting.');if($i->delivery_status!=='delivered')throw new BusinessException('Invoice hanya dapat diposting setelah seluruh Surat Jalan diposting.');$lines=$this->CI->db->where(array('invoice_id'=>$i->id,'company_id'=>$this->company()))->get('invoice_details')->result();if(!$lines)throw new BusinessException('Invoice belum mempunyai item.');
$i=$this->lockInvoice($id,'approved');$this->CI->fiscalperiodservice->assertOpen($i->tanggal);if($i->journal_id)throw new BusinessException('Invoice sudah pernah diposting.');$lines=$this->CI->db->where(array('invoice_id'=>$i->id,'company_id'=>$this->company()))->get('invoice_details')->result();if(!$lines)throw new BusinessException('Invoice belum mempunyai item.');
$family=$this->lineFamily($lines[0]->line_type);$entries=array();$cogs=0;$revenueAmount=0;$hasSales=false;$hasSavings=false;$hasLoan=false;foreach($lines as$l){if(in_array($l->line_type,array('inventory','service'),true))$hasSales=true;elseif($l->line_type==='savings')$hasSavings=true;elseif($l->line_type==='loan')$hasLoan=true;}
if($hasSavings&&!$hasSales&&!$hasLoan){
$this->CI->db->where('id',$i->id)->update('invoices',array('workflow_status'=>'posted','posted'=>1,'posted_by'=>$user,'posted_at'=>date('Y-m-d H:i:s'),'journal_id'=>null,'status'=>'unpaid','sisa_piutang'=>$i->total,'recognized_revenue'=>0,'recognized_cogs'=>0,'recognition_policy'=>'on_payment'));
@@ -484,7 +491,22 @@ class SalesService
private function recordCashSubledger($glAccountId,$date,$direction,$amount,$partyType,$partyId,$sourceType,$sourceId,$journalId,$description,$user)
{
if(!$this->CI->db->table_exists('cash_transactions'))return;$cash=$this->CI->db->get_where('cash_accounts',array('company_id'=>$this->company(),'gl_account_id'=>(int)$glAccountId,'is_active'=>1))->row();if(!$cash)return;
if(!$this->CI->db->table_exists('cash_accounts')||!$this->CI->db->table_exists('cash_transactions'))return;
// cash_accounts pada database lama belum memiliki company_id. Scope tenant
// tetap dijaga melalui akun GL yang terhubung karena accounts.company_id
// merupakan sumber kepemilikan akun yang digunakan saat pembayaran.
$cash=$this->CI->db
->select('cash_accounts.*')
->from('cash_accounts')
->join('accounts','accounts.id=cash_accounts.gl_account_id')
->where('accounts.company_id',$this->company())
->where('cash_accounts.gl_account_id',(int)$glAccountId)
->where('cash_accounts.is_active',1)
->limit(1)
->get()
->row();
if(!$cash)return;
$key='C'.$this->company().'-'.strtoupper($sourceType).'-'.(int)$sourceId.'-'.(int)$cash->id.'-'.$direction;if($this->CI->db->where('idempotency_key',$key)->count_all_results('cash_transactions'))return;
$row=$this->columns('cash_transactions',array('company_id'=>$this->company(),'transaction_no'=>$this->CI->numberingservice->next('cash_transaction',$date),'transaction_date'=>$date,'cash_account_id'=>$cash->id,'transaction_type'=>$direction==='in'?'receipt':'payment','direction'=>$direction,'amount'=>$amount,'counter_account_id'=>null,'party_type'=>$partyType,'party_id'=>$partyId,'reference_no'=>null,'description'=>'[AUTO] '.$description,'status'=>'posted','requires_approval'=>0,'source_type'=>$sourceType,'source_id'=>$sourceId,'journal_id'=>$journalId,'idempotency_key'=>$key,'created_by'=>$user,'posted_by'=>$user,'posted_at'=>date('Y-m-d H:i:s'),'created_at'=>date('Y-m-d H:i:s')));$this->CI->db->insert('cash_transactions',$row);
}
+11 -4
View File
@@ -1,4 +1,10 @@
<div class="container mt-4">
<link rel="stylesheet" href="<?=base_url('assets/css/invoices-professional.css').'?v='.filemtime(FCPATH.'assets/css/invoices-professional.css')?>">
<section class="invoice-workspace">
<header class="invoice-hero">
<div><span>SIKLUS PENJUALAN</span><h2>Customer</h2><p>Kelola identitas, termin pembayaran, dan kebijakan kredit customer.</p></div>
</header>
<?php $this->load->view('sales/_section_nav',array('sales_section'=>'customer','sales_summary'=>$summary)); ?>
<div class="container-fluid px-0">
<div class="card modern-card shadow-sm border-0 rounded-4">
<div class="card-body p-4">
@@ -24,9 +30,10 @@
</div>
</div>
</div>
<!-- MODAL -->
</div>
</section>
<!-- MODAL -->
<div class="modal fade" id="modalCustomer">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
+2 -2
View File
@@ -17,7 +17,7 @@ function invLabel($value){$labels=array('draft'=>'Draft','submitted'=>'Menunggu
<div class="hero-actions"><a class="btn btn-light" href="<?=base_url('invoices')?>"><i class="bi bi-arrow-left"></i> Daftar Invoice</a><?php if($invoice&&$p['export']):?><button class="btn btn-outline-danger open-invoice-pdf" data-id="<?=$invoice->id?>" data-url="<?=base_url('invoices/pdf/'.$invoice->id)?>"><i class="bi bi-file-pdf"></i> PDF</button><?php endif?></div>
</header>
<?php if($invoice):?><div class="invoice-progress"><span class="done"><i class="bi bi-check-lg"></i> Header Draft</span><span class="<?=$invoice->delivery_status==='delivered'?'done':''?>"><i class="bi bi-truck"></i> <?=$invoice->inventory_line_count?'Pengiriman':'Tanpa Surat Jalan'?></span><span class="<?=in_array($invoice->workflow_status,array('submitted','approved','posted'),true)?'done':''?>"><i class="bi bi-send"></i> Diajukan</span><span class="<?=$invoice->workflow_status==='posted'?'done':''?>"><i class="bi bi-patch-check"></i> Approved & Posted</span></div><?php endif?>
<?php if($invoice):?><div class="invoice-progress"><span class="done"><i class="bi bi-check-lg"></i> Header Draft</span><span class="<?=$invoice->delivery_status==='delivered'?'done':'optional'?>"><i class="bi bi-truck"></i> <?=$invoice->inventory_line_count?'Surat Jalan (Opsional)':'Tanpa Surat Jalan'?></span><span class="<?=in_array($invoice->workflow_status,array('submitted','approved','posted'),true)?'done':''?>"><i class="bi bi-send"></i> Diajukan</span><span class="<?=$invoice->workflow_status==='posted'?'done':''?>"><i class="bi bi-patch-check"></i> Approved & Posted</span></div><?php endif?>
<div class="invoice-editor-grid <?=$creating?'is-creating':''?>">
<main class="invoice-editor-main">
@@ -57,7 +57,7 @@ function invLabel($value){$labels=array('draft'=>'Draft','submitted'=>'Menunggu
</div>
<?php if($editable):?><button class="btn btn-primary btn-lg w-100 add-invoice-item-button" type="button" data-bs-toggle="modal" data-bs-target="#invoiceItemModal"><i class="bi bi-plus-lg"></i> Tambah Item</button><?php if((int)$invoice->pending_delivery_count>0):?><button class="btn btn-outline-primary w-100 generate-delivery"><i class="bi bi-truck"></i> Buat Surat Jalan</button><?php endif?><?php endif?>
<?php endif?>
<?php if($invoice):?><div class="card editor-card invoice-sidebar-actions"><div class="card-body"><div class="sidebar-next-status"><small>Status selanjutnya</small><b><?=$draft?($invoice->pending_delivery_count?'Buat dan posting Surat Jalan, lalu ajukan invoice.':'Ajukan invoice untuk approval.'):invLabel($invoice->workflow_status)?></b></div><div class="sidebar-action-buttons"><?php if($draft&&$p['submit']):?><button class="btn btn-primary submit-invoice"><i class="bi bi-send"></i> Ajukan Invoice</button><?php endif?><?php if($draft&&$p['delete']):?><button class="btn btn-outline-danger delete-invoice"><i class="bi bi-trash"></i> Batalkan Draft</button><?php endif?><?php if($invoice->workflow_status==='submitted'&&$p['approve']):?><button class="btn btn-success approve-invoice"><i class="bi bi-check-lg"></i> Setujui & Posting</button><button class="btn btn-outline-danger reject-invoice"><i class="bi bi-x-lg"></i> Tolak</button><?php endif?><?php if($invoice->workflow_status==='posted'&&in_array($invoice->status,array('unpaid','partial'),true)&&$p['post']):?><a class="btn btn-warning" href="<?=base_url('receivables?invoice_id='.$invoice->id)?>"><i class="bi bi-cash-coin"></i> Terima Pembayaran</a><?php endif?></div></div></div><?php endif?>
<?php if($invoice):?><div class="card editor-card invoice-sidebar-actions"><div class="card-body"><div class="sidebar-next-status"><small>Status selanjutnya</small><b><?=$draft?'Ajukan invoice untuk approval. Surat Jalan dapat dibuat bila diperlukan.':invLabel($invoice->workflow_status)?></b></div><div class="sidebar-action-buttons"><?php if($draft&&$p['submit']):?><button class="btn btn-primary submit-invoice"><i class="bi bi-send"></i> Ajukan Invoice</button><?php endif?><?php if($draft&&$p['delete']):?><button class="btn btn-outline-danger delete-invoice"><i class="bi bi-trash"></i> Batalkan Draft</button><?php endif?><?php if($invoice->workflow_status==='submitted'&&$p['approve']):?><button class="btn btn-success approve-invoice"><i class="bi bi-check-lg"></i> Setujui & Posting</button><button class="btn btn-outline-danger reject-invoice"><i class="bi bi-x-lg"></i> Tolak</button><?php endif?><?php if($invoice->workflow_status==='posted'&&in_array($invoice->status,array('unpaid','partial'),true)&&$p['post']):?><a class="btn btn-warning" href="<?=base_url('receivables?invoice_id='.$invoice->id)?>"><i class="bi bi-cash-coin"></i> Terima Pembayaran</a><?php endif?></div></div></div><?php endif?>
</div>
</aside>
</div>
+1 -8
View File
@@ -5,14 +5,7 @@
<div><span>SIKLUS PENJUALAN</span><h2>Invoice Penjualan</h2><p>Kelola tagihan customer, pengiriman barang bila diperlukan, dan penerimaan pembayaran.</p></div>
<?php if($p['create']):?><a class="btn btn-primary" href="<?=base_url('invoices/create')?>"><i class="bi bi-plus-lg"></i> Buat Invoice</a><?php endif?>
</header>
<nav class="sales-section-nav" aria-label="Bagian Penjualan">
<a href="<?=base_url('sales')?>"><i class="bi bi-speedometer2"></i><span>Ringkasan</span></a>
<a class="active" href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i><span>Invoice</span><?php if($summary['draft']):?><b><?=$summary['draft']?></b><?php endif?></a>
<a href="<?=base_url('sales/deliveries')?>"><i class="bi bi-truck"></i><span>Surat Jalan</span><?php if($summary['delivery']):?><b><?=$summary['delivery']?></b><?php endif?></a>
<a href="<?=base_url('receivables')?>"><i class="bi bi-cash-coin"></i><span>Piutang</span><?php if($summary['open_invoice']):?><b><?=$summary['open_invoice']?></b><?php endif?></a>
<a href="<?=base_url('sales/returns')?>"><i class="bi bi-arrow-counterclockwise"></i><span>Retur</span><?php if($summary['return']):?><b><?=$summary['return']?></b><?php endif?></a>
<a href="<?=base_url('customers')?>"><i class="bi bi-people"></i><span>Customer</span></a>
</nav>
<?php $this->load->view('sales/_section_nav',array('sales_section'=>'invoice','sales_summary'=>$summary)); ?>
<div class="invoice-metrics">
<article><small>Penjualan bulan ini</small><strong>Rp <?=number_format($summary['month_sales'],2,',','.')?></strong></article>
<article><small>Invoice belum lunas</small><strong><?=$summary['open_invoice']?></strong><span><?=$summary['overdue']?> sudah jatuh tempo</span></article>
+1 -1
View File
@@ -6,7 +6,7 @@
$(function(){
const 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='active';d.server=1}},pageLength:25,language:{search:'Cari:',processing:'Memuat data...',lengthMenu:'Tampilkan _MENU_',info:'_START_–_END_ dari _TOTAL_',paginate:{previous:'Sebelumnya',next:'Berikutnya'}}});
$(document).on('click','.btn-detail',function(){let id=$(this).data('id');$('#itemDetailBody').html('<div class="inventory-loading"><span class="spinner-border spinner-border-sm"></span> Memuat detail…</div>');bootstrap.Modal.getOrCreateInstance('#itemDetailModal').show();$.get('<?=base_url('items/detail')?>/'+id).done(function(r){let rows=(r.logs||[]).map(x=>'<tr><td>'+esc(x.created_at)+'</td><td>'+esc(x.gudang)+'</td><td><span class="status-chip '+(x.tipe==='masuk'?'status-approved':'status-rejected')+'">'+esc(x.tipe)+'</span></td><td class="text-end">'+Number(x.qty||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</td><td>'+esc(x.keterangan)+'</td><td>'+esc(x.ref_type)+' #'+esc(x.ref_id)+'</td></tr>').join('');$('#itemDetailBody').html('<div class="document-meta"><span>Kode<strong>'+esc(r.item.kode_detail)+'</strong></span><span>Nama Barang<strong>'+esc(r.item.nama_barang)+'</strong></span><span>Stok<strong>'+Number(r.item.stok||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</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>Status<strong><span class="status-chip status-approved">AKTIF</span></strong></span></div><div class="d-flex justify-content-between align-items-center mt-4 mb-2"><h6 class="mb-0">Riwayat Stok</h6><button class="btn btn-sm btn-outline-danger stock-pdf" data-id="'+r.item.id+'"><i class="bi bi-file-earmark-pdf"></i> PDF Audit</button></div><div class="table-responsive"><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><th>Sumber</th></tr></thead><tbody>'+rows+'</tbody></table></div>')}).fail(()=>AppAlert.toast('Detail barang tidak dapat dimuat.','error'))});
$(document).on('click','.btn-detail',function(){let id=$(this).data('id');$('#itemDetailBody').html('<div class="inventory-loading"><span class="spinner-border spinner-border-sm"></span> Memuat detail…</div>');bootstrap.Modal.getOrCreateInstance('#itemDetailModal').show();$.get('<?=base_url('items/detail')?>/'+id).done(function(r){let rows=(r.logs||[]).map(x=>'<tr><td>'+esc(x.created_at)+'</td><td>'+esc(x.gudang)+'</td><td><span class="status-chip '+(x.tipe==='masuk'?'status-approved':'status-rejected')+'">'+esc(x.tipe)+'</span></td><td class="text-end">'+Number(x.qty||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</td><td>'+esc(x.keterangan)+'</td><td>'+esc(x.ref_type)+' #'+esc(x.ref_id)+'</td></tr>').join('');$('#itemDetailBody').html('<div class="document-meta"><span>Kode<strong>'+esc(r.item.kode_detail)+'</strong></span><span>Nama Barang<strong>'+esc(r.item.nama_barang)+'</strong></span><span>Stok Tersedia<strong>'+Number(r.item.available_stock||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</strong></span><span>Dalam Invoice/Reservasi<strong>'+Number(r.item.reserved_stock||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</strong></span><span>Stok Fisik<strong>'+Number(r.item.physical_stock||0).toLocaleString('id-ID',{maximumFractionDigits:4})+'</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>Status<strong><span class="status-chip status-approved">AKTIF</span></strong></span></div><div class="d-flex justify-content-between align-items-center mt-4 mb-2"><h6 class="mb-0">Riwayat Stok</h6><button class="btn btn-sm btn-outline-danger stock-pdf" data-id="'+r.item.id+'"><i class="bi bi-file-earmark-pdf"></i> PDF Audit</button></div><div class="table-responsive"><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><th>Sumber</th></tr></thead><tbody>'+rows+'</tbody></table></div>')}).fail(()=>AppAlert.toast('Detail barang tidak dapat dimuat.','error'))});
$(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:'Cetak Barcode',html:'<label class="form-label d-block text-start">Status barcode</label><select id="printStatus" class="form-select mb-3"><option value="available">Tersedia / Belum Dijual</option><option value="sold_out">Sudah Terjual / Habis</option><option value="all">Semua Status</option></select><label class="form-label d-block text-start">Jumlah kolom</label><select id="printColumn" class="form-select mb-3"><option value="2">2 Kolom</option><option value="3" selected>3 Kolom</option><option value="4">4 Kolom</option></select><label class="form-label d-block text-start">Salinan per barcode</label><input id="printCopies" type="number" min="1" max="20" value="1" class="form-control">',preConfirm:()=>({status:$('#printStatus').val(),column:$('#printColumn').val(),duplicat:$('#printCopies').val()}),confirmButtonText:'Preview'}).then(r=>{if(!r.isConfirmed)return;let url='<?=base_url('items/print_barcode')?>/'+id+'?column='+r.value.column+'&duplicat='+r.value.duplicat+'&status='+encodeURIComponent(r.value.status);$('#barcodePrintModal .modal-title').text('Preview Barcode');$('#barcodePrintModal iframe').attr('src',url);bootstrap.Modal.getOrCreateInstance(document.getElementById('barcodePrintModal')).show()})});
});
+1 -1
View File
@@ -3,7 +3,7 @@
<link rel="stylesheet" href="<?=base_url('assets/css/receivables-professional.css').'?v='.filemtime(FCPATH.'assets/css/receivables-professional.css')?>">
<section class="receivable-workspace">
<header class="invoice-hero"><div><span>SIKLUS PENJUALAN</span><h2>Piutang & Pembayaran</h2><p>Alokasikan pelunasan melalui akun Aktiva atau Passiva, termasuk pemindahbukuan simpanan customer.</p></div><a class="btn btn-light" href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i> Daftar Invoice</a></header>
<nav class="sales-section-nav" aria-label="Bagian Penjualan"><a href="<?=base_url('sales')?>"><i class="bi bi-speedometer2"></i><span>Ringkasan</span></a><a href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i><span>Invoice</span></a><a href="<?=base_url('sales/deliveries')?>"><i class="bi bi-truck"></i><span>Surat Jalan</span></a><a class="active" href="<?=base_url('receivables')?>"><i class="bi bi-cash-coin"></i><span>Piutang</span></a><a href="<?=base_url('sales/returns')?>"><i class="bi bi-arrow-counterclockwise"></i><span>Retur</span></a><a href="<?=base_url('customers')?>"><i class="bi bi-people"></i><span>Customer</span></a></nav>
<?php $this->load->view('sales/_section_nav',array('sales_section'=>'receivable','sales_summary'=>$summary)); ?>
<div class="receivable-grid">
<div class="card receivable-card">
+26
View File
@@ -0,0 +1,26 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
$section = isset($sales_section) ? (string) $sales_section : '';
$counts = isset($sales_summary) && is_array($sales_summary) ? $sales_summary : array();
$count = static function ($key) use ($counts) {
return max(0, (int) ($counts[$key] ?? 0));
};
$links = array(
array('key'=>'summary', 'url'=>'sales', 'icon'=>'bi-speedometer2', 'label'=>'Ringkasan', 'badge'=>0),
array('key'=>'invoice', 'url'=>'invoices', 'icon'=>'bi-receipt', 'label'=>'Invoice', 'badge'=>$count('draft')),
array('key'=>'delivery','url'=>'sales/deliveries', 'icon'=>'bi-truck', 'label'=>'Surat Jalan', 'badge'=>$count('delivery')),
array('key'=>'receivable','url'=>'receivables', 'icon'=>'bi-cash-coin', 'label'=>'Piutang', 'badge'=>$count('open_invoice')),
array('key'=>'return', 'url'=>'sales/returns', 'icon'=>'bi-arrow-counterclockwise','label'=>'Retur', 'badge'=>$count('return')),
array('key'=>'customer','url'=>'customers', 'icon'=>'bi-people', 'label'=>'Customer', 'badge'=>0),
);
?>
<nav class="sales-section-nav" aria-label="Bagian Penjualan">
<?php foreach ($links as $link): ?>
<a class="<?= $section === $link['key'] ? 'active' : '' ?>" href="<?= base_url($link['url']) ?>">
<i class="bi <?= html_escape($link['icon']) ?>"></i>
<span><?= html_escape($link['label']) ?></span>
<?php if ($link['badge'] > 0): ?><b><?= number_format($link['badge'], 0, ',', '.') ?></b><?php endif; ?>
</a>
<?php endforeach; ?>
</nav>
+1 -1
View File
@@ -11,7 +11,7 @@ function saleStatus($v){$m=array('draft'=>'Draft','submitted'=>'Menunggu Approva
<style>.sales-page[data-section="summary"] #salesDelivery,.sales-page[data-section="delivery"] .sales-metrics,.sales-page[data-section="delivery"] #salesReturn,.sales-page[data-section="return"] .sales-metrics,.sales-page[data-section="return"] #salesDelivery{display:none!important}</style>
<section class="sales-page" data-section="<?=html_escape($section)?>">
<header class="sales-hero"><div><span>SIKLUS PENJUALAN</span><h2><?=html_escape($pageTitle)?></h2><p><?=html_escape($pageIntro)?></p></div><?php if($section==='return'&&$p['create']):?><a class="btn btn-primary" href="<?=base_url('sales/deliveries')?>"><i class="bi bi-arrow-counterclockwise"></i> Ajukan dari Surat Jalan</a><?php elseif($section==='delivery'):?><a class="btn btn-outline-primary" href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i> Lihat Invoice</a><?php elseif($p['create']):?><a class="btn btn-primary" href="<?=base_url('invoices/create')?>"><i class="bi bi-plus-lg"></i> Buat Invoice</a><?php endif?></header>
<nav class="sales-section-nav" aria-label="Bagian Penjualan"><a class="<?=$section==='summary'?'active':''?>" href="<?=base_url('sales')?>"><i class="bi bi-speedometer2"></i><span>Ringkasan</span></a><a href="<?=base_url('invoices')?>"><i class="bi bi-receipt"></i><span>Invoice</span><?php if($summary['draft']):?><b><?=$summary['draft']?></b><?php endif?></a><a class="<?=$section==='delivery'?'active':''?>" href="<?=base_url('sales/deliveries')?>"><i class="bi bi-truck"></i><span>Surat Jalan</span><?php if($summary['delivery']):?><b><?=$summary['delivery']?></b><?php endif?></a><a href="<?=base_url('receivables')?>"><i class="bi bi-cash-coin"></i><span>Piutang</span><?php if($summary['open_invoice']):?><b><?=$summary['open_invoice']?></b><?php endif?></a><a class="<?=$section==='return'?'active':''?>" href="<?=base_url('sales/returns')?>"><i class="bi bi-arrow-counterclockwise"></i><span>Retur</span><?php if($summary['return']):?><b><?=$summary['return']?></b><?php endif?></a><a href="<?=base_url('customers')?>"><i class="bi bi-people"></i><span>Customer</span></a></nav>
<?php $this->load->view('sales/_section_nav',array('sales_section'=>$section,'sales_summary'=>$summary)); ?>
<div class="sales-metrics"><article><small>Penjualan bulan ini</small><strong><?=saleMoney($summary['month_sales'])?></strong></article><article><small>Invoice belum lunas</small><strong><?=$summary['open_invoice']?></strong><span><?=$summary['overdue']?> jatuh tempo</span></article><article><small>Draft / approval</small><strong><?=$summary['draft']?></strong><span><a href="<?=base_url('invoices')?>">Buka daftar invoice</a></span></article><article><small>Operasional</small><strong><?=$summary['delivery']+$summary['return']?></strong><span><?=$summary['delivery']?> pengiriman · <?=$summary['return']?> retur</span></article></div>
<div class="row g-3">
<div class="col-12" id="salesDelivery"><div class="card"><div class="card-header"><div><b>Pengiriman / Surat Jalan</b><small>Posting Surat Jalan adalah satu-satunya proses pengurangan stok fisik.</small></div><a class="btn btn-sm btn-outline-primary" href="<?=base_url('invoices')?>">Lihat Invoice</a></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Surat Jalan</th><th>Customer / Invoice</th><th>Tanggal</th><th>Gudang</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php foreach($deliveries as$d):?><tr><td><b><?=html_escape($d->delivery_no)?></b></td><td><?=html_escape($d->customer_name)?><small><?=html_escape($d->no_invoice?:'-')?></small></td><td><?=html_escape($d->delivery_date)?></td><td><?=html_escape($d->warehouse_name?:'-')?></td><td><span class="status status-<?=html_escape($d->status)?>"><?=saleStatus($d->status)?></span></td><td class="actions"><button class="btn btn-sm btn-outline-primary detail-delivery" data-id="<?=$d->id?>">Detail</button><?php if($p['export']):?><button class="btn btn-sm btn-outline-danger pdf-open" data-url="<?=base_url('sales/delivery_pdf/'.$d->id)?>">PDF</button><?php endif?><?php if($d->status==='draft'&&$p['post']):?><button class="btn btn-sm btn-success sale-action" data-url="<?=base_url('sales/post_delivery/'.$d->id)?>" data-confirm="Posting Surat Jalan dan keluarkan stok?">Post Pengiriman</button><?php endif?><?php if(in_array($d->status,array('posted','partially_returned'),true)&&$p['create']):?><button class="btn btn-sm btn-warning new-return" data-id="<?=$d->id?>">Ajukan Retur</button><?php endif?></td></tr><?php endforeach?><?php if(!$deliveries):?><tr><td colspan="6" class="empty">Belum ada dokumen pengiriman.</td></tr><?php endif?></tbody></table></div></div></div>
+62 -1
View File
@@ -1 +1,62 @@
<!doctype html><html><head><meta charset="utf-8"><style>@page{margin:35px}body{font-family:DejaVu Sans,sans-serif;font-size:11px;color:#20242c}.head{width:100%;margin-bottom:24px}.right{text-align:right}.box{border:1px solid #d8dee8;border-radius:8px;padding:14px}.grid{width:100%;border-collapse:collapse;margin-top:15px}.grid th,.grid td{border:1px solid #d8dee8;padding:8px}.grid th{background:#eef2f6}.total{font-size:18px;text-align:right;margin-top:16px}</style></head><body><table class="head"><tr><td><h2><?=html_escape($pdf_company->name??'Perusahaan')?></h2></td><td class="right"><h2>BUKTI PENERIMAAN</h2><b><?=html_escape($payment->payment_no)?></b></td></tr></table><div class="box"><b>Diterima dari:</b> <?=html_escape($payment->customer_name)?><br><b>Tanggal:</b> <?=$payment->tanggal?><br><b>Referensi:</b> <?=html_escape($payment->reference_no?:'-')?><br><b>Catatan:</b> <?=html_escape($payment->notes?:'-')?></div><h3>Alokasi Invoice</h3><table class="grid"><thead><tr><th>Invoice</th><th class="right">Nilai</th></tr></thead><tbody><?php foreach($allocations as$a):?><tr><td><?=html_escape($a->no_invoice)?></td><td class="right">Rp <?=number_format($a->amount,2,',','.')?></td></tr><?php endforeach?></tbody></table><h3>Sumber Kas/Bank</h3><table class="grid"><thead><tr><th>Akun</th><th class="right">Nilai</th></tr></thead><tbody><?php foreach($sources as$s):?><tr><td><?=html_escape($s->kode_akun.' - '.$s->nama_akun)?></td><td class="right">Rp <?=number_format($s->amount,2,',','.')?></td></tr><?php endforeach?></tbody></table><div class="total"><b>Total diterima: Rp <?=number_format($payment->jumlah,2,',','.')?></b></div></body></html>
<?php
$company=$pdf_company;
$currencyCode=isset($pdf_currency->code)?$pdf_currency->code:($company->base_currency??'IDR');
$currencySymbol=isset($pdf_currency->symbol)&&trim((string)$pdf_currency->symbol)!==''?$pdf_currency->symbol:'Rp';
$allocatedTotal=0;foreach($allocations as$allocation)$allocatedTotal+=(float)$allocation->amount;
$sourceTotal=0;foreach($sources as$source)$sourceTotal+=(float)$source->amount;
$advance=max(0,round((float)$payment->jumlah-$allocatedTotal,2));
function receiptMoney($value){return number_format((float)$value,2,',','.');}
function receiptDate($value){
if(!$value)return'-';$time=strtotime($value);if(!$time)return$value;
$months=array(1=>'Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember');
return date('j',$time).' '.$months[(int)date('n',$time)].' '.date('Y',$time);
}
function receiptWords($number){
$number=abs((int)$number);$base=array('','satu','dua','tiga','empat','lima','enam','tujuh','delapan','sembilan','sepuluh','sebelas');
if($number<12)return$base[$number];
if($number<20)return receiptWords($number-10).' belas';
if($number<100)return trim(receiptWords(intdiv($number,10)).' puluh '.receiptWords($number%10));
if($number<200)return trim('seratus '.receiptWords($number-100));
if($number<1000)return trim(receiptWords(intdiv($number,100)).' ratus '.receiptWords($number%100));
if($number<2000)return trim('seribu '.receiptWords($number-1000));
if($number<1000000)return trim(receiptWords(intdiv($number,1000)).' ribu '.receiptWords($number%1000));
if($number<1000000000)return trim(receiptWords(intdiv($number,1000000)).' juta '.receiptWords($number%1000000));
if($number<1000000000000)return trim(receiptWords(intdiv($number,1000000000)).' miliar '.receiptWords($number%1000000000));
return trim(receiptWords(intdiv($number,1000000000000)).' triliun '.receiptWords($number%1000000000000));
}
$amountWords=ucfirst(receiptWords((int)floor((float)$payment->jumlah))).' rupiah';
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Bukti Penerimaan <?=html_escape($payment->payment_no)?></title>
<style>
@page{margin:25px 30px 35px}body{font-family:DejaVu Sans,sans-serif;font-size:9px;color:#172033;line-height:1.45;margin:0}.header{width:100%;border-collapse:collapse;margin-bottom:16px}.header td{vertical-align:top}.brand{width:62%}.logo{float:left;width:58px;height:58px;object-fit:contain;margin-right:10px}.company-name{font-size:14px;font-weight:700;color:#172b4d;margin-bottom:2px}.company-legal{font-size:8px;color:#667085;margin-bottom:3px}.company-contact{font-size:8px;color:#475467;line-height:1.4}.document{text-align:right}.document-title{font-size:18px;font-weight:700;color:#172b4d;letter-spacing:.03em}.document-number{font-size:10px;font-weight:700;margin-top:3px}.status{display:inline-block;background:#e8f7ef;color:#087443;border:1px solid #a9dfc4;padding:3px 9px;margin-top:7px;font-size:7.5px;font-weight:700;letter-spacing:.08em}.accent{height:3px;background:#f47b20;margin-bottom:15px}.amount-panel{width:100%;border-collapse:collapse;background:#172b4d;color:#fff;margin-bottom:14px}.amount-panel td{padding:12px 14px}.amount-label{font-size:7.5px;letter-spacing:.12em;color:#d7e0ef}.amount-value{text-align:right;font-size:17px;font-weight:700}.info{width:100%;border-collapse:collapse;margin-bottom:15px}.info td{width:50%;vertical-align:top;border:1px solid #d9e0ea;padding:10px 11px}.info td+td{border-left:0}.eyebrow{font-size:7px;color:#667085;font-weight:700;letter-spacing:.1em;text-transform:uppercase;margin-bottom:4px}.customer{font-size:11px;font-weight:700;color:#172b4d;margin-bottom:3px}.meta{width:100%;border-collapse:collapse}.meta td{border:0!important;padding:1px 0!important;font-size:8.5px}.meta .label{width:42%;color:#667085}.meta .value{text-align:right;font-weight:600}.section{margin-top:14px}.section-title{font-size:10px;font-weight:700;color:#172b4d;margin:0 0 6px}.section-subtitle{font-size:7.5px;color:#667085;font-weight:400}.grid{width:100%;border-collapse:collapse}.grid th{background:#f1f4f8;color:#344054;font-size:7px;letter-spacing:.06em;text-transform:uppercase;text-align:left}.grid th,.grid td{border:1px solid #d9e0ea;padding:6px 7px}.grid .number{width:24px;text-align:center}.right{text-align:right!important}.center{text-align:center!important}.strong{font-weight:700;color:#172b4d}.empty{text-align:center;color:#667085;padding:12px!important}.summary{width:46%;margin:8px 0 0 auto;border-collapse:collapse}.summary td{padding:3px 2px}.summary td:last-child{text-align:right}.summary .grand td{border-top:1px solid #98a2b3;padding-top:6px;font-size:10px;font-weight:700;color:#172b4d}.words{border-left:3px solid #f47b20;background:#fff8f1;padding:8px 10px;margin-top:13px;color:#344054}.words-label{font-size:7px;color:#a14b0b;letter-spacing:.08em;font-weight:700;text-transform:uppercase}.notes{margin-top:10px;padding:8px 10px;border:1px solid #d9e0ea;background:#fafbfc}.signatures{width:100%;border-collapse:collapse;margin-top:27px;text-align:center}.signatures td{width:50%;height:72px;vertical-align:top;color:#475467}.signature-name{font-weight:700;color:#172b4d;border-top:1px solid #98a2b3;padding-top:4px;margin:42px auto 0;width:70%}.legal-note{font-size:7.5px;color:#667085;text-align:center;border-top:1px solid #e4e7ec;padding-top:7px;margin-top:12px}.footer{position:fixed;left:0;right:0;bottom:-22px;font-size:7px;color:#98a2b3}.footer-right{float:right}
</style>
</head>
<body>
<table class="header"><tr><td class="brand">
<?php if(!empty($pdf_logo_data)):?><img class="logo" src="<?=$pdf_logo_data?>" alt="Logo"><?php endif?>
<div class="company-name"><?=html_escape($company->name??'Perusahaan')?></div>
<?php if(!empty($company->legal_name)&&$company->legal_name!==$company->name):?><div class="company-legal"><?=html_escape($company->legal_name)?></div><?php endif?>
<div class="company-contact"><?=nl2br(html_escape($company->address?:'-'))?><br>Telp: <?=html_escape($company->phone?:'-')?><?php if(!empty($company->email)):?> &nbsp;|&nbsp; <?=html_escape($company->email)?><?php endif?></div>
</td><td class="document"><div class="document-title">BUKTI PENERIMAAN</div><div class="document-number"><?=html_escape($payment->payment_no)?></div><span class="status">PEMBAYARAN TERCATAT</span></td></tr></table>
<div class="accent"></div>
<table class="amount-panel"><tr><td><div class="amount-label">TOTAL DANA DITERIMA</div><div><?=html_escape($currencyCode)?> &bull; <?=receiptDate($payment->tanggal)?></div></td><td class="amount-value"><?=html_escape($currencySymbol)?> <?=receiptMoney($payment->jumlah)?></td></tr></table>
<table class="info"><tr><td><div class="eyebrow">Diterima dari</div><div class="customer"><?=html_escape($payment->customer_name)?></div><div><?=nl2br(html_escape($payment->customer_address?:'-'))?></div><?php if(!empty($payment->customer_phone)||!empty($payment->customer_email)):?><div style="margin-top:3px;color:#667085"><?=html_escape($payment->customer_phone?:'-')?><?php if(!empty($payment->customer_email)):?> &bull; <?=html_escape($payment->customer_email)?><?php endif?></div><?php endif?></td><td><div class="eyebrow">Informasi penerimaan</div><table class="meta"><tr><td class="label">Tanggal</td><td class="value"><?=receiptDate($payment->tanggal)?></td></tr><tr><td class="label">Referensi</td><td class="value"><?=html_escape($payment->reference_no?:'-')?></td></tr><tr><td class="label">Metode</td><td class="value"><?=html_escape($payment->metode==='multi_account'?'Multi Akun':ucwords(str_replace('_',' ',$payment->metode?:'Pembayaran'))) ?></td></tr><tr><td class="label">Status</td><td class="value">TERCATAT</td></tr></table></td></tr></table>
<div class="section"><div class="section-title">Alokasi Pembayaran <span class="section-subtitle">&mdash; rincian tagihan yang dilunasi</span></div><table class="grid"><thead><tr><th class="number">No.</th><th>Nomor Invoice</th><th>Tanggal Invoice</th><th>Jatuh Tempo</th><th class="right">Nilai Dialokasikan</th></tr></thead><tbody><?php foreach($allocations as$i=>$allocation):?><tr><td class="number"><?=$i+1?></td><td class="strong"><?=html_escape($allocation->no_invoice)?></td><td><?=receiptDate($allocation->invoice_date)?></td><td><?=receiptDate($allocation->jatuh_tempo)?></td><td class="right strong"><?=html_escape($currencySymbol)?> <?=receiptMoney($allocation->amount)?></td></tr><?php endforeach?><?php if(!$allocations):?><tr><td colspan="5" class="empty">Pembayaran belum dialokasikan ke invoice tertentu.</td></tr><?php endif?></tbody></table><?php if($advance>.001):?><table class="summary"><tr><td>Alokasi invoice</td><td><?=receiptMoney($allocatedTotal)?></td></tr><tr><td>Uang muka customer</td><td><?=receiptMoney($advance)?></td></tr><tr class="grand"><td>Total diterima</td><td><?=html_escape($currencySymbol)?> <?=receiptMoney($payment->jumlah)?></td></tr></table><?php endif?></div>
<div class="section"><div class="section-title">Sumber Penerimaan <span class="section-subtitle">&mdash; akun tujuan pencatatan dana</span></div><table class="grid"><thead><tr><th class="number">No.</th><th>Kode Akun</th><th>Nama Akun Kas/Bank</th><th class="right">Nominal</th></tr></thead><tbody><?php foreach($sources as$i=>$source):?><tr><td class="number"><?=$i+1?></td><td><?=html_escape($source->kode_akun)?></td><td class="strong"><?=html_escape($source->nama_akun)?></td><td class="right strong"><?=html_escape($currencySymbol)?> <?=receiptMoney($source->amount)?></td></tr><?php endforeach?><?php if(!$sources):?><tr><td colspan="4" class="empty">Sumber penerimaan tidak tersedia.</td></tr><?php endif?></tbody><tfoot><tr><td colspan="3" class="right strong">TOTAL PENERIMAAN</td><td class="right strong"><?=html_escape($currencySymbol)?> <?=receiptMoney($sourceTotal)?></td></tr></tfoot></table></div>
<div class="words"><div class="words-label">Terbilang</div><b><?=html_escape($amountWords)?></b></div>
<div class="notes"><div class="eyebrow">Catatan pembayaran</div><?=nl2br(html_escape($payment->notes?:'Tidak ada catatan tambahan.'))?></div>
<table class="signatures"><tr><td>Dicatat oleh<div class="signature-name"><?=html_escape($payment->creator_name?:'-')?></div><div>Finance / Administrasi</div></td><td>Pihak Pembayar<div class="signature-name"><?=html_escape($payment->customer_name)?></div><div>Customer</div></td></tr></table>
<div class="legal-note">Dokumen ini merupakan bukti penerimaan pembayaran yang tercatat pada sistem <?=html_escape($company->name??'perusahaan')?> dengan nomor referensi unik <?=html_escape($payment->payment_no)?>.</div>
<div class="footer"><span>Dicetak <?=date('d-m-Y H:i')?> &bull; <?=html_escape($payment->payment_no)?></span><span class="footer-right"><?=html_escape($company->company_code??'')?></span></div>
</body>
</html>