7 Commits

15 changed files with 853 additions and 114 deletions

104
README.md
View File

@@ -1,2 +1,106 @@
# 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 1–5)
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

@@ -0,0 +1,296 @@
<?php
class ServiceCoaMapping extends MY_Controller
{
var $db;
public function index()
{
echo "Jasa Coa Mapping API";
}
public function __construct()
{
parent::__construct();
}
public function getListItemJasa()
{
try {
if (!$this->sys_input) {
throw new Exception("Invalid token");
}
$para = $this->sys_input;
$keyword = "%";
if ($para['keyword'] != '') {
$keyword .= $para['keyword'] . "%";
}
$sql = "SELECT
M_ItemID,
M_ItemCode,
M_ItemDesc
FROM m_item
WHERE M_ItemIsActive = 'Y'
AND M_ItemDesc LIKE ?
AND M_ItemItem_CategoryID = '4'";
$que = $this->db->query($sql, [$keyword]);
if (!$que) {
throw new Exception('failed to get item jasa', 1);
}
$data = $que->result_array();
$this->sys_ok($data);
} catch (Exception $e) {
$mssg = "[Error] " . $e->getMessage();
$code = $e->getCode();
if ($code == 0) {
$this->sys_error($mssg);
} else {
$this->sys_error_db($mssg);
}
exit();
}
}
public function getListCoa()
{
try {
if (!$this->sys_input) {
throw new Exception("Invalid token");
}
$para = $this->sys_input;
$keyword = "%" . $para["keyword"] . "%";
$sql = "SELECT
coaID,
coaAccountNo,
coaDescription
FROM coa
WHERE coaIsInput = 'Y'
AND (
coaDescription LIKE ?
OR coaAccountNo LIKE ?
)
AND coaIsActive = 'Y'
LIMIT 15";
$que = $this->db->query($sql, [$keyword, $keyword]);
if (!$que) {
throw new Exception("failed to query data coa", 1);
}
$data = $que->result_array();
$this->sys_ok($data);
} catch (Exception $e) {
$mssg = "[Error] " . $e->getMessage();
$code = $e->getCode();
if ($code == 0) {
$this->sys_error($mssg);
} else {
$this->sys_error_db($mssg);
}
exit();
}
}
public function getListServiceMapping()
{
try {
if (!$this->isLogin) {
throw new Exception('invalid token');
}
$para = $this->sys_input;
$keyword = "%" . $para['search'] . "%";
$offset = 0;
$limit = 10;
if ($para['pages'] > 0) {
$offset = ($para['pages'] - 1) * $limit;
}
$sql = "SELECT
ServiceCoaMappingID,
ServiceCoaMappingM_ItemID,
M_ItemDesc,
ServiceCoaMappingCoaJasaID,
coaJasa.coaAccountNo,
coaJasa.coaDescription,
ServiceCoaMappingCoaHutangID
coaDebt.coaAccountNo,
coaDebt.coaDescription
FROM service_coa_mapping
JOIN m_item ON M_ItemID = ServiceCoaMappingM_ItemID
AND M_ItemDesc LIKE ?
AND M_ItemIsActive = 'Y'
LEFT JOIN coa AS coaJasa
ON coaJasa.coaID = ServiceCoaMappingCoaJasaID
LEFT JOIN coa AS coaDebt
ON coaDebt.coaID = ServiceCoaMappingCoaHutangID
WHERE ServiceCoaMappingIsActive = 'Y'";
$sql_total = "SELECT COUNT(*) AS total FROM ($sql) AS x";
$que_total = $this->db->query($sql_total, [$keyword]);
if (!$que_total) {
throw new Exception('failed query total data', 1);
}
$total = $que_total->row_array()['total'];
$sql_data = $sql . " LIMIT ? OFFSET ? ";
$que_data = $this->db->query($sql_data, [
$keyword,
$limit,
$offset
]);
if (!$que_data) {
throw new Exception('failed to query data', 1);
}
$data = $que_data->result_array();
$output = [
'records' => $data,
'total' => $total
];
$this->sys_ok($output);
} catch (Exception $e) {
$mssg = "[Error] " . $e->getMessage();
$code = $e->getCode();
if ($code == 0) {
$this->sys_error($mssg);
} else {
$this->sys_error_db($mssg);
}
exit();
}
}
public function insertJasaCoaMapping()
{
try {
if (!$this->sys_input) {
throw new Exception("Invalid token");
}
$para = $this->sys_input;
$user = $this->sys_user;
$this->db->trans_begin();
$sql = "INSERT INTO service_coa_mapping (
ServiceCoaMappingM_ItemID,
ServiceCoaMappingCoaJasaID,
ServiceCoaMappingCoaHutangID,
ServiceCoaMappingIsActive,
ServiceCoaMappingUserID
) VALUES (?, ?, ?, 'Y', ?)";
$que = $this->db->query($sql, [
$para['M_ItemID'],
$para['CoaJasaID'],
$para['CoaHutangID'],
$user['M_UserID']
]);
if (!$que) {
throw new Exception('failed insert coa mapping jasa', 1);
}
$this->db->trans_commit();
$this->sys_ok("[Success] insert coa mapping jasa");
} catch (Exception $e) {
$this->db->trans_rollback();
$mssg = "[Error] " . $e->getMessage();
$code = $e->getCode();
if ($code == 0) {
$this->sys_error($mssg);
} else {
$this->sys_error_db($mssg);
}
exit();
}
}
public function updateJasaCoaMapping()
{
try {
if (!$this->isLogin) {
throw new Exception('invalid token');
}
$para = $this->sys_input;
$user = $this->sys_user;
$this->db->trans_begin();
$sql = "UPDATE service_coa_mapping
SET
ServiceCoaMappingM_ItemID = ?,
ServiceCoaMappingCoaJasaID = ?,
ServiceCoaMappingCoaHutangID = ?,
ServiceCoaMappingUserID = ?
WHERE ServiceCoaMappingID = ?";
$que = $this->db->query($sql, [
$para['M_ItemID'],
$para['CoaJasaID'],
$para['CoaHutangID'],
$user['M_UserID'],
$para['ServiceCoaMappingID']
]);
if (!$que) {
throw new Exception('failed update coa mapping jasa', 1);
}
$this->db->trans_commit();
$this->sys_ok("[success] update coa mapping jasa");
} catch (Exception $e) {
$this->db->trans_rollback();
$mssg = "[Error] " . $e->getMessage();
$code = $e->getCode();
if ($code == 0) {
$this->sys_error($mssg);
} else {
$this->sys_error_db($mssg);
}
exit();
}
}
public function deleteJasaCoaMapping()
{
try {
if (!$this->isLogin) {
throw new Exception('invalid token', 1);
}
$para = $this->sys_input;
$user = $this->sys_user;
$this->db->trans_begin();
$sql = "UPDATE service_coa_mapping
SET
ServiceCoaMappingIsActive = 'N',
ServiceCoaMappingUserID = ?
WHERE ServiceCoaMappingID = ?";
$que = $this->db->query($sql, [
$user['M_UserID'],
$para['ServiceCoaMappingID']
]);
if (!$que) {
throw new Exception('failed soft delete coa mapping jasa', 1);
}
$this->db->trans_commit();
$this->sys_ok("[success] soft delete coa mapping jasa");
} catch (Exception $e) {
$this->db->trans_rollback();
$mssg = "[Error] " . $e->getMessage();
$code = $e->getCode();
if ($code == 0) {
$this->sys_error($mssg);
} else {
$this->sys_error_db($mssg);
}
exit();
}
}
}

