浏览代码

added paste item code to some reports

production
Fai Luk 2 天前
父节点
当前提交
876f3477ad
共有 17 个文件被更改,包括 585 次插入58 次删除
  1. +113
    -19
      src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt
  2. +9
    -2
      src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt
  3. +9
    -0
      src/main/java/com/ffii/fpsms/modules/common/SettingNames.java
  4. +2
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderRepository.kt
  5. +4
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt
  6. +1
    -5
      src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt
  7. +12
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/ReportMultiValueTokens.kt
  8. +1
    -1
      src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt
  9. +16
    -11
      src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt
  10. +60
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt
  11. +131
    -11
      src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt
  12. +33
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/ReportItemFilterRequest.kt
  13. +54
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt
  14. +46
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt
  15. +72
    -9
      src/main/java/com/ffii/fpsms/modules/report/web/StockLedgerReportController.kt
  16. +14
    -0
      src/main/resources/db/changelog/changes/20260918_po_stockin_refresh_if_newer/01_setting.sql
  17. +8
    -0
      src/test/kotlin/com/ffii/fpsms/modules/report/service/ReportMultiValueTokensTest.kt

+ 113
- 19
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<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
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<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{
val purchaseOrdersWithType = getPurchaseOrdersWithType(request)
return savePurchaseOrdersWithPreparedList(


+ 9
- 2
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")


+ 9
- 0
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";



+ 2
- 0
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<PurchaseOrder, Long> {
fun findTopByM18DataLogIdAndDeletedIsFalseOrderByModifiedDesc(m18datalogId: Serializable): PurchaseOrder?

fun findTopByCodeAndDeletedIsFalseOrderByModifiedDesc(code: String): PurchaseOrder?

fun findPurchaseOrderInfoByIdAndDeletedIsFalse(id: Long): PurchaseOrderInfo

fun findByIdAndDeletedFalse(id: Long): Optional<PurchaseOrder>


+ 4
- 0
src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt 查看文件

@@ -360,6 +360,10 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> {
return purchaseOrderRepository.findTopByM18DataLogIdAndDeletedIsFalseOrderByModifiedDesc(m18DataLogId)
}

open fun findByCode(code: String): PurchaseOrder? {
return purchaseOrderRepository.findTopByCodeAndDeletedIsFalseOrderByModifiedDesc(code)
}

@Throws(IOException::class)
@Transactional
open fun savePurchaseOrder(request: SavePurchaseOrderRequest): SavePurchaseOrderResponse {


+ 1
- 5
src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt 查看文件

@@ -32,11 +32,7 @@ open class M18BomShopSyncReportService(
): Map<String, Any> {
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" }



+ 12
- 0
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>?): 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(",")
}
}

+ 1
- 1
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"


+ 16
- 11
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, Any>
): 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 ")})"
}

/** = 多值工具方法 */


+ 60
- 0
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<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> {
val parameters = mutableMapOf<String, Any>()

@@ -94,6 +125,35 @@ class MaterialStockOutTraceabilityReportController(
@RequestParam(required = false) lastOutDateStart: String?,
@RequestParam(required = false) lastOutDateEnd: 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> {
val parameters = mutableMapOf<String, Any>()



+ 131
- 11
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<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> {
val parameters = mutableMapOf<String, Any>()
@@ -162,6 +183,27 @@ class ReportController(
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false) lastInDateStart: 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> {
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<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> {
val parameters = mutableMapOf<String, Any>()
@@ -240,6 +307,31 @@ class ReportController(
@RequestParam(required = false) year: String?,
@RequestParam(required = false) lastOutDateStart: 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> {
val dbData = reportService.searchFGDeliveryReport(
stockCategory,
@@ -366,17 +458,7 @@ class ReportController(
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 = 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<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> {
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<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 (店鋪訂單補貨記錄).
* JSON for Excel export: completed replenishments with linked ticket only.


+ 33
- 0
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<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)
}

+ 54
- 0
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<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> {
val parameters = mutableMapOf<String, Any>()
@@ -94,6 +121,33 @@ class SemiFGProductionAnalysisReportController(
@RequestParam(required = false) year: String?,
@RequestParam(required = false) lastOutDateStart: 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> {
val dbData = semiFGProductionAnalysisReportService.searchSemiFGProductionAnalysisReport(
stockCategory,


+ 46
- 0
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<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> {
try {
val parameters = mutableMapOf<String, Any>()
@@ -98,6 +121,29 @@ class StockItemConsumptionTrendReportController(
@RequestParam(required = false) year: String?,
@RequestParam(required = false) lastOutDateStart: 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> {
val dbData = stockItemConsumptionTrendReportService.searchStockItemConsumptionTrendReport(
stockCategory = stockCategory,


+ 72
- 9
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<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> {
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>()

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<ByteArray> {
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<String>? = null,
val stockCategory: String? = null,
val storeLocation: String? = null,
)

+ 14
- 0
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'
);

+ 8
- 0
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")),
)
}
}

正在加载...
取消
保存