|
|
|
@@ -296,6 +296,263 @@ ORDER BY |
|
|
|
return result |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* V2:依盤點輪次 + 可選物料編號;僅輸出該輪已有 `stocktakerecord` 且 `status='completed'` 的列(未建立盤點紀錄的批號不列出)。 |
|
|
|
* 期初/累計區間取該輪紀錄之 MIN(`date`)~MAX(`date`)(與 V1 之 in/out 聚合邏輯一致,但以 rb 帶入)。 |
|
|
|
* 「審核時間」欄位:`approverTime` 之本地日期時間字串;無則退回盤點日 `date`。 |
|
|
|
*/ |
|
|
|
fun searchStockTakeVarianceReportV2( |
|
|
|
stockTakeRoundId: Long, |
|
|
|
itemCode: String?, |
|
|
|
): List<Map<String, Any>> { |
|
|
|
val countSql = """ |
|
|
|
SELECT COUNT(*) AS c FROM stocktakerecord s |
|
|
|
WHERE s.deleted = 0 |
|
|
|
AND s.stockTakeRoundId = :stockTakeRoundId |
|
|
|
AND s.status = 'completed' |
|
|
|
""".trimIndent() |
|
|
|
val cntRow = jdbcDao.queryForList( |
|
|
|
countSql, |
|
|
|
mapOf("stockTakeRoundId" to stockTakeRoundId) |
|
|
|
).firstOrNull() |
|
|
|
val cnt = (cntRow?.get("c") as? Number)?.toLong() ?: 0L |
|
|
|
if (cnt == 0L) return emptyList() |
|
|
|
|
|
|
|
val args = mutableMapOf<String, Any>() |
|
|
|
args["stockTakeRoundId"] = stockTakeRoundId |
|
|
|
val itemCodeSql = buildMultiValueLikeClause( |
|
|
|
itemCode, |
|
|
|
"it.code", |
|
|
|
"itemCode", |
|
|
|
args |
|
|
|
) |
|
|
|
|
|
|
|
val sql = """ |
|
|
|
WITH rb AS ( |
|
|
|
SELECT |
|
|
|
COALESCE(MIN(s.date), CURRENT_DATE) AS fromDate, |
|
|
|
COALESCE(MAX(s.date), CURRENT_DATE) AS toDate |
|
|
|
FROM stocktakerecord s |
|
|
|
WHERE s.deleted = 0 |
|
|
|
AND s.stockTakeRoundId = :stockTakeRoundId |
|
|
|
AND s.status = 'completed' |
|
|
|
), |
|
|
|
latest_str AS ( |
|
|
|
SELECT |
|
|
|
str.lotId, |
|
|
|
str.warehouseId, |
|
|
|
str.bookQty, |
|
|
|
str.varianceQty, |
|
|
|
str.approverStockTakeQty, |
|
|
|
str.date AS strDate, |
|
|
|
str.id, |
|
|
|
str.approverTime |
|
|
|
FROM stocktakerecord str |
|
|
|
WHERE str.deleted = 0 |
|
|
|
AND str.stockTakeRoundId = :stockTakeRoundId |
|
|
|
AND str.status = 'completed' |
|
|
|
), |
|
|
|
in_agg AS ( |
|
|
|
SELECT |
|
|
|
ill.id AS inventoryLotLineId, |
|
|
|
SUM(CASE WHEN DATE(sil.receiptDate) < rb.fromDate THEN |
|
|
|
CASE WHEN sil.purchaseOrderLineId IS NOT NULL |
|
|
|
THEN COALESCE(sil.acceptedQty, 0) |
|
|
|
WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL |
|
|
|
THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0)) |
|
|
|
ELSE COALESCE(sil.acceptedQty, 0) |
|
|
|
END |
|
|
|
ELSE 0 END) AS inBefore, |
|
|
|
SUM(CASE WHEN DATE(sil.receiptDate) BETWEEN rb.fromDate AND rb.toDate THEN |
|
|
|
CASE WHEN sil.purchaseOrderLineId IS NOT NULL |
|
|
|
THEN COALESCE(sil.acceptedQty, 0) |
|
|
|
WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL |
|
|
|
THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0)) |
|
|
|
ELSE COALESCE(sil.acceptedQty, 0) |
|
|
|
END |
|
|
|
ELSE 0 END) AS inDuring, |
|
|
|
MAX(CASE WHEN sil.receiptDate IS NOT NULL THEN DATE(sil.receiptDate) END) AS lastInDate |
|
|
|
FROM inventory_lot_line ill |
|
|
|
CROSS JOIN rb |
|
|
|
INNER JOIN inventory_lot il |
|
|
|
ON ill.inventoryLotId = il.id |
|
|
|
AND il.deleted = 0 |
|
|
|
INNER JOIN items it |
|
|
|
ON il.itemId = it.id |
|
|
|
AND it.deleted = 0 |
|
|
|
LEFT JOIN stock_in_line sil |
|
|
|
ON sil.inventoryLotLineId = ill.id |
|
|
|
AND sil.deleted = 0 |
|
|
|
AND sil.status = 'completed' |
|
|
|
LEFT JOIN item_uom iu_purchase |
|
|
|
ON it.id = iu_purchase.itemId |
|
|
|
AND iu_purchase.purchaseUnit = 1 |
|
|
|
AND iu_purchase.deleted = 0 |
|
|
|
LEFT JOIN item_uom iu_stock |
|
|
|
ON it.id = iu_stock.itemId |
|
|
|
AND iu_stock.stockUnit = 1 |
|
|
|
AND iu_stock.deleted = 0 |
|
|
|
WHERE ill.deleted = 0 |
|
|
|
GROUP BY ill.id |
|
|
|
), |
|
|
|
out_agg AS ( |
|
|
|
SELECT |
|
|
|
ill.id AS inventoryLotLineId, |
|
|
|
SUM(CASE WHEN DATE(sol.endTime) < rb.fromDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outBefore, |
|
|
|
SUM(CASE WHEN DATE(sol.endTime) BETWEEN rb.fromDate AND rb.toDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outDuring, |
|
|
|
MAX(CASE WHEN sol.endTime IS NOT NULL THEN DATE(sol.endTime) END) AS lastOutDate |
|
|
|
FROM inventory_lot_line ill |
|
|
|
CROSS JOIN rb |
|
|
|
LEFT JOIN stock_out_line sol |
|
|
|
ON sol.inventoryLotLineId = ill.id |
|
|
|
AND sol.deleted = 0 |
|
|
|
AND sol.status = 'completed' |
|
|
|
WHERE ill.deleted = 0 |
|
|
|
GROUP BY ill.id |
|
|
|
), |
|
|
|
in_out AS ( |
|
|
|
SELECT |
|
|
|
i.inventoryLotLineId, |
|
|
|
COALESCE(i.inBefore, 0) AS inBefore, |
|
|
|
COALESCE(o.outBefore, 0) AS outBefore, |
|
|
|
COALESCE(i.inDuring, 0) AS inDuring, |
|
|
|
COALESCE(o.outDuring, 0) AS outDuring, |
|
|
|
i.lastInDate, |
|
|
|
o.lastOutDate |
|
|
|
FROM in_agg i |
|
|
|
LEFT JOIN out_agg o ON o.inventoryLotLineId = i.inventoryLotLineId |
|
|
|
), |
|
|
|
data AS ( |
|
|
|
SELECT |
|
|
|
it.type AS stockSubCategory, |
|
|
|
it.code AS itemNo, |
|
|
|
it.name AS itemName, |
|
|
|
uc.udfudesc AS unitOfMeasure, |
|
|
|
|
|
|
|
il.lotNo AS lotNo, |
|
|
|
COALESCE(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate, |
|
|
|
wh.code AS storeLocation, |
|
|
|
|
|
|
|
(COALESCE(io.inBefore, 0) - COALESCE(io.outBefore, 0)) AS openingQty, |
|
|
|
COALESCE(io.inDuring, 0) AS inQty, |
|
|
|
COALESCE(io.outDuring, 0) AS outQty, |
|
|
|
((COALESCE(io.inBefore, 0) - COALESCE(io.outBefore, 0)) + COALESCE(io.inDuring, 0) - COALESCE(io.outDuring, 0)) AS currentQty, |
|
|
|
|
|
|
|
io.lastInDate AS lastInDateRaw, |
|
|
|
io.lastOutDate AS lastOutDateRaw, |
|
|
|
|
|
|
|
ls.bookQty AS stkBookQty, |
|
|
|
ls.approverStockTakeQty AS stkApproverQty, |
|
|
|
ls.varianceQty AS stkVarianceQty, |
|
|
|
ls.strDate AS stockTakeDateRaw, |
|
|
|
ls.approverTime AS approvalDateTimeRaw |
|
|
|
FROM latest_str ls |
|
|
|
INNER JOIN inventory_lot il |
|
|
|
ON ls.lotId = il.id |
|
|
|
AND il.deleted = 0 |
|
|
|
INNER JOIN inventory_lot_line ill |
|
|
|
ON ill.inventoryLotId = il.id |
|
|
|
AND ill.warehouseId = ls.warehouseId |
|
|
|
AND ill.deleted = 0 |
|
|
|
INNER JOIN items it |
|
|
|
ON il.itemId = it.id |
|
|
|
AND it.deleted = 0 |
|
|
|
INNER JOIN warehouse wh |
|
|
|
ON wh.id = ls.warehouseId |
|
|
|
AND wh.deleted = 0 |
|
|
|
|
|
|
|
LEFT JOIN item_uom iu |
|
|
|
ON it.id = iu.itemId |
|
|
|
AND iu.stockUnit = 1 |
|
|
|
AND iu.deleted = 0 |
|
|
|
LEFT JOIN uom_conversion uc |
|
|
|
ON iu.uomId = uc.id |
|
|
|
|
|
|
|
LEFT JOIN in_out io |
|
|
|
ON io.inventoryLotLineId = ill.id |
|
|
|
|
|
|
|
WHERE 1=1 |
|
|
|
$itemCodeSql |
|
|
|
) |
|
|
|
|
|
|
|
SELECT |
|
|
|
stockSubCategory, |
|
|
|
itemNo, |
|
|
|
itemName, |
|
|
|
unitOfMeasure, |
|
|
|
lotNo, |
|
|
|
expiryDate, |
|
|
|
storeLocation, |
|
|
|
|
|
|
|
CASE WHEN COALESCE(openingQty, 0) < 0 THEN CONCAT('(', FORMAT(-openingQty, 0), ')') ELSE FORMAT(COALESCE(openingQty, 0), 0) END AS openingBalance, |
|
|
|
CASE WHEN COALESCE(inQty, 0) < 0 THEN CONCAT('(', FORMAT(-inQty, 0), ')') ELSE FORMAT(COALESCE(inQty, 0), 0) END AS cumStockIn, |
|
|
|
CASE WHEN COALESCE(outQty, 0) < 0 THEN CONCAT('(', FORMAT(-outQty, 0), ')') ELSE FORMAT(COALESCE(outQty, 0), 0) END AS cumStockOut, |
|
|
|
CASE WHEN COALESCE(stkBookQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkBookQty, 0), ')') ELSE FORMAT(COALESCE(stkBookQty, 0), 0) END AS currentBookBalance, |
|
|
|
|
|
|
|
COALESCE(DATE_FORMAT(lastInDateRaw, '%Y-%m-%d'), '') AS lastInDate, |
|
|
|
COALESCE(DATE_FORMAT(lastOutDateRaw, '%Y-%m-%d'), '') AS lastOutDate, |
|
|
|
COALESCE( |
|
|
|
DATE_FORMAT(approvalDateTimeRaw, '%Y-%m-%d %H:%i:%s'), |
|
|
|
COALESCE(DATE_FORMAT(stockTakeDateRaw, '%Y-%m-%d'), '') |
|
|
|
) AS stockTakeDate, |
|
|
|
|
|
|
|
CASE |
|
|
|
WHEN stkApproverQty IS NULL THEN '0' |
|
|
|
WHEN COALESCE(stkApproverQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkApproverQty, 0), ')') |
|
|
|
ELSE FORMAT(COALESCE(stkApproverQty, 0), 0) |
|
|
|
END AS stockTakeQty, |
|
|
|
|
|
|
|
CASE |
|
|
|
WHEN stkVarianceQty IS NULL THEN '0' |
|
|
|
WHEN COALESCE(stkVarianceQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkVarianceQty, 0), ')') |
|
|
|
ELSE FORMAT(COALESCE(stkVarianceQty, 0), 0) |
|
|
|
END AS variance, |
|
|
|
|
|
|
|
CASE |
|
|
|
WHEN stkVarianceQty IS NULL THEN '0%' |
|
|
|
WHEN COALESCE(stkBookQty, 0) = 0 THEN '0%' |
|
|
|
WHEN (COALESCE(stkVarianceQty, 0) / stkBookQty) * 100 < 0 THEN CONCAT('(', FORMAT(-(COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%)') |
|
|
|
ELSE CONCAT(FORMAT((COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%') |
|
|
|
END AS variancePercentage, |
|
|
|
|
|
|
|
CASE WHEN SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalOpeningBalance, |
|
|
|
CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn, |
|
|
|
CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockOut, |
|
|
|
CASE WHEN SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCurrentBalance |
|
|
|
|
|
|
|
FROM data |
|
|
|
ORDER BY |
|
|
|
itemNo, |
|
|
|
lotNo, |
|
|
|
storeLocation |
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
return jdbcDao.queryForList(sql, args) |
|
|
|
} |
|
|
|
|
|
|
|
/** 報表表頭:盤點輪次說明(與 /report/stock-take-rounds 選項格式一致) */ |
|
|
|
fun getStockTakeRoundCaption(stockTakeRoundId: Long): String { |
|
|
|
val sql = """ |
|
|
|
SELECT CONCAT( |
|
|
|
'Round ', |
|
|
|
CAST(st.stockTakeRoundId AS CHAR), |
|
|
|
' (', |
|
|
|
DATE_FORMAT(MIN(st.planStart), '%Y-%m-%d'), |
|
|
|
')' |
|
|
|
) AS cap |
|
|
|
FROM stock_take st |
|
|
|
WHERE st.deleted = 0 |
|
|
|
AND st.stockTakeRoundId = :stockTakeRoundId |
|
|
|
GROUP BY st.stockTakeRoundId |
|
|
|
""".trimIndent() |
|
|
|
val row = jdbcDao.queryForList( |
|
|
|
sql, |
|
|
|
mapOf("stockTakeRoundId" to stockTakeRoundId) |
|
|
|
).firstOrNull() |
|
|
|
val cap = row?.get("cap") as? String |
|
|
|
return if (!cap.isNullOrBlank()) cap else "Round $stockTakeRoundId" |
|
|
|
} |
|
|
|
|
|
|
|
/** LIKE 多值工具方法 */ |
|
|
|
private fun buildMultiValueLikeClause( |
|
|
|
paramValue: String?, |
|
|
|
|