Procházet zdrojové kódy

adding scheduler for DO1, DO2;

master
[email protected] před 2 týdny
rodič
revize
fa8831693e
10 změnil soubory, kde provedl 347 přidání a 193 odebrání
  1. +37
    -8
      src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt
  2. +38
    -8
      src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt
  3. +3
    -0
      src/main/java/com/ffii/fpsms/m18/web/models/M18TestRequest.kt
  4. +3
    -0
      src/main/java/com/ffii/fpsms/modules/common/SettingNames.java
  5. +77
    -23
      src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt
  6. +142
    -140
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt
  7. +13
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt
  8. +15
    -11
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  9. +16
    -0
      src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql
  10. +3
    -3
      src/main/resources/jasper/FGDeliveryReport.jrxml

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

@@ -57,16 +57,33 @@ open class M18DeliveryOrderService(
open fun getDeliveryOrdersWithType(request: M18CommonRequest): M18PurchaseOrderListResponseWithType? {
val deliveryOrders = M18PurchaseOrderListResponseWithType(mutableListOf())
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val dateFrom = request.modifiedDateFrom?.let {
LocalDateTime.parse(it, formatter).toLocalDate().toString()

val lastDateConds = if (request.modifiedDateFrom != null && request.modifiedDateTo != null) {
val dateFrom = LocalDateTime.parse(request.modifiedDateFrom, formatter).toString()
val dateTo = LocalDateTime.parse(request.modifiedDateTo, formatter).toString()
"lastModifyDate=largerOrEqual=$dateFrom=and=lastModifyDate=lessOrEqual=$dateTo"
} else {
""
}
val dateTo = request.modifiedDateTo?.let {
LocalDateTime.parse(it, formatter).toLocalDate().toString()

val dDateConds = if (request.dDateFrom != null && request.dDateTo != null) {
val dateFrom = LocalDateTime.parse(request.dDateFrom, formatter).toLocalDate().toString()
val dateTo = LocalDateTime.parse(request.dDateTo, formatter).toLocalDate().toString()
"dDate=largerOrEqual=$dateFrom=and=dDate=lessOrEqual=$dateTo"
} else {
""
}
val lastDateConds =
//"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}"
"dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}"

val dDateEqualConds = if (request.dDateEqual != null) {
val dDateEqual = LocalDateTime.parse(request.dDateEqual, formatter).toLocalDate().toString()
"dDate=equal=$dDateEqual"
} else {
""
}
// Shop PO
val shopPoBuyers = commonUtils.listToString(listOf(m18Config.BEID_TOA), "beId=equal=", "=or=")
val shopPoSupplier = commonUtils.listToString(
@@ -74,8 +91,20 @@ open class M18DeliveryOrderService(
"venId=equal=",
"=or="
)
val shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})=and=(${lastDateConds})"
var shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})"

if (request.modifiedDateFrom != null && request.modifiedDateTo != null) {
shopPoConds += "=and=(${lastDateConds})"
}
if (request.dDateFrom != null && request.dDateTo != null) {
shopPoConds += "=and=(${dDateConds})"
}
if (request.dDateEqual != null) {
shopPoConds += "=and=(${dDateEqualConds})"
}

println("shopPoConds: ${shopPoConds}")
val shopPoParams = M18PurchaseOrderListRequest(
params = null,
conds = shopPoConds


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

@@ -59,15 +59,32 @@ open class M18PurchaseOrderService(
open fun getPurchaseOrdersWithType(request: M18CommonRequest): M18PurchaseOrderListResponseWithType? {
val purchaseOrders = M18PurchaseOrderListResponseWithType(mutableListOf())
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val dateFrom = request.modifiedDateFrom?.let {
LocalDateTime.parse(it, formatter).toLocalDate().toString()
val lastDateConds = if (request.modifiedDateFrom != null && request.modifiedDateTo != null) {
val dateFrom = LocalDateTime.parse(request.modifiedDateFrom, formatter).toLocalDate().toString()
val dateTo = LocalDateTime.parse(request.modifiedDateTo, formatter).toLocalDate().toString()
"lastModifyDate=largerOrEqual=$dateFrom=and=lastModifyDate=lessOrEqual=$dateTo"
} else {
""
}
val dateTo = request.modifiedDateTo?.let {
LocalDateTime.parse(it, formatter).toLocalDate().toString()

val dDateConds = if (request.dDateFrom != null && request.dDateTo != null) {
val dateFrom = LocalDateTime.parse(request.dDateFrom, formatter).toLocalDate().toString()
val dateTo = LocalDateTime.parse(request.dDateTo, formatter).toLocalDate().toString()
"dDate=largerOrEqual=$dateFrom=and=dDate=lessOrEqual=$dateTo"
} else {
""
}
val lastDateConds =
//"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}"
"dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}"

val dDateEqualConds = if (request.dDateEqual != null) {
val dDateEqual = LocalDateTime.parse(request.dDateEqual, formatter).toLocalDate().toString()
"dDate=equal=$dDateEqual"
} else {
""
}

// Material PO
val materialPoBuyers =
commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=")
@@ -76,8 +93,21 @@ open class M18PurchaseOrderService(
"venId=unequal=",
"=or="
)
val materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot})=and=(${lastDateConds})"
var materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot}))"
if (request.modifiedDateFrom != null && request.modifiedDateTo != null) {
materialPoConds += "=and=(${lastDateConds})"
}
if (request.dDateFrom != null && request.dDateTo != null) {
materialPoConds += "=and=(${dDateConds})"
}
if (request.dDateEqual != null) {
materialPoConds += "=and=(${dDateEqualConds})"
}

println("materialPoConds: ${materialPoConds}")


val materialPoParams = M18PurchaseOrderListRequest(
params = null,
conds = materialPoConds


+ 3
- 0
src/main/java/com/ffii/fpsms/m18/web/models/M18TestRequest.kt Zobrazit soubor

@@ -3,4 +3,7 @@ package com.ffii.fpsms.m18.web.models
data class M18CommonRequest(
val modifiedDateFrom: String? = null,
val modifiedDateTo: String? = null,
val dDateFrom: String? = null,
val dDateTo: String? = null,
val dDateEqual: String? = null,
)

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/common/SettingNames.java Zobrazit soubor

@@ -25,6 +25,9 @@ public abstract class SettingNames {
*/
public static final String SCHEDULE_M18_PO = "SCHEDULE.m18.po";

public static final String SCHEDULE_M18_DO1 = "SCHEDULE.m18.do1";
public static final String SCHEDULE_M18_DO2 = "SCHEDULE.m18.do2";

public static final String SCHEDULE_M18_MASTER = "SCHEDULE.m18.master";

public static final String SCHEDULE_PROD_ROUGH = "SCHEDULE.prod.rough";


+ 77
- 23
src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt Zobrazit soubor

@@ -31,19 +31,23 @@ open class SchedulerService(
) {
var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)
val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val dateTimeStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val defaultCronExpression = "0 0 2 * * *";

@Volatile
//@Volatile
var scheduledM18Po: ScheduledFuture<*>? = null

var scheduledM18Do1: ScheduledFuture<*>? = null
var scheduledM18Do2: ScheduledFuture<*>? = null

@Volatile
var scheduledM18Master: ScheduledFuture<*>? = null

@Volatile
var scheduledRoughProd: ScheduledFuture<*>? = null
//@Volatile
//var scheduledRoughProd: ScheduledFuture<*>? = null

@Volatile
var scheduledDetailedProd: ScheduledFuture<*>? = null
//@Volatile
//var scheduledDetailedProd: ScheduledFuture<*>? = null

// Common Function
fun isValidCronExpression(cronExpression: String): Boolean {
@@ -55,52 +59,63 @@ open class SchedulerService(
}
}

fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit) {
fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit): ScheduledFuture<*>? {
scheduled?.cancel(false)

var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCronExpression;
var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCronExpression

if (!isValidCronExpression(cron)) {
cron = defaultCronExpression
}
scheduledM18Po = taskScheduler.schedule(
{
scheduleFunc()
},
// Now Kotlin is happy because both the method and the function allow nulls
return taskScheduler.schedule(
{ scheduleFunc() },
CronTrigger(cron)
)
}

// Init Scheduler
// @PostConstruct
@PostConstruct
fun init() {
// scheduleM18PoTask();
//scheduleM18PoTask();
scheduleM18Do1();
scheduleM18Do2();
scheduleM18MasterData();
scheduleRoughProd();
scheduleDetailedProd();
//scheduleRoughProd();
//scheduleDetailedProd();
}

// Scheduler
// --------------------------- FP-MTMS --------------------------- //
fun scheduleRoughProd() {
commonSchedule(scheduledRoughProd, SettingNames.SCHEDULE_PROD_ROUGH, ::getRoughProdSchedule)
}
//fun scheduleRoughProd() {
// commonSchedule(scheduledRoughProd, SettingNames.SCHEDULE_PROD_ROUGH, ::getRoughProdSchedule)
//}

fun scheduleDetailedProd() {
commonSchedule(scheduledDetailedProd, SettingNames.SCHEDULE_PROD_DETAILED, ::getDetailedProdSchedule)
}
//fun scheduleDetailedProd() {
// commonSchedule(scheduledDetailedProd, SettingNames.SCHEDULE_PROD_DETAILED, ::getDetailedProdSchedule)
//}

// --------------------------- M18 --------------------------- //
fun scheduleM18PoTask() {
commonSchedule(scheduledM18Po, SettingNames.SCHEDULE_M18_PO, ::getM18Pos)
}

fun scheduleM18Do1() {
commonSchedule(scheduledM18Do1, SettingNames.SCHEDULE_M18_DO1, ::getM18Dos1)
}

fun scheduleM18Do2() {
commonSchedule(scheduledM18Do2, SettingNames.SCHEDULE_M18_DO2, ::getM18Dos2)
}

fun scheduleM18MasterData() {
commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData)
}

// Function for schedule
// --------------------------- FP-MTMS --------------------------- //
open fun getRoughProdSchedule() {
try {
logger.info("Daily Scheduler - Rough Prod")
@@ -123,7 +138,7 @@ open class SchedulerService(
throw RuntimeException("Error generate schedule: ${e.message}", e)
}
}
open fun getDetailedProdSchedule() {
try {
logger.info("Daily Scheduler - Detailed Prod")
@@ -145,7 +160,6 @@ open class SchedulerService(
throw RuntimeException("Error generate schedule: ${e.message}", e)
}
}

// --------------------------- M18 --------------------------- //
// @Async
// @Scheduled(cron = "0 0 2 * * *") // (SS/MM/HH/DD/MM/YY)
@@ -175,6 +189,46 @@ open class SchedulerService(
// logger.info("yesterday: ${yesterday.format(dataStringFormat)}")
}

open fun getM18Dos1() {
logger.info("DO Scheduler 1 - DO")
val currentTime = LocalDateTime.now()
val today = currentTime.toLocalDate().atStartOfDay()
val twoDaysLater = today.plusDays(2L)

var requestDO = M18CommonRequest(
dDateTo = twoDaysLater.format(dateTimeStringFormat),
dDateFrom = twoDaysLater.format(dateTimeStringFormat)
)
m18DeliveryOrderService.saveDeliveryOrders(requestDO);
}

open fun getM18Dos2() {
logger.info("DO Scheduler 2 - DO")
val currentTime = LocalDateTime.now()

// .atStartOfDay() results in 00:00:00
val today = currentTime.toLocalDate().atStartOfDay()
val ysd = today.minusDays(1L)
val tmr = today.plusDays(1L)

// Set to 19:00:00 of yesterday
val ysdNight = ysd.withHour(19).withMinute(0).withSecond(0)

// Set to 11:00:00 of today
val todayEleven = today.withHour(11).withMinute(0).withSecond(0)

val requestDO = M18CommonRequest(
// These will now produce "yyyy-MM-dd HH:mm:ss"
dDateTo = tmr.format(dateTimeStringFormat), // e.g. 2026-01-19 00:00:00
dDateFrom = tmr.format(dateTimeStringFormat), // e.g. 2026-01-19 00:00:00
modifiedDateTo = todayEleven.format(dateTimeStringFormat), // 2026-01-18 11:00:00
modifiedDateFrom = ysdNight.format(dateTimeStringFormat) // 2026-01-17 19:00:00
)
m18DeliveryOrderService.saveDeliveryOrders(requestDO)
}

open fun getM18MasterData() {
logger.info("Daily Scheduler - Master Data")
val currentTime = LocalDateTime.now()


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

@@ -4,7 +4,6 @@ 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 org.springframework.stereotype.Service

import java.awt.Color
import java.awt.Font
import java.awt.Graphics2D
@@ -24,112 +23,117 @@ import java.nio.charset.Charset

import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.ConnectException
import java.net.SocketTimeoutException

// 1. Data class to store both the raw bytes and the width for XML injection
// Data class to store bitmap bytes + width (for XML)
data class BitmapResult(val bytes: ByteArray, val width: Int)

@Service
open class PlasticBagPrinterService(
val jobOrderRepository: JobOrderRepository,
) {
fun generatePrintJobBundle(itemCode: String, lotNo: String, expiryDate: String, productName: String): ByteArray {

fun generatePrintJobBundle(
itemCode: String,
lotNo: String,
expiryDate: String,
productName: String
): ByteArray {
val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zos ->
// Generate Bitmaps and capture their widths
val productBmp = createMonochromeBitmap(productName, 704)
val codeBmp = createMonochromeBitmap(itemCode, 1173)
val expBmp = createMonochromeBitmap(expiryDate, 1173)
val qrBmp = createQrCodeBitmap("$itemCode|$lotNo|$expiryDate", 1000)
// Reduced sizes to make files smaller while keeping readability/scanability
val productBmp = createMonochromeBitmap(productName, 520) // was 704
val codeBmp = createMonochromeBitmap(itemCode, 900) // was 1173
val expBmp = createMonochromeBitmap(expiryDate, 900) // was 1173
val qrBmp = createQrCodeBitmap("$itemCode|$lotNo|$expiryDate", 450) // was 1000

val nameFile = "${lotNo}_product.bmp"
val codeFile = "${lotNo}_code.bmp"
val expFile = "${lotNo}_expiry.bmp"
val qrFile = "${lotNo}_qr.bmp"
val expFile = "${lotNo}_expiry.bmp"
val qrFile = "${lotNo}_qr.bmp"

// Now "bytes" property is resolved
addToZip(zos, nameFile, productBmp.bytes)
addToZip(zos, codeFile, codeBmp.bytes)
addToZip(zos, expFile, expBmp.bytes)
addToZip(zos, qrFile, qrBmp.bytes)

// 2. Generate the .image file with XML headers and mandatory X40 tags
val imageXml = """
<?xml version="1.0" encoding="utf-8"?>
<Legend type="Image.V1" PaperColor="White" BackgroundColor="DarkGray">
<DeviceInfo DeviceURI="NGEDriver[SDX40c.cadb94e3-677c-4bcb-9c80-88bf9526d6fe]/"><DeviceName /></DeviceInfo>
<Mirrored>false</Mirrored>
<Orientation>DEG_0</Orientation>
<Width>5300</Width><Height>7100</Height>
<FieldList>
<Logo type="Logo.V1">
<Name>LOGO</Name>
<Geometry><X>0</X><Y>250</Y></Geometry>
<FieldColor>BLACK</FieldColor>
<FileName>$nameFile</FileName>
<Width>${productBmp.width}</Width>
<Height>704</Height>
</Logo>
<Logo type="Logo.V1">
<Name>LOGO_2</Name>
<Geometry><X>1000</X><Y>1250</Y></Geometry>
<FieldColor>BLACK</FieldColor>
<FileName>$codeFile</FileName>
<Width>${codeBmp.width}</Width>
<Height>1173</Height>
</Logo>
<Logo type="Logo.V1">
<Name>LOGO_3</Name>
<Geometry><X>500</X><Y>2500</Y></Geometry>
<FieldColor>BLACK</FieldColor>
<FileName>$expFile</FileName>
<Width>${expBmp.width}</Width>
<Height>1173</Height>
</Logo>
<Logo type="Logo.V1">
<Name>LOGO_4</Name>
<Geometry><X>750</X><Y>3750</Y></Geometry>
<FieldColor>BLACK</FieldColor>
<FileName>$qrFile</FileName>
<Width>${qrBmp.width}</Width>
<Height>1000</Height>
</Logo>
</FieldList>
</Legend>
""".trimIndent()
addToZip(zos, expFile, expBmp.bytes)
addToZip(zos, qrFile, qrBmp.bytes)

// Minified XML - no indentation/newlines between tags
val imageXml = """<?xml version="1.0" encoding="utf-8"?><Legend type="Image.V1" PaperColor="White" BackgroundColor="DarkGray"><DeviceInfo DeviceURI="NGEDriver[SDX40c.cadb94e3-677c-4bcb-9c80-88bf9526d6fe]/"><DeviceName/></DeviceInfo><Mirrored>false</Mirrored><Orientation>DEG_0</Orientation><Width>5300</Width><Height>7100</Height><FieldList><Logo type="Logo.V1"><Name>LOGO</Name><Geometry><X>0</X><Y>250</Y></Geometry><FieldColor>BLACK</FieldColor><FileName>$nameFile</FileName><Width>${productBmp.width}</Width><Height>520</Height></Logo><Logo type="Logo.V1"><Name>LOGO_2</Name><Geometry><X>1000</X><Y>1250</Y></Geometry><FieldColor>BLACK</FieldColor><FileName>$codeFile</FileName><Width>${codeBmp.width}</Width><Height>900</Height></Logo><Logo type="Logo.V1"><Name>LOGO_3</Name><Geometry><X>500</X><Y>2500</Y></Geometry><FieldColor>BLACK</FieldColor><FileName>$expFile</FileName><Width>${expBmp.width}</Width><Height>900</Height></Logo><Logo type="Logo.V1"><Name>LOGO_4</Name><Geometry><X>750</X><Y>3750</Y></Geometry><FieldColor>BLACK</FieldColor><FileName>$qrFile</FileName><Width>${qrBmp.width}</Width><Height>450</Height></Logo></FieldList></Legend>""".trimIndent()

addToZip(zos, "$lotNo.image", imageXml.toByteArray(Charsets.UTF_8))

val jobXml = """<?xml version="1.0" encoding="utf-8"?><Job><ImageFileName>$lotNo.image</ImageFileName></Job>"""
addToZip(zos, "$lotNo.job", jobXml.toByteArray(Charsets.UTF_8))
}
return baos.toByteArray()
}

private fun createMonochromeBitmap(text: String, maxHeight: Int): BitmapResult {
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)
val g2dTemp = tempImg.createGraphics()
val font = Font("SimSun", Font.BOLD, (maxHeight * 0.8).toInt())
val font = Font("SimSun", Font.BOLD, (targetHeight * 0.92).toInt())
g2dTemp.font = font
val metrics = g2dTemp.getFontMetrics(font)
val width = metrics.stringWidth(text).let { if (it < 1) 1 else it }
val height = maxHeight
val textWidth = metrics.stringWidth(text).coerceAtLeast(1)
g2dTemp.dispose()

val finalImg = BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY)
val g2d = finalImg.createGraphics()
g2d.color = Color.WHITE
g2d.fillRect(0, 0, width, height)
g2d.color = Color.BLACK
g2d.font = font
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)
g2d.drawString(text, 0, metrics.ascent)
g2d.dispose()
// Step 2: Create actual image
// CHANGE 1: Changed 'val' to 'var' so we can reassign it on line 84
var img = BufferedImage(textWidth, targetHeight, BufferedImage.TYPE_BYTE_BINARY)
// CHANGE 2: Removed 'val g2d =' because it was unused
img.createGraphics().apply {
color = Color.WHITE
fillRect(0, 0, textWidth, targetHeight)
color = Color.BLACK
this.font = font // use 'this.font' to be explicit inside apply
setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)
drawString(text, 0, metrics.ascent)
dispose()
}

// Step 3: Crop excess white space
// This line (84) will now work because 'img' is a 'var'
img = cropToContent(img)

val baos = ByteArrayOutputStream()
ImageIO.write(finalImg, "bmp", baos)
return BitmapResult(baos.toByteArray(), width)
ImageIO.write(img, "bmp", baos)
return BitmapResult(baos.toByteArray(), img.width)
}
fun testSmartDateX40Tcp(printerIp: String, port: Int = 3001, timeoutMs: Int = 3000): Pair<Boolean, String> {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress(printerIp, port), timeoutMs)
socket.soTimeout = 2000 // read timeout

val output = PrintWriter(socket.getOutputStream(), true)
output.println("~VR") // or any simple command your printer understands
output.flush()

val input = BufferedReader(InputStreamReader(socket.getInputStream()))
val response = try {
input.readLine() ?: "No response line"
} catch (e: java.net.SocketTimeoutException) {
"No response within timeout"
}

if (response.isNotBlank() && response != "No response within timeout") {
true to "Connected successfully. Response: $response"
} else {
true to "Connected (port open), but no meaningful response"
}
}
} catch (e: SocketTimeoutException) {
false to "Connection timed out (port may be closed or printer unreachable)"
} catch (e: ConnectException) {
false to "Connection refused (port closed or wrong IP)"
} catch (e: Exception) {
false to "Test failed: ${e.javaClass.simpleName} - ${e.message}"
}
}

