fix query get cicilan dan insert supplier invoice

This commit is contained in:
Hanan Askarim
2026-07-12 15:15:21 +07:00
committed by sasadib
parent 977694905c
commit 712cbbb80b
3 changed files with 388 additions and 403 deletions

View File

@@ -1,17 +1,7 @@
@host = accone.aplikasi.web.id/one-api/mockup @host = https://accone.aplikasi.web.id/one-api/
### Generate PI cicilan langsung ### Generate PI cicilan langsung
POST https://{{host}}/scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices POST {{host}}/mockup/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
Content-Type: application/json Content-Type: application/json
{ {
@@ -19,38 +9,32 @@ Content-Type: application/json
"endDate": "2026-07-31" "endDate": "2026-07-31"
} }
### Generate PI cicilan untuk 1 kontrak ### List kontrak eligible yang belum diproses
POST https://{{host}}/scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices POST https://accone.aplikasi.web.id/one-api/mockup/scheduler/PurchaseInvoiceInstallment/ListEligibleContracts
Content-Type: application/json Content-Type: application/json
{ {
"startDate": "2026-07-01", "startDate": "2026-07-01",
"endDate": "2026-07-31", "endDate": "2026-07-31"
"contractID": 123,
"dryRun": "Y"
} }
### Panggil endpoint generate via cURL internal ### Insert supplier_invoice lewat controller terpisah
POST https://{{host}}/scheduler/PurchaseInvoiceInstallment/CurlGenerateMonthlyInvoices POST {{host}}/mockup/scheduler/PurchaseInvoiceInstallmentInsert/InsertSupplierInvoice
Content-Type: application/json Content-Type: application/json
{ {
"baseUrl": "https://accone.aplikasi.web.id/one-api/index.php", "nomorPI": "PI-INS-20260712120000-000123-00456-00000",
"startDate": "2026-07-01", "tanggalPI": "2026-07-31",
"endDate": "2026-07-31", "tanggalJatuhTempo": "2026-08-14",
"userID": 1, "jumlahCicilan": 15000000,
"dryRun": "Y" "catatan": "PI Cicilan Otomatis",
} "deskripsi": "Cicilan Kontrak Aset",
"userID": 0,
### Panggil endpoint generate via cURL internal untuk kontrak tertentu "purchaseOrderAssetContractID": 123,
POST https://{{host}}/scheduler/PurchaseInvoiceInstallment/CurlGenerateMonthlyInvoices "purchaseOrderID": 456,
Content-Type: application/json "receiveOrderPoID": 789,
"supplierID": 12,
{ "purchaseOrderSummaryID": 111,
"baseUrl": "https://accone.aplikasi.web.id/one-api/index.php", "purchaseOrderSummaryItemID": 222,
"startDate": "2026-07-01", "purchaseOrderSummaryItemUnitID": 333
"endDate": "2026-07-31",
"userID": 1,
"contractID": 123,
"dryRun": "N"
} }

View File