View File

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

View File

@@ -791,26 +791,28 @@ class ReceiveItemPOAsset extends MY_Controller
// insert table ro #
$sqlInsRO = 'INSERT INTO receive_order_po (
ReceiveOrderPoNumber,
ReceiveOrderPoSupplierID,
ReceiveOrderPoIDate,
ReceiveOrderPoWarehouseID,
ReceiveOrderPoM_BranchCode,
ReceiveOrderPoS_RegionalID,
ReceiveOrderShippingCostAmount,
ReceiveOrderShippingCostIsPaid,
ReceiveOrderPoNote,
ReceiveOrderPoRefNumber,
ReceiveOrderPoDONumber,
ReceiveOrderPoTypePurchase,
ReceiveOrderPoCreatedUserID
) VALUE (?,?,?,?,?,?,?,?,?,?,?,?,?)';
ReceiveOrderPoNumber,
ReceiveOrderPoSupplierID,
ReceiveOrderPoIDate,
ReceiveOrderPoWarehouseID,
ReceiveOrderPoM_BranchCode,
ReceiveOrderPoS_RegionalID,
ReceiveOrderShippingCostAmount,
ReceiveOrderShippingCostIsPaid,
ReceiveOrderPoNote,
ReceiveOrderPoRefNumber,
ReceiveOrderPoDONumber,
ReceiveOrderPoTypePurchase,
ReceiveOrderPoCreatedUserID
)
SELECT ?, ?, ?, ?, b.M_BranchCode, ?, ?, ?, ?, ?, ?, ?, ?
FROM m_branch b
WHERE b.M_BranchID = ?';
$queInsRO = $this->db->query($sqlInsRO, [
$numGR,
$param['supplier'],
$param['date'],
$param['gudangID'],
$user['M_BranchCode'],
$user['S_RegionalID'],
$param['shipping'],
$param['shippingIsPaid'],
@@ -819,6 +821,7 @@ class ReceiveItemPOAsset extends MY_Controller
$param['reference'],
'aset',
$user['M_UserID'],
$param['branchID']
]);
if (!$queInsRO) {
$this->db->trans_rollback();
@@ -958,26 +961,28 @@ class ReceiveItemPOAsset extends MY_Controller
$roID = $param['ReceiveOrderPoID'];
// update receive order header #
$sqlHeader = "UPDATE receive_order_po SET
ReceiveOrderPoNumber = ?,
ReceiveOrderPoSupplierID = ?,
ReceiveOrderPoIDate = ?,
ReceiveOrderPoWarehouseID = ?,
ReceiveOrderPoM_BranchCode = ?,
ReceiveOrderPoS_RegionalID = ?,
ReceiveOrderShippingCostAmount = ?,
ReceiveOrderShippingCostIsPaid = ?,
ReceiveOrderPoNote = ?,
ReceiveOrderPoRefNumber = ?,
ReceiveOrderPoDONumber = ?
WHERE ReceiveOrderPoID = ?
AND ReceiveOrderPoIsActive = 'Y'";
$sqlHeader = "UPDATE receive_order_po r
JOIN m_branch b ON b.M_BranchID = ?
SET
r.ReceiveOrderPoNumber = ?,
r.ReceiveOrderPoSupplierID = ?,
r.ReceiveOrderPoIDate = ?,
r.ReceiveOrderPoWarehouseID = ?,
r.ReceiveOrderPoM_BranchCode = b.M_BranchCode,
r.ReceiveOrderPoS_RegionalID = ?,
r.ReceiveOrderShippingCostAmount = ?,
r.ReceiveOrderShippingCostIsPaid = ?,
r.ReceiveOrderPoNote = ?,
r.ReceiveOrderPoRefNumber = ?,
r.ReceiveOrderPoDONumber = ?
WHERE r.ReceiveOrderPoID = ?
AND r.ReceiveOrderPoIsActive = 'Y'";
$queHeader = $this->db->query($sqlHeader, [
$param['branchID'],
$param['ReceiveOrderPoNumber'],
$param['supplier'],
$param['date'],
$param['gudangID'],
$user['M_BranchCode'],
$user['S_RegionalID'],
$param['shipping'],
$param['shippingIsPaid'],
@@ -1460,7 +1465,7 @@ class ReceiveItemPOAsset extends MY_Controller
$diskon_proratapo = $besarDiskonPO / doubleval($data_po['totalAllPricePO']);
$ratioQty = (intval($elem['ReceiveOrderPoDetailQty']) / intval($elem['QtyAllPO']));
$diskon_prorata_item = round($ratioQty * $diskon_proratapo,2);
$diskon_prorata_item = round($ratioQty * $diskon_proratapo, 2);
$total_diskonpo = round($total_diskonpo + $diskon_prorata_item, 2);
$input_diskon = [$diskon, $diskon_prorata_item];
@@ -1918,33 +1923,6 @@ class ReceiveItemPOAsset extends MY_Controller
'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) {
throw new Exception('failed insert into t barcode barang', 1);
}
$barcodeID = $this->db->insert_id();
// check if item already in stock #
$sql_cekstock = "SELECT StockID
@@ -1964,6 +1942,36 @@ class ReceiveItemPOAsset extends MY_Controller
}
$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) {
throw new Exception('failed insert into t barcode barang', 1);
}
$barcodeID = $this->db->insert_id();
/* insert into table stock asset */
$sql_stockasset = 'INSERT INTO stock_asset (
StockAssetStockID,

