Przeglądaj źródła

Merge branch 'production' into fix負數倉

undefined
Harry Groves 1 tydzień temu
rodzic
commit
d9a0cb7146
7 zmienionych plików z 365 dodań i 74 usunięć
  1. +23
    -2
      src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt
  2. +4
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/projections/PurchaseOrderLineInfo.kt
  3. +10
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt
  4. +29
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/ReportMultiValueTokens.kt
  5. +36
    -7
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  6. +212
    -65
      src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt
  7. +51
    -0
      src/test/kotlin/com/ffii/fpsms/modules/report/service/ReportMultiValueTokensTest.kt

+ 23
- 2
src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt Wyświetl plik

@@ -226,6 +226,27 @@ open class ItemUomService(
return stockQty.setScale(0, RoundingMode.DOWN) return stockQty.setScale(0, RoundingMode.DOWN)
} }


/**
* Count-type stock units must be whole numbers: PCS/CTN…, 包, 箱, 件, 個, PCS, CTN, etc.
* Weight/volume units (克, 公斤, 升…) keep 2 decimals.
*/
open fun isIntegerCountStockUom(stockUnit: ItemUom?): Boolean {
if (stockUnit == null) return false
val code = stockUnit.uom?.code?.trim().orEmpty()
if (code.startsWith("PCS/CTN", ignoreCase = true)) return true
val short = stockUnit.uom?.udfShortDesc?.trim().orEmpty()
return isIntegerCountUnitToken(short) || isIntegerCountUnitToken(code)
}

private fun isIntegerCountUnitToken(raw: String): Boolean {
val token = raw.trim()
if (token.isEmpty()) return false
if (token == "包" || token == "箱" || token == "件" || token == "個") return true
val upper = token.uppercase()
return upper == "PCS" || upper == "PC" || upper == "CTN" || upper == "EA" ||
upper == "PK" || upper == "PKT" || upper == "BOX"
}

/** /**
* Stock UOM code [PCS/CTN]* denotes pieces-per-carton; displayed stock qty should be whole cartons (integer). * Stock UOM code [PCS/CTN]* denotes pieces-per-carton; displayed stock qty should be whole cartons (integer).
*/ */
@@ -234,9 +255,9 @@ open class ItemUomService(
return code.startsWith("PCS/CTN", ignoreCase = true) return code.startsWith("PCS/CTN", ignoreCase = true)
} }


