Files
be-accone/application/controllers/mockup/scheduler/JurnalPiCicilanOrderAset.php
2026-07-15 10:48:53 +07:00

939 lines
40 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* JurnalPiCicilanOrderAset
*
* Controller scheduler untuk membuat jurnal akuntansi atas Purchase Invoice (PI)
* cicilan order aset secara otomatis.
*
* Alur bisnis proses:
* 1. Validasi parameter input (tanggal, branchCode, title, description, details)
* 2. Insert header jurnal ke tabel `jurnal`
* - Nomor jurnal di-generate via fn_numbering('J')
* - Tipe jurnal: PAYMENT
* - Berisi: title, description, tanggal, regionalID, kode cabang, branchCompanyID
* 3. Insert baris transaksi ke tabel `jurnal_tx` (untuk setiap baris cicilan)
* - Referensi ke jurnalID header
* - COA (Chart of Account) dan deskripsinya:
* * Sisi hutang cicilan → nilai DEBIT (diambil dari HutangCoaID di fa_class)
* * Sisi pembayaran cicilan → nilai KREDIT (diambil dari ItemCoaID di fa_class)
* 4. Insert data tambahan ke tabel `jurnal_addon` (jika ada addonValue atau mItemID)
* - Referensi ke jurnalID dan jurnalTxID
* - Berisi: addonCode (default: "PIINSTALMENT"), addonValue, mItemID
*
* Endpoint:
* POST /mockup/scheduler/JurnalPiCicilanOrderAset/CreateJournal
* POST /mockup/scheduler/JurnalPiCicilanOrderAset/GenerateJournalsFromContracts
* GET /mockup/scheduler/JurnalPiCicilanOrderAset/ListEligiblePurchaseOrderAssetContracts
*/
class JurnalPiCicilanOrderAset extends MY_Controller
{
var $db;
/**
* Endpoint default — menampilkan nama controller sebagai penanda aktif.
*/
public function index()
{
echo "Jurnal PI Cicilan Order Aset";
}
/**
* Konstruktor — inisialisasi parent controller.
*/
public function __construct()
{
parent::__construct();
}
/**
* CreateJournal — Membuat jurnal PI cicilan order aset.
*
* Proses bisnis:
* 1. Validasi semua parameter wajib: date, branchCode, title, description, details
* 2. Ambil periodeID dari tanggal transaksi (tabel periode)
* 3. Ambil regionalID dari branchCode (tabel m_branch)
* 4. Ambil branchCompanyID dari branchCode (tabel m_branch_companydetail)
* 5. Ambil jurnalTypeID dari kode tipe jurnal (default: "PAYMENT")
* 6. Generate nomor jurnal otomatis via fn_numbering('J')
* 7. Validasi balance: total debit harus sama dengan total credit
* 8. INSERT header jurnal ke tabel `jurnal`:
* - jurnalNo : nomor jurnal hasil generate
* - jurnalJurnalTypeID: tipe PAYMENT
* - jurnalTitle : judul jurnal
* - jurnalDescription: keterangan jurnal
* - jurnalDate : tanggal transaksi
* - JurnalS_RegionalID: ID regional cabang
* - jurnalM_BranchCode: kode cabang
* 9. Untuk setiap baris detail, INSERT ke `jurnal_tx`:
* - jurnalTxJurnalID : referensi ke header jurnal
* - jurnalTxCoaID : COA yang digunakan (hutang cicilan / kas/bank)
* - jurnalTxDescription: keterangan baris (nama cicilan/aset)
* - jurnalTxDebit : nilai hutang (sisi debit)
* - jurnalTxCredit : nilai bayar (sisi kredit)
* 10. Jika ada addonValue atau mItemID, INSERT ke `jurnal_addon`:
* - jurnalAddOnJurnalID : referensi ke header jurnal
* - jurnalAddOnJurnalTxID: referensi ke baris jurnal_tx
* - jurnalAddOnCode : kode addon (default: "PIINSTALMENT")
* - jurnalAddOnValue : nilai tambahan (misal: nomor PI/kontrak)
* - jurnalAddOnM_ItemID : ID item aset yang terkait
*
* Parameter (POST JSON):
* - date : Tanggal jurnal (YYYY-MM-DD). Default: hari ini.
* - branchCode : Kode cabang. WAJIB.
* - title : Judul jurnal. WAJIB.
* - description : Keterangan jurnal. WAJIB.
* - details : Array baris transaksi. WAJIB, minimal 1 baris.
* - coaID : ID Chart of Account. WAJIB per baris.
* - description : Keterangan baris.
* - debit : Nilai debit (hutang cicilan).
* - credit : Nilai kredit (pembayaran cicilan).
* - addonValue : (opsional) Nilai addon, misal nomor PI.
* - mItemID : (opsional) ID item aset.
* - addonCode : (opsional) Kode addon. Default: "PIINSTALMENT".
* - periodeID : (opsional) ID periode. Default: diambil dari tanggal.
* - regionalID : (opsional) ID regional. Default: diambil dari branchCode.
* - branchCompanyID: (opsional) ID branch company. Default: diambil dari branchCode.
* - jurnalTypeID : (opsional) ID tipe jurnal. Default: diambil dari jurnalTypeCode.
* - jurnalTypeCode: (opsional) Kode tipe jurnal. Default: "PAYMENT".
* - userID : ID user yang membuat jurnal.
*
* @return JSON { jurnalID, jurnalNo, detailCount, addonCount }
*/
public function CreateJournal()
{
try {
$para = $this->sys_input;
$date = isset($para["date"]) && $para["date"] != "" ? $para["date"] : date("Y-m-d");
if (!$this->isValidDate($date)) {
throw new Exception("Format tanggal tidak valid. Gunakan YYYY-MM-DD.");
}
$branchCode = isset($para["branchCode"]) ? trim($para["branchCode"]) : "";
if ($branchCode == "") {
throw new Exception("branchCode wajib diisi.");
}
$title = isset($para["title"]) ? trim($para["title"]) : "";
if ($title == "") {
throw new Exception("title wajib diisi.");
}
$description = isset($para["description"]) ? trim($para["description"]) : "";
if ($description == "") {
throw new Exception("description wajib diisi.");
}
$details = isset($para["details"]) && is_array($para["details"]) ? $para["details"] : [];
if (count($details) == 0) {
throw new Exception("details wajib diisi minimal 1 baris.");
}
$periodeID = isset($para["periodeID"]) && (int) $para["periodeID"] > 0
? (int) $para["periodeID"]
: $this->getPeriodeIDByDate($date);
if ($periodeID <= 0) {
throw new Exception("Periode jurnal tidak ditemukan untuk tanggal {$date}.");
}
$regionalID = isset($para["regionalID"]) && (int) $para["regionalID"] > 0
? (int) $para["regionalID"]
: $this->getRegionalIDByBranchCode($branchCode);
if ($regionalID <= 0) {
throw new Exception("regionalID wajib diisi atau harus bisa diturunkan dari branchCode.");
}
$branchCompanyID = isset($para["branchCompanyID"]) && (int) $para["branchCompanyID"] > 0
? (int) $para["branchCompanyID"]
: $this->getBranchCompanyIDByBranchCode($branchCode);
if ($branchCompanyID <= 0) {
throw new Exception("branchCompanyID wajib diisi atau harus bisa diturunkan dari branchCode.");
}
$jurnalTypeID = isset($para["jurnalTypeID"]) && (int) $para["jurnalTypeID"] > 0
? (int) $para["jurnalTypeID"]
: $this->getJurnalTypeID(isset($para["jurnalTypeCode"]) && $para["jurnalTypeCode"] != "" ? $para["jurnalTypeCode"] : "PAYMENT");
if ($jurnalTypeID <= 0) {
throw new Exception("jurnalTypeID / jurnalTypeCode tidak valid.");
}
$userID = 0;
$result = $this->insertJournal(
$date,
$branchCode,
$title,
$description,
$details,
$periodeID,
$regionalID,
$branchCompanyID,
$jurnalTypeID,
$userID
);
if ($result === false) {
exit;
}
$this->sys_ok($result);
} catch (Exception $exc) {
$this->sys_error($exc->getMessage());
}
}
/**
* GenerateJournalsFromContracts — Generate jurnal otomatis dari seluruh kontrak aset eligible.
*
* Fungsi ini mengintegrasikan data dari getEligiblePurchaseOrderAssetContracts()
* dengan proses pembuatan jurnal (insertJournal()) secara otomatis.
*
* Untuk setiap kontrak yang eligible, dibuat 1 jurnal dengan 2 baris transaksi:
* - Baris DEBIT : COA hutang cicilan (HutangCoaID dari fa_class) — mencatat hutang cicilan
* - Baris KREDIT : COA kas/bank/aset (ItemCoaID dari fa_class) — mencatat pembayaran cicilan
* Addon (jurnal_addon): menyimpan ID kontrak sebagai addonValue dan ItemID sebagai mItemID
*
* Parameter (POST JSON):
* - date : Tanggal jurnal (YYYY-MM-DD). Default: hari ini.
* - userID : ID user yang membuat jurnal.
* - dryRun : "Y" = simulasi tanpa simpan ke DB. Default: "N".
* - contractID : (opsional) Filter hanya 1 kontrak tertentu.
*
* Syarat kontrak dilewati (skipped):
* - branchCode kosong (data cabang belum lengkap)
* - COA hutang (HutangCoaID) tidak ditemukan di fa_class
* - COA bayar/aset (ItemCoaID) tidak ditemukan di fa_class
*
* @return JSON { tanggal, totalDibuat, totalDilewati, daftarDibuat[], daftarDilewati[] }
*/
public function GenerateJournalsFromContracts()
{
try {
$para = $this->sys_input;
$date = isset($para["date"]) && $para["date"] != "" ? $para["date"] : date("Y-m-d");
if (!$this->isValidDate($date)) {
throw new Exception("Format tanggal tidak valid. Gunakan YYYY-MM-DD.");
}
$userID = 0;
$dryRun = isset($para["dryRun"]) && ($para["dryRun"] === true || $para["dryRun"] == "Y" || $para["dryRun"] == "1");
$filterContractID = isset($para["contractID"]) && (int) $para["contractID"] > 0
? (int) $para["contractID"]
: null;
$monthStart = date("Y-m-01", strtotime($date));
$monthEnd = date("Y-m-t", strtotime($date));
// Ambil semua kontrak yang eligible untuk periode bulan ini
$contracts = $this->getEligiblePurchaseOrderAssetContracts($monthStart, $monthEnd);
$dibuat = [];
$dilewati = [];
foreach ($contracts as $kontrak) {
$contractID = (int) $kontrak["PurchaseOrderAssetContractID"];
// Filter 1 kontrak jika diminta
if ($filterContractID !== null && $contractID !== $filterContractID) {
continue;
}
$branchCode = $kontrak["M_BranchCode"] ?? "";
$regionalID = (int) ($kontrak["M_BranchS_RegionalID"] ?? 0);
$branchCompanyID = (int) ($kontrak["M_BranchCompanyID"] ?? 0);
$coaHutangID = (int) ($kontrak["HutangCoaID"] ?? 0); // COA debit dari fa_class (Fa_ClassHutangCoaID)
$coaBayarID = (int) ($kontrak["ItemCoaID"] ?? 0); // COA kredit dari fa_class (Fa_ClassCoaID)
$jumlah = (float) $kontrak["PurchaseOrderAssetContractInstallmentPayAmount"];
$itemID = (int) ($kontrak["PurchaseOrderSummaryItemID"] ?? 0);
$namaKontrak = $kontrak["PurchaseOrderAssetContractName"] != ""
? $kontrak["PurchaseOrderAssetContractName"]
: "Kontrak ID {$contractID}";
$nomorPO = $kontrak["PurchaseOrderNumber"] ?? "";
$cicilanKe = ((int) $kontrak["PurchaseOrderAssetContractInstallmentPaid"]) + 1;
$totalCicilan = (int) $kontrak["PurchaseOrderAssetContractInstallmentNumber"];
$periode = date("Y-m", strtotime($date));
// Validasi: branchCode harus ada
if ($branchCode == "") {
$dilewati[] = [
"kontrakID" => $contractID,
"alasan" => "Kode cabang (branchCode) tidak ditemukan. Pastikan warehouse PO sudah terhubung ke data cabang."
];
continue;
}
// Validasi: COA hutang harus ada (untuk sisi DEBIT)
if ($coaHutangID <= 0) {
$dilewati[] = [
"kontrakID" => $contractID,
"branchCode" => $branchCode,
"alasan" => "COA hutang tidak ditemukan. Pastikan item PO memiliki Fa_Class dengan Fa_ClassHutangCoaID."
];
continue;
}
// Validasi: COA bayar/aset harus ada (untuk sisi KREDIT)
if ($coaBayarID <= 0) {
$dilewati[] = [
"kontrakID" => $contractID,
"branchCode" => $branchCode,
"alasan" => "COA bayar/aset tidak ditemukan. Pastikan item PO memiliki Fa_Class dengan Fa_ClassCoaID."
];
continue;
}
// Mode simulasi: catat tanpa simpan ke DB
if ($dryRun) {
$dibuat[] = [
"kontrakID" => $contractID,
"branchCode" => $branchCode,
"namaKontrak" => $namaKontrak,
"jumlah" => $jumlah,
"coaHutangID" => $coaHutangID,
"coaBayarID" => $coaBayarID,
"keterangan" => "Mode simulasi (dryRun), data tidak disimpan."
];
continue;
}
// Susun title dan description jurnal
$title = "PI Cicilan Aset - {$nomorPO}";
$description = "Jurnal cicilan ke-{$cicilanKe} dari {$totalCicilan} | {$namaKontrak} | Periode {$periode}";
// Ambil periodeID dari tanggal
$periodeID = $this->getPeriodeIDByDate($date);
if ($periodeID <= 0) {
$dilewati[] = [
"kontrakID" => $contractID,
"branchCode" => $branchCode,
"alasan" => "Periode jurnal tidak ditemukan untuk tanggal {$date}."
];
continue;
}
// Ambil jurnalTypeID untuk tipe PAYMENT
$jurnalTypeID = $this->getJurnalTypeID("PAYMENT");
if ($jurnalTypeID <= 0) {
$dilewati[] = [
"kontrakID" => $contractID,
"branchCode" => $branchCode,
"alasan" => "Tipe jurnal PAYMENT tidak ditemukan di tabel jurnal_type."
];
continue;
}
// Ambil description detail dari database fa_class dengan fallback jika kosong
$descHutang = isset($kontrak["HutangCoaDesc"]) && $kontrak["HutangCoaDesc"] != ""
? $kontrak["HutangCoaDesc"]
: "Hutang cicilan aset - {$namaKontrak}";
$descBayar = isset($kontrak["ItemCoaDesc"]) && $kontrak["ItemCoaDesc"] != ""
? $kontrak["ItemCoaDesc"]
: "Pembayaran cicilan aset - {$namaKontrak}";
// Susun 2 baris detail jurnal:
// Baris 1 — DEBIT : hutang cicilan aset (COA dari fa_class)
// Baris 2 — KREDIT : pembayaran cicilan (COA kas/bank dari parameter)
$details = [
[
"coaID" => $coaHutangID,
"description" => $descHutang,
"debit" => $jumlah,
"credit" => 0,
"addonValue" => (string) $contractID,
"mItemID" => $itemID,
"addonCode" => "PIINSTALMENT"
],
[
"coaID" => $coaBayarID,
"description" => $descBayar,
"debit" => 0,
"credit" => $jumlah,
"addonValue" => (string) $contractID,
"mItemID" => $itemID,
"addonCode" => "PIINSTALMENT"
]
];
// Panggil insertJournal() untuk simpan ke DB
$result = $this->insertJournal(
$date,
$branchCode,
$title,
$description,
$details,
$periodeID,
$regionalID,
$branchCompanyID,
$jurnalTypeID,
$userID
);
if ($result === false) {
$dilewati[] = [
"kontrakID" => $contractID,
"branchCode" => $branchCode,
"alasan" => "Gagal menyimpan jurnal ke database."
];
continue;
}
$dibuat[] = [
"kontrakID" => $contractID,
"branchCode" => $branchCode,
"namaKontrak" => $namaKontrak,
"jurnalID" => $result["jurnalID"],
"jurnalNo" => $result["jurnalNo"],
"jumlah" => $jumlah
];
}
$this->sys_ok([
"tanggal" => $date,
"periodeAwal" => $monthStart,
"periodeAkhir" => $monthEnd,
"totalDibuat" => count($dibuat),
"totalDilewati" => count($dilewati),
"daftarDibuat" => $dibuat,
"daftarDilewati" => $dilewati
]);
} catch (Exception $exc) {
$this->sys_error($exc->getMessage());
}
}
/**
* ListEligiblePurchaseOrderAssetContracts — Menampilkan daftar kontrak aset
* yang memenuhi syarat untuk dibuatkan jurnal PI cicilan pada periode tertentu.
*
* Syarat kontrak eligible:
* - Kontrak aktif dan berstatus "belum lunas"
* - Nilai cicilan > 0
* - Jumlah cicilan terbayar < total cicilan
* - Tanggal kontrak masuk dalam rentang startDateendDate
* - PO sudah berstatus Approved
* - Barang sudah diterima (receive_order_po sudah confirmed)
*
* Endpoint ini digunakan untuk preview/verifikasi sebelum proses generate jurnal
* dijalankan secara massal oleh CRON.
*
* Parameter (GET/POST):
* - startDate : Awal periode (YYYY-MM-DD). Default: tanggal 1 bulan ini.
* - endDate : Akhir periode (YYYY-MM-DD). Default: tanggal akhir bulan ini.
*
* @return JSON { startDate, endDate, total, records[] }
*/
public function ListEligiblePurchaseOrderAssetContracts()
{
try {
$para = $this->sys_input;
$startDate = isset($para["startDate"]) && $para["startDate"] != ""
? $para["startDate"]
: date("Y-m-01");
$endDate = isset($para["endDate"]) && $para["endDate"] != ""
? $para["endDate"]
: date("Y-m-t");
if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) {
throw new Exception("Format tanggal tidak valid. Gunakan YYYY-MM-DD.");
}
if (strtotime($startDate) > strtotime($endDate)) {
throw new Exception("startDate tidak boleh lebih besar dari endDate");
}
$records = $this->getEligiblePurchaseOrderAssetContracts($startDate, $endDate);
$this->sys_ok([
"startDate" => $startDate,
"endDate" => $endDate,
"total" => count($records),
"records" => $records
]);
} catch (Exception $exc) {
$this->sys_error($exc->getMessage());
}
}
// =========================================================================
// FUNGSI PRIVATE / HELPER
// =========================================================================
/**
* insertJournal — Menyimpan 1 jurnal lengkap ke database (header + tx + addon).
*
* Fungsi internal yang dipanggil oleh CreateJournal() dan GenerateJournalsFromContracts().
* Mengelola transaksi DB secara penuh (begin → commit/rollback).
*
* Alur:
* 1. Validasi balance debit == kredit
* 2. Generate nomor jurnal via fn_numbering('J')
* 3. INSERT header ke tabel `jurnal`
* 4. INSERT baris transaksi ke `jurnal_tx` (per detail)
* 5. INSERT addon ke `jurnal_addon` (jika addonValue atau mItemID ada)
*
* @param string $date Tanggal jurnal (YYYY-MM-DD)
* @param string $branchCode Kode cabang
* @param string $title Judul jurnal
* @param string $description Keterangan jurnal
* @param array $details Baris transaksi [ coaID, description, debit, credit, addonValue?, mItemID?, addonCode? ]
* @param int $periodeID ID periode akuntansi
* @param int $regionalID ID regional
* @param int $branchCompanyID ID branch company
* @param int $jurnalTypeID ID tipe jurnal
* @param int $userID ID user pembuat jurnal
*
* @return array|false Array { jurnalID, jurnalNo, detailCount, addonCount }, atau false jika gagal
*/
private function insertJournal($date, $branchCode, $title, $description, $details, $periodeID, $regionalID, $branchCompanyID, $jurnalTypeID, $userID)
{
// Validasi balance sebelum menyimpan
$totalDebit = 0;
$totalCredit = 0;
foreach ($details as $idx => $detail) {
$debit = isset($detail["debit"]) ? (float) $detail["debit"] : 0;
$credit = isset($detail["credit"]) ? (float) $detail["credit"] : 0;
if ($debit < 0 || $credit < 0) {
$this->sys_error("Nilai debit/credit tidak boleh negatif pada detail ke-" . ($idx + 1));
return false;
}
$totalDebit += $debit;
$totalCredit += $credit;
}
if (round($totalDebit, 2) !== round($totalCredit, 2)) {
$this->sys_error("Total debit dan credit harus sama. Debit={$totalDebit}, Credit={$totalCredit}");
return false;
}
$jurnalNo = $this->generateJournalNumber();
if ($jurnalNo === false || $jurnalNo == "") {
$this->sys_error("Gagal generate nomor jurnal.");
return false;
}
$this->db->trans_begin();
// -----------------------------------------------------------------
// INSERT header jurnal ke tabel `jurnal`
// -----------------------------------------------------------------
$sqlHeader = "INSERT INTO jurnal (
jurnalM_BranchCompanyID,
JurnalS_RegionalID,
jurnalM_BranchCode,
jurnalperiodeID,
jurnalNo,
jurnalTitle,
jurnalDescription,
jurnalDate,
jurnalJurnalTypeID,
jurnalCreated,
jurnalM_UserID
) VALUES (?,?,?,?,?,?,?,?,?,NOW(),?)";
$qryHeader = $this->db->query($sqlHeader, [
$branchCompanyID,
$regionalID,
$branchCode,
$periodeID,
$jurnalNo,
$title,
$description,
$date,
$jurnalTypeID,
$userID
]);
if (!$qryHeader) {
$this->db->trans_rollback();
$this->sys_error_db("Gagal insert jurnal header.");
return false;
}
$jurnalID = $this->db->insert_id();
$createdAddonCount = 0;
// -----------------------------------------------------------------
// INSERT baris transaksi ke tabel `jurnal_tx` (per baris cicilan)
// -----------------------------------------------------------------
foreach ($details as $idx => $detail) {
$coaID = isset($detail["coaID"]) ? (int) $detail["coaID"] : 0;
$descriptionTx = isset($detail["description"]) ? trim($detail["description"]) : "";
$debit = isset($detail["debit"]) ? (float) $detail["debit"] : 0;
$credit = isset($detail["credit"]) ? (float) $detail["credit"] : 0;
if ($coaID <= 0) {
$this->db->trans_rollback();
$this->sys_error_db("coaID wajib diisi pada detail ke-" . ($idx + 1));
return false;
}
// Sisi debit = hutang cicilan (COA hutang aset dari fa_class)
// Sisi kredit = pembayaran cicilan (COA kas/bank)
$sqlTx = "INSERT INTO jurnal_tx (
jurnalTxJurnalID,
jurnalTxCoaID,
jurnalTxDescription,
jurnalTxDebit,
jurnalTxCredit,
jurnalTxCreated,
jurnalTxM_UserID
) VALUES (?,?,?,?,?,NOW(),?)";
$qryTx = $this->db->query($sqlTx, [
$jurnalID,
$coaID,
$descriptionTx,
$debit,
$credit,
$userID
]);
if (!$qryTx) {
$this->db->trans_rollback();
$this->sys_error_db("Gagal insert jurnal_tx pada detail ke-" . ($idx + 1));
return false;
}
$jurnalTxID = $this->db->insert_id();
// -----------------------------------------------------------------
// INSERT addon ke `jurnal_addon` (jika addonValue atau mItemID ada)
// addonValue = ID kontrak aset (untuk tracing)
// mItemID = ID item aset dari purchase_order_summary
// -----------------------------------------------------------------
$addonValue = isset($detail["addonValue"]) ? trim($detail["addonValue"]) : "";
$mItemID = isset($detail["mItemID"]) ? (int) $detail["mItemID"] : 0;
$addonCode = isset($detail["addonCode"]) && $detail["addonCode"] != "" ? trim($detail["addonCode"]) : "PIINSTALMENT";
if ($addonValue != "" || $mItemID > 0) {
$sqlAddon = "INSERT INTO jurnal_addon (
jurnalAddOnJurnalID,
jurnalAddOnJurnalTxID,
jurnalAddOnCode,
jurnalAddOnValue,
jurnalAddOnM_ItemID,
jurnalAddOnCreated,
jurnalAddOnCreatedUserID
) VALUES (?,?,?,?,?,NOW(),?)";
$qryAddon = $this->db->query($sqlAddon, [
$jurnalID,
$jurnalTxID,
$addonCode,
$addonValue,
$mItemID > 0 ? $mItemID : null,
$userID
]);
if (!$qryAddon) {
$this->db->trans_rollback();
$this->sys_error_db("Gagal insert jurnal_addon pada detail ke-" . ($idx + 1));
return false;
}
$createdAddonCount++;
}
}
if ($this->db->trans_status() === false) {
$this->db->trans_rollback();
$this->sys_error_db("Transaksi jurnal gagal.");
return false;
}
$this->db->trans_commit();
return [
"jurnalID" => $jurnalID,
"jurnalNo" => $jurnalNo,
"detailCount" => count($details),
"addonCount" => $createdAddonCount
];
}
/**
* generateJournalNumber — Generate nomor jurnal otomatis.
*
* Memanggil stored function MySQL fn_numbering('J') untuk mendapatkan
* nomor jurnal berikutnya sesuai urutan penomoran yang berlaku.
*
* @return string|false Nomor jurnal (misal: "J/2025/07/0001"), atau false jika gagal.
*/
private function generateJournalNumber()
{
$qry = $this->db->query("SELECT fn_numbering('J') AS JNumber");
if (!$qry || $qry->num_rows() == 0) {
return false;
}
return $qry->row_array()["JNumber"];
}
/**
* getPeriodeIDByDate — Ambil ID periode akuntansi berdasarkan tanggal transaksi.
*
* Periode diambil dari tabel `periode` yang aktif dan mencakup tanggal yang diberikan.
* Digunakan untuk mengisi field jurnalperiodeID pada header jurnal.
*
* @param string $date Tanggal transaksi (format YYYY-MM-DD)
* @return int periodeID yang sesuai, atau 0 jika tidak ditemukan
*/
private function getPeriodeIDByDate($date)
{
$sql = "SELECT periodeID
FROM periode
WHERE periodeIsActive = 'Y'
AND DATE(?) BETWEEN periodeStartDate AND periodeEndDate
LIMIT 1";
$qry = $this->db->query($sql, [$date]);
if (!$qry || $qry->num_rows() == 0) {
return 0;
}
return (int) $qry->row_array()["periodeID"];
}
/**
* getBranchCompanyIDByBranchCode — Ambil ID branch company berdasarkan kode cabang.
*
* Data diambil dari tabel `m_branch_companydetail` yang aktif.
* Digunakan untuk mengisi field jurnalM_BranchCompanyID pada header jurnal.
*
* @param string $branchCode Kode cabang (misal: "JKT-01")
* @return int branchCompanyID yang sesuai, atau 0 jika tidak ditemukan
*/
private function getBranchCompanyIDByBranchCode($branchCode)
{
$sql = "SELECT M_BranchCompanyDetailM_BranchCompanyID AS branchCompanyID
FROM m_branch_companydetail
WHERE M_BranchCompanyDetailM_BranchCode = ?
AND M_BranchCompanyDetailIsActive = 'Y'
LIMIT 1";
$qry = $this->db->query($sql, [$branchCode]);
if (!$qry || $qry->num_rows() == 0) {
return 0;
}
return (int) $qry->row_array()["branchCompanyID"];
}
/**
* getRegionalIDByBranchCode — Ambil ID regional berdasarkan kode cabang.
*
* Data diambil dari tabel `m_branch` yang aktif.
* Digunakan untuk mengisi field JurnalS_RegionalID pada header jurnal.
*
* @param string $branchCode Kode cabang (misal: "JKT-01")
* @return int regionalID yang sesuai, atau 0 jika tidak ditemukan
*/
private function getRegionalIDByBranchCode($branchCode)
{
$sql = "SELECT M_BranchS_RegionalID AS regionalID
FROM m_branch
WHERE M_BranchCode = ?
AND M_BranchIsActive = 'Y'
LIMIT 1";
$qry = $this->db->query($sql, [$branchCode]);
if (!$qry || $qry->num_rows() == 0) {
return 0;
}
return (int) $qry->row_array()["regionalID"];
}
/**
* getJurnalTypeID — Ambil ID tipe jurnal berdasarkan kode tipe.
*
* Data diambil dari tabel `jurnal_type` yang aktif.
* Untuk PI cicilan order aset, kode yang digunakan adalah "PAYMENT".
*
* @param string $jurnalTypeCode Kode tipe jurnal (misal: "PAYMENT")
* @return int JurnalTypeID yang sesuai, atau 0 jika tidak ditemukan
*/
private function getJurnalTypeID($jurnalTypeCode)
{
$sql = "SELECT JurnalTypeID
FROM jurnal_type
WHERE JurnalTypeCode = ?
AND JurnalTypeIsActive = 'Y'
LIMIT 1";
$qry = $this->db->query($sql, [$jurnalTypeCode]);
if (!$qry || $qry->num_rows() == 0) {
return 0;
}
return (int) $qry->row_array()["JurnalTypeID"];
}
/**
* getEligiblePurchaseOrderAssetContracts — Ambil kontrak aset yang layak dibuatkan jurnal.
*
* Query ini mengambil semua kontrak dari `purchase_order_asset_contract` yang memenuhi
* syarat berikut:
* - Kontrak aktif, status "belum lunas", nilai cicilan > 0
* - Cicilan terbayar masih kurang dari total cicilan
* - Tanggal kontrak masuk dalam rentang periode yang diberikan
* - Tanggal jatuh tempo cicilan sudah lewat atau sama dengan hari ini
* - PO sudah Approved dan aktif
* - Barang sudah diterima dan receive order sudah confirmed
*
* Data yang dikembalikan mencakup:
* - Detail kontrak (ID, nama, tanggal, nilai cicilan)
* - Data PO (nomor PO, supplier, payment term, warehouse)
* - Data cabang (kode cabang, nama, regionalID, branchCompanyID)
* - Data item PO pertama (sebagai referensi baris jurnal)
*
* @param string $startDate Awal periode filter (YYYY-MM-DD)
* @param string $endDate Akhir periode filter (YYYY-MM-DD)
* @return array Array of row kontrak yang eligible
*/
private function getEligiblePurchaseOrderAssetContracts($startDate, $endDate)
{
$sql = "SELECT
c.PurchaseOrderAssetContractID,
c.PurchaseOrderAssetContractPurchaseOrderID,
c.PurchaseOrderAssetContractName,
c.PurchaseOrderAssetContractStartDate,
c.PurchaseOrderAssetContractEndDate,
c.PurchaseOrderAssetContractInstallmentNumber,
c.PurchaseOrderAssetContractInstallmentPaid,
c.PurchaseOrderAssetContractInstallmentDate,
c.PurchaseOrderAssetContractInstallmentPayAmount,
c.PurchaseOrderAssetContractReceiveOrderPoID,
po.PurchaseOrderID,
po.PurchaseOrderNumber,
po.PurchaseOrderSupplierID,
po.PurchaseOrderPaymentTerm,
po.PurchaseOrderWarehouseType,
po.PurchaseOrderWarehouseID,
wh.WarehouseCode,
wh.WarehouseName,
wh.WarehouseType,
wh.WarehouseS_RegionalID AS WarehouseS_RegionalID,
wh.WarehouseM_BranchID AS WarehouseM_BranchID,
ro.ReceiveOrderPoID,
ro.ReceiveOrderPoConfirmed,
mi.M_ItemID AS ItemID,
mi.M_ItemCode AS ItemCode,
mi.M_ItemDesc AS ItemDesc,
mi.M_ItemFa_ClassID,
fc.Fa_ClassID AS ItemFaClassID,
fc.Fa_ClassName AS ItemFaClassName,
coa_bayar.coaID AS ItemCoaID,
coa_bayar.coaAccountNo AS ItemCoaAccountNo,
coa_bayar.coaDescription AS ItemCoaDesc,
coa_hutang.coaID AS HutangCoaID,
coa_hutang.coaAccountNo AS HutangCoaAccountNo,
coa_hutang.coaDescription AS HutangCoaDesc,
m_branch.M_BranchCode,
m_branch.M_BranchName,
m_branch.M_BranchS_RegionalID,
m_branch_companydetail.M_BranchCompanyDetailM_BranchCompanyID AS M_BranchCompanyID,
ps.PurchaseOrderSummaryID,
ps.PurchaseOrderSummaryItemID,
ps.PurchaseOrderSummaryItemUnitID
FROM purchase_order_asset_contract c
-- Pastikan PO sudah Approved dan aktif
JOIN purchase_order po
ON po.PurchaseOrderID = c.PurchaseOrderAssetContractPurchaseOrderID
AND po.PurchaseOrderIsActive = 'Y'
AND po.PurchaseOrderStatus = 'Approved'
-- Data warehouse/gudang tujuan PO
LEFT JOIN warehouse wh
ON wh.WarehouseID = po.PurchaseOrderWarehouseID
-- Pastikan barang sudah diterima (receive order confirmed)
JOIN (
SELECT
rd.ReceiveOrderPoDetailPurchaseOrderID,
MIN(ro0.ReceiveOrderPoID) AS ReceiveOrderPoID,
MAX(ro0.ReceiveOrderPoConfirmed) AS ReceiveOrderPoConfirmed
FROM receive_order_po ro0
JOIN receive_order_po_detail rd
ON rd.ReceiveOrderPoDetailReceiveOrderPoID = ro0.ReceiveOrderPoID
AND rd.ReceiveOrderPoDetailIsActive = 'Y'
WHERE ro0.ReceiveOrderPoIsActive = 'Y'
AND ro0.ReceiveOrderPoConfirmed = 'Y'
GROUP BY rd.ReceiveOrderPoDetailPurchaseOrderID
) ro
ON ro.ReceiveOrderPoDetailPurchaseOrderID = po.PurchaseOrderID
-- Data cabang dari warehouse
LEFT JOIN m_branch
ON m_branch.M_BranchID = wh.WarehouseM_BranchID
AND m_branch.M_BranchIsActive = 'Y'
-- Data branch company untuk header jurnal
LEFT JOIN m_branch_companydetail
ON m_branch_companydetail.M_BranchCompanyDetailM_BranchCode = m_branch.M_BranchCode
AND m_branch_companydetail.M_BranchCompanyDetailIsActive = 'Y'
-- Ambil 1 item PO pertama sebagai referensi baris jurnal
LEFT JOIN (
SELECT ps0.*
FROM purchase_order_summary ps0
JOIN (
SELECT
PurchaseOrderSummaryPurchaseOrderID,
MIN(PurchaseOrderSummaryID) AS PurchaseOrderSummaryID
FROM purchase_order_summary
WHERE PurchaseOrderSummaryIsActive = 'Y'
GROUP BY PurchaseOrderSummaryPurchaseOrderID
) psx
ON psx.PurchaseOrderSummaryID = ps0.PurchaseOrderSummaryID
) ps
ON ps.PurchaseOrderSummaryPurchaseOrderID = po.PurchaseOrderID
-- COA jurnal diambil dari item -> fa_class
LEFT JOIN m_item mi
ON mi.M_ItemID = ps.PurchaseOrderSummaryItemID
AND mi.M_ItemIsActive = 'Y'
LEFT JOIN fa_class fc
ON fc.Fa_ClassID = mi.M_ItemFa_ClassID
AND fc.Fa_ClassIsActive = 'Y'
-- Join langsung ke tabel coa untuk memastikan kevalidan akun dan deskripsi
LEFT JOIN coa coa_hutang
ON coa_hutang.coaID = fc.Fa_ClassHutangCoaID
AND coa_hutang.coaIsActive = 'Y'
LEFT JOIN coa coa_bayar
ON coa_bayar.coaID = fc.Fa_ClassCoaID
AND coa_bayar.coaIsActive = 'Y'
WHERE c.PurchaseOrderAssetContractIsActive = 'Y'
AND c.PurchaseOrderAssetContractStatus = 'belum lunas'
-- Hanya kontrak yang ada nilai cicilannya
AND IFNULL(c.PurchaseOrderAssetContractInstallmentPayAmount, 0) > 0
-- Hanya kontrak yang belum selesai seluruh cicilannya
AND IFNULL(c.PurchaseOrderAssetContractInstallmentPaid, 0) < IFNULL(c.PurchaseOrderAssetContractInstallmentNumber, 0)
-- Kontrak sudah mulai sebelum atau pada akhir periode
AND DATE(c.PurchaseOrderAssetContractStartDate) <= DATE(?)
-- Kontrak belum berakhir (atau tidak ada tanggal akhir)
AND (
c.PurchaseOrderAssetContractEndDate IS NULL
OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?)
)
-- Tanggal jatuh tempo cicilan sudah tiba
AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?
ORDER BY c.PurchaseOrderAssetContractID ASC";
$qry = $this->db->query($sql, [$endDate, $startDate, (int) date("d", strtotime($endDate))]);
if (!$qry) {
$this->sys_error_db("Gagal mengambil data purchase_order_asset_contract.");
exit;
}
return $qry->result_array();
}
/**
* isValidDate — Validasi format tanggal YYYY-MM-DD.
*
* @param string $tanggal String tanggal yang akan divalidasi
* @return bool true jika format valid, false jika tidak
*/
private function isValidDate($tanggal)
{
$d = DateTime::createFromFormat("Y-m-d", $tanggal);
return $d && $d->format("Y-m-d") === $tanggal;
}
}