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; } }