diff --git a/application/controllers/mockup/scheduler/PurchaseInvoiceInstallment.http b/application/controllers/mockup/scheduler/PurchaseInvoiceInstallment.http index 20cdd4d..e791797 100644 --- a/application/controllers/mockup/scheduler/PurchaseInvoiceInstallment.http +++ b/application/controllers/mockup/scheduler/PurchaseInvoiceInstallment.http @@ -1,17 +1,7 @@ -@host = accone.aplikasi.web.id/one-api/mockup +@host = https://accone.aplikasi.web.id/one-api/ ### Generate PI cicilan langsung -POST https://{{host}}/scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices -Content-Type: application/json - -{ - "startDate": "2026-07-01", - "endDate": "2026-07-31", - "dryRun": "Y" -} - -### List kontrak eligible yang belum diproses -POST https://{{host}}/scheduler/PurchaseInvoiceInstallment/ListEligibleContracts +POST {{host}}/mockup/scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices Content-Type: application/json { @@ -19,38 +9,32 @@ Content-Type: application/json "endDate": "2026-07-31" } -### Generate PI cicilan untuk 1 kontrak -POST https://{{host}}/scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices +### List kontrak eligible yang belum diproses +POST https://accone.aplikasi.web.id/one-api/mockup/scheduler/PurchaseInvoiceInstallment/ListEligibleContracts Content-Type: application/json { "startDate": "2026-07-01", - "endDate": "2026-07-31", - "contractID": 123, - "dryRun": "Y" + "endDate": "2026-07-31" } -### Panggil endpoint generate via cURL internal -POST https://{{host}}/scheduler/PurchaseInvoiceInstallment/CurlGenerateMonthlyInvoices +### Insert supplier_invoice lewat controller terpisah +POST {{host}}/mockup/scheduler/PurchaseInvoiceInstallmentInsert/InsertSupplierInvoice Content-Type: application/json { - "baseUrl": "https://accone.aplikasi.web.id/one-api/index.php", - "startDate": "2026-07-01", - "endDate": "2026-07-31", - "userID": 1, - "dryRun": "Y" -} - -### Panggil endpoint generate via cURL internal untuk kontrak tertentu -POST https://{{host}}/scheduler/PurchaseInvoiceInstallment/CurlGenerateMonthlyInvoices -Content-Type: application/json - -{ - "baseUrl": "https://accone.aplikasi.web.id/one-api/index.php", - "startDate": "2026-07-01", - "endDate": "2026-07-31", - "userID": 1, - "contractID": 123, - "dryRun": "N" + "nomorPI": "PI-INS-20260712120000-000123-00456-00000", + "tanggalPI": "2026-07-31", + "tanggalJatuhTempo": "2026-08-14", + "jumlahCicilan": 15000000, + "catatan": "PI Cicilan Otomatis", + "deskripsi": "Cicilan Kontrak Aset", + "userID": 0, + "purchaseOrderAssetContractID": 123, + "purchaseOrderID": 456, + "receiveOrderPoID": 789, + "supplierID": 12, + "purchaseOrderSummaryID": 111, + "purchaseOrderSummaryItemID": 222, + "purchaseOrderSummaryItemUnitID": 333 } diff --git a/application/controllers/mockup/scheduler/PurchaseInvoiceInstallment.php b/application/controllers/mockup/scheduler/PurchaseInvoiceInstallment.php index 7ff96d1..8370dd5 100644 --- a/application/controllers/mockup/scheduler/PurchaseInvoiceInstallment.php +++ b/application/controllers/mockup/scheduler/PurchaseInvoiceInstallment.php @@ -13,7 +13,7 @@ class PurchaseInvoiceInstallment extends MY_Controller { var $db; - + var $baseUrl = "https://accone.aplikasi.web.id/one-api/"; public function index() { echo "Purchase Invoice Installment — Auto Generate PI Cicilan Aset"; @@ -22,7 +22,6 @@ class PurchaseInvoiceInstallment extends MY_Controller public function __construct() { parent::__construct(); - $baseUrl = "devone.aplikasi.web.id"; } /** @@ -34,26 +33,23 @@ class PurchaseInvoiceInstallment extends MY_Controller * Parameter (POST JSON): * - startDate : Tanggal awal periode (format YYYY-MM-DD). Default: awal bulan ini. * - endDate : Tanggal akhir periode (format YYYY-MM-DD). Default: akhir bulan ini. - * - dryRun : "Y" = simulasi tanpa menyimpan data. Default: "N". - * - contractID : (opsional) Filter hanya 1 kontrak tertentu. - * * userID diambil otomatis dari token (sys_user["M_UserID"]). - * Jika CRON berjalan tanpa token, fallback ke user ID 1 (user sistem). + * Jika CRON berjalan tanpa token, fallback ke user ID 0. * * Syarat kontrak diproses: * 1. Kontrak aktif dan berstatus "belum lunas" * 2. Nilai cicilan > 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 + * 5. RO sudah confirmed * 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; + // Ambil userID dari token JWT (sys_user). Fallback ke 0 jika CRON berjalan tanpa token. + $userID = !empty($this->sys_user["M_UserID"]) ? (int) $this->sys_user["M_UserID"] : 0; $user = $this->getCronUser($userID); $startDate = isset($para["startDate"]) && $para["startDate"] != "" @@ -62,8 +58,6 @@ class PurchaseInvoiceInstallment extends MY_Controller $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"); @@ -84,7 +78,7 @@ class PurchaseInvoiceInstallment extends MY_Controller $sqlKontrak = "SELECT c.PurchaseOrderAssetContractID, c.PurchaseOrderAssetContractPurchaseOrderID, - c.PurchaseOrderAssetContractReceiveOrderPoID, + ro.ReceiveOrderPoID AS PurchaseOrderAssetContractReceiveOrderPoID, c.PurchaseOrderAssetContractName, c.PurchaseOrderAssetContractStartDate, c.PurchaseOrderAssetContractEndDate, @@ -98,9 +92,7 @@ class PurchaseInvoiceInstallment extends MY_Controller po.PurchaseOrderPaymentTerm, po.PurchaseOrderWarehouseType, po.PurchaseOrderWarehouseID, - ro.ReceiveOrderPoID, - si_base.SupplierInvoiceID AS BaseSupplierInvoiceID, - si_base.SupplierInvoiceNumber AS BaseSupplierInvoiceNumber, + ro.ReceiveOrderPoConfirmed, ps.PurchaseOrderSummaryID, ps.PurchaseOrderSummaryItemID, ps.PurchaseOrderSummaryItemUnitID @@ -112,27 +104,20 @@ class PurchaseInvoiceInstallment extends MY_Controller 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' - ) - ) - ) + 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 -- Ambil 1 item PO pertama sebagai referensi baris detail PI LEFT JOIN ( @@ -163,17 +148,10 @@ class PurchaseInvoiceInstallment extends MY_Controller 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]; - } + $params = [$monthEnd, $monthStart, $dayOfMonth]; $qryKontrak = $this->db->query($sqlKontrak, $params); if (!$qryKontrak) { @@ -188,8 +166,6 @@ class PurchaseInvoiceInstallment extends MY_Controller // 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) @@ -248,35 +224,14 @@ class PurchaseInvoiceInstallment extends MY_Controller 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"]); + $paymentTerm = isset($kontrak["PurchaseOrderPaymentTerm"]) && $kontrak["PurchaseOrderPaymentTerm"] !== null + ? (int) $kontrak["PurchaseOrderPaymentTerm"] + : 0; + $tanggalJatuhTempo = $this->hitungJatuhTempo($endDate, $paymentTerm); $catatan = "PI Cicilan Otomatis — Kontrak Aset ID " . $kontrak["PurchaseOrderAssetContractID"] . " periode " . date("Y-m", strtotime($startDate)); // ------------------------------------------------------- @@ -287,39 +242,63 @@ class PurchaseInvoiceInstallment extends MY_Controller : "Cicilan Kontrak Aset ID " . $kontrak["PurchaseOrderAssetContractID"]; $deskripsi .= " periode " . date("Y-m", strtotime($startDate)); - $supplierInvoiceID = $this->insertPurchaseInvoice( - $nomorPI, - $kontrak, - $endDate, - $tanggalJatuhTempo, - $jumlahCicilan, - $catatan, - $deskripsi, - $userID - ); + $payloadInsert = [ + "nomorPI" => $nomorPI, + "tanggalPI" => $endDate, + "tanggalJatuhTempo" => $tanggalJatuhTempo, + "jumlahCicilan" => $jumlahCicilan, + "catatan" => $catatan, + "deskripsi" => $deskripsi, + "userID" => $userID, + "purchaseOrderAssetContractID" => $kontrak["PurchaseOrderAssetContractID"], + "purchaseOrderID" => $kontrak["PurchaseOrderID"], + "receiveOrderPoID" => $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"], + "supplierID" => $kontrak["PurchaseOrderSupplierID"], + "purchaseOrderSummaryID" => $kontrak["PurchaseOrderSummaryID"], + "purchaseOrderSummaryItemID" => $kontrak["PurchaseOrderSummaryItemID"], + "purchaseOrderSummaryItemUnitID" => $kontrak["PurchaseOrderSummaryItemUnitID"] + ]; - if ($supplierInvoiceID === false) { - $this->db->trans_rollback(); + $hasilInsert = $this->curlInsertSupplierInvoice($payloadInsert); + if ($hasilInsert === false) { $this->sys_error_db("Gagal menyimpan Purchase Invoice cicilan untuk kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . "."); exit; } + if (!empty($hasilInsert["duplicate"])) { + $dilewati[] = [ + "kontrakID" => $kontrak["PurchaseOrderAssetContractID"], + "purchaseOrderID" => $kontrak["PurchaseOrderID"], + "receiveOrderPoID" => $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"], + "supplierInvoiceID" => $hasilInsert["supplierInvoiceID"], + "nomorInvoice" => $hasilInsert["supplierInvoiceNumber"], + "alasan" => "PI cicilan untuk bulan ini sudah dibuat sebelumnya, tidak perlu dibuat ulang." + ]; + continue; + } + + $supplierInvoiceID = $hasilInsert["supplierInvoiceID"]; + // ------------------------------------------------------- // UPDATE jumlah cicilan terbayar pada kontrak // ------------------------------------------------------- + $this->db->trans_begin(); + $cicilanTerbayarBaru = ((int) $kontrak["PurchaseOrderAssetContractInstallmentPaid"]) + 1; $statusKontrakBaru = $cicilanTerbayarBaru >= (int) $kontrak["PurchaseOrderAssetContractInstallmentNumber"] ? "lunas" : "belum lunas"; $sqlUpdateKontrak = "UPDATE purchase_order_asset_contract - SET PurchaseOrderAssetContractInstallmentPaid = ?, + SET PurchaseOrderAssetContractReceiveOrderPoID = ?, + PurchaseOrderAssetContractInstallmentPaid = ?, PurchaseOrderAssetContractStatus = ?, PurchaseOrderAssetContractLastUpdated = NOW() WHERE PurchaseOrderAssetContractID = ? AND PurchaseOrderAssetContractIsActive = 'Y'"; $qryUpdateKontrak = $this->db->query($sqlUpdateKontrak, [ + $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"], $cicilanTerbayarBaru, $statusKontrakBaru, $kontrak["PurchaseOrderAssetContractID"] @@ -371,6 +350,60 @@ class PurchaseInvoiceInstallment extends MY_Controller } } + /** + * Kirim payload insert supplier_invoice ke controller terpisah via cURL. + * + * @param array $payload + * @return array|false + */ + private function curlInsertSupplierInvoice($payload) + { + $endpoint = rtrim($this->baseUrl, "/") . "/mockup/scheduler/PurchaseInvoiceInstallmentInsert/InsertSupplierInvoice"; + + $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, + CURLOPT_TIMEOUT => 120 + ]); + + $response = curl_exec($ch); + if (curl_errno($ch)) { + $pesanError = curl_error($ch); + curl_close($ch); + $this->sys_error("Gagal menghubungi endpoint insert supplier_invoice. Detail: " . $pesanError); + return false; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode < 200 || $httpCode >= 300) { + $this->sys_error("Endpoint insert supplier_invoice mengembalikan HTTP " . $httpCode . "."); + return false; + } + + $decoded = json_decode($response, true); + if (!is_array($decoded)) { + $this->sys_error("Response insert supplier_invoice tidak valid JSON."); + return false; + } + + if (!isset($decoded["status"]) || strtoupper($decoded["status"]) !== "OK") { + $pesan = isset($decoded["message"]) ? $decoded["message"] : "Insert supplier_invoice gagal."; + $this->sys_error($pesan); + return false; + } + + return isset($decoded["data"]) && is_array($decoded["data"]) ? $decoded["data"] : $decoded; + } + public function ListEligibleContracts() { try { @@ -397,7 +430,7 @@ class PurchaseInvoiceInstallment extends MY_Controller $sqlKontrak = "SELECT c.PurchaseOrderAssetContractID, c.PurchaseOrderAssetContractPurchaseOrderID, - c.PurchaseOrderAssetContractReceiveOrderPoID, + ro.ReceiveOrderPoID AS PurchaseOrderAssetContractReceiveOrderPoID, c.PurchaseOrderAssetContractName, c.PurchaseOrderAssetContractStartDate, c.PurchaseOrderAssetContractEndDate, @@ -411,9 +444,7 @@ class PurchaseInvoiceInstallment extends MY_Controller po.PurchaseOrderPaymentTerm, po.PurchaseOrderWarehouseType, po.PurchaseOrderWarehouseID, - ro.ReceiveOrderPoID, - si_base.SupplierInvoiceID AS BaseSupplierInvoiceID, - si_base.SupplierInvoiceNumber AS BaseSupplierInvoiceNumber, + ro.ReceiveOrderPoConfirmed, ps.PurchaseOrderSummaryID, ps.PurchaseOrderSummaryItemID, ps.PurchaseOrderSummaryItemUnitID @@ -423,26 +454,21 @@ class PurchaseInvoiceInstallment extends MY_Controller AND po.PurchaseOrderIsActive = 'Y' AND po.PurchaseOrderStatus = 'Approved' - JOIN receive_order_po ro - ON ro.ReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID - AND ro.ReceiveOrderPoIsActive = 'Y' + 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 - 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 @@ -466,7 +492,6 @@ class PurchaseInvoiceInstallment extends MY_Controller c.PurchaseOrderAssetContractEndDate IS NULL OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?) ) - AND c.PurchaseOrderAssetContractReceiveOrderPoID > 0 AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ? ORDER BY c.PurchaseOrderAssetContractID ASC"; @@ -499,254 +524,33 @@ class PurchaseInvoiceInstallment extends MY_Controller * - 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. + * - userID : ID user yang menjalankan. Default: 0. */ - 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). + * Generate nomor Purchase Invoice default untuk proses sementara. * * @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 + * @return string Nomor PI yang dihasilkan */ private function generateNomorPI($kontrak, $user) { - // Tentukan tipe area: Regional (R) atau Branch (B) - $tipeArea = $kontrak["PurchaseOrderWarehouseType"] == "R" ? "R" : "B"; - $areaID = (int) $kontrak["PurchaseOrderWarehouseID"]; + $contractID = isset($kontrak["PurchaseOrderAssetContractID"]) ? (int) $kontrak["PurchaseOrderAssetContractID"] : 0; + $poID = isset($kontrak["PurchaseOrderID"]) ? (int) $kontrak["PurchaseOrderID"] : 0; + $userID = isset($user["M_UserID"]) ? (int) $user["M_UserID"] : 0; - // 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"]; + return sprintf( + "PI-INS-%s-%06d-%05d-%05d", + date("YmdHis"), + $contractID, + $poID, + $userID + ); } /** @@ -791,7 +595,7 @@ class PurchaseInvoiceInstallment extends MY_Controller */ private function hitungJatuhTempo($tanggal, $term) { - $hari = (int) $term; + $hari = is_numeric($term) ? (int) $term : 0; if ($hari < 0) { $hari = 0; } @@ -810,27 +614,4 @@ class PurchaseInvoiceInstallment extends MY_Controller $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"; - } } diff --git a/application/controllers/mockup/scheduler/PurchaseInvoiceInstallmentInsert.php b/application/controllers/mockup/scheduler/PurchaseInvoiceInstallmentInsert.php new file mode 100644 index 0000000..e284b02 --- /dev/null +++ b/application/controllers/mockup/scheduler/PurchaseInvoiceInstallmentInsert.php @@ -0,0 +1,220 @@ +sys_input; + + $required = [ + "nomorPI", + "tanggalPI", + "tanggalJatuhTempo", + "jumlahCicilan", + "catatan", + "deskripsi", + "userID", + "purchaseOrderAssetContractID", + "purchaseOrderID", + "receiveOrderPoID", + "supplierID", + "purchaseOrderSummaryID", + "purchaseOrderSummaryItemID", + "purchaseOrderSummaryItemUnitID" + ]; + + foreach ($required as $field) { + if (!isset($para[$field]) || $para[$field] === "" || $para[$field] === null) { + throw new Exception("Field wajib belum lengkap: " . $field); + } + } + + $tanggalPI = $para["tanggalPI"]; + $tanggalJatuhTempo = $para["tanggalJatuhTempo"]; + if (!$this->isValidDate($tanggalPI) || !$this->isValidDate($tanggalJatuhTempo)) { + throw new Exception("Format tanggal tidak valid. Gunakan YYYY-MM-DD."); + } + + $jumlahCicilan = (float) $para["jumlahCicilan"]; + if ($jumlahCicilan <= 0) { + throw new Exception("jumlahCicilan harus lebih besar dari 0."); + } + + $receiveOrderPoID = (int) $para["receiveOrderPoID"]; + $monthKey = date("Y-m", strtotime($tanggalPI)); + + $sqlDuplikat = "SELECT SupplierInvoiceID, SupplierInvoiceNumber + FROM supplier_invoice + WHERE SupplierInvoiceIsActive = 'Y' + AND SupplierInvoiceStatus = 'Draft' + AND SupplierInvoiceReceiveOrderPoID = ? + AND DATE_FORMAT(SupplierInvoiceDate, '%Y-%m') = ? + LIMIT 1"; + + $qryDuplikat = $this->db->query($sqlDuplikat, [$receiveOrderPoID, $monthKey]); + if (!$qryDuplikat) { + $this->sys_error_db("Gagal mengecek duplikasi supplier_invoice."); + exit; + } + + if ($qryDuplikat->num_rows() > 0) { + $existing = $qryDuplikat->row_array(); + $this->sys_ok([ + "duplicate" => true, + "supplierInvoiceID" => $existing["SupplierInvoiceID"], + "supplierInvoiceNumber" => $existing["SupplierInvoiceNumber"] + ]); + return; + } + + $this->db->trans_begin(); + + $sqlHeader = "INSERT INTO supplier_invoice ( + SupplierInvoiceNumber, + 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, [ + $para["nomorPI"], + $receiveOrderPoID, + $tanggalPI, + $tanggalJatuhTempo, + $tanggalPI, + (int) $para["supplierID"], + null, + null, + $jumlahCicilan, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + $jumlahCicilan, + $jumlahCicilan, + $para["catatan"], + "Draft", + (int) $para["userID"] + ]); + + if (!$qryHeader) { + $this->db->trans_rollback(); + $this->sys_error_db("Gagal insert header supplier_invoice."); + exit; + } + + $supplierInvoiceID = $this->db->insert_id(); + + $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, + (int) $para["purchaseOrderID"], + $receiveOrderPoID, + (int) $para["purchaseOrderSummaryID"], + (int) $para["purchaseOrderSummaryItemID"], + (int) $para["purchaseOrderSummaryItemUnitID"], + $para["deskripsi"], + 1, + $jumlahCicilan, + 0, + 0, + "R", + 0, + 0, + $jumlahCicilan, + $jumlahCicilan, + (int) $para["userID"] + ]); + + if (!$qryDetail) { + $this->db->trans_rollback(); + $this->sys_error_db("Gagal insert detail supplier_invoice_detail."); + exit; + } + + if ($this->db->trans_status() === false) { + $this->db->trans_rollback(); + $this->sys_error_db("Transaksi insert supplier_invoice gagal."); + exit; + } + + $this->db->trans_commit(); + + $this->sys_ok([ + "duplicate" => false, + "supplierInvoiceID" => $supplierInvoiceID, + "supplierInvoiceNumber" => $para["nomorPI"] + ]); + } catch (Exception $exc) { + $this->sys_error($exc->getMessage()); + } + } + + private function isValidDate($tanggal) + { + $d = DateTime::createFromFormat("Y-m-d", $tanggal); + return $d && $d->format("Y-m-d") === $tanggal; + } +}