939 lines
40 KiB
PHP
939 lines
40 KiB
PHP
<?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 startDate–endDate
|
||
* - 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;
|
||
}
|
||
}
|