From 876f3477add699777b328fb1dfc907d9412a240b Mon Sep 17 00:00:00 2001 From: Fai Luk Date: Fri, 18 Sep 2026 14:35:47 +0800 Subject: [PATCH] added paste item code to some reports --- .../m18/service/M18PurchaseOrderService.kt | 132 +++++++++++++--- .../ffii/fpsms/m18/web/M18TestController.kt | 11 +- .../fpsms/modules/common/SettingNames.java | 9 ++ .../entity/PurchaseOrderRepository.kt | 2 + .../service/PurchaseOrderService.kt | 4 + .../service/M18BomShopSyncReportService.kt | 6 +- .../report/service/ReportMultiValueTokens.kt | 12 ++ .../StockItemConsumptionTrendReportService.kt | 2 +- .../service/StockLedgerReportService.kt | 27 ++-- ...ialStockOutTraceabilityReportController.kt | 60 ++++++++ .../modules/report/web/ReportController.kt | 142 ++++++++++++++++-- .../report/web/ReportItemFilterRequest.kt | 33 ++++ ...emiFGProductionAnalysisReportController.kt | 54 +++++++ ...ockItemConsumptionTrendReportController.kt | 46 ++++++ .../report/web/StockLedgerReportController.kt | 81 ++++++++-- .../01_setting.sql | 14 ++ .../service/ReportMultiValueTokensTest.kt | 8 + 17 files changed, 585 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/report/web/ReportItemFilterRequest.kt create mode 100644 src/main/resources/db/changelog/changes/20260918_po_stockin_refresh_if_newer/01_setting.sql diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt index 0cba470..a0c4eed 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt @@ -15,10 +15,13 @@ import com.ffii.fpsms.modules.master.service.ShopService import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderLineStatus import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderStatus import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderType +import com.ffii.fpsms.modules.common.SettingNames import com.ffii.fpsms.modules.purchaseOrder.service.PurchaseOrderLineService import com.ffii.fpsms.modules.purchaseOrder.service.PurchaseOrderService import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderLineRequest import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderRequest +import com.ffii.fpsms.modules.settings.entity.Settings +import com.ffii.fpsms.modules.settings.service.SettingsService import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @@ -39,13 +42,15 @@ open class M18PurchaseOrderService( val itemsService: ItemsService, val shopService: ShopService, val itemUomService: ItemUomService, - val m18MasterDataService: M18MasterDataService + val m18MasterDataService: M18MasterDataService, + val settingsService: SettingsService, ) { val commonUtils = CommonUtils() val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) val lastModifyDateStart = "2025-05-14 14:00:00" val lastModifyDateEnd = "2025-05-14 14:30:00" + private val M18_LAST_MODIFY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") // val lastModifyDateConds = // "lastModifyDate=largerOrEqual=${lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${lastModifyDateEnd}" // val lastModifyDate = LocalDateTime.now().minusMinutes(30) @@ -227,40 +232,129 @@ open class M18PurchaseOrderService( // Follow the scheduler's original approach: // 1) use /search/search to find the PO id by code // 2) then sync by PO id (this reuses the same create/update logic) - val searchRequest = M18PurchaseOrderListRequest( - stSearch = "po", - params = null, - conds = "(code=equal=$code)" - ) - val poListResponse = try { - apiCallerService.get( - M18_FETCH_PURCHASE_ORDER_LIST_API, - searchRequest - ).block() - } catch (e: Exception) { - logger.error("(Getting Po List By Code) Error on Function - ${e.stackTrace}") - logger.error(e.message) - null + val conds = "(code=equal=$code)" + return saveFromPoListSearch(conds, searchPurchaseOrdersByConds(conds)) + } + + /** + * Stock-in `/po` lookup: create if missing; for a local **pending** PO, pull from M18 only when + * M18 list `lastModifyDate` is after [PurchaseOrder.modified] (last MTMS header write, usually last sync). + * Receiving / completed POs are not refreshed. + */ + open fun savePurchaseOrderByCodeIfNewer(code: String): SyncResult { + val trimmed = code.trim() + val localPo = purchaseOrderService.findByCode(trimmed) + if (localPo != null && !isStockInRefreshIfNewerEnabled()) { + logger.info( + "Purchase Order: Skipping if-newer M18 sync — setting ${SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED} " + + "is not true. code=$trimmed" + ) + return SyncResult( + totalProcessed = 0, + totalSuccess = 0, + totalFail = 0, + query = "skipped (ifNewer): setting ${SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED} is not true" + ) + } + if (localPo != null && localPo.status != PurchaseOrderStatus.PENDING) { + logger.info( + "Purchase Order: Skipping if-newer M18 sync — local PO id=${localPo.id} code=${localPo.code} " + + "status=${localPo.status?.value} (only pending may be refreshed)." + ) + return SyncResult( + totalProcessed = 0, + totalSuccess = 0, + totalFail = 0, + query = "skipped (ifNewer): local PO not pending: $trimmed status=${localPo.status?.value}" + ) + } + if (localPo == null) { + return savePurchaseOrderByCode(trimmed) + } + + val conds = "(code=equal=$trimmed)" + val poListResponse = searchPurchaseOrdersByConds(conds) + val m18Row = poListResponse?.values?.firstOrNull() + val localModified = localPo.modified ?: localPo.created + val m18LastModify = parseM18LastModifyDate(m18Row?.lastModifyDate) + if (m18Row == null || m18LastModify == null || localModified == null || !m18LastModify.isAfter(localModified)) { + logger.info( + "Purchase Order: Skipping if-newer M18 sync — M18 lastModifyDate not after " + + "purchase_order.modified. code=$trimmed localModified=$localModified " + + "m18LastModifyRaw=${m18Row?.lastModifyDate} m18LastModify=$m18LastModify" + ) + return SyncResult( + totalProcessed = 0, + totalSuccess = 0, + totalFail = 0, + query = "skipped (ifNewer): M18 lastModifyDate not after purchase_order.modified: " + + "code=$trimmed localModified=$localModified m18LastModify=${m18Row?.lastModifyDate}" + ) } + logger.info( + "Purchase Order: M18 lastModifyDate after purchase_order.modified — syncing. " + + "code=$trimmed localModified=$localModified m18LastModify=$m18LastModify" + ) + return saveFromPoListSearch(conds, poListResponse) + } + private fun saveFromPoListSearch( + query: String, + poListResponse: M18PurchaseOrderListResponse?, + ): SyncResult { val poValues = poListResponse?.values if (poValues.isNullOrEmpty()) { return SyncResult( totalProcessed = 1, totalSuccess = 0, totalFail = 1, - query = "code=equal=$code" + query = query ) } - val request = M18PurchaseOrderListResponseWithType( valuesWithType = mutableListOf(Pair(PurchaseOrderType.MATERIAL, poListResponse)), - query = "code=equal=$code" + query = query ) - return savePurchaseOrdersWithPreparedList(purchaseOrdersWithType = request) } + private fun isStockInRefreshIfNewerEnabled(): Boolean = + settingsService.findByName(SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED) + .map { Settings.VALUE_BOOLEAN_TRUE.equals(it.value?.trim(), ignoreCase = true) } + .orElse(false) + + private fun parseM18LastModifyDate(raw: String?): LocalDateTime? { + val value = raw?.trim().orEmpty() + if (value.isEmpty()) return null + return try { + LocalDateTime.parse(value, M18_LAST_MODIFY_FORMATTER) + } catch (_: Exception) { + try { + LocalDateTime.parse(value) + } catch (_: Exception) { + value.toLongOrNull()?.let { commonUtils.timestampToLocalDateTime(it) } + } + } + } + + private fun searchPurchaseOrdersByConds(conds: String): M18PurchaseOrderListResponse? { + val searchRequest = M18PurchaseOrderListRequest( + stSearch = "po", + params = null, + conds = conds + ) + return try { + apiCallerService.get( + M18_FETCH_PURCHASE_ORDER_LIST_API, + searchRequest + ).block() + } catch (e: Exception) { + logger.error("(Getting Po List By Code) Error on Function - ${e.stackTrace}") + logger.error(e.message) + null + } + } + open fun savePurchaseOrders(request: M18CommonRequest) : SyncResult{ val purchaseOrdersWithType = getPurchaseOrdersWithType(request) return savePurchaseOrdersWithPreparedList( diff --git a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt index 2138251..4c5e1d9 100644 --- a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt +++ b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt @@ -76,8 +76,15 @@ class M18TestController ( } @GetMapping("/test/po-by-code") - fun testSyncPoByCode(@RequestParam code: String): SyncResult { - return m18PurchaseOrderService.savePurchaseOrderByCode(code) + fun testSyncPoByCode( + @RequestParam code: String, + @RequestParam(required = false, defaultValue = "false") ifNewer: Boolean, + ): SyncResult { + return if (ifNewer) { + m18PurchaseOrderService.savePurchaseOrderByCodeIfNewer(code) + } else { + m18PurchaseOrderService.savePurchaseOrderByCode(code) + } } @GetMapping("/test/do-by-code") diff --git a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java index a80a077..00a13e2 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java +++ b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java @@ -52,6 +52,15 @@ public abstract class SettingNames { */ public static final String M18_BOM_SHOP_SYNC_ENABLED = "M18.bom.shop.sync.enabled"; + /** + * When "true", stock-in {@code /po} lookup for an existing pending PO asks M18 whether + * {@code lastModifyDate} is after {@code purchase_order.modified} and re-syncs if newer. + * Read from DB on each request (no server restart). Missing POs still auto-sync. + */ + public static final String M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED = + "M18.po.stockIn.refreshIfNewer.enabled"; + + /** Post completed DN and process M18 GRN (cron, e.g. "0 40 23 * * *" for 23:40 daily) */ public static final String SCHEDULE_POST_COMPLETED_DN_GRN = "SCHEDULE.postCompletedDn.grn"; diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderRepository.kt index 707e8c2..6ad5424 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderRepository.kt @@ -16,6 +16,8 @@ import org.springframework.data.repository.query.Param interface PurchaseOrderRepository : AbstractRepository { fun findTopByM18DataLogIdAndDeletedIsFalseOrderByModifiedDesc(m18datalogId: Serializable): PurchaseOrder? + fun findTopByCodeAndDeletedIsFalseOrderByModifiedDesc(code: String): PurchaseOrder? + fun findPurchaseOrderInfoByIdAndDeletedIsFalse(id: Long): PurchaseOrderInfo fun findByIdAndDeletedFalse(id: Long): Optional diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt index d794d58..9474f07 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt @@ -360,6 +360,10 @@ open fun getPoSummariesByIds(ids: List): List { return purchaseOrderRepository.findTopByM18DataLogIdAndDeletedIsFalseOrderByModifiedDesc(m18DataLogId) } + open fun findByCode(code: String): PurchaseOrder? { + return purchaseOrderRepository.findTopByCodeAndDeletedIsFalseOrderByModifiedDesc(code) + } + @Throws(IOException::class) @Transactional open fun savePurchaseOrder(request: SavePurchaseOrderRequest): SavePurchaseOrderResponse { diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt index dbb798d..812b234 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt @@ -32,11 +32,7 @@ open class M18BomShopSyncReportService( ): Map { val start = parseDateStart(syncDateStart) val end = parseDateEnd(syncDateEnd) - val itemTokens = finishedItemCode - ?.split(",") - ?.map { it.trim() } - ?.filter { it.isNotEmpty() } - .orEmpty() + val itemTokens = ReportMultiValueTokens.split(finishedItemCode) val sqlItemFilter = itemTokens.singleOrNull() val statusFilter = syncStatus?.trim()?.lowercase()?.takeIf { it.isNotEmpty() && it != "all" } diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportMultiValueTokens.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportMultiValueTokens.kt index 9e54bd3..e14075e 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportMultiValueTokens.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportMultiValueTokens.kt @@ -26,4 +26,16 @@ object ReportMultiValueTokens { val fuzzy = tokens.filter { token -> exact.none { it.equals(token, ignoreCase = true) } } return exact to fuzzy } + + fun combine(itemCode: String?, itemCodePaste: String?, itemCodes: List?): String? { + val joined = buildString { + if (!itemCode.isNullOrBlank()) append(itemCode).append(' ') + if (!itemCodePaste.isNullOrBlank()) append(itemCodePaste).append(' ') + itemCodes?.forEach { code -> + if (code.isNotBlank()) append(code).append(' ') + } + } + val tokens = split(joined) + return tokens.takeIf { it.isNotEmpty() }?.joinToString(",") + } } diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt index aa854b5..b244c0b 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt @@ -99,7 +99,7 @@ class StockItemConsumptionTrendReportService( // itemCode now contains prefixes (first 2 letters), so we use LIKE to match codes starting with those prefixes val itemCodeSql = if (!itemCode.isNullOrBlank()) { - val prefixes = itemCode.split(",").map { it.trim() }.filter { it.isNotBlank() } + val prefixes = ReportMultiValueTokens.split(itemCode) if (prefixes.isNotEmpty()) { val conditions = prefixes.mapIndexed { index, prefix -> val paramName = "itemCode_$index" diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt index 55c546b..ea39625 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt @@ -64,7 +64,7 @@ open class StockLedgerReportService( args ) - val itemCodeSql = buildMultiValueLikeClause( + val itemCodeSql = buildItemCodeFilterClause( itemCode, "sl.itemCode", "itemCode", @@ -278,23 +278,28 @@ ORDER BY return jdbcDao.queryForList(sql, args) } - /** LIKE 多值工具方法 */ - private fun buildMultiValueLikeClause( + /** Full codes use equality; short tokens use prefix LIKE. */ + private fun buildItemCodeFilterClause( paramValue: String?, columnName: String, paramPrefix: String, args: MutableMap ): String { - if (paramValue.isNullOrBlank()) return "" - val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() } - if (values.isEmpty()) return "" + val (exact, fuzzy) = ReportMultiValueTokens.partitionExactAndFuzzy(paramValue) + if (exact.isEmpty() && fuzzy.isEmpty()) return "" - val conditions = values.mapIndexed { index, value -> - val paramName = "${paramPrefix}_$index" - args[paramName] = "%$value%" - "$columnName LIKE :$paramName" + val parts = mutableListOf() + exact.forEachIndexed { index, value -> + val paramName = "${paramPrefix}_eq_$index" + args[paramName] = value + parts.add("$columnName = :$paramName") } - return "AND (${conditions.joinToString(" OR ")})" + fuzzy.forEachIndexed { index, value -> + val paramName = "${paramPrefix}_like_$index" + args[paramName] = if ('%' in value || '_' in value) value else "$value%" + parts.add("$columnName LIKE :$paramName") + } + return "AND (${parts.joinToString(" OR ")})" } /** = 多值工具方法 */ diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt index 17834af..d43dde9 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt @@ -7,6 +7,8 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -45,6 +47,35 @@ class MaterialStockOutTraceabilityReportController( @RequestParam(required = false) lastOutDateStart: String?, @RequestParam(required = false) lastOutDateEnd: String?, @RequestParam(required = false) handler: String?, + ): ResponseEntity = + buildMaterialStockOutTraceabilityPdf( + stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd, handler, + ) + + @PostMapping("/print-material-stock-out-traceability") + fun generateMaterialStockOutTraceabilityReportPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildMaterialStockOutTraceabilityPdf( + r.stockCategory, + r.stockSubCategory, + r.combinedItemCode(), + r.year, + r.lastOutDateStart, + r.lastOutDateEnd, + r.handler, + ) + } + + private fun buildMaterialStockOutTraceabilityPdf( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, + handler: String?, ): ResponseEntity { val parameters = mutableMapOf() @@ -94,6 +125,35 @@ class MaterialStockOutTraceabilityReportController( @RequestParam(required = false) lastOutDateStart: String?, @RequestParam(required = false) lastOutDateEnd: String?, @RequestParam(required = false) handler: String?, + ): ResponseEntity = + buildMaterialStockOutTraceabilityExcel( + stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd, handler, + ) + + @PostMapping("/print-material-stock-out-traceability-excel") + fun exportMaterialStockOutTraceabilityReportExcelPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildMaterialStockOutTraceabilityExcel( + r.stockCategory, + r.stockSubCategory, + r.combinedItemCode(), + r.year, + r.lastOutDateStart, + r.lastOutDateEnd, + r.handler, + ) + } + + private fun buildMaterialStockOutTraceabilityExcel( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, + handler: String?, ): ResponseEntity { val parameters = mutableMapOf() 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 7fba474..816098b 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 @@ -123,6 +123,27 @@ class ReportController( @RequestParam(required = false) itemCode: String?, @RequestParam(required = false) lastInDateStart: String?, @RequestParam(required = false) lastInDateEnd: String? + ): ResponseEntity = + buildStockInTraceabilityPdf(stockCategory, itemCode, lastInDateStart, lastInDateEnd) + + @PostMapping("/print-stock-in-traceability") + fun generateStockInTraceabilityReportPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildStockInTraceabilityPdf( + r.stockCategory, + r.combinedItemCode(), + r.lastInDateStart, + r.lastInDateEnd, + ) + } + + private fun buildStockInTraceabilityPdf( + stockCategory: String?, + itemCode: String?, + lastInDateStart: String?, + lastInDateEnd: String?, ): ResponseEntity { val parameters = mutableMapOf() @@ -162,6 +183,27 @@ class ReportController( @RequestParam(required = false) itemCode: String?, @RequestParam(required = false) lastInDateStart: String?, @RequestParam(required = false) lastInDateEnd: String?, + ): ResponseEntity = + buildStockInTraceabilityExcel(stockCategory, itemCode, lastInDateStart, lastInDateEnd) + + @PostMapping("/print-stock-in-traceability-excel") + fun exportStockInTraceabilityReportExcelPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildStockInTraceabilityExcel( + r.stockCategory, + r.combinedItemCode(), + r.lastInDateStart, + r.lastInDateEnd, + ) + } + + private fun buildStockInTraceabilityExcel( + stockCategory: String?, + itemCode: String?, + lastInDateStart: String?, + lastInDateEnd: String?, ): ResponseEntity { val dbData = reportService.searchStockInTraceabilityReport( stockCategory, @@ -193,6 +235,31 @@ class ReportController( @RequestParam(required = false) year: String?, @RequestParam(required = false) lastOutDateStart: String?, @RequestParam(required = false) lastOutDateEnd: String? + ): ResponseEntity = + buildFgDeliveryPdf(stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd) + + @PostMapping("/print-fg-delivery-report") + fun generateFGDeliveryReportPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildFgDeliveryPdf( + r.stockCategory, + r.stockSubCategory, + r.combinedItemCode(), + r.year, + r.lastOutDateStart, + r.lastOutDateEnd, + ) + } + + private fun buildFgDeliveryPdf( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, ): ResponseEntity { val parameters = mutableMapOf() @@ -240,6 +307,31 @@ class ReportController( @RequestParam(required = false) year: String?, @RequestParam(required = false) lastOutDateStart: String?, @RequestParam(required = false) lastOutDateEnd: String?, + ): ResponseEntity = + buildFgDeliveryExcel(stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd) + + @PostMapping("/print-fg-delivery-report-excel") + fun exportFGDeliveryReportExcelPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildFgDeliveryExcel( + r.stockCategory, + r.stockSubCategory, + r.combinedItemCode(), + r.year, + r.lastOutDateStart, + r.lastOutDateEnd, + ) + } + + private fun buildFgDeliveryExcel( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, ): ResponseEntity { val dbData = reportService.searchFGDeliveryReport( stockCategory, @@ -366,17 +458,7 @@ class ReportController( itemCode: String?, itemCodePaste: String?, itemCodes: List?, - ): String? { - val joined = buildString { - if (!itemCode.isNullOrBlank()) append(itemCode).append(' ') - if (!itemCodePaste.isNullOrBlank()) append(itemCodePaste).append(' ') - itemCodes?.forEach { code -> - if (code.isNotBlank()) append(code).append(' ') - } - } - val tokens = ReportMultiValueTokens.split(joined) - return tokens.takeIf { it.isNotEmpty() }?.joinToString(",") - } + ): String? = ReportMultiValueTokens.combine(itemCode, itemCodePaste, itemCodes) private fun loadStockBalanceRows( stockCategory: String?, @@ -1075,6 +1157,31 @@ class ReportController( @RequestParam(required = false) supplier: String?, @RequestParam(required = false) poCode: String?, @RequestParam(required = false) grnCode: String?, + ): Map = + buildGrnReport(receiptDateStart, receiptDateEnd, itemCode, supplier, poCode, grnCode) + + @PostMapping("/grn-report") + fun getGrnReportPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): Map { + val r = req ?: ReportItemFilterRequest() + return buildGrnReport( + r.receiptDateStart, + r.receiptDateEnd, + r.combinedItemCode(), + r.supplier, + r.poCode, + r.grnCode, + ) + } + + private fun buildGrnReport( + receiptDateStart: String?, + receiptDateEnd: String?, + itemCode: String?, + supplier: String?, + poCode: String?, + grnCode: String?, ): Map { val rows = reportService.searchGrnReport( receiptDateStart, @@ -1135,6 +1242,19 @@ class ReportController( syncStatus = syncStatus, ) + @PostMapping("/bom-shop-sync-history") + fun getBomShopSyncHistoryReportPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): Map { + val r = req ?: ReportItemFilterRequest() + return m18BomShopSyncReportService.searchBomShopSyncHistoryReport( + syncDateStart = r.syncDateStart, + syncDateEnd = r.syncDateEnd, + finishedItemCode = r.combinedFinishedItemCode(), + syncStatus = r.syncStatus, + ) + } + /** * Shop Orders Replenishment Records (店鋪訂單補貨記錄). * JSON for Excel export: completed replenishments with linked ticket only. diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/ReportItemFilterRequest.kt b/src/main/java/com/ffii/fpsms/modules/report/web/ReportItemFilterRequest.kt new file mode 100644 index 0000000..168731a --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/web/ReportItemFilterRequest.kt @@ -0,0 +1,33 @@ +package com.ffii.fpsms.modules.report.web + +import com.ffii.fpsms.modules.report.service.ReportMultiValueTokens + +/** Shared POST body for report filters that accept pasted item codes. */ +data class ReportItemFilterRequest( + val itemCode: String? = null, + val itemCodePaste: String? = null, + val itemCodes: List? = null, + val finishedItemCode: String? = null, + val stockCategory: String? = null, + val stockSubCategory: String? = null, + val year: String? = null, + val lastInDateStart: String? = null, + val lastInDateEnd: String? = null, + val lastOutDateStart: String? = null, + val lastOutDateEnd: String? = null, + val receiptDateStart: String? = null, + val receiptDateEnd: String? = null, + val syncDateStart: String? = null, + val syncDateEnd: String? = null, + val syncStatus: String? = null, + val handler: String? = null, + val supplier: String? = null, + val poCode: String? = null, + val grnCode: String? = null, +) { + fun combinedItemCode(): String? = + ReportMultiValueTokens.combine(itemCode, itemCodePaste, itemCodes) + + fun combinedFinishedItemCode(): String? = + ReportMultiValueTokens.combine(finishedItemCode, itemCodePaste, itemCodes) +} diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt index 2362797..4182292 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt @@ -46,6 +46,33 @@ class SemiFGProductionAnalysisReportController( @RequestParam(required = false) year: String?, @RequestParam(required = false) lastOutDateStart: String?, @RequestParam(required = false) lastOutDateEnd: String? + ): ResponseEntity = + buildSemiFgProductionAnalysisPdf( + stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd, + ) + + @PostMapping("/print-semi-fg-production-analysis") + fun generateSemiFGProductionAnalysisReportPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildSemiFgProductionAnalysisPdf( + r.stockCategory, + r.stockSubCategory, + r.combinedItemCode(), + r.year, + r.lastOutDateStart, + r.lastOutDateEnd, + ) + } + + private fun buildSemiFgProductionAnalysisPdf( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, ): ResponseEntity { val parameters = mutableMapOf() @@ -94,6 +121,33 @@ class SemiFGProductionAnalysisReportController( @RequestParam(required = false) year: String?, @RequestParam(required = false) lastOutDateStart: String?, @RequestParam(required = false) lastOutDateEnd: String? + ): ResponseEntity = + buildSemiFgProductionAnalysisExcel( + stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd, + ) + + @PostMapping("/print-semi-fg-production-analysis-excel") + fun exportSemiFGProductionAnalysisReportExcelPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildSemiFgProductionAnalysisExcel( + r.stockCategory, + r.stockSubCategory, + r.combinedItemCode(), + r.year, + r.lastOutDateStart, + r.lastOutDateEnd, + ) + } + + private fun buildSemiFgProductionAnalysisExcel( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, ): ResponseEntity { val dbData = semiFGProductionAnalysisReportService.searchSemiFGProductionAnalysisReport( stockCategory, diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt index 97a5abd..7572bd5 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt @@ -45,6 +45,29 @@ class StockItemConsumptionTrendReportController( @RequestParam(required = false) year: String?, @RequestParam(required = false) lastOutDateStart: String?, @RequestParam(required = false) lastOutDateEnd: String? + ): ResponseEntity = + buildStockItemConsumptionTrendPdf(stockCategory, itemCode, year, lastOutDateStart, lastOutDateEnd) + + @PostMapping("/print-stock-item-consumption-trend") + fun generateStockItemConsumptionTrendReportPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildStockItemConsumptionTrendPdf( + r.stockCategory, + r.combinedItemCode(), + r.year, + r.lastOutDateStart, + r.lastOutDateEnd, + ) + } + + private fun buildStockItemConsumptionTrendPdf( + stockCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, ): ResponseEntity { try { val parameters = mutableMapOf() @@ -98,6 +121,29 @@ class StockItemConsumptionTrendReportController( @RequestParam(required = false) year: String?, @RequestParam(required = false) lastOutDateStart: String?, @RequestParam(required = false) lastOutDateEnd: String? + ): ResponseEntity = + buildStockItemConsumptionTrendExcel(stockCategory, itemCode, year, lastOutDateStart, lastOutDateEnd) + + @PostMapping("/print-stock-item-consumption-trend-excel") + fun exportStockItemConsumptionTrendReportExcelPost( + @RequestBody(required = false) req: ReportItemFilterRequest?, + ): ResponseEntity { + val r = req ?: ReportItemFilterRequest() + return buildStockItemConsumptionTrendExcel( + r.stockCategory, + r.combinedItemCode(), + r.year, + r.lastOutDateStart, + r.lastOutDateEnd, + ) + } + + private fun buildStockItemConsumptionTrendExcel( + stockCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, ): ResponseEntity { val dbData = stockItemConsumptionTrendReportService.searchStockItemConsumptionTrendReport( stockCategory = stockCategory, diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/StockLedgerReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/StockLedgerReportController.kt index 5345032..7fa4fb4 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/StockLedgerReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/StockLedgerReportController.kt @@ -4,6 +4,7 @@ import org.springframework.http.* import java.time.LocalDate import java.time.LocalTime import java.time.format.DateTimeFormatter +import com.ffii.fpsms.modules.report.service.ReportMultiValueTokens import com.ffii.fpsms.modules.report.service.StockLedgerReportService import com.ffii.fpsms.modules.report.service.ReportService import org.apache.poi.ss.usermodel.BorderStyle @@ -48,12 +49,66 @@ class StockLedgerReportController( @RequestParam(required = false) storeLocation: String?, @RequestParam(name = "lastInDateStart", required = false) reportPeriodStart: String?, @RequestParam(name = "lastInDateEnd", required = false) reportPeriodEnd: String?, + ): ResponseEntity = + generateStockLedgerPdf(stockCategory, itemCode, storeLocation, reportPeriodStart, reportPeriodEnd) + + @PostMapping("/print-stock-ledger") + fun generateStockLedgerReportPost( + @RequestBody(required = false) req: StockLedgerReportRequest?, + ): ResponseEntity { + val r = req ?: StockLedgerReportRequest() + return generateStockLedgerPdf( + stockCategory = r.stockCategory, + itemCode = ReportMultiValueTokens.combine(r.itemCode, r.itemCodePaste, r.itemCodes), + storeLocation = r.storeLocation, + reportPeriodStart = r.lastInDateStart, + reportPeriodEnd = r.lastInDateEnd, + ) + } + + /** FP-MTMS Version Checklist | Functions Ref. No. 65 | v1.0.0 | 2026-08-11 */ + @GetMapping("/print-stock-ledger-excel") + fun exportStockLedgerReportExcel( + @RequestParam(required = false) stockCategory: String?, + @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) storeLocation: String?, + @RequestParam(name = "lastInDateStart", required = false) reportPeriodStart: String?, + @RequestParam(name = "lastInDateEnd", required = false) reportPeriodEnd: String?, + ): ResponseEntity = + generateStockLedgerExcel(stockCategory, itemCode, storeLocation, reportPeriodStart, reportPeriodEnd) + + @PostMapping("/print-stock-ledger-excel") + fun exportStockLedgerReportExcelPost( + @RequestBody(required = false) req: StockLedgerReportRequest?, ): ResponseEntity { + val r = req ?: StockLedgerReportRequest() + return generateStockLedgerExcel( + stockCategory = r.stockCategory, + itemCode = ReportMultiValueTokens.combine(r.itemCode, r.itemCodePaste, r.itemCodes), + storeLocation = r.storeLocation, + reportPeriodStart = r.lastInDateStart, + reportPeriodEnd = r.lastInDateEnd, + ) + } + + private fun generateStockLedgerPdf( + stockCategory: String?, + itemCode: String?, + storeLocation: String?, + reportPeriodStart: String?, + reportPeriodEnd: String?, + ): ResponseEntity { + val tokenCount = ReportMultiValueTokens.split(itemCode).size val parameters = mutableMapOf() parameters["stockCategory"] = stockCategory ?: "All" parameters["stockSubCategory"] = stockCategory ?: "All" - parameters["itemNo"] = itemCode ?: "All" + parameters["itemNo"] = + when { + itemCode.isNullOrBlank() -> "All" + tokenCount > 30 -> "$tokenCount items" + else -> itemCode + } parameters["year"] = LocalDate.now().year.toString() parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) @@ -86,14 +141,12 @@ class StockLedgerReportController( return ResponseEntity(pdfBytes, headers, HttpStatus.OK) } - /** FP-MTMS Version Checklist | Functions Ref. No. 65 | v1.0.0 | 2026-08-11 */ - @GetMapping("/print-stock-ledger-excel") - fun exportStockLedgerReportExcel( - @RequestParam(required = false) stockCategory: String?, - @RequestParam(required = false) itemCode: String?, - @RequestParam(required = false) storeLocation: String?, - @RequestParam(name = "lastInDateStart", required = false) reportPeriodStart: String?, - @RequestParam(name = "lastInDateEnd", required = false) reportPeriodEnd: String?, + private fun generateStockLedgerExcel( + stockCategory: String?, + itemCode: String?, + storeLocation: String?, + reportPeriodStart: String?, + reportPeriodEnd: String?, ): ResponseEntity { val dbData = stockLedgerReportService.searchStockLedgerReport( stockCategory = stockCategory, @@ -436,3 +489,13 @@ class StockLedgerReportController( } } } + +data class StockLedgerReportRequest( + val lastInDateStart: String? = null, + val lastInDateEnd: String? = null, + val itemCode: String? = null, + val itemCodePaste: String? = null, + val itemCodes: List? = null, + val stockCategory: String? = null, + val storeLocation: String? = null, +) diff --git a/src/main/resources/db/changelog/changes/20260918_po_stockin_refresh_if_newer/01_setting.sql b/src/main/resources/db/changelog/changes/20260918_po_stockin_refresh_if_newer/01_setting.sql new file mode 100644 index 0000000..77303c7 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260918_po_stockin_refresh_if_newer/01_setting.sql @@ -0,0 +1,14 @@ +--liquibase formatted sql +--changeset fai:20260918_po_stockin_refresh_if_newer_setting + +INSERT INTO `settings` (`name`, `value`, `category`, `type`) +SELECT v.name, v.value, v.category, v.type +FROM ( + SELECT 'M18.po.stockIn.refreshIfNewer.enabled' AS name, + 'false' AS value, + 'M18' AS category, + 'boolean' AS type +) v +WHERE NOT EXISTS ( + SELECT 1 FROM `settings` s WHERE s.name = 'M18.po.stockIn.refreshIfNewer.enabled' +); diff --git a/src/test/kotlin/com/ffii/fpsms/modules/report/service/ReportMultiValueTokensTest.kt b/src/test/kotlin/com/ffii/fpsms/modules/report/service/ReportMultiValueTokensTest.kt index a24dd94..b796ab4 100644 --- a/src/test/kotlin/com/ffii/fpsms/modules/report/service/ReportMultiValueTokensTest.kt +++ b/src/test/kotlin/com/ffii/fpsms/modules/report/service/ReportMultiValueTokensTest.kt @@ -48,4 +48,12 @@ class ReportMultiValueTokensTest { assertEquals(listOf("FA0123"), exact) assertEquals(listOf("FA"), fuzzy) } + + @Test + fun `combine merges select paste and json list`() { + assertEquals( + "FA0123,FA2332,FA9999", + ReportMultiValueTokens.combine("FA0123", "FA2332 FA0123", listOf("FA9999")), + ) + } }