Files
accounting_dev_v2/application/controllers/Purchases.php
T
2026-09-11 16:03:00 +07:00

96 lines
25 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Purchases extends MY_Controller
{
public function __construct(){parent::__construct();$this->load->library('PurchaseService');}
private function uid(){return(int)$this->session->userdata('user_id');}private function role(){if($id=(int)$this->session->userdata('role_id'))return$id;$u=$this->db->select('role_id')->get_where('users',array('id'=>$this->uid()))->row();return$u?(int)$u->role_id:0;}
private function departmentName($id){$d=$this->db->get_where('k_departments',array('id'=>(int)$id,'is_active'=>1))->row();return$d?$d->department_name:'';}
public function index(){$data=array('active_menu'=>'purchases','requests'=>$this->db->order_by('id','DESC')->get('purchase_requests')->result(),'orders'=>$this->db->select('purchase_orders.*,suppliers.name')->from('purchase_orders')->join('suppliers','suppliers.id=purchase_orders.supplier_id')->order_by('purchase_orders.id','DESC')->get()->result(),'receipts'=>$this->db->select('goods_receipts.*,purchase_orders.po_no,suppliers.name')->from('goods_receipts')->join('purchase_orders','purchase_orders.id=goods_receipts.purchase_order_id')->join('suppliers','suppliers.id=goods_receipts.supplier_id')->order_by('goods_receipts.id','DESC')->get()->result(),'bills'=>$this->db->select("supplier_invoices.*,suppliers.name,(SELECT po.payment_timing FROM supplier_invoice_lines sil JOIN purchase_order_lines pol ON pol.id=sil.po_line_id JOIN purchase_orders po ON po.id=pol.purchase_order_id WHERE sil.supplier_invoice_id=supplier_invoices.id LIMIT 1) po_payment_timing,(SELECT po.paid_amount FROM supplier_invoice_lines sil JOIN purchase_order_lines pol ON pol.id=sil.po_line_id JOIN purchase_orders po ON po.id=pol.purchase_order_id WHERE sil.supplier_invoice_id=supplier_invoices.id LIMIT 1) po_paid_amount,(SELECT po.total FROM supplier_invoice_lines sil JOIN purchase_order_lines pol ON pol.id=sil.po_line_id JOIN purchase_orders po ON po.id=pol.purchase_order_id WHERE sil.supplier_invoice_id=supplier_invoices.id LIMIT 1) po_total",false)->from('supplier_invoices')->join('suppliers','suppliers.id=supplier_invoices.supplier_id')->order_by('supplier_invoices.id','DESC')->get()->result(),'suppliers'=>$this->db->where('is_active',1)->order_by('name')->get('suppliers')->result(),'catalogs'=>$this->db->order_by('kode_barang')->get('kode_barang')->result(),'warehouses'=>$this->db->order_by('nama')->get('warehouses')->result(),'asset_locations'=>$this->db->order_by('nama')->get('lokasi_asset')->result(),'asset_accounts'=>$this->db->where('tipe','asset')->where('allow_posting',1)->order_by('kode_akun')->get('accounts')->result());$step=$this->input->get('step')?:'request';$this->load->view('partials/header',$data);$this->load->view($step==='request'?'purchases/index':'purchases/workflow',$data);$this->load->view('partials/footer');}
private function requestDocument($id){$header=$this->db->select('pr.*,d.code department_code,d.department_name,creator.nama creator_name,creator.username creator_username,approver.nama approver_name,approver.username approver_username')->from('purchase_requests pr')->join('k_departments d','d.id=pr.department_id','left')->join('users creator','creator.id=pr.requested_by','left')->join('users approver','approver.id=pr.approved_by','left')->where('pr.id',(int)$id)->get()->row();if(!$header||($header->status==='draft'&&(int)$header->requested_by!==$this->uid()))return null;$header->applicant_display=((int)$header->applicant_is_creator===1)?($header->creator_name?:$header->creator_username?:'Pengguna #'.$header->requested_by):$header->applicant_name;$lines=$this->db->select('l.*,k.kode_barang,k.nama master_name')->from('purchase_request_lines l')->join('kode_barang k','k.id=l.kode_barang_id','left')->where('l.purchase_request_id',$header->id)->order_by('l.id')->get()->result();return array('request'=>$header,'lines'=>$lines);}
public function request_detail($id){$data=$this->requestDocument($id);if(!$data)return json_response(false,'Purchase Request tidak ditemukan.',array(),404);return json_response(true,'Detail Purchase Request.',$data);}
public function request_pdf($id){
$data=$this->requestDocument($id);if(!$data)show_404();require_once APPPATH.'third_party/fpdf/fpdf.php';$r=$data['request'];
$creator=$r->creator_name?:$r->creator_username?:'Pengguna #'.$r->requested_by;$requester=$r->applicant_display?:$creator;$approver=$r->approver_name?:$r->approver_username?:'-';
$pdf=new FPDF('P','mm','A4');$pdf->SetMargins(15,15,15);$pdf->AddPage();$pdf->SetFont('Arial','B',15);$pdf->Cell(0,8,'PERMINTAAN PEMBELIAN',0,1,'C');$pdf->SetFont('Arial','',9);$pdf->Cell(0,6,'Nomor: '.$r->request_no,0,1,'C');$pdf->Ln(5);
$pdf->SetFont('Arial','',10);foreach(array('Tanggal'=>$r->request_date,'Dibutuhkan'=>$r->needed_date,'Departemen'=>trim(($r->department_code?:'').' '.($r->department_name?:$r->department)),'Status'=>strtoupper($r->status),'Kebutuhan'=>$r->purpose,'Dibuat oleh'=>$creator,'Diajukan oleh'=>$requester,'Disetujui oleh'=>$approver)as$label=>$value){$pdf->SetFont('Arial','B',10);$pdf->Cell(35,6,$label,0,0);$pdf->SetFont('Arial','',10);$pdf->MultiCell(0,6,': '.($value?:'-'));}
$pdf->Ln(3);$pdf->SetFont('Arial','B',9);foreach(array(12=>'No',38=>'Kode - Barang',70=>'Deskripsi / Spesifikasi',22=>'Qty',30=>'Estimasi')as$w=>$label)$pdf->Cell($w,8,$label,1,0,'C');$pdf->Ln();$no=1;$total=0;foreach($data['lines']as$l){$amount=(float)$l->qty*(float)$l->estimated_price;$total+=$amount;$pdf->SetFont('Arial','',8);$pdf->Cell(12,8,$no++,1,0,'C');$pdf->Cell(38,8,substr(($l->kode_barang?:'-').' - '.($l->master_name?:'-'),0,24),1);$pdf->Cell(70,8,substr($l->description,0,42),1);$pdf->Cell(22,8,number_format($l->qty,0,',','.'),1,0,'R');$pdf->Cell(30,8,number_format($amount,0,',','.'),1,0,'R');$pdf->Ln();}$pdf->SetFont('Arial','B',9);$pdf->Cell(142,8,'TOTAL ESTIMASI',1,0,'R');$pdf->Cell(30,8,number_format($total,0,',','.'),1,1,'R');
$pdf->Ln(14);$pdf->SetFont('Arial','',9);$pdf->Cell(86,6,'Pemohon / Yang Mengajukan',0,0,'C');$pdf->Cell(86,6,'Yang Menyetujui',0,1,'C');$pdf->Ln(18);$pdf->SetFont('Arial','B',9);$pdf->Cell(86,6,'('.$requester.')',0,0,'C');$pdf->Cell(86,6,'('.$approver.')',0,1,'C');$pdf->SetFont('Arial','',8);$pdf->Cell(86,5,'Tanggal: '.$r->request_date,0,0,'C');$pdf->Cell(86,5,'Tanggal: '.($r->approved_at?:'-'),0,1,'C');$pdf->Output('I','PR-'.$r->request_no.'.pdf');
}
public function create_request(){$catalogs=(array)$this->input->post('kode_barang_id');$descriptions=(array)$this->input->post('description',true);$quantities=(array)$this->input->post('qty');$barcodeQuantities=(array)$this->input->post('qty_per_barcode');$prices=(array)$this->input->post('estimated_price');$lines=array();foreach($catalogs as$i=>$catalogId)$lines[]=array('kode_barang_id'=>$catalogId,'description'=>$descriptions[$i]??'','qty'=>$quantities[$i]??0,'qty_per_barcode'=>$barcodeQuantities[$i]??null,'estimated_price'=>$prices[$i]??0);return$this->act(fn()=>$this->purchaseservice->createCatalogRequest(array('purpose'=>$this->input->post('purpose',true),'destination_type'=>'warehouse','date'=>$this->input->post('date'),'needed_date'=>$this->input->post('needed_date'),'department_id'=>$this->input->post('department_id'),'department'=>$this->departmentName($this->input->post('department_id')),'lines'=>$lines,'applicant_is_creator'=>$this->input->post('applicant_is_creator'),'applicant_name'=>$this->input->post('applicant_name',true)),$this->uid()),'Purchase request gudang dibuat dengan '.count($lines).' barang.');}
public function submit_request($id){return$this->act(fn()=>$this->purchaseservice->submitRequest($id,$this->uid()),'Purchase request diajukan.');}public function approve_request($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->approveRequest($id,$this->uid(),$this->role()),'Purchase request disetujui.');}public function reject_request($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->rejectRequest($id,$this->uid(),$this->role(),$this->input->post('reason',true)),'Purchase request ditolak.');}
public function delete_request($id){return$this->act(fn()=>$this->purchaseservice->deleteRequest($id,$this->uid()),'Draft Purchase Request berhasil dihapus.');}
public function create_order(){return$this->act(function(){$method=$this->input->post('procurement_method');$this->validatePurchaseDocuments($method);$id=$this->purchaseservice->createProfessionalOrder(array('request_id'=>$this->input->post('request_id'),'supplier_id'=>$this->input->post('supplier_id'),'warehouse_id'=>$this->input->post('warehouse_id'),'date'=>$this->input->post('date'),'expected_date'=>$this->input->post('expected_date'),'procurement_method'=>$method,'payment_timing'=>$this->input->post('payment_timing'),'marketplace'=>$this->input->post('marketplace',true),'external_order_no'=>$this->input->post('external_order_no',true),'courier'=>$this->input->post('courier',true),'tracking_no'=>$this->input->post('tracking_no',true),'shipping_cost'=>$this->input->post('shipping_cost'),'insurance_cost'=>$this->input->post('insurance_cost'),'service_fee'=>$this->input->post('service_fee'),'discount_amount'=>$this->input->post('discount_amount'),'cashback_amount'=>$this->input->post('cashback_amount'),'down_payment_required'=>$this->input->post('down_payment_required'),'down_payment_amount'=>$this->input->post('down_payment_amount'),'installment_count'=>$this->input->post('installment_count'),'first_due_date'=>$this->input->post('first_due_date'),'unit_prices'=>(array)$this->input->post('unit_price'),'tax_rates'=>(array)$this->input->post('tax_rate'),'notes'=>$this->input->post('notes',true)),$this->uid());$this->applyPoBudgetAccounts($id);$this->storePurchaseDocuments($id);return$id;},'Purchase Order multi-item, akun budget pengeluaran, dan dokumen pendukung berhasil dibuat.');}
private function applyPoBudgetAccounts($poId){
if(!$this->db->field_exists('budget_account_id','purchase_order_lines'))return;
$company=$this->companycontext->id();$accounts=(array)$this->input->post('budget_account_id');
foreach($this->db->where('purchase_order_id',(int)$poId)->get('purchase_order_lines')->result()as$line){
$account=(int)($accounts[$line->request_line_id]??0);
if($account&&!$this->db->get_where('accounts',array('id'=>$account,'company_id'=>$company,'tipe'=>'expense','is_active'=>1,'allow_posting'=>1))->row())throw new BusinessException('Akun budget pengeluaran pada barang PO tidak valid.');
$this->db->where('id',$line->id)->update('purchase_order_lines',array('budget_account_id'=>$account?:null));
}
if($this->db->field_exists('company_id','purchase_orders'))$this->db->where('id',(int)$poId)->update('purchase_orders',array('company_id'=>$company));
}
private function validatePurchaseDocuments($method){$this->validateEvidenceFiles('purchase_documents',$method==='online'?'Bukti/detail pesanan online wajib diunggah.':($method==='direct'?'Foto struk pembelian wajib diunggah.':'Dokumen penawaran/detail order supplier wajib diunggah.'));}
private function scopePurchaseOrderItems($orderId)
{
$companyId=(int)$this->session->userdata('company_id')?:1;
$this->db->query('UPDATE items i JOIN purchase_order_lines pol ON pol.item_id=i.id SET i.company_id=? WHERE pol.purchase_order_id=? AND (i.company_id IS NULL OR i.company_id=0)',array($companyId,(int)$orderId));
$foreign=$this->db->select('i.*,pol.id po_line_id,pol.qty po_qty,pol.unit_price po_unit_price,pol.warehouse_id po_warehouse_id,po.order_date po_date')->from('purchase_order_lines pol')->join('purchase_orders po','po.id=pol.purchase_order_id')->join('items i','i.id=pol.item_id')->where('pol.purchase_order_id',(int)$orderId)->where('i.company_id !=',$companyId)->get()->result();
$itemFields=array_flip($this->db->list_fields('items'));
foreach($foreign as$row){$copy=array_intersect_key((array)$row,$itemFields);unset($copy['id']);$copy['company_id']=$companyId;$suffix='-C'.$companyId.'-PO'.$orderId.'-'.$row->po_line_id;$copy['kode_detail']=substr((string)$row->kode_detail,0,max(1,100-strlen($suffix))).$suffix;$copy['stok']=0;$copy['harga_beli']=$row->po_unit_price;$copy['harga_jual']=0;$copy['status']='draft';if(isset($itemFields['planned_qty']))$copy['planned_qty']=$row->po_qty;if(isset($itemFields['planned_warehouse_id']))$copy['planned_warehouse_id']=$row->po_warehouse_id;if(isset($itemFields['tanggal_pembelian']))$copy['tanggal_pembelian']=$row->po_date;$this->db->insert('items',$copy);$newId=$this->db->insert_id();if(!$newId)throw new BusinessException('Barang PO gagal dipisahkan untuk perusahaan aktif.');$this->db->where('id',$row->po_line_id)->update('purchase_order_lines',array('item_id'=>$newId));}
$remaining=(int)$this->db->select('COUNT(*) total',false)->from('purchase_order_lines pol')->join('items i','i.id=pol.item_id')->where('pol.purchase_order_id',(int)$orderId)->where('i.company_id !=',$companyId)->get()->row()->total;if($remaining>0)throw new BusinessException('Purchase Order memuat barang milik perusahaan lain. Proses dibatalkan.');
}
private function storePurchaseDocuments($entityId,$input='purchase_documents',$entityType='purchase_order')
{
if($entityType==='purchase_order'){
$this->scopePurchaseOrderItems($entityId);
}
$files=$_FILES[$input]??array('name'=>array(),'type'=>array(),'tmp_name'=>array(),'error'=>array(),'size'=>array());$dir=FCPATH.'uploads/accounting/';
if(!is_dir($dir)&&!mkdir($dir,0750,true))throw new BusinessException('Folder lampiran tidak tersedia.');
$count=count((array)$files['name']);if($count>10)throw new BusinessException('Maksimal 10 lampiran untuk satu transaksi.');
$this->load->library('upload');$stored=array();
try{
for($i=0;$i<$count;$i++){
if(empty($files['name'][$i]))continue;
$_FILES['attachment']=array('name'=>$files['name'][$i],'type'=>$files['type'][$i],'tmp_name'=>$files['tmp_name'][$i],'error'=>$files['error'][$i],'size'=>$files['size'][$i]);
$this->upload->initialize(array('upload_path'=>$dir,'allowed_types'=>'pdf|jpg|jpeg|jpe|jfif|png|webp|gif|bmp|tif|tiff|heic|heif|avif|jp2|j2k|jpf|jpg2|jpx|ico','max_size'=>5120,'encrypt_name'=>true));
if(!$this->upload->do_upload('attachment'))throw new BusinessException('File '.$files['name'][$i].' ditolak: '.strip_tags($this->upload->display_errors()));
$f=$this->upload->data();$stored[]=$f['full_path'];
$this->db->insert('transaction_attachments',array('module'=>'purchase','entity_type'=>$entityType,'entity_id'=>$entityId,'original_name'=>$f['orig_name'],'stored_name'=>$f['file_name'],'mime_type'=>$f['file_type'],'file_size'=>$f['file_size']*1024,'sha256'=>hash_file('sha256',$f['full_path']),'uploaded_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s')));
if(!$this->db->affected_rows())throw new BusinessException('Metadata lampiran gagal disimpan.');
}
return$stored;
}catch(Throwable$e){foreach($stored as$path)if(is_file($path))@unlink($path);throw$e;}
}
public function submit_order($id){return$this->act(fn()=>$this->purchaseservice->submitOrder($id,$this->uid()),'Purchase order diajukan.');}public function approve_order($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->approveOrder($id,$this->uid(),$this->role()),'Purchase order disetujui.');}public function reject_order($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->rejectOrder($id,$this->uid(),$this->role(),$this->input->post('reason',true)),'Purchase order ditolak.');}
public function update_order($id){$this->requirePermission('purchases','can_update');$prices=(array)$this->input->post('unit_price');$taxes=(array)$this->input->post('tax_rate');return$this->act(fn()=>$this->purchaseservice->updateDraftOrder($id,array('supplier_id'=>$this->input->post('supplier_id'),'warehouse_id'=>$this->input->post('warehouse_id'),'date'=>$this->input->post('date'),'expected_date'=>$this->input->post('expected_date'),'procurement_method'=>$this->input->post('procurement_method'),'payment_timing'=>$this->input->post('payment_timing'),'down_payment_required'=>$this->input->post('down_payment_required'),'down_payment_amount'=>$this->input->post('down_payment_amount'),'installment_count'=>$this->input->post('installment_count'),'first_due_date'=>$this->input->post('first_due_date'),'marketplace'=>$this->input->post('marketplace',true),'external_order_no'=>$this->input->post('external_order_no',true),'courier'=>$this->input->post('courier',true),'tracking_no'=>$this->input->post('tracking_no',true),'shipping_cost'=>$this->input->post('shipping_cost'),'insurance_cost'=>$this->input->post('insurance_cost'),'service_fee'=>$this->input->post('service_fee'),'discount_amount'=>$this->input->post('discount_amount'),'cashback_amount'=>$this->input->post('cashback_amount'),'notes'=>$this->input->post('notes',true),'unit_prices'=>$prices,'tax_rates'=>$taxes),$this->uid()),'Draft Purchase Order berhasil diperbarui.');}
public function pay_order($id){$this->requirePermission('purchases','can_post');$accounts=(array)$this->input->post('source_account_id');$amounts=(array)$this->input->post('source_amount');$sources=array();foreach($accounts as$i=>$account)if($account)$sources[(int)$account]=($sources[(int)$account]??0)+$this->purchaseAmount($amounts[$i]??0);$bankCharge=$this->purchaseAmount($this->input->post('bank_charge_amount'));$bankChargeAccount=(int)$this->input->post('bank_charge_account_id');return$this->act(function()use($id,$sources,$bankCharge,$bankChargeAccount){$this->validateEvidenceFiles('payment_documents','Bukti pembayaran wajib diunggah oleh Finance.');$paymentId=$this->purchaseservice->prepayOrderWithBankCharge($id,$this->input->post('date'),$sources,$this->input->post('reference_no',true),$this->input->post('notes',true),$this->uid(),$bankCharge,$bankChargeAccount);$this->storePurchaseDocuments($paymentId,'payment_documents','supplier_payment');return$paymentId;},'Pembayaran PO, biaya admin bank, dan bukti pembayaran berhasil diposting.');}
private function purchaseAmount($value){$value=trim((string)$value);if($value==='')return 0.0;$value=preg_replace('/[^0-9,\.\-]/','',$value);if(strpos($value,',')!==false)return(float)str_replace(',','.',str_replace('.','',$value));if(preg_match('/^-?\d{1,3}(?:\.\d{3})+$/',$value))return(float)str_replace('.','',$value);return(float)$value;}
private function validateEvidenceFiles($input,$requiredMessage){$files=$_FILES[$input]??array();$names=array_values(array_filter((array)($files['name']??array()),function($name){return trim((string)$name)!=='';}));if(!$names)throw new BusinessException($requiredMessage);if(count($names)>10)throw new BusinessException('Maksimal 10 lampiran untuk satu transaksi.');$extensions=array('pdf','jpg','jpeg','jpe','jfif','png','webp','gif','bmp','tif','tiff','heic','heif','avif','jp2','j2k','jpf','jpg2','jpx','ico');foreach((array)$files['name']as$i=>$name){if(trim((string)$name)==='')continue;$error=(int)($files['error'][$i]??UPLOAD_ERR_NO_FILE);if($error!==UPLOAD_ERR_OK)throw new BusinessException($error===UPLOAD_ERR_INI_SIZE||$error===UPLOAD_ERR_FORM_SIZE?'Ukuran file '.$name.' melebihi batas server.':'File '.$name.' gagal diterima oleh server (kode '.$error.').');if((int)($files['size'][$i]??0)>5*1024*1024)throw new BusinessException('Ukuran file '.$name.' melebihi 5 MB.');$extension=strtolower(pathinfo($name,PATHINFO_EXTENSION));if(!in_array($extension,$extensions,true))throw new BusinessException('Format .'.$extension.' tidak diizinkan. Gunakan PDF atau file gambar.');$tmp=$files['tmp_name'][$i]??'';$mime=$tmp&&is_file($tmp)&&function_exists('finfo_open')?(new finfo(FILEINFO_MIME_TYPE))->file($tmp):($files['type'][$i]??'');if($mime!=='application/pdf'&&strpos((string)$mime,'image/')!==0)throw new BusinessException('Isi file '.$name.' bukan PDF atau gambar yang valid.');}}
public function refund_order($id){return$this->movedAdjustment('refund','Proses refund lama dinonaktifkan agar jurnal dan penerimaan dana tidak terduplikasi. Gunakan submenu Retur & Refund.','po_id',(int)$id);}
public function receive($id)
{
$qty=(array)$this->input->post('qty');
return$this->act(function()use($id,$qty){
$this->validateEvidenceFiles('receipt_documents','Bukti penerimaan barang wajib diunggah oleh petugas Gudang.');
$this->db->trans_begin();
try{
$rid=$this->purchaseservice->receive($id,$this->input->post('date'),$this->input->post('delivery_no',true),$qty,$this->uid());
$this->purchaseservice->finalizeReceiptTracking($rid);
$this->purchaseservice->syncReceiptLedger($rid);
$this->purchaseservice->clearTransitForReceipt($rid,$this->uid());
$this->storePurchaseDocuments($rid,'receipt_documents','goods_receipt');
if($this->db->trans_status()===false)throw new BusinessException('Penerimaan gagal disimpan secara lengkap.');
$this->db->trans_commit();return$rid;
}catch(Throwable$e){$this->db->trans_rollback();throw$e;}
},'Penerimaan barang, bukti penerimaan, barcode, dan stock ledger berhasil diposting.');
}
public function create_bill(){return$this->act(function(){$this->validateEvidenceFiles('invoice_documents','Invoice/nota supplier wajib diunggah.');$po=$this->input->post('po_id');$id=$this->purchaseservice->createSupplierInvoice($po,$this->input->post('receipt_id'),$this->input->post('supplier_invoice_no',true),$this->input->post('date'),$this->uid());$this->purchaseservice->syncInvoicePaymentPlan($id,$po);$this->storePurchaseDocuments($id,'invoice_documents','supplier_invoice');$this->purchaseservice->verifySupplierInvoice($id,$this->uid());return$id;},'Invoice Supplier tersimpan, matching terverifikasi, dan siap ditindaklanjuti.');}
public function submit_bill($id){return$this->act(fn()=>$this->purchaseservice->submitSupplierInvoice($id,$this->uid()),'Tagihan diajukan.');}public function approve_bill($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->approveSupplierInvoice($id,$this->uid(),$this->role()),'Tagihan disetujui.');}public function post_bill($id){$this->requirePermission('purchases','can_post');$accounts=(array)$this->input->post('source_account_id');$amounts=(array)$this->input->post('source_amount');$sources=array();foreach($accounts as$i=>$account)if((int)$account)$sources[(int)$account]=($sources[(int)$account]??0)+$this->purchaseAmount($amounts[$i]??0);$bankCharge=$this->purchaseAmount($this->input->post('bank_charge_amount'));$bankChargeAccount=(int)$this->input->post('bank_charge_account_id');return$this->act(function()use($id,$sources,$bankCharge,$bankChargeAccount){$bill=$this->db->select('si.payment_plan,si.total,po.payment_timing,po.paid_amount')->from('supplier_invoices si')->join('supplier_invoice_lines sil','sil.supplier_invoice_id=si.id')->join('purchase_order_lines pol','pol.id=sil.po_line_id')->join('purchase_orders po','po.id=pol.purchase_order_id')->where('si.id',(int)$id)->limit(1)->get()->row();if(!$bill)throw new BusinessException('Invoice supplier tidak ditemukan.');$alreadyPaid=in_array($bill->payment_timing,array('prepaid','on_receipt'),true)&&(float)$bill->paid_amount>=(float)$bill->total-.001;$createsPayment=$bill->payment_plan==='cash'&&!$alreadyPaid;if($createsPayment)$this->validateEvidenceFiles('invoice_payment_documents','Bukti pembayaran invoice supplier wajib diunggah oleh Finance.');$result=$this->purchaseservice->settleSupplierInvoiceMultiWithBankCharge($id,$sources,$this->input->post('payment_date'),$this->input->post('reference_no',true),$this->uid(),$bankCharge,$bankChargeAccount);if($createsPayment&&!empty($result['payment_id']))$this->storePurchaseDocuments($result['payment_id'],'invoice_payment_documents','supplier_payment');return$result;},'Invoice supplier, pembayaran, dan biaya admin bank berhasil diproses.');}
public function verify_bill($id){return$this->act(fn()=>$this->purchaseservice->verifySupplierInvoice($id,$this->uid()),'Matching Invoice Supplier terverifikasi tanpa approval.');}
public function purchase_return(){return$this->movedAdjustment('return','Retur langsung lama dinonaktifkan. Buat pengajuan retur agar approval, reservasi barcode, stok, dan audit trail tetap konsisten.','receipt_line_id',(int)$this->input->post('receipt_line_id'));}
public function debit_note(){return$this->movedAdjustment('debit_note','Debit Note dipindahkan ke submenu Retur & Refund agar saldo invoice dan reversal dapat ditelusuri.','invoice_id',(int)$this->input->post('bill_id'));}
private function movedAdjustment($action,$message,$parameter,$id){if($this->input->is_ajax_request())return json_response(false,$message,array('redirect'=>base_url('purchases/returns?action='.$action.'&'.$parameter.'='.$id)),409);$this->session->set_flashdata('error',$message);redirect('purchases/returns?action='.$action.'&'.$parameter.'='.$id);}
private function act($fn,$msg){if(strtoupper($this->input->method())!=='POST')show_404();$step=$this->input->post('return_step',true)?:$this->input->get('step',true);try{$r=$fn();$this->session->set_flashdata('success',$msg);redirect('purchases'.($step?'?step='.$step:''));}catch(Throwable$e){$this->session->set_flashdata('error',$e->getMessage());redirect('purchases'.($step?'?step='.$step:''));}}
}