private fun createQrCodeBitmap(content: String, size: Int): BitmapResult {
@@ -141,10 +145,43 @@ open class PlasticBagPrinterService(
image.setRGB(x, y, if (bitMatrix.get(x, y)) Color.BLACK.rgb else Color.WHITE.rgb)
}
}

// Crop QR code (removes quiet zone excess if any)
val cropped = cropToContent(image)

val baos = ByteArrayOutputStream()
ImageIO.write(image, "bmp", baos)
// For QR code, width and height are the same (size)
return BitmapResult(baos.toByteArray(), size)
ImageIO.write(cropped, "bmp", baos)
return BitmapResult(baos.toByteArray(), cropped.width)
}

private fun cropToContent(img: BufferedImage): BufferedImage {
val width = img.width
val height = img.height
var minX = width
var minY = height
var maxX = 0
var maxY = 0

for (x in 0 until width) {
for (y in 0 until height) {
if (img.getRGB(x, y) == Color.BLACK.rgb) {
minX = minOf(minX, x)
minY = minOf(minY, y)
maxX = maxOf(maxX, x)
maxY = maxOf(maxY, y)
}
}
}

if (minX > maxX || minY > maxY) return img // empty image fallback

val padding = 4
val cropX = (minX - padding).coerceAtLeast(0)
val cropY = (minY - padding).coerceAtLeast(0)
val cropW = (maxX - minX + padding * 2).coerceAtMost(width - cropX)
val cropH = (maxY - minY + padding * 2).coerceAtMost(height - cropY)

return img.getSubimage(cropX, cropY, cropW, cropH)
}

