0 * 3. Jumlah cicilan terbayar < total cicilan * 4. Tanggal kontrak masuk dalam bulan yang dituju * 5. Invoice dasar (tukar faktur) sudah ada untuk PO tersebut * 6. PO sudah berstatus Approved */ public function GenerateMonthlyInvoices() { try { $para = $this->sys_input; // Ambil userID dari token JWT (sys_user). Fallback ke 1 jika CRON berjalan tanpa token. $userID = !empty($this->sys_user["M_UserID"]) ? (int) $this->sys_user["M_UserID"] : 1; $user = $this->getCronUser($userID); $startDate = isset($para["startDate"]) && $para["startDate"] != "" ? $para["startDate"] : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-01", strtotime($para["date"])) : date("Y-m-01")); $endDate = isset($para["endDate"]) && $para["endDate"] != "" ? $para["endDate"] : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-t", strtotime($para["date"])) : date("Y-m-t")); $dryRun = isset($para["dryRun"]) && ($para["dryRun"] === true || $para["dryRun"] == "Y" || $para["dryRun"] == "1"); // Validasi format tanggal if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) { throw new Exception("Format tanggal tidak valid. Gunakan format YYYY-MM-DD, contoh: 2025-07-01"); } if (strtotime($startDate) > strtotime($endDate)) { throw new Exception("startDate tidak boleh lebih besar dari endDate"); } // Tentukan rentang bulan berdasarkan tanggal acuan $monthStart = $startDate; $monthEnd = $endDate; $dayOfMonth = (int) date("d", strtotime($endDate)); // ------------------------------------------------------------------------- // Ambil semua kontrak cicilan yang memenuhi syarat pada bulan ini // ------------------------------------------------------------------------- $sqlKontrak = "SELECT c.PurchaseOrderAssetContractID, c.PurchaseOrderAssetContractPurchaseOrderID, c.PurchaseOrderAssetContractReceiveOrderPoID, c.PurchaseOrderAssetContractName, c.PurchaseOrderAssetContractStartDate, c.PurchaseOrderAssetContractEndDate, c.PurchaseOrderAssetContractInstallmentNumber, c.PurchaseOrderAssetContractInstallmentPaid, c.PurchaseOrderAssetContractInstallmentDate, c.PurchaseOrderAssetContractInstallmentPayAmount, po.PurchaseOrderID, po.PurchaseOrderNumber, po.PurchaseOrderSupplierID, po.PurchaseOrderPaymentTerm, po.PurchaseOrderWarehouseType, po.PurchaseOrderWarehouseID, ro.ReceiveOrderPoID, si_base.SupplierInvoiceID AS BaseSupplierInvoiceID, si_base.SupplierInvoiceNumber AS BaseSupplierInvoiceNumber, 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' JOIN receive_order_po ro ON ro.ReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID AND ro.ReceiveOrderPoIsActive = 'Y' -- Pastikan tukar faktur (invoice dasar) sudah ada untuk PO ini JOIN supplier_invoice si_base ON si_base.SupplierInvoiceID = ( SELECT MIN(si0.SupplierInvoiceID) FROM supplier_invoice si0 WHERE si0.SupplierInvoiceIsActive = 'Y' AND ( si0.SupplierInvoiceReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID OR EXISTS ( SELECT 1 FROM supplier_invoice_detail sid0 WHERE sid0.SupplierInvoiceDetailSupplierInvoiceID = si0.SupplierInvoiceID AND sid0.SupplierInvoiceDetailReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID AND sid0.SupplierInvoiceDetailIsActive = 'Y' ) ) ) -- Ambil 1 item PO pertama sebagai referensi baris detail PI 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 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 bulan ini AND DATE(c.PurchaseOrderAssetContractStartDate) <= DATE(?) -- Kontrak belum berakhir (atau tidak ada tanggal akhir) AND ( c.PurchaseOrderAssetContractEndDate IS NULL OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?) ) AND c.PurchaseOrderAssetContractReceiveOrderPoID > 0 -- Tanggal jatuh tempo cicilan sudah melewati atau sama dengan hari ini AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?"; // Filter kontrak tertentu jika diminta if (isset($para["contractID"]) && (int) $para["contractID"] > 0) { $sqlKontrak .= " AND c.PurchaseOrderAssetContractID = ?"; $params = [$monthEnd, $monthStart, $dayOfMonth, (int) $para["contractID"]]; } else { $params = [$monthEnd, $monthStart, $dayOfMonth]; } $qryKontrak = $this->db->query($sqlKontrak, $params); if (!$qryKontrak) { $this->sys_error_db("Gagal mengambil daftar kontrak cicilan dari database."); exit; } $berhasil = []; // PI yang berhasil dibuat $dilewati = []; // PI yang dilewati beserta alasannya // ------------------------------------------------------------------------- // Proses tiap kontrak satu per satu // ------------------------------------------------------------------------- $kontraks = $qryKontrak->result_array(); print_r($kontraks); exit; foreach ($kontraks as $kontrak) { // Lewati jika item/satuan PO tidak ditemukan (tidak bisa buat baris detail) if (empty($kontrak["PurchaseOrderSummaryItemID"]) || empty($kontrak["PurchaseOrderSummaryItemUnitID"])) { $dilewati[] = [ "kontrakID" => $kontrak["PurchaseOrderAssetContractID"], "purchaseOrderID" => $kontrak["PurchaseOrderID"], "alasan" => "Item atau satuan pada Purchase Order tidak ditemukan, tidak bisa membuat baris detail PI." ]; continue; } // ------------------------------------------------------- // Cek apakah PI cicilan bulan ini sudah pernah dibuat // ------------------------------------------------------- $sqlCekDuplikat = "SELECT SupplierInvoiceID, SupplierInvoiceNumber FROM supplier_invoice WHERE SupplierInvoiceIsActive = 'Y' AND SupplierInvoiceDate >= DATE(?) AND SupplierInvoiceDate <= DATE(?) AND SupplierInvoiceStatus = 'Draft' AND ( SupplierInvoiceReceiveOrderPoID = ? OR EXISTS ( SELECT 1 FROM supplier_invoice_detail sid WHERE sid.SupplierInvoiceDetailSupplierInvoiceID = SupplierInvoiceID AND sid.SupplierInvoiceDetailReceiveOrderPoID = ? AND sid.SupplierInvoiceDetailIsActive = 'Y' ) ) LIMIT 1"; $qryCekDuplikat = $this->db->query($sqlCekDuplikat, [ $monthStart, $monthEnd, $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"], $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"] ]); if (!$qryCekDuplikat) { $this->sys_error_db("Gagal memeriksa duplikasi PI cicilan untuk kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . "."); exit; } if ($qryCekDuplikat->num_rows() > 0) { $piExisting = $qryCekDuplikat->row_array(); $dilewati[] = [ "kontrakID" => $kontrak["PurchaseOrderAssetContractID"], "purchaseOrderID" => $kontrak["PurchaseOrderID"], "receiveOrderPoID" => $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"], "supplierInvoiceID" => $piExisting["SupplierInvoiceID"], "nomorInvoice" => $piExisting["SupplierInvoiceNumber"], "alasan" => "PI cicilan untuk bulan ini sudah dibuat sebelumnya, tidak perlu dibuat ulang." ]; continue; } // ------------------------------------------------------- // Mode simulasi (dryRun): catat tanpa simpan ke database // ------------------------------------------------------- if ($dryRun) { $berhasil[] = [ "kontrakID" => $kontrak["PurchaseOrderAssetContractID"], "purchaseOrderID" => $kontrak["PurchaseOrderID"], "receiveOrderPoID" => $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"], "jumlahCicilan" => (float) $kontrak["PurchaseOrderAssetContractInstallmentPayAmount"], "keterangan" => "Mode simulasi (dryRun), data tidak disimpan ke database." ]; continue; } // ------------------------------------------------------- // Mulai transaksi database // ------------------------------------------------------- $this->db->trans_begin(); // Generate nomor PI otomatis via stored function fn_penomoran $nomorPI = $this->generateNomorPI($kontrak, $user); if ($nomorPI === false) { $this->db->trans_rollback(); $this->sys_error_db("Gagal generate nomor Purchase Invoice untuk kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . ". Pastikan divisi user sudah diatur."); exit; } $jumlahCicilan = (float) $kontrak["PurchaseOrderAssetContractInstallmentPayAmount"]; $tanggalJatuhTempo = $this->hitungJatuhTempo($endDate, $kontrak["PurchaseOrderPaymentTerm"]); $catatan = "PI Cicilan Otomatis — Kontrak Aset ID " . $kontrak["PurchaseOrderAssetContractID"] . " periode " . date("Y-m", strtotime($startDate)); // ------------------------------------------------------- // INSERT header + detail Purchase Invoice (supplier_invoice & supplier_invoice_detail) // ------------------------------------------------------- $deskripsi = $kontrak["PurchaseOrderAssetContractName"] != "" ? $kontrak["PurchaseOrderAssetContractName"] : "Cicilan Kontrak Aset ID " . $kontrak["PurchaseOrderAssetContractID"]; $deskripsi .= " periode " . date("Y-m", strtotime($startDate)); $supplierInvoiceID = $this->insertPurchaseInvoice( $nomorPI, $kontrak, $endDate, $tanggalJatuhTempo, $jumlahCicilan, $catatan, $deskripsi, $userID ); if ($supplierInvoiceID === false) { $this->db->trans_rollback(); $this->sys_error_db("Gagal menyimpan Purchase Invoice cicilan untuk kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . "."); exit; } // ------------------------------------------------------- // UPDATE jumlah cicilan terbayar pada kontrak // ------------------------------------------------------- $cicilanTerbayarBaru = ((int) $kontrak["PurchaseOrderAssetContractInstallmentPaid"]) + 1; $statusKontrakBaru = $cicilanTerbayarBaru >= (int) $kontrak["PurchaseOrderAssetContractInstallmentNumber"] ? "lunas" : "belum lunas"; $sqlUpdateKontrak = "UPDATE purchase_order_asset_contract SET PurchaseOrderAssetContractInstallmentPaid = ?, PurchaseOrderAssetContractStatus = ?, PurchaseOrderAssetContractLastUpdated = NOW() WHERE PurchaseOrderAssetContractID = ? AND PurchaseOrderAssetContractIsActive = 'Y'"; $qryUpdateKontrak = $this->db->query($sqlUpdateKontrak, [ $cicilanTerbayarBaru, $statusKontrakBaru, $kontrak["PurchaseOrderAssetContractID"] ]); if (!$qryUpdateKontrak) { $this->db->trans_rollback(); $this->sys_error_db("Gagal memperbarui data cicilan terbayar pada kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . "."); exit; } // Pastikan tidak ada error di dalam transaksi sebelum commit if ($this->db->trans_status() === false) { $this->db->trans_rollback(); $this->sys_error_db("Transaksi database gagal saat memproses kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . ". Semua perubahan dibatalkan."); exit; } $this->db->trans_commit(); // Catat PI yang berhasil dibuat $berhasil[] = [ "kontrakID" => $kontrak["PurchaseOrderAssetContractID"], "purchaseOrderID" => $kontrak["PurchaseOrderID"], "supplierInvoiceID" => $supplierInvoiceID, "nomorInvoice" => $nomorPI, "jumlahCicilan" => $jumlahCicilan ]; } // ------------------------------------------------------- // Response sukses — ringkasan hasil proses // ------------------------------------------------------- $this->sys_ok([ "startDate" => $startDate, "endDate" => $endDate, "periodeAwal" => $monthStart, "periodeAkhir" => $monthEnd, "totalDibuat" => count($berhasil), "totalDilewati" => count($dilewati), "daftarDibuat" => $berhasil, "daftarDilewati" => $dilewati ]); } catch (Exception $exc) { if ($this->db->trans_status() === false) { $this->db->trans_rollback(); } $this->sys_error($exc->getMessage()); } } public function ListEligibleContracts() { try { $para = $this->sys_input; $startDate = isset($para["startDate"]) && $para["startDate"] != "" ? $para["startDate"] : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-01", strtotime($para["date"])) : date("Y-m-01")); $endDate = isset($para["endDate"]) && $para["endDate"] != "" ? $para["endDate"] : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-t", strtotime($para["date"])) : date("Y-m-t")); if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) { throw new Exception("Format tanggal tidak valid. Gunakan format YYYY-MM-DD, contoh: 2025-07-01"); } if (strtotime($startDate) > strtotime($endDate)) { throw new Exception("startDate tidak boleh lebih besar dari endDate"); } $monthStart = $startDate; $monthEnd = $endDate; $dayOfMonth = (int) date("d", strtotime($endDate)); $sqlKontrak = "SELECT c.PurchaseOrderAssetContractID, c.PurchaseOrderAssetContractPurchaseOrderID, c.PurchaseOrderAssetContractReceiveOrderPoID, c.PurchaseOrderAssetContractName, c.PurchaseOrderAssetContractStartDate, c.PurchaseOrderAssetContractEndDate, c.PurchaseOrderAssetContractInstallmentNumber, c.PurchaseOrderAssetContractInstallmentPaid, c.PurchaseOrderAssetContractInstallmentDate, c.PurchaseOrderAssetContractInstallmentPayAmount, po.PurchaseOrderID, po.PurchaseOrderNumber, po.PurchaseOrderSupplierID, po.PurchaseOrderPaymentTerm, po.PurchaseOrderWarehouseType, po.PurchaseOrderWarehouseID, ro.ReceiveOrderPoID, si_base.SupplierInvoiceID AS BaseSupplierInvoiceID, si_base.SupplierInvoiceNumber AS BaseSupplierInvoiceNumber, ps.PurchaseOrderSummaryID, ps.PurchaseOrderSummaryItemID, ps.PurchaseOrderSummaryItemUnitID FROM purchase_order_asset_contract c JOIN purchase_order po ON po.PurchaseOrderID = c.PurchaseOrderAssetContractPurchaseOrderID AND po.PurchaseOrderIsActive = 'Y' AND po.PurchaseOrderStatus = 'Approved' JOIN receive_order_po ro ON ro.ReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID AND ro.ReceiveOrderPoIsActive = 'Y' JOIN supplier_invoice si_base ON si_base.SupplierInvoiceID = ( SELECT MIN(si0.SupplierInvoiceID) FROM supplier_invoice si0 WHERE si0.SupplierInvoiceIsActive = 'Y' AND ( si0.SupplierInvoiceReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID OR EXISTS ( SELECT 1 FROM supplier_invoice_detail sid0 WHERE sid0.SupplierInvoiceDetailSupplierInvoiceID = si0.SupplierInvoiceID AND sid0.SupplierInvoiceDetailReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID AND sid0.SupplierInvoiceDetailIsActive = 'Y' ) ) ) 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 WHERE c.PurchaseOrderAssetContractIsActive = 'Y' AND c.PurchaseOrderAssetContractStatus = 'belum lunas' AND IFNULL(c.PurchaseOrderAssetContractInstallmentPayAmount, 0) > 0 AND IFNULL(c.PurchaseOrderAssetContractInstallmentPaid, 0) < IFNULL(c.PurchaseOrderAssetContractInstallmentNumber, 0) AND DATE(c.PurchaseOrderAssetContractStartDate) <= DATE(?) AND ( c.PurchaseOrderAssetContractEndDate IS NULL OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?) ) AND c.PurchaseOrderAssetContractReceiveOrderPoID > 0 AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ? ORDER BY c.PurchaseOrderAssetContractID ASC"; $qryKontrak = $this->db->query($sqlKontrak, [$monthEnd, $monthStart, $dayOfMonth]); if (!$qryKontrak) { $this->sys_error_db("Gagal mengambil daftar kontrak cicilan eligible."); exit; } $this->sys_ok([ "startDate" => $startDate, "endDate" => $endDate, "periodeAwal" => $monthStart, "periodeAkhir" => $monthEnd, "total" => $qryKontrak->num_rows(), "records" => $qryKontrak->result_array() ]); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * CurlGenerateMonthlyInvoices * * Wrapper untuk memanggil GenerateMonthlyInvoices melalui HTTP request (cURL). * Cocok digunakan oleh CRON eksternal yang tidak bisa memanggil function PHP langsung. * * Parameter (POST JSON): * - baseUrl : Base URL server tujuan. Default: URL server ini sendiri. * - startDate : Tanggal awal periode (YYYY-MM-DD). Default: awal bulan ini. * - endDate : Tanggal akhir periode (YYYY-MM-DD). Default: akhir bulan ini. * - userID : ID user yang menjalankan. Default: 1. * - dryRun : "Y" untuk simulasi. Default: "N". * - contractID : (opsional) Filter 1 kontrak tertentu. */ public function CurlGenerateMonthlyInvoices() { try { $para = $this->sys_input; $baseUrl = isset($para["baseUrl"]) && $para["baseUrl"] != "" ? rtrim($para["baseUrl"], "/") : $this->getBaseUrl(); $startDate = isset($para["startDate"]) && $para["startDate"] != "" ? $para["startDate"] : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-01", strtotime($para["date"])) : date("Y-m-01")); $endDate = isset($para["endDate"]) && $para["endDate"] != "" ? $para["endDate"] : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-t", strtotime($para["date"])) : date("Y-m-t")); $userID = isset($para["userID"]) && (int) $para["userID"] > 0 ? (int) $para["userID"] : 1; $dryRun = isset($para["dryRun"]) ? $para["dryRun"] : "N"; $contractID = isset($para["contractID"]) && (int) $para["contractID"] > 0 ? (int) $para["contractID"] : null; $endpoint = $baseUrl . "/tools/PurchaseInvoiceInstallment/GenerateMonthlyInvoices"; $payload = [ "startDate" => $startDate, "endDate" => $endDate, "userID" => $userID, "dryRun" => $dryRun ]; if ($contractID !== null) { $payload["contractID"] = $contractID; } // Kirim request ke endpoint GenerateMonthlyInvoices $ch = curl_init($endpoint); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Content-Type: application/json", "Accept: application/json" ], CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_CONNECTTIMEOUT => 15, // timeout koneksi: 15 detik CURLOPT_TIMEOUT => 120 // timeout proses: 2 menit ]); $response = curl_exec($ch); if (curl_errno($ch)) { $pesanError = curl_error($ch); curl_close($ch); $this->sys_error("Gagal menghubungi endpoint generate PI cicilan. Detail: " . $pesanError); exit; } $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $this->sys_ok([ "endpoint" => $endpoint, "httpStatus" => $httpCode, "response" => $response ]); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } // ========================================================================= // FUNGSI PRIVATE / HELPER // ========================================================================= /** * insertPurchaseInvoice * * Menyimpan header Purchase Invoice (supplier_invoice) sekaligus * 1 baris detail-nya (supplier_invoice_detail) dalam satu fungsi. * * Fungsi ini TIDAK mengelola transaksi DB — begin/commit/rollback * tetap menjadi tanggung jawab pemanggil (GenerateMonthlyInvoices). * * @param string $nomorPI Nomor PI yang sudah di-generate * @param array $kontrak Data baris kontrak dari query * @param string $tanggal Tanggal PI (format YYYY-MM-DD) * @param string $tanggalJatuhTempo Tanggal jatuh tempo pembayaran (format YYYY-MM-DD) * @param float $jumlahCicilan Nilai cicilan yang harus dibayar bulan ini * @param string $catatan Catatan / keterangan pada header PI * @param string $deskripsi Deskripsi baris detail PI * @param int $userID ID user yang membuat PI * * @return int|false ID supplier_invoice yang baru dibuat, atau false jika salah satu INSERT gagal */ private function insertPurchaseInvoice($nomorPI, $kontrak, $tanggal, $tanggalJatuhTempo, $jumlahCicilan, $catatan, $deskripsi, $userID) { // ----------------------------------------------------------------- // 1. INSERT header ke supplier_invoice // ----------------------------------------------------------------- $sqlHeader = "INSERT INTO supplier_invoice ( SupplierInvoiceNumber, SupplierInvoicePurchaseOrderID, SupplierInvoiceReceiveOrderPoID, SupplierInvoiceDate, SupplierInvoiceDueDate, SupplierInvoiceDraftPaymentDate, SupplierInvoiceSupplierID, SupplierInvoiceSupplierInvoiceNumber, SupplierInvoiceSupplierInvoiceDate, SupplierInvoiceSubTotal, SupplierInvoiceDiscountPercent, SupplierInvoiceDiscountAmount, SupplierInvoiceTaxPercentPph, SupplierInvoiceTaxAmountPph, SupplierInvoiceTaxPercentPpn, SupplierInvoiceTaxAmountPpn, SupplierInvoiceShippingCost, SupplierInvoiceAdjustmentAmount, SupplierInvoiceAdjustmentNote, SupplierInvoiceGrandTotal, SupplierInvoiceUnpaid, SupplierInvoiceNote, SupplierInvoiceStatus, SupplierInvoiceCreatedUserID ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; $qryHeader = $this->db->query($sqlHeader, [ $nomorPI, $kontrak["PurchaseOrderID"], $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"], $tanggal, $tanggalJatuhTempo, $tanggal, // draft payment date = tanggal PI $kontrak["PurchaseOrderSupplierID"], $kontrak["BaseSupplierInvoiceNumber"], // nomor faktur dasar (tukar faktur) $tanggal, // tanggal faktur supplier = tanggal PI $jumlahCicilan, // subtotal 0, // diskon persen 0, // diskon rupiah 0, // PPh persen 0, // PPh nominal 0, // PPN persen 0, // PPN nominal 0, // ongkos kirim 0, // penyesuaian null, // catatan penyesuaian $jumlahCicilan, // grand total $jumlahCicilan, // sisa belum terbayar $catatan, "Draft", $userID ]); if (!$qryHeader) { return false; } $supplierInvoiceID = $this->db->insert_id(); // ----------------------------------------------------------------- // 2. INSERT 1 baris detail ke supplier_invoice_detail // ----------------------------------------------------------------- $sqlDetail = "INSERT INTO supplier_invoice_detail ( SupplierInvoiceDetailSupplierInvoiceID, SupplierInvoiceDetailPurchaseOrderID, SupplierInvoiceDetailReceiveOrderPoID, SupplierInvoiceDetailPurchaseOrderSummaryID, SupplierInvoiceDetailItemID, SupplierInvoiceDetailItemUnitID, SupplierInvoiceDetailDescription, SupplierInvoiceDetailQty, SupplierInvoiceDetailPrice, SupplierInvoiceDetailDiscountPercent, SupplierInvoiceDetailDiscountDiscountRupiah, SupplierInvoiceDetailDiscountDiscountType, SupplierInvoiceDetailDiscountPoProrata, SupplierInvoiceDetailDiscountAmount, SupplierInvoiceDetailTotal, SupplierInvoiceDetailUnpaid, SupplierInvoiceDetailCreatedUserID ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; $qryDetail = $this->db->query($sqlDetail, [ $supplierInvoiceID, $kontrak["PurchaseOrderID"], $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"], $kontrak["PurchaseOrderSummaryID"], $kontrak["PurchaseOrderSummaryItemID"], $kontrak["PurchaseOrderSummaryItemUnitID"], $deskripsi, 1, // qty = 1 (1 periode cicilan) $jumlahCicilan, // harga satuan = nilai cicilan 0, // diskon persen 0, // diskon rupiah "R", // tipe diskon: Rupiah 0, // prorata PO 0, // total diskon $jumlahCicilan, // total baris $jumlahCicilan, // sisa belum terbayar $userID ]); if (!$qryDetail) { return false; } return $supplierInvoiceID; } /** * Generate nomor Purchase Invoice otomatis menggunakan stored function fn_penomoran. * Nomor PI ditentukan berdasarkan tipe dokumen, divisi user, dan area (branch/regional). * * @param array $kontrak Data baris kontrak dari query * @param array $user Data user CRON * @return string|false Nomor PI yang dihasilkan, atau false jika gagal */ private function generateNomorPI($kontrak, $user) { // Tentukan tipe area: Regional (R) atau Branch (B) $tipeArea = $kontrak["PurchaseOrderWarehouseType"] == "R" ? "R" : "B"; $areaID = (int) $kontrak["PurchaseOrderWarehouseID"]; // Fallback ke area user jika warehouse PO tidak terisi if ($areaID <= 0) { $areaID = isset($user["M_BranchID"]) ? (int) $user["M_BranchID"] : 0; if (isset($user["loginLevel"]) && $user["loginLevel"] == "regional") { $tipeArea = "R"; $areaID = (int) $user["S_RegionalID"]; } } // Ambil divisi user untuk keperluan penomoran $sqlDivisi = "SELECT M_UserDivisionDivisionID FROM m_userdivision WHERE M_UserDivisionM_UserID = ? AND M_UserDivisionIsActive = 'Y' LIMIT 1"; $qryDivisi = $this->db->query($sqlDivisi, [$user["M_UserID"]]); if (!$qryDivisi || $qryDivisi->num_rows() == 0) { return false; // User tidak memiliki divisi, nomor PI tidak bisa dibuat } $divisiID = $qryDivisi->row_array()["M_UserDivisionDivisionID"]; // Panggil stored function penomoran untuk mendapatkan nomor PI berikutnya $sqlNomor = "SELECT `fn_penomoran`(?, ?, ?, ?, ?, ?) AS nomorPI"; $qryNomor = $this->db->query($sqlNomor, ["PI", $divisiID, $tipeArea, $areaID, "SM", "Y"]); if (!$qryNomor || $qryNomor->num_rows() == 0) { return false; // Stored function tidak mengembalikan hasil } return $qryNomor->row_array()["nomorPI"]; } /** * Ambil data user berdasarkan userID untuk keperluan CRON. * Jika user tidak ditemukan, kembalikan data default (tanpa branch/regional). * * @param int $userID * @return array */ private function getCronUser($userID) { $sql = "SELECT M_UserID, M_UserM_BranchID AS M_BranchID, M_UserS_RegionalID AS S_RegionalID FROM m_user WHERE M_UserID = ? LIMIT 1"; $qry = $this->db->query($sql, [$userID]); if ($qry && $qry->num_rows() > 0) { $user = $qry->row_array(); $user["loginLevel"] = "branch"; return $user; } // User tidak ditemukan, gunakan data kosong agar proses tetap berjalan return [ "M_UserID" => $userID, "M_BranchID" => 0, "S_RegionalID" => 0, "loginLevel" => "branch" ]; } /** * Hitung tanggal jatuh tempo berdasarkan tanggal acuan dan payment term (dalam hari). * * @param string $tanggal Format YYYY-MM-DD * @param int $term Jumlah hari payment term * @return string Tanggal jatuh tempo (YYYY-MM-DD) */ private function hitungJatuhTempo($tanggal, $term) { $hari = (int) $term; if ($hari < 0) { $hari = 0; } return date("Y-m-d", strtotime($tanggal . " +" . $hari . " days")); } /** * Validasi apakah string adalah tanggal yang valid dengan format YYYY-MM-DD. * * @param string $tanggal * @return bool */ private function isValidDate($tanggal) { $d = DateTime::createFromFormat("Y-m-d", $tanggal); return $d && $d->format("Y-m-d") === $tanggal; } /** * Deteksi base URL server saat ini secara otomatis (HTTP/HTTPS). * Digunakan sebagai fallback pada CurlGenerateMonthlyInvoices. * * @return string Base URL lengkap termasuk path index.php */ private function getBaseUrl() { $scheme = "http"; if ( (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off") || (isset($_SERVER["SERVER_PORT"]) && (int) $_SERVER["SERVER_PORT"] === 443) ) { $scheme = "https"; } $host = isset($_SERVER["HTTP_HOST"]) && $_SERVER["HTTP_HOST"] != "" ? $_SERVER["HTTP_HOST"] : "localhost"; $scriptName = isset($_SERVER["SCRIPT_NAME"]) ? $_SERVER["SCRIPT_NAME"] : "/index.php"; $basePath = rtrim(str_replace("/index.php", "", $scriptName), "/"); return $scheme . "://" . $host . $basePath . "/index.php"; } }