|
|
|
@@ -1,4 +1,4 @@ |
|
|
|
package com.ffii.fpsms.modules.report.service |
|
|
|
package com.ffii.fpsms.modules.report.service |
|
|
|
|
|
|
|
import org.springframework.stereotype.Service |
|
|
|
import net.sf.jasperreports.engine.* |
|
|
|
@@ -272,6 +272,246 @@ return result |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fun getDistinctHandlersForFGStockOutTraceability(): List<String> { |
|
|
|
val sql = """ |
|
|
|
SELECT DISTINCT COALESCE(picker_user.name, modified_user.name, '') AS handler |
|
|
|
FROM stock_out_line sol |
|
|
|
INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0 AND so.type = 'do' |
|
|
|
LEFT JOIN user picker_user ON sol.handled_by = picker_user.id AND picker_user.deleted = 0 |
|
|
|
LEFT JOIN user modified_user ON sol.modifiedBy = modified_user.staffNo AND modified_user.deleted = 0 AND sol.handled_by IS NULL |
|
|
|
WHERE sol.deleted = 0 |
|
|
|
ORDER BY handler |
|
|
|
""".trimIndent() |
|
|
|
return jdbcDao.queryForList(sql, emptyMap<String, Any>()).map { row -> (row["handler"]?.toString() ?: "").trim() }.filter { it.isNotBlank() } |
|
|
|
} |
|
|
|
|
|
|
|
fun getStockTakeRoundOptions(): List<Map<String, Any>> { |
|
|
|
val sql = """ |
|
|
|
SELECT |
|
|
|
CAST(st.stockTakeRoundId AS CHAR) AS value, |
|
|
|
CONCAT( |
|
|
|
'Round ', |
|
|
|
st.stockTakeRoundId, |
|
|
|
' (', |
|
|
|
DATE_FORMAT(MIN(st.planStart), '%Y-%m-%d'), |
|
|
|
')' |
|
|
|
) AS label |
|
|
|
FROM stock_take st |
|
|
|
WHERE st.deleted = 0 |
|
|
|
AND st.stockTakeRoundId IS NOT NULL |
|
|
|
GROUP BY st.stockTakeRoundId |
|
|
|
ORDER BY MIN(st.planStart) DESC |
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
return jdbcDao.queryForList(sql, emptyMap<String, Any>()) |
|
|
|
} |
|
|
|
|
|
|
|
fun searchFGStockOutTraceabilityReport( |
|
|
|
stockCategory: String?, |
|
|
|
stockSubCategory: String?, |
|
|
|
itemCode: String?, |
|
|
|
year: String?, |
|
|
|
lastOutDateStart: String?, |
|
|
|
lastOutDateEnd: String?, |
|
|
|
handler: String? |
|
|
|
): List<Map<String, Any>> { |
|
|
|
val args = mutableMapOf<String, Any>() |
|
|
|
|
|
|
|
// Stock Category 过滤:通过 items.type |
|
|
|
val stockCategorySql = if (!stockCategory.isNullOrBlank()) { |
|
|
|
val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() } |
|
|
|
if (categories.isNotEmpty()) { |
|
|
|
val conditions = categories.mapIndexed { index, cat -> |
|
|
|
val paramName = "stockCategory_$index" |
|
|
|
args[paramName] = cat |
|
|
|
"it.type = :$paramName" |
|
|
|
} |
|
|
|
"AND (${conditions.joinToString(" OR ")})" |
|
|
|
} else { |
|
|
|
"" |
|
|
|
} |
|
|
|
} else { |
|
|
|
"" |
|
|
|
} |
|
|
|
|
|
|
|
// 移除 stockSubCategory 过滤(不需要) |
|
|
|
|
|
|
|
val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args) |
|
|
|
|
|
|
|
val yearSql = if (!year.isNullOrBlank()) { |
|
|
|
args["year"] = year |
|
|
|
"AND YEAR(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) = :year" |
|
|
|
} else { |
|
|
|
"" |
|
|
|
} |
|
|
|
|
|
|
|
val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { |
|
|
|
val formattedDate = lastOutDateStart.replace("/", "-") |
|
|
|
args["lastOutDateStart"] = formattedDate |
|
|
|
"AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) >= DATE(:lastOutDateStart)" |
|
|
|
} else "" |
|
|
|
|
|
|
|
val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { |
|
|
|
val formattedDate = lastOutDateEnd.replace("/", "-") |
|
|
|
args["lastOutDateEnd"] = formattedDate |
|
|
|
"AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)" |
|
|
|
} else "" |
|
|
|
|
|
|
|
val handlerSql = buildMultiValueExactClause( |
|
|
|
handler, |
|
|
|
"COALESCE(picker_user.name, modified_user.name, '')", |
|
|
|
"handler", |
|
|
|
args |
|
|
|
) |
|
|
|
|
|
|
|
val sql = """ |
|
|
|
SELECT |
|
|
|
IFNULL(DATE_FORMAT( |
|
|
|
IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate), |
|
|
|
'%Y-%m-%d' |
|
|
|
), '') AS deliveryDate, |
|
|
|
IFNULL(it.code, '') AS itemNo, |
|
|
|
IFNULL(it.name, '') AS itemName, |
|
|
|
IFNULL(uc.udfudesc, '') AS unitOfMeasure, |
|
|
|
IFNULL(dpor.deliveryNoteCode, '') AS dnNo, |
|
|
|
CAST(IFNULL(sp.id, 0) AS CHAR) AS customerId, |
|
|
|
IFNULL(sp.name, '') AS customerName, |
|
|
|
FORMAT( |
|
|
|
ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0 |
|
|
|
) AS qtyNumeric, |
|
|
|
FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS qty, |
|
|
|
'' AS truckNo, |
|
|
|
'' AS driver, |
|
|
|
IFNULL(do.code, '') AS deliveryOrderNo, |
|
|
|
IFNULL(po.code, '') AS fgPickOrderNo, |
|
|
|
IFNULL(po.code, '') AS stockReqNo, |
|
|
|
IFNULL(il.lotNo, '') AS lotNo, |
|
|
|
IFNULL(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate, |
|
|
|
FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS stockOutQty, |
|
|
|
COALESCE( |
|
|
|
picker_user.name, |
|
|
|
modified_user.name, |
|
|
|
'' |
|
|
|
) AS handler, |
|
|
|
COALESCE( |
|
|
|
picker_user.name, |
|
|
|
modified_user.name, |
|
|
|
'' |
|
|
|
) AS pickedBy, |
|
|
|
GROUP_CONCAT(DISTINCT wh.code ORDER BY wh.code SEPARATOR ', ') AS storeLocation, |
|
|
|
'' AS pickRemark, |
|
|
|
FORMAT( |
|
|
|
ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0 |
|
|
|
) AS totalStockOutQty, |
|
|
|
0 AS stockSubCategory |
|
|
|
FROM do_pick_order_line_record dpolr |
|
|
|
LEFT JOIN do_pick_order_record dpor |
|
|
|
ON dpolr.record_id = dpor.id |
|
|
|
AND dpor.deleted = 0 |
|
|
|
AND dpor.ticket_status = 'completed' |
|
|
|
INNER JOIN delivery_order do |
|
|
|
ON dpolr.do_order_id = do.id |
|
|
|
AND do.deleted = 0 |
|
|
|
LEFT JOIN shop sp |
|
|
|
ON do.shopId = sp.id |
|
|
|
AND sp.deleted = 0 |
|
|
|
LEFT JOIN delivery_order_line dol |
|
|
|
ON do.id = dol.deliveryOrderId |
|
|
|
AND dol.deleted = 0 |
|
|
|
LEFT JOIN items it |
|
|
|
ON dol.itemId = it.id |
|
|
|
AND it.deleted = 0 |
|
|
|
LEFT JOIN item_uom iu |
|
|
|
ON it.id = iu.itemId |
|
|
|
AND iu.stockUnit = 1 |
|
|
|
LEFT JOIN uom_conversion uc |
|
|
|
ON iu.uomId = uc.id |
|
|
|
LEFT JOIN pick_order_line pol |
|
|
|
ON dpolr.pick_order_id = pol.poId |
|
|
|
AND pol.itemId = it.id |
|
|
|
AND pol.deleted = 0 |
|
|
|
LEFT JOIN pick_order po |
|
|
|
ON pol.poId = po.id |
|
|
|
AND po.deleted = 0 |
|
|
|
LEFT JOIN stock_out_line sol |
|
|
|
ON pol.id = sol.pickOrderLineId |
|
|
|
AND sol.itemId = it.id |
|
|
|
AND sol.deleted = 0 |
|
|
|
LEFT JOIN stock_out so |
|
|
|
ON sol.stockOutId = so.id |
|
|
|
AND so.deleted = 0 |
|
|
|
AND so.type = 'do' |
|
|
|
LEFT JOIN inventory_lot_line ill |
|
|
|
ON sol.inventoryLotLineId = ill.id |
|
|
|
AND ill.deleted = 0 |
|
|
|
LEFT JOIN inventory_lot il |
|
|
|
ON ill.inventoryLotId = il.id |
|
|
|
AND il.deleted = 0 |
|
|
|
LEFT JOIN warehouse wh |
|
|
|
ON ill.warehouseId = wh.id |
|
|
|
AND wh.deleted = 0 |
|
|
|
LEFT JOIN user picker_user |
|
|
|
ON sol.handled_by = picker_user.id |
|
|
|
AND picker_user.deleted = 0 |
|
|
|
LEFT JOIN user modified_user |
|
|
|
ON sol.modifiedBy = modified_user.staffNo |
|
|
|
AND modified_user.deleted = 0 |
|
|
|
AND sol.handled_by IS NULL |
|
|
|
WHERE |
|
|
|
dpolr.deleted = 0 |
|
|
|
$stockCategorySql |
|
|
|
$itemCodeSql |
|
|
|
$yearSql |
|
|
|
$lastOutDateStartSql |
|
|
|
$lastOutDateEndSql |
|
|
|
$handlerSql |
|
|
|
GROUP BY |
|
|
|
sol.id, |
|
|
|
dpor.RequiredDeliveryDate, |
|
|
|
do.estimatedArrivalDate, |
|
|
|
it.code, |
|
|
|
it.name, |
|
|
|
uc.udfudesc, |
|
|
|
dpor.deliveryNoteCode, |
|
|
|
sp.id, |
|
|
|
sp.name, |
|
|
|
sol.qty, |
|
|
|
picker_user.name, |
|
|
|
modified_user.name, |
|
|
|
po.code, |
|
|
|
do.code, |
|
|
|
il.lotNo, |
|
|
|
il.expiryDate |
|
|
|
ORDER BY |
|
|
|
it.code, |
|
|
|
deliveryDate, |
|
|
|
il.lotNo |
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
val result = jdbcDao.queryForList(sql, args) |
|
|
|
|
|
|
|
// 打印查询结果 |
|
|
|
println("=== Query Result (Total: ${result.size} rows) ===") |
|
|
|
result.take(50).forEachIndexed { index, row -> |
|
|
|
println("Row $index:") |
|
|
|
println(" deliveryDate: ${row["deliveryDate"]}") |
|
|
|
println(" itemNo: ${row["itemNo"]}") |
|
|
|
println(" itemName: ${row["itemName"]}") |
|
|
|
println(" qty: ${row["qty"]}") |
|
|
|
println(" qtyNumeric: ${row["qtyNumeric"]}") |
|
|
|
println(" deliveryOrderNo: ${row["deliveryOrderNo"]}") |
|
|
|
println(" dnNo: ${row["dnNo"]}") |
|
|
|
println(" fgPickOrderNo: ${row["fgPickOrderNo"]}") |
|
|
|
println(" pickedBy: ${row["pickedBy"]}") |
|
|
|
println(" storeLocation: ${row["storeLocation"]}") |
|
|
|
println(" ---") |
|
|
|
} |
|
|
|
if (result.size > 50) { |
|
|
|
println("... (showing first 50 rows, total ${result.size} rows)") |
|
|
|
} |
|
|
|
|
|
|
|
return result |
|
|
|
} |
|
|
|
/** |
|
|
|
* Helper function to build SQL clause for comma-separated values. |
|
|
|
* Supports multiple values like "val1, val2, val3" and generates OR conditions with LIKE. |
|
|
|
@@ -791,16 +1031,61 @@ return result |
|
|
|
lastInDateEnd: String?, |
|
|
|
lastOutDateStart: String?, |
|
|
|
lastOutDateEnd: String?, |
|
|
|
stockTakeRoundId: Long, |
|
|
|
reportPeriodStart: String? = null, |
|
|
|
reportPeriodEnd: String? = null |
|
|
|
): List<Map<String, Any>> { |
|
|
|
val args = mutableMapOf<String, Any>() |
|
|
|
val fromDate = reportPeriodStart?.replace("/", "-")?.takeIf { it.isNotBlank() } |
|
|
|
?: java.time.LocalDate.now().withDayOfYear(1).toString() |
|
|
|
val toDate = reportPeriodEnd?.replace("/", "-")?.takeIf { it.isNotBlank() } |
|
|
|
?: java.time.LocalDate.now().toString() |
|
|
|
args["fromDate"] = fromDate |
|
|
|
args["toDate"] = toDate |
|
|
|
|
|
|
|
fun toLocalDate(value: Any?): java.time.LocalDate? = when (value) { |
|
|
|
is java.sql.Timestamp -> value.toLocalDateTime().toLocalDate() |
|
|
|
is java.time.LocalDateTime -> value.toLocalDate() |
|
|
|
is java.time.LocalDate -> value |
|
|
|
else -> null |
|
|
|
} |
|
|
|
|
|
|
|
val (resolvedFromDate, resolvedToDate) = run { |
|
|
|
// Fallback to existing date-range behavior (year-start -> today) when stock take round can't be resolved. |
|
|
|
val fallbackFrom = |
|
|
|
reportPeriodStart?.replace("/", "-")?.takeIf { it.isNotBlank() } |
|
|
|
?: java.time.LocalDate.now().withDayOfYear(1).toString() |
|
|
|
val fallbackTo = |
|
|
|
reportPeriodEnd?.replace("/", "-")?.takeIf { it.isNotBlank() } |
|
|
|
?: java.time.LocalDate.now().toString() |
|
|
|
|
|
|
|
val currentPlanStartAny = jdbcDao.queryForList( |
|
|
|
""" |
|
|
|
SELECT MIN(planStart) AS planStart |
|
|
|
FROM stock_take |
|
|
|
WHERE deleted = 0 |
|
|
|
AND stockTakeRoundId = :stockTakeRoundId |
|
|
|
""".trimIndent(), |
|
|
|
mapOf("stockTakeRoundId" to stockTakeRoundId) |
|
|
|
).firstOrNull()?.get("planStart") |
|
|
|
|
|
|
|
val currentPlanStartDate = toLocalDate(currentPlanStartAny) |
|
|
|
if (currentPlanStartDate == null) { |
|
|
|
fallbackFrom to fallbackTo |
|
|
|
} else { |
|
|
|
val nextPlanStartAny = jdbcDao.queryForList( |
|
|
|
""" |
|
|
|
SELECT MIN(planStart) AS planStart |
|
|
|
FROM stock_take |
|
|
|
WHERE deleted = 0 |
|
|
|
AND planStart > :currentPlanStart |
|
|
|
""".trimIndent(), |
|
|
|
mapOf("currentPlanStart" to currentPlanStartAny) |
|
|
|
).firstOrNull()?.get("planStart") |
|
|
|
|
|
|
|
val nextPlanStartDate = toLocalDate(nextPlanStartAny) |
|
|
|
val from = currentPlanStartDate.toString() |
|
|
|
val to = nextPlanStartDate?.minusDays(1)?.toString() ?: java.time.LocalDate.now().toString() |
|
|
|
from to to |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
args["fromDate"] = resolvedFromDate |
|
|
|
args["toDate"] = resolvedToDate |
|
|
|
|
|
|
|
val stockCategorySql = buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args) |
|
|
|
val itemCodeSql = buildMultiValueLikeClause(itemCode, "sl.itemCode", "itemCode", args) |
|
|
|
@@ -868,10 +1153,18 @@ return result |
|
|
|
sl.itemCode, |
|
|
|
sl.itemId, |
|
|
|
COALESCE(il_in.id, il_out.id) AS lotId, |
|
|
|
SUM(CASE WHEN DATE(sl.date) < :fromDate THEN COALESCE(sl.inQty, 0) - COALESCE(sl.outQty, 0) ELSE 0 END) AS openingBalance, |
|
|
|
SUM( |
|
|
|
CASE |
|
|
|
WHEN DATE(sl.date) BETWEEN :fromDate AND :toDate |
|
|
|
WHEN DATE(sl.date) <= :fromDate |
|
|
|
THEN COALESCE(sl.inQty, 0) - COALESCE(sl.outQty, 0) |
|
|
|
ELSE 0 |
|
|
|
END |
|
|
|
) AS openingBalance, |
|
|
|
SUM( |
|
|
|
CASE |
|
|
|
WHEN DATE(sl.date) > :fromDate |
|
|
|
AND DATE(sl.date) <= :toDate |
|
|
|
AND UPPER(TRIM(COALESCE(sl.type, ''))) <> 'TKE' |
|
|
|
AND sil.stockTakeLineId IS NULL |
|
|
|
THEN COALESCE(sl.inQty, 0) |
|
|
|
ELSE 0 |
|
|
|
@@ -879,8 +1172,10 @@ return result |
|
|
|
) AS cumStockIn, |
|
|
|
SUM( |
|
|
|
CASE |
|
|
|
WHEN DATE(sl.date) BETWEEN :fromDate AND :toDate |
|
|
|
WHEN DATE(sl.date) > :fromDate |
|
|
|
AND DATE(sl.date) <= :toDate |
|
|
|
AND COALESCE(sl.outQty, 0) > 0 |
|
|
|
AND UPPER(TRIM(COALESCE(sl.type, ''))) <> 'TKE' |
|
|
|
AND NOT ( |
|
|
|
LOWER(TRIM(COALESCE(sl.type, ''))) = 'stocktake' |
|
|
|
OR ( |
|
|
|
|