private fun addToZip(zos: ZipOutputStream, entryName: String, content: ByteArray) {
@@ -153,30 +190,35 @@ open class PlasticBagPrinterService(
zos.write(content)
zos.closeEntry()
}

// ────────────────────────────────────────────────
// The rest of your printer communication methods remain unchanged
// ────────────────────────────────────────────────

fun sendDataFlexJob(request: PrintRequest) {
Socket().use { socket ->
try {
socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 3000)
val writer = PrintWriter(socket.getOutputStream(), true)

// Videojet/DataFlex ASCII protocol
// We assign the Lot number to both the text variable and the barcode variable
val command = """
^SVAR1|${request.itemCode}
^SVAR2|${request.itemName}
^SVAR3|${request.lotNo}
^SVAR4|${request.expiryDate}
^SVAR_QR|${request.lotNo}
^PRNT1
""".trimIndent()

writer.println(command)
socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 5000)
val output = socket.getOutputStream()
val writer = PrintWriter(output, true)

val command = buildString {
append("\u0002") // <STX>
append("<ESC>JI;YOUR_JOB_NAME\r\n")
append("<ESC>VB;ItemCode;${request.itemCode}\r\n")
append("<ESC>VB;ItemName;${request.itemName}\r\n")
append("<ESC>VB;LotNo;${request.lotNo}\r\n")
append("<ESC>VB;ExpiryDate;${request.expiryDate}\r\n")
append("<ESC>VB;QRContent;lot=${request.lotNo}&item=${request.itemCode}\r\n")
append("<ESC>P;0\r\n")
append("\u0003") // <ETX>
}

writer.print(command)
writer.flush()
println("DataFlex: Print job with QR data sent to ${request.printerIp}")
println("DataFlex: Print job sent to ${request.printerIp}:${request.printerPort}")
} catch (e: Exception) {
throw RuntimeException("DataFlex Error: ${e.message}")
throw RuntimeException("DataFlex communication failed: ${e.message}", e)
}
}
}
@@ -185,46 +227,20 @@ open class PlasticBagPrinterService(
Socket().use { socket ->
socket.connect(InetSocketAddress(request.printerIp, request.printerPort.toInt()), 3000)
val writer = PrintWriter(socket.getOutputStream(), true)

// Standard HANS/General Laser Command Format:
// [STX]Command|Variable1|Variable2[ETX]
// Note: Exact protocol depends on your HANS controller software (usually JCZ or similar)
val command = "STRSET|${request.templateId}|LOT=${request.lotNo}|EXP=${request.expiryDate}\n"
writer.print(command)
writer.flush()
// Optional: Trigger the marking immediately
// writer.print("STRMARK\n")
// writer.flush()
}
}

