feature: payment instruction & cashier case cicilan dan downpayment
This commit is contained in:
@@ -3,20 +3,19 @@
|
||||
/**
|
||||
* PurchaseInvoiceInstallment
|
||||
*
|
||||
* Digunakan oleh CRON untuk membuat Purchase Invoice (PI) cicilan aset
|
||||
* secara otomatis setiap bulan berdasarkan kontrak yang aktif.
|
||||
* CRON scheduler untuk membuat baris cicilan otomatis ke tabel supplier_installment
|
||||
* setiap bulan berdasarkan kontrak aset yang aktif dan belum lunas.
|
||||
*
|
||||
* Endpoint utama:
|
||||
* Endpoint:
|
||||
* POST /scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices
|
||||
* POST /scheduler/PurchaseInvoiceInstallment/CurlGenerateMonthlyInvoices
|
||||
*/
|
||||
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";
|
||||
echo "Purchase Invoice Installment — Auto Generate Installment Cicilan Aset";
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
@@ -25,327 +24,121 @@ class PurchaseInvoiceInstallment extends MY_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* GenerateMonthlyInvoices
|
||||
* GenerateMonthlyInvoices — Step 6
|
||||
*
|
||||
* Membuat Purchase Invoice cicilan untuk semua kontrak aset yang aktif
|
||||
* dan belum lunas pada bulan yang ditentukan.
|
||||
* Creates supplier_installment rows for all eligible contracts this month.
|
||||
* No journal is created here — that happens at cashier payment time.
|
||||
* Contract InstallmentPaid is NOT updated here — that happens at payment time.
|
||||
*
|
||||
* 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.
|
||||
* userID diambil otomatis dari token (sys_user["M_UserID"]).
|
||||
* 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. RO sudah confirmed
|
||||
* 6. PO sudah berstatus Approved
|
||||
* Called by CRON daily at 1:00 AM.
|
||||
* Defaults to current month if no startDate/endDate provided.
|
||||
*/
|
||||
public function GenerateMonthlyInvoices()
|
||||
{
|
||||
try {
|
||||
$para = $this->sys_input;
|
||||
// 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);
|
||||
$para = $this->sys_input;
|
||||
|
||||
$startDate = isset($para["startDate"]) && $para["startDate"] != ""
|
||||
$userID = !empty($this->sys_user["M_UserID"])
|
||||
? (int) $this->sys_user["M_UserID"]
|
||||
: 0;
|
||||
|
||||
$startDate = !empty($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"] != ""
|
||||
: date("Y-m-01");
|
||||
|
||||
$endDate = !empty($para["endDate"])
|
||||
? $para["endDate"]
|
||||
: (isset($para["date"]) && $para["date"] != "" ? date("Y-m-t", strtotime($para["date"])) : date("Y-m-t"));
|
||||
// Validasi format tanggal
|
||||
: 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");
|
||||
throw new Exception("Format tanggal tidak valid.");
|
||||
}
|
||||
|
||||
if (strtotime($startDate) > strtotime($endDate)) {
|
||||
throw new Exception("startDate tidak boleh lebih besar dari endDate");
|
||||
throw new Exception("startDate > endDate.");
|
||||
}
|
||||
|
||||
// Tentukan rentang bulan berdasarkan tanggal acuan
|
||||
$monthStart = $startDate;
|
||||
$monthEnd = $endDate;
|
||||
$dayOfMonth = (int) date("d", strtotime($endDate));
|
||||
// ── Steps 1–5: Get eligible contracts ────────────────────
|
||||
$eligible = $this->getEligibleInstallments($startDate, $endDate);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Ambil semua kontrak cicilan yang memenuhi syarat pada bulan ini
|
||||
// -------------------------------------------------------------------------
|
||||
$sqlKontrak = "SELECT
|
||||
c.PurchaseOrderAssetContractID,
|
||||
c.PurchaseOrderAssetContractPurchaseOrderID,
|
||||
ro.ReceiveOrderPoID AS PurchaseOrderAssetContractReceiveOrderPoID,
|
||||
c.PurchaseOrderAssetContractName,
|
||||
c.PurchaseOrderAssetContractStartDate,
|
||||
c.PurchaseOrderAssetContractEndDate,
|
||||
c.PurchaseOrderAssetContractInstallmentNumber,
|
||||
c.PurchaseOrderAssetContractInstallmentPaid,
|
||||
c.PurchaseOrderAssetContractInstallmentDate,
|
||||
c.PurchaseOrderAssetContractInstallmentPayAmount,
|
||||
c.PurchaseOrderAssetContractCreatedUserID,
|
||||
po.PurchaseOrderID,
|
||||
po.PurchaseOrderNumber,
|
||||
po.PurchaseOrderSupplierID,
|
||||
po.PurchaseOrderPaymentTerm,
|
||||
po.PurchaseOrderWarehouseType,
|
||||
po.PurchaseOrderWarehouseID,
|
||||
ro.ReceiveOrderPoConfirmed,
|
||||
ps.PurchaseOrderSummaryID,
|
||||
ps.PurchaseOrderSummaryItemID,
|
||||
ps.PurchaseOrderSummaryItemUnitID
|
||||
FROM purchase_order_asset_contract c
|
||||
$created = [];
|
||||
$inserted = 0;
|
||||
|
||||
-- Pastikan PO sudah Approved dan aktif
|
||||
JOIN purchase_order po
|
||||
ON po.PurchaseOrderID = c.PurchaseOrderAssetContractPurchaseOrderID
|
||||
AND po.PurchaseOrderIsActive = 'Y'
|
||||
AND po.PurchaseOrderStatus = 'Approved'
|
||||
$this->db->trans_begin();
|
||||
|
||||
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
|
||||
foreach ($eligible as $row) {
|
||||
|
||||
-- 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
|
||||
$amount = (float) $row["PurchaseOrderAssetContractInstallmentPayAmount"];
|
||||
$dayOfMonth = (int) ($row["PurchaseOrderAssetContractInstallmentDate"] ?? 1);
|
||||
$lastDay = (int) date("t", strtotime($endDate));
|
||||
$dayOfMonth = min($dayOfMonth, $lastDay);
|
||||
$installDate = date("Y-m", strtotime($endDate)) . "-" . str_pad($dayOfMonth, 2, "0", STR_PAD_LEFT);
|
||||
$dueDate = date("Y-m-d", strtotime($installDate . " +7 days"));
|
||||
$createdBy = (int) ($row["PurchaseOrderAssetContractCreatedUserID"] ?? $userID);
|
||||
|
||||
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(?)
|
||||
)
|
||||
-- Tanggal jatuh tempo cicilan sudah melewati atau sama dengan hari ini
|
||||
AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?";
|
||||
// ── Step 6: INSERT supplier_installment ──────────────
|
||||
$sql = "INSERT INTO supplier_installment (
|
||||
SupplierInstallmentPurchaseOrderID,
|
||||
SupplierInstallmentSupplierID,
|
||||
SupplierInstallmentSupplierInvoiceID,
|
||||
SupplierInstallmentAmount,
|
||||
SupplierInstallmentDate,
|
||||
SupplierInstallmentDueDate,
|
||||
SupplierInstallmentPaymentID,
|
||||
SupplierInstallmentStatus,
|
||||
SupplierInstallmentIsLunas,
|
||||
SupplierInstallmentIsActive,
|
||||
SupplierInstallmentCreated,
|
||||
SupplierInstallmentCreatedUserID
|
||||
) VALUES (?, ?, ?, ?, ?, ?, 0, 'Pending', 'N', 'Y', NOW(), ?)";
|
||||
|
||||
$params = [$monthEnd, $monthStart, $dayOfMonth];
|
||||
$que = $this->db->query($sql, [
|
||||
$row["PurchaseOrderID"],
|
||||
$row["PurchaseOrderSupplierID"],
|
||||
$row["SupplierInvoiceID"],
|
||||
$amount,
|
||||
$installDate,
|
||||
$dueDate,
|
||||
$createdBy
|
||||
]);
|
||||
|
||||
$qryKontrak = $this->db->query($sqlKontrak, $params);
|
||||
if (!$qryKontrak) {
|
||||
$this->sys_error_db("Gagal mengambil daftar kontrak cicilan dari database.");
|
||||
if (!$que) {
|
||||
$this->db->trans_rollback();
|
||||
$this->sys_error_db("Gagal insert supplier_installment.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$installmentID = $this->db->insert_id();
|
||||
$inserted++;
|
||||
|
||||
$created[] = [
|
||||
"installmentID" => $installmentID,
|
||||
"contractID" => $row["PurchaseOrderAssetContractID"],
|
||||
"purchaseOrderID" => $row["PurchaseOrderID"],
|
||||
"parentInvoiceID" => $row["SupplierInvoiceID"],
|
||||
"parentInvoiceNumber" => $row["SupplierInvoiceNumber"],
|
||||
"amount" => $amount,
|
||||
"installDate" => $installDate,
|
||||
"dueDate" => $dueDate
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->db->trans_status() === false) {
|
||||
$this->db->trans_rollback();
|
||||
$this->sys_error_db("Transaksi gagal.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$berhasil = []; // PI yang berhasil dibuat
|
||||
$dilewati = []; // PI yang dilewati beserta alasannya
|
||||
$this->db->trans_commit();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Proses tiap kontrak satu per satu
|
||||
// -------------------------------------------------------------------------
|
||||
$kontraks = $qryKontrak->result_array();
|
||||
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;
|
||||
}
|
||||
|
||||
$contractUserID = (int) ($kontrak["PurchaseOrderAssetContractCreatedUserID"] ?? 0);
|
||||
$contractUser = $this->getCronUser($contractUserID);
|
||||
|
||||
// Generate nomor PI otomatis via stored function fn_penomoran
|
||||
$nomorPI = $this->generateNomorPI($kontrak, $contractUser);
|
||||
|
||||
$jumlahCicilan = (float) $kontrak["PurchaseOrderAssetContractInstallmentPayAmount"];
|
||||
$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));
|
||||
|
||||
// -------------------------------------------------------
|
||||
// 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));
|
||||
|
||||
$payloadInsert = [
|
||||
"nomorPI" => $nomorPI,
|
||||
"tanggalPI" => $endDate,
|
||||
"tanggalJatuhTempo" => $tanggalJatuhTempo,
|
||||
"jumlahCicilan" => $jumlahCicilan,
|
||||
"catatan" => $catatan,
|
||||
"deskripsi" => $deskripsi,
|
||||
"userID" => $contractUserID,
|
||||
"purchaseOrderAssetContractID" => $kontrak["PurchaseOrderAssetContractID"],
|
||||
"purchaseOrderID" => $kontrak["PurchaseOrderID"],
|
||||
"receiveOrderPoID" => $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"],
|
||||
"supplierID" => $kontrak["PurchaseOrderSupplierID"],
|
||||
"purchaseOrderSummaryID" => $kontrak["PurchaseOrderSummaryID"],
|
||||
"purchaseOrderSummaryItemID" => $kontrak["PurchaseOrderSummaryItemID"],
|
||||
"purchaseOrderSummaryItemUnitID" => $kontrak["PurchaseOrderSummaryItemUnitID"]
|
||||
];
|
||||
|
||||
$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 PurchaseOrderAssetContractReceiveOrderPoID = ?,
|
||||
PurchaseOrderAssetContractInstallmentPaid = ?,
|
||||
PurchaseOrderAssetContractStatus = ?,
|
||||
PurchaseOrderAssetContractLastUpdated = NOW()
|
||||
WHERE PurchaseOrderAssetContractID = ?
|
||||
AND PurchaseOrderAssetContractIsActive = 'Y'";
|
||||
|
||||
$qryUpdateKontrak = $this->db->query($sqlUpdateKontrak, [
|
||||
$kontrak["PurchaseOrderAssetContractReceiveOrderPoID"],
|
||||
$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
|
||||
"totalEligible" => count($eligible),
|
||||
"totalInserted" => $inserted,
|
||||
"created" => $created
|
||||
]);
|
||||
|
||||
} catch (Exception $exc) {
|
||||
if ($this->db->trans_status() === false) {
|
||||
$this->db->trans_rollback();
|
||||
@@ -354,229 +147,6 @@ 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 {
|
||||
$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,
|
||||
ro.ReceiveOrderPoID AS 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.ReceiveOrderPoConfirmed,
|
||||
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 (
|
||||
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
|
||||
|
||||
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 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: 0.
|
||||
*/
|
||||
|
||||
// =========================================================================
|
||||
// FUNGSI PRIVATE / HELPER
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Generate nomor Purchase Invoice.
|
||||
*
|
||||
* @param array $kontrak Data baris kontrak dari query
|
||||
* @param array $user Data user CRON
|
||||
* @return string Nomor PI yang dihasilkan
|
||||
*/
|
||||
private function generateNomorPI($kontrak, $user)
|
||||
{
|
||||
$userID = isset($user["M_UserID"]) ? (int) $user["M_UserID"] : 0;
|
||||
|
||||
// Tentukan area ID dan type
|
||||
$areaid = isset($user["M_BranchID"]) && (int) $user["M_BranchID"] > 0 ? (int) $user["M_BranchID"] : 0;
|
||||
$areatype = 'B';
|
||||
if (isset($user["loginLevel"]) && $user["loginLevel"] == 'regional') {
|
||||
$areaid = isset($user["S_RegionalID"]) && (int) $user["S_RegionalID"] > 0 ? (int) $user["S_RegionalID"] : 0;
|
||||
$areatype = 'R';
|
||||
}
|
||||
|
||||
// Ambil divisi user
|
||||
$userDivID = 0;
|
||||
if ($userID > 0) {
|
||||
$sqlusrdivisi = "SELECT M_UserDivisionDivisionID FROM m_userdivision
|
||||
WHERE M_UserDivisionM_UserID = ? AND M_UserDivisionIsActive = 'Y' LIMIT 1";
|
||||
$queusrdivisi = $this->db->query($sqlusrdivisi, [$userID]);
|
||||
if ($queusrdivisi && $queusrdivisi->num_rows() > 0) {
|
||||
$userDivID = (int) $queusrdivisi->row_array()['M_UserDivisionDivisionID'];
|
||||
}
|
||||
}
|
||||
|
||||
// Jalankan stored function fn_penomoran
|
||||
$sqlnum = "SELECT `fn_penomoran`(?, ?, ?, ?, ?, ?) AS numpd;";
|
||||
$quenum = $this->db->query($sqlnum, ['PI', $userDivID, $areatype, $areaid, 'SM', 'N']);
|
||||
if ($quenum && $quenum->num_rows() > 0) {
|
||||
return $quenum->row_array()['numpd'];
|
||||
}
|
||||
|
||||
// Fallback jika stored function gagal
|
||||
$contractID = isset($kontrak["PurchaseOrderAssetContractID"]) ? (int) $kontrak["PurchaseOrderAssetContractID"] : 0;
|
||||
return "PI-INS-FALLBACK-" . $contractID . "-" . date("Ymd");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil data user berdasarkan userID untuk keperluan CRON.
|
||||
* Jika user tidak ditemukan, kembalikan data default (tanpa branch/regional).
|
||||
@@ -638,4 +208,95 @@ class PurchaseInvoiceInstallment extends MY_Controller
|
||||
$d = DateTime::createFromFormat("Y-m-d", $tanggal);
|
||||
return $d && $d->format("Y-m-d") === $tanggal;
|
||||
}
|
||||
|
||||
/**
|
||||
* getEligibleInstallments — Steps 1–5 combined
|
||||
*
|
||||
* Returns contracts that:
|
||||
* Step 1: Are active, not paid off, within date range, due date reached
|
||||
* Step 2: Have approved & active PO
|
||||
* Step 3: Have confirmed & active RO
|
||||
* Step 4: Have a parent supplier_invoice with IsInstallment = 'Y'
|
||||
* Step 5: Do NOT already have a supplier_installment for this month
|
||||
*
|
||||
* @param string $startDate YYYY-MM-DD
|
||||
* @param string $endDate YYYY-MM-DD
|
||||
* @return array
|
||||
*/
|
||||
private function getEligibleInstallments($startDate, $endDate)
|
||||
{
|
||||
$dayOfMonth = (int) date("d", strtotime($endDate));
|
||||
|
||||
$sql = "SELECT
|
||||
c.PurchaseOrderAssetContractID,
|
||||
c.PurchaseOrderAssetContractInstallmentPayAmount,
|
||||
c.PurchaseOrderAssetContractInstallmentDate,
|
||||
c.PurchaseOrderAssetContractCreatedUserID,
|
||||
po.PurchaseOrderID,
|
||||
po.PurchaseOrderSupplierID,
|
||||
si.SupplierInvoiceID,
|
||||
si.SupplierInvoiceNumber
|
||||
FROM purchase_order_asset_contract c
|
||||
|
||||
-- Step 2: PO must be Approved and Active
|
||||
JOIN purchase_order po
|
||||
ON po.PurchaseOrderID = c.PurchaseOrderAssetContractPurchaseOrderID
|
||||
AND po.PurchaseOrderIsActive = 'Y'
|
||||
AND po.PurchaseOrderStatus = 'Approved'
|
||||
|
||||
-- Step 3: RO must be Confirmed and Active
|
||||
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
|
||||
|
||||
-- Step 4: Parent invoice must exist with IsInstallment = 'Y'
|
||||
JOIN supplier_invoice si
|
||||
ON si.SupplierInvoiceReceiveOrderPoID = ro.ReceiveOrderPoID
|
||||
AND si.SupplierInvoiceIsActive = 'Y'
|
||||
AND si.SupplierInvoiceIsInstallment = 'Y'
|
||||
|
||||
-- Step 1: Contract eligibility
|
||||
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 IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?
|
||||
|
||||
-- Step 5: Dedup — no existing supplier_installment this month (any status)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM supplier_installment inst
|
||||
WHERE inst.SupplierInstallmentIsActive = 'Y'
|
||||
AND inst.SupplierInstallmentPurchaseOrderID = po.PurchaseOrderID
|
||||
AND DATE_FORMAT(inst.SupplierInstallmentDate, '%Y-%m') = DATE_FORMAT(?, '%Y-%m')
|
||||
)
|
||||
|
||||
ORDER BY c.PurchaseOrderAssetContractID ASC";
|
||||
|
||||
$params = [$endDate, $startDate, $dayOfMonth, $endDate];
|
||||
|
||||
$qry = $this->db->query($sql, $params);
|
||||
if (!$qry) {
|
||||
$this->sys_error_db("Gagal mengambil daftar installment eligible.");
|
||||
exit;
|
||||
}
|
||||
|
||||
return $qry->result_array();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user