View File

@@ -2693,36 +2693,6 @@ class ReceiveItemPoInventaris extends MY_Controller
# generate barcode number each inventory item #
$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 #
$sql_cekstock = "SELECT StockID
@@ -2744,6 +2714,39 @@ class ReceiveItemPoInventaris extends MY_Controller
}
$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 #
$sql_stockinventaris = "INSERT INTO stock_inventory (
StockInventoryStockID,

View File

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

View File

@@ -361,25 +361,25 @@ class PaymentV2 extends MY_Controller
// GET data bayar #
$sql_databayar = "SELECT
CONCAT('Jurnal Payment Invoice Nomor : ',
SupplierPaymentNumber, DATE_FORMAT(now(),
', Tanggal : %d-%m-%Y ')
) AS jurnaltitle,
CONCAT('Nomor pembayaran kasir : ',
SupplierPaymentCashierNumber,
' dan Nomor grup pembayaran : ',
SupplierPaymentGroupNumber
) AS jurnaldesc,
SupplierPaymentNumber AS no_payinv,
SupplierPaymentCashierNumber AS addonvalue,
SupplierPaymentCoaID AS coaID,
coaDescription,
SupplierPaymentAmount AS amount,
SupplierPaymentRounding AS rounding,
(SupplierPaymentAmount + SupplierPaymentRounding) AS bayar
FROM supplier_payment
JOIN coa ON coaID = SupplierPaymentCoaID
WHERE SupplierPaymentID = ?";
CONCAT('Jurnal Payment Invoice Nomor : ',
SupplierPaymentNumber, DATE_FORMAT(now(),
', Tanggal : %d-%m-%Y ')
) AS jurnaltitle,
CONCAT('Nomor pembayaran kasir : ',
SupplierPaymentCashierNumber,
' dan Nomor grup pembayaran : ',
SupplierPaymentGroupNumber
) AS jurnaldesc,
SupplierPaymentNumber AS no_payinv,
SupplierPaymentCashierNumber AS addonvalue,
SupplierPaymentCoaID AS coaID,
coaDescription,
SupplierPaymentAmount AS amount,
SupplierPaymentRounding AS rounding,
(SupplierPaymentAmount + SupplierPaymentRounding) AS bayar
FROM supplier_payment
JOIN coa ON coaID = SupplierPaymentCoaID
WHERE SupplierPaymentID = ?";
$que_databayar = $this->db->query($sql_databayar, [$param['orderid']]);
if (!$que_databayar) {
$this->db->trans_rollback();
@@ -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']) {
case 'DP':
$data_bayar['jurnaltitle'] .= " | Pembayaran Uang Muka Aset - {$param['dpkode']}";

View File

@@ -0,0 +1,131 @@
# Inventaris COA Mapping Notes
## Existing Table Purposes
`coa` is the Chart of Account master. It stores the real account identity:
- `coaID`: primary key, use this for mapping.
- `coaAccountNo`: account number for display/reporting.
- `coaDescription`: account name/description.
- `coaIsInput`: marks accounts that can be posted to.
- `coaIsActive`: active account flag.
- `coaLevel`: account hierarchy level.
`coa_group`, `coa_subgroup`, and `coa_mapgroup` organize COA rows for reporting or account selection:
```text
coa_group
-> coa_subgroup
-> coa_mapgroup
-> coa
```
Relationship:
```text
coa_group.CoaGroupID
= coa_subgroup.CoaSubGroupCoa_GroupID
coa_subgroup.CoaSubGroupID
= coa_mapgroup.CoaMapGroupCoaSubGroupID
coa.coaID
= coa_mapgroup.CoaMapGroupCoaID
```
These tables should not be changed for inventaris mapping. They can be used only to filter or help select valid COA rows in the UI.
`nat_group` and `nat_subgroup` classify normal item nature/type. They are mainly used by non-inventaris item flows, especially item category `1`.
```text
nat_group
-> nat_subgroup
-> m_item
```
Relationship:
```text
nat_group.Nat_GroupID
= nat_subgroup.Nat_SubGroupNat_GroupID
m_item.M_ItemNat_GroupID
= nat_group.Nat_GroupID
m_item.M_ItemNat_SubGroupID
= nat_subgroup.Nat_SubGroupID
```
`nat_subgroup_map`, `map_nat_group`, and `map_nat_subgroup` connect NAT classification to COA for normal item accounting:
- `nat_subgroup_map`: maps NAT subgroup to pendapatan, diskon, and retur COA.
- `map_nat_group`: maps NAT group to diskon, retur, DP, and debt/hutang COA.
- `map_nat_subgroup`: maps NAT group/subgroup to pendapatan, biaya, and persediaan COA.
Typical normal item accounting flow:
```text
m_item
-> M_ItemNat_GroupID + M_ItemNat_SubGroupID
-> map_nat_subgroup / map_nat_group / nat_subgroup_map
-> coa.coaID
```
## Relationship To `m_item`
`m_item` has multiple classification fields:
```text
M_ItemItem_CategoryID
M_ItemNat_GroupID
M_ItemNat_SubGroupID
M_ItemFa_ClassID
M_ItemM_InventarisGolID
```
For item category `1`, NAT group/subgroup is the main accounting classification.
For inventaris item category `2`, `M_ItemM_InventarisGolID` should be the main accounting classification. Do not add COA fields to `m_item`.
## New Inventaris COA Mapping
Use `m_inventaris_coa_mapping` as the default COA mapping source for inventaris items based on golongan inventaris:
```text
m_item.M_ItemM_InventarisGolID
-> m_inventaris_coa_mapping.M_InventarisCoaMappingM_InventarisGolID
-> coa.coaID fields
```
The mapping table stores `coa.coaID`, not account number text. Account number and description should be read from `coa` when displaying or posting journals.
Recommended lookup rule:
```text
1. Check active item-level override in m_inventaris_item_coa_mapping.
2. If no override exists, use active gol-level default in m_inventaris_coa_mapping.
3. If no mapping exists, stop the transaction and show an incomplete COA mapping error.
```
`m_inventaris_item_coa_mapping` is optional and only for specific item exceptions. Its COA fields are nullable so one item can override only one account and inherit the rest from the gol-level default.
## COA Fields For Inventaris
The default gol-level mapping should provide:
- `CoaAsetID`: asset/inventaris account.
- `CoaHutangID`: payable account.
- `CoaPembelianID`: purchase or acquisition account.
- `CoaBebanPenyusutanID`: depreciation expense account.
- `CoaAkumulasiPenyusutanID`: accumulated depreciation account.
- `CoaLabaPelepasanID`: gain on disposal account.
- `CoaRugiPelepasanID`: loss on disposal account.
Use active, postable COA rows where possible:
```sql
coa.coaIsActive = 'Y'
AND coa.coaIsInput = 'Y'
```
`coa_group` and `coa_subgroup` may help filter dropdown options, but the saved mapping should still point directly to `coa.coaID`.

View File

@@ -0,0 +1,53 @@
CREATE TABLE m_inventaris_coa_mapping (
M_InventarisCoaMappingID int(11) NOT NULL AUTO_INCREMENT,
M_InventarisCoaMappingM_InventarisGolID int(11) NOT NULL,
M_InventarisCoaMappingCoaInventarisID int(11) NOT NULL,
M_InventarisCoaMappingCoaHutangID int(11) NOT NULL,
M_InventarisCoaMappingCoaPembelianID int(11) NOT NULL,
M_InventarisCoaMappingCoaBebanPenyusutanID int(11) DEFAULT NULL,
M_InventarisCoaMappingCoaAkumulasiPenyusutanID int(11) DEFAULT NULL,
M_InventarisCoaMappingCoaLabaPelepasanID int(11) DEFAULT NULL,
M_InventarisCoaMappingCoaRugiPelepasanID int(11) DEFAULT NULL,
M_InventarisCoaMappingCreatedUserID int(11) NOT NULL,
M_InventarisCoaMappingCreated datetime NOT NULL DEFAULT current_timestamp(),
M_InventarisCoaMappingLastUpdated datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
M_InventarisCoaMappingIsActive char(1) NOT NULL DEFAULT 'Y',
PRIMARY KEY (M_InventarisCoaMappingID),
KEY M_InventarisCoaMappingM_InventarisGolID (M_InventarisCoaMappingM_InventarisGolID),
KEY M_InventarisCoaMappingCoaInventarisID (M_InventarisCoaMappingCoaInventarisID),
KEY M_InventarisCoaMappingCoaHutangID (M_InventarisCoaMappingCoaHutangID),
KEY M_InventarisCoaMappingCoaPembelianID (M_InventarisCoaMappingCoaPembelianID),
KEY M_InventarisCoaMappingCoaBebanPenyusutanID (M_InventarisCoaMappingCoaBebanPenyusutanID),
KEY M_InventarisCoaMappingCoaAkumulasiPenyusutanID (M_InventarisCoaMappingCoaAkumulasiPenyusutanID),
KEY M_InventarisCoaMappingCoaLabaPelepasanID (M_InventarisCoaMappingCoaLabaPelepasanID),
KEY M_InventarisCoaMappingCoaRugiPelepasanID (M_InventarisCoaMappingCoaRugiPelepasanID),
KEY M_InventarisCoaMappingCreatedUserID (M_InventarisCoaMappingCreatedUserID),
KEY M_InventarisCoaMappingIsActive (M_InventarisCoaMappingIsActive)
);
CREATE TABLE m_inventaris_item_coa_mapping (
M_InventarisItemCoaMappingID int(11) NOT NULL AUTO_INCREMENT,
M_InventarisItemCoaMappingM_ItemID int(11) NOT NULL,
M_InventarisItemCoaMappingCoaInventarisID int(11) DEFAULT NULL,
M_InventarisItemCoaMappingCoaHutangID int(11) DEFAULT NULL,
M_InventarisItemCoaMappingCoaPembelianID int(11) DEFAULT NULL,
M_InventarisItemCoaMappingCoaBebanPenyusutanID int(11) DEFAULT NULL,
M_InventarisItemCoaMappingCoaAkumulasiPenyusutanID int(11) DEFAULT NULL,
M_InventarisItemCoaMappingCoaLabaPelepasanID int(11) DEFAULT NULL,
M_InventarisItemCoaMappingCoaRugiPelepasanID int(11) DEFAULT NULL,
M_InventarisItemCoaMappingCreatedUserID int(11) NOT NULL,
M_InventarisItemCoaMappingCreated datetime NOT NULL DEFAULT current_timestamp(),
M_InventarisItemCoaMappingLastUpdated datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
M_InventarisItemCoaMappingIsActive char(1) NOT NULL DEFAULT 'Y',
PRIMARY KEY (M_InventarisItemCoaMappingID),
KEY M_InventarisItemCoaMappingM_ItemID (M_InventarisItemCoaMappingM_ItemID),
KEY M_InventarisItemCoaMappingCoaInventarisID (M_InventarisItemCoaMappingCoaInventarisID),
KEY M_InventarisItemCoaMappingCoaHutangID (M_InventarisItemCoaMappingCoaHutangID),
KEY M_InventarisItemCoaMappingCoaPembelianID (M_InventarisItemCoaMappingCoaPembelianID),
KEY M_InventarisItemCoaMappingCoaBebanPenyusutanID (M_InventarisItemCoaMappingCoaBebanPenyusutanID),
KEY M_InventarisItemCoaMappingCoaAkumulasiPenyusutanID (M_InventarisItemCoaMappingCoaAkumulasiPenyusutanID),
KEY M_InventarisItemCoaMappingCoaLabaPelepasanID (M_InventarisItemCoaMappingCoaLabaPelepasanID),
KEY M_InventarisItemCoaMappingCoaRugiPelepasanID (M_InventarisItemCoaMappingCoaRugiPelepasanID),
KEY M_InventarisItemCoaMappingCreatedUserID (M_InventarisItemCoaMappingCreatedUserID),
KEY M_InventarisItemCoaMappingIsActive (M_InventarisItemCoaMappingIsActive)
);

View File

@@ -0,0 +1,14 @@
CREATE TABLE contract_asset_attachment (
ContractAssetAttachmentID int(11) NOT NULL AUTO_INCREMENT,
ContractAssetAttachmentPurchaseOrderID int(11) NOT NULL,
ContractAssetAttachmentPurchaseOrderAssetContractID int(11) NOT NULL,
ContractAssetAttachmentFileName varchar(255) DEFAULT NULL,
ContractAssetAttachmentDate date DEFAULT NULL,
ContractAssetAttachmentIsActive char(1) DEFAULT 'Y',
ContractAssetAttachmentCreated datetime NOT NULL DEFAULT current_timestamp(),
ContractAssetAttachmentLastUpdated datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
ContractAssetAttachmentUserID int(11) DEFAULT 0,
PRIMARY KEY (ContractAssetAttachmentID),
KEY ContractAssetAttachmentPurchaseOrderID (ContractAssetAttachmentPurchaseOrderID),
KEY ContractAssetAttachmentIsActive (ContractAssetAttachmentIsActive)
);

View File

@@ -0,0 +1,24 @@
CREATE TABLE purchase_order_asset_contract (
PurchaseOrderAssetContractID int(11) NOT NULL AUTO_INCREMENT,
PurchaseOrderAssetContractPurchaseOrderID int(11) NOT NULL,
PurchaseOrderAssetContractTAssetAttachmentID int(11) NOT NULL,
PurchaseOrderAssetContractName varchar(128) DEFAULT NULL,
PurchaseOrderAssetContractDate date DEFAULT NULL,
PurchaseOrderAssetContractStartDate date DEFAULT NULL,
PurchaseOrderAssetContractEndDate date DEFAULT NULL,
PurchaseOrderAssetContractDuration int(11) DEFAULT 1,
PurchaseOrderAssetContractInstallmentNumber int(11) DEFAULT 1,
PurchaseOrderAssetContractInstallmentDate int(11) DEFAULT 1,
PurchaseOrderAssetContractInstallmentPayAmount decimal(20,2) DEFAULT 00.0,
PurchaseOrderAssetContractInstallmentDownPaymentType enum('nominal','persen') DEFAULT 'nominal',
PurchaseOrderAssetContractInstallmentDownPayment decimal(20,2) DEFAULT 00.0,
PurchaseOrderAssetContractCreatedUserID int(11) NOT NULL,
PurchaseOrderAssetContractCreated datetime NOT NULL DEFAULT current_timestamp(),
PurchaseOrderAssetContractLastUpdated datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PurchaseOrderAssetContractIsActive char(1) NOT NULL DEFAULT 'Y',
PRIMARY KEY (PurchaseOrderAssetContractID),
KEY PurchaseOrderAssetContractPurchaseOrderID (PurchaseOrderAssetContractPurchaseOrderID),
KEY PurchaseOrderAssetContractTAssetAttachmentID (PurchaseOrderAssetContractTAssetAttachmentID),
KEY PurchaseOrderAssetContractName (PurchaseOrderAssetContractName),
KEY PurchaseOrderAssetContractIsActive (PurchaseOrderAssetContractIsActive)
);

View File

@@ -0,0 +1,15 @@
CREATE TABLE service_coa_mapping (
ServiceCoaMappingID int(11) NOT NULL AUTO_INCREMENT,
ServiceCoaMappingM_ItemID int(11) NOT NULL,
ServiceCoaMappingCoaJasaID int(11) NOT NULL,
ServiceCoaMappingCoaHutangID int(11) NOT NULL,
ServiceCoaMappingIsActive char(1) NOT NULL DEFAULT 'Y',
ServiceCoaMappingCreated datetime NULL DEFAULT current_timestamp(),
ServiceCoaMappingLastUpdated datetime NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
ServiceCoaMappingUserID int(11) NULL DEFAULT 0,
PRIMARY KEY (ServiceCoaMappingID),
KEY ServiceCoaMappingM_ItemID (ServiceCoaMappingM_ItemID),
KEY ServiceCoaMappingCoaJasaID (ServiceCoaMappingCoaJasaID),
KEY ServiceCoaMappingCoaHutangID (ServiceCoaMappingCoaHutangID),
KEY ServiceCoaMappingIsActive (ServiceCoaMappingIsActive)
);

View File

@@ -0,0 +1,13 @@
CREATE TABLE stock_asset (
StockAssetID int(11) NOT NULL AUTO_INCREMENT,
StockAssetStockID int(11) NOT NULL,
StockAssetCabangID int(11) NOT NULL,
StockAssetCreatedUserID int(11) NOT NULL,
StockAssetCreated datetime NOT NULL DEFAULT current_timestamp(),
StockAssetLastUpdated datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
StockAssetIsActive char(1) NOT NULL DEFAULT 'Y',
PRIMARY KEY (StockAssetID),
KEY StockAssetStockID (StockAssetStockID),
KEY StockAssetCabangID (StockAssetCabangID),
KEY StockAssetIsActive (StockAssetIsActive)
);

View File

@@ -0,0 +1,36 @@
CREATE TABLE stock_inventory (
StockInventoryID int(11) NOT NULL AUTO_INCREMENT,
StockInventoryStockID int(11) NOT NULL,
StockInventoryRuanganID int(11) NOT NULL,
StockInventoryBarcode varchar(128) DEFAULT NULL,
StockInventoryCabangID int(11) NOT NULL,
StockInventoryCreatedUserID int(11) NOT NULL,
StockInventoryCreated datetime NOT NULL DEFAULT current_timestamp(),
StockInventoryLastUpdated datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
StockInventoryIsActive char(1) NOT NULL DEFAULT 'Y',
PRIMARY KEY (StockInventoryID),
KEY StockInventoryStockID (StockInventoryStockID),
KEY StockInventoryBarcode (StockInventoryBarcode),
KEY StockInventoryRuanganID (StockInventoryRuanganID),
KEY StockInventoryCabangID (StockInventoryCabangID),
KEY StockInventoryIsActive (StockInventoryIsActive)
);
CREATE TABLE stock_asset (
StockAssetID int(11) NOT NULL AUTO_INCREMENT,
StockAssetStockID int(11) NOT NULL,
StockAssetRuanganID int(11) NOT NULL,
StockAssetBarcode varchar(128) DEFAULT NULL,
StockAssetCabangID int(11) NOT NULL,
StockAssetCreatedUserID int(11) NOT NULL,
StockAssetCreated datetime NOT NULL DEFAULT current_timestamp(),
StockAssetLastUpdated datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
StockAssetIsActive char(1) NOT NULL DEFAULT 'Y',
PRIMARY KEY (StockAssetID),
KEY StockAssetStockID (StockAssetStockID),
KEY StockAssetBarcode (StockAssetBarcode),
KEY StockAssetRuanganID (StockAssetRuanganID),
KEY StockAssetCabangID (StockAssetCabangID),
KEY StockAssetIsActive (StockAssetIsActive)
);

View File

@@ -0,0 +1,46 @@
CREATE TABLE supplier_downpayment (
SupplierDownpaymentID int(11) NOT NULL AUTO_INCREMENT,
SupplierDownpaymentPurchasOrderID int(11) NOT NULL,
SupplierDownpaymentSupplierID int(11) NOT NULL,
SupplierDownpaymentAmount decimal(20,2) DEFAULT 00.0,
SupplierDownpaymentDate date DEFAULT NULL,
SupplierDownpaymentDueDate date DEFAULT NULL,
SupplierDownpaymentStatus ENUM('Draft','Pending','Paid') DEFAULT 'Draft',
SupplierDownpaymentIsLunas ENUM('Y','N') DEFAULT 'N',
SupplierDownpaymentIsActive ENUM('Y','N') DEFAULT 'Y',
SupplierDownpaymentCreated datetime NOT NULL DEFAULT current_timestamp(),
SupplierDownpaymentCreatedUserID int(11) DEFAULT 0,
SupplierDownpaymentLastUpdated datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
SupplierDownpaymentLastUpdatedUserID int(11) DEFAULT 0,
PRIMARY KEY (SupplierDownpaymentID),
KEY SupplierDownpaymentPurchasOrderID (SupplierDownpaymentPurchasOrderID),
KEY SupplierDownpaymentSupplierID (SupplierDownpaymentSupplierID),
KEY SupplierDownpaymentStatus (SupplierDownpaymentStatus),
KEY SupplierDownpaymentIsActive (SupplierDownpaymentIsActive)
);
CREATE TABLE supplier_installment (
SupplierInstallmentID int(11) NOT NULL AUTO_INCREMENT,
SupplierInstallmentPurchaseOrderID int(11) NOT NULL,
SupplierInstallmentSupplierID int(11) NOT NULL,
SupplierInstallmentSupplierInvoiceID int(11) NULL,
SupplierInstallmentAmount decimal(20,2) DEFAULT 00.0,
SupplierInstallmentDate date DEFAULT NULL,
SupplierInstallmentDueDate date DEFAULT NULL,
SupplierInstallmentPaymentID int(11) NOT NULL,
SupplierInstallmentPaidDate date DEFAULT NULL,
SupplierInstallmentStatus ENUM('Pending', 'Paid', 'Overdue') DEFAULT 'Pending',
SupplierInstallmentIsLunas ENUM('Y','N') DEFAULT 'N',
SupplierInstallmentIsActive ENUM('Y'm 'N') DEFAULT 'Y',
SupplierInstallmentCreated datetime NOT NULL DEFAULT current_timestamp(),
SupplierInstallmentCreatedUserID int(11) DEFAULT 0,
SupplierInstallmentLastUpdated datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
SupplierInstallmentLastUpdatedUserID int(11) DEFAULT 0,
PRIMARY KEY (SupplierInstallmentID),
KEY SupplierInstallmentPurchaseOrderID (SupplierInstallmentPurchaseOrderID),
KEY SupplierInstallmentSupplierID (SupplierInstallmentSupplierID),
KEY SupplierInstallmentSupplierInvoiceID (SupplierInstallmentSupplierInvoiceID),
KEY SupplierInstallmentPaymentID (SupplierInstallmentPaymentID),
KEY SupplierInstallmentStatus (SupplierInstallmentStatus),
KEY SupplierInstallmentIsActive (SupplierInstallmentIsActive)
);