瀏覽代碼

Remove hard-coded placeholders and data

debrand
Harry Groves 1 月之前
父節點
當前提交
79fa957896
共有 17 個檔案被更改,包括 119 行新增 和 108 行删除
  1. +1
    -1
      src/main/java/com/ffii/fpsms/modules/common/SettingNames.java
  2. +1
    -1
      src/main/java/com/ffii/fpsms/modules/common/alert/SchedulerSyncAlertProperties.kt
  3. +2
    -2
      src/main/java/com/ffii/fpsms/modules/common/alert/SchedulerSyncAlertService.kt
  4. +21
    -0
      src/main/java/com/ffii/fpsms/modules/common/demo/DemoSandboxSeeder.kt
  5. +3
    -3
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt
  6. +3
    -3
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt
  7. +1
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SettingsResponse.kt
  8. +30
    -45
      src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt
  9. +14
    -7
      src/main/java/com/ffii/fpsms/modules/report/service/StockLotOnhandReportService.kt
  10. +24
    -26
      src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt
  11. +1
    -1
      src/main/java/com/ffii/fpsms/py/PyJobOrderListItem.kt
  12. +2
    -2
      src/main/java/com/ffii/fpsms/py/PyJobOrderListMapper.kt
  13. +2
    -2
      src/main/resources/application-prod.yml
  14. +1
    -1
      src/main/resources/application.yml
  15. +6
    -6
      src/test/kotlin/com/ffii/fpsms/modules/jobOrder/service/LaserBag2PayloadTest.kt
  16. +1
    -1
      src/test/kotlin/com/ffii/fpsms/modules/report/service/StockLedgerPdfSpeedTest.kt
  17. +6
    -6
      src/test/kotlin/com/ffii/fpsms/py/PyJobOrderListMapperTest.kt

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/common/SettingNames.java 查看文件

