|
|
|
@@ -69,6 +69,8 @@ data class OnPackZipResult( |
|
|
|
|
|
|
|
private data class OnPackBmpExportItem( |
|
|
|
val codeLower: String, |
|
|
|
/** ZIP / template file stem (e.g. `pp2211` or `pp2211-2` for expiry multi-JO). */ |
|
|
|
val fileStem: String, |
|
|
|
val itemId: Long, |
|
|
|
val stockInLineId: Long, |
|
|
|
val itemCode: String, |
|
|
|
@@ -149,6 +151,19 @@ class PlasticBagPrinterService( |
|
|
|
fun laserAckLooksInvalid(ack: String?): Boolean = |
|
|
|
ack?.contains("invalid", ignoreCase = true) == true |
|
|
|
|
|
|
|
/** Expiry 汁水機 ZIP: one file set per job order, ordered by job order no then id. */ |
|
|
|
fun sortOnPackExportJobOrders( |
|
|
|
orders: List<OnPackQrJobOrderRequest>, |
|
|
|
jobOrdersById: Map<Long, JobOrder>, |
|
|
|
): List<OnPackQrJobOrderRequest> = |
|
|
|
orders.sortedWith( |
|
|
|
compareBy<OnPackQrJobOrderRequest> { jobOrdersById[it.jobOrderId]?.code?.trim().orEmpty() } |
|
|
|
.thenBy { it.jobOrderId }, |
|
|
|
) |
|
|
|
|
|
|
|
fun onPackExpiryFileStem(itemCodeLower: String, indexOneBased: Int): String = |
|
|
|
"$itemCodeLower-$indexOneBased" |
|
|
|
|
|
|
|
/** ISO `yyyy-MM-dd`, compact `yyyyMMdd`, or already `Expiry Date yyyyMMdd`. */ |
|
|
|
fun formatLaserExpiryParam(expiryDate: String?): String { |
|
|
|
val raw = expiryDate?.trim().orEmpty() |
|
|
|
@@ -285,6 +300,36 @@ class PlasticBagPrinterService( |
|
|
|
.toSet() |
|
|
|
} |
|
|
|
|
|
|
|
private fun toOnPackBmpExportItem( |
|
|
|
codeLower: String, |
|
|
|
fileStem: String, |
|
|
|
order: OnPackQrJobOrderRequest, |
|
|
|
expiryPrintNames: Map<String, String>, |
|
|
|
jobOrder: JobOrder?, |
|
|
|
): OnPackBmpExportItem? { |
|
|
|
val stockInLine = stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(order.jobOrderId) |
|
|
|
?: return null |
|
|
|
val itemId = stockInLine.item?.id ?: return null |
|
|
|
val stockInLineId = stockInLine.id ?: return null |
|
|
|
val jo = jobOrder ?: jobOrderRepository.findById(order.jobOrderId).orElse(null) |
|
|
|
val baseName = jo?.bom?.name ?: stockInLine.item?.name |
|
|
|
val stockDesc = itemUomService.findStockUnitByItemId(itemId)?.uom?.udfudesc |
|
|
|
val computedName = PyJobOrderListMapper.buildDisplayItemName(baseName, stockDesc) |
|
|
|
val itemCode = (stockInLine.item?.code ?: stockInLine.itemNo ?: codeLower).trim().uppercase() |
|
|
|
val productName = expiryPrintNames[itemCode]?.trim()?.takeIf { it.isNotEmpty() } |
|
|
|
?: computedName |
|
|
|
?: itemCode |
|
|
|
return OnPackBmpExportItem( |
|
|
|
codeLower = codeLower, |
|
|
|
fileStem = fileStem, |
|
|
|
itemId = itemId, |
|
|
|
stockInLineId = stockInLineId, |
|
|
|
itemCode = itemCode, |
|
|
|
productName = productName, |
|
|
|
planDate = jo?.planStart?.toLocalDate(), |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Bag2.py [send_job_to_laser] / [send_job_to_laser_with_retry]: UTF-8 TCP payload and optional ack read. |
|
|
|
*/ |
|
|
|
@@ -725,30 +770,23 @@ class PlasticBagPrinterService( |
|
|
|
emptyMap() |
|
|
|
} |
|
|
|
|
|
|
|
val jobOrdersById = jobOrderRepository.findAllById( |
|
|
|
packagingJobOrders.map { it.jobOrderId }.distinct(), |
|
|
|
).associateBy { it.id!! } |
|
|
|
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 |
|
|
|
val jo = jobOrderRepository.findById(order.jobOrderId).orElse(null) |
|
|
|
val baseName = jo?.bom?.name ?: stockInLine.item?.name |
|
|
|
val stockDesc = itemUomService.findStockUnitByItemId(itemId)?.uom?.udfudesc |
|
|
|
val computedName = PyJobOrderListMapper.buildDisplayItemName(baseName, stockDesc) |
|
|
|
val itemCode = (stockInLine.item?.code ?: stockInLine.itemNo ?: codeLower).trim().uppercase() |
|
|
|
val productName = expiryPrintNames[itemCode]?.trim()?.takeIf { it.isNotEmpty() } |
|
|
|
?: computedName |
|
|
|
?: itemCode |
|
|
|
OnPackBmpExportItem( |
|
|
|
codeLower, |
|
|
|
itemId, |
|
|
|
stockInLineId, |
|
|
|
itemCode, |
|
|
|
productName, |
|
|
|
jo?.planStart?.toLocalDate(), |
|
|
|
) |
|
|
|
.flatMap { (codeLower, orders) -> |
|
|
|
val sorted = sortOnPackExportJobOrders(orders, jobOrdersById) |
|
|
|
val selected = if (includeExpiry) sorted else listOfNotNull(sorted.firstOrNull()) |
|
|
|
selected.mapIndexedNotNull { index, order -> |
|
|
|
toOnPackBmpExportItem( |
|
|
|
codeLower = codeLower, |
|
|
|
fileStem = if (includeExpiry) onPackExpiryFileStem(codeLower, index + 1) else codeLower, |
|
|
|
order = order, |
|
|
|
expiryPrintNames = expiryPrintNames, |
|
|
|
jobOrder = jobOrdersById[order.jobOrderId], |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
require(exportItemsRaw.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" } |
|
|
|
@@ -808,12 +846,12 @@ class PlasticBagPrinterService( |
|
|
|
ZipOutputStream(baos).use { zos -> |
|
|
|
val addedEntries = linkedSetOf<String>() |
|
|
|
exportItems.forEach { item -> |
|
|
|
val codeLower = item.codeLower |
|
|
|
val fileStem = item.fileStem |
|
|
|
val imageTemplate = if (includeExpiry) { |
|
|
|
val master = expiryMasterImage ?: return@forEach |
|
|
|
OnPackPp1181Master.rewriteImageBytes(master, codeLower) |
|
|
|
OnPackPp1181Master.rewriteImageBytes(master, fileStem) |
|
|
|
} else { |
|
|
|
loadOnPackImageTemplateOrNull(codeLower, forExpiry = false) ?: return@forEach |
|
|
|
loadOnPackImageTemplateOrNull(item.codeLower, forExpiry = false) ?: return@forEach |
|
|
|
} |
|
|
|
|
|
|
|
val qrContent = """{"itemId": ${item.itemId}, "stockInLineId": ${item.stockInLineId}}""" |
|
|
|
@@ -821,13 +859,13 @@ class PlasticBagPrinterService( |
|
|
|
// Width = 386 + (42 * 2) = 470 |
|
|
|
// Height = 386 + (1 * 2) = 388 (~389) |
|
|
|
val bmp = createQrCodeBitmap(qrContent, contentSize = 386, horizontalPadding = 42, verticalPadding = 1) |
|
|
|
val qrBmpFileName = "${codeLower}qr.bmp" |
|
|
|
val imageFileName = "$codeLower.image" |
|
|
|
val qrBmpFileName = "${fileStem}qr.bmp" |
|
|
|
val imageFileName = "$fileStem.image" |
|
|
|
var imageContent = withOnPackLogo4Bmp(imageTemplate, qrBmpFileName) |
|
|
|
if (includeExpiry) { |
|
|
|
val productFile = "${codeLower}Product.bmp" |
|
|
|
val codeFile = "${codeLower}Code.bmp" |
|
|
|
val dateFile = "${codeLower}Date.bmp" |
|
|
|
val productFile = "${fileStem}Product.bmp" |
|
|
|
val codeFile = "${fileStem}Code.bmp" |
|
|
|
val dateFile = "${fileStem}Date.bmp" |
|
|
|
val productBmp = createMonochromeBitmapFixed( |
|
|
|
item.productName, |
|
|
|
ONPACK_PRODUCT_BMP_WIDTH, |
|
|
|
@@ -857,7 +895,7 @@ class PlasticBagPrinterService( |
|
|
|
} |
|
|
|
} |
|
|
|
val expiryLabel = if (includeExpiry) { |
|
|
|
itemDefaultShelfLifeService.expiryDatePrintLabel(codeLower, effectivePrintDate) |
|
|
|
itemDefaultShelfLifeService.expiryDatePrintLabel(item.itemCode, effectivePrintDate) |
|
|
|
} else { |
|
|
|
null |
|
|
|
} |
|
|
|
@@ -867,7 +905,7 @@ class PlasticBagPrinterService( |
|
|
|
ONPACK_EXPIRY_BMP_WIDTH, |
|
|
|
ONPACK_EXPIRY_BMP_HEIGHT, |
|
|
|
) |
|
|
|
val expBmpFileName = "${codeLower}exp.bmp" |
|
|
|
val expBmpFileName = "${fileStem}exp.bmp" |
|
|
|
imageContent = withOnPackExpiryLogo(imageContent, expBmpFileName, expBmp.width) |
|
|
|
if (addedEntries.add(expBmpFileName)) { |
|
|
|
addToZip(zos, expBmpFileName, expBmp.bytes) |
|
|
|
@@ -893,7 +931,7 @@ class PlasticBagPrinterService( |
|
|
|
addToZip(zos, bmpName, bmpBytes) |
|
|
|
} |
|
|
|
} |
|
|
|
val jobFileName = "${codeLower}.job" |
|
|
|
val jobFileName = "$fileStem.job" |
|
|
|
if (includeExpiry) { |
|
|
|
val masterJob = expiryMasterJob |
|
|
|
if (masterJob != null && addedEntries.add(jobFileName)) { |
|
|
|
|