| @@ -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.PurchaseOrderLineStatus | ||||
| import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderStatus | import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderStatus | ||||
| import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderType | 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.PurchaseOrderLineService | ||||
| import com.ffii.fpsms.modules.purchaseOrder.service.PurchaseOrderService | 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.SavePurchaseOrderLineRequest | ||||
| import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderRequest | 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.Logger | ||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| @@ -39,13 +42,15 @@ open class M18PurchaseOrderService( | |||||
| val itemsService: ItemsService, | val itemsService: ItemsService, | ||||
| val shopService: ShopService, | val shopService: ShopService, | ||||
| val itemUomService: ItemUomService, | val itemUomService: ItemUomService, | ||||
| val m18MasterDataService: M18MasterDataService | |||||
| val m18MasterDataService: M18MasterDataService, | |||||
| val settingsService: SettingsService, | |||||
| ) { | ) { | ||||
| val commonUtils = CommonUtils() | val commonUtils = CommonUtils() | ||||
| val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) | val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) | ||||
| val lastModifyDateStart = "2025-05-14 14:00:00" | val lastModifyDateStart = "2025-05-14 14:00:00" | ||||
| val lastModifyDateEnd = "2025-05-14 14:30: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 = | // val lastModifyDateConds = | ||||
| // "lastModifyDate=largerOrEqual=${lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${lastModifyDateEnd}" | // "lastModifyDate=largerOrEqual=${lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${lastModifyDateEnd}" | ||||
| // val lastModifyDate = LocalDateTime.now().minusMinutes(30) | // val lastModifyDate = LocalDateTime.now().minusMinutes(30) | ||||
| @@ -227,40 +232,129 @@ open class M18PurchaseOrderService( | |||||
| // Follow the scheduler's original approach: | // Follow the scheduler's original approach: | ||||
| // 1) use /search/search to find the PO id by code | // 1) use /search/search to find the PO id by code | ||||
| // 2) then sync by PO id (this reuses the same create/update logic) | // 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<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>( | |||||
| 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 | val poValues = poListResponse?.values | ||||
| if (poValues.isNullOrEmpty()) { | if (poValues.isNullOrEmpty()) { | ||||
| return SyncResult( | return SyncResult( | ||||
| totalProcessed = 1, | totalProcessed = 1, | ||||
| totalSuccess = 0, | totalSuccess = 0, | ||||
| totalFail = 1, | totalFail = 1, | ||||
| query = "code=equal=$code" | |||||
| query = query | |||||
| ) | ) | ||||
| } | } | ||||
| val request = M18PurchaseOrderListResponseWithType( | val request = M18PurchaseOrderListResponseWithType( | ||||
| valuesWithType = mutableListOf(Pair(PurchaseOrderType.MATERIAL, poListResponse)), | valuesWithType = mutableListOf(Pair(PurchaseOrderType.MATERIAL, poListResponse)), | ||||
| query = "code=equal=$code" | |||||
| query = query | |||||
| ) | ) | ||||
| return savePurchaseOrdersWithPreparedList(purchaseOrdersWithType = request) | 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<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>( | |||||
| 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{ | open fun savePurchaseOrders(request: M18CommonRequest) : SyncResult{ | ||||
| val purchaseOrdersWithType = getPurchaseOrdersWithType(request) | val purchaseOrdersWithType = getPurchaseOrdersWithType(request) | ||||
| return savePurchaseOrdersWithPreparedList( | return savePurchaseOrdersWithPreparedList( | ||||
| @@ -76,8 +76,15 @@ class M18TestController ( | |||||
| } | } | ||||
| @GetMapping("/test/po-by-code") | @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") | @GetMapping("/test/do-by-code") | ||||
| @@ -52,6 +52,15 @@ public abstract class SettingNames { | |||||
| */ | */ | ||||
| public static final String M18_BOM_SHOP_SYNC_ENABLED = "M18.bom.shop.sync.enabled"; | 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) */ | /** 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"; | public static final String SCHEDULE_POST_COMPLETED_DN_GRN = "SCHEDULE.postCompletedDn.grn"; | ||||
| @@ -16,6 +16,8 @@ import org.springframework.data.repository.query.Param | |||||
| interface PurchaseOrderRepository : AbstractRepository<PurchaseOrder, Long> { | interface PurchaseOrderRepository : AbstractRepository<PurchaseOrder, Long> { | ||||
| fun findTopByM18DataLogIdAndDeletedIsFalseOrderByModifiedDesc(m18datalogId: Serializable): PurchaseOrder? | fun findTopByM18DataLogIdAndDeletedIsFalseOrderByModifiedDesc(m18datalogId: Serializable): PurchaseOrder? | ||||
| fun findTopByCodeAndDeletedIsFalseOrderByModifiedDesc(code: String): PurchaseOrder? | |||||
| fun findPurchaseOrderInfoByIdAndDeletedIsFalse(id: Long): PurchaseOrderInfo | fun findPurchaseOrderInfoByIdAndDeletedIsFalse(id: Long): PurchaseOrderInfo | ||||
| fun findByIdAndDeletedFalse(id: Long): Optional<PurchaseOrder> | fun findByIdAndDeletedFalse(id: Long): Optional<PurchaseOrder> | ||||
| @@ -360,6 +360,10 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> { | |||||
| return purchaseOrderRepository.findTopByM18DataLogIdAndDeletedIsFalseOrderByModifiedDesc(m18DataLogId) | return purchaseOrderRepository.findTopByM18DataLogIdAndDeletedIsFalseOrderByModifiedDesc(m18DataLogId) | ||||
| } | } | ||||
| open fun findByCode(code: String): PurchaseOrder? { | |||||
| return purchaseOrderRepository.findTopByCodeAndDeletedIsFalseOrderByModifiedDesc(code) | |||||
| } | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| @Transactional | @Transactional | ||||
| open fun savePurchaseOrder(request: SavePurchaseOrderRequest): SavePurchaseOrderResponse { | open fun savePurchaseOrder(request: SavePurchaseOrderRequest): SavePurchaseOrderResponse { | ||||
| @@ -32,11 +32,7 @@ open class M18BomShopSyncReportService( | |||||
| ): Map<String, Any> { | ): Map<String, Any> { | ||||
| val start = parseDateStart(syncDateStart) | val start = parseDateStart(syncDateStart) | ||||
| val end = parseDateEnd(syncDateEnd) | 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 sqlItemFilter = itemTokens.singleOrNull() | ||||
| val statusFilter = syncStatus?.trim()?.lowercase()?.takeIf { it.isNotEmpty() && it != "all" } | val statusFilter = syncStatus?.trim()?.lowercase()?.takeIf { it.isNotEmpty() && it != "all" } | ||||
| @@ -26,4 +26,16 @@ object ReportMultiValueTokens { | |||||
| val fuzzy = tokens.filter { token -> exact.none { it.equals(token, ignoreCase = true) } } | val fuzzy = tokens.filter { token -> exact.none { it.equals(token, ignoreCase = true) } } | ||||
| return exact to fuzzy | return exact to fuzzy | ||||
| } | } | ||||
| fun combine(itemCode: String?, itemCodePaste: String?, itemCodes: List<String>?): 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(",") | |||||
| } | |||||
| } | } | ||||
| @@ -99,7 +99,7 @@ class StockItemConsumptionTrendReportService( | |||||
| // itemCode now contains prefixes (first 2 letters), so we use LIKE to match codes starting with those prefixes | // itemCode now contains prefixes (first 2 letters), so we use LIKE to match codes starting with those prefixes | ||||
| val itemCodeSql = if (!itemCode.isNullOrBlank()) { | val itemCodeSql = if (!itemCode.isNullOrBlank()) { | ||||
| val prefixes = itemCode.split(",").map { it.trim() }.filter { it.isNotBlank() } | |||||
| val prefixes = ReportMultiValueTokens.split(itemCode) | |||||
| if (prefixes.isNotEmpty()) { | if (prefixes.isNotEmpty()) { | ||||
| val conditions = prefixes.mapIndexed { index, prefix -> | val conditions = prefixes.mapIndexed { index, prefix -> | ||||
| val paramName = "itemCode_$index" | val paramName = "itemCode_$index" | ||||
| @@ -64,7 +64,7 @@ open class StockLedgerReportService( | |||||
| args | args | ||||
| ) | ) | ||||
| val itemCodeSql = buildMultiValueLikeClause( | |||||
| val itemCodeSql = buildItemCodeFilterClause( | |||||
| itemCode, | itemCode, | ||||
| "sl.itemCode", | "sl.itemCode", | ||||
| "itemCode", | "itemCode", | ||||
| @@ -278,23 +278,28 @@ ORDER BY | |||||
| return jdbcDao.queryForList(sql, args) | return jdbcDao.queryForList(sql, args) | ||||
| } | } | ||||
| /** LIKE 多值工具方法 */ | |||||
| private fun buildMultiValueLikeClause( | |||||
| /** Full codes use equality; short tokens use prefix LIKE. */ | |||||
| private fun buildItemCodeFilterClause( | |||||
| paramValue: String?, | paramValue: String?, | ||||
| columnName: String, | columnName: String, | ||||
| paramPrefix: String, | paramPrefix: String, | ||||
| args: MutableMap<String, Any> | args: MutableMap<String, Any> | ||||
| ): String { | ): 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<String>() | |||||
| 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 ")})" | |||||
| } | } | ||||
| /** = 多值工具方法 */ | /** = 多值工具方法 */ | ||||
| @@ -7,6 +7,8 @@ import org.springframework.http.HttpStatus | |||||
| import org.springframework.http.MediaType | import org.springframework.http.MediaType | ||||
| import org.springframework.http.ResponseEntity | import org.springframework.http.ResponseEntity | ||||
| import org.springframework.web.bind.annotation.GetMapping | 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.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RequestParam | import org.springframework.web.bind.annotation.RequestParam | ||||
| import org.springframework.web.bind.annotation.RestController | import org.springframework.web.bind.annotation.RestController | ||||
| @@ -45,6 +47,35 @@ class MaterialStockOutTraceabilityReportController( | |||||
| @RequestParam(required = false) lastOutDateStart: String?, | @RequestParam(required = false) lastOutDateStart: String?, | ||||
| @RequestParam(required = false) lastOutDateEnd: String?, | @RequestParam(required = false) lastOutDateEnd: String?, | ||||
| @RequestParam(required = false) handler: String?, | @RequestParam(required = false) handler: String?, | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildMaterialStockOutTraceabilityPdf( | |||||
| stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd, handler, | |||||
| ) | |||||
| @PostMapping("/print-material-stock-out-traceability") | |||||
| fun generateMaterialStockOutTraceabilityReportPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val parameters = mutableMapOf<String, Any>() | val parameters = mutableMapOf<String, Any>() | ||||
| @@ -94,6 +125,35 @@ class MaterialStockOutTraceabilityReportController( | |||||
| @RequestParam(required = false) lastOutDateStart: String?, | @RequestParam(required = false) lastOutDateStart: String?, | ||||
| @RequestParam(required = false) lastOutDateEnd: String?, | @RequestParam(required = false) lastOutDateEnd: String?, | ||||
| @RequestParam(required = false) handler: String?, | @RequestParam(required = false) handler: String?, | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildMaterialStockOutTraceabilityExcel( | |||||
| stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd, handler, | |||||
| ) | |||||
| @PostMapping("/print-material-stock-out-traceability-excel") | |||||
| fun exportMaterialStockOutTraceabilityReportExcelPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val parameters = mutableMapOf<String, Any>() | val parameters = mutableMapOf<String, Any>() | ||||
| @@ -123,6 +123,27 @@ class ReportController( | |||||
| @RequestParam(required = false) itemCode: String?, | @RequestParam(required = false) itemCode: String?, | ||||
| @RequestParam(required = false) lastInDateStart: String?, | @RequestParam(required = false) lastInDateStart: String?, | ||||
| @RequestParam(required = false) lastInDateEnd: String? | @RequestParam(required = false) lastInDateEnd: String? | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildStockInTraceabilityPdf(stockCategory, itemCode, lastInDateStart, lastInDateEnd) | |||||
| @PostMapping("/print-stock-in-traceability") | |||||
| fun generateStockInTraceabilityReportPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val parameters = mutableMapOf<String, Any>() | val parameters = mutableMapOf<String, Any>() | ||||
| @@ -162,6 +183,27 @@ class ReportController( | |||||
| @RequestParam(required = false) itemCode: String?, | @RequestParam(required = false) itemCode: String?, | ||||
| @RequestParam(required = false) lastInDateStart: String?, | @RequestParam(required = false) lastInDateStart: String?, | ||||
| @RequestParam(required = false) lastInDateEnd: String?, | @RequestParam(required = false) lastInDateEnd: String?, | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildStockInTraceabilityExcel(stockCategory, itemCode, lastInDateStart, lastInDateEnd) | |||||
| @PostMapping("/print-stock-in-traceability-excel") | |||||
| fun exportStockInTraceabilityReportExcelPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val dbData = reportService.searchStockInTraceabilityReport( | val dbData = reportService.searchStockInTraceabilityReport( | ||||
| stockCategory, | stockCategory, | ||||
| @@ -193,6 +235,31 @@ class ReportController( | |||||
| @RequestParam(required = false) year: String?, | @RequestParam(required = false) year: String?, | ||||
| @RequestParam(required = false) lastOutDateStart: String?, | @RequestParam(required = false) lastOutDateStart: String?, | ||||
| @RequestParam(required = false) lastOutDateEnd: String? | @RequestParam(required = false) lastOutDateEnd: String? | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildFgDeliveryPdf(stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd) | |||||
| @PostMapping("/print-fg-delivery-report") | |||||
| fun generateFGDeliveryReportPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val parameters = mutableMapOf<String, Any>() | val parameters = mutableMapOf<String, Any>() | ||||
| @@ -240,6 +307,31 @@ class ReportController( | |||||
| @RequestParam(required = false) year: String?, | @RequestParam(required = false) year: String?, | ||||
| @RequestParam(required = false) lastOutDateStart: String?, | @RequestParam(required = false) lastOutDateStart: String?, | ||||
| @RequestParam(required = false) lastOutDateEnd: String?, | @RequestParam(required = false) lastOutDateEnd: String?, | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildFgDeliveryExcel(stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd) | |||||
| @PostMapping("/print-fg-delivery-report-excel") | |||||
| fun exportFGDeliveryReportExcelPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val dbData = reportService.searchFGDeliveryReport( | val dbData = reportService.searchFGDeliveryReport( | ||||
| stockCategory, | stockCategory, | ||||
| @@ -366,17 +458,7 @@ class ReportController( | |||||
| itemCode: String?, | itemCode: String?, | ||||
| itemCodePaste: String?, | itemCodePaste: String?, | ||||
| itemCodes: List<String>?, | itemCodes: List<String>?, | ||||
| ): 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( | private fun loadStockBalanceRows( | ||||
| stockCategory: String?, | stockCategory: String?, | ||||
| @@ -1075,6 +1157,31 @@ class ReportController( | |||||
| @RequestParam(required = false) supplier: String?, | @RequestParam(required = false) supplier: String?, | ||||
| @RequestParam(required = false) poCode: String?, | @RequestParam(required = false) poCode: String?, | ||||
| @RequestParam(required = false) grnCode: String?, | @RequestParam(required = false) grnCode: String?, | ||||
| ): Map<String, Any> = | |||||
| buildGrnReport(receiptDateStart, receiptDateEnd, itemCode, supplier, poCode, grnCode) | |||||
| @PostMapping("/grn-report") | |||||
| fun getGrnReportPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): Map<String, Any> { | |||||
| 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<String, Any> { | ): Map<String, Any> { | ||||
| val rows = reportService.searchGrnReport( | val rows = reportService.searchGrnReport( | ||||
| receiptDateStart, | receiptDateStart, | ||||
| @@ -1135,6 +1242,19 @@ class ReportController( | |||||
| syncStatus = syncStatus, | syncStatus = syncStatus, | ||||
| ) | ) | ||||
| @PostMapping("/bom-shop-sync-history") | |||||
| fun getBomShopSyncHistoryReportPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): Map<String, Any> { | |||||
| val r = req ?: ReportItemFilterRequest() | |||||
| return m18BomShopSyncReportService.searchBomShopSyncHistoryReport( | |||||
| syncDateStart = r.syncDateStart, | |||||
| syncDateEnd = r.syncDateEnd, | |||||
| finishedItemCode = r.combinedFinishedItemCode(), | |||||
| syncStatus = r.syncStatus, | |||||
| ) | |||||
| } | |||||
| /** | /** | ||||
| * Shop Orders Replenishment Records (店鋪訂單補貨記錄). | * Shop Orders Replenishment Records (店鋪訂單補貨記錄). | ||||
| * JSON for Excel export: completed replenishments with linked ticket only. | * JSON for Excel export: completed replenishments with linked ticket only. | ||||
| @@ -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<String>? = 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) | |||||
| } | |||||
| @@ -46,6 +46,33 @@ class SemiFGProductionAnalysisReportController( | |||||
| @RequestParam(required = false) year: String?, | @RequestParam(required = false) year: String?, | ||||
| @RequestParam(required = false) lastOutDateStart: String?, | @RequestParam(required = false) lastOutDateStart: String?, | ||||
| @RequestParam(required = false) lastOutDateEnd: String? | @RequestParam(required = false) lastOutDateEnd: String? | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildSemiFgProductionAnalysisPdf( | |||||
| stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd, | |||||
| ) | |||||
| @PostMapping("/print-semi-fg-production-analysis") | |||||
| fun generateSemiFGProductionAnalysisReportPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val parameters = mutableMapOf<String, Any>() | val parameters = mutableMapOf<String, Any>() | ||||
| @@ -94,6 +121,33 @@ class SemiFGProductionAnalysisReportController( | |||||
| @RequestParam(required = false) year: String?, | @RequestParam(required = false) year: String?, | ||||
| @RequestParam(required = false) lastOutDateStart: String?, | @RequestParam(required = false) lastOutDateStart: String?, | ||||
| @RequestParam(required = false) lastOutDateEnd: String? | @RequestParam(required = false) lastOutDateEnd: String? | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildSemiFgProductionAnalysisExcel( | |||||
| stockCategory, stockSubCategory, itemCode, year, lastOutDateStart, lastOutDateEnd, | |||||
| ) | |||||
| @PostMapping("/print-semi-fg-production-analysis-excel") | |||||
| fun exportSemiFGProductionAnalysisReportExcelPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val dbData = semiFGProductionAnalysisReportService.searchSemiFGProductionAnalysisReport( | val dbData = semiFGProductionAnalysisReportService.searchSemiFGProductionAnalysisReport( | ||||
| stockCategory, | stockCategory, | ||||
| @@ -45,6 +45,29 @@ class StockItemConsumptionTrendReportController( | |||||
| @RequestParam(required = false) year: String?, | @RequestParam(required = false) year: String?, | ||||
| @RequestParam(required = false) lastOutDateStart: String?, | @RequestParam(required = false) lastOutDateStart: String?, | ||||
| @RequestParam(required = false) lastOutDateEnd: String? | @RequestParam(required = false) lastOutDateEnd: String? | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildStockItemConsumptionTrendPdf(stockCategory, itemCode, year, lastOutDateStart, lastOutDateEnd) | |||||
| @PostMapping("/print-stock-item-consumption-trend") | |||||
| fun generateStockItemConsumptionTrendReportPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| try { | try { | ||||
| val parameters = mutableMapOf<String, Any>() | val parameters = mutableMapOf<String, Any>() | ||||
| @@ -98,6 +121,29 @@ class StockItemConsumptionTrendReportController( | |||||
| @RequestParam(required = false) year: String?, | @RequestParam(required = false) year: String?, | ||||
| @RequestParam(required = false) lastOutDateStart: String?, | @RequestParam(required = false) lastOutDateStart: String?, | ||||
| @RequestParam(required = false) lastOutDateEnd: String? | @RequestParam(required = false) lastOutDateEnd: String? | ||||
| ): ResponseEntity<ByteArray> = | |||||
| buildStockItemConsumptionTrendExcel(stockCategory, itemCode, year, lastOutDateStart, lastOutDateEnd) | |||||
| @PostMapping("/print-stock-item-consumption-trend-excel") | |||||
| fun exportStockItemConsumptionTrendReportExcelPost( | |||||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val dbData = stockItemConsumptionTrendReportService.searchStockItemConsumptionTrendReport( | val dbData = stockItemConsumptionTrendReportService.searchStockItemConsumptionTrendReport( | ||||
| stockCategory = stockCategory, | stockCategory = stockCategory, | ||||
| @@ -4,6 +4,7 @@ import org.springframework.http.* | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.LocalTime | import java.time.LocalTime | ||||
| import java.time.format.DateTimeFormatter | 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.StockLedgerReportService | ||||
| import com.ffii.fpsms.modules.report.service.ReportService | import com.ffii.fpsms.modules.report.service.ReportService | ||||
| import org.apache.poi.ss.usermodel.BorderStyle | import org.apache.poi.ss.usermodel.BorderStyle | ||||
| @@ -48,12 +49,66 @@ class StockLedgerReportController( | |||||
| @RequestParam(required = false) storeLocation: String?, | @RequestParam(required = false) storeLocation: String?, | ||||
| @RequestParam(name = "lastInDateStart", required = false) reportPeriodStart: String?, | @RequestParam(name = "lastInDateStart", required = false) reportPeriodStart: String?, | ||||
| @RequestParam(name = "lastInDateEnd", required = false) reportPeriodEnd: String?, | @RequestParam(name = "lastInDateEnd", required = false) reportPeriodEnd: String?, | ||||
| ): ResponseEntity<ByteArray> = | |||||
| generateStockLedgerPdf(stockCategory, itemCode, storeLocation, reportPeriodStart, reportPeriodEnd) | |||||
| @PostMapping("/print-stock-ledger") | |||||
| fun generateStockLedgerReportPost( | |||||
| @RequestBody(required = false) req: StockLedgerReportRequest?, | |||||
| ): ResponseEntity<ByteArray> { | |||||
| 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<ByteArray> = | |||||
| generateStockLedgerExcel(stockCategory, itemCode, storeLocation, reportPeriodStart, reportPeriodEnd) | |||||
| @PostMapping("/print-stock-ledger-excel") | |||||
| fun exportStockLedgerReportExcelPost( | |||||
| @RequestBody(required = false) req: StockLedgerReportRequest?, | |||||
| ): ResponseEntity<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| 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<ByteArray> { | |||||
| val tokenCount = ReportMultiValueTokens.split(itemCode).size | |||||
| val parameters = mutableMapOf<String, Any>() | val parameters = mutableMapOf<String, Any>() | ||||
| parameters["stockCategory"] = stockCategory ?: "All" | parameters["stockCategory"] = stockCategory ?: "All" | ||||
| parameters["stockSubCategory"] = 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["year"] = LocalDate.now().year.toString() | ||||
| parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) | parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) | ||||
| parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) | parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) | ||||
| @@ -86,14 +141,12 @@ class StockLedgerReportController( | |||||
| return ResponseEntity(pdfBytes, headers, HttpStatus.OK) | 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<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val dbData = stockLedgerReportService.searchStockLedgerReport( | val dbData = stockLedgerReportService.searchStockLedgerReport( | ||||
| stockCategory = stockCategory, | 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<String>? = null, | |||||
| val stockCategory: String? = null, | |||||
| val storeLocation: String? = null, | |||||
| ) | |||||
| @@ -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' | |||||
| ); | |||||
| @@ -48,4 +48,12 @@ class ReportMultiValueTokensTest { | |||||
| assertEquals(listOf("FA0123"), exact) | assertEquals(listOf("FA0123"), exact) | ||||
| assertEquals(listOf("FA"), fuzzy) | assertEquals(listOf("FA"), fuzzy) | ||||
| } | } | ||||
| @Test | |||||
| fun `combine merges select paste and json list`() { | |||||
| assertEquals( | |||||
| "FA0123,FA2332,FA9999", | |||||
| ReportMultiValueTokens.combine("FA0123", "FA2332 FA0123", listOf("FA9999")), | |||||
| ) | |||||
| } | |||||
| } | } | ||||