From 2383b62ad0dca34114dd936837e7aad056be7d6b Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 26 May 2026 17:38:05 +0800 Subject: [PATCH] stock take update --- .../master/web/models/SaveWarehouseRequest.kt | 6 +- .../modules/report/service/ReportService.kt | 6 +- .../service/StockTakeVarianceReportService.kt | 311 +++++++++++++++++- .../web/StockTakeVarianceReportController.kt | 96 +++++- .../stock/entity/StockLedgerRepository.kt | 54 +++ 5 files changed, 445 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt b/src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt index bd042c4..458cbca 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt @@ -41,7 +41,11 @@ data class NewWarehouseRequest( data class StockTakeSectionInfo( val stockTakeSection: String, val stockTakeSectionDescription: String?, - val warehouseCount: Long + val warehouseCount: Long, + /** 該盤點區域所屬樓層/店別(取自倉庫 `store_id`,多筆時取第一筆非空) */ + val storeId: String? = null, + /** 倉庫 `area`(多筆時取第一筆非空),對應列表卡片上的區域欄位 */ + val warehouseArea: String? = null, ) 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 f6c5bf6..26ea397 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 @@ -279,7 +279,11 @@ return result SELECT CAST(st.stockTakeRoundId AS CHAR) AS value, CONCAT( - CAST(st.stockTakeRoundId AS CHAR), + CASE + WHEN NULLIF(TRIM(MAX(st.stockTakeRoundName)), '') IS NULL + THEN CONCAT('盤點輪次', CAST(st.stockTakeRoundId AS CHAR)) + ELSE TRIM(MAX(st.stockTakeRoundName)) + END, ' — ', DATE_FORMAT(MIN(st.planStart), '%Y-%m-%d') ) AS label diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt index 510019d..330ae9d 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt @@ -565,16 +565,311 @@ ORDER BY return jdbcDao.queryForList(sql, args) } + /** + * V2 Overall:每個 (lotId, warehouseId) 取 **status=completed** 且 **stockTakeRoundId 最大** 之一筆盤點紀錄 + * (同輪多筆時取 id 較大者)。期初/累計區間依該筆所屬輪次之 MIN~MAX(`date`)。 + * @param limitedToRoundIds null=全系統所有輪次;非 null=僅在指定輪次內取最新已完成 + */ + fun searchStockTakeVarianceReportV2Overall( + itemCode: String?, + storeId: String?, + limitedToRoundIds: List? = null, + ): List> { + val args = mutableMapOf() + + val roundIdsFilterSql = if (limitedToRoundIds.isNullOrEmpty()) { + "" + } else { + val inClause = limitedToRoundIds.mapIndexed { index, id -> + val key = "overallRoundId_$index" + args[key] = id + ":$key" + }.joinToString(", ") + "AND s.stockTakeRoundId IN ($inClause)" + } + + val storeIdSql = run { + val normalized = storeId?.trim() + if (normalized.isNullOrBlank() || normalized.equals("all", ignoreCase = true)) { + "" + } else { + args["storeId"] = normalized + """ + AND REPLACE(COALESCE(wh.store_id, ''), '/', '') = REPLACE(:storeId, '/', '') + """.trimIndent() + } + } + val itemCodeSql = buildMultiValueLikeClause( + itemCode, + "it.code", + "itemCode", + args + ) + + val sql = """ +WITH round_bounds AS ( + SELECT + s.stockTakeRoundId, + COALESCE(MIN(s.date), CURRENT_DATE) AS fromDate, + COALESCE(MAX(s.date), CURRENT_DATE) AS toDate + FROM stocktakerecord s + WHERE s.deleted = 0 + $roundIdsFilterSql + GROUP BY s.stockTakeRoundId +), +latest_str AS ( + SELECT + str.lotId, + str.warehouseId, + str.stockTakeRoundId, + str.bookQty, + str.varianceQty, + str.approverStockTakeQty, + str.date AS strDate, + str.id, + str.approverTime, + str.status AS stockTakeRecordStatus + FROM stocktakerecord str + INNER JOIN ( + SELECT + s.lotId, + s.warehouseId, + MAX(s.stockTakeRoundId) AS maxRound + FROM stocktakerecord s + WHERE s.deleted = 0 + AND s.status = 'completed' + $roundIdsFilterSql + GROUP BY s.lotId, s.warehouseId + ) mx ON mx.lotId = str.lotId + AND mx.warehouseId = str.warehouseId + AND mx.maxRound = str.stockTakeRoundId + WHERE str.deleted = 0 + AND str.status = 'completed' + AND NOT EXISTS ( + SELECT 1 + FROM stocktakerecord str2 + WHERE str2.deleted = 0 + AND str2.status = 'completed' + AND str2.lotId = str.lotId + AND str2.warehouseId = str.warehouseId + AND str2.stockTakeRoundId = str.stockTakeRoundId + AND str2.id > str.id + ) +), +line_bounds AS ( + SELECT + ill.id AS inventoryLotLineId, + ls.lotId, + ls.warehouseId, + rb.fromDate, + rb.toDate, + ls.bookQty, + ls.varianceQty, + ls.approverStockTakeQty, + ls.strDate, + ls.approverTime, + ls.stockTakeRecordStatus + FROM latest_str ls + INNER JOIN round_bounds rb ON rb.stockTakeRoundId = ls.stockTakeRoundId + 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 +), +in_agg AS ( + SELECT + lb.inventoryLotLineId, + SUM(CASE WHEN DATE(sil.receiptDate) < lb.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 lb.fromDate AND lb.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 line_bounds lb + INNER JOIN inventory_lot_line ill ON ill.id = lb.inventoryLotLineId + 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 lb.inventoryLotLineId +), +out_agg AS ( + SELECT + lb.inventoryLotLineId, + SUM(CASE WHEN DATE(sol.endTime) < lb.fromDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outBefore, + SUM(CASE WHEN DATE(sol.endTime) BETWEEN lb.fromDate AND lb.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 line_bounds lb + INNER JOIN inventory_lot_line ill ON ill.id = lb.inventoryLotLineId + 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 lb.inventoryLotLineId +), +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, + + lb.bookQty AS stkBookQty, + lb.approverStockTakeQty AS stkApproverQty, + lb.varianceQty AS stkVarianceQty, + lb.strDate AS stockTakeDateRaw, + lb.approverTime AS approvalDateTimeRaw, + lb.stockTakeRecordStatus AS stockTakeRecordStatus + FROM line_bounds lb + INNER JOIN inventory_lot_line ill ON ill.id = lb.inventoryLotLineId + 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 + INNER JOIN warehouse wh ON wh.id = lb.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 + $storeIdSql +) + +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 COALESCE(stockTakeRecordStatus, '') <> 'completed' THEN '0' + 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 COALESCE(stockTakeRecordStatus, '') <> 'completed' THEN '0%' + 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, + CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty + +FROM data +ORDER BY + itemNo, + lotNo, + storeLocation +""".trimIndent() + + return jdbcDao.queryForList(sql, args) + } + + /** Overall 模式報表副標題(全系統輪次,舊 API 相容) */ + fun getStockTakeVarianceOverallCaption(): String = + "全輪次(各批號/倉別最新已完成盤點)" + + /** 多選輪次 Overall 模式報表副標題 */ + fun getStockTakeVarianceMultiRoundCaption(stockTakeRoundIds: List): String { + if (stockTakeRoundIds.isEmpty()) return getStockTakeVarianceOverallCaption() + val parts = stockTakeRoundIds.map { getStockTakeRoundCaption(it) } + return "已選輪次(各批號/倉別最新已完成盤點):${parts.joinToString(";")}" + } + /** 報表表頭:盤點輪次說明(與 /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 + SELECT + CONCAT( + CASE + WHEN NULLIF(TRIM(MAX(st.stockTakeRoundName)), '') IS NULL + THEN CONCAT('盤點輪次', CAST(st.stockTakeRoundId AS CHAR)) + ELSE TRIM(MAX(st.stockTakeRoundName)) + END, + ' — ', + DATE_FORMAT(MIN(st.planStart), '%Y-%m-%d') + ) AS cap FROM stock_take st WHERE st.deleted = 0 AND st.stockTakeRoundId = :stockTakeRoundId @@ -585,7 +880,7 @@ ORDER BY mapOf("stockTakeRoundId" to stockTakeRoundId) ).firstOrNull() val cap = row?.get("cap") as? String - return if (!cap.isNullOrBlank()) cap else "Round $stockTakeRoundId" + return if (!cap.isNullOrBlank()) cap else "盤點輪次$stockTakeRoundId" } /** LIKE 多值工具方法 */ diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt index d78a82a..a181bf8 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt @@ -141,11 +141,14 @@ class StockTakeVarianceReportController( */ @GetMapping("/print-stock-take-variance-v2") fun generateStockTakeVarianceReportV2( - @RequestParam stockTakeRoundId: Long, + @RequestParam(required = false) overall: Boolean?, + @RequestParam(required = false) stockTakeRoundId: String?, @RequestParam(required = false) itemCode: String?, @RequestParam(required = false, name = "store_id") storeId: String?, @RequestParam(required = false) status: String?, ): ResponseEntity { + val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status) + val parameters = mutableMapOf() parameters["stockCategory"] = "All" @@ -164,16 +167,10 @@ class StockTakeVarianceReportController( parameters["lastInDateEnd"] = "" parameters["lastOutDateEnd"] = "" - parameters["stockTakeFilterCaption"] = - stockTakeVarianceReportService.getStockTakeRoundCaption(stockTakeRoundId) - parameters["stockTakeConditionLabel"] = "盤點輪次:" + parameters["stockTakeFilterCaption"] = rep012.caption + parameters["stockTakeConditionLabel"] = rep012.conditionLabel - val dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2( - stockTakeRoundId = stockTakeRoundId, - itemCode = itemCode, - storeId = storeId, - status = status, - ) + val dbData = rep012.dbData val stockTakeDateDisplay = dbData .mapNotNull { it["stockTakeDate"] as? String } .filter { it.isNotBlank() } @@ -198,18 +195,15 @@ class StockTakeVarianceReportController( @GetMapping("/print-stock-take-variance-v2-excel") fun exportStockTakeVarianceReportV2Excel( - @RequestParam stockTakeRoundId: Long, + @RequestParam(required = false) overall: Boolean?, + @RequestParam(required = false) stockTakeRoundId: String?, @RequestParam(required = false) itemCode: String?, @RequestParam(required = false, name = "store_id") storeId: String?, @RequestParam(required = false) status: String?, ): ResponseEntity { - val cap = stockTakeVarianceReportService.getStockTakeRoundCaption(stockTakeRoundId) - val dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2( - stockTakeRoundId = stockTakeRoundId, - itemCode = itemCode, - storeId = storeId, - status = status, - ) + val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status) + val cap = rep012.caption + val dbData = rep012.dbData val excelBytes = createStockTakeVarianceExcel( dbData = dbData, @@ -579,5 +573,71 @@ class StockTakeVarianceReportController( val abs = kotlin.math.abs(v).toLong() return "%,d".format(abs) } + + private data class Rep012ReportData( + val caption: String, + val conditionLabel: String, + val dbData: List>, + ) + + private fun parseStockTakeRoundIds(raw: String?): List = + raw + ?.split(",") + ?.mapNotNull { it.trim().toLongOrNull() } + ?.distinct() + ?: emptyList() + + /** + * rep-012:單輪=單輪查詢;多輪=已選輪次內各批號/倉最新已完成(status 固定 completed)。 + * overall=true 且未傳輪次時保留舊「全系統輪次」行為。 + */ + private fun resolveRep012ReportData( + overall: Boolean?, + stockTakeRoundId: String?, + itemCode: String?, + storeId: String?, + status: String?, + ): Rep012ReportData { + val roundIds = parseStockTakeRoundIds(stockTakeRoundId) + val useLegacyOverall = overall == true && roundIds.isEmpty() + require(useLegacyOverall || roundIds.isNotEmpty()) { + "stockTakeRoundId is required" + } + + if (useLegacyOverall) { + return Rep012ReportData( + caption = stockTakeVarianceReportService.getStockTakeVarianceOverallCaption(), + conditionLabel = "盤點條件:", + dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2Overall( + itemCode = itemCode, + storeId = storeId, + ), + ) + } + + if (roundIds.size == 1) { + val id = roundIds.first() + return Rep012ReportData( + caption = stockTakeVarianceReportService.getStockTakeRoundCaption(id), + conditionLabel = "盤點輪次:", + dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2( + stockTakeRoundId = id, + itemCode = itemCode, + storeId = storeId, + status = status, + ), + ) + } + + return Rep012ReportData( + caption = stockTakeVarianceReportService.getStockTakeVarianceMultiRoundCaption(roundIds), + conditionLabel = "盤點條件:", + dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2Overall( + itemCode = itemCode, + storeId = storeId, + limitedToRoundIds = roundIds, + ), + ) + } } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt index 1a9858a..1f4a276 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository +import java.time.LocalDate import java.time.LocalDateTime @Repository interface StockLedgerRepository: AbstractRepository { @@ -59,4 +60,57 @@ interface StockLedgerRepository: AbstractRepository { """) fun findLatestByItemId(@Param("itemId") itemId: Long): List fun findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId: Long): StockLedger? + + @Query(""" + SELECT sl FROM StockLedger sl + LEFT JOIN sl.stockOutLine sol + LEFT JOIN sol.inventoryLotLine ill + LEFT JOIN ill.inventoryLot il + LEFT JOIN sl.inventory inv + LEFT JOIN inv.item i + WHERE sl.deleted = false + AND sl.type = :type + AND sl.outQty IS NOT NULL AND sl.outQty > 0 + AND (:itemCode IS NULL OR sl.itemCode LIKE CONCAT('%', :itemCode, '%')) + AND (:itemName IS NULL OR i.name LIKE CONCAT('%', :itemName, '%')) + AND (:lotNo IS NULL OR il.lotNo LIKE CONCAT('%', :lotNo, '%')) + AND (:startDate IS NULL OR sl.date >= :startDate) + AND (:endDateExclusive IS NULL OR sl.date < :endDateExclusive) + ORDER BY sl.date DESC, sl.id DESC + """) + fun findStockIssueHandleRecords( + @Param("type") type: String, + @Param("itemCode") itemCode: String?, + @Param("itemName") itemName: String?, + @Param("lotNo") lotNo: String?, + @Param("startDate") startDate: LocalDate?, + @Param("endDateExclusive") endDateExclusive: LocalDate?, + pageable: Pageable, + ): Page + + @Query(""" + SELECT sl FROM StockLedger sl + LEFT JOIN sl.stockOutLine sol + LEFT JOIN sol.inventoryLotLine ill + LEFT JOIN ill.inventoryLot il + LEFT JOIN sl.inventory inv + LEFT JOIN inv.item i + WHERE sl.deleted = false + AND sl.type = 'Expiry' + AND sl.outQty IS NOT NULL AND sl.outQty > 0 + AND (:itemCode IS NULL OR sl.itemCode LIKE CONCAT('%', :itemCode, '%')) + AND (:itemName IS NULL OR i.name LIKE CONCAT('%', :itemName, '%')) + AND (:lotNo IS NULL OR il.lotNo LIKE CONCAT('%', :lotNo, '%')) + AND (:startDate IS NULL OR il.expiryDate >= :startDate) + AND (:endDateExclusive IS NULL OR il.expiryDate < :endDateExclusive) + ORDER BY sl.date DESC, sl.id DESC + """) + fun findExpiryItemHandleRecords( + @Param("itemCode") itemCode: String?, + @Param("itemName") itemName: String?, + @Param("lotNo") lotNo: String?, + @Param("startDate") startDate: LocalDate?, + @Param("endDateExclusive") endDateExclusive: LocalDate?, + pageable: Pageable, + ): Page } \ No newline at end of file