440 lines
17 KiB
PHP
440 lines
17 KiB
PHP
<?php
|
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
|
|
|
class Jurnal extends MY_Controller {
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$this->load->model('DynamicModel', 'dm');
|
|
$this->load->library('JournalService');
|
|
$this->load->library('NumberingService');
|
|
$this->load->library('JournalWorkflowService');
|
|
|
|
if (!$this->session->userdata('logged_in')) {
|
|
redirect('auth');
|
|
}
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
|
|
// if($this->session->userdata('role') != 'Admin'){
|
|
// show_error('Akses ditolak', 403);
|
|
// }
|
|
|
|
$data = ["active_menu" => "jurnal"];
|
|
$this->load->view('partials/header', $data);
|
|
$this->load->view('jurnal/layout');
|
|
$this->load->view('partials/footer');
|
|
}
|
|
|
|
// =========================
|
|
// GET DATA DATATABLE (SUDAH ADA TOTAL)
|
|
// =========================
|
|
public function get_data()
|
|
{
|
|
$draw = $_POST['draw'];
|
|
$start = $_POST['start'];
|
|
$length = $_POST['length'];
|
|
$search = $_POST['search']['value'];
|
|
$status = (string) $this->input->post('status', true);
|
|
$allowedStatuses = array('draft', 'submitted', 'approved', 'posted', 'rejected', 'reversed');
|
|
if (!in_array($status, $allowedStatuses, true)) $status = '';
|
|
|
|
// ================= TOTAL ALL
|
|
$totalAll = $this->db->where('company_id',$this->companycontext->id())->count_all_results('journals');
|
|
|
|
// ================= QUERY FILTERED (HITUNG)
|
|
$this->db->from('journals')->where('company_id',$this->companycontext->id());
|
|
|
|
if ($status !== '') {
|
|
$this->db->where('status', $status);
|
|
}
|
|
|
|
if(!empty($search)){
|
|
$this->db->group_start();
|
|
$this->db->like('no_ref', $search);
|
|
$this->db->or_like('keterangan', $search);
|
|
$this->db->or_like('tanggal', $search);
|
|
$this->db->group_end();
|
|
}
|
|
|
|
$filtered = $this->db->count_all_results();
|
|
|
|
// ================= QUERY DATA (AMBIL DATA)
|
|
$this->db->select('
|
|
journals.id,
|
|
journals.tanggal,
|
|
journals.no_ref,
|
|
journals.keterangan,
|
|
journals.status,
|
|
journals.is_reversed,
|
|
journals.ref_type,
|
|
journals.created_by,
|
|
journals.rejection_reason,
|
|
COALESCE(SUM(journal_details.debit),0) as total_debit,
|
|
COALESCE(SUM(journal_details.kredit),0) as total_kredit
|
|
');
|
|
$this->db->from('journals');
|
|
$this->db->join('journal_details','journal_details.journal_id = journals.id','LEFT');
|
|
$this->db->where('journals.company_id',$this->companycontext->id());
|
|
|
|
if ($status !== '') {
|
|
$this->db->where('journals.status', $status);
|
|
}
|
|
|
|
if(!empty($search)){
|
|
$this->db->group_start();
|
|
$this->db->like('journals.no_ref', $search);
|
|
$this->db->or_like('journals.keterangan', $search);
|
|
$this->db->or_like('journals.tanggal', $search);
|
|
$this->db->group_end();
|
|
}
|
|
|
|
$this->db->group_by('journals.id');
|
|
$this->db->order_by('journals.tanggal','DESC');
|
|
$this->db->order_by('journals.created_at','DESC');
|
|
$this->db->limit($length, $start);
|
|
|
|
$query = $this->db->get()->result();
|
|
|
|
// ================= FORMAT
|
|
$data = [];
|
|
$no = $start;
|
|
|
|
foreach ($query as $row) {
|
|
$no++;
|
|
|
|
$isAdmin = is_master_admin_user() || $this->session->userdata('role') === 'Admin';
|
|
$isOwner = (int)$row->created_by === (int)$this->session->userdata('user_id');
|
|
$aksi = '<button class="btn btn-sm btn-info btn-detail" data-id="'.$row->id.'">Detail</button> ';
|
|
if ($row->status === 'draft') {
|
|
$aksi .= '<button class="btn btn-sm btn-primary btn-submit" data-id="'.$row->id.'">Submit</button> ';
|
|
if ($isAdmin) $aksi .= '<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Hapus</button> ';
|
|
}
|
|
if ($row->status === 'rejected' && ($isOwner || $isAdmin) && in_array((string)$row->ref_type, array('', 'manual_journal'), true)) {
|
|
$aksi .= '<button class="btn btn-sm btn-warning btn-edit-rejected" data-id="'.$row->id.'">Edit & Koreksi</button> ';
|
|
if ($isAdmin) $aksi .= '<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Hapus</button> ';
|
|
}
|
|
if ($isAdmin && $row->status === 'submitted') {
|
|
$aksi .= '<button class="btn btn-sm btn-success btn-approve" data-id="'.$row->id.'">Approve</button> ';
|
|
$aksi .= '<button class="btn btn-sm btn-outline-danger btn-reject" data-id="'.$row->id.'">Reject</button> ';
|
|
}
|
|
if ($isAdmin && $row->status === 'posted') {
|
|
$aksi .= '<button class="btn btn-sm btn-outline-warning btn-reverse" data-id="'.$row->id.'">Reverse</button>';
|
|
}
|
|
|
|
$statusClass = array('draft'=>'secondary','submitted'=>'info','approved'=>'primary','posted'=>'success','rejected'=>'danger','reversed'=>'warning');
|
|
$badge = '<span class="badge bg-'.(isset($statusClass[$row->status]) ? $statusClass[$row->status] : 'secondary').'">'.htmlspecialchars(strtoupper($row->status), ENT_QUOTES, 'UTF-8').'</span>';
|
|
|
|
$data[] = [
|
|
$no,
|
|
$row->tanggal,
|
|
$row->no_ref,
|
|
$row->keterangan,
|
|
number_format($row->total_debit,0,',','.'),
|
|
number_format($row->total_kredit,0,',','.'),
|
|
$badge,
|
|
$aksi
|
|
];
|
|
}
|
|
|
|
echo json_encode([
|
|
"draw" => $draw,
|
|
"recordsTotal" => $totalAll,
|
|
"recordsFiltered" => $filtered,
|
|
"data" => $data
|
|
]);
|
|
}
|
|
|
|
// =========================
|
|
// GET ACCOUNTS
|
|
// =========================
|
|
public function get_accounts()
|
|
{
|
|
$data = $this->db->where(['company_id'=>$this->companycontext->id(),'is_active'=>1,'allow_posting'=>1])->order_by('kode_akun','ASC')->get('accounts')->result();
|
|
echo json_encode($data);
|
|
}
|
|
|
|
// =========================
|
|
// SAVE
|
|
// =========================
|
|
public function save()
|
|
{
|
|
$tanggal = (string) $this->input->post('tanggal', true);
|
|
$keterangan = trim((string) $this->input->post('keterangan', true));
|
|
$accounts = (array) $this->input->post('account_id');
|
|
$debit = (array) $this->input->post('debit');
|
|
$kredit = (array) $this->input->post('kredit');
|
|
|
|
try {
|
|
$noRef = $this->numberingservice->next('journal', $tanggal);
|
|
$lines = array();
|
|
foreach ($accounts as $index => $accountId) {
|
|
$lineDebit = isset($debit[$index]) ? $this->parseNominal($debit[$index]) : 0;
|
|
$lineCredit = isset($kredit[$index]) ? $this->parseNominal($kredit[$index]) : 0;
|
|
if ($lineDebit <= 0 && $lineCredit <= 0) continue;
|
|
$lines[] = array(
|
|
'account_id' => $accountId,
|
|
'debit' => $lineDebit,
|
|
'kredit' => $lineCredit
|
|
);
|
|
}
|
|
|
|
$journalId = $this->journalservice->create(array(
|
|
'tanggal' => $tanggal,
|
|
'no_ref' => $noRef,
|
|
'keterangan' => $keterangan,
|
|
'ref_type' => 'manual_journal',
|
|
'created_by' => (int) $this->session->userdata('user_id')
|
|
), $lines);
|
|
|
|
log_activity('Jurnal', 'create', 'Membuat jurnal No Ref: ' . $noRef, 'success');
|
|
return json_response(true, 'Jurnal berhasil disimpan.', array(
|
|
'journal_id' => $journalId,
|
|
'no_ref' => $noRef
|
|
));
|
|
} catch (Throwable $exception) {
|
|
return business_exception_response($exception);
|
|
}
|
|
}
|
|
|
|
public function update_rejected($id)
|
|
{
|
|
if (strtoupper($this->input->method()) !== 'POST') return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
|
|
try {
|
|
$accounts=(array)$this->input->post('account_id');$debit=(array)$this->input->post('debit');$kredit=(array)$this->input->post('kredit');$lines=array();
|
|
foreach($accounts as$index=>$accountId){$d=isset($debit[$index])?$this->parseNominal($debit[$index]):0;$k=isset($kredit[$index])?$this->parseNominal($kredit[$index]):0;if($d<=0&&$k<=0)continue;$lines[]=array('account_id'=>(int)$accountId,'debit'=>$d,'kredit'=>$k);}
|
|
$journal=$this->journalservice->reviseRejected((int)$id,array('tanggal'=>(string)$this->input->post('tanggal',true),'keterangan'=>trim((string)$this->input->post('keterangan',true))),$lines,(int)$this->session->userdata('user_id'));
|
|
log_activity('Jurnal','revise_rejected','Mengoreksi jurnal ditolak '.$journal->no_ref.' menjadi Draft.','success');
|
|
return json_response(true,'Koreksi jurnal berhasil disimpan sebagai Draft dan dapat diajukan kembali.',array('journal_id'=>$journal->id));
|
|
} catch (Throwable $exception) { return business_exception_response($exception); }
|
|
}
|
|
|
|
/** Menerima nominal raw maupun berformat 20,000 / 20.000 / 20.000,50. */
|
|
private function parseNominal($value)
|
|
{
|
|
$value = trim(str_ireplace(array('Rp', ' '), '', (string) $value));
|
|
if ($value === '') return 0.0;
|
|
$comma = strrpos($value, ','); $dot = strrpos($value, '.');
|
|
if ($comma !== false && $dot !== false) {
|
|
$value = $comma > $dot ? str_replace(',', '.', str_replace('.', '', $value)) : str_replace(',', '', $value);
|
|
} elseif ($comma !== false) {
|
|
$decimal = strlen($value) - $comma - 1;
|
|
$value = $decimal > 0 && $decimal <= 2 ? str_replace(',', '.', $value) : str_replace(',', '', $value);
|
|
} elseif ($dot !== false && strlen($value) - $dot - 1 === 3) {
|
|
$value = str_replace('.', '', $value);
|
|
}
|
|
return is_numeric($value) ? round((float) $value, 2) : 0.0;
|
|
}
|
|
|
|
// =========================
|
|
// DETAIL
|
|
// =========================
|
|
public function detail($id)
|
|
{
|
|
$header = $this->db->get_where('journals',['id'=>$id,'company_id'=>$this->companycontext->id()])->row();
|
|
if (!$header) return json_response(false, 'Jurnal tidak ditemukan.', array(), 404);
|
|
|
|
$detail = $this->db
|
|
->select('accounts.kode_akun, accounts.nama_akun, journal_details.*')
|
|
->from('journal_details')
|
|
->join('accounts','accounts.id = journal_details.account_id')
|
|
->where('journal_id',$id)
|
|
->get()->result();
|
|
|
|
// print_r($header);
|
|
|
|
$userid = $header->created_by;
|
|
$user = $this->db
|
|
->select('nama')
|
|
->where('id', $userid)
|
|
->get('users')
|
|
->row();
|
|
|
|
$history = $this->db
|
|
->select('journal_status_histories.*, users.nama changed_by_name')
|
|
->from('journal_status_histories')
|
|
->join('users', 'users.id = journal_status_histories.changed_by', 'left')
|
|
->where('journal_id', $id)
|
|
->order_by('journal_status_histories.id', 'ASC')
|
|
->get()->result();
|
|
|
|
echo json_encode([
|
|
'header'=>$header,
|
|
'detail'=>$detail,
|
|
'user' =>$user,
|
|
'history'=>$history
|
|
]);
|
|
}
|
|
|
|
// =========================
|
|
// DELETE
|
|
// =========================
|
|
public function delete($id)
|
|
{
|
|
if (strtoupper($this->input->method()) !== 'POST') {
|
|
return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
|
|
}
|
|
$this->requireAdmin();
|
|
$journal = $this->db->get_where('journals', ['id' => $id])->row();
|
|
|
|
// VALIDASI
|
|
if (!$journal) {
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Jurnal tidak ditemukan'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
if (!in_array($journal->status, array('draft', 'rejected'), true)) {
|
|
return json_response(false, 'Hanya jurnal draft atau rejected yang dapat dihapus.', array(), 422);
|
|
}
|
|
|
|
$this->db->trans_start();
|
|
|
|
// =========================
|
|
// HANDLE STOCK LOGS
|
|
// =========================
|
|
if ($journal->ref_type == 'stock_logs') {
|
|
|
|
$stock = $this->db
|
|
->select('item_id, warehouse_id')
|
|
->where('id', $journal->ref_id)
|
|
->get('stock_logs')
|
|
->row();
|
|
|
|
if ($stock) {
|
|
|
|
// hapus log stok
|
|
$this->db->delete('stock_logs', ['id' => $journal->ref_id]);
|
|
|
|
// update stok ulang (pakai item_id saja biar gak ganggu sistem lama)
|
|
$this->update_item_stok($stock->item_id);
|
|
}
|
|
}
|
|
|
|
// =========================
|
|
// DELETE JOURNAL
|
|
// =========================
|
|
$this->db->delete('journal_details', ['journal_id' => $id]);
|
|
$this->db->delete('journals', ['id' => $id]);
|
|
|
|
$this->db->trans_complete();
|
|
|
|
// =========================
|
|
// VALIDASI TRANSAKSI
|
|
// =========================
|
|
if (!$this->db->trans_status()) {
|
|
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Gagal hapus jurnal'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// ================= LOG ACTIVITY =================
|
|
log_activity(
|
|
'Jurnal',
|
|
'delete',
|
|
'Menghapus jurnal no_ref: ' . $journal->no_ref . ' ket: ' . $journal->keterangan,
|
|
'success'
|
|
);
|
|
|
|
echo json_encode([
|
|
'status' => true, // ✅ FIX
|
|
'message' => 'Jurnal berhasil dihapus',
|
|
'journal' => $journal->ref_type
|
|
]);
|
|
}
|
|
|
|
public function submit($id)
|
|
{
|
|
return $this->workflowAction(function () use ($id) {
|
|
return $this->journalworkflowservice->submit($id, $this->session->userdata('user_id'));
|
|
}, 'Jurnal berhasil diajukan.');
|
|
}
|
|
|
|
public function approve($id)
|
|
{
|
|
$this->requireAdmin();
|
|
return $this->workflowAction(function () use ($id) {
|
|
return $this->journalworkflowservice->approve($id, $this->session->userdata('user_id'));
|
|
}, 'Approval jurnal berhasil diproses. Setelah approval final, jurnal otomatis diposting.');
|
|
}
|
|
|
|
public function post($id)
|
|
{
|
|
$this->requireAdmin();
|
|
return $this->workflowAction(function () use ($id) {
|
|
return $this->journalworkflowservice->post($id, $this->session->userdata('user_id'));
|
|
}, 'Jurnal berhasil diposting.');
|
|
}
|
|
|
|
public function reject($id)
|
|
{
|
|
$this->requireAdmin();
|
|
$reason = $this->input->post('reason', true);
|
|
return $this->workflowAction(function () use ($id, $reason) {
|
|
return $this->journalworkflowservice->reject($id, $this->session->userdata('user_id'), $reason);
|
|
}, 'Jurnal berhasil ditolak.');
|
|
}
|
|
|
|
public function reverse($id)
|
|
{
|
|
$this->requireAdmin();
|
|
$date = $this->input->post('date', true);
|
|
$reason = $this->input->post('reason', true);
|
|
return $this->workflowAction(function () use ($id, $date, $reason) {
|
|
return $this->journalworkflowservice->reverse($id, $date, $this->session->userdata('user_id'), $reason);
|
|
}, 'Reversal jurnal berhasil dibuat.');
|
|
}
|
|
|
|
private function workflowAction(callable $callback, $message)
|
|
{
|
|
if (strtoupper($this->input->method()) !== 'POST') {
|
|
return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
|
|
}
|
|
try {
|
|
$result = $callback();
|
|
log_activity('Jurnal', 'workflow', $message, 'success');
|
|
return json_response(true, $message, array('result' => $result));
|
|
} catch (Throwable $exception) {
|
|
return business_exception_response($exception);
|
|
}
|
|
}
|
|
|
|
public function get_base_jurnal()
|
|
{
|
|
echo json_encode(
|
|
$this->db->order_by('kode','ASC')->get('base_journal')->result()
|
|
);
|
|
}
|
|
|
|
public function get_base_jurnal_detail($id)
|
|
{
|
|
echo json_encode(
|
|
$this->db->where('base_journal_id',$id)
|
|
->get('base_journal_detail')
|
|
->result()
|
|
);
|
|
}
|
|
|
|
private function update_item_stok($item_id)
|
|
{
|
|
$stok = $this->db->query("
|
|
SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok
|
|
FROM stock_logs
|
|
WHERE item_id = ?
|
|
", [$item_id])->row()->stok;
|
|
|
|
$this->db->where('id', $item_id)
|
|
->update('items', [
|
|
'stok' => $stok
|
|
]);
|
|
}
|
|
}
|