From 33ac59b976783b62ea0457386ac1b3bb5617f400 Mon Sep 17 00:00:00 2001 From: "Tommy\\2Fi-Staff" Date: Mon, 23 Mar 2026 18:04:28 +0800 Subject: [PATCH] stockbalancereport --- .../modules/report/service/ReportService.kt | 315 +++++++++++++++++- .../modules/report/web/ReportController.kt | 10 +- 2 files changed, 313 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index 605c08c..3665737 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -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 { + 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()).map { row -> (row["handler"]?.toString() ?: "").trim() }.filter { it.isNotBlank() } +} + + fun getStockTakeRoundOptions(): List> { + 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()) + } + + fun searchFGStockOutTraceabilityReport( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, + handler: String? +): List> { + val args = mutableMapOf() + + // 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> { val args = mutableMapOf() - 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 ( diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt index 0f157fe..eabb5a2 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt @@ -17,6 +17,10 @@ class ReportController( private val reportService: ReportService, ) { + @GetMapping("/stock-take-rounds") + fun getStockTakeRounds(): List> = + reportService.getStockTakeRoundOptions() + @GetMapping("/print-report1") fun generateReport1( @RequestParam fromDate: String, @@ -175,7 +179,8 @@ class ReportController( @RequestParam(required = false) lastInDateStart: String?, @RequestParam(required = false) lastInDateEnd: String?, @RequestParam(required = false) lastOutDateStart: String?, - @RequestParam(required = false) lastOutDateEnd: String? + @RequestParam(required = false) lastOutDateEnd: String?, + @RequestParam stockTakeRoundId: Long ): ResponseEntity { val parameters = mutableMapOf() parameters["stockCategory"] = stockCategory ?: "All" @@ -199,7 +204,8 @@ class ReportController( lastInDateStart, lastInDateEnd, lastOutDateStart, - lastOutDateEnd + lastOutDateEnd, + stockTakeRoundId ) val pdfBytes = reportService.createPdfResponse(