/*
fun previewLaser(request: LaserRequest) {
Socket().use { socket ->
socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 2000)
val writer = PrintWriter(socket.getOutputStream(), true)
// Typical HANS command for Red Light Preview
writer.println("JOBLOAD|${request.templateId}")
writer.println("REDLIGHT|1") // 1 to turn on, 0 to turn off
writer.flush()
}
} */

fun sendLaserPreview(request: LaserRequest) {
Socket().use { socket ->
socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 3000)
val writer = PrintWriter(socket.getOutputStream(), true)

// HANS Protocol for Red Light
// Often requires loading the job first so it knows the boundary
val command = """
JOBLOAD|${request.templateId}
REDLIGHT|1
""".trimIndent()

writer.println(command)
writer.flush()
}
@@ -236,38 +252,25 @@ open class PlasticBagPrinterService(
socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 3000)
val out = DataOutputStream(socket.getOutputStream())

// 203 DPI: 8 dots = 1mm
val tspl = StringBuilder()
.append("SIZE 100 mm, 50 mm\n")
.append("GAP 3 mm, 0 mm\n")
.append("DIRECTION 1\n")
.append("CLS\n")
// --- Text Content ---
.append("TEXT 50,40,\"ROMAN.TTF\",0,1,1,\"ITEM: ${request.itemCode}\"\n")
.append("TEXT 50,80,\"ROMAN.TTF\",0,1,1,\"NAME: ${request.itemName}\"\n")
.append("TEXT 50,120,\"ROMAN.TTF\",0,1,1,\"EXP: ${request.expiryDate}\"\n")
.append("TEXT 50,160,\"ROMAN.TTF\",0,1,1,\"LOT: ${request.lotNo}\"\n")

