diff --git a/src/main/java/com/ffii/fpsms/py/PyJobOrderListItem.kt b/src/main/java/com/ffii/fpsms/py/PyJobOrderListItem.kt index 710a775..6c39629 100644 --- a/src/main/java/com/ffii/fpsms/py/PyJobOrderListItem.kt +++ b/src/main/java/com/ffii/fpsms/py/PyJobOrderListItem.kt @@ -8,8 +8,9 @@ import java.time.LocalDateTime * No login required. * stockInLineId and itemId are for QR code: {"itemId": xxx, "stockInLineId": xxx} * lotNo replaces job order no. on the label display. - * [itemName] is BOM/item display name plus stock UOM [udfudesc] in parentheses, e.g. "名稱(單位)"; - * if the name already contains both "(" and ")", the stock unit is not appended. + * [itemName] is BOM/item display name plus stock UOM [udfudesc] in parentheses, e.g. "名稱(1包X2磅)"; + * stock unit is not appended when a parenthetical segment already looks like packaging UOM + * (digits + unit tokens). Usage/grade notes in parentheses, e.g. "(菠菜用)" or "(P+4)", still get UOM appended. */ data class PyJobOrderListItem( val id: Long, diff --git a/src/main/java/com/ffii/fpsms/py/PyJobOrderListMapper.kt b/src/main/java/com/ffii/fpsms/py/PyJobOrderListMapper.kt index 9cbdcb2..1fe4ed9 100644 --- a/src/main/java/com/ffii/fpsms/py/PyJobOrderListMapper.kt +++ b/src/main/java/com/ffii/fpsms/py/PyJobOrderListMapper.kt @@ -1,96 +1,89 @@ package com.ffii.fpsms.py - - import com.ffii.fpsms.modules.jobOrder.entity.JobOrder - import com.ffii.fpsms.modules.master.service.ItemUomService - import com.ffii.fpsms.modules.stock.entity.StockInLineRepository +object PyJobOrderListMapper { + private val GRADE_IN_PARENS = Regex("""^P\+\d+$""", RegexOption.IGNORE_CASE) + private val PAREN_SEGMENT = Regex("""[((]([^()()]*)[))]""") + private val UOM_UNIT_TOKEN = Regex( + """(包|袋|磅|千克|公斤|克|毫升|公升|個|隻|斤|安士|ml|ML|kg|KG|\d+\s*[gG]\b|/包|/[磅千克克])""", + ) + + /** + * True when [segment] looks like stock/pack UOM text (e.g. "1包X2磅", "0.9L/包"), + * not usage/grade notes (e.g. "菠菜用", "熟", "P+4"). + */ + fun looksLikePackagingUom(segment: String): Boolean { + val s = segment.trim() + if (s.isEmpty()) return false + if (GRADE_IN_PARENS.matches(s)) return false + if (!s.any { it.isDigit() }) return false + return UOM_UNIT_TOKEN.containsMatchIn(s) + } -object PyJobOrderListMapper { + /** True if any parenthetical segment in [name] is packaging/UOM-like. */ + fun nameAlreadyHasPackagingUom(name: String): Boolean = + PAREN_SEGMENT.findAll(name).any { looksLikePackagingUom(it.groupValues[1]) } - fun toListItem( + /** + * Append stock [udfudesc] in parentheses when the base name lacks packaging UOM. + * Used by Bag3 / bagPrint via GET /py/job-orders. + */ + fun buildDisplayItemName(baseName: String?, stockUnitDesc: String?): String? { + val stock = stockUnitDesc?.trim().orEmpty() + val baseTrim = baseName?.trim().orEmpty() + return when { + stock.isEmpty() && baseTrim.isEmpty() -> null + stock.isEmpty() -> baseTrim + baseTrim.isEmpty() -> "($stock)" + nameAlreadyHasPackagingUom(baseTrim) -> baseTrim + stockAlreadyPresentInName(baseTrim, stock) -> baseTrim + else -> "$baseTrim($stock)" + } + } - jo: JobOrder, + private fun stockAlreadyPresentInName(name: String, stockUnitDesc: String): Boolean { + val norm = stockUnitDesc.replace(" ", "").lowercase() + if (norm.isEmpty()) return false + val nameNorm = name.replace(" ", "").lowercase() + return nameNorm.contains(norm) + } + fun toListItem( + jo: JobOrder, printed: PrintedQtyByChannel?, - stockInLineRepository: StockInLineRepository, - itemUomService: ItemUomService, - ): PyJobOrderListItem { - val itemCode = jo.bom?.item?.code ?: jo.bom?.code - val baseName = jo.bom?.name ?: jo.bom?.item?.name - val itemId = jo.bom?.item?.id - val stockUnitDesc = itemId?.let { id -> - itemUomService.findStockUnitByItemId(id)?.uom?.udfudesc - - }?.trim().orEmpty() - - val baseTrim = baseName?.trim().orEmpty() - - val baseAlreadyHasParens = '(' in baseTrim && ')' in baseTrim - - val itemName = when { - - stockUnitDesc.isEmpty() && baseTrim.isEmpty() -> null - - stockUnitDesc.isEmpty() -> baseTrim - - baseTrim.isEmpty() -> "($stockUnitDesc)" - - baseAlreadyHasParens -> baseTrim - - else -> "$baseTrim($stockUnitDesc)" - } + val itemName = buildDisplayItemName(baseName, stockUnitDesc) val stockInLine = jo.id?.let { stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(it) } - val stockInLineId = stockInLine?.id - val lotNo = stockInLine?.lotNo - val p = printed ?: PrintedQtyByChannel() return PyJobOrderListItem( - id = jo.id!!, - code = jo.code, - planStart = jo.planStart, - itemCode = itemCode, - itemName = itemName, - reqQty = jo.reqQty, - stockInLineId = stockInLineId, - itemId = itemId, - lotNo = lotNo, - bagPrintedQty = p.bagPrintedQty, - labelPrintedQty = p.labelPrintedQty, - laserPrintedQty = p.laserPrintedQty, - ) - } - } - diff --git a/src/test/kotlin/com/ffii/fpsms/py/PyJobOrderListMapperTest.kt b/src/test/kotlin/com/ffii/fpsms/py/PyJobOrderListMapperTest.kt new file mode 100644 index 0000000..7d22baf --- /dev/null +++ b/src/test/kotlin/com/ffii/fpsms/py/PyJobOrderListMapperTest.kt @@ -0,0 +1,73 @@ +package com.ffii.fpsms.py + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class PyJobOrderListMapperTest { + + private val stockUom = "1包X2磅" + + @Test + fun looksLikePackagingUom_recognizes_standard_pack_strings() { + assertTrue(PyJobOrderListMapper.looksLikePackagingUom("1包X2磅")) + assertTrue(PyJobOrderListMapper.looksLikePackagingUom("2磅/包")) + assertTrue(PyJobOrderListMapper.looksLikePackagingUom("1KG")) + assertTrue(PyJobOrderListMapper.looksLikePackagingUom("0.9L/包")) + assertTrue(PyJobOrderListMapper.looksLikePackagingUom("900ml包")) + assertTrue(PyJobOrderListMapper.looksLikePackagingUom("10個/包")) + assertTrue(PyJobOrderListMapper.looksLikePackagingUom("1包x800毫升")) + } + + @Test + fun looksLikePackagingUom_rejects_usage_and_grade_notes() { + assertFalse(PyJobOrderListMapper.looksLikePackagingUom("菠菜用")) + assertFalse(PyJobOrderListMapper.looksLikePackagingUom("熟")) + assertFalse(PyJobOrderListMapper.looksLikePackagingUom("樽裝用")) + assertFalse(PyJobOrderListMapper.looksLikePackagingUom("餐廳用")) + assertFalse(PyJobOrderListMapper.looksLikePackagingUom("無糖")) + assertFalse(PyJobOrderListMapper.looksLikePackagingUom("P+4")) + assertFalse(PyJobOrderListMapper.looksLikePackagingUom("P+10")) + } + + @Test + fun buildDisplayItemName_appends_uom_when_parens_are_not_packaging() { + assertEquals( + "芝士醬(菠菜用)($stockUom)", + PyJobOrderListMapper.buildDisplayItemName("芝士醬(菠菜用)", stockUom), + ) + assertEquals( + "(熟)大冬菇($stockUom)", + PyJobOrderListMapper.buildDisplayItemName("(熟)大冬菇", stockUom), + ) + assertEquals( + "鮮檸檬汁(P+4)($stockUom)", + PyJobOrderListMapper.buildDisplayItemName("鮮檸檬汁(P+4)", stockUom), + ) + } + + @Test + fun buildDisplayItemName_skips_append_when_packaging_uom_already_in_name() { + assertEquals( + "咖哩汁(1包X2磅)", + PyJobOrderListMapper.buildDisplayItemName("咖哩汁(1包X2磅)", stockUom), + ) + assertEquals( + "(樽裝用)凍咖啡底P+10(0.9L/包)", + PyJobOrderListMapper.buildDisplayItemName("(樽裝用)凍咖啡底P+10(0.9L/包)", stockUom), + ) + assertEquals( + "酸辣湯(2磅/包)", + PyJobOrderListMapper.buildDisplayItemName("酸辣湯(2磅/包)", "2磅/包"), + ) + } + + @Test + fun buildDisplayItemName_appends_when_no_parens() { + assertEquals( + "大冬菇($stockUom)", + PyJobOrderListMapper.buildDisplayItemName("大冬菇", stockUom), + ) + } +}