6 Commits

7 changed files with 1025 additions and 412 deletions

104
README.md
View File

@@ -1,2 +1,106 @@
# be-accone # be-accone
Backend service for Accone.
---
## Generate Monthly Installments (Cicilan Aset)
Automatically creates `supplier_installment` rows for all eligible active contracts every month. This is designed to be executed via **CRON daily at 1:00 AM**.
> **Note:** No journal entries are created here — that happens at cashier payment time. The contract's `InstallmentPaid` field is also **not** updated here — it's updated when payment is received.
### Endpoint
```
POST /one-api/mockup/scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices
```
### Request
| Field | Value |
|---------------|--------------------|
| **Method** | `POST` |
| **Content-Type** | `application/json` |
| **Body** | `{}` (optional `startDate` / `endDate`) |
```bash
curl -s -X POST "https://accone.aplikasi.web.id/one-api/mockup/scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices" \
-H "Content-Type: application/json" \
-d '{}'
```
**Optional body parameters:**
```json
{
"startDate": "2026-07-01",
"endDate": "2026-07-31"
}
```
If omitted, defaults to the current month (`startDate` = first day, `endDate` = last day).
### Eligibility Logic (Steps 15)
A contract is eligible for installment generation only if **all** of the following conditions are met:
| Step | Condition |
|------|-----------|
| **1** | Contract is **active** (`IsActive = 'Y'`), status is **"belum lunas"**, has a positive `InstallmentPayAmount`, `InstallmentPaid < InstallmentNumber`, falls within the date range, and the scheduled day of month has been reached |
| **2** | Associated **Purchase Order** is **Approved** and **Active** |
| **3** | Associated **Receive Order** is **Confirmed** and **Active** |
| **4** | A parent **Supplier Invoice** exists with `IsInstallment = 'Y'` |
| **5** | No `supplier_installment` record already exists for the same PO in the same month |
### Step 6 — Insert
For each eligible contract, a row is inserted into `supplier_installment` with:
- `SupplierInstallmentStatus` = `"Pending"`
- `SupplierInstallmentIsLunas` = `"N"`
- `SupplierInstallmentDate` = the contract's scheduled day of month (capped to the last day of the month)
- `SupplierInstallmentDueDate` = same as InstallmentDate, can be changed at payment instrutions manu
### Response
```json
{
"status": "OK",
"data": {
"startDate": "2026-07-01",
"endDate": "2026-07-31",
"totalEligible": 1,
"totalInserted": 1,
"created": [
{
"installmentID": 6,
"contractID": "11",
"purchaseOrderID": "20",
"parentInvoiceID": "16",
"parentInvoiceNumber": "PI-016/SDM-14/SM-1/VII/2026",
"amount": 5000000,
"installDate": "2026-07-22",
"dueDate": "2026-07-22"
}
]
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `status` | `string` | `"OK"` on success |
| `data.startDate` | `string` | Start of the billing period (`YYYY-MM-DD`) |
| `data.endDate` | `string` | End of the billing period (`YYYY-MM-DD`) |
| `data.totalEligible` | `number` | Total contracts that passed eligibility |
| `data.totalInserted` | `number` | Total installments successfully created |
| `data.created` | `array` | List of generated installment records |
| `data.created[].installmentID` | `number` | ID of the created `supplier_installment` row |
| `data.created[].contractID` | `string` | Associated contract ID (`PurchaseOrderAssetContract`) |
| `data.created[].purchaseOrderID` | `string` | Associated purchase order ID |
| `data.created[].parentInvoiceID` | `string` | Source supplier invoice ID |
| `data.created[].parentInvoiceNumber` | `string` | Source supplier invoice number |
| `data.created[].amount` | `number` | Installment amount (IDR) |
| `data.created[].installDate` | `string` | Date the installment is issued |
| `data.created[].dueDate` | `string` | Payment due date (`installDate + 7 days`) |

View File

@@ -1635,55 +1635,12 @@ class Fakturv4 extends MY_Controller
0, 0,
$kredit $kredit
); );
if (!$status) { if (!$status['status']) {
$this->sys_error_db($status['msg']); $this->sys_error_db($status['msg']);
exit; exit;
} }
} else if ($item['M_ItemItem_CategoryID'] == '2') { } else if ($item['M_ItemItem_CategoryID'] == '2') {
if (empty($item['M_ItemM_InventarisGolID'])) { $this->InsertJurnalTxInventaris($jurnalID, $item, $inv, $user['M_UserID']);
$this->db->trans_rollback();
$this->sys_error_db("[Error] item golongan inventaris belum ditentukan");
exit;
}
$sql_coainv = "SELECT
CoaMapInventarisHutangCoaID,
CoaMapInventarisHutangCoaNo,
CoaMapInventarisHutangCoaDesc
FROM coa_map_inventaris
WHERE CoaMapInventarisM_InventarisGolID = ?
AND CoaMapInventarisIsActive = 'Y'";
$que_coainv = $this->db->query($sql_coainv, [$item['M_ItemM_InventarisGolID']]);
if (!$que_coainv) {
$this->db->trans_rollback();
$this->sys_error_db("[Error] failed to get coa inventaris gol");
exit;
}
$item_coainv = $que_coainv->row_array();
if (empty($item_coainv)) {
$this->db->trans_rollback();
$this->sys_error_db("[Error] item inventaris coa not found");
exit;
}
$price = (doubleval($inv['SupplierInvoiceDetailTotal']) - doubleval($inv['SupplierInvoiceDetailDiscountPoProrata']));
// $taxPPH = doubleval($inv['SupplierInvoiceTaxPercentPph']) * $price / 100;
$taxPPN = doubleval($inv['SupplierInvoiceTaxPercentPpn']) * $price / 100;
$kredit = round($price + $taxPPN, 2);
$status = $this->InsertJurnalTx(
$jurnalID,
$item_coainv['CoaMapInventarisHutangCoaID'],
$item_coainv['CoaMapInventarisHutangCoaDesc'],
$user['M_UserID'],
0,
$kredit
);
if (!$status) {
$this->sys_error_db($status['msg']);
exit;
}
} else if ($item['M_ItemItem_CategoryID'] == '3') { } else if ($item['M_ItemItem_CategoryID'] == '3') {
$this->InsertJurnalTxAsset($jurnalID, $item['M_ItemID'], $inv, $user['M_UserID']); $this->InsertJurnalTxAsset($jurnalID, $item['M_ItemID'], $inv, $user['M_UserID']);
} else if ($item['M_ItemItem_CategoryID'] == '4') { } else if ($item['M_ItemItem_CategoryID'] == '4') {
@@ -1720,6 +1677,71 @@ class Fakturv4 extends MY_Controller
} }
} }
private function InsertJurnalTxInventaris($jurnalID, $items, $invoice, $userid)
{
if (empty($items['M_ItemM_InventarisGolID'])) {
$this->db->trans_rollback();
$this->sys_error_db("[Error] item golongan inventaris belum ditentukan");
exit;
}
$sql_coa = "SELECT
COALESCE(
i.M_InventarisItemCoaMappingID,
g.M_InventarisCoaMappingID
) AS MappingID,
COALESCE(
i.M_InventarisItemCoaMappingCoaHutangID,
g.M_InventarisCoaMappingCoaHutangID
) AS CoaHutangID,
c.coaDescription AS CoaHutangDesc
FROM (SELECT 1) AS inventory
LEFT JOIN m_inventaris_item_coa_mapping i
ON i.M_InventarisItemCoaMappingM_ItemID = ?
AND i.M_InventarisItemCoaMappingIsActive = 'Y'
LEFT JOIN m_inventaris_coa_mapping g
ON g.M_InventarisCoaMappingM_InventarisGolID = ?
AND g.M_InventarisCoaMappingIsActive = 'Y'
LEFT JOIN coa c
ON c.coaID = COALESCE(i.M_InventarisItemCoaMappingCoaHutangID, g.M_InventarisCoaMappingCoaHutangID)
AND c.coaIsActive = 'Y'";
$que_coa = $this->db->query($sql_coa, [
$items['M_ItemID'],
$items['M_ItemM_InventarisGolID'],
]);
if (!$que_coa) {
$this->db->trans_rollback();
$this->sys_error_db("[Error] failed query coa hutang inventaris");
exit;
}
$item_coainv = $que_coa->row_array();
if (empty($item_coainv['CoaHutangID'])) {
$this->db->trans_rollback();
$this->sys_error_db("[Error] coa ivnentory item {$items['M_ItemDesc']} not found");
exit;
}
$invtotal = doubleval($invoice['SupplierInvoiceDetailTotal']);
$disprorata = doubleval($invoice['SupplierInvoiceDetailDiscountPoProrata']);
$price = $invtotal - $disprorata;
$taxPPN = doubleval($invoice['SupplierInvoiceTaxPercentPpn']) * $price / 100;
$kredit = round($price + $taxPPN, 2);
$insert = $this->InsertJurnalTx(
$jurnalID,
$item_coainv['CoaHutangID'],
$item_coainv['CoaHutangDesc'],
$userid,
0,
$kredit
);
if (!$insert['status']) {
$this->db->trans_rollback();
$this->sys_error_db($insert['msg']);
exit;
}
}
private function InsertJurnalTxJasa($jurnalID, $itemid, $invoice, $userid) private function InsertJurnalTxJasa($jurnalID, $itemid, $invoice, $userid)
{ {
$sql_coa = "SELECT $sql_coa = "SELECT
@@ -1759,7 +1781,7 @@ class Fakturv4 extends MY_Controller
0, 0,
$kredit $kredit
); );
if (!$insert) { if (!$insert['status']) {
$this->db->trans_rollback(); $this->db->trans_rollback();
$this->sys_error_db($insert['msg']); $this->sys_error_db($insert['msg']);
exit; exit;
@@ -1804,7 +1826,7 @@ class Fakturv4 extends MY_Controller
0, 0,
$finalValue $finalValue
); );
if (!$insert) { if (!$insert['status']) {
$this->db->trans_rollback(); $this->db->trans_rollback();
$this->sys_error_db($insert['msg']); $this->sys_error_db($insert['msg']);
exit; exit;

View File

@@ -786,8 +786,8 @@ class PurchaseOrderAset extends MY_Controller {
$PurchaseOrderID, $PurchaseOrderID,
$para['supplierID'], $para['supplierID'],
$summary_downpayment ?: 0.00, $summary_downpayment ?: 0.00,
$para['contractStart'], $para['contractDate'],
$para['contractStart'], $para['contractDate'],
'Draft', 'Draft',
$user['M_UserID'], $user['M_UserID'],
$user['M_UserID'] $user['M_UserID']

View File

@@ -73,7 +73,7 @@ class ReceiveItemPoInventaris extends MY_Controller
$qry = $this->db->query($sql, [$user['M_BranchID'], $search]); $qry = $this->db->query($sql, [$user['M_BranchID'], $search]);
if (!$qry) { if (!$qry) {
$this->sys_error_db("[Error] query listing ruangan");; $this->sys_error_db("[Error] query listing ruangan");
exit; exit;
} }
@@ -2693,36 +2693,6 @@ class ReceiveItemPoInventaris extends MY_Controller
# generate barcode number each inventory item # # generate barcode number each inventory item #
$noBarcode = $this->generateNoBarcode($user, $param['ruanganID'], 'BNP', $item['ReceiveOrderPoItemID']); $noBarcode = $this->generateNoBarcode($user, $param['ruanganID'], 'BNP', $item['ReceiveOrderPoItemID']);
$sql_insert_barcode = "INSERT INTO t_barcode_barang(
T_BarcodeBarangReceiveOrderPoID,
T_BarcodeBarangReceiveOrderPoDetailID,
T_BarcodeBarangRefType,
T_BarcodeBarangM_ItemID,
T_BarcodeBarangItemUnitID,
T_BarcodeBarangNumber,
T_BarcodeBarangM_RuanganID,
T_BarcodeBarangM_BranchID,
T_BarcodeBarangIsActive,
T_BarcodeBarangUserID,
T_BarcodeBarangCreated
) VALUES(?,?,'PO',?,?,?,?,?,'Y',?,NOW())";
$que_insert_barcode = $this->db->query($sql_insert_barcode, [
$param['receiveOrderPoID'],
$item['ReceiveOrderPoDetailID'],
$item['ReceiveOrderPoItemID'],
$item['ReceiveOrderPoItemUnitID'],
$noBarcode,
$param['ruanganID'],
$user['M_BranchID'],
$user['M_UserID']
]);
if (!$que_insert_barcode) {
$this->db->trans_rollback();
$this->sys_error_db("[Error] insert no barcode invetaris");
exit;
}
$barcodeID = $this->db->insert_id();
# check if item already in stock # # check if item already in stock #
$sql_cekstock = "SELECT StockID $sql_cekstock = "SELECT StockID
@@ -2744,6 +2714,39 @@ class ReceiveItemPoInventaris extends MY_Controller
} }
$stockID = $que_cekstock->row_array()['StockID']; $stockID = $que_cekstock->row_array()['StockID'];
$sql_insert_barcode = "INSERT INTO t_barcode_barang(
T_BarcodeBarangStockID,
T_BarcodeBarangReceiveOrderPoID,
T_BarcodeBarangReceiveOrderPoDetailID,
T_BarcodeBarangRefType,
T_BarcodeBarangM_ItemID,
T_BarcodeBarangItemUnitID,
T_BarcodeBarangNumber,
T_BarcodeBarangM_RuanganID,
T_BarcodeBarangM_BranchID,
T_BarcodeBarangIsActive,
T_BarcodeBarangUserID,
T_BarcodeBarangCreated
) VALUES(?,?,?,'PO',?,?,?,?,?,'Y',?,NOW())";
$que_insert_barcode = $this->db->query($sql_insert_barcode, [
$stockID,
$param['receiveOrderPoID'],
$item['ReceiveOrderPoDetailID'],
$item['ReceiveOrderPoItemID'],
$item['ReceiveOrderPoItemUnitID'],
$noBarcode,
$param['ruanganID'],
$user['M_BranchID'],
$user['M_UserID']
]);
if (!$que_insert_barcode) {
$this->db->trans_rollback();
$this->sys_error_db("[Error] insert no barcode invetaris");
exit;
}
$barcodeID = $this->db->insert_id();
# insert stock inventaris # # insert stock inventaris #
$sql_stockinventaris = "INSERT INTO stock_inventory ( $sql_stockinventaris = "INSERT INTO stock_inventory (
StockInventoryStockID, StockInventoryStockID,

View File

@@ -98,7 +98,7 @@ class PurchaseInvoiceInstallment extends MY_Controller
$row["SupplierInvoiceID"], $row["SupplierInvoiceID"],
$amount, $amount,
$installDate, $installDate,
$dueDate, $installDate,
$createdBy $createdBy
]); ]);

View File

@@ -441,10 +441,6 @@ class PaymentV2 extends MY_Controller
]; ];
} }
// "PI Cicilan Aset - {$invoiceNumber}";
$description = '';
// "Jurnal cicilan ke-{$cicilanKe} dari {$totalCicilan} | {$namaKontrak} | PO: {$nomorPO} | Periode {$periode}";
switch ($param['type']) { switch ($param['type']) {
case 'DP': case 'DP':
$data_bayar['jurnaltitle'] .= " | Pembayaran Uang Muka Aset - {$param['dpkode']}"; $data_bayar['jurnaltitle'] .= " | Pembayaran Uang Muka Aset - {$param['dpkode']}";