// --- QR Code for Lot Number ---
// Syntax: QRCODE x, y, ECC, cell_width, mode, rotation, "content"
// ECC Level M (Standard), Cell width 4 (size), Auto mode, 0 rotation
.append("QRCODE 450,40,M,4,A,0,\"${request.lotNo}\"\n")
// Optional: Label for the QR Code
.append("TEXT 450,180,\"ROMAN.TTF\",0,1,1,\"SCAN LOT\"\n")

.append("PRINT 1,1\n")
.toString()

// Use TIS-620 for Thai support or Windows-1252 for English
val bytes = tspl.toByteArray(Charset.forName("TIS-620"))
out.write(bytes)
out.flush()
println("TSC: Print job with QR code sent to ${request.printerIp}")
println("TSC: Print job sent to ${request.printerIp}")
} catch (e: Exception) {
println("TSC Error: ${e.message}")
throw RuntimeException("TSC Printer Error: ${e.message}")
}
}
@@ -335,5 +338,4 @@ open class PlasticBagPrinterService(
println("Han's Laser TCP 連接已關閉")
}
}

}

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

@@ -88,6 +88,19 @@ class PlasticBagPrinterController(
}
}

@GetMapping("/test-x40")
fun testX40(
@RequestParam printerIp: String,
@RequestParam(defaultValue = "3001") port: Int
): ResponseEntity<String> {
val (success, message) = plasticBagPrinterService.testSmartDateX40Tcp(printerIp, port)
return if (success) {
ResponseEntity.ok("OK - $message")
} else {
ResponseEntity.status(503).body("FAIL - $message")
}
}