/** After ratio math: 2 decimals, or integer when stock unit is PCS/CTN… */
/** After ratio math: integer HALF_UP for count units (包/箱/PCS…), else 2 decimals. */
private fun finalizePreciseStockQty(stockUnit: ItemUom?, stockQty: BigDecimal): BigDecimal { private fun finalizePreciseStockQty(stockUnit: ItemUom?, stockQty: BigDecimal): BigDecimal {
if (isPiecesPerCartonStockUomCode(stockUnit)) {
if (isIntegerCountStockUom(stockUnit) || isPiecesPerCartonStockUomCode(stockUnit)) {
return stockQty.setScale(0, RoundingMode.HALF_UP) return stockQty.setScale(0, RoundingMode.HALF_UP)
} }
return stockQty.setScale(2, RoundingMode.HALF_UP) return stockQty.setScale(2, RoundingMode.HALF_UP)


+ 4
- 0
src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/projections/PurchaseOrderLineInfo.kt Wyświetl plik

@@ -57,9 +57,13 @@ data class StockUomForPoLine (
val id: Long? = null, val id: Long? = null,
val stockUomCode: String? = null, val stockUomCode: String? = null,
val stockUomDesc: String? = null, val stockUomDesc: String? = null,
val stockUomShortDesc: String? = null,
val stockQty: BigDecimal? = null, val stockQty: BigDecimal? = null,
val stockRatioN: BigDecimal? = null, val stockRatioN: BigDecimal? = null,
val stockRatioD: BigDecimal? = null, val stockRatioD: BigDecimal? = null,
val purchaseRatioN: BigDecimal? = null, val purchaseRatioN: BigDecimal? = null,
val purchaseRatioD: BigDecimal? = null, val purchaseRatioD: BigDecimal? = null,
val sourceRatioN: BigDecimal? = null,
val sourceRatioD: BigDecimal? = null,
val stockQtyInteger: Boolean? = null,
) )

+ 10
- 0
src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt Wyświetl plik

@@ -294,6 +294,12 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> {
val itemId = thisPol.item?.id val itemId = thisPol.item?.id
val qtyM18 = thisPol.qtyM18 val qtyM18 = thisPol.qtyM18
val uomM18Id = thisPol.uomM18?.id val uomM18Id = thisPol.uomM18?.id
val sourceUomId = uomM18Id ?: thisPol.uom?.id
val sourceItemUom = if (itemId != null && sourceUomId != null) {
itemUomService.findFirstByItemIdAndUomId(itemId, sourceUomId)
} else {
null
}
val stockQtyFromM18 = if (itemId != null && qtyM18 != null && uomM18Id != null) { val stockQtyFromM18 = if (itemId != null && qtyM18 != null && uomM18Id != null) {
itemUomService.convertQtyToStockQtyPrecise(itemId, uomM18Id, qtyM18) itemUomService.convertQtyToStockQtyPrecise(itemId, uomM18Id, qtyM18)
} else { } else {
@@ -303,6 +309,7 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> {
id = iu?.id, id = iu?.id,
stockUomCode = iu?.uom?.code, stockUomCode = iu?.uom?.code,
stockUomDesc = iu?.uom?.udfudesc, stockUomDesc = iu?.uom?.udfudesc,
stockUomShortDesc = iu?.uom?.udfShortDesc,
stockQty = if (stockQtyFromM18 > BigDecimal.ZERO) stockQtyFromM18 else { stockQty = if (stockQtyFromM18 > BigDecimal.ZERO) stockQtyFromM18 else {
// fallback to legacy behavior when M18 fields are missing // fallback to legacy behavior when M18 fields are missing
iu?.item?.id?.let { iId -> itemUomService.convertPurchaseQtyToStockQtyPrecise(iId, (thisPol.qty ?: BigDecimal.ZERO)) } ?: BigDecimal.ZERO iu?.item?.id?.let { iId -> itemUomService.convertPurchaseQtyToStockQtyPrecise(iId, (thisPol.qty ?: BigDecimal.ZERO)) } ?: BigDecimal.ZERO
@@ -311,6 +318,9 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> {
stockRatioD = iu?.ratioD, stockRatioD = iu?.ratioD,
purchaseRatioN = purchaseUnit?.ratioN, purchaseRatioN = purchaseUnit?.ratioN,
purchaseRatioD = purchaseUnit?.ratioD, purchaseRatioD = purchaseUnit?.ratioD,
sourceRatioN = sourceItemUom?.ratioN ?: purchaseUnit?.ratioN,
sourceRatioD = sourceItemUom?.ratioD ?: purchaseUnit?.ratioD,
stockQtyInteger = itemUomService.isIntegerCountStockUom(iu),
) )
} } ?: StockUomForPoLine() } } ?: StockUomForPoLine()
PoLineWithStockInLine( PoLineWithStockInLine(


+ 29
- 0
src/main/java/com/ffii/fpsms/modules/report/service/ReportMultiValueTokens.kt Wyświetl plik

@@ -0,0 +1,29 @@
package com.ffii.fpsms.modules.report.service

/**
* Splits report filter values pasted from Excel or typed in a text field.
* Accepts commas, semicolons, Chinese commas, and any whitespace (spaces, tabs, newlines).
*/
object ReportMultiValueTokens {
private val SEPARATORS = Regex("[\\s,;,、\\u00A0\\u3000]+")

fun split(paramValue: String?): List<String> {
if (paramValue.isNullOrBlank()) return emptyList()
return paramValue
.split(SEPARATORS)
.map { it.trim() }
.filter { it.isNotBlank() }
.distinctBy { it.uppercase() }
}

/**
* Full item codes (e.g. FA0123) can use an indexed IN-list.
* Short / wildcard tokens stay as prefix/LIKE matches.
*/
fun partitionExactAndFuzzy(paramValue: String?, exactMinLength: Int = 5): Pair<List<String>, List<String>> {
val tokens = split(paramValue)
val exact = tokens.filter { it.length >= exactMinLength && '%' !in it && '_' !in it }
val fuzzy = tokens.filter { token -> exact.none { it.equals(token, ignoreCase = true) } }
return exact to fuzzy
}
}

+ 36
- 7
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Wyświetl plik

@@ -507,8 +507,8 @@ return result
return result return result
} }
/** /**
* Helper function to build SQL clause for comma-separated values.
* Supports multiple values like "val1, val2, val3" and generates OR conditions with LIKE.
* Helper function to build SQL clause for comma/space-separated values.
* Supports Excel paste like "FA0123 FA2332" or "FA0123,FA2332, FA23238" (LIKE OR).
*/ */
private fun buildMultiValueLikeClause( private fun buildMultiValueLikeClause(
paramValue: String?, paramValue: String?,
@@ -518,7 +518,7 @@ return result
): String { ): String {
if (paramValue.isNullOrBlank()) return "" if (paramValue.isNullOrBlank()) return ""
val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
val values = ReportMultiValueTokens.split(paramValue)
if (values.isEmpty()) return "" if (values.isEmpty()) return ""
val conditions = values.mapIndexed { index, value -> val conditions = values.mapIndexed { index, value ->
@@ -530,6 +530,33 @@ return result
return "AND (${conditions.joinToString(" OR ")})" return "AND (${conditions.joinToString(" OR ")})"
} }


/**
* Item-code filter that prefers indexed equality (`IN`) for pasted/full codes,
* and prefix LIKE only for short tokens such as "FA".
*/
private fun buildItemCodeFilterClause(
paramValue: String?,
columnName: String,
paramPrefix: String,
args: MutableMap<String, Any>,
): String {
val (exact, fuzzy) = ReportMultiValueTokens.partitionExactAndFuzzy(paramValue)
if (exact.isEmpty() && fuzzy.isEmpty()) return ""

val parts = mutableListOf<String>()
exact.forEachIndexed { index, value ->
val paramName = "${paramPrefix}_eq_$index"
args[paramName] = value
parts.add("$columnName = :$paramName")
}
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 ")})"
}

/** /**
* Helper function to build SQL clause for comma-separated values with exact match. * Helper function to build SQL clause for comma-separated values with exact match.
* Supports multiple values like "val1, val2, val3" and generates OR conditions with =. * Supports multiple values like "val1, val2, val3" and generates OR conditions with =.
@@ -1501,7 +1528,9 @@ return result
* FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.1 | 2026-09-16 * FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.1 | 2026-09-16
* Stock Balance Report (date-driven). One summarized row per item. * Stock Balance Report (date-driven). One summarized row per item.
* *
* Qty comes from [stock_lot_day] (daily lot close) instead of scanning all stock_ledger:
* Qty comes from [stock_lot_day] (daily lot close) instead of scanning all stock_ledger.
* Full pasted item codes use indexed equality (`=` / `IN`); short tokens stay prefix LIKE.
*
* - 期初存量: historical = that day's opening; today = yesterday's closing * - 期初存量: historical = that day's opening; today = yesterday's closing
* - 現存存貨: historical = that day's closing; today = inventory.onHandQty * - 現存存貨: historical = that day's closing; today = inventory.onHandQty
* (today is not closed yet). If the day row is missing, fall back to previous * (today is not closed yet). If the day row is missing, fall back to previous
@@ -1529,9 +1558,9 @@ return result
args["d0EndExclusive"] = stockLocalDate.plusDays(1).atStartOfDay() args["d0EndExclusive"] = stockLocalDate.plusDays(1).atStartOfDay()


val stockCategorySql = buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args) val stockCategorySql = buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args)
val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
val itemCodeSqlDay = buildMultiValueLikeClause(itemCode, "d.itemCode", "itemCodeDay", args)
val itemCodeSqlSl = buildMultiValueLikeClause(itemCode, "sl.itemCode", "itemCodeSl", args)
val itemCodeSql = buildItemCodeFilterClause(itemCode, "it.code", "itemCode", args)
val itemCodeSqlDay = buildItemCodeFilterClause(itemCode, "d.itemCode", "itemCodeDay", args)
val itemCodeSqlSl = buildItemCodeFilterClause(itemCode, "sl.itemCode", "itemCodeSl", args)
val storeLocationSql = if (!storeLocation.isNullOrBlank()) { val storeLocationSql = if (!storeLocation.isNullOrBlank()) {
args["storeLocation"] = "%$storeLocation%" args["storeLocation"] = "%$storeLocation%"
""" """


+ 212
- 65
src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt Wyświetl plik

@@ -23,6 +23,7 @@ import java.time.LocalTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.common.SecurityUtils import com.ffii.fpsms.modules.common.SecurityUtils
import com.ffii.fpsms.modules.report.service.M18BomShopSyncReportService import com.ffii.fpsms.modules.report.service.M18BomShopSyncReportService
import com.ffii.fpsms.modules.report.service.ReportMultiValueTokens
import com.ffii.fpsms.modules.report.service.ReportService import com.ffii.fpsms.modules.report.service.ReportService
import com.ffii.fpsms.modules.report.service.ShopOrderReplenishmentReportService import com.ffii.fpsms.modules.report.service.ShopOrderReplenishmentReportService


@@ -309,10 +310,164 @@ class ReportController(
@RequestParam(required = false) lastOutDateStart: String?, @RequestParam(required = false) lastOutDateStart: String?,
@RequestParam(required = false) lastOutDateEnd: String?, @RequestParam(required = false) lastOutDateEnd: String?,
@RequestParam(required = false, defaultValue = "0") stockTakeRoundId: Long, @RequestParam(required = false, defaultValue = "0") stockTakeRoundId: Long,
): ResponseEntity<ByteArray> =
generateStockBalancePdf(
stockCategory = stockCategory,
itemCode = itemCode,
stockDate = stockDate,
balanceFilterStart = balanceFilterStart,
balanceFilterEnd = balanceFilterEnd,
storeLocation = storeLocation,
lastInDateStart = lastInDateStart,
lastInDateEnd = lastInDateEnd,
lastOutDateStart = lastOutDateStart,
lastOutDateEnd = lastOutDateEnd,
stockTakeRoundId = stockTakeRoundId,
)

@PostMapping("/print-stock-balance")
fun generateStockBalanceReportPost(
@RequestBody(required = false) req: StockBalanceReportRequest?,
): ResponseEntity<ByteArray> {
val r = req ?: StockBalanceReportRequest()
return generateStockBalancePdf(
stockCategory = r.stockCategory,
itemCode = combinedItemCode(r.itemCode, r.itemCodePaste, r.itemCodes),
stockDate = r.stockDate,
balanceFilterStart = r.balanceFilterStart,
balanceFilterEnd = r.balanceFilterEnd,
storeLocation = r.storeLocation,
lastInDateStart = r.lastInDateStart,
lastInDateEnd = r.lastInDateEnd,
lastOutDateStart = r.lastOutDateStart,
lastOutDateEnd = r.lastOutDateEnd,
stockTakeRoundId = r.stockTakeRoundId ?: 0L,
)
}

@GetMapping("/print-stock-balance-excel")
fun exportStockBalanceReportExcel(
@RequestParam(required = false) stockCategory: String?,
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false) stockDate: String?,
@RequestParam(required = false) balanceFilterStart: String?,
@RequestParam(required = false) balanceFilterEnd: String?,
@RequestParam(required = false) storeLocation: String?,
@RequestParam(required = false) lastInDateStart: String?,
@RequestParam(required = false) lastInDateEnd: String?,
@RequestParam(required = false) lastOutDateStart: String?,
@RequestParam(required = false) lastOutDateEnd: String?,
@RequestParam(required = false, defaultValue = "0") stockTakeRoundId: Long,
): ResponseEntity<ByteArray> =
generateStockBalanceExcel(
stockCategory = stockCategory,
itemCode = itemCode,
stockDate = stockDate,
balanceFilterStart = balanceFilterStart,
balanceFilterEnd = balanceFilterEnd,
storeLocation = storeLocation,
lastInDateStart = lastInDateStart,
lastInDateEnd = lastInDateEnd,
lastOutDateStart = lastOutDateStart,
lastOutDateEnd = lastOutDateEnd,
stockTakeRoundId = stockTakeRoundId,
)

@PostMapping("/print-stock-balance-excel")
fun exportStockBalanceReportExcelPost(
@RequestBody(required = false) req: StockBalanceReportRequest?,
): ResponseEntity<ByteArray> { ): ResponseEntity<ByteArray> {
val r = req ?: StockBalanceReportRequest()
return generateStockBalanceExcel(
stockCategory = r.stockCategory,
itemCode = combinedItemCode(r.itemCode, r.itemCodePaste, r.itemCodes),
stockDate = r.stockDate,
balanceFilterStart = r.balanceFilterStart,
balanceFilterEnd = r.balanceFilterEnd,
storeLocation = r.storeLocation,
lastInDateStart = r.lastInDateStart,
lastInDateEnd = r.lastInDateEnd,
lastOutDateStart = r.lastOutDateStart,
lastOutDateEnd = r.lastOutDateEnd,
stockTakeRoundId = r.stockTakeRoundId ?: 0L,
)
}

private fun combinedItemCode(
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(",")
}

private fun loadStockBalanceRows(
stockCategory: String?,
itemCode: String?,
stockDate: String?,
balanceFilterStart: String?,
balanceFilterEnd: String?,
storeLocation: String?,
lastInDateStart: String?,
lastInDateEnd: String?,
lastOutDateStart: String?,
lastOutDateEnd: String?,
stockTakeRoundId: Long,
): List<Map<String, Any>> =
if (!stockDate.isNullOrBlank()) {
reportService.searchStockBalanceReportByDate(
stockCategory = stockCategory,
itemCode = itemCode,
stockDate = stockDate,
balanceFilterStart = balanceFilterStart,
balanceFilterEnd = balanceFilterEnd,
storeLocation = storeLocation,
)
} else {
reportService.searchStockBalanceReport(
stockCategory,
itemCode,
balanceFilterStart,
balanceFilterEnd,
storeLocation,
lastInDateStart,
lastInDateEnd,
lastOutDateStart,
lastOutDateEnd,
stockTakeRoundId,
)
}

private fun generateStockBalancePdf(
stockCategory: String?,
itemCode: String?,
stockDate: String?,
balanceFilterStart: String?,
balanceFilterEnd: String?,
storeLocation: String?,
lastInDateStart: String?,
lastInDateEnd: String?,
lastOutDateStart: String?,
lastOutDateEnd: String?,
stockTakeRoundId: Long,
): 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["itemNo"] = itemCode ?: "All"
parameters["itemNo"] =
when {
itemCode.isNullOrBlank() -> "All"
tokenCount > 30 -> "$tokenCount items"
else -> itemCode
}
parameters["reportDate"] = (stockDate ?: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))) parameters["reportDate"] = (stockDate ?: 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"))
parameters["storeLocation"] = storeLocation ?: "" parameters["storeLocation"] = storeLocation ?: ""
@@ -323,35 +478,24 @@ class ReportController(
parameters["lastOutDateStart"] = lastOutDateStart ?: "" parameters["lastOutDateStart"] = lastOutDateStart ?: ""
parameters["lastOutDateEnd"] = lastOutDateEnd ?: "" parameters["lastOutDateEnd"] = lastOutDateEnd ?: ""


val dbData =
if (!stockDate.isNullOrBlank()) {
reportService.searchStockBalanceReportByDate(
stockCategory = stockCategory,
itemCode = itemCode,
stockDate = stockDate,
balanceFilterStart = balanceFilterStart,
balanceFilterEnd = balanceFilterEnd,
storeLocation = storeLocation,
)
} else {
reportService.searchStockBalanceReport(
stockCategory,
itemCode,
balanceFilterStart,
balanceFilterEnd,
storeLocation,
lastInDateStart,
lastInDateEnd,
lastOutDateStart,
lastOutDateEnd,
stockTakeRoundId
)
}
val dbData = loadStockBalanceRows(
stockCategory = stockCategory,
itemCode = itemCode,
stockDate = stockDate,
balanceFilterStart = balanceFilterStart,
balanceFilterEnd = balanceFilterEnd,
storeLocation = storeLocation,
lastInDateStart = lastInDateStart,
lastInDateEnd = lastInDateEnd,
lastOutDateStart = lastOutDateStart,
lastOutDateEnd = lastOutDateEnd,
stockTakeRoundId = stockTakeRoundId,
)


val pdfBytes = reportService.createPdfResponse( val pdfBytes = reportService.createPdfResponse(
"/jasper/StockBalanceReport.jrxml", "/jasper/StockBalanceReport.jrxml",
parameters, parameters,
dbData
dbData,
) )


val headers = HttpHeaders().apply { val headers = HttpHeaders().apply {
@@ -359,48 +503,35 @@ class ReportController(
setContentDispositionFormData("attachment", "StockBalanceReport.pdf") setContentDispositionFormData("attachment", "StockBalanceReport.pdf")
set("filename", "StockBalanceReport.pdf") set("filename", "StockBalanceReport.pdf")
} }

return ResponseEntity(pdfBytes, headers, HttpStatus.OK) return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
} }


@GetMapping("/print-stock-balance-excel")
fun exportStockBalanceReportExcel(
@RequestParam(required = false) stockCategory: String?,
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false) stockDate: String?,
@RequestParam(required = false) balanceFilterStart: String?,
@RequestParam(required = false) balanceFilterEnd: String?,
@RequestParam(required = false) storeLocation: String?,
@RequestParam(required = false) lastInDateStart: String?,
@RequestParam(required = false) lastInDateEnd: String?,
@RequestParam(required = false) lastOutDateStart: String?,
@RequestParam(required = false) lastOutDateEnd: String?,
@RequestParam(required = false, defaultValue = "0") stockTakeRoundId: Long,
private fun generateStockBalanceExcel(
stockCategory: String?,
itemCode: String?,
stockDate: String?,
balanceFilterStart: String?,
balanceFilterEnd: String?,
storeLocation: String?,
lastInDateStart: String?,
lastInDateEnd: String?,
lastOutDateStart: String?,
lastOutDateEnd: String?,
stockTakeRoundId: Long,
): ResponseEntity<ByteArray> { ): ResponseEntity<ByteArray> {
val dbData =
if (!stockDate.isNullOrBlank()) {
reportService.searchStockBalanceReportByDate(
stockCategory = stockCategory,
itemCode = itemCode,
stockDate = stockDate,
balanceFilterStart = balanceFilterStart,
balanceFilterEnd = balanceFilterEnd,
storeLocation = storeLocation,
)
} else {
reportService.searchStockBalanceReport(
stockCategory,
itemCode,
balanceFilterStart,
balanceFilterEnd,
storeLocation,
lastInDateStart,
lastInDateEnd,
lastOutDateStart,
lastOutDateEnd,
stockTakeRoundId
)
}
val dbData = loadStockBalanceRows(
stockCategory = stockCategory,
itemCode = itemCode,
stockDate = stockDate,
balanceFilterStart = balanceFilterStart,
balanceFilterEnd = balanceFilterEnd,
storeLocation = storeLocation,
lastInDateStart = lastInDateStart,
lastInDateEnd = lastInDateEnd,
lastOutDateStart = lastOutDateStart,
lastOutDateEnd = lastOutDateEnd,
stockTakeRoundId = stockTakeRoundId,
)


val excelBytes = createStockBalanceExcel( val excelBytes = createStockBalanceExcel(
dbData = dbData, dbData = dbData,
@@ -1130,4 +1261,20 @@ class ReportController(
return mapOf("rows" to rows) return mapOf("rows" to rows)
} }


}
}

data class StockBalanceReportRequest(
val stockDate: String? = null,
val itemCode: String? = null,
val itemCodePaste: String? = null,
val itemCodes: List<String>? = null,
val stockCategory: String? = null,
val balanceFilterStart: String? = null,
val balanceFilterEnd: String? = null,
val storeLocation: String? = null,
val lastInDateStart: String? = null,
val lastInDateEnd: String? = null,
val lastOutDateStart: String? = null,
val lastOutDateEnd: String? = null,
val stockTakeRoundId: Long? = 0L,
)

+ 51
- 0
src/test/kotlin/com/ffii/fpsms/modules/report/service/ReportMultiValueTokensTest.kt Wyświetl plik

@@ -0,0 +1,51 @@
package com.ffii.fpsms.modules.report.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class ReportMultiValueTokensTest {
@Test
fun `splits space-separated item codes`() {
assertEquals(
listOf("FA0123", "FA2332"),
ReportMultiValueTokens.split("FA0123 FA2332"),
)
}

@Test
fun `splits commas with extra spaces`() {
assertEquals(
listOf("FA0123", "FA2332", "FA23238"),
ReportMultiValueTokens.split("FA0123,FA2332, FA23238"),
)
}

@Test
fun `splits mixed commas spaces and newlines`() {
val raw = "FA0123,FA2332, FA23238\nFA9999 FA0001,, FA2332"
assertEquals(
listOf("FA0123", "FA2332", "FA23238", "FA9999", "FA0001"),
ReportMultiValueTokens.split(raw),
)
}

@Test
fun `returns empty for blank input`() {
assertEquals(emptyList<String>(), ReportMultiValueTokens.split(null))
assertEquals(emptyList<String>(), ReportMultiValueTokens.split(" , ; "))
}

@Test
fun `full item codes use exact IN partition`() {
val (exact, fuzzy) = ReportMultiValueTokens.partitionExactAndFuzzy("FA0123 FA2332, FA23238")
assertEquals(listOf("FA0123", "FA2332", "FA23238"), exact)
assertEquals(emptyList<String>(), fuzzy)
}

@Test
fun `short prefix stays fuzzy`() {
val (exact, fuzzy) = ReportMultiValueTokens.partitionExactAndFuzzy("FA FA0123")
assertEquals(listOf("FA0123"), exact)
assertEquals(listOf("FA"), fuzzy)
}
}

Ładowanie…
Anuluj
Zapisz