303 lines
11 KiB
PHP
303 lines
11 KiB
PHP
<?php
|
||
|
||
/**
|
||
* PurchaseInvoiceInstallment
|
||
*
|
||
* CRON scheduler untuk membuat baris cicilan otomatis ke tabel supplier_installment
|
||
* setiap bulan berdasarkan kontrak aset yang aktif dan belum lunas.
|
||
*
|
||
* Endpoint:
|
||
* POST /scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices
|
||
*/
|
||
class PurchaseInvoiceInstallment extends MY_Controller
|
||
{
|
||
var $db;
|
||
|
||
public function index()
|
||
{
|
||
echo "Purchase Invoice Installment — Auto Generate Installment Cicilan Aset";
|
||
}
|
||
|
||
public function __construct()
|
||
{
|
||
parent::__construct();
|
||
}
|
||
|
||
/**
|
||
* GenerateMonthlyInvoices — Step 6
|
||
*
|
||
* 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.
|
||
*
|
||
* 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;
|
||
|
||
$userID = !empty($this->sys_user["M_UserID"])
|
||
? (int) $this->sys_user["M_UserID"]
|
||
: 0;
|
||
|
||
$startDate = !empty($para["startDate"])
|
||
? $para["startDate"]
|
||
: date("Y-m-01");
|
||
|
||
$endDate = !empty($para["endDate"])
|
||
? $para["endDate"]
|
||
: date("Y-m-t");
|
||
|
||
if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) {
|
||
throw new Exception("Format tanggal tidak valid.");
|
||
}
|
||
|
||
if (strtotime($startDate) > strtotime($endDate)) {
|
||
throw new Exception("startDate > endDate.");
|
||
}
|
||
|
||
// ── Steps 1–5: Get eligible contracts ────────────────────
|
||
$eligible = $this->getEligibleInstallments($startDate, $endDate);
|
||
|
||
$created = [];
|
||
$inserted = 0;
|
||
|
||
$this->db->trans_begin();
|
||
|
||
foreach ($eligible as $row) {
|
||
|
||
$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);
|
||
|
||
// ── 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(), ?)";
|
||
|
||
$que = $this->db->query($sql, [
|
||
$row["PurchaseOrderID"],
|
||
$row["PurchaseOrderSupplierID"],
|
||
$row["SupplierInvoiceID"],
|
||
$amount,
|
||
$installDate,
|
||
$installDate,
|
||
$createdBy
|
||
]);
|
||
|
||
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;
|
||
}
|
||
|
||
$this->db->trans_commit();
|
||
|
||
$this->sys_ok([
|
||
"startDate" => $startDate,
|
||
"endDate" => $endDate,
|
||
"totalEligible" => count($eligible),
|
||
"totalInserted" => $inserted,
|
||
"created" => $created
|
||
]);
|
||
|
||
} catch (Exception $exc) {
|
||
if ($this->db->trans_status() === false) {
|
||
$this->db->trans_rollback();
|
||
}
|
||
$this->sys_error($exc->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ambil data user berdasarkan userID untuk keperluan CRON.
|
||
* Jika user tidak ditemukan, kembalikan data default (tanpa branch/regional).
|
||
*
|
||
* @param int $userID
|
||
* @return array
|
||
*/
|
||
private function getCronUser($userID)
|
||
{
|
||
$sql = "SELECT
|
||
M_UserID,
|
||
M_UserM_BranchID AS M_BranchID,
|
||
M_UserS_RegionalID AS S_RegionalID
|
||
FROM m_user
|
||
WHERE M_UserID = ?
|
||
LIMIT 1";
|
||
|
||
$qry = $this->db->query($sql, [$userID]);
|
||
if ($qry && $qry->num_rows() > 0) {
|
||
$user = $qry->row_array();
|
||
$user["loginLevel"] = "branch";
|
||
return $user;
|
||
}
|
||
|
||
// User tidak ditemukan, gunakan data kosong agar proses tetap berjalan
|
||
return [
|
||
"M_UserID" => $userID,
|
||
"M_BranchID" => 0,
|
||
"S_RegionalID" => 0,
|
||
"loginLevel" => "branch"
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Hitung tanggal jatuh tempo berdasarkan tanggal acuan dan payment term (dalam hari).
|
||
*
|
||
* @param string $tanggal Format YYYY-MM-DD
|
||
* @param int $term Jumlah hari payment term
|
||
* @return string Tanggal jatuh tempo (YYYY-MM-DD)
|
||
*/
|
||
private function hitungJatuhTempo($tanggal, $term)
|
||
{
|
||
$hari = is_numeric($term) ? (int) $term : 0;
|
||
if ($hari < 0) {
|
||
$hari = 0;
|
||
}
|
||
|
||
return date("Y-m-d", strtotime($tanggal . " +" . $hari . " days"));
|
||
}
|
||
|
||
/**
|
||
* Validasi apakah string adalah tanggal yang valid dengan format YYYY-MM-DD.
|
||
*
|
||
* @param string $tanggal
|
||
* @return bool
|
||
*/
|
||
private function isValidDate($tanggal)
|
||
{
|
||
$d = DateTime::createFromFormat("Y-m-d", $tanggal);
|
||
return $d && $d->format("Y-m-d") === $tanggal;
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
}
|
||
}
|