hr dan payroll
This commit is contained in:
@@ -295,7 +295,10 @@ class SalesService
|
||||
$invoice=$this->invoiceForEditor($invoiceId);$start=max(0,(int)$start);$length=max(1,min(100,(int)$length));$search=trim((string)$search);
|
||||
$base=function()use($invoiceId,$search){$this->CI->db->from('invoice_details d')->join('items it','it.id=d.items_id','left')->join('accounts a','a.id=d.account_id','left')->join('accounts ca','ca.id=d.counter_account_id','left')->where(array('d.invoice_id'=>(int)$invoiceId,'d.company_id'=>$this->company()));if($search!=='')$this->CI->db->group_start()->like('d.nama_item',$search)->or_like('d.specification',$search)->or_like('d.keterangan',$search)->or_like('it.kode_detail',$search)->or_like('a.nama_akun',$search)->or_like('ca.nama_akun',$search)->group_end();};
|
||||
$base();$filtered=(int)$this->CI->db->count_all_results();$total=(int)$this->CI->db->where(array('invoice_id'=>(int)$invoiceId,'company_id'=>$this->company()))->count_all_results('invoice_details');
|
||||
$base();$rows=$this->CI->db->select("d.*,it.kode_detail item_code,a.kode_akun,a.nama_akun,ca.kode_akun counter_kode_akun,ca.nama_akun counter_nama_akun,(SELECT GROUP_CONCAT(CONCAT(ib.barcode,IF(NULLIF(ib.serial_number,'') IS NULL,'',CONCAT(' / ',ib.serial_number))) ORDER BY ilb.id SEPARATOR '||') FROM invoice_line_barcodes ilb JOIN item_barcodes ib ON ib.id=ilb.barcode_id WHERE ilb.invoice_detail_id=d.id) barcode_labels",false)->order_by('d.service_date','ASC')->order_by('d.id','ASC')->limit($length,$start)->get()->result();
|
||||
$base();$rows=$this->CI->db->select("d.*,it.kode_detail item_code,it.harga_jual item_sale_price,kb.tracking_type,kb.unit item_unit,w.nama warehouse_name,a.kode_akun,a.nama_akun,ca.kode_akun counter_kode_akun,ca.nama_akun counter_nama_akun,(SELECT GROUP_CONCAT(CONCAT(ib.barcode,IF(NULLIF(ib.serial_number,'') IS NULL,'',CONCAT(' / ',ib.serial_number))) ORDER BY ilb.id SEPARATOR '||') FROM invoice_line_barcodes ilb JOIN item_barcodes ib ON ib.id=ilb.barcode_id WHERE ilb.invoice_detail_id=d.id) barcode_labels",false)->join('kode_barang kb','kb.id=it.kode_id','left')->join('warehouses w','w.id=d.warehouse_id','left')->order_by('d.service_date','ASC')->order_by('d.id','ASC')->limit($length,$start)->get()->result();
|
||||
$lineIds=array_map(function($row){return(int)$row->id;},$rows);$allocations=array();
|
||||
if($lineIds){$bars=$this->CI->db->select('ilb.invoice_detail_id,ilb.barcode_id id,ib.barcode,ib.serial_number,ilb.qty,(ib.qty_sisa-COALESCE(ib.reserved_qty,0)-COALESCE(ib.return_reserved_qty,0)+ilb.qty) available,kb.tracking_type',false)->from('invoice_line_barcodes ilb')->join('item_barcodes ib','ib.id=ilb.barcode_id')->join('items it','it.id=ib.item_id')->join('kode_barang kb','kb.id=it.kode_id','left')->where('ilb.company_id',$this->company())->where_in('ilb.invoice_detail_id',$lineIds)->order_by('ilb.id')->get()->result();foreach($bars as$bar)$allocations[(int)$bar->invoice_detail_id][]=$bar;}
|
||||
foreach($rows as$row)$row->barcode_allocations=$allocations[(int)$row->id]??array();
|
||||
return array('invoice'=>$invoice,'rows'=>$rows,'recordsTotal'=>$total,'recordsFiltered'=>$filtered,'summary'=>array('line_count'=>$total,'subtotal'=>(float)$invoice->subtotal_before_tax,'discount'=>(float)$invoice->discount_total,'tax'=>(float)$invoice->tax_total,'total'=>(float)$invoice->total));
|
||||
}
|
||||
public function updateInvoiceHeader($id,array$data,$user)
|
||||
@@ -326,26 +329,51 @@ class SalesService
|
||||
$this->CI->db->where(array('invoice_detail_id'=>(int)$lineId,'company_id'=>$this->company()))->delete('invoice_line_barcodes');
|
||||
}
|
||||
|
||||
public function updateNonInventoryLine($invoiceId,$lineId,array$data,$user)
|
||||
public function updateInvoiceLine($invoiceId,$lineId,array$data,$user)
|
||||
{
|
||||
return $this->CI->transactionservice->run(function()use($invoiceId,$lineId,$data,$user){
|
||||
$invoice=$this->lockInvoice($invoiceId,'draft');$this->assertDraftOwner($invoice,$user);
|
||||
$line=$this->CI->db->query('SELECT * FROM invoice_details WHERE id=? AND invoice_id=? AND company_id=? FOR UPDATE',array((int)$lineId,$invoice->id,$this->company()))->row();
|
||||
if(!$line||!in_array($line->line_type,array('service','savings'),true))throw new BusinessException('Hanya item Jasa/Nonbarang atau Tabungan pada Draft yang dapat diedit.');
|
||||
$type=$line->line_type;$description=trim((string)($data['description']??''));if($description==='')throw new BusinessException('Deskripsi item wajib diisi.');
|
||||
if(!$line||!in_array($line->line_type,array('inventory','service','savings'),true))throw new BusinessException('Item invoice ini tidak dapat diedit.');
|
||||
$type=$line->line_type;if(isset($data['line_type'])&&$data['line_type']!==''&&$data['line_type']!==$type)throw new BusinessException('Jenis item tidak dapat diubah saat edit. Hapus lalu tambahkan kembali bila jenis transaksi berubah.');
|
||||
if($type==='inventory')$this->assertInventoryInvoiceLineEditable($line);
|
||||
$description=$type==='inventory'?'':trim((string)($data['description']??''));if($type!=='inventory'&&$description==='')throw new BusinessException('Deskripsi item wajib diisi.');
|
||||
$account=$this->tenant('accounts',$data['account_id']??0,$type==='savings'?'Akun simpanan customer':'Akun pendapatan');
|
||||
if(isset($account->is_active)&&!(int)$account->is_active||isset($account->allow_posting)&&!(int)$account->allow_posting)throw new BusinessException('Akun item tidak aktif atau tidak dapat diposting.');
|
||||
$counter=null;if($type==='service'&&(string)$account->tipe!=='revenue')throw new BusinessException('Jasa harus menggunakan akun pendapatan.');
|
||||
$mayUseHidden=function_exists('is_master_admin_user')&&is_master_admin_user();if((isset($account->is_active)&&(int)$account->is_active!==1)||(isset($account->allow_posting)&&(int)$account->allow_posting!==1)||(!$mayUseHidden&&isset($account->is_hidden)&&(int)$account->is_hidden===1))throw new BusinessException('Akun item tidak aktif atau tidak dapat diposting.');
|
||||
$counter=null;if(in_array($type,array('inventory','service'),true)&&(string)$account->tipe!=='revenue')throw new BusinessException('Barang dan jasa harus menggunakan akun pendapatan.');
|
||||
if($type==='savings'&&(string)$account->tipe!=='liability')throw new BusinessException('Tabungan harus menggunakan akun Passiva/Kewajiban.');
|
||||
$qty=$type==='savings'?1:$this->qty($data['qty']??0);$price=$this->money($data['unit_price']??0);$discount=$type==='savings'?0:max(0,$this->money($data['discount_amount']??0));if($discount>$qty*$price)throw new BusinessException('Diskon tidak boleh melebihi nilai bruto item.');
|
||||
$tax=$type==='service'?max(0,(float)str_replace(',','.',(string)($data['tax_rate']??0))):0;$net=max(0,$qty*$price-$discount);$taxAmount=round($net*$tax/100,2);$service=$data['service_date']??$line->service_date;if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',(string)$service))throw new BusinessException('Tanggal transaksi item tidak valid.');
|
||||
$update=$this->columns('invoice_details',array('service_date'=>$service,'tanggal'=>$service,'account_id'=>$account->id,'counter_account_id'=>$counter?$counter->id:null,'nama_item'=>$description,'unit'=>trim((string)($data['unit']??$line->unit)),'qty'=>$qty,'harga'=>$price,'discount_amount'=>$discount,'tax_rate'=>$tax,'tax_amount'=>$taxAmount,'net_amount'=>$net,'subtotal'=>$net+$taxAmount,'subtotal_asli'=>$net+$taxAmount,'keterangan'=>trim((string)($data['notes']??''))));
|
||||
$item=null;$warehouse=null;$kb=null;if($type==='inventory'){$item=$this->tenant('items',$data['item_id']??0,'Barang');$warehouse=$this->tenant('warehouses',$data['warehouse_id']??0,'Gudang');if($item->status!=='active')throw new BusinessException('Barang '.$item->nama_barang.' belum aktif.');$kb=$this->CI->db->get_where('kode_barang',array('id'=>$item->kode_id))->row();}
|
||||
$qty=$type==='savings'?1:$this->qty($data['qty']??0);$price=$this->money($data['unit_price']??($item?$item->harga_jual:0));$discount=$type==='savings'?0:max(0,$this->money($data['discount_amount']??0));if($discount>$qty*$price)throw new BusinessException('Diskon tidak boleh melebihi nilai bruto item.');
|
||||
$tax=$type==='savings'?0:max(0,(float)str_replace(',','.',(string)($data['tax_rate']??0)));$net=max(0,$qty*$price-$discount);$taxAmount=round($net*$tax/100,2);$service=$data['service_date']??$line->service_date;if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',(string)$service))throw new BusinessException('Tanggal transaksi item tidak valid.');
|
||||
$requestedBarcodes=(array)($data['barcodes']??array());if($type==='inventory'&&!$requestedBarcodes)throw new BusinessException('Pilih minimal satu barcode untuk barang gudang.');
|
||||
$beforeBars=$this->CI->db->where(array('invoice_detail_id'=>$line->id,'company_id'=>$this->company()))->order_by('id')->get('invoice_line_barcodes')->result_array();$before=array('line'=>$line,'barcodes'=>$beforeBars);
|
||||
$lineJournalMode=true;$otherLines=$this->CI->db->select('id,line_type,subtotal,total_hpp_barang')->where(array('invoice_id'=>$invoice->id,'company_id'=>$this->company()))->where('id !=',$line->id)->get('invoice_details')->result();foreach($otherLines as$other)if($other->line_type!=='savings'&&((float)$other->subtotal>0||(float)$other->total_hpp_barang>0)){$otherJournal=$this->initialLineJournal($other->id);if(!$otherJournal||$otherJournal->is_reversed||$otherJournal->status!=='posted'){$lineJournalMode=false;break;}}
|
||||
$this->reverseInvoiceLineInitialJournal($line,$user,'Item Draft invoice '.$invoice->no_invoice.' diedit');
|
||||
$this->CI->db->where(array('id'=>$line->id,'invoice_id'=>$invoice->id,'company_id'=>$this->company()))->update('invoice_details',$update);$updated=$this->CI->db->get_where('invoice_details',array('id'=>$line->id,'company_id'=>$this->company()))->row();$customer=$this->tenant('customers',$invoice->customer_id,'Customer');if($lineJournalMode)$this->postInvoiceLineInitialJournal($invoice,$line->id,$updated,null,array(),$customer,$user);$this->refreshInvoiceTotals($invoice->id);$this->CI->auditservice->record('sales','invoice_detail',$line->id,'update',$line,$update,$user);return$line->id;
|
||||
$cleanBarcodes=array();if($type==='inventory'){$this->detachDraftDeliveryLine($line);$this->releaseInvoiceLineBarcodes($line->id);$seen=array();$barcodeQty=0;foreach($requestedBarcodes as$requested){$barcodeId=(int)($requested['id']??$requested['barcode_id']??0);if(!$barcodeId||isset($seen[$barcodeId]))throw new BusinessException('Barcode tidak boleh dipilih dua kali pada satu item.');$seen[$barcodeId]=true;$barcodeQtyValue=$this->qty($requested['qty']??1);$barcode=$this->CI->db->query("SELECT * FROM item_barcodes WHERE id=? AND item_id=? AND warehouse_id=? AND status IN('available','reserved') FOR UPDATE",array($barcodeId,$item->id,$warehouse->id))->row();if(!$barcode)throw new BusinessException('Barcode barang '.$item->nama_barang.' tidak tersedia pada gudang terpilih.');if($this->CI->db->where(array('invoice_id'=>$invoice->id,'barcode_id'=>$barcodeId))->count_all_results('invoice_line_barcodes'))throw new BusinessException('Barcode '.$barcode->barcode.' sudah dipakai item lain pada invoice ini.');if($kb&&strtoupper((string)$kb->tracking_type)==='UNIT'){$used=(int)$this->CI->db->select('COUNT(*) total',false)->from('invoice_line_barcodes used')->join('invoice_details used_detail','used_detail.id=used.invoice_detail_id AND used_detail.stock_posted_at IS NULL')->join('invoices used_invoice','used_invoice.id=used.invoice_id')->where(array('used.barcode_id'=>$barcode->id,'used_invoice.company_id'=>$this->company()))->where('used_invoice.deleted_at IS NULL',null,false)->where("COALESCE(used_invoice.status,'') <> 'cancelled'",null,false)->get()->row()->total;if($used>0)throw new BusinessException('Barcode/Serial '.$barcode->barcode.' sedang digunakan invoice lain.');if(abs($barcodeQtyValue-1)>.0001)throw new BusinessException('Barcode UNIT harus memiliki qty 1.');}$available=(float)$barcode->qty_sisa-(float)$barcode->reserved_qty-(float)($barcode->return_reserved_qty??0);if($barcodeQtyValue>$available+.0001)throw new BusinessException('Qty barcode '.$barcode->barcode.' melebihi stok tersedia.');$reservedAfter=(float)$barcode->reserved_qty+$barcodeQtyValue;$barcodeUpdate=array('reserved_qty'=>$reservedAfter,'version'=>(int)$barcode->version+1);if($kb&&strtoupper((string)$kb->tracking_type)==='UNIT')$barcodeUpdate['status']='reserved';$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',$barcodeUpdate);if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode sedang diproses pengguna lain. Silakan ulangi.');$barcode->reserved_qty=$reservedAfter;$cleanBarcodes[]=array('row'=>$barcode,'qty'=>$barcodeQtyValue);$barcodeQty+=$barcodeQtyValue;}if(abs($barcodeQty-$qty)>.0001)throw new BusinessException('Total qty barcode harus sama dengan qty barang '.$item->nama_barang.'.');}
|
||||
$update=$this->columns('invoice_details',array('service_date'=>$service,'tanggal'=>$service,'account_id'=>$account->id,'counter_account_id'=>$counter?$counter->id:null,'items_id'=>$item?$item->id:null,'warehouse_id'=>$warehouse?$warehouse->id:null,'delivery_line_id'=>null,'nama_item'=>$type==='inventory'?$item->nama_barang:$description,'unit'=>$kb?$kb->unit:trim((string)($data['unit']??$line->unit)),'qty'=>$qty,'harga'=>$price,'discount_amount'=>$discount,'tax_rate'=>$tax,'tax_amount'=>$taxAmount,'net_amount'=>$net,'subtotal'=>$net+$taxAmount,'subtotal_asli'=>$net+$taxAmount,'total_hpp_barang'=>$item?round($qty*(float)$item->harga_beli,2):0,'keterangan'=>trim((string)($data['notes']??''))));
|
||||
$this->CI->db->where(array('id'=>$line->id,'invoice_id'=>$invoice->id,'company_id'=>$this->company()))->update('invoice_details',$update);if($type==='inventory')foreach($cleanBarcodes as$bar)$this->CI->db->insert('invoice_line_barcodes',array('company_id'=>$this->company(),'invoice_id'=>$invoice->id,'invoice_detail_id'=>$line->id,'sales_delivery_id'=>null,'sales_delivery_line_id'=>null,'barcode_id'=>$bar['row']->id,'qty'=>$bar['qty'],'created_at'=>date('Y-m-d H:i:s')));
|
||||
$updated=$this->CI->db->get_where('invoice_details',array('id'=>$line->id,'company_id'=>$this->company()))->row();$customer=$this->tenant('customers',$invoice->customer_id,'Customer');if($lineJournalMode)$this->postInvoiceLineInitialJournal($invoice,$line->id,$updated,$item,$cleanBarcodes,$customer,$user);$this->refreshInvoiceTotals($invoice->id);$this->updateInvoiceDeliveryStatus($invoice->id);$afterBars=$this->CI->db->where(array('invoice_detail_id'=>$line->id,'company_id'=>$this->company()))->order_by('id')->get('invoice_line_barcodes')->result_array();$this->CI->auditservice->record('sales','invoice_detail',$line->id,'update',$before,array('line'=>$updated,'barcodes'=>$afterBars),$user);return$line->id;
|
||||
});
|
||||
}
|
||||
|
||||
/** Kompatibilitas pemanggil lama; seluruh validasi sekarang terpusat. */
|
||||
public function updateNonInventoryLine($invoiceId,$lineId,array$data,$user){return$this->updateInvoiceLine($invoiceId,$lineId,$data,$user);}
|
||||
|
||||
private function assertInventoryInvoiceLineEditable($line)
|
||||
{
|
||||
if(!empty($line->stock_posted_at)||!empty($line->stock_log_id))throw new BusinessException('Barang sudah memiliki mutasi stok dan tidak dapat diedit. Gunakan retur atau koreksi persediaan.');
|
||||
$deliveryLineId=(int)$line->delivery_line_id;if($deliveryLineId){$delivery=$this->CI->db->select('l.stock_posted_at,d.status')->from('sales_delivery_lines l')->join('sales_deliveries d','d.id=l.sales_delivery_id')->where(array('l.id'=>$deliveryLineId,'d.company_id'=>$this->company()))->get()->row();if(!$delivery||$delivery->status!=='draft'||!empty($delivery->stock_posted_at))throw new BusinessException('Surat Jalan barang sudah diproses. Gunakan retur/koreksi agar stok dan audit trail tetap benar.');}
|
||||
$stockCount=(int)$this->CI->db->query("SELECT COUNT(*) total FROM stock_logs WHERE ((ref_type='invoice_details' AND ref_id=?) OR (? > 0 AND ref_type='sales_delivery' AND ref_id=?))",array((int)$line->id,$deliveryLineId,$deliveryLineId))->row()->total;
|
||||
$movementCount=(int)$this->CI->db->query("SELECT COUNT(*) total FROM item_movements WHERE ((reference_type IN('invoice','invoice_details') AND reference_id=?) OR (? > 0 AND reference_type='sales_delivery' AND reference_id=?))",array((int)$line->id,$deliveryLineId,$deliveryLineId))->row()->total;
|
||||
$ledgerCount=(int)$this->CI->db->query("SELECT COUNT(*) total FROM inventory_ledger WHERE ((document_type IN('invoice','invoice_details') AND document_line_id=?) OR (? > 0 AND document_type='sales_delivery' AND document_line_id=?))",array((int)$line->id,$deliveryLineId,$deliveryLineId))->row()->total;
|
||||
if($stockCount||$movementCount||$ledgerCount)throw new BusinessException('Barang sudah terhubung dengan stock log atau item movement. Edit dihentikan; gunakan retur/koreksi persediaan.');
|
||||
}
|
||||
|
||||
private function detachDraftDeliveryLine($line)
|
||||
{
|
||||
if(!(int)$line->delivery_line_id)return;$deliveryLine=$this->CI->db->select('l.sales_delivery_id,l.stock_posted_at,d.status')->from('sales_delivery_lines l')->join('sales_deliveries d','d.id=l.sales_delivery_id')->where(array('l.id'=>(int)$line->delivery_line_id,'d.company_id'=>$this->company()))->get()->row();if(!$deliveryLine||$deliveryLine->status!=='draft'||!empty($deliveryLine->stock_posted_at))throw new BusinessException('Item tidak dapat dilepas dari Surat Jalan yang sudah diproses.');$this->CI->db->where('sales_delivery_line_id',(int)$line->delivery_line_id)->delete('sales_delivery_barcodes');$this->CI->db->where('id',(int)$line->delivery_line_id)->delete('sales_delivery_lines');if(!$this->CI->db->where('sales_delivery_id',(int)$deliveryLine->sales_delivery_id)->count_all_results('sales_delivery_lines'))$this->CI->db->where(array('id'=>(int)$deliveryLine->sales_delivery_id,'company_id'=>$this->company()))->update('sales_deliveries',array('status'=>'cancelled'));
|
||||
}
|
||||
|
||||
public function updateDelivery($id,array$data,array$attachments,$user)
|
||||
{
|
||||
return$this->CI->transactionservice->run(function()use($id,$data,$attachments,$user){$d=$this->CI->db->query('SELECT * FROM sales_deliveries WHERE id=? AND company_id=? FOR UPDATE',array((int)$id,$this->company()))->row();if(!$d||$d->status!=='draft')throw new BusinessException('Hanya Surat Jalan Draft yang dapat dilengkapi.');$date=$data['delivery_date']??$d->delivery_date;if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date))throw new BusinessException('Tanggal pengiriman tidak valid.');$update=array('delivery_date'=>$date,'address'=>trim((string)($data['address']??'')),'driver_name'=>trim((string)($data['driver_name']??'')),'vehicle_no'=>trim((string)($data['vehicle_no']??'')),'courier'=>trim((string)($data['courier']??'')),'tracking_no'=>trim((string)($data['tracking_no']??'')),'recipient_name'=>trim((string)($data['recipient_name']??'')),'notes'=>trim((string)($data['notes']??'')),'version'=>(int)$d->version+1);$this->CI->db->where('id',$d->id)->update('sales_deliveries',$update);foreach($attachments as$a)$this->insertAttachment('sales_delivery',$d->id,$a,$user);$this->CI->auditservice->record('sales','sales_delivery',$d->id,'update',$d,$update,$user);return$d->id;});
|
||||
|
||||
Reference in New Issue
Block a user