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

332 lines
16 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Payables extends MY_Controller
{
public function __construct()
{
parent::__construct();
$this->load->library('PurchaseService');
}
public function index()
{
$companyId = $this->companycontext->id();
$this->db->select('s.*')->from('suppliers s');
if ($this->db->field_exists('company_id', 'suppliers')) {
$this->db->where('s.company_id', $companyId);
}
$this->db->group_start()
->where('s.is_active', 1)
->or_where(
"EXISTS(SELECT 1 FROM supplier_invoices si WHERE si.supplier_id=s.id AND si.status='partial' AND si.balance>0".
($this->db->field_exists('company_id', 'supplier_invoices') ? ' AND si.company_id='.(int)$companyId : '').')',
null,
false
)
->group_end();
$suppliers = $this->db->order_by('s.name')->get()->result();
$this->db->from('accounts')->where(array('is_active'=>1, 'allow_posting'=>1));
if ($this->db->field_exists('company_id', 'accounts')) {
$this->db->where('company_id', $companyId);
}
$accounts = $this->db
->group_start()->like('nama_akun', 'kas')->or_like('nama_akun', 'bank')->group_end()
->order_by('kode_akun')->get()->result();
$this->db->from('accounts')->where(array('tipe'=>'expense', 'is_active'=>1, 'allow_posting'=>1));
if ($this->db->field_exists('company_id', 'accounts')) {
$this->db->where('company_id', $companyId);
}
$bankChargeAccounts = $this->db->order_by('kode_akun')->get()->result();
$mapping = $this->db->get_where('system_account_mappings', array(
'mapping_key' => 'bank_charge_expense',
'is_active' => 1,
))->row();
$data = array(
'active_menu' => 'payables',
'aging' => $this->purchaseservice->aging($this->input->get('as_of')),
'suppliers' => $suppliers,
'accounts' => $accounts,
'bank_charge_accounts' => $bankChargeAccounts,
'bank_charge_account_id' => $mapping ? (int)$mapping->account_id : 0,
);
$this->load->view('partials/header', $data);
$this->load->view('payables/index', $data);
$this->load->view('partials/footer');
}
public function open_bills($supplier)
{
$supplier = (int)$supplier;
$companyId = $this->companycontext->id();
$this->db
->select("si.id,si.internal_no,si.supplier_invoice_no,si.invoice_date,si.due_date,si.total,si.paid_amount,si.balance,si.status,si.payment_plan,CASE WHEN si.due_date<=CURDATE() THEN 'overdue' ELSE 'upcoming' END due_state,po.id po_id,po.po_no,po.order_date,po.payment_timing,po.total po_total,po.paid_amount po_paid_amount,po.subtotal po_subtotal,po.tax_total po_tax_total,po.shipping_cost,po.insurance_cost,po.service_fee,po.discount_amount,po.cashback_amount,GROUP_CONCAT(DISTINCT gr.receipt_no ORDER BY gr.receipt_date,gr.id SEPARATOR ', ') receipt_no", false)
->from('supplier_invoices si')
->join('supplier_invoice_lines sil', 'sil.supplier_invoice_id=si.id', 'left')
->join('purchase_order_lines pol', 'pol.id=sil.po_line_id', 'left')
->join('purchase_orders po', 'po.id=pol.purchase_order_id', 'left')
->join('goods_receipt_lines grl', 'grl.id=sil.receipt_line_id', 'left')
->join('goods_receipts gr', 'gr.id=grl.goods_receipt_id', 'left')
->where('si.supplier_id', $supplier)
->where('si.status', 'partial')
->where('si.balance >', 0);
if ($this->db->field_exists('company_id', 'supplier_invoices')) {
$this->db->where('si.company_id', $companyId);
}
$bills = $this->db->group_by('si.id')->order_by('si.due_date')->get()->result();
foreach ($bills as $bill) {
$bill->items = $this->db
->select('kb.kode_barang,i.nama_barang,sil.qty,sil.unit_price,sil.tax_amount,sil.line_total')
->from('supplier_invoice_lines sil')
->join('items i', 'i.id=sil.item_id', 'left')
->join('kode_barang kb', 'kb.id=i.kode_id', 'left')
->where('sil.supplier_invoice_id', $bill->id)
->get()->result();
$bill->installment_schedules = $bill->payment_timing === 'installment'
? $this->db->where('purchase_order_id', $bill->po_id)->order_by('installment_no')->get('purchase_installment_schedules')->result()
: array();
}
$this->db
->select('si.id,si.internal_no,si.supplier_invoice_no,si.balance,si.status,po.po_no')
->from('supplier_invoices si')
->join('supplier_invoice_lines sil', 'sil.supplier_invoice_id=si.id', 'left')
->join('purchase_order_lines pol', 'pol.id=sil.po_line_id', 'left')
->join('purchase_orders po', 'po.id=pol.purchase_order_id', 'left')
->where('si.supplier_id', $supplier)
->where('si.status', 'verified')
->where('si.balance >', 0);
if ($this->db->field_exists('company_id', 'supplier_invoices')) {
$this->db->where('si.company_id', $companyId);
}
$pending = $this->db->group_by('si.id')->order_by('si.due_date')->get()->result();
$this->db
->select('sp.id,sp.payment_no,sp.payment_date,sp.amount,sp.bank_charge_amount,sp.total_cash_out,sp.reference_no,spa.amount allocated_amount,si.internal_no,si.supplier_invoice_no')
->from('supplier_payments sp')
->join('supplier_payment_allocations spa', 'spa.supplier_payment_id=sp.id', 'left')
->join('supplier_invoices si', 'si.id=spa.supplier_invoice_id', 'left')
->where('sp.supplier_id', $supplier)
->where('sp.status', 'posted');
if ($this->db->field_exists('company_id', 'supplier_payments')) {
$this->db->where('sp.company_id', $companyId);
}
$history = $this->db->order_by('sp.payment_date', 'DESC')->order_by('sp.id', 'DESC')->limit(100)->get()->result();
foreach ($history as $row) {
$row->sources = $this->db
->select('pps.amount,a.kode_akun,a.nama_akun')
->from('purchase_payment_sources pps')
->join('accounts a', 'a.id=pps.account_id')
->where('pps.supplier_payment_id', $row->id)
->get()->result();
}
return json_response(true, 'Tagihan dan riwayat supplier.', array(
'bills' => $bills,
'pending' => $pending,
'history' => $history,
));
}
public function payment()
{
if (strtoupper($this->input->method()) !== 'POST') {
show_404();
}
$storedFiles = array();
try {
$this->validatePaymentEvidence('payment_documents');
$accounts = (array)$this->input->post('source_account_id');
$amounts = (array)$this->input->post('source_amount');
$sources = array();
foreach ($accounts as $index => $accountId) {
if ((int)$accountId) {
$sources[(int)$accountId] = ($sources[(int)$accountId] ?? 0) + $this->parseAmount($amounts[$index] ?? 0);
}
}
$allocations = array();
foreach ((array)$this->input->post('allocations') as $invoiceId => $amount) {
$allocations[(int)$invoiceId] = $this->parseAmount($amount);
}
// Transaksi luar memastikan pembayaran, jurnal, alokasi, dan metadata
// bukti pembayaran berhasil atau gagal sebagai satu kesatuan.
$this->db->trans_begin();
$result = $this->purchaseservice->paymentMultiWithBankCharge(
(int)$this->input->post('supplier_id'),
$this->input->post('date'),
$sources,
$allocations,
(bool)$this->input->post('allow_advance'),
(int)$this->session->userdata('user_id'),
$this->input->post('reference_no', true),
$this->parseAmount($this->input->post('bank_charge_amount')),
(int)$this->input->post('bank_charge_account_id')
);
$storedFiles = $this->storePaymentEvidence($result['payment_id'], 'payment_documents');
if ($this->db->trans_status() === false) {
throw new BusinessException('Pembayaran dan bukti pembayaran gagal disimpan secara lengkap.');
}
$this->db->trans_commit();
return json_response(true, 'Pembayaran supplier berhasil diposting.', array('result'=>$result));
} catch (Throwable $e) {
if ($this->db->trans_depth() > 0) {
$this->db->trans_rollback();
}
foreach ($storedFiles as $path) {
if (is_file($path)) @unlink($path);
}
return business_exception_response($e);
}
}
private function validatePaymentEvidence($input)
{
$files = $_FILES[$input] ?? array();
$names = array_values(array_filter((array)($files['name'] ?? array()), function ($name) {
return trim((string)$name) !== '';
}));
if (!$names) {
throw new BusinessException('Bukti pembayaran wajib diunggah sebelum pembayaran diposting.');
}
if (count($names) > 10) {
throw new BusinessException('Maksimal 10 bukti pembayaran 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 $index => $name) {
if (trim((string)$name) === '') continue;
$error = (int)($files['error'][$index] ?? UPLOAD_ERR_NO_FILE);
if ($error !== UPLOAD_ERR_OK) {
throw new BusinessException(
in_array($error, array(UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE), true)
? 'Ukuran file '.$name.' melebihi batas server.'
: 'File '.$name.' gagal diterima oleh server (kode '.$error.').'
);
}
if ((int)($files['size'][$index] ?? 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.');
}
$temporary = $files['tmp_name'][$index] ?? '';
$mime = $temporary && is_file($temporary) && function_exists('finfo_open')
? (new finfo(FILEINFO_MIME_TYPE))->file($temporary)
: ($files['type'][$index] ?? '');
if ($mime !== 'application/pdf' && strpos((string)$mime, 'image/') !== 0) {
throw new BusinessException('Isi file '.$name.' bukan PDF atau gambar yang valid.');
}
}
}
private function storePaymentEvidence($paymentId, $input)
{
if (!(int)$paymentId) {
throw new BusinessException('Nomor pembayaran tidak valid untuk penyimpanan bukti.');
}
$files = $_FILES[$input];
$directory = FCPATH.'uploads/accounting/';
if (!is_dir($directory) && !mkdir($directory, 0750, true)) {
throw new BusinessException('Folder bukti pembayaran tidak tersedia.');
}
$this->load->library('upload');
$stored = array();
try {
foreach ((array)$files['name'] as $index => $name) {
if (trim((string)$name) === '') continue;
$_FILES['attachment'] = array(
'name' => $name,
'type' => $files['type'][$index],
'tmp_name' => $files['tmp_name'][$index],
'error' => $files['error'][$index],
'size' => $files['size'][$index],
);
$this->upload->initialize(array(
'upload_path' => $directory,
'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 '.$name.' ditolak: '.strip_tags($this->upload->display_errors()));
}
$file = $this->upload->data();
$stored[] = $file['full_path'];
$this->db->insert('transaction_attachments', array(
'module' => 'purchase',
'entity_type' => 'supplier_payment',
'entity_id' => (int)$paymentId,
'original_name' => $file['orig_name'],
'stored_name' => $file['file_name'],
'mime_type' => $file['file_type'],
'file_size' => $file['file_size'] * 1024,
'sha256' => hash_file('sha256', $file['full_path']),
'uploaded_by' => (int)$this->session->userdata('user_id'),
'created_at' => date('Y-m-d H:i:s'),
));
if (!$this->db->affected_rows()) {
throw new BusinessException('Metadata bukti pembayaran gagal disimpan.');
}
}
return $stored;
} catch (Throwable $e) {
foreach ($stored as $path) {
if (is_file($path)) @unlink($path);
}
throw $e;
}
}
public function statement($supplier)
{
$companyId = $this->companycontext->id();
$this->db->where('id', (int)$supplier);
if ($this->db->field_exists('company_id', 'suppliers')) {
$this->db->where('company_id', $companyId);
}
$supplierRow = $this->db->get('suppliers')->row();
if (!$supplierRow) {
show_404();
}
$this->db->where('supplier_id', $supplierRow->id);
if ($this->db->field_exists('company_id', 'supplier_invoices')) {
$this->db->where('company_id', $companyId);
}
$bills = $this->db->order_by('invoice_date')->get('supplier_invoices')->result();
$this->db->where('supplier_id', $supplierRow->id);
if ($this->db->field_exists('company_id', 'supplier_payments')) {
$this->db->where('company_id', $companyId);
}
$payments = $this->db->order_by('payment_date')->get('supplier_payments')->result();
$data = array('active_menu'=>'payables', 'supplier'=>$supplierRow, 'bills'=>$bills, 'payments'=>$payments);
$this->load->view('partials/header', $data);
$this->load->view('payables/statement', $data);
$this->load->view('partials/footer');
}
/** Mendukung format Indonesia (1.250.000,50) maupun angka normal (1250000.50). */
private function parseAmount($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;
}
}