@PostMapping("/print-laser-tcp")
fun printLaserTcp(@RequestBody request: Laser2Request): ResponseEntity<String> {
return try {


+ 15
- 11
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt Zobrazit soubor

@@ -628,19 +628,20 @@ open class ProductionScheduleService(
i.outputQty,
i.avgQtyLastMonth,
(i.onHandQty -500),
(i.onHandQty -500) * 1.0 / i.avgQtyLastMonth as daysLeft,
-- (i.onHandQty -500) * 1.0 / i.avgQtyLastMonth as daysLeft,
i.avgQtyLastMonth * 2.6 - stockQty as needQty,
i.stockQty,
CASE
WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN
CEIL((i.avgQtyLastMonth * 2.6 - stockQty) / i.outputQty)
ELSE 0
END AS needNoOfJobOrder,
CASE
WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN
CEIL((i.avgQtyLastMonth * 2.6 - stockQty) / i.outputQty)
ELSE 0
END AS batchNeed,
-- CASE
-- WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN
-- CEIL((i.avgQtyLastMonth * 2.6 - stockQty) / i.outputQty)
-- ELSE 0
-- END AS needNoOfJobOrder,
-- The first date only consider how many batch pending for job order
0 AS needNoOfJobOrder,
0 AS batchNeed,
i.pendingJobQty,
((i.stockQty * 1.0) + (i.needNoOfJobOrder * i.outputQty) ) / i.avgQtyLastMonth as daysLeft,

25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority,
i.*
FROM
@@ -658,6 +659,9 @@ open class ProductionScheduleService(
-- AND MONTH(do.estimatedArrivalDate) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))
AND do.estimatedArrivalDate >= '2025-12-01' AND do.estimatedArrivalDate < '2025-12-11'
GROUP BY do.estimatedArrivalDate) AS d) AS avgQtyLastMonth,