@@ -13,7 +13,7 @@
class PurchaseInvoiceInstallment extends MY_Controller class PurchaseInvoiceInstallment extends MY_Controller
{ {
var $db; var $db;
var $baseUrl = "https://accone.aplikasi.web.id/one-api/";
public function index() public function index()
{ {
echo "Purchase Invoice Installment — Auto Generate PI Cicilan Aset"; echo "Purchase Invoice Installment — Auto Generate PI Cicilan Aset";
@@ -22,7 +22,6 @@ class PurchaseInvoiceInstallment extends MY_Controller
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$baseUrl = "devone.aplikasi.web.id";
} }
/** /**
@@ -34,26 +33,23 @@ class PurchaseInvoiceInstallment extends MY_Controller
* Parameter (POST JSON): * Parameter (POST JSON):
* - startDate : Tanggal awal periode (format YYYY-MM-DD). Default: awal bulan ini. * - startDate : Tanggal awal periode (format YYYY-MM-DD). Default: awal bulan ini.
* - endDate : Tanggal akhir periode (format YYYY-MM-DD). Default: akhir 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"]). * 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: * Syarat kontrak diproses:
* 1. Kontrak aktif dan berstatus "belum lunas" * 1. Kontrak aktif dan berstatus "belum lunas"
* 2. Nilai cicilan > 0 * 2. Nilai cicilan > 0
* 3. Jumlah cicilan terbayar < total cicilan * 3. Jumlah cicilan terbayar < total cicilan
* 4. Tanggal kontrak masuk dalam bulan yang dituju * 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 * 6. PO sudah berstatus Approved
*/ */
public function GenerateMonthlyInvoices() public function GenerateMonthlyInvoices()
{ {
try { try {
$para = $this->sys_input; $para = $this->sys_input;
// Ambil userID dari token JWT (sys_user). Fallback ke 1 jika CRON berjalan tanpa token. // 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"] : 1; $userID = !empty($this->sys_user["M_UserID"]) ? (int) $this->sys_user["M_UserID"] : 0;
$user = $this->getCronUser($userID); $user = $this->getCronUser($userID);
$startDate = isset($para["startDate"]) && $para["startDate"] != "" $startDate = isset($para["startDate"]) && $para["startDate"] != ""
@@ -62,8 +58,6 @@ class PurchaseInvoiceInstallment extends MY_Controller
$endDate = isset($para["endDate"]) && $para["endDate"] != "" $endDate = isset($para["endDate"]) && $para["endDate"] != ""
? $para["endDate"] ? $para["endDate"]
: (isset($para["date"]) && $para["date"] != "" ? date("Y-m-t", strtotime($para["date"])) : date("Y-m-t")); : (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 // Validasi format tanggal
if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) { if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) {
throw new Exception("Format tanggal tidak valid. Gunakan format YYYY-MM-DD, contoh: 2025-07-01"); 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 $sqlKontrak = "SELECT
c.PurchaseOrderAssetContractID, c.PurchaseOrderAssetContractID,
c.PurchaseOrderAssetContractPurchaseOrderID, c.PurchaseOrderAssetContractPurchaseOrderID,
c.PurchaseOrderAssetContractReceiveOrderPoID, ro.ReceiveOrderPoID AS PurchaseOrderAssetContractReceiveOrderPoID,
c.PurchaseOrderAssetContractName, c.PurchaseOrderAssetContractName,
c.PurchaseOrderAssetContractStartDate, c.PurchaseOrderAssetContractStartDate,
c.PurchaseOrderAssetContractEndDate, c.PurchaseOrderAssetContractEndDate,
@@ -98,9 +92,7 @@ class PurchaseInvoiceInstallment extends MY_Controller
po.PurchaseOrderPaymentTerm, po.PurchaseOrderPaymentTerm,
po.PurchaseOrderWarehouseType, po.PurchaseOrderWarehouseType,
po.PurchaseOrderWarehouseID, po.PurchaseOrderWarehouseID,
ro.ReceiveOrderPoID, ro.ReceiveOrderPoConfirmed,
si_base.SupplierInvoiceID AS BaseSupplierInvoiceID,
si_base.SupplierInvoiceNumber AS BaseSupplierInvoiceNumber,
ps.PurchaseOrderSummaryID, ps.PurchaseOrderSummaryID,
ps.PurchaseOrderSummaryItemID, ps.PurchaseOrderSummaryItemID,
ps.PurchaseOrderSummaryItemUnitID ps.PurchaseOrderSummaryItemUnitID
@@ -112,27 +104,20 @@ class PurchaseInvoiceInstallment extends MY_Controller
AND po.PurchaseOrderIsActive = 'Y' AND po.PurchaseOrderIsActive = 'Y'
AND po.PurchaseOrderStatus = 'Approved' AND po.PurchaseOrderStatus = 'Approved'
JOIN receive_order_po ro JOIN (
ON ro.ReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID SELECT
AND ro.ReceiveOrderPoIsActive = 'Y' rd.ReceiveOrderPoDetailPurchaseOrderID,
MIN(ro0.ReceiveOrderPoID) AS ReceiveOrderPoID,
-- Pastikan tukar faktur (invoice dasar) sudah ada untuk PO ini MAX(ro0.ReceiveOrderPoConfirmed) AS ReceiveOrderPoConfirmed
JOIN supplier_invoice si_base FROM receive_order_po ro0
ON si_base.SupplierInvoiceID = ( JOIN receive_order_po_detail rd
SELECT MIN(si0.SupplierInvoiceID) ON rd.ReceiveOrderPoDetailReceiveOrderPoID = ro0.ReceiveOrderPoID
FROM supplier_invoice si0 AND rd.ReceiveOrderPoDetailIsActive = 'Y'
WHERE si0.SupplierInvoiceIsActive = 'Y' WHERE ro0.ReceiveOrderPoIsActive = 'Y'
AND ( AND ro0.ReceiveOrderPoConfirmed = 'Y'
si0.SupplierInvoiceReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID GROUP BY rd.ReceiveOrderPoDetailPurchaseOrderID
OR EXISTS ( ) ro
SELECT 1 ON ro.ReceiveOrderPoDetailPurchaseOrderID = po.PurchaseOrderID
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 -- Ambil 1 item PO pertama sebagai referensi baris detail PI
LEFT JOIN ( LEFT JOIN (
@@ -163,17 +148,10 @@ class PurchaseInvoiceInstallment extends MY_Controller
c.PurchaseOrderAssetContractEndDate IS NULL c.PurchaseOrderAssetContractEndDate IS NULL
OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?) OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?)
) )
AND c.PurchaseOrderAssetContractReceiveOrderPoID > 0
-- Tanggal jatuh tempo cicilan sudah melewati atau sama dengan hari ini -- Tanggal jatuh tempo cicilan sudah melewati atau sama dengan hari ini
AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?"; AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?";
// Filter kontrak tertentu jika diminta $params = [$monthEnd, $monthStart, $dayOfMonth];
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); $qryKontrak = $this->db->query($sqlKontrak, $params);
if (!$qryKontrak) { if (!$qryKontrak) {
@@ -188,8 +166,6 @@ class PurchaseInvoiceInstallment extends MY_Controller
// Proses tiap kontrak satu per satu // Proses tiap kontrak satu per satu
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
$kontraks = $qryKontrak->result_array(); $kontraks = $qryKontrak->result_array();
print_r($kontraks);
exit;
foreach ($kontraks as $kontrak) { foreach ($kontraks as $kontrak) {
// Lewati jika item/satuan PO tidak ditemukan (tidak bisa buat baris detail) // Lewati jika item/satuan PO tidak ditemukan (tidak bisa buat baris detail)
@@ -248,35 +224,14 @@ class PurchaseInvoiceInstallment extends MY_Controller
continue; 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 // Generate nomor PI otomatis via stored function fn_penomoran
$nomorPI = $this->generateNomorPI($kontrak, $user); $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"]; $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)); $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"]; : "Cicilan Kontrak Aset ID " . $kontrak["PurchaseOrderAssetContractID"];
$deskripsi .= " periode " . date("Y-m", strtotime($startDate)); $deskripsi .= " periode " . date("Y-m", strtotime($startDate));
$supplierInvoiceID = $this->insertPurchaseInvoice( $payloadInsert = [
$nomorPI, "nomorPI" => $nomorPI,
$kontrak, "tanggalPI" => $endDate,
$endDate, "tanggalJatuhTempo" => $tanggalJatuhTempo,
$tanggalJatuhTempo, "jumlahCicilan" => $jumlahCicilan,
$jumlahCicilan, "catatan" => $catatan,
$catatan, "deskripsi" => $deskripsi,
$deskripsi, "userID" => $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) { $hasilInsert = $this->curlInsertSupplierInvoice($payloadInsert);
$this->db->trans_rollback(); if ($hasilInsert === false) {
$this->sys_error_db("Gagal menyimpan Purchase Invoice cicilan untuk kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . "."); $this->sys_error_db("Gagal menyimpan Purchase Invoice cicilan untuk kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . ".");
exit; 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 // UPDATE jumlah cicilan terbayar pada kontrak
// ------------------------------------------------------- // -------------------------------------------------------
$this->db->trans_begin();
$cicilanTerbayarBaru = ((int) $kontrak["PurchaseOrderAssetContractInstallmentPaid"]) + 1; $cicilanTerbayarBaru = ((int) $kontrak["PurchaseOrderAssetContractInstallmentPaid"]) + 1;
$statusKontrakBaru = $cicilanTerbayarBaru >= (int) $kontrak["PurchaseOrderAssetContractInstallmentNumber"] $statusKontrakBaru = $cicilanTerbayarBaru >= (int) $kontrak["PurchaseOrderAssetContractInstallmentNumber"]
? "lunas" ? "lunas"
: "belum lunas"; : "belum lunas";
$sqlUpdateKontrak = "UPDATE purchase_order_asset_contract $sqlUpdateKontrak = "UPDATE purchase_order_asset_contract
SET PurchaseOrderAssetContractInstallmentPaid = ?, SET PurchaseOrderAssetContractReceiveOrderPoID = ?,
PurchaseOrderAssetContractInstallmentPaid = ?,
PurchaseOrderAssetContractStatus = ?, PurchaseOrderAssetContractStatus = ?,
PurchaseOrderAssetContractLastUpdated = NOW() PurchaseOrderAssetContractLastUpdated = NOW()
WHERE PurchaseOrderAssetContractID = ? WHERE PurchaseOrderAssetContractID = ?
AND PurchaseOrderAssetContractIsActive = 'Y'"; AND PurchaseOrderAssetContractIsActive = 'Y'";
$qryUpdateKontrak = $this->db->query($sqlUpdateKontrak, [ $qryUpdateKontrak = $this->db->query($sqlUpdateKontrak, [
$kontrak["PurchaseOrderAssetContractReceiveOrderPoID"],
$cicilanTerbayarBaru, $cicilanTerbayarBaru,
$statusKontrakBaru, $statusKontrakBaru,
$kontrak["PurchaseOrderAssetContractID"] $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() public function ListEligibleContracts()
{ {
try { try {
@@ -397,7 +430,7 @@ class PurchaseInvoiceInstallment extends MY_Controller
$sqlKontrak = "SELECT $sqlKontrak = "SELECT
c.PurchaseOrderAssetContractID, c.PurchaseOrderAssetContractID,
c.PurchaseOrderAssetContractPurchaseOrderID, c.PurchaseOrderAssetContractPurchaseOrderID,
c.PurchaseOrderAssetContractReceiveOrderPoID, ro.ReceiveOrderPoID AS PurchaseOrderAssetContractReceiveOrderPoID,
c.PurchaseOrderAssetContractName, c.PurchaseOrderAssetContractName,
c.PurchaseOrderAssetContractStartDate, c.PurchaseOrderAssetContractStartDate,
c.PurchaseOrderAssetContractEndDate, c.PurchaseOrderAssetContractEndDate,
@@ -411,9 +444,7 @@ class PurchaseInvoiceInstallment extends MY_Controller
po.PurchaseOrderPaymentTerm, po.PurchaseOrderPaymentTerm,
po.PurchaseOrderWarehouseType, po.PurchaseOrderWarehouseType,
po.PurchaseOrderWarehouseID, po.PurchaseOrderWarehouseID,
ro.ReceiveOrderPoID, ro.ReceiveOrderPoConfirmed,
si_base.SupplierInvoiceID AS BaseSupplierInvoiceID,
si_base.SupplierInvoiceNumber AS BaseSupplierInvoiceNumber,
ps.PurchaseOrderSummaryID, ps.PurchaseOrderSummaryID,
ps.PurchaseOrderSummaryItemID, ps.PurchaseOrderSummaryItemID,
ps.PurchaseOrderSummaryItemUnitID ps.PurchaseOrderSummaryItemUnitID
@@ -423,26 +454,21 @@ class PurchaseInvoiceInstallment extends MY_Controller
AND po.PurchaseOrderIsActive = 'Y' AND po.PurchaseOrderIsActive = 'Y'
AND po.PurchaseOrderStatus = 'Approved' AND po.PurchaseOrderStatus = 'Approved'
JOIN receive_order_po ro JOIN (
ON ro.ReceiveOrderPoID = c.PurchaseOrderAssetContractReceiveOrderPoID SELECT
AND ro.ReceiveOrderPoIsActive = 'Y' 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 ( LEFT JOIN (
SELECT ps0.* SELECT ps0.*
FROM purchase_order_summary ps0 FROM purchase_order_summary ps0
@@ -466,7 +492,6 @@ class PurchaseInvoiceInstallment extends MY_Controller
c.PurchaseOrderAssetContractEndDate IS NULL c.PurchaseOrderAssetContractEndDate IS NULL
OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?) OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?)
) )
AND c.PurchaseOrderAssetContractReceiveOrderPoID > 0
AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ? AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?
ORDER BY c.PurchaseOrderAssetContractID ASC"; ORDER BY c.PurchaseOrderAssetContractID ASC";
@@ -499,254 +524,33 @@ class PurchaseInvoiceInstallment extends MY_Controller
* - baseUrl : Base URL server tujuan. Default: URL server ini sendiri. * - baseUrl : Base URL server tujuan. Default: URL server ini sendiri.
* - startDate : Tanggal awal periode (YYYY-MM-DD). Default: awal bulan ini. * - startDate : Tanggal awal periode (YYYY-MM-DD). Default: awal bulan ini.
* - endDate : Tanggal akhir periode (YYYY-MM-DD). Default: akhir bulan ini. * - endDate : Tanggal akhir periode (YYYY-MM-DD). Default: akhir bulan ini.
* - userID : ID user yang menjalankan. Default: 1. * - userID : ID user yang menjalankan. Default: 0.
* - 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 // FUNGSI PRIVATE / HELPER
// ========================================================================= // =========================================================================
/** /**
* insertPurchaseInvoice * Generate nomor Purchase Invoice default untuk proses sementara.
*
* 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 $kontrak Data baris kontrak dari query
* @param array $user Data user CRON * @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) private function generateNomorPI($kontrak, $user)
{ {
// Tentukan tipe area: Regional (R) atau Branch (B) $contractID = isset($kontrak["PurchaseOrderAssetContractID"]) ? (int) $kontrak["PurchaseOrderAssetContractID"] : 0;
$tipeArea = $kontrak["PurchaseOrderWarehouseType"] == "R" ? "R" : "B"; $poID = isset($kontrak["PurchaseOrderID"]) ? (int) $kontrak["PurchaseOrderID"] : 0;
$areaID = (int) $kontrak["PurchaseOrderWarehouseID"]; $userID = isset($user["M_UserID"]) ? (int) $user["M_UserID"] : 0;
// Fallback ke area user jika warehouse PO tidak terisi return sprintf(
if ($areaID <= 0) { "PI-INS-%s-%06d-%05d-%05d",
$areaID = isset($user["M_BranchID"]) ? (int) $user["M_BranchID"] : 0; date("YmdHis"),
if (isset($user["loginLevel"]) && $user["loginLevel"] == "regional") { $contractID,
$tipeArea = "R"; $poID,
$areaID = (int) $user["S_RegionalID"]; $userID
} );
}
// 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"];
} }
/** /**
@@ -791,7 +595,7 @@ class PurchaseInvoiceInstallment extends MY_Controller
*/ */
private function hitungJatuhTempo($tanggal, $term) private function hitungJatuhTempo($tanggal, $term)
{ {
$hari = (int) $term; $hari = is_numeric($term) ? (int) $term : 0;
if ($hari < 0) { if ($hari < 0) {
$hari = 0; $hari = 0;
} }
@@ -810,27 +614,4 @@ class PurchaseInvoiceInstallment extends MY_Controller
$d = DateTime::createFromFormat("Y-m-d", $tanggal); $d = DateTime::createFromFormat("Y-m-d", $tanggal);
return $d && $d->format("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";
}
} }

View File

@@ -0,0 +1,220 @@
<?php
/**
* PurchaseInvoiceInstallmentInsert
*
* Endpoint khusus untuk menyimpan supplier_invoice dan supplier_invoice_detail
* dari payload yang dikirim controller generator via cURL.
*/
class PurchaseInvoiceInstallmentInsert extends MY_Controller
{
var $db;
public function index()
{
echo "Purchase Invoice Installment Insert";
}
public function __construct()
{
parent::__construct();
}
public function InsertSupplierInvoice()
{
try {
$para = $this->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;
}
}