|
|
@@ -1,8 +1,11 @@ |
|
|
package com.ffii.fpsms.modules.jobOrder.service |
|
|
package com.ffii.fpsms.modules.jobOrder.service |
|
|
|
|
|
|
|
|
|
|
|
import com.ffii.core.support.JdbcDao |
|
|
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository |
|
|
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository |
|
|
import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest |
|
|
import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest |
|
|
import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest |
|
|
import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest |
|
|
|
|
|
import com.ffii.fpsms.modules.jobOrder.web.model.OnPackQrJobOrderRequest |
|
|
|
|
|
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository |
|
|
import org.springframework.stereotype.Service |
|
|
import org.springframework.stereotype.Service |
|
|
import java.awt.Color |
|
|
import java.awt.Color |
|
|
import java.awt.Font |
|
|
import java.awt.Font |
|
|
@@ -32,6 +35,8 @@ data class BitmapResult(val bytes: ByteArray, val width: Int) |
|
|
@Service |
|
|
@Service |
|
|
open class PlasticBagPrinterService( |
|
|
open class PlasticBagPrinterService( |
|
|
val jobOrderRepository: JobOrderRepository, |
|
|
val jobOrderRepository: JobOrderRepository, |
|
|
|
|
|
private val jdbcDao: JdbcDao, |
|
|
|
|
|
private val stockInLineRepository: StockInLineRepository, |
|
|
) { |
|
|
) { |
|
|
|
|
|
|
|
|
fun generatePrintJobBundle( |
|
|
fun generatePrintJobBundle( |
|
|
@@ -142,6 +147,65 @@ open class PlasticBagPrinterService( |
|
|
return baos.toByteArray() |
|
|
return baos.toByteArray() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fun generateOnPackQrZip(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 normalizedCodes = normalizedJobOrders |
|
|
|
|
|
.map { it.itemCode } |
|
|
|
|
|
.distinct() |
|
|
|
|
|
|
|
|
|
|
|
val sql = """ |
|
|
|
|
|
select code, filename |
|
|
|
|
|
from onpack_qr |
|
|
|
|
|
where code in (:itemCodes) |
|
|
|
|
|
order by code asc |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
|
|
|
|
val rows = jdbcDao.queryForList( |
|
|
|
|
|
sql, |
|
|
|
|
|
mapOf("itemCodes" to normalizedCodes), |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
require(rows.isNotEmpty()) { "No OnPack QR records found for the selected date" } |
|
|
|
|
|
|
|
|
|
|
|
val filenameByCode = rows.associate { row -> |
|
|
|
|
|
row["code"]?.toString()?.trim().orEmpty() to row["filename"]?.toString()?.trim().orEmpty() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
val baos = ByteArrayOutputStream() |
|
|
|
|
|
ZipOutputStream(baos).use { zos -> |
|
|
|
|
|
val addedEntries = linkedSetOf<String>() |
|
|
|
|
|
normalizedJobOrders.forEach { jobOrder -> |
|
|
|
|
|
val filename = filenameByCode[jobOrder.itemCode].orEmpty() |
|
|
|
|
|
if (filename.isBlank()) return@forEach |
|
|
|
|
|
|
|
|
|
|
|
val stockInLine = stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(jobOrder.jobOrderId) |
|
|
|
|
|
?: return@forEach |
|
|
|
|
|
val itemId = stockInLine.item?.id ?: return@forEach |
|
|
|
|
|
val stockInLineId = stockInLine.id ?: return@forEach |
|
|
|
|
|
|
|
|
|
|
|
val qrContent = """{"itemId": $itemId, "stockInLineId": $stockInLineId}""" |
|
|
|
|
|
val bmp = createQrCodeBitmap(qrContent, 600) |
|
|
|
|
|
val zipEntryName = buildUniqueZipEntryName(filename, addedEntries) |
|
|
|
|
|
if (!addedEntries.add(zipEntryName)) return@forEach |
|
|
|
|
|
addToZip(zos, zipEntryName, bmp.bytes) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
require(addedEntries.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" } |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return baos.toByteArray() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private fun createMonochromeBitmap(text: String, targetHeight: Int): BitmapResult { |
|
|
private fun createMonochromeBitmap(text: String, targetHeight: Int): BitmapResult { |
|
|
// Step 1: Measure text width with temporary image |
|
|
// Step 1: Measure text width with temporary image |
|
|
val tempImg = BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY) |
|
|
val tempImg = BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY) |
|
|
@@ -208,6 +272,73 @@ open class PlasticBagPrinterService( |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fun checkPrinterConnection( |
|
|
|
|
|
printerType: String, |
|
|
|
|
|
printerIp: String?, |
|
|
|
|
|
printerPort: Int?, |
|
|
|
|
|
labelCom: String?, |
|
|
|
|
|
): Pair<Boolean, String> { |
|
|
|
|
|
return when (printerType.lowercase()) { |
|
|
|
|
|
"dataflex" -> checkTcpPrinter(printerIp, printerPort ?: 3008, "DataFlex") |
|
|
|
|
|
"laser" -> checkTcpPrinter(printerIp, printerPort ?: 45678, "Laser") |
|
|
|
|
|
"label" -> { |
|
|
|
|
|
val comPort = labelCom?.trim().orEmpty() |
|
|
|
|
|
if (comPort.isBlank()) { |
|
|
|
|
|
false to "Label printer COM port is not configured" |
|
|
|
|
|
} else { |
|
|
|
|
|
checkSerialPrinter(comPort) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
else -> false to "Unsupported printer type: $printerType" |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun checkTcpPrinter(printerIp: String?, port: Int, printerName: String): Pair<Boolean, String> { |
|
|
|
|
|
val ip = printerIp?.trim().orEmpty() |
|
|
|
|
|
if (ip.isBlank()) { |
|
|
|
|
|
return false to "$printerName IP is not configured" |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return try { |
|
|
|
|
|
Socket().use { socket -> |
|
|
|
|
|
socket.connect(InetSocketAddress(ip, port), 3000) |
|
|
|
|
|
true to "$printerName connected" |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e: SocketTimeoutException) { |
|
|
|
|
|
false to "$printerName connection timed out" |
|
|
|
|
|
} catch (e: ConnectException) { |
|
|
|
|
|
false to "$printerName connection refused" |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
false to "$printerName connection failed: ${e.message}" |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun checkSerialPrinter(comPort: String): Pair<Boolean, String> { |
|
|
|
|
|
val normalizedPort = comPort.trim().uppercase() |
|
|
|
|
|
val osName = System.getProperty("os.name")?.lowercase().orEmpty() |
|
|
|
|
|
|
|
|
|
|
|
if (!osName.contains("win")) { |
|
|
|
|
|
return false to "Label printer COM check is only supported on Windows" |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return try { |
|
|
|
|
|
val process = ProcessBuilder("cmd", "/c", "mode", normalizedPort) |
|
|
|
|
|
.redirectErrorStream(true) |
|
|
|
|
|
.start() |
|
|
|
|
|
|
|
|
|
|
|
val output = process.inputStream.bufferedReader().use { it.readText() } |
|
|
|
|
|
val exitCode = process.waitFor() |
|
|
|
|
|
|
|
|
|
|
|
if (exitCode == 0) { |
|
|
|
|
|
true to "Label printer connected on $normalizedPort" |
|
|
|
|
|
} else { |
|
|
|
|
|
false to (output.trim().ifBlank { "Label printer $normalizedPort not available" }) |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
false to "Label printer check failed: ${e.message}" |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private fun createQrCodeBitmap(content: String, contentSize: Int, totalSize: Int = contentSize + 80): BitmapResult { |
|
|
private fun createQrCodeBitmap(content: String, contentSize: Int, totalSize: Int = contentSize + 80): BitmapResult { |
|
|
if (totalSize < contentSize) throw IllegalArgumentException("totalSize must be >= contentSize") |
|
|
if (totalSize < contentSize) throw IllegalArgumentException("totalSize must be >= contentSize") |
|
|
|
|
|
|
|
|
@@ -272,6 +403,34 @@ open class PlasticBagPrinterService( |
|
|
zos.closeEntry() |
|
|
zos.closeEntry() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun sanitizeFilePart(value: String): String { |
|
|
|
|
|
return value.replace(Regex("""[\\/:*?"<>|]"""), "_") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun buildUniqueZipEntryName(filename: String, existingEntries: Set<String>): String { |
|
|
|
|
|
val sanitized = sanitizeFilePart(filename) |
|
|
|
|
|
if (sanitized.isBlank()) { |
|
|
|
|
|
return buildUniqueZipEntryName("qr.bmp", existingEntries) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!existingEntries.contains(sanitized)) { |
|
|
|
|
|
return sanitized |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
val dotIndex = sanitized.lastIndexOf('.') |
|
|
|
|
|
val baseName = if (dotIndex > 0) sanitized.substring(0, dotIndex) else sanitized |
|
|
|
|
|
val extension = if (dotIndex > 0) sanitized.substring(dotIndex) else "" |
|
|
|
|
|
|
|
|
|
|
|
var counter = 2 |
|
|
|
|
|
while (true) { |
|
|
|
|
|
val candidate = "${baseName}_${counter}${extension}" |
|
|
|
|
|
if (!existingEntries.contains(candidate)) { |
|
|
|
|
|
return candidate |
|
|
|
|
|
} |
|
|
|
|
|
counter++ |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// ──────────────────────────────────────────────── |
|
|
// ──────────────────────────────────────────────── |
|
|
// The rest of your printer communication methods remain unchanged |
|
|
// The rest of your printer communication methods remain unchanged |
|
|
// ──────────────────────────────────────────────── |
|
|
// ──────────────────────────────────────────────── |
|
|
|