Selaa lähdekoodia

adding onPack related file gen api

reset-do-picking-order
[email protected] 1 viikko sitten
vanhempi
commit
e81a7651fe
4 muutettua tiedostoa jossa 237 lisäystä ja 0 poistoa
  1. +159
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt
  2. +47
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt
  3. +21
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt
  4. +10
    -0
      src/main/resources/db/changelog/changes/20260310_fai/01_onpack.sql

+ 159
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt Näytä tiedosto

@@ -1,8 +1,11 @@
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.web.model.PrintRequest
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 java.awt.Color
import java.awt.Font
@@ -32,6 +35,8 @@ data class BitmapResult(val bytes: ByteArray, val width: Int)
@Service
open class PlasticBagPrinterService(
val jobOrderRepository: JobOrderRepository,
private val jdbcDao: JdbcDao,
private val stockInLineRepository: StockInLineRepository,
) {

fun generatePrintJobBundle(
@@ -142,6 +147,65 @@ open class PlasticBagPrinterService(
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 {
// Step 1: Measure text width with temporary image
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 {
if (totalSize < contentSize) throw IllegalArgumentException("totalSize must be >= contentSize")

@@ -272,6 +403,34 @@ open class PlasticBagPrinterService(
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
// ────────────────────────────────────────────────


+ 47
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt Näytä tiedosto

@@ -4,6 +4,9 @@ import com.ffii.fpsms.modules.jobOrder.service.PlasticBagPrinterService
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.Laser2Request
import com.ffii.fpsms.modules.jobOrder.web.model.OnPackQrDownloadRequest
import com.ffii.fpsms.modules.jobOrder.web.model.PrinterStatusRequest
import com.ffii.fpsms.modules.jobOrder.web.model.PrinterStatusResponse
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpHeaders
import org.springframework.web.bind.annotation.*
@@ -16,6 +19,50 @@ class PlasticBagPrinterController(
private val plasticBagPrinterService: PlasticBagPrinterService,
) {

@PostMapping("/check-printer")
fun checkPrinter(@RequestBody request: PrinterStatusRequest): ResponseEntity<PrinterStatusResponse> {
val (connected, message) = plasticBagPrinterService.checkPrinterConnection(
printerType = request.printerType,
printerIp = request.printerIp,
printerPort = request.printerPort,
labelCom = request.labelCom,
)

val body = PrinterStatusResponse(
connected = connected,
message = message,
)

return if (connected) {
ResponseEntity.ok(body)
} else {
ResponseEntity.status(503).body(body)
}
}

@PostMapping("/download-onpack-qr")
fun downloadOnPackQr(
@RequestBody request: OnPackQrDownloadRequest,
response: HttpServletResponse,
) {
try {
val zipBytes = plasticBagPrinterService.generateOnPackQrZip(request.jobOrders)
response.contentType = "application/zip"
response.setHeader(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"onpack_qr_codes.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


+ 21
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt Näytä tiedosto

@@ -31,4 +31,25 @@ data class Laser2Request(
val textChannel4: String? = null,
val text3ObjectName: String? = "Text3",
val text4ObjectName: String? = "Text4"
)

data class PrinterStatusRequest(
val printerType: String,
val printerIp: String? = null,
val printerPort: Int? = null,
val labelCom: String? = null,
)

data class PrinterStatusResponse(
val connected: Boolean,
val message: String,
)

data class OnPackQrDownloadRequest(
val jobOrders: List<OnPackQrJobOrderRequest>,
)

data class OnPackQrJobOrderRequest(
val jobOrderId: Long,
val itemCode: String,
)

+ 10
- 0
src/main/resources/db/changelog/changes/20260310_fai/01_onpack.sql Näytä tiedosto

@@ -0,0 +1,10 @@
--liquibase formatted sql
--changeset fai:onpack_qr

CREATE TABLE `onpack_qr` (
`code` varchar(100) NOT NULL,
`filename` varchar(200) NOT NULL,
PRIMARY KEY (`code`,`filename`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `onpack_qr` VALUES ('PP1074','7ca95d6d3bac41260fd574ef1f3bdeee.bmp'),('PP1082','0f18c99102e492558b58881b6b870bff.bmp'),('PP1137','d52f73249ac72b3e98cc53690d9f32a9.bmp'),('PP1144','0f4099201cb4a745d7cd4060b265b319.bmp'),('PP1152','9a67037ccb6424b1f03074cf4d481756.bmp'),('PP1156','729613796bffee0191efa65cdb1ac56a.bmp'),('PP1178','d01a522c4b1469bb4208f8341253a29d.bmp'),('PP1181','6486634fee7e1115dceb242881598c84.bmp'),('PP1185','4759a46839b9c83b759c443c346b2925.bmp'),('PP1185','c8ad8bf83d9b34fd75dadc0a380920bc.bmp'),('PP1213','5efebf816c4a7900e9958a8f770b5df1.bmp'),('PP1214','b31bc80a6b29a4886afae778c69cf8f6.bmp'),('PP1216','9f51ebf7d98f4b1235d5be8a2e107f20.bmp'),('PP1217','55cb367a93c36a4658a031ef6d90a043.bmp'),('PP1234','264471bab05dd256ff27cde6dc9fc673.bmp'),('PP2214','d401cf78e5cc3a1d592571ceb42da22a.bmp'),('PP2215','d8dc55bf3e2bf26aa121caebbdde8c2d.bmp'),('PP2243','0cea359e7334fd827579af62b7141552.bmp'),('PP2331','d532731e132cb2207cd67ef2944b4001.bmp');

Ladataan…
Peruuta
Tallenna