Procházet zdrojové kódy

refining the Purchase Order from m18 syn, onpack 2nd machine template files and zip export

master
Fai Luk před 1 dnem
rodič
revize
a8e47e4829
40 změnil soubory, kde provedl 400 přidání a 35 odebrání
  1. +9
    -0
      src/main/java/com/ffii/fpsms/m18/M18GrnRules.kt
  2. +2
    -0
      src/main/java/com/ffii/fpsms/m18/model/M18PurchaseOrderResponse.kt
  3. +47
    -2
      src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt
  4. +5
    -2
      src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt
  5. +2
    -1
      src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt
  6. +5
    -0
      src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt
  7. +242
    -2
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt
  8. +24
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt
  9. +3
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt
  10. +3
    -1
      src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt
  11. +4
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrder.kt
  12. +1
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt
  13. +2
    -1
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/model/SavePurchaseOrderRequest.kt
  14. +4
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  15. +6
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  16. +17
    -26
      src/main/resources/FGStockInLabel/FGStockInLabel.jrxml
  17. +7
    -0
      src/main/resources/db/changelog/changes/20260324_02_fp/01_update_purchase_order_m18_created_uid.sql
  18. +5
    -0
      src/main/resources/db/changelog/changes/20260325_onpack_qr_template_type.sql
  19. +12
    -0
      src/main/resources/db/changelog/changes/20260326_onpack_qr_text_rows.sql
  20. binární
      src/main/resources/onpack2030_2/29BF30F0E9FA4F800147936FC6B1DCFE.bmp
  21. binární
      src/main/resources/onpack2030_2/33D5006F86DAFDE8BA743C4C55DEAEDA.bmp
  22. binární
      src/main/resources/onpack2030_2/72CB65B12DF2E8E4A2698BB3DAE8155A.bmp
  23. binární
      src/main/resources/onpack2030_2/731813F5BB5E82176C254DDAF620529A.bmp
  24. binární
      src/main/resources/onpack2030_2/8CF3057D15F4C2931D361668CCCB66A9.bmp
  25. binární
      src/main/resources/onpack2030_2/93628E5851758D32BCA25B00602F6D2E.bmp
  26. binární
      src/main/resources/onpack2030_2/A49477813D8529AAE0442F3BCD81327A.bmp
  27. binární
      src/main/resources/onpack2030_2/PP1175.image
  28. binární
      src/main/resources/onpack2030_2/PP2236.image
  29. binární
      src/main/resources/onpack2030_2/PP2237.image
  30. binární
      src/main/resources/onpack2030_2/PP2238.image
  31. binární
      src/main/resources/onpack2030_2/PP2239.image
  32. binární
      src/main/resources/onpack2030_2/PP2336.image
  33. binární
      src/main/resources/onpack2030_2/PP2338.image
  34. binární
      src/main/resources/onpack2030_2/pp1175.job
  35. binární
      src/main/resources/onpack2030_2/pp2236.job
  36. binární
      src/main/resources/onpack2030_2/pp2237.job
  37. binární
      src/main/resources/onpack2030_2/pp2238.job
  38. binární
      src/main/resources/onpack2030_2/pp2239.job
  39. binární
      src/main/resources/onpack2030_2/pp2336.job
  40. binární
      src/main/resources/onpack2030_2/pp2338.job

+ 9
- 0
src/main/java/com/ffii/fpsms/m18/M18GrnRules.kt Zobrazit soubor

@@ -0,0 +1,9 @@
package com.ffii.fpsms.m18

/**
* M18 PO [createUid] is stored on [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder.m18CreatedUId].
* For these M18 user ids, FPSMS does not post GRN to M18 (see [com.ffii.fpsms.modules.stock.service.StockInLineService]).
*/
object M18GrnRules {
val SKIP_GRN_FOR_M18_CREATED_UIDS: Set<Long> = setOf(2569L, 2676L)
}

+ 2
- 0
src/main/java/com/ffii/fpsms/m18/model/M18PurchaseOrderResponse.kt Zobrazit soubor

