|
|
@@ -69,26 +69,439 @@ open class ChartService( |
|
|
/** |
|
|
/** |
|
|
* Purchase orders: count by status (pending, receiving, completed). |
|
|
* Purchase orders: count by status (pending, receiving, completed). |
|
|
* targetDate: when set, only POs whose orderDate is on that date; when null, all POs. |
|
|
* targetDate: when set, only POs whose orderDate is on that date; when null, all POs. |
|
|
|
|
|
* Optional multi-filters: supplierIds, itemCodes (line itemNo), purchaseOrderNos (po.code). |
|
|
*/ |
|
|
*/ |
|
|
fun getPurchaseOrderByStatus(targetDate: LocalDate?): List<Map<String, Any>> { |
|
|
|
|
|
|
|
|
fun getPurchaseOrderByStatus( |
|
|
|
|
|
targetDate: LocalDate?, |
|
|
|
|
|
supplierIds: List<Long>?, |
|
|
|
|
|
itemCodes: List<String>?, |
|
|
|
|
|
purchaseOrderNos: List<String>?, |
|
|
|
|
|
): List<Map<String, Any>> { |
|
|
val args = mutableMapOf<String, Any>() |
|
|
val args = mutableMapOf<String, Any>() |
|
|
val dateSql = if (targetDate != null) { |
|
|
val dateSql = if (targetDate != null) { |
|
|
args["targetDate"] = targetDate.toString() |
|
|
args["targetDate"] = targetDate.toString() |
|
|
"AND DATE(po.orderDate) = :targetDate" |
|
|
"AND DATE(po.orderDate) = :targetDate" |
|
|
} else "" |
|
|
} else "" |
|
|
|
|
|
val multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, null, args) |
|
|
val sql = """ |
|
|
val sql = """ |
|
|
SELECT |
|
|
SELECT |
|
|
COALESCE(po.status, 'unknown') AS status, |
|
|
COALESCE(po.status, 'unknown') AS status, |
|
|
COUNT(po.id) AS count |
|
|
COUNT(po.id) AS count |
|
|
FROM purchase_order po |
|
|
FROM purchase_order po |
|
|
WHERE po.deleted = 0 |
|
|
WHERE po.deleted = 0 |
|
|
$dateSql |
|
|
|
|
|
|
|
|
$dateSql $multiSql |
|
|
GROUP BY po.status |
|
|
GROUP BY po.status |
|
|
ORDER BY count DESC |
|
|
ORDER BY count DESC |
|
|
""".trimIndent() |
|
|
""".trimIndent() |
|
|
return jdbcDao.queryForList(sql, args) |
|
|
return jdbcDao.queryForList(sql, args) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Filter options for purchase chart (distinct suppliers / items / PO numbers on orderDate). |
|
|
|
|
|
*/ |
|
|
|
|
|
fun getPurchaseOrderFilterOptions(targetDate: LocalDate?): Map<String, Any> { |
|
|
|
|
|
if (targetDate == null) { |
|
|
|
|
|
return mapOf( |
|
|
|
|
|
"suppliers" to emptyList<Any>(), |
|
|
|
|
|
"items" to emptyList<Any>(), |
|
|
|
|
|
"poNos" to emptyList<Any>(), |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
val args = mutableMapOf<String, Any>("targetDate" to targetDate.toString()) |
|
|
|
|
|
val suppliersSql = """ |
|
|
|
|
|
SELECT DISTINCT po.supplierId AS supplierId, s.code AS code, COALESCE(s.name, '') AS name |
|
|
|
|
|
FROM purchase_order po |
|
|
|
|
|
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0 |
|
|
|
|
|
WHERE po.deleted = 0 AND DATE(po.orderDate) = :targetDate AND po.supplierId IS NOT NULL |
|
|
|
|
|
ORDER BY s.code |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
val itemsSql = """ |
|
|
|
|
|
SELECT DISTINCT pol.itemNo AS itemCode, COALESCE(it.name, '') AS itemName |
|
|
|
|
|
FROM purchase_order_line pol |
|
|
|
|
|
INNER JOIN purchase_order po ON po.id = pol.purchaseOrderId AND po.deleted = 0 |
|
|
|
|
|
LEFT JOIN items it ON it.id = pol.itemId AND it.deleted = 0 |
|
|
|
|
|
WHERE pol.deleted = 0 AND DATE(po.orderDate) = :targetDate |
|
|
|
|
|
ORDER BY pol.itemNo |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
val poNosSql = """ |
|
|
|
|
|
SELECT DISTINCT po.code AS poNo |
|
|
|
|
|
FROM purchase_order po |
|
|
|
|
|
WHERE po.deleted = 0 AND DATE(po.orderDate) = :targetDate |
|
|
|
|
|
ORDER BY po.code |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
return mapOf( |
|
|
|
|
|
"suppliers" to jdbcDao.queryForList(suppliersSql, args), |
|
|
|
|
|
"items" to jdbcDao.queryForList(itemsSql, args), |
|
|
|
|
|
"poNos" to jdbcDao.queryForList(poNosSql, args), |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 預計送貨: POs with estimatedArrivalDate on targetDate, grouped by 已送 / 未送 / 已取消. |
|
|
|
|
|
* 已取消: deleted = 1; 已送: deleted = 0 and status = completed; 未送: deleted = 0 and pending/receiving. |
|
|
|
|
|
* other: remaining active statuses (if any). |
|
|
|
|
|
*/ |
|
|
|
|
|
fun getPurchaseOrderEstimatedArrivalSummary( |
|
|
|
|
|
targetDate: LocalDate?, |
|
|
|
|
|
supplierIds: List<Long>?, |
|
|
|
|
|
itemCodes: List<String>?, |
|
|
|
|
|
purchaseOrderNos: List<String>?, |
|
|
|
|
|
): List<Map<String, Any>> { |
|
|
|
|
|
if (targetDate == null) return emptyList() |
|
|
|
|
|
val args = mutableMapOf<String, Any>("targetDate" to targetDate.toString()) |
|
|
|
|
|
val multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, null, args) |
|
|
|
|
|
val sql = """ |
|
|
|
|
|
SELECT |
|
|
|
|
|
CASE |
|
|
|
|
|
WHEN COALESCE(po.deleted, 0) = 1 THEN 'cancelled' |
|
|
|
|
|
WHEN COALESCE(po.deleted, 0) = 0 AND LOWER(COALESCE(po.status, '')) = 'completed' THEN 'delivered' |
|
|
|
|
|
WHEN COALESCE(po.deleted, 0) = 0 AND LOWER(COALESCE(po.status, '')) IN ('pending', 'receiving') THEN 'not_delivered' |
|
|
|
|
|
ELSE 'other' |
|
|
|
|
|
END AS bucket, |
|
|
|
|
|
COUNT(DISTINCT po.id) AS count |
|
|
|
|
|
FROM purchase_order po |
|
|
|
|
|
WHERE DATE(po.estimatedArrivalDate) = :targetDate |
|
|
|
|
|
AND po.estimatedArrivalDate IS NOT NULL |
|
|
|
|
|
$multiSql |
|
|
|
|
|
GROUP BY bucket |
|
|
|
|
|
ORDER BY count DESC |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
return jdbcDao.queryForList(sql, args) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 預計送貨: for a single bucket (delivered / not_delivered / cancelled / other) on [targetDate] |
|
|
|
|
|
* (DATE(estimatedArrivalDate)), return distinct suppliers, items, and PO headers matching the same rules as |
|
|
|
|
|
* [getPurchaseOrderEstimatedArrivalSummary] plus [buildPoMultiFiltersSql] bar filters. |
|
|
|
|
|
*/ |
|
|
|
|
|
fun getPurchaseOrderEstimatedArrivalBreakdown( |
|
|
|
|
|
targetDate: LocalDate?, |
|
|
|
|
|
estimatedArrivalBucket: String?, |
|
|
|
|
|
supplierIds: List<Long>?, |
|
|
|
|
|
itemCodes: List<String>?, |
|
|
|
|
|
purchaseOrderNos: List<String>?, |
|
|
|
|
|
): Map<String, Any> { |
|
|
|
|
|
if (targetDate == null || estimatedArrivalBucket.isNullOrBlank()) { |
|
|
|
|
|
return mapOf( |
|
|
|
|
|
"suppliers" to emptyList<Any>(), |
|
|
|
|
|
"items" to emptyList<Any>(), |
|
|
|
|
|
"purchaseOrders" to emptyList<Any>(), |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
val args = mutableMapOf<String, Any>() |
|
|
|
|
|
val bucketSql = buildPoDeletedAndEstimatedArrivalBucketSql(estimatedArrivalBucket, targetDate, args) |
|
|
|
|
|
val multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, null, args) |
|
|
|
|
|
val suppliersSql = """ |
|
|
|
|
|
SELECT |
|
|
|
|
|
po.supplierId AS supplierId, |
|
|
|
|
|
s.code AS supplierCode, |
|
|
|
|
|
COALESCE(s.name, '') AS supplierName, |
|
|
|
|
|
COUNT(DISTINCT po.id) AS poCount |
|
|
|
|
|
FROM purchase_order po |
|
|
|
|
|
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0 |
|
|
|
|
|
WHERE $bucketSql |
|
|
|
|
|
$multiSql |
|
|
|
|
|
GROUP BY po.supplierId, s.code, s.name |
|
|
|
|
|
ORDER BY poCount DESC, supplierCode |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
val itemsSql = """ |
|
|
|
|
|
SELECT |
|
|
|
|
|
pol.itemNo AS itemCode, |
|
|
|
|
|
COALESCE(it.name, '') AS itemName, |
|
|
|
|
|
COUNT(DISTINCT po.id) AS poCount, |
|
|
|
|
|
COALESCE(SUM(pol.qty), 0) AS totalQty |
|
|
|
|
|
FROM purchase_order po |
|
|
|
|
|
INNER JOIN purchase_order_line pol ON pol.purchaseOrderId = po.id AND pol.deleted = 0 |
|
|
|
|
|
LEFT JOIN items it ON it.id = pol.itemId AND it.deleted = 0 |
|
|
|
|
|
WHERE $bucketSql |
|
|
|
|
|
$multiSql |
|
|
|
|
|
GROUP BY pol.itemNo, it.name |
|
|
|
|
|
ORDER BY totalQty DESC, pol.itemNo |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
val posSql = """ |
|
|
|
|
|
SELECT |
|
|
|
|
|
po.id AS purchaseOrderId, |
|
|
|
|
|
po.code AS purchaseOrderNo, |
|
|
|
|
|
COALESCE(po.status, '') AS status, |
|
|
|
|
|
DATE_FORMAT(po.orderDate, '%Y-%m-%d') AS orderDate, |
|
|
|
|
|
po.supplierId AS supplierId, |
|
|
|
|
|
s.code AS supplierCode, |
|
|
|
|
|
COALESCE(s.name, '') AS supplierName |
|
|
|
|
|
FROM purchase_order po |
|
|
|
|
|
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0 |
|
|
|
|
|
WHERE $bucketSql |
|
|
|
|
|
$multiSql |
|
|
|
|
|
ORDER BY po.code |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
return mapOf( |
|
|
|
|
|
"suppliers" to jdbcDao.queryForList(suppliersSql, args), |
|
|
|
|
|
"items" to jdbcDao.queryForList(itemsSql, args), |
|
|
|
|
|
"purchaseOrders" to jdbcDao.queryForList(posSql, args), |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun buildPoMultiFiltersSql( |
|
|
|
|
|
supplierIds: List<Long>?, |
|
|
|
|
|
itemCodes: List<String>?, |
|
|
|
|
|
purchaseOrderNos: List<String>?, |
|
|
|
|
|
supplierCode: String?, |
|
|
|
|
|
args: MutableMap<String, Any>, |
|
|
|
|
|
): String { |
|
|
|
|
|
val sb = StringBuilder() |
|
|
|
|
|
val sids = supplierIds?.mapNotNull { it }?.filter { it > 0 }?.distinct() ?: emptyList() |
|
|
|
|
|
val scode = supplierCode?.trim()?.takeIf { it.isNotEmpty() } |
|
|
|
|
|
if (sids.isNotEmpty()) { |
|
|
|
|
|
args["filterSupplierIds"] = sids |
|
|
|
|
|
sb.append(" AND po.supplierId IN (:filterSupplierIds)") |
|
|
|
|
|
} else if (scode != null) { |
|
|
|
|
|
args["filterSupplierCode"] = scode |
|
|
|
|
|
sb.append( |
|
|
|
|
|
" AND EXISTS (SELECT 1 FROM shop sfc WHERE sfc.id = po.supplierId AND sfc.deleted = 0 AND sfc.code = :filterSupplierCode)" |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
val icodes = itemCodes?.map { it.trim() }?.filter { it.isNotEmpty() }?.distinct() ?: emptyList() |
|
|
|
|
|
if (icodes.isNotEmpty()) { |
|
|
|
|
|
args["filterItemCodes"] = icodes |
|
|
|
|
|
sb.append( |
|
|
|
|
|
""" AND EXISTS ( |
|
|
|
|
|
SELECT 1 FROM purchase_order_line polf |
|
|
|
|
|
WHERE polf.purchaseOrderId = po.id AND polf.deleted = 0 AND polf.itemNo IN (:filterItemCodes) |
|
|
|
|
|
)""" |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
val poNos = purchaseOrderNos?.map { it.trim() }?.filter { it.isNotEmpty() }?.distinct() ?: emptyList() |
|
|
|
|
|
if (poNos.isNotEmpty()) { |
|
|
|
|
|
args["filterPoNos"] = poNos |
|
|
|
|
|
sb.append(" AND po.code IN (:filterPoNos)") |
|
|
|
|
|
} |
|
|
|
|
|
return sb.toString() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Purchase orders detail rows by status for chart drill-down. |
|
|
|
|
|
* Example use: click "pending" segment -> show matching PO headers. |
|
|
|
|
|
* targetDate: optional filter — by order date or complete date (see dateFilter). |
|
|
|
|
|
* dateFilter: "order" = DATE(po.orderDate); "complete" = DATE(po.completeDate) (for received/completed on a day). |
|
|
|
|
|
* supplierIds / itemCodes / purchaseOrderNos: optional multi-filters (same as chart filter bar). |
|
|
|
|
|
* supplierCode: optional when supplierId list is empty (e.g. drill-down with code only). |
|
|
|
|
|
*/ |
|
|
|
|
|
fun getPurchaseOrderDetailsByStatus( |
|
|
|
|
|
status: String, |
|
|
|
|
|
targetDate: LocalDate?, |
|
|
|
|
|
dateFilter: String, |
|
|
|
|
|
supplierIds: List<Long>?, |
|
|
|
|
|
itemCodes: List<String>?, |
|
|
|
|
|
purchaseOrderNos: List<String>?, |
|
|
|
|
|
supplierCode: String?, |
|
|
|
|
|
estimatedArrivalBucket: String?, |
|
|
|
|
|
): List<Map<String, Any>> { |
|
|
|
|
|
val args = mutableMapOf<String, Any>( |
|
|
|
|
|
"status" to status.trim().lowercase() |
|
|
|
|
|
) |
|
|
|
|
|
val dateSql = buildPurchaseOrderDrillOrderDateSql(estimatedArrivalBucket, targetDate, dateFilter, args) |
|
|
|
|
|
val multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, supplierCode, args) |
|
|
|
|
|
val deletedAndEaSql = buildPoDeletedAndEstimatedArrivalBucketSql(estimatedArrivalBucket, targetDate, args) |
|
|
|
|
|
/** When 預計送貨 bucket is active, [buildPoDeletedAndEstimatedArrivalBucketSql] already fixes status shape (e.g. 未送 = pending|receiving). Do not AND with :status (would wrongly require e.g. completed). */ |
|
|
|
|
|
val statusSingleFilterSql = if (estimatedArrivalBucket.isNullOrBlank() || targetDate == null) { |
|
|
|
|
|
"AND LOWER(COALESCE(po.status, 'unknown')) = :status" |
|
|
|
|
|
} else { |
|
|
|
|
|
"" |
|
|
|
|
|
} |
|
|
|
|
|
val sql = """ |
|
|
|
|
|
SELECT |
|
|
|
|
|
po.id AS purchaseOrderId, |
|
|
|
|
|
po.code AS purchaseOrderNo, |
|
|
|
|
|
COALESCE(po.status, 'unknown') AS status, |
|
|
|
|
|
DATE_FORMAT(po.orderDate, '%Y-%m-%d') AS orderDate, |
|
|
|
|
|
DATE_FORMAT(po.estimatedArrivalDate, '%Y-%m-%d') AS estimatedArrivalDate, |
|
|
|
|
|
po.supplierId AS supplierId, |
|
|
|
|
|
s.code AS supplierCode, |
|
|
|
|
|
COALESCE(s.name, '') AS supplierName, |
|
|
|
|
|
COUNT(DISTINCT pol.id) AS itemCount, |
|
|
|
|
|
COALESCE(SUM(pol.qty), 0) AS totalQty |
|
|
|
|
|
FROM purchase_order po |
|
|
|
|
|
LEFT JOIN purchase_order_line pol ON pol.purchaseOrderId = po.id AND pol.deleted = 0 |
|
|
|
|
|
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0 |
|
|
|
|
|
WHERE $deletedAndEaSql |
|
|
|
|
|
$statusSingleFilterSql |
|
|
|
|
|
$dateSql $multiSql |
|
|
|
|
|
GROUP BY |
|
|
|
|
|
po.id, po.code, po.status, po.orderDate, po.estimatedArrivalDate, po.supplierId, s.code, s.name |
|
|
|
|
|
ORDER BY po.orderDate DESC, po.code DESC |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
return jdbcDao.queryForList(sql, args) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* When [estimatedArrivalBucket] is set (delivered / not_delivered / cancelled / other), filter by |
|
|
|
|
|
* [getPurchaseOrderEstimatedArrivalSummary] bucket rules on [targetDate] (estimated arrival date). |
|
|
|
|
|
* Includes soft-deleted rows when bucket is cancelled. |
|
|
|
|
|
*/ |
|
|
|
|
|
private fun buildPoDeletedAndEstimatedArrivalBucketSql( |
|
|
|
|
|
estimatedArrivalBucket: String?, |
|
|
|
|
|
targetDate: LocalDate?, |
|
|
|
|
|
args: MutableMap<String, Any>, |
|
|
|
|
|
): String { |
|
|
|
|
|
val b = estimatedArrivalBucket?.trim()?.lowercase()?.takeIf { it.isNotEmpty() } |
|
|
|
|
|
if (b == null || targetDate == null) { |
|
|
|
|
|
return "po.deleted = 0" |
|
|
|
|
|
} |
|
|
|
|
|
args["eaBucket"] = b |
|
|
|
|
|
args["eaTargetDate"] = targetDate.toString() |
|
|
|
|
|
return """ |
|
|
|
|
|
1=1 |
|
|
|
|
|
AND po.estimatedArrivalDate IS NOT NULL |
|
|
|
|
|
AND DATE(po.estimatedArrivalDate) = :eaTargetDate |
|
|
|
|
|
AND ( |
|
|
|
|
|
CASE |
|
|
|
|
|
WHEN COALESCE(po.deleted, 0) = 1 THEN 'cancelled' |
|
|
|
|
|
WHEN COALESCE(po.deleted, 0) = 0 AND LOWER(COALESCE(po.status, '')) = 'completed' THEN 'delivered' |
|
|
|
|
|
WHEN COALESCE(po.deleted, 0) = 0 AND LOWER(COALESCE(po.status, '')) IN ('pending', 'receiving') THEN 'not_delivered' |
|
|
|
|
|
ELSE 'other' |
|
|
|
|
|
END |
|
|
|
|
|
) = :eaBucket |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun buildPurchaseOrderTargetDateSql( |
|
|
|
|
|
targetDate: LocalDate?, |
|
|
|
|
|
dateFilter: String, |
|
|
|
|
|
args: MutableMap<String, Any>, |
|
|
|
|
|
): String { |
|
|
|
|
|
if (targetDate == null) return "" |
|
|
|
|
|
args["targetDate"] = targetDate.toString() |
|
|
|
|
|
return if (dateFilter.equals("complete", ignoreCase = true)) { |
|
|
|
|
|
"AND po.completeDate IS NOT NULL AND DATE(po.completeDate) = :targetDate" |
|
|
|
|
|
} else { |
|
|
|
|
|
"AND DATE(po.orderDate) = :targetDate" |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Drill-down rows for 預計送貨 must match [getPurchaseOrderEstimatedArrivalSummary] (estimated arrival date only). |
|
|
|
|
|
* If we also applied [buildPurchaseOrderTargetDateSql], many POs (orderDate ≠ 預計到貨日) would drop out and |
|
|
|
|
|
* supplier / item charts would collapse incorrectly. |
|
|
|
|
|
* |
|
|
|
|
|
* For 實際已送貨 (no bucket), [getPurchaseOrderByStatus] is always filtered by **order date** only. If drill used |
|
|
|
|
|
* [dateFilter] = complete, rows would be restricted to completeDate = targetDate and supplier/item charts would |
|
|
|
|
|
* collapse to a tiny set (often one supplier) vs the donut counts. |
|
|
|
|
|
*/ |
|
|
|
|
|
private fun buildPurchaseOrderDrillOrderDateSql( |
|
|
|
|
|
estimatedArrivalBucket: String?, |
|
|
|
|
|
targetDate: LocalDate?, |
|
|
|
|
|
@Suppress("UNUSED_PARAMETER") dateFilter: String, |
|
|
|
|
|
args: MutableMap<String, Any>, |
|
|
|
|
|
): String { |
|
|
|
|
|
if (!estimatedArrivalBucket.isNullOrBlank() && targetDate != null) { |
|
|
|
|
|
return "" |
|
|
|
|
|
} |
|
|
|
|
|
return buildPurchaseOrderTargetDateSql(targetDate, "order", args) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Purchase order item lines for drill-down. |
|
|
|
|
|
* Includes ordered qty, received qty and pending qty. |
|
|
|
|
|
*/ |
|
|
|
|
|
fun getPurchaseOrderItems(purchaseOrderId: Long): List<Map<String, Any>> { |
|
|
|
|
|
val args = mutableMapOf<String, Any>("purchaseOrderId" to purchaseOrderId) |
|
|
|
|
|
val sql = """ |
|
|
|
|
|
SELECT |
|
|
|
|
|
pol.id AS purchaseOrderLineId, |
|
|
|
|
|
pol.itemNo AS itemCode, |
|
|
|
|
|
COALESCE(it.name, '') AS itemName, |
|
|
|
|
|
COALESCE(pol.qty, 0) AS orderedQty, |
|
|
|
|
|
COALESCE(u.udfudesc, u.code, u.udfShortDesc, '') AS uom, |
|
|
|
|
|
COALESCE(SUM( |
|
|
|
|
|
CASE |
|
|
|
|
|
WHEN sil.id IS NOT NULL THEN COALESCE(sil.acceptedQty, 0) |
|
|
|
|
|
ELSE 0 |
|
|
|
|
|
END |
|
|
|
|
|
), 0) AS receivedQty, |
|
|
|
|
|
GREATEST( |
|
|
|
|
|
COALESCE(pol.qty, 0) - COALESCE(SUM( |
|
|
|
|
|
CASE |
|
|
|
|
|
WHEN sil.id IS NOT NULL THEN COALESCE(sil.acceptedQty, 0) |
|
|
|
|
|
ELSE 0 |
|
|
|
|
|
END |
|
|
|
|
|
), 0), |
|
|
|
|
|
0 |
|
|
|
|
|
) AS pendingQty |
|
|
|
|
|
FROM purchase_order_line pol |
|
|
|
|
|
INNER JOIN purchase_order po ON po.id = pol.purchaseOrderId AND po.deleted = 0 |
|
|
|
|
|
LEFT JOIN items it ON it.id = pol.itemId AND it.deleted = 0 |
|
|
|
|
|
LEFT JOIN uom_conversion u ON u.id = pol.uomId |
|
|
|
|
|
LEFT JOIN stock_in_line sil ON sil.purchaseOrderLineId = pol.id AND sil.deleted = 0 |
|
|
|
|
|
WHERE pol.deleted = 0 |
|
|
|
|
|
AND pol.purchaseOrderId = :purchaseOrderId |
|
|
|
|
|
GROUP BY pol.id, pol.itemNo, it.name, pol.qty, u.udfudesc, u.code, u.udfShortDesc |
|
|
|
|
|
ORDER BY pol.id |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
return jdbcDao.queryForList(sql, args) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Purchase order items grouped by item for a status (and optional targetDate). |
|
|
|
|
|
* Used for default drill-down chart when no PO is selected. |
|
|
|
|
|
* supplierIds / itemCodes / purchaseOrderNos: optional multi-filters. |
|
|
|
|
|
* supplierCode: optional when supplierId list is empty (e.g. drill-down with code only). |
|
|
|
|
|
*/ |
|
|
|
|
|
fun getPurchaseOrderItemsByStatus( |
|
|
|
|
|
status: String, |
|
|
|
|
|
targetDate: LocalDate?, |
|
|
|
|
|
dateFilter: String, |
|
|
|
|
|
supplierIds: List<Long>?, |
|
|
|
|
|
itemCodes: List<String>?, |
|
|
|
|
|
purchaseOrderNos: List<String>?, |
|
|
|
|
|
supplierCode: String?, |
|
|
|
|
|
estimatedArrivalBucket: String?, |
|
|
|
|
|
): List<Map<String, Any>> { |
|
|
|
|
|
val args = mutableMapOf<String, Any>( |
|
|
|
|
|
"status" to status.trim().lowercase() |
|
|
|
|
|
) |
|
|
|
|
|
val dateSql = buildPurchaseOrderDrillOrderDateSql(estimatedArrivalBucket, targetDate, dateFilter, args) |
|
|
|
|
|
val multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, supplierCode, args) |
|
|
|
|
|
val deletedAndEaSql = buildPoDeletedAndEstimatedArrivalBucketSql(estimatedArrivalBucket, targetDate, args) |
|
|
|
|
|
val statusSingleFilterSql = if (estimatedArrivalBucket.isNullOrBlank() || targetDate == null) { |
|
|
|
|
|
"AND LOWER(COALESCE(po.status, 'unknown')) = :status" |
|
|
|
|
|
} else { |
|
|
|
|
|
"" |
|
|
|
|
|
} |
|
|
|
|
|
val sql = """ |
|
|
|
|
|
SELECT |
|
|
|
|
|
pol.itemNo AS itemCode, |
|
|
|
|
|
COALESCE(it.name, '') AS itemName, |
|
|
|
|
|
COALESCE(u.udfudesc, u.code, u.udfShortDesc, '') AS uom, |
|
|
|
|
|
COALESCE(SUM(pol.qty), 0) AS orderedQty, |
|
|
|
|
|
COALESCE(SUM( |
|
|
|
|
|
CASE |
|
|
|
|
|
WHEN sil.id IS NOT NULL THEN COALESCE(sil.acceptedQty, 0) |
|
|
|
|
|
ELSE 0 |
|
|
|
|
|
END |
|
|
|
|
|
), 0) AS receivedQty, |
|
|
|
|
|
GREATEST( |
|
|
|
|
|
COALESCE(SUM(pol.qty), 0) - COALESCE(SUM( |
|
|
|
|
|
CASE |
|
|
|
|
|
WHEN sil.id IS NOT NULL THEN COALESCE(sil.acceptedQty, 0) |
|
|
|
|
|
ELSE 0 |
|
|
|
|
|
END |
|
|
|
|
|
), 0), |
|
|
|
|
|
0 |
|
|
|
|
|
) AS pendingQty |
|
|
|
|
|
FROM purchase_order po |
|
|
|
|
|
INNER JOIN purchase_order_line pol ON pol.purchaseOrderId = po.id AND pol.deleted = 0 |
|
|
|
|
|
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0 |
|
|
|
|
|
LEFT JOIN items it ON it.id = pol.itemId AND it.deleted = 0 |
|
|
|
|
|
LEFT JOIN uom_conversion u ON u.id = pol.uomId |
|
|
|
|
|
LEFT JOIN stock_in_line sil ON sil.purchaseOrderLineId = pol.id AND sil.deleted = 0 |
|
|
|
|
|
WHERE $deletedAndEaSql |
|
|
|
|
|
$statusSingleFilterSql |
|
|
|
|
|
$dateSql $multiSql |
|
|
|
|
|
GROUP BY pol.itemNo, it.name, u.udfudesc, u.code, u.udfShortDesc |
|
|
|
|
|
ORDER BY orderedQty DESC, pol.itemNo |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
return jdbcDao.queryForList(sql, args) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Stock in vs stock out by date. |
|
|
* Stock in vs stock out by date. |
|
|
* Stock in: stock_in_line.acceptedQty, date from stock_in.completeDate or receiptDate/created. |
|
|
* Stock in: stock_in_line.acceptedQty, date from stock_in.completeDate or receiptDate/created. |
|
|
|