(select sum(reqQty) from job_order where bomId = bom.id and status != 'completed') AS pendingJobQty,

inventory.onHandQty - 500 AS stockQty,
bom.outputQty,
bom.outputQtyUom,


+ 16
- 0
src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql Zobrazit soubor

@@ -0,0 +1,16 @@
--liquibase formatted sql
--changeset author:add_scheduler

INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`)
SELECT 'SCHEDULE.m18.do1', '0 0 19 * * *', 'SCHEDULE', 'string'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'SCHEDULE.m18.do1'
);

INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`)
SELECT 'SCHEDULE.m18.do2', '0 0 11 * * *', 'SCHEDULE', 'string'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'SCHEDULE.m18.do2'
);

+ 3
- 3
src/main/resources/jasper/FGDeliveryReport.jrxml Zobrazit soubor

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.17.0.final using JasperReports Library version 6.17.0-6d93193241dd8cc42629e188b94f9e0bc5722efd -->
<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="FGStockOutTraceabilityReport" pageWidth="842" pageHeight="595" orientation="Landscape" columnWidth="802" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="c40e235b-365d-48a0-a1b8-a8e0ece2cdd3">
<!-- Created with Jaspersoft Studio version 6.21.3.final using JasperReports Library version 5.5.2 -->
<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="FGStockOutTraceabilityReport" pageWidth="842" pageHeight="595" orientation="Landscape" whenNoDataType="AllSectionsNoDetail" columnWidth="802" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="c40e235b-365d-48a0-a1b8-a8e0ece2cdd3">
<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"/>
@@ -310,7 +310,7 @@
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Justified">
<textElement textAlignment="Center" verticalAlignment="Top">
<font fontName="微軟正黑體" size="12"/>
</textElement>
<text><![CDATA[/]]></text>


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