@@ -129,7 +129,7 @@ public abstract class SettingNames {
/** Laser marking TCP port (default 45678) */ /** Laser marking TCP port (default 45678) */
public static final String LASER_PRINT_PORT = "LASER_PRINT.port"; public static final String LASER_PRINT_PORT = "LASER_PRINT.port";


/** Comma-separated BOM item codes shown on /laserPrint job list (e.g. PP1175); blank = no filter (all packaging JOs) */
/** Comma-separated BOM item codes shown on /laserPrint job list; blank = no filter (all packaging JOs) */
public static final String LASER_PRINT_ITEM_CODES = "LASER_PRINT.itemCodes"; public static final String LASER_PRINT_ITEM_CODES = "LASER_PRINT.itemCodes";


/** JSON: last laser TCP send where printer returned receive (job order no., lot, itemId/stockInLineId, etc.) */ /** JSON: last laser TCP send where printer returned receive (job order no., lot, itemId/stockInLineId, etc.) */


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/common/alert/SchedulerSyncAlertProperties.kt 查看文件

@@ -46,7 +46,7 @@ data class SmsProperties(
data class EmailAlertProperties( data class EmailAlertProperties(
/** Send sync alerts by email (uses DB MAIL.smtp.* — Office 365). */ /** Send sync alerts by email (uses DB MAIL.smtp.* — Office 365). */
val enabled: Boolean = false, val enabled: Boolean = false,
/** Comma-separated recipients, e.g. [email protected],[email protected] */
/** Comma-separated recipients. */
val toAddresses: String = "", val toAddresses: String = "",
/** Subject prefix; full subject is prefix + job + code in brackets. */ /** Subject prefix; full subject is prefix + job + code in brackets. */
val subjectPrefix: String = "FPSMS M18 sync alert", val subjectPrefix: String = "FPSMS M18 sync alert",


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/common/alert/SchedulerSyncAlertService.kt 查看文件

@@ -424,7 +424,7 @@ open class SchedulerSyncAlertService(
.map { it.trim() } .map { it.trim() }
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
if (emailTo.isEmpty()) { if (emailTo.isEmpty()) {
return "Set scheduler.sync-alert.email.to-addresses (e.g. [email protected])"
return "Set scheduler.sync-alert.email.to-addresses"
} }
if (!syncAlertEmailSender.isSmtpConfigured()) { if (!syncAlertEmailSender.isSmtpConfigured()) {
return syncAlertEmailSender.smtpConfigIssue() return syncAlertEmailSender.smtpConfigIssue()
@@ -452,7 +452,7 @@ open class SchedulerSyncAlertService(
e.message?.contains("not authenticated", ignoreCase = true) == true -> e.message?.contains("not authenticated", ignoreCase = true) == true ->
"\n\nOffice 365 fix checklist:\n" + "\n\nOffice 365 fix checklist:\n" +
"1. UPDATE settings SET value='true' WHERE name='MAIL.smtp.auth';\n" + "1. UPDATE settings SET value='true' WHERE name='MAIL.smtp.auth';\n" +
"2. MAIL.smtp.username must match the mailbox ([email protected])\n" +
"2. MAIL.smtp.username must match the sending mailbox\n" +
"3. Use an App password if MFA is on (not your normal login password)\n" + "3. Use an App password if MFA is on (not your normal login password)\n" +
"4. IT must enable 'Authenticated SMTP' for this mailbox in Exchange admin\n" + "4. IT must enable 'Authenticated SMTP' for this mailbox in Exchange admin\n" +
"5. Restart backend after changing settings (SMTP client is cached)" "5. Restart backend after changing settings (SMTP client is cached)"


+ 21
- 0
src/main/java/com/ffii/fpsms/modules/common/demo/DemoSandboxSeeder.kt 查看文件

@@ -135,6 +135,7 @@ class DemoSandboxSeeder(
refuseIfLiveSchemaReadable() refuseIfLiveSchemaReadable()


transactionTemplate.executeWithoutResult { transactionTemplate.executeWithoutResult {
clearLivePrinterDefaults()
ensureAuthorities() ensureAuthorities()
val existing = userRepository.findByUsernameAndDeletedFalse(DEMO_USERNAME) val existing = userRepository.findByUsernameAndDeletedFalse(DEMO_USERNAME)
if (existing.isPresent) { if (existing.isPresent) {
@@ -154,6 +155,26 @@ class DemoSandboxSeeder(
* The demo login must not be able to read the live company schema. * The demo login must not be able to read the live company schema.
* MySQL error 1142/1044 means access denied; 1049 means that schema is not visible. * MySQL error 1142/1044 means access denied; 1049 means that schema is not visible.
*/ */
/** Drop live printer host and item-code defaults copied in with the schema. */
private fun clearLivePrinterDefaults() {
jdbcTemplate.update(
"""
UPDATE settings
SET value = ''
WHERE name = 'LASER_PRINT.host'
AND value IN ('192.168.18.77', '192.168.17.10')
""".trimIndent(),
)
jdbcTemplate.update(
"""
UPDATE settings
SET value = ''
WHERE name = 'LASER_PRINT.itemCodes'
AND value IN ('PP1175', 'PP2211')
""".trimIndent(),
)
}

private fun refuseIfLiveSchemaReadable() { private fun refuseIfLiveSchemaReadable() {
try { try {
jdbcTemplate.queryForObject("SELECT 1 FROM fpsmsdb.`user` LIMIT 1", Int::class.java) jdbcTemplate.queryForObject("SELECT 1 FROM fpsmsdb.`user` LIMIT 1", Int::class.java)


+ 3
- 3
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt 查看文件

@@ -13,8 +13,8 @@ open class DoFloorSupplierSettingsService(
private const val SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F" private const val SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F"
private const val SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F" private const val SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F"


private val DEFAULT_SUPPLIERS_2F = listOf("P07", "P06D", "P06Y")
private val DEFAULT_SUPPLIERS_4F = listOf("P06B")
private val DEFAULT_SUPPLIERS_2F = listOf("DEMO-SUP")
private val DEFAULT_SUPPLIERS_4F = listOf("DEMO-SUP")
} }


open fun supplierCodesFromSetting(settingName: String, defaultList: List<String>): List<String> { open fun supplierCodesFromSetting(settingName: String, defaultList: List<String>): List<String> {
@@ -33,7 +33,7 @@ open class DoFloorSupplierSettingsService(
} }


open fun allowedSupplierCodesForFloor(floor: String?): List<String> { open fun allowedSupplierCodesForFloor(floor: String?): List<String> {
val f = floor?.trim()?.uppercase(Locale.ROOT).orEmpty()
val f = floor?.trim()?.replace("/", "")?.uppercase(Locale.ROOT).orEmpty()
val (codes2F, codes4F) = loadDoFloorSupplierLists() val (codes2F, codes4F) = loadDoFloorSupplierLists()
return when { return when {
f.isEmpty() || f == "ALL" || f == "All" -> (codes2F + codes4F).distinct() f.isEmpty() || f == "ALL" || f == "All" -> (codes2F + codes4F).distinct()


+ 3
- 3
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt 查看文件

@@ -106,9 +106,9 @@ class PlasticBagPrinterService(
private val hongKongZoneId = ZoneId.of("Asia/Hong_Kong") private val hongKongZoneId = ZoneId.of("Asia/Hong_Kong")


companion object { companion object {
private const val DEFAULT_LASER_BAG2_HOST = "192.168.18.77"
private const val DEFAULT_LASER_BAG2_HOST = ""
private const val DEFAULT_LASER_BAG2_PORT = 45678 private const val DEFAULT_LASER_BAG2_PORT = 45678
private const val DEFAULT_LASER_ITEM_CODES = "PP1175"
private const val DEFAULT_LASER_ITEM_CODES = ""
private const val PACKAGING_PROCESS_NAME = "包裝" private const val PACKAGING_PROCESS_NAME = "包裝"
/** Designer product BMP canvas (LOGO), 1-bit. */ /** Designer product BMP canvas (LOGO), 1-bit. */
private const val ONPACK_PRODUCT_BMP_WIDTH = 593 private const val ONPACK_PRODUCT_BMP_WIDTH = 593
@@ -1226,7 +1226,7 @@ class PlasticBagPrinterService(
return resource.inputStream.use { it.readBytes() } return resource.inputStream.use { it.readBytes() }
} }


/** Template files on classpath use uppercase code, e.g. `onpack2030_2/PP1175.image`. */
/** Template files on classpath use uppercase code, e.g. `onpack2030_2/DEMO-FG-01.image`. */
private fun loadOnPack2030_2ImageTemplateOrNull(codeLower: String): ByteArray? { private fun loadOnPack2030_2ImageTemplateOrNull(codeLower: String): ByteArray? {
onPackTemplateFileService.loadImage(OnPackTemplateFileService.MACHINE_LEMON, codeLower)?.let { return it } onPackTemplateFileService.loadImage(OnPackTemplateFileService.MACHINE_LEMON, codeLower)?.let { return it }
val resourcePath = "onpack2030_2/${codeLower.uppercase()}.image" val resourcePath = "onpack2030_2/${codeLower.uppercase()}.image"


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SettingsResponse.kt 查看文件

@@ -1,7 +1,7 @@
package com.ffii.fpsms.modules.jobOrder.web.model package com.ffii.fpsms.modules.jobOrder.web.model


/** /**
* @param itemCodes Comma-separated item codes for the laser job list filter (e.g. `PP1175` or `PP1175,AB123`). Empty string means no filter (show all packaging job orders).
* @param itemCodes Comma-separated item codes for the laser job list filter. Empty string means no filter (show all packaging job orders).
* @param lastReceiveSuccess Last job where the laser printer returned a receive ack (from settings JSON); null if never recorded or empty. * @param lastReceiveSuccess Last job where the laser printer returned a receive ack (from settings JSON); null if never recorded or empty.
*/ */
data class LaserBag2SettingsResponse( data class LaserBag2SettingsResponse(


+ 30
- 45
src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt 查看文件

@@ -55,10 +55,36 @@ class StockItemConsumptionTrendReportService(
return "AND (${conditions.joinToString(" OR ")})" return "AND (${conditions.joinToString(" OR ")})"
} }


private val itemTypes = listOf("mat", "sfg", "fg")

/** Item types used by the demo catalogue. "All" includes every one of them. */
private fun itemTypeFilterSql(
stockCategory: String?,
args: MutableMap<String, Any>,
paramPrefix: String,
): String {
val selected = if (
stockCategory.isNullOrBlank() ||
stockCategory == "All" ||
stockCategory.contains("All")
) {
itemTypes
} else {
stockCategory.split(",").map { it.trim() }.filter { it in itemTypes }
}
val categories = selected.ifEmpty { itemTypes }
val conditions = categories.mapIndexed { index, cat ->
val paramName = "${paramPrefix}_$index"
args[paramName] = cat
"it.type = :$paramName"
}
return "AND (${conditions.joinToString(" OR ")})"
}

/** /**
* Queries the database for Stock Item Consumption Trend Report data. * Queries the database for Stock Item Consumption Trend Report data.
* Flow: * Flow:
* 1. Filter items by type (MAT, WIP) - this is the stockCategory filter
* 1. Filter items by type (mat, sfg, fg) - this is the stockCategory filter
* 2. Use items.code to match stock_ledger.itemCode * 2. Use items.code to match stock_ledger.itemCode
* 3. Aggregate stock_ledger.outQty by month based on stock_ledger.modified * 3. Aggregate stock_ledger.outQty by month based on stock_ledger.modified
* Supports comma-separated values for stockCategory, stockSubCategory, and itemCode. * Supports comma-separated values for stockCategory, stockSubCategory, and itemCode.
@@ -72,30 +98,7 @@ class StockItemConsumptionTrendReportService(
): List<Map<String, Any>> { ): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>() val args = mutableMapOf<String, Any>()
// Filter by stockCategory from items.type (MAT/WIP/NM/FG/CMB)
// Supports multiple categories separated by comma (e.g., "MAT,WIP,NM")
// Valid categories: MAT, WIP, NM, FG, CMB
// If "All" is selected or empty, show all valid categories
// If specific categories are selected, show only those
val validCategories = setOf("MAT", "WIP", "NM", "FG", "CMB")
val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) {
// Handle multiple categories (comma-separated)
val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" && validCategories.contains(it) }
if (categories.isNotEmpty()) {
val conditions = categories.mapIndexed { index, cat ->
val paramName = "stockCategory_$index"
args[paramName] = cat
"it.type = :$paramName"
}
"AND (${conditions.joinToString(" OR ")})"
} else {
// If no valid categories, default to all valid categories
"AND (it.type = 'MAT' OR it.type = 'WIP' OR it.type = 'NM' OR it.type = 'FG' OR it.type = 'CMB')"
}
} else {
// If "All" or empty, filter to all valid categories
"AND (it.type = 'MAT' OR it.type = 'WIP' OR it.type = 'NM' OR it.type = 'FG' OR it.type = 'CMB')"
}
val stockCategorySql = itemTypeFilterSql(stockCategory, args, "stockCategory")
// 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()) {
@@ -176,30 +179,12 @@ class StockItemConsumptionTrendReportService(


/** /**
* Gets list of item code prefixes (first 2 letters) based on stockCategory filter. * Gets list of item code prefixes (first 2 letters) based on stockCategory filter.
* Supports multiple categories separated by comma (e.g., "MAT,WIP,NM").
* If stockCategory is "All" or null, returns prefixes for all valid categories (MAT, WIP, NM, FG, CMB).
* If stockCategory is "All" or null, returns prefixes for mat, sfg, and fg.
* Returns a list of maps with "prefix" key. * Returns a list of maps with "prefix" key.
*/ */
fun getStockItemCodePrefixes(stockCategory: String?): List<Map<String, String>> { fun getStockItemCodePrefixes(stockCategory: String?): List<Map<String, String>> {
val args = mutableMapOf<String, Any>() val args = mutableMapOf<String, Any>()
val validCategories = setOf("MAT", "WIP", "NM", "FG", "CMB")
val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) {
// Handle multiple categories (comma-separated)
val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" && validCategories.contains(it) }
if (categories.isNotEmpty()) {
val conditions = categories.mapIndexed { index, cat ->
val paramName = "stockCategory_$index"
args[paramName] = cat
"it.type = :$paramName"
}
"AND (${conditions.joinToString(" OR ")})"
} else {
"AND (it.type = 'MAT' OR it.type = 'WIP' OR it.type = 'NM' OR it.type = 'FG' OR it.type = 'CMB')"
}
} else {
"AND (it.type = 'MAT' OR it.type = 'WIP' OR it.type = 'NM' OR it.type = 'FG' OR it.type = 'CMB')"
}
val stockCategorySql = itemTypeFilterSql(stockCategory, args, "stockCategory")


val sql = """ val sql = """
SELECT DISTINCT LEFT(it.code, 2) as prefix SELECT DISTINCT LEFT(it.code, 2) as prefix


+ 14
- 7
src/main/java/com/ffii/fpsms/modules/report/service/StockLotOnhandReportService.kt 查看文件

@@ -178,7 +178,11 @@ stock_one AS (
4 4
) )
END AS avgUnitPriceRaw, END AS avgUnitPriceRaw,
UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) AS rootPoPrefix
CASE
WHEN root_sil.jobOrderId IS NOT NULL THEN 'production'
WHEN root_sil.purchaseOrderId IS NOT NULL THEN 'purchase'
ELSE 'other'
END AS lotOriginKey
FROM inventory_lot_line ill FROM inventory_lot_line ill
INNER JOIN inventory_lot il INNER JOIN inventory_lot il
ON il.id = ill.inventoryLotId AND il.deleted = 0 ON il.id = ill.inventoryLotId AND il.deleted = 0
@@ -372,8 +376,8 @@ stock_one AS (
val avg = toDouble(r["avgUnitPriceRaw"]) val avg = toDouble(r["avgUnitPriceRaw"])
val key = "${r["itemNo"]}|${r["unitOfMeasure"]}" val key = "${r["itemNo"]}|${r["unitOfMeasure"]}"
val last = lastTrnByLot[lotId] val last = lastTrnByLot[lotId]
val origin = r["rootPoPrefix"]?.toString().orEmpty().uppercase()
val showPrice = origin == "PP" || origin == "PF"
val origin = r["lotOriginKey"]?.toString().orEmpty()
val showPrice = origin == "purchase"
val row = HashMap<String, Any>(24) val row = HashMap<String, Any>(24)
row["inventoryLotLineId"] = lotId.toString() row["inventoryLotLineId"] = lotId.toString()
row["remarks"] = r["remarks"] ?: "" row["remarks"] = r["remarks"] ?: ""
@@ -419,14 +423,17 @@ stock_one AS (
return v.toString().toLongOrNull() ?: 0L return v.toString().toLongOrNull() ?: 0L
} }


/** PP/PF = root PO code prefix after TRF walk; other = not PP/PF (ADJ, TRF of other origin, JO, OPEN, …). */
/** purchase = root stock-in from a PO; production = job order; other = neither. */
private fun buildLotOriginFilterSql(lotOrigin: String?): String { private fun buildLotOriginFilterSql(lotOrigin: String?): String {
val v = lotOrigin?.trim().orEmpty() val v = lotOrigin?.trim().orEmpty()
if (v.isBlank() || v.equals("All", ignoreCase = true)) return "" if (v.isBlank() || v.equals("All", ignoreCase = true)) return ""
return when (v.lowercase()) { return when (v.lowercase()) {
"pp" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP'"
"pf" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF'"
"other" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) NOT IN ('PP', 'PF')"
"purchase" -> "AND root_sil.purchaseOrderId IS NOT NULL AND root_sil.jobOrderId IS NULL"
"production" -> "AND root_sil.jobOrderId IS NOT NULL"
"other" -> """
AND (root_sil.purchaseOrderId IS NULL OR root_sil.purchaseOrderId = 0)
AND root_sil.jobOrderId IS NULL
""".trimIndent()
else -> "" else -> ""
} }
} }


+ 24
- 26
src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt 查看文件

@@ -79,14 +79,11 @@ lot_root_origin AS (


private const val LOT_ROOT_CATEGORY_SQL = """ private const val LOT_ROOT_CATEGORY_SQL = """
CASE CASE
WHEN root_sil.jobOrderId IS NOT NULL THEN '工廠生產'
WHEN root_sil.jobOrderId IS NOT NULL THEN '生產'
WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN' THEN '期初存貨' WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN' THEN '期初存貨'
WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ' THEN '倉存調整' WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ' THEN '倉存調整'
WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP' THEN 'PP'
WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF' THEN 'PF'
WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'TOA' THEN 'TOA'
WHEN root_sil.purchaseOrderId IS NOT NULL THEN '採購入倉'
ELSE '其他入倉'
WHEN root_sil.purchaseOrderId IS NOT NULL THEN '採購'
ELSE '其他'
END""" END"""


private const val LOT_TYPE_DISPLAY_SQL = """ private const val LOT_TYPE_DISPLAY_SQL = """
@@ -1007,26 +1004,27 @@ ORDER BY
* - 篩選用 root_sil / root_po(根來源類型) * - 篩選用 root_sil / root_po(根來源類型)
*/ */
private fun buildLotTypeFilterClause(type: String?): String { private fun buildLotTypeFilterClause(type: String?): String {
val normalized = type?.trim().orEmpty()
if (normalized.isBlank() || normalized.equals("all", ignoreCase = true)) return ""
return when (normalized) {
"PP" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP'"
"PF" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF'"
"TOA" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'TOA'"
"工廠生產" -> "AND root_sil.jobOrderId IS NOT NULL"
"倉存調整" -> "AND UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ'"
"期初存貨" -> "AND UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN'"
"採購入倉" -> """
AND root_sil.purchaseOrderId IS NOT NULL
AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) NOT IN ('PP', 'PF', 'TOA')
""".trimIndent()
"其他入倉" -> """
AND (root_sil.purchaseOrderId IS NULL OR root_sil.purchaseOrderId = 0)
AND root_sil.jobOrderId IS NULL
AND UPPER(TRIM(COALESCE(root_sil.type, ''))) NOT IN ('OPEN', 'ADJ')
""".trimIndent()
else -> ""
}
val parts = type?.split(",")
?.map { it.trim() }
?.filter { it.isNotBlank() && !it.equals("all", ignoreCase = true) }
.orEmpty()
if (parts.isEmpty()) return ""
val predicates = parts.mapNotNull { lotTypePredicate(it) }
if (predicates.isEmpty()) return ""
return "AND (" + predicates.joinToString(" OR ") { "($it)" } + ")"
}

private fun lotTypePredicate(type: String): String? = when (type) {
"purchase" -> "root_sil.purchaseOrderId IS NOT NULL AND root_sil.jobOrderId IS NULL"
"production" -> "root_sil.jobOrderId IS NOT NULL"
"adjustment" -> "UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ'"
"opening" -> "UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN'"
"other" -> """
(root_sil.purchaseOrderId IS NULL OR root_sil.purchaseOrderId = 0)
AND root_sil.jobOrderId IS NULL
AND UPPER(TRIM(COALESCE(root_sil.type, ''))) NOT IN ('OPEN', 'ADJ')
""".trimIndent()
else -> null
} }


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


+ 1
- 1
src/main/java/com/ffii/fpsms/py/PyJobOrderListItem.kt 查看文件

@@ -10,7 +10,7 @@ import java.time.LocalDateTime
* stockInLineId and itemId are for QR code: {"itemId": xxx, "stockInLineId": xxx} * stockInLineId and itemId are for QR code: {"itemId": xxx, "stockInLineId": xxx}
* lotNo replaces job order no. on the label display. * lotNo replaces job order no. on the label display.
* [itemName] is BOM/item display name plus stock UOM [udfudesc] in parentheses when appropriate; * [itemName] is BOM/item display name plus stock UOM [udfudesc] in parentheses when appropriate;
* GET /py/laser-job-orders (/laserPrint) may omit that suffix for specific item codes (e.g. PP1175) due to laser name length.
* GET /py/laser-job-orders (/laserPrint) may omit that suffix for item codes listed for the laser name-length limit.
*/ */
data class PyJobOrderListItem( data class PyJobOrderListItem(
val id: Long, val id: Long,


+ 2
- 2
src/main/java/com/ffii/fpsms/py/PyJobOrderListMapper.kt 查看文件

@@ -57,9 +57,9 @@ object PyJobOrderListMapper {


/** /**
* Item codes where /laserPrint must not append stock UOM to [itemName] (laser field length limit). * Item codes where /laserPrint must not append stock UOM to [itemName] (laser field length limit).
* BOM base name only, e.g. PP1175 "鮮檸檬汁(P+4)" without "(1包X2磅)" suffix.
* Empty on the demo branch so no live item code is special-cased.
*/ */
private val LASER_OMIT_APPENDED_STOCK_UOM_ITEM_CODES = setOf("PP1175")
private val LASER_OMIT_APPENDED_STOCK_UOM_ITEM_CODES = emptySet<String>()


/** /**
* Display name for GET /py/laser-job-orders and /plastic/laser-job-orders (/laserPrint page). * Display name for GET /py/laser-job-orders and /plastic/laser-job-orders (/laserPrint page).


+ 2
- 2
src/main/resources/application-prod.yml 查看文件

@@ -61,10 +61,10 @@ scheduler:
to-numbers: "" to-numbers: ""
email: email:
enabled: true enabled: true
to-addresses: "[email protected],[email protected]"
to-addresses: ""
po-line: po-line:
enabled: true enabled: true
# From = MAIL.smtp.username in DB (e.g. [email protected] + Gmail app password)
# From = MAIL.smtp.username in DB settings.


# Laser Bag2 (/laserPrint) TCP auto-send; uses LASER_PRINT host/port/itemCodes from DB and sends first matching job only. # Laser Bag2 (/laserPrint) TCP auto-send; uses LASER_PRINT host/port/itemCodes from DB and sends first matching job only.
laser: laser:


+ 1
- 1
src/main/resources/application.yml 查看文件

@@ -57,7 +57,7 @@ scheduler:
content-sid: ${TWILIO_WHATSAPP_CONTENT_SID:} content-sid: ${TWILIO_WHATSAPP_CONTENT_SID:}
email: email:
enabled: ${SYNC_ALERT_EMAIL_ENABLED:true} enabled: ${SYNC_ALERT_EMAIL_ENABLED:true}
to-addresses: "[email protected],[email protected]"
to-addresses: ""
do1: do1:
min-records-processed: 400 min-records-processed: 400
grace-minutes-after-schedule: 30 grace-minutes-after-schedule: 30


+ 6
- 6
src/test/kotlin/com/ffii/fpsms/modules/jobOrder/service/LaserBag2PayloadTest.kt 查看文件

@@ -11,20 +11,20 @@ class LaserBag2PayloadTest {
@Test @Test
fun payload_without_expiry_keeps_original_three_fields() { fun payload_without_expiry_keeps_original_three_fields() {
assertEquals( assertEquals(
"{\"itemId\":1,\"stockInLineId\":2};PP1175;鮮檸檬汁;;",
PlasticBagPrinterService.buildLaserBag2Payload(1, 2, "PP1175", "鮮檸檬汁", null),
"{\"itemId\":1,\"stockInLineId\":2};DEMO-FG-01;鄉村麵包;;",
PlasticBagPrinterService.buildLaserBag2Payload(1, 2, "DEMO-FG-01", "鄉村麵包", null),
) )
assertEquals( assertEquals(
"0;PP1175;鮮檸檬汁;;",
PlasticBagPrinterService.buildLaserBag2Payload(null, null, "PP1175", "鮮檸檬汁"),
"0;DEMO-FG-01;鄉村麵包;;",
PlasticBagPrinterService.buildLaserBag2Payload(null, null, "DEMO-FG-01", "鄉村麵包"),
) )
} }


@Test @Test
fun payload_with_iso_expiry_adds_fourth_print_label_field() { fun payload_with_iso_expiry_adds_fourth_print_label_field() {
assertEquals( assertEquals(
"{\"itemId\":1,\"stockInLineId\":2};PP1175;鮮檸檬汁;Expiry Date 20260821;;",
PlasticBagPrinterService.buildLaserBag2Payload(1, 2, "PP1175", "鮮檸檬汁", "2026-08-21"),
"{\"itemId\":1,\"stockInLineId\":2};DEMO-FG-01;鄉村麵包;Expiry Date 20260821;;",
PlasticBagPrinterService.buildLaserBag2Payload(1, 2, "DEMO-FG-01", "鄉村麵包", "2026-08-21"),
) )
} }




+ 1
- 1
src/test/kotlin/com/ffii/fpsms/modules/report/service/StockLedgerPdfSpeedTest.kt 查看文件

@@ -23,7 +23,7 @@ class StockLedgerPdfSpeedTest {
"stockOut" to "0", "stockOut" to "0",
"cumBalance" to "1,210", "cumBalance" to "1,210",
"orderRefNo" to "DO-20260901-${i % 1000}", "orderRefNo" to "DO-20260901-${i % 1000}",
"storeLocation" to "W201",
"storeLocation" to "DEMO-A",
"totalStockIn" to "400", "totalStockIn" to "400",
"totalStockOut" to "0", "totalStockOut" to "0",
"totalCumBalance" to "1,210", "totalCumBalance" to "1,210",


+ 6
- 6
src/test/kotlin/com/ffii/fpsms/py/PyJobOrderListMapperTest.kt 查看文件

@@ -66,14 +66,14 @@ class PyJobOrderListMapperTest {
} }


@Test @Test
fun buildDisplayItemNameForLaser_omits_appended_uom_for_PP1175() {
fun buildDisplayItemNameForLaser_appends_uom_when_name_has_none() {
assertEquals( assertEquals(
"鮮檸檬汁(P+4)",
PyJobOrderListMapper.buildDisplayItemNameForLaser("鮮檸檬汁(P+4)", stockUom, "PP1175"),
"鄉村麵包($stockUom)",
PyJobOrderListMapper.buildDisplayItemNameForLaser("鄉村麵包", stockUom, "DEMO-FG-01"),
) )
assertEquals( assertEquals(
"咖哩汁(1包X2磅)",
PyJobOrderListMapper.buildDisplayItemNameForLaser("咖哩汁(1包X2磅)", stockUom, "PP1080"),
"奶包(1包X2磅)",
PyJobOrderListMapper.buildDisplayItemNameForLaser("奶包", stockUom, "DEMO-FG-02"),
) )
} }


@@ -92,7 +92,7 @@ class PyJobOrderListMapperTest {
@Test @Test
fun shelfLifeForItem_is_null_when_item_missing() { fun shelfLifeForItem_is_null_when_item_missing() {
val (days, useMinus18, expiry) = PyJobOrderListMapper.shelfLifeForItem( val (days, useMinus18, expiry) = PyJobOrderListMapper.shelfLifeForItem(
"PP1175",
"DEMO-FG-01",
emptyMap(), emptyMap(),
LocalDate.of(2026, 8, 20), LocalDate.of(2026, 8, 20),
) )


Loading…
取消
儲存