74 lines
12 KiB
PHP
74 lines
12 KiB
PHP
<?php
|
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
|
|
|
/** Pemeriksaan read-only khusus Retur, Refund, Pengganti, dan Debit Note. */
|
|
class Purchaseadjustmentcheck extends CI_Controller
|
|
{
|
|
public function __construct(){parent::__construct();if(!$this->input->is_cli_request())show_404();}
|
|
|
|
public function integrity()
|
|
{
|
|
$checks=array(
|
|
'draft_return_without_stock'=>"SELECT COUNT(*) total FROM purchase_returns r JOIN stock_logs l ON l.ref_type='purchase_return' AND l.ref_id=r.id WHERE r.status IN('draft','submitted','approved','reserved')",
|
|
'over_returned_receipt'=>"SELECT COUNT(*) total FROM(SELECT l.receipt_line_id,SUM(IF(r.status NOT IN('draft','rejected','cancelled','reversed'),l.qty,0)) returned,MAX(gr.qty) received FROM purchase_return_lines l JOIN purchase_returns r ON r.id=l.purchase_return_id JOIN goods_receipt_lines gr ON gr.id=l.receipt_line_id GROUP BY l.receipt_line_id HAVING returned>received+.0001)x",
|
|
'duplicate_return_shipment_line'=>"SELECT COUNT(*) total FROM(SELECT purchase_return_line_id FROM purchase_return_shipment_lines GROUP BY purchase_return_line_id HAVING COUNT(*)>1)x",
|
|
'mixed_supplier_return_batch'=>"SELECT COUNT(*) total FROM purchase_return_lines l JOIN purchase_returns r ON r.id=l.purchase_return_id JOIN goods_receipts gr ON gr.id=l.goods_receipt_id WHERE gr.supplier_id<>r.supplier_id",
|
|
'pre_invoice_return_overbilled'=>"SELECT COUNT(*) total FROM(SELECT sil.id,sil.qty,grl.qty-COALESCE(SUM(CASE WHEN pr.shipped_at IS NOT NULL AND pr.shipped_at<si.created_at AND pr.status IN('shipped','waiting_supplier','partially_resolved','resolved') THEN prl.shipped_qty ELSE 0 END),0) net_received FROM supplier_invoice_lines sil JOIN supplier_invoices si ON si.id=sil.supplier_invoice_id JOIN goods_receipt_lines grl ON grl.id=sil.receipt_line_id LEFT JOIN purchase_return_lines prl ON prl.receipt_line_id=grl.id LEFT JOIN purchase_returns pr ON pr.id=prl.purchase_return_id GROUP BY sil.id,sil.qty,grl.qty HAVING sil.qty>net_received+.0001)x",
|
|
'posted_shipment_without_stock_trace'=>"SELECT COUNT(*) total FROM purchase_return_shipment_lines sl JOIN purchase_return_shipments sh ON sh.id=sl.purchase_return_shipment_id JOIN purchase_return_lines rl ON rl.id=sl.purchase_return_line_id WHERE sh.status='posted' AND (rl.stock_log_id IS NULL OR rl.movement_id IS NULL OR rl.ledger_id IS NULL)",
|
|
'replacement_over_return_quantity'=>"SELECT COUNT(*) total FROM(SELECT rl.id,rl.shipped_qty,COALESCE(SUM(pl.qty),0) replacement_qty FROM purchase_return_lines rl LEFT JOIN purchase_replacement_lines pl ON pl.purchase_return_line_id=rl.id LEFT JOIN purchase_replacements p ON p.id=pl.purchase_replacement_id AND p.status NOT IN('cancelled','reversed') GROUP BY rl.id,rl.shipped_qty HAVING replacement_qty>shipped_qty+.0001)x",
|
|
'replacement_creates_payable'=>"SELECT COUNT(*) total FROM journals j JOIN journal_details d ON d.journal_id=j.id JOIN system_account_mappings m ON m.mapping_key='accounts_payable' AND m.account_id=d.account_id WHERE j.ref_type='purchase_replacement' AND ABS(d.debit-d.kredit)>.01",
|
|
'unit_return_invalid_quantity'=>"SELECT COUNT(*) total FROM purchase_return_lines WHERE tracking_type='UNIT' AND ABS(qty-1)>.0001",
|
|
'invalid_return_reservation'=>"SELECT COUNT(*) total FROM item_barcodes WHERE return_reserved_qty<0 OR return_reserved_qty>qty_sisa+.0001",
|
|
'refund_over_received'=>"SELECT COUNT(*) total FROM supplier_refund_claims WHERE received_amount>claim_amount+.01 OR ABS(balance-GREATEST(claim_amount-received_amount,0))>.01",
|
|
'refund_receipt_sum_mismatch'=>"SELECT COUNT(*) total FROM(SELECT c.id,c.received_amount,COALESCE(SUM(IF(r.status='reversed',0,r.amount)),0) receipt_total FROM supplier_refund_claims c LEFT JOIN supplier_refund_receipts r ON r.supplier_refund_claim_id=c.id GROUP BY c.id,c.received_amount HAVING ABS(received_amount-receipt_total)>.01)x",
|
|
'debit_note_negative_invoice'=>"SELECT COUNT(*) total FROM supplier_invoices WHERE balance<-.01",
|
|
'unbalanced_adjustment_journal'=>"SELECT COUNT(*) total FROM(SELECT j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE j.ref_type IN('purchase_return','supplier_refund_claim','supplier_refund_receipt','supplier_debit_note') GROUP BY j.id HAVING ABS(SUM(d.debit)-SUM(d.kredit))>.01)x",
|
|
'duplicate_adjustment_idempotency'=>"SELECT (SELECT COUNT(*) FROM(SELECT idempotency_key FROM purchase_returns WHERE idempotency_key IS NOT NULL GROUP BY idempotency_key HAVING COUNT(*)>1)a)+(SELECT COUNT(*) FROM(SELECT idempotency_key FROM purchase_return_shipments GROUP BY idempotency_key HAVING COUNT(*)>1)b)+(SELECT COUNT(*) FROM(SELECT idempotency_key FROM purchase_replacements GROUP BY idempotency_key HAVING COUNT(*)>1)c)+(SELECT COUNT(*) FROM(SELECT idempotency_key FROM supplier_refund_receipts GROUP BY idempotency_key HAVING COUNT(*)>1)d) total",
|
|
'cross_company_return_line'=>"SELECT COUNT(*) total FROM purchase_return_lines l JOIN purchase_returns h ON h.id=l.purchase_return_id WHERE COALESCE(l.company_id,0)<>COALESCE(h.company_id,0)",
|
|
'cross_company_adjustment_child'=>"SELECT (SELECT COUNT(*) FROM purchase_return_shipments x JOIN purchase_returns h ON h.id=x.purchase_return_id WHERE COALESCE(x.company_id,0)<>COALESCE(h.company_id,0))+(SELECT COUNT(*) FROM purchase_replacements x JOIN purchase_returns h ON h.id=x.purchase_return_id WHERE COALESCE(x.company_id,0)<>COALESCE(h.company_id,0))+(SELECT COUNT(*) FROM supplier_refund_receipts x JOIN supplier_refund_claims h ON h.id=x.supplier_refund_claim_id WHERE COALESCE(x.company_id,0)<>COALESCE(h.company_id,0)) total",
|
|
'orphan_refund_allocation'=>"SELECT COUNT(*) total FROM supplier_refund_allocations a LEFT JOIN supplier_refund_claims c ON c.id=a.supplier_refund_claim_id WHERE c.id IS NULL"
|
|
);
|
|
$failed=0;$result=array();foreach($checks as$name=>$sql){try{$value=(int)$this->db->query($sql)->row()->total;$result[$name]=array('status'=>$value?'fail':'pass','violations'=>$value);if($value)$failed++;}catch(Throwable$e){$result[$name]=array('status'=>'fail','error'=>$e->getMessage());$failed++;}}
|
|
foreach(array('supplier_refund_receivable','purchase_return_adjustment','purchase_price_variance')as$key){$valid=(int)$this->db->select('COUNT(*) total',false)->from('system_account_mappings m')->join('accounts a','a.id=m.account_id')->where(array('m.mapping_key'=>$key,'m.is_active'=>1,'a.is_active'=>1,'a.allow_posting'=>1))->get()->row()->total;$result['mapping_'.$key]=array('status'=>$valid?'pass':'fail','violations'=>$valid?0:1);if(!$valid)$failed++;}
|
|
echo json_encode(array('status'=>$failed?'failed':'ok','checks'=>$result),JSON_PRETTY_PRINT).PHP_EOL;if($failed)exit(1);
|
|
}
|
|
|
|
/** Smoke test read-only: memaksa query service dan menu memakai schema aktual. */
|
|
public function runtime()
|
|
{
|
|
$access=$this->db->select('uc.user_id,uc.company_id,u.role_id,r.nama_role')->from('user_companies uc')->join('users u','u.id=uc.user_id')->join('roles r','r.id=u.role_id')->order_by('uc.is_default','DESC')->limit(1)->get()->row();
|
|
if(!$access)throw new RuntimeException('User/company untuk runtime QA tidak tersedia.');
|
|
$this->session->set_userdata(array('logged_in'=>true,'user_id'=>$access->user_id,'company_id'=>$access->company_id,'role_id'=>$access->role_id,'role'=>$access->nama_role));
|
|
$this->load->library(array('CompanyContext','PurchaseAdjustmentService','NavigationService','ProfessionalReportService'));
|
|
$sources=$this->purchaseadjustmentservice->returnSources();$barcodes=array();if($sources)$barcodes=$this->purchaseadjustmentservice->barcodesForReceiptLine($sources[0]['receipt_line_id']);
|
|
$document=null;$return=$this->db->where('company_id',$access->company_id)->order_by('id','DESC')->get('purchase_returns')->row();if($return)$document=$this->purchaseadjustmentservice->document('purchase_return',$return->id);
|
|
$menu=$this->navigationservice->items();$filters=array('from'=>'1900-01-01','to'=>date('Y-m-d'));
|
|
$reports=array('purchase_returns'=>count($this->professionalreportservice->run('purchase_returns',$filters)),'supplier_refunds'=>count($this->professionalreportservice->run('supplier_refunds',$filters)),'outstanding_supplier_refunds'=>count($this->professionalreportservice->run('outstanding_supplier_refunds',$filters)),'supplier_debit_notes'=>count($this->professionalreportservice->run('supplier_debit_notes',$filters)));
|
|
$netInvoiceReturn=$this->db->query("SELECT COALESCE(SUM(prl.shipped_qty*pol.unit_price*(1+(pol.tax_rate/100))),0) total FROM purchase_return_lines prl JOIN purchase_returns pr ON pr.id=prl.purchase_return_id JOIN goods_receipt_lines grl0 ON grl0.id=prl.receipt_line_id JOIN purchase_order_lines pol ON pol.id=grl0.po_line_id WHERE pol.purchase_order_id=0 AND pr.status IN('shipped','waiting_supplier','partially_resolved','resolved') AND NOT EXISTS (SELECT 1 FROM supplier_invoice_lines sil0 JOIN supplier_invoices si0 ON si0.id=sil0.supplier_invoice_id WHERE sil0.receipt_line_id=prl.receipt_line_id AND si0.created_at<=pr.shipped_at)")->row();
|
|
echo json_encode(array('status'=>'ok','company_id'=>(int)$access->company_id,'return_sources'=>count($sources),'first_source_barcodes'=>count($barcodes),'return_document_loaded'=>$return?(bool)$document:null,'net_invoice_return_query'=>is_object($netInvoiceReturn),'menu_items'=>count($menu),'reports'=>$reports),JSON_PRETTY_PRINT).PHP_EOL;
|
|
}
|
|
|
|
public function inventory_differences()
|
|
{
|
|
$rows=$this->db->query("SELECT l.item_id,l.warehouse_id,l.qty ledger_qty,COALESCE(s.qty,0) stock_log_qty,i.stok item_cache,l.qty-COALESCE(s.qty,0) log_diff,i.stok-l.qty cache_diff FROM(SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id,warehouse_id)l LEFT JOIN(SELECT item_id,COALESCE(warehouse_id,0) warehouse_id,SUM(IF(tipe='masuk',qty,-qty)) qty FROM stock_logs GROUP BY item_id,COALESCE(warehouse_id,0))s ON s.item_id=l.item_id AND s.warehouse_id=l.warehouse_id LEFT JOIN items i ON i.id=l.item_id HAVING ABS(log_diff)>.0001 OR ABS(cache_diff)>.0001")->result_array();
|
|
foreach($rows as&$row){
|
|
$row['item']=$this->db->where('id',(int)$row['item_id'])->get('items')->row_array();
|
|
$row['ledger_entries']=$this->db->where(array('item_id'=>(int)$row['item_id'],'warehouse_id'=>(int)$row['warehouse_id']))->order_by('id')->get('inventory_ledger')->result_array();
|
|
$row['stock_logs']=$this->db->where(array('item_id'=>(int)$row['item_id'],'warehouse_id'=>(int)$row['warehouse_id']))->order_by('id')->get('stock_logs')->result_array();
|
|
$row['barcodes']=$this->db->where(array('item_id'=>(int)$row['item_id'],'warehouse_id'=>(int)$row['warehouse_id']))->order_by('id')->get('item_barcodes')->result_array();
|
|
}unset($row);
|
|
echo json_encode(array('status'=>$rows?'difference':'ok','rows'=>$rows),JSON_PRETTY_PRINT).PHP_EOL;
|
|
}
|
|
|
|
/** Breakdown read-only untuk investigasi rekonsiliasi nilai persediaan. */
|
|
public function accounting_reconciliation()
|
|
{
|
|
$mapping=$this->db->where(array('mapping_key'=>'inventory','is_active'=>1))->get('system_account_mappings')->row();
|
|
if(!$mapping)throw new RuntimeException('Mapping akun persediaan tidak tersedia.');
|
|
$ledger=$this->db->query("SELECT document_type,direction,COUNT(*) row_count,SUM(qty) qty,SUM(value) value FROM inventory_ledger GROUP BY document_type,direction ORDER BY document_type,direction")->result_array();
|
|
$gl=$this->db->query("SELECT j.ref_type,COUNT(DISTINCT j.id) journal_count,SUM(d.debit-d.kredit) value FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE d.account_id=? AND j.status IN('posted','reversed') GROUP BY j.ref_type ORDER BY j.ref_type",array((int)$mapping->account_id))->result_array();
|
|
$openReceipts=$this->db->query("SELECT gr.receipt_no,po.po_no,SUM(grl.qty*pol.unit_price) value,po.paid_amount,po.total FROM goods_receipts gr JOIN purchase_orders po ON po.id=gr.purchase_order_id JOIN goods_receipt_lines grl ON grl.goods_receipt_id=gr.id JOIN purchase_order_lines pol ON pol.id=grl.po_line_id LEFT JOIN supplier_invoice_lines sil ON sil.receipt_line_id=grl.id WHERE gr.status='posted' AND sil.id IS NULL GROUP BY gr.id,gr.receipt_no,po.po_no,po.paid_amount,po.total ORDER BY gr.id")->result_array();
|
|
echo json_encode(array('inventory_account_id'=>(int)$mapping->account_id,'ledger_by_source'=>$ledger,'gl_by_source'=>$gl,'received_not_invoiced'=>$openReceipts),JSON_PRETTY_PRINT).PHP_EOL;
|
|
}
|
|
}
|