@@ -19,6 +19,8 @@ data class M18PurchaseOrderData (
data class M18PurchaseOrderMainPo (
val id: Long,
val code: String,
/** M18 user id who created the PO (persisted in [com.ffii.fpsms.m18.entity.M18DataLog.dataLog] via sync reflection). */
val createUid: Long? = null,
/** Supplier Id */
val venId: Long,
/** ETA */


+ 47
- 2
src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt Zobrazit soubor

@@ -151,9 +151,54 @@ open class M18DeliveryOrderService(
return deliveryOrder
}

open fun saveDeliveryOrders(request: M18CommonRequest) : SyncResult{
logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------")
open fun saveDeliveryOrders(request: M18CommonRequest): SyncResult {
val deliveryOrdersWithType = getDeliveryOrdersWithType(request)
return saveDeliveryOrdersWithPreparedList(deliveryOrdersWithType)
}

/**
* Sync a single M18 shop PO / delivery order by document [code], same search pattern as
* [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode].
*/
open fun saveDeliveryOrderByCode(code: String): SyncResult {
val searchRequest = M18PurchaseOrderListRequest(
stSearch = "po",
params = null,
conds = "(code=equal=$code)"
)
val doListResponse = try {
apiCallerService.get<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>(
M18_FETCH_PURCHASE_ORDER_LIST_API,
searchRequest
).block()
} catch (e: Exception) {
logger.error("(Getting DO list By Code) Error on Function - ${e.stackTrace}")
logger.error(e.message)
null
}

val doValues = doListResponse?.values
if (doValues.isNullOrEmpty()) {
return SyncResult(
totalProcessed = 1,
totalSuccess = 0,
totalFail = 1,
query = "code=equal=$code"
)
}

val prepared = M18PurchaseOrderListResponseWithType(
valuesWithType = mutableListOf(Pair(PurchaseOrderType.SHOP, doListResponse)),
query = "code=equal=$code"
)

return saveDeliveryOrdersWithPreparedList(prepared)
}

private fun saveDeliveryOrdersWithPreparedList(
deliveryOrdersWithType: M18PurchaseOrderListResponseWithType?
): SyncResult {
logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------")

val successList = mutableListOf<Long>()
val successDetailList = mutableListOf<Long>()


+ 5
- 2
src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt Zobrazit soubor

@@ -253,6 +253,9 @@ open class M18MasterDataService(
shouldSyncRatioFromCunitByLocalUomId(localBaseId, p.ratioN, p.ratioD)
} == true)
price?.forEach {
val endMillis = it.endDate
val endInstant = Instant.ofEpochMilli(endMillis)
val now = Instant.now()
val localUomId = uomConversionService.findByM18Id(it.unitId)?.id
val keepOriginalItemRatio = forceCunitForAllRows
val useUnitRatios = forceCunitForAllRows
@@ -278,7 +281,7 @@ open class M18MasterDataService(
ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN,
itemRatioD = if (keepOriginalItemRatio) it.ratioD else null,
itemRatioN = if (keepOriginalItemRatio) it.ratioN else null,
deleted = it.expired
deleted = it.expired || endInstant.isBefore(now)
)

// logger.info("saved item id: ${savedItem.id}")
@@ -414,7 +417,7 @@ open class M18MasterDataService(
ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN,
itemRatioD = if (keepOriginalItemRatio) it.ratioD else null,
itemRatioN = if (keepOriginalItemRatio) it.ratioN else null,
deleted = endInstant.isBefore(now)
deleted = it.expired || endInstant.isBefore(now)
)

itemUomService.saveItemUom(itemUomRequest)


+ 2
- 1
src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt Zobrazit soubor

@@ -356,7 +356,8 @@ open class M18PurchaseOrderService(
status = PurchaseOrderStatus.PENDING.value,
type = resolvePoTypeByBeId(mainpo.beId).value,
m18DataLogId = saveM18PurchaseOrderLog.id,
m18BeId = mainpo.beId
m18BeId = mainpo.beId,
m18CreatedUId = mainpo.createUid,
)

val savePurchaseOrderResponse =


+ 5
- 0
src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt Zobrazit soubor

@@ -69,6 +69,11 @@ class M18TestController (
fun testSyncPoByCode(@RequestParam code: String): SyncResult {
return m18PurchaseOrderService.savePurchaseOrderByCode(code)
}

@GetMapping("/test/do-by-code")
fun testSyncDoByCode(@RequestParam code: String): SyncResult {
return m18DeliveryOrderService.saveDeliveryOrderByCode(code)
}
// --------------------------------------------- Scheduler --------------------------------------------- ///
// @GetMapping("/schedule/po")
// fun schedulePo(@RequestParam @Valid newCron: String) {


+ 242
- 2
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt Zobrazit soubor

@@ -31,6 +31,7 @@ import java.io.InputStreamReader
import java.net.ConnectException
import java.net.SocketTimeoutException
import org.springframework.core.io.ClassPathResource
import org.slf4j.LoggerFactory

// Data class to store bitmap bytes + width (for XML)
data class BitmapResult(val bytes: ByteArray, val width: Int)
@@ -41,6 +42,7 @@ open class PlasticBagPrinterService(
private val jdbcDao: JdbcDao,
private val stockInLineRepository: StockInLineRepository,
) {
private val logger = LoggerFactory.getLogger(javaClass)

fun generatePrintJobBundle(
itemCode: String,
@@ -186,7 +188,7 @@ open class PlasticBagPrinterService(
val packagingJobOrders = normalizedJobOrders.filter { it.jobOrderId in allowedJobOrderIds }
require(packagingJobOrders.isNotEmpty()) { "No 包裝 process job orders found for export" }

val exportItems = packagingJobOrders
val exportItemsRaw = packagingJobOrders
.groupBy { it.itemCode.trim().lowercase() }
.mapNotNull { (codeLower, orders) ->
val order = orders.firstOrNull() ?: return@mapNotNull null
@@ -197,7 +199,13 @@ open class PlasticBagPrinterService(
Triple(codeLower, itemId, stockInLineId)
}

require(exportItems.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" }
require(exportItemsRaw.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" }

val codesUpper = exportItemsRaw.map { it.first.uppercase() }.toSet()
val allowedBmpCodes = codesOnPackMatchingTemplateType(codesUpper, "bmp")
val exportItems = exportItemsRaw.filter { allowedBmpCodes.contains(it.first.uppercase()) }

require(exportItems.isNotEmpty()) { "No OnPack QR (bmp) rows in onpack_qr for the selected job orders, or no matching templates" }

val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zos ->
@@ -228,6 +236,124 @@ open class PlasticBagPrinterService(
return baos.toByteArray()
}

/**
* OnPack2023 檸檬機: templates under classpath `onpack2030_2/{code}.image` with embedded QR (Static text).
* Only replaces `<Text>...</Text>` under `<Static type="StaticSrc.V1">` with JSON payload. No separate .bmp in zip.
*/
fun generateOnPackQrTextZip(jobOrders: List<OnPackQrJobOrderRequest>): ByteArray {
val normalizedJobOrders = jobOrders
.map {
OnPackQrJobOrderRequest(
jobOrderId = it.jobOrderId,
itemCode = it.itemCode.trim(),
)
}
.filter { it.jobOrderId > 0 && it.itemCode.isNotBlank() }

require(normalizedJobOrders.isNotEmpty()) { "No job orders provided" }

val requestedJobOrderIds = normalizedJobOrders.map { it.jobOrderId }.distinct()
val allowedRows = jdbcDao.queryForList(
"""
SELECT DISTINCT jo.id AS jobOrderId
FROM job_order jo
JOIN bom b ON b.id = jo.bomId
JOIN bom_process bp ON bp.bomId = b.id
JOIN process p ON p.id = bp.processId
WHERE jo.id IN (:jobOrderIds)
AND jo.deleted = 0
AND p.name = :processName
""".trimIndent(),
mapOf(
"jobOrderIds" to requestedJobOrderIds,
"processName" to "包裝",
)
)
val allowedJobOrderIds = allowedRows
.mapNotNull { (it["jobOrderId"] as? Number)?.toLong() }
.toSet()
val packagingJobOrders = normalizedJobOrders.filter { it.jobOrderId in allowedJobOrderIds }
require(packagingJobOrders.isNotEmpty()) { "No 包裝 process job orders found for export" }

val exportItemsRaw = packagingJobOrders
.groupBy { it.itemCode.trim().lowercase() }
.mapNotNull { (codeLower, orders) ->
val order = orders.firstOrNull() ?: return@mapNotNull null
val stockInLine = stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(order.jobOrderId)
?: return@mapNotNull null
val itemId = stockInLine.item?.id ?: return@mapNotNull null
val stockInLineId = stockInLine.id ?: return@mapNotNull null
Triple(codeLower, itemId, stockInLineId)
}

require(exportItemsRaw.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" }

val codesUpper = exportItemsRaw.map { it.first.uppercase() }.toSet()
val allowedTextCodes = codesOnPackMatchingTemplateType(codesUpper, "text")
val exportItems = exportItemsRaw.filter { allowedTextCodes.contains(it.first.uppercase()) }

require(exportItems.isNotEmpty()) { "No OnPack QR (text) rows in onpack_qr for the selected job orders, or no matching templates" }

val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zos ->
val addedEntries = linkedSetOf<String>()
exportItems.forEach { (codeLower, itemId, stockInLineId) ->
val imageTemplate = loadOnPack2030_2ImageTemplateOrNull(codeLower) ?: run {
logger.warn("OnPack text ZIP: missing classpath template onpack2030_2/{}.image", codeLower)
return@forEach
}
val imageFileName = "$codeLower.image"
val imageContent = withOnPackStaticQrText(codeLower, imageTemplate, itemId, stockInLineId)
if (addedEntries.add(imageFileName)) {
addToZip(zos, imageFileName, imageContent)
}
val decodedXmlForAssets = decodeOnPackImageTemplateForTextEdit(imageTemplate).first
extractLogoBmpFileNamesFromOnPackImageXml(decodedXmlForAssets).forEach { bmpName ->
if (!addedEntries.add(bmpName)) return@forEach
val bmpBytes = loadOnPack2030_2AssetOrNull(bmpName) ?: run {
logger.warn("OnPack text ZIP: missing classpath asset onpack2030_2/{}", bmpName)
return@forEach
}
addToZip(zos, bmpName, bmpBytes)
}
val jobFileName = "$codeLower.job"
loadOnPack2030_2AssetOrNull(jobFileName)?.let { jobBytes ->
if (addedEntries.add(jobFileName)) {
addToZip(zos, jobFileName, jobBytes)
}
}
}
require(addedEntries.isNotEmpty()) { "No OnPack text template files could be generated for the selected date" }
}
return baos.toByteArray()
}

/**
* Returns uppercase item codes present in `onpack_qr` with the given [templateType] (`bmp` or `text`).
* Empty or NULL `template_type` is treated as `bmp` for backward compatibility.
*/
private fun codesOnPackMatchingTemplateType(codesUpper: Set<String>, templateType: String): Set<String> {
if (codesUpper.isEmpty()) return emptySet()
val normalizedType = templateType.trim().lowercase()
val sql = when (normalizedType) {
"bmp" -> """
SELECT DISTINCT UPPER(TRIM(code)) AS code
FROM onpack_qr
WHERE UPPER(TRIM(code)) IN (:codes)
AND COALESCE(NULLIF(TRIM(template_type), ''), 'bmp') = 'bmp'
""".trimIndent()
"text" -> """
SELECT DISTINCT UPPER(TRIM(code)) AS code
FROM onpack_qr
WHERE UPPER(TRIM(code)) IN (:codes)
AND LOWER(TRIM(template_type)) = 'text'
""".trimIndent()
else -> return emptySet()
}
val rows = jdbcDao.queryForList(sql, mapOf("codes" to codesUpper.toList()))
return rows.mapNotNull { it["code"]?.toString()?.trim()?.uppercase() }.toSet()
}

private fun loadOnPackImageTemplateOrNull(codeLower: String): ByteArray? {
val resourcePath = "onpack2030/${codeLower}.image"
val resource = ClassPathResource(resourcePath)
@@ -235,6 +361,31 @@ open class PlasticBagPrinterService(
return resource.inputStream.use { it.readBytes() }
}

private fun loadOnPack2030_2ImageTemplateOrNull(codeLower: String): ByteArray? {
val resourcePath = "onpack2030_2/${codeLower}.image"
val resource = ClassPathResource(resourcePath)
if (!resource.exists()) return null
return resource.inputStream.use { it.readBytes() }
}

private fun loadOnPack2030_2AssetOrNull(fileName: String): ByteArray? {
val safe = fileName.trim().replace(Regex("""[\\/]+"""), "").ifBlank { return null }
val resource = ClassPathResource("onpack2030_2/$safe")
if (!resource.exists()) return null
return resource.inputStream.use { it.readBytes() }
}

/** Collect `<FileName>xxx.bmp</FileName>` inside each `<Logo>...</Logo>` block (decoded template XML). */
private fun extractLogoBmpFileNamesFromOnPackImageXml(xml: String): Set<String> {
val out = linkedSetOf<String>()
Regex("""(?s)<Logo[^>]*>[\s\S]*?</Logo>""").findAll(xml).forEach { block ->
Regex("""<FileName>([^<]+\.bmp)</FileName>""", RegexOption.IGNORE_CASE).findAll(block.value).forEach { m ->
out.add(m.groupValues[1].trim())
}
}
return out
}

private fun withOnPackLogo4Bmp(imageBytes: ByteArray, qrBmpFileName: String): ByteArray {
// Use ISO-8859-1 one-byte mapping so all original bytes are preserved,
// while replacing only ASCII XML fragment for LOGO_4 filename.
@@ -246,6 +397,95 @@ open class PlasticBagPrinterService(
return replaced.toByteArray(StandardCharsets.ISO_8859_1)
}

/**
* `.image` templates from Windows tools are often UTF-16 LE with BOM; decoding as ISO-8859-1 breaks
* substring/regex matching (`hasNameQr=false` while XML is valid).
*/
private fun decodeOnPackImageTemplateForTextEdit(bytes: ByteArray): Pair<String, (String) -> ByteArray> {
val bomUtf16Le = byteArrayOf(0xFF.toByte(), 0xFE.toByte())
val bomUtf16Be = byteArrayOf(0xFE.toByte(), 0xFF.toByte())
val bomUtf8 = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte())

when {
bytes.size >= 2 && bytes[0] == bomUtf16Le[0] && bytes[1] == bomUtf16Le[1] -> {
val body = bytes.copyOfRange(2, bytes.size)
val text = String(body, StandardCharsets.UTF_16LE)
return text to { s -> bomUtf16Le + s.toByteArray(StandardCharsets.UTF_16LE) }
}
bytes.size >= 2 && bytes[0] == bomUtf16Be[0] && bytes[1] == bomUtf16Be[1] -> {
val body = bytes.copyOfRange(2, bytes.size)
val text = String(body, StandardCharsets.UTF_16BE)
return text to { s -> bomUtf16Be + s.toByteArray(StandardCharsets.UTF_16BE) }
}
bytes.size >= 3 && bytes[0] == bomUtf8[0] && bytes[1] == bomUtf8[1] && bytes[2] == bomUtf8[2] -> {
val body = bytes.copyOfRange(3, bytes.size)
val text = String(body, StandardCharsets.UTF_8)
return text to { s -> bomUtf8 + s.toByteArray(StandardCharsets.UTF_8) }
}
// UTF-16 LE without BOM: "<" == 0x3C 0x00
bytes.size >= 2 && bytes[0] == 0x3C.toByte() && bytes[1] == 0x00.toByte() -> {
val text = String(bytes, StandardCharsets.UTF_16LE)
return text to { s -> s.toByteArray(StandardCharsets.UTF_16LE) }
}
else -> {
val utf8 = String(bytes, StandardCharsets.UTF_8)
return utf8 to { s -> s.toByteArray(StandardCharsets.UTF_8) }
}
}
}

private fun withOnPackStaticQrText(forCode: String, imageBytes: ByteArray, itemId: Long, stockInLineId: Long): ByteArray {
val payload = """{"itemId": $itemId, "stockInLineId": $stockInLineId}"""
val (oneByteText, encodeBack) = decodeOnPackImageTemplateForTextEdit(imageBytes)
// Must NOT use Static[^>]*StaticSrc — [^>]* already eats type='StaticSrc.V1', so StaticSrc can never match.
// Match the attribute form: <Static type='StaticSrc.V1'> or type="StaticSrc.V1"
val staticQrTextOpen =
"""<Static\s+type=(?:'|\u0022)StaticSrc\.V1(?:'|\u0022)[^>]*>\s*<Text>"""
val regexWithComment = Regex(
"""(?s)(<!--\s*2\.\s*QR\s+Code\s*-\s*Change\s+this\s+part\s+daily\s+for\s+dynamic\s+JSON\s*-->[\s\S]*?<Name>QR</Name>[\s\S]*?)($staticQrTextOpen)([^<]*)(</Text>)""",
)
val regexFallbackNameQr = Regex(
"""(?s)(<Name>QR</Name>[\s\S]*?)($staticQrTextOpen)([^<]*)(</Text>)""",
)
val afterComment = oneByteText.replace(regexWithComment, "$1$2$payload$4")
val usedComment = afterComment !== oneByteText
val replaced = if (usedComment) {
afterComment
} else {
oneByteText.replace(regexFallbackNameQr, "$1$2$payload$4")
}

if (replaced === oneByteText) {
logger.warn(
"OnPack text QR: NO regex match for code={} itemId={} stockInLineId={} templateBytes={} hasNameQr={} hasStaticSrc={} staticPatternSample={}",
forCode,
itemId,
stockInLineId,
imageBytes.size,
oneByteText.contains("<Name>QR</Name>"),
oneByteText.contains("StaticSrc.V1"),
previewOneLineForLog(oneByteText, 600),
)
}
val withQrCellWidth = replaceQrV1CellWidth67To50(replaced)
return encodeBack(withQrCellWidth)
}

/** First `<CellWidth>67</CellWidth>` inside `<QR type='QR.V1'>` → 50 (export tuning). */
private fun replaceQrV1CellWidth67To50(xml: String): String {
return Regex(
"""(?s)(<QR\s+type=['"]QR\.V1['"][^>]*>[\s\S]*?<CellWidth>)(67)(</CellWidth>)""",
).replace(xml) { m ->
"${m.groupValues[1]}50${m.groupValues[3]}"
}
}

/** Single-line preview for logs (avoids multiline noise). */
private fun previewOneLineForLog(s: String, maxLen: Int): String {
val collapsed = s.replace(Regex("\\s+"), " ").trim()
return if (collapsed.length <= maxLen) collapsed else collapsed.take(maxLen) + "…"
}

private fun createMonochromeBitmap(text: String, targetHeight: Int): BitmapResult {
// Step 1: Measure text width with temporary image
val tempImg = BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY)


+ 24
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt Zobrazit soubor

@@ -63,6 +63,30 @@ class PlasticBagPrinterController(
}
}

/** OnPack2023 檸檬機: `onpack2030_2` templates with embedded QR (Static text only; no separate .bmp). */
@PostMapping("/download-onpack-qr-text")
fun downloadOnPackQrText(
@RequestBody request: OnPackQrDownloadRequest,
response: HttpServletResponse,
) {
try {
val zipBytes = plasticBagPrinterService.generateOnPackQrTextZip(request.jobOrders)
response.contentType = "application/zip"
response.setHeader(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"onpack2023_lemon_qr.zip\""
)
response.setContentLength(zipBytes.size)
response.outputStream.write(zipBytes)
response.outputStream.flush()
} catch (e: IllegalArgumentException) {
response.status = HttpServletResponse.SC_BAD_REQUEST
response.contentType = "text/plain;charset=UTF-8"
response.writer.write(e.message ?: "Invalid request")
response.writer.flush()
}
}

/**
* Test API to generate and download the printer job files as a ZIP.
* ONPACK2030


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt Zobrazit soubor

@@ -13,6 +13,9 @@ interface ItemUomRespository : AbstractRepository<ItemUom, Long> {

fun findByM18IdAndDeletedIsFalse(m18Id: Serializable): ItemUom?

/** Includes soft-deleted rows — used by [com.ffii.fpsms.modules.master.service.ItemUomService.saveItemUom] to reactivate. */
fun findFirstByM18IdOrderByIdDesc(m18Id: Long): ItemUom?

fun deleteAllByIdIn(id: List<Serializable>)

fun findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(itemId: Serializable): ItemUom?


+ 3
- 1
src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt Zobrazit soubor

@@ -205,7 +205,9 @@ open class ItemUomService(

// See if need to update the response
open fun saveItemUom(request: ItemUomRequest): ItemUom {
val itemUom = request.m18Id?.let { findByM18Id(it) } ?: request.id?.let { findById(it) } ?: ItemUom()
val itemUom = request.m18Id?.let { itemUomRespository.findFirstByM18IdOrderByIdDesc(it) }
?: request.id?.let { itemUomRespository.findById(it).orElse(null) }
?: ItemUom()

//if (request.m18LastModifyDate == itemUom.m18LastModifyDate) {
// logger.info("Skipping update - m18LastModifyDate unchanged")


+ 4
- 0
src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrder.kt Zobrazit soubor

@@ -59,4 +59,8 @@ open class PurchaseOrder : BaseEntity<Long>() {

@Column(name = "m18BeId")
open var m18BeId: Long? = null

/** M18 PO header [createUid] from sync; used for GRN rules (see [com.ffii.fpsms.m18.M18GrnRules]). */
@Column(name = "m18CreatedUId")
open var m18CreatedUId: Long? = null
}

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt Zobrazit soubor

@@ -352,6 +352,7 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> {
this.type = type
this.m18DataLog = m18DataLog
m18BeId = request.m18BeId
m18CreatedUId = request.m18CreatedUId
}

val savedPurchaseOrder = purchaseOrderRepository.saveAndFlush(purchaseOrder).let {


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/model/SavePurchaseOrderRequest.kt Zobrazit soubor

@@ -19,5 +19,6 @@ data class SavePurchaseOrderRequest (
val status: String?,
val type: String?,
val m18DataLogId: Long?,
val m18BeId: Long?
val m18BeId: Long?,
val m18CreatedUId: Long? = null,
)

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Zobrazit soubor

@@ -6,6 +6,7 @@ import net.sf.jasperreports.engine.data.JRMapCollectionDataSource
import java.io.ByteArrayOutputStream
import java.io.InputStream
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.m18.M18GrnRules
import com.ffii.fpsms.modules.master.entity.ShopRepository
import com.ffii.fpsms.modules.master.enums.ShopType
import com.ffii.fpsms.modules.master.service.ItemUomService
@@ -962,10 +963,12 @@ return result
/**
* GRN preview for M18: show both stock qty (acceptedQty) and purchase qty (converted) for a specific receipt date.
* This is a DRY-RUN preview only (does not call M18).
* Excludes POs whose [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder.m18CreatedUId] is in [com.ffii.fpsms.m18.M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS] (same as live GRN posting).
*/
fun searchGrnPreviewM18(receiptDate: String): List<Map<String, Any?>> {
val formatted = receiptDate.replace("/", "-")
val args = mutableMapOf<String, Any>("receiptDate" to formatted)
val skipGrnM18CreatedUidInList = M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS.joinToString(", ")
val sql = """
SELECT
sil.id AS stockInLineId,
@@ -996,6 +999,7 @@ return result
AND DATE(sil.receiptDate) = DATE(:receiptDate)
AND sil.purchaseOrderId IS NOT NULL
AND sil.status = 'completed'
AND (po.m18CreatedUId IS NULL OR po.m18CreatedUId NOT IN ($skipGrnM18CreatedUidInList))
ORDER BY sil.purchaseOrderId, sil.purchaseOrderLineId, sil.id
""".trimIndent()
val rows = jdbcDao.queryForList(sql, args)


+ 6
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Zobrazit soubor

@@ -63,6 +63,7 @@ import com.ffii.fpsms.m18.model.GoodsReceiptNoteAnt
import com.ffii.fpsms.m18.model.GoodsReceiptNoteAntValue
import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLog
import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLogRepository
import com.ffii.fpsms.m18.M18GrnRules
import com.ffii.fpsms.m18.service.M18GoodsReceiptNoteService
import com.ffii.fpsms.m18.service.M18PurchaseOrderService
import com.ffii.fpsms.m18.utils.CommonUtils
@@ -688,6 +689,11 @@ open class StockInLineService(
logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - missing M18 ids for PO id=${savedPo.id} code=${savedPo.code}. m18BeId=${savedPo.m18BeId}, supplier.m18Id=${savedPo.supplier?.m18Id}, currency.m18Id=${savedPo.currency?.m18Id}")
return
}
val m18Created = savedPo.m18CreatedUId
if (m18Created != null && m18Created in M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS) {
logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN - m18CreatedUId=$m18Created in skip list for PO id=${savedPo.id} code=${savedPo.code}")
return
}
//temp comment this TODO will only check purchase order + dnNo duplicated
//if (m18GoodsReceiptNoteLogRepository.existsByPurchaseOrderIdAndStatusTrue(savedPo.id!!)) {
// logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN - already created for PO id=${savedPo.id} code=${savedPo.code} (avoids core_201 duplicate)")


+ 17
- 26
src/main/resources/FGStockInLabel/FGStockInLabel.jrxml Zobrazit soubor

@@ -1,20 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.21.3.final using JasperReports Library version 6.21.3-4a3078d20785ebe464f18037d738d12fc98c13cf -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="425" pageHeight="283" columnWidth="385" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="baa9f270-b398-4f1c-b01e-ba216b7997e9">
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="410" pageHeight="243" columnWidth="380" leftMargin="20" rightMargin="10" topMargin="5" bottomMargin="5" uuid="baa9f270-b398-4f1c-b01e-ba216b7997e9">
<parameter name="jobOrderCode" class="java.lang.String"/>
<parameter name="finishedGoodItemCode" class="java.lang.String"/>
<parameter name="finishedGoodItemName" class="java.lang.String"/>
<parameter name="finishedGoodLotNo" class="java.lang.String"/>
<parameter name="FGStockInQty" class="java.lang.String"/>
<parameter name="FGStockInQty" class="java.lang.String"/>
<parameter name="productionDate" class="java.lang.String"/>
<parameter name="expiryDate" class="java.lang.String"/>
<parameter name="uom" class="java.lang.String"/>
@@ -27,10 +18,10 @@
<band splitType="Stretch"/>
</background>
<detail>
<band height="243" splitType="Stretch">
<band height="233" splitType="Stretch">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<textField>
<reportElement x="80" y="180" width="180" height="30" uuid="8fac39f8-4936-43a5-8e1f-1afbc8ccca9c">
<reportElement x="80" y="172" width="180" height="30" uuid="8fac39f8-4936-43a5-8e1f-1afbc8ccca9c">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
@@ -40,7 +31,7 @@
<textFieldExpression><![CDATA[$P{productionDate}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="180" width="80" height="30" uuid="e03fcb92-259c-4427-a68e-60fe5924d763">
<reportElement x="0" y="172" width="80" height="30" uuid="e03fcb92-259c-4427-a68e-60fe5924d763">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
@@ -79,7 +70,7 @@
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16" isBold="false"/>
</textElement>
<textFieldExpression><![CDATA[$P{FGStockInQty} + $P{shortName} + " (" + $P{uom} +")"]]></textFieldExpression>
<textFieldExpression><![CDATA[$P{FGStockInQty} + $P{shortName} + " (" + $P{uom} +")"]]></textFieldExpression>
</textField>
<line>
<reportElement x="0" y="160" width="380" height="1" uuid="3e37c027-d6e9-4a88-b64d-58ba1dd3b22e">
@@ -106,7 +97,7 @@
<textFieldExpression><![CDATA[$P{finishedGoodLotNo}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="210" width="80" height="30" uuid="93d48729-edea-4acf-beca-00c6221c10bb">
<reportElement x="0" y="202" width="80" height="30" uuid="93d48729-edea-4acf-beca-00c6221c10bb">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
@@ -117,7 +108,7 @@
]]></text>
</staticText>
<textField>
<reportElement x="80" y="210" width="180" height="30" uuid="802555cd-a78b-49c4-b018-61c55f28070c">
<reportElement x="80" y="202" width="180" height="30" uuid="802555cd-a78b-49c4-b018-61c55f28070c">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
@@ -147,15 +138,15 @@
<text><![CDATA[數量:
]]></text>
</staticText>
<image>
<reportElement x="265" y="38" width="115" height="115" uuid="f61ca1c1-333b-42e1-9a00-85f5ef1e3492">
<property name="com.jaspersoft.studio.unit.x" value="px"/>
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<imageExpression><![CDATA[$P{qrCode}]]></imageExpression>
</image>
<image>
<reportElement x="265" y="38" width="115" height="115" uuid="f61ca1c1-333b-42e1-9a00-85f5ef1e3492">
<property name="com.jaspersoft.studio.unit.x" value="px"/>
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<imageExpression><![CDATA[$P{qrCode}]]></imageExpression>
</image>
</band>
</detail>
</jasperReport>

+ 7
- 0
src/main/resources/db/changelog/changes/20260324_02_fp/01_update_purchase_order_m18_created_uid.sql Zobrazit soubor

@@ -0,0 +1,7 @@
-- liquibase formatted sql
-- changeset fp:20260324_purchase_order_m18_created_uid

-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'purchase_order' AND COLUMN_NAME = 'm18CreatedUId'

ALTER TABLE `purchase_order`
ADD COLUMN `m18CreatedUId` BIGINT NULL COMMENT 'M18 PO create user id (mainpo.createUid)' AFTER `m18BeId`;

+ 5
- 0
src/main/resources/db/changelog/changes/20260325_onpack_qr_template_type.sql Zobrazit soubor

@@ -0,0 +1,5 @@
--liquibase formatted sql
--changeset fpsms:onpack_qr_template_type

ALTER TABLE `onpack_qr`
ADD COLUMN `template_type` varchar(20) NOT NULL DEFAULT 'bmp' COMMENT 'bmp: LOGO_4 bmp + onpack2030; text: Static QR text + onpack2030_2' AFTER `filename`;

+ 12
- 0
src/main/resources/db/changelog/changes/20260326_onpack_qr_text_rows.sql Zobrazit soubor

@@ -0,0 +1,12 @@
--liquibase formatted sql
--changeset fpsms:onpack_qr_text_rows

INSERT INTO `onpack_qr` (`code`, `filename`, `template_type`) VALUES
('PP2338', '', 'text'),
('PP1175', '', 'text'),
('PP2236', '', 'text'),
('PP2237', '', 'text'),
('PP2238', '', 'text'),
('PP2239', '', 'text'),
('PP2336', '', 'text')
ON DUPLICATE KEY UPDATE `template_type` = VALUES(`template_type`);

binární
src/main/resources/onpack2030_2/29BF30F0E9FA4F800147936FC6B1DCFE.bmp Zobrazit soubor

Před Za

binární
src/main/resources/onpack2030_2/33D5006F86DAFDE8BA743C4C55DEAEDA.bmp Zobrazit soubor

Před Za

binární
src/main/resources/onpack2030_2/72CB65B12DF2E8E4A2698BB3DAE8155A.bmp Zobrazit soubor

Před Za

binární
src/main/resources/onpack2030_2/731813F5BB5E82176C254DDAF620529A.bmp Zobrazit soubor

Před Za

binární
src/main/resources/onpack2030_2/8CF3057D15F4C2931D361668CCCB66A9.bmp Zobrazit soubor

Před Za

binární
src/main/resources/onpack2030_2/93628E5851758D32BCA25B00602F6D2E.bmp Zobrazit soubor

Před Za

binární
src/main/resources/onpack2030_2/A49477813D8529AAE0442F3BCD81327A.bmp Zobrazit soubor

Před Za

binární
src/main/resources/onpack2030_2/PP1175.image Zobrazit soubor


binární
src/main/resources/onpack2030_2/PP2236.image Zobrazit soubor


binární
src/main/resources/onpack2030_2/PP2237.image Zobrazit soubor


binární
src/main/resources/onpack2030_2/PP2238.image Zobrazit soubor


binární
src/main/resources/onpack2030_2/PP2239.image Zobrazit soubor


binární
src/main/resources/onpack2030_2/PP2336.image Zobrazit soubor


binární
src/main/resources/onpack2030_2/PP2338.image Zobrazit soubor


binární
src/main/resources/onpack2030_2/pp1175.job Zobrazit soubor


binární
src/main/resources/onpack2030_2/pp2236.job Zobrazit soubor


binární
src/main/resources/onpack2030_2/pp2237.job Zobrazit soubor


binární
src/main/resources/onpack2030_2/pp2238.job Zobrazit soubor


binární
src/main/resources/onpack2030_2/pp2239.job Zobrazit soubor


binární
src/main/resources/onpack2030_2/pp2336.job Zobrazit soubor


binární
src/main/resources/onpack2030_2/pp2338.job Zobrazit soubor


Načítá se…
Zrušit
Uložit