Pārlūkot izejas kodu

label printer

production
tommy pirms 4 dienas
vecāks
revīzija
76ff1cd22f
11 mainītis faili ar 800 papildinājumiem un 104 dzēšanām
  1. +6
    -0
      src/main/java/com/ffii/fpsms/config/security/SecurityConfig.java
  2. +36
    -19
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  3. +3
    -7
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  4. +30
    -0
      src/main/java/com/ffii/fpsms/modules/monitoring/scheduler/LabelPrinterMonitorScheduler.kt
  5. +365
    -0
      src/main/java/com/ffii/fpsms/modules/monitoring/service/LabelPrinterMonitorService.kt
  6. +62
    -0
      src/main/java/com/ffii/fpsms/modules/monitoring/web/LabelPrinterMonitorController.kt
  7. +121
    -0
      src/main/java/com/ffii/fpsms/modules/monitoring/zebra/ZebraLinkOsClient.kt
  8. +104
    -62
      src/main/resources/DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml
  9. +7
    -0
      src/main/resources/application.yml
  10. +29
    -0
      src/main/resources/db/changelog/changes/20260617_label_printer_monitor/01_label_printer_monitor.sql
  11. +37
    -16
      src/main/resources/jasper/StockTakeVarianceReport.jrxml

+ 6
- 0
src/main/java/com/ffii/fpsms/config/security/SecurityConfig.java Parādīt failu

@@ -103,6 +103,12 @@ public class SecurityConfig {
.hasAnyAuthority("TESTING", "ADMIN")
.requestMatchers(HttpMethod.POST, "/printer-monitor/check")
.hasAnyAuthority("TESTING", "ADMIN")
.requestMatchers(HttpMethod.GET, "/label-printer-monitor/status")
.hasAnyAuthority("TESTING", "ADMIN")
.requestMatchers(HttpMethod.POST, "/label-printer-monitor/check")
.hasAnyAuthority("TESTING", "ADMIN")
.requestMatchers(HttpMethod.GET, "/label-printer-monitor/label-stats")
.hasAnyAuthority("TESTING", "ADMIN")
.anyRequest().authenticated())
.httpBasic(httpBasic -> httpBasic.authenticationEntryPoint(
(request, response, authException) -> sendUnauthorizedJson(response, "Unauthorized", "UNAUTHORIZED")))


+ 36
- 19
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Parādīt failu

@@ -1881,42 +1881,60 @@ open class DeliveryOrderService(

data class ParsedShopLabelForCartonLabel(
val shopCode: String,
val shopCodeAbbr: String,
val shopNameForLabel: String
val shopNameForLabel: String,
)

fun parseShopLabelForCartonLabel(rawInput: String): ParsedShopLabelForCartonLabel {
// Fixed input format: shopCode - shopName1-shopName2
// Input format: shopCode - shopName1-shopName2 (e.g. CT001 - 茶泰-啟田店)
// Optional trailing note in parentheses on its own line, e.g. (午更落貨表:15:00到貨)
val raw = rawInput.trim()

val (shopCodePartRaw, restPart) = raw.split(" - ", limit = 2).let { parts ->
(parts.getOrNull(0)?.trim().orEmpty()) to (parts.getOrNull(1)?.trim().orEmpty())
}

val shopCode = shopCodePartRaw.let { code ->
val trimmed = code.trim()
if (trimmed.length > 5) trimmed.substring(0, 5) else trimmed
}
val trailingNoteRegex = Regex("\\s*(\\([^)]+\\))\\s*$")
val trailingNoteMatch = trailingNoteRegex.find(restPart)
val namePart = trailingNoteMatch
?.let { restPart.removeRange(it.range).trim() }
?: restPart
val trailingNote = trailingNoteMatch?.groupValues?.get(1)?.trim().orEmpty()

val (shopName1, shopName2) = restPart.split("-", limit = 2).let { parts ->
val (shopName1, shopName2) = namePart.split("-", limit = 2).let { parts ->
(parts.getOrNull(0)?.trim().orEmpty()) to (parts.getOrNull(1)?.trim().orEmpty())
}

val shopNameForLabel = if (shopName2.isNotBlank()) {
"$shopName1\n$shopName2"
} else {
shopName1
val shopNameLines = buildList {
if (shopName1.isNotBlank()) add(shopName1)
if (shopName2.isNotBlank()) add(shopName2)
if (trailingNote.isNotBlank()) add(trailingNote)
}

val shopCodeAbbr = if (shopCode.length >= 2) shopCode.substring(0, 2) else shopCode

return ParsedShopLabelForCartonLabel(
shopCode = shopCode,
shopCodeAbbr = shopCodeAbbr,
shopNameForLabel = shopNameForLabel
shopCode = shopCodePartRaw.trim(),
shopNameForLabel = shopNameLines.joinToString("\n"),
)
}

fun formatTruckLaneCodeForCartonLabel(raw: String?): String {
val trimmed = raw?.trim().orEmpty()
if (trimmed.isEmpty()) return trimmed

val withoutLanePrefix = if (trimmed.startsWith("車線-")) {
trimmed.removePrefix("車線-")
} else {
trimmed
}

// 4F route board: P06B_Wed_區6_新界,九龍 → 區6_新界,九龍
val pRouteDistrict = Regex("^P[^_]+_[^_]+_(.+)$").matchEntire(withoutLanePrefix)
if (pRouteDistrict != null) {
return pRouteDistrict.groupValues[1]
}

return withoutLanePrefix
}


//Print Carton Labels
@Transactional
@@ -2047,9 +2065,8 @@ open class DeliveryOrderService(
val rawShopLabel = doPickOrderRecord.shopName ?: cartonLabelInfo[0].shopName ?: ""
val parsedShopLabel = parseShopLabelForCartonLabel(rawShopLabel)
params["shopCode"] = parsedShopLabel.shopCode
params["shopCodeAbbr"] = parsedShopLabel.shopCodeAbbr
params["shopName"] = parsedShopLabel.shopNameForLabel
params["truckNo"] = doPickOrderRecord.truckLanceCode ?: ""
params["truckNo"] = formatTruckLaneCodeForCartonLabel(doPickOrderRecord.truckLanceCode)

return DnLabelExportContext(
cartonLabelTemplate = cartonLabelTemplate,


+ 3
- 7
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt Parādīt failu

@@ -1765,6 +1765,7 @@ return MessageResponse(
dop.storeId as store_id,
dop.truckLanceCode as TruckLanceCode,
dop.truckDepartureTime as truck_departure_time,
dop.requiredDeliveryDate as required_delivery_date,
dop.shopCode as ShopCode,
dop.shopName as ShopName,
dop.ticketStatus as doTicketStatus
@@ -2824,19 +2825,14 @@ return MessageResponse(
cartonLabelInfo: MutableList<DeliveryOrderInfo>,
): MutableMap<String, Any> {
val params = mutableMapOf<String, Any>()
params["shopPurchaseOrderNo"] = if (ctx.deliveryOrderIds.size > 1) {
"請查閲送貨單(採購單共${ctx.deliveryOrderIds.size}張)"
} else {
cartonLabelInfo[0].code
}
params["shopPurchaseOrderNo"] = "請查閲送貨單(採購單共${ctx.deliveryOrderIds.size}張)"
params["deliveryNoteCode"] = ctx.header.deliveryNoteCode ?: ""
params["shopAddress"] = cartonLabelInfo[0].shopAddress ?: ""
val rawShopLabel = ctx.header.shopName ?: cartonLabelInfo[0].shopName ?: ""
val parsedShopLabel = deliveryOrderService.parseShopLabelForCartonLabel(rawShopLabel)
params["shopCode"] = parsedShopLabel.shopCode
params["shopCodeAbbr"] = parsedShopLabel.shopCodeAbbr
params["shopName"] = parsedShopLabel.shopNameForLabel
params["truckNo"] = ctx.header.truckLanceCode ?: ""
params["truckNo"] = deliveryOrderService.formatTruckLaneCodeForCartonLabel(ctx.header.truckLanceCode)
return params
}



+ 30
- 0
src/main/java/com/ffii/fpsms/modules/monitoring/scheduler/LabelPrinterMonitorScheduler.kt Parādīt failu

@@ -0,0 +1,30 @@
package com.ffii.fpsms.modules.monitoring.scheduler

import com.ffii.fpsms.modules.monitoring.service.LabelPrinterMonitorService
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Component
@ConditionalOnProperty(prefix = "fpsms.monitoring", name = ["enabled"], havingValue = "true")
class LabelPrinterMonitorScheduler(
private val labelPrinterMonitorService: LabelPrinterMonitorService,
) {
private val log = LoggerFactory.getLogger(LabelPrinterMonitorScheduler::class.java)

@Scheduled(fixedRateString = "\${fpsms.label-printer-monitor.check-interval-ms:120000}")
fun scanLabelPrinters() {
try {
val result = labelPrinterMonitorService.checkAllLabelPrinters()
@Suppress("UNCHECKED_CAST")
val summary = result["summary"] as? Map<String, Any>
val offline = summary?.get("offline") ?: 0
if ((offline as? Number)?.toInt()?.let { it > 0 } == true) {
log.warn("Label printer monitor: {} label printer(s) offline", offline)
}
} catch (e: Exception) {
log.error("Label printer connectivity / odometer scan failed", e)
}
}
}

+ 365
- 0
src/main/java/com/ffii/fpsms/modules/monitoring/service/LabelPrinterMonitorService.kt Parādīt failu

@@ -0,0 +1,365 @@
package com.ffii.fpsms.modules.monitoring.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.master.entity.Printer
import com.ffii.fpsms.modules.master.entity.PrinterRepository
import com.ffii.fpsms.modules.monitoring.zebra.ZebraLinkOsClient
import com.ffii.fpsms.py.PyPrintChannel
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.net.InetSocketAddress
import java.net.Socket
import java.time.LocalDate
import java.time.LocalDateTime

@Service
open class LabelPrinterMonitorService(
private val printerRepository: PrinterRepository,
private val jdbcDao: JdbcDao,
) {
@Value("\${fpsms.label-printer-monitor.connect-timeout-ms:3000}")
private var connectTimeoutMs: Int = 3000

@Value("\${fpsms.label-printer-monitor.zebra-query-timeout-ms:2500}")
private var zebraQueryTimeoutMs: Int = 2500

@Value("\${fpsms.label-printer-monitor.default-port:9100}")
private var defaultPort: Int = 9100

@Value("\${fpsms.label-printer-monitor.zebra-odometer-enabled:true}")
private var zebraOdometerEnabled: Boolean = true

@Value("\${fpsms.label-printer-monitor.history-max-range-days:30}")
private var historyMaxRangeDays: Long = 30

data class ProbeResult(
val reachable: Boolean,
val latencyMs: Int?,
val errorMessage: String?,
)

@Transactional
open fun checkAllLabelPrinters(): Map<String, Any> {
val printers = labelPrinters()
val results = printers.map { checkAndPersist(it) }
val today = LocalDate.now()
val stats = labelPrintStats(today.atStartOfDay(), LocalDateTime.now())
return mapOf(
"printers" to results,
"labelStats" to stats,
"summary" to buildSummary(results),
)
}

@Transactional(readOnly = true)
open fun listStatus(): Map<String, Any> {
val rows = jdbcDao.queryForList(
"""
SELECT
p.id,
p.code,
p.name,
p.type,
p.brand,
p.ip,
p.port,
p.last_label_odometer AS lastLabelOdometer,
p.last_label_odometer_at AS lastLabelOdometerAt,
log.odometer_total AS odometerTotal,
log.delta_since_previous AS deltaSincePrevious,
log.reachable AS lastReachable,
log.latency_ms AS latencyMs,
log.error_message AS errorMessage,
log.host_status_snippet AS hostStatusSnippet,
log.recorded_at AS lastCheckAt
FROM printer p
LEFT JOIN printer_label_odometer_log log ON log.id = (
SELECT l2.id
FROM printer_label_odometer_log l2
WHERE l2.printer_id = p.id
ORDER BY l2.id DESC
LIMIT 1
)
WHERE p.deleted = 0
AND p.type = 'Label'
ORDER BY p.name, p.code, p.id
""".trimIndent(),
)
val results = rows.map { rowToPrinterStatus(it) }
val today = LocalDate.now()
val stats = labelPrintStats(today.atStartOfDay(), LocalDateTime.now())
return mapOf(
"printers" to results,
"labelStats" to stats,
"summary" to buildSummary(results),
)
}

@Transactional(readOnly = true)
open fun labelPrintStats(from: LocalDateTime, to: LocalDateTime): Map<String, Any> {
require(!from.isAfter(to)) { "fromDateTime must be before toDateTime" }
require(!to.isAfter(from.plusDays(historyMaxRangeDays))) {
"Date range must not exceed $historyMaxRangeDays days"
}

val totalRow = jdbcDao.queryForList(
"""
SELECT COALESCE(SUM(qty), 0) AS total
FROM py_job_order_print_submit
WHERE deleted = 0
AND print_channel = :channel
AND created >= :from
AND created <= :to
""".trimIndent(),
mapOf(
"channel" to PyPrintChannel.LABEL,
"from" to from,
"to" to to,
),
).firstOrNull()

val todayStart = LocalDate.now().atStartOfDay()
val todayRow = jdbcDao.queryForList(
"""
SELECT COALESCE(SUM(qty), 0) AS total
FROM py_job_order_print_submit
WHERE deleted = 0
AND print_channel = :channel
AND created >= :from
AND created <= :to
""".trimIndent(),
mapOf(
"channel" to PyPrintChannel.LABEL,
"from" to todayStart,
"to" to LocalDateTime.now(),
),
).firstOrNull()

val recent = jdbcDao.queryForList(
"""
SELECT
s.id,
s.job_order_id AS jobOrderId,
s.qty,
s.created,
jo.code AS jobCode
FROM py_job_order_print_submit s
LEFT JOIN job_order jo ON jo.id = s.job_order_id
WHERE s.deleted = 0
AND s.print_channel = :channel
AND s.created >= :from
AND s.created <= :to
ORDER BY s.created DESC, s.id DESC
LIMIT 50
""".trimIndent(),
mapOf(
"channel" to PyPrintChannel.LABEL,
"from" to from,
"to" to to,
),
)

return mapOf(
"todayTotal" to (numberValue(todayRow?.get("total")) ?: 0L),
"rangeTotal" to (numberValue(totalRow?.get("total")) ?: 0L),
"from" to from.toString(),
"to" to to.toString(),
"recentSubmits" to recent,
)
}

private fun labelPrinters(): List<Printer> =
printerRepository.findAllByDeletedIsFalse().filter { it.type == "Label" }

private fun checkAndPersist(printer: Printer): Map<String, Any?> {
val printerId = printer.id ?: return emptyMap()
val now = LocalDateTime.now()
val probe = probeTcp(printer.ip?.trim().orEmpty(), printer.port ?: defaultPort)
val status = connectivityStatus(printer.ip, probe)

var odometerTotal: Long? = null
var deltaSincePrevious: Long? = null
var hostStatusSnippet: String? = null
var odometerError: String? = null

val previousOdometer = loadPreviousOdometer(printerId)

if (
probe.reachable &&
zebraOdometerEnabled &&
ZebraLinkOsClient.isZebraBrand(printer.brand)
) {
val ip = printer.ip?.trim().orEmpty()
val port = printer.port ?: defaultPort
val zebra = ZebraLinkOsClient.queryOdometer(
ip = ip,
port = port,
connectTimeoutMs = connectTimeoutMs,
queryTimeoutMs = zebraQueryTimeoutMs,
)
odometerTotal = zebra.totalLabelCount
hostStatusSnippet = zebra.hostStatusSnippet
odometerError = zebra.errorMessage
if (odometerTotal != null && previousOdometer != null && odometerTotal >= previousOdometer) {
deltaSincePrevious = odometerTotal - previousOdometer
}
}

val combinedError = when {
!probe.reachable -> probe.errorMessage
odometerError != null && ZebraLinkOsClient.isZebraBrand(printer.brand) && zebraOdometerEnabled ->
odometerError
else -> null
}

jdbcDao.executeUpdate(
"""
INSERT INTO printer_label_odometer_log (
printer_id, odometer_total, delta_since_previous,
reachable, latency_ms, error_message, host_status_snippet, recorded_at
) VALUES (
:printerId, :odometerTotal, :deltaSincePrevious,
:reachable, :latencyMs, :error, :hostStatus, :recordedAt
)
""".trimIndent(),
mapOf(
"printerId" to printerId,
"odometerTotal" to odometerTotal,
"deltaSincePrevious" to deltaSincePrevious,
"reachable" to if (probe.reachable) 1 else 0,
"latencyMs" to probe.latencyMs,
"error" to combinedError?.take(255),
"hostStatus" to hostStatusSnippet?.take(500),
"recordedAt" to now,
),
)

if (odometerTotal != null) {
jdbcDao.executeUpdate(
"""
UPDATE printer SET
last_label_odometer = :odometer,
last_label_odometer_at = :checkedAt
WHERE id = :id
""".trimIndent(),
mapOf(
"id" to printerId,
"odometer" to odometerTotal,
"checkedAt" to now,
),
)
}

return mapOf(
"id" to printerId,
"code" to printer.code,
"name" to printer.name,
"type" to printer.type,
"brand" to printer.brand,
"ip" to printer.ip,
"port" to (printer.port ?: defaultPort),
"status" to status,
"reachable" to probe.reachable,
"latencyMs" to probe.latencyMs,
"errorMessage" to combinedError,
"odometerTotal" to odometerTotal,
"deltaSincePrevious" to deltaSincePrevious,
"lastLabelOdometer" to (odometerTotal ?: previousOdometer),
"hostStatusSnippet" to hostStatusSnippet,
"zebraOdometerEnabled" to zebraOdometerEnabled,
"lastCheckAt" to now,
)
}

private fun loadPreviousOdometer(printerId: Long): Long? {
val row = jdbcDao.queryForList(
"SELECT last_label_odometer AS v FROM printer WHERE id = :id",
mapOf("id" to printerId),
).firstOrNull() ?: return null
return numberValue(row["v"])
}

private fun rowToPrinterStatus(row: Map<String, Any>): Map<String, Any?> {
val ip = row["ip"]?.toString()?.trim().orEmpty()
val lastReachable = row["lastReachable"]
val reachable = when (lastReachable) {
null -> null
true, 1, 1L -> true
else -> false
}
val status = when {
ip.isBlank() -> "unconfigured"
reachable == null -> "unchecked"
reachable == true -> "online"
else -> "offline"
}
return mapOf(
"id" to row["id"],
"code" to row["code"],
"name" to row["name"],
"type" to row["type"],
"brand" to row["brand"],
"ip" to row["ip"],
"port" to (row["port"] ?: defaultPort),
"status" to status,
"reachable" to reachable,
"latencyMs" to row["latencyMs"],
"errorMessage" to row["errorMessage"],
"odometerTotal" to numberValue(row["odometerTotal"] ?: row["lastLabelOdometer"]),
"deltaSincePrevious" to numberValue(row["deltaSincePrevious"]),
"lastLabelOdometerAt" to row["lastLabelOdometerAt"],
"hostStatusSnippet" to row["hostStatusSnippet"],
"zebraOdometerEnabled" to zebraOdometerEnabled,
"lastCheckAt" to row["lastCheckAt"],
)
}

private fun connectivityStatus(ip: String?, probe: ProbeResult): String {
val trimmed = ip?.trim().orEmpty()
return when {
trimmed.isBlank() -> "unconfigured"
probe.reachable -> "online"
else -> "offline"
}
}

private fun probeTcp(ip: String, port: Int): ProbeResult {
if (ip.isBlank()) {
return ProbeResult(reachable = false, latencyMs = null, errorMessage = "未設定 IP")
}
val start = System.currentTimeMillis()
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress(ip, port), connectTimeoutMs)
}
ProbeResult(
reachable = true,
latencyMs = (System.currentTimeMillis() - start).toInt(),
errorMessage = null,
)
} catch (e: Exception) {
ProbeResult(
reachable = false,
latencyMs = null,
errorMessage = e.message?.take(200) ?: e.javaClass.simpleName,
)
}
}

private fun buildSummary(results: List<Map<String, Any?>>): Map<String, Any> {
return mapOf(
"total" to results.size,
"online" to results.count { it["status"] == "online" },
"offline" to results.count { it["status"] == "offline" },
"unconfigured" to results.count { it["status"] == "unconfigured" },
"unchecked" to results.count { it["status"] == "unchecked" },
)
}

private fun numberValue(value: Any?): Long? = when (value) {
null -> null
is Number -> value.toLong()
else -> value.toString().trim().toLongOrNull()
}
}

+ 62
- 0
src/main/java/com/ffii/fpsms/modules/monitoring/web/LabelPrinterMonitorController.kt Parādīt failu

@@ -0,0 +1,62 @@
package com.ffii.fpsms.modules.monitoring.web

import com.ffii.fpsms.modules.monitoring.service.LabelPrinterMonitorService
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeParseException

@RestController
@RequestMapping("/label-printer-monitor")
@ConditionalOnProperty(prefix = "fpsms.monitoring", name = ["enabled"], havingValue = "true")
class LabelPrinterMonitorController(
private val labelPrinterMonitorService: LabelPrinterMonitorService,
) {

@GetMapping("/status")
fun status(): ResponseEntity<Map<String, Any>> {
return ResponseEntity.ok(labelPrinterMonitorService.listStatus())
}

@PostMapping("/check")
fun checkNow(): ResponseEntity<Map<String, Any>> {
return ResponseEntity.ok(labelPrinterMonitorService.checkAllLabelPrinters())
}

@GetMapping("/label-stats")
fun labelStats(
@RequestParam fromDateTime: String,
@RequestParam toDateTime: String,
): ResponseEntity<Any> {
val from = parseDateTimeParam(fromDateTime, endOfDay = false)
?: return ResponseEntity.badRequest().body(mapOf("error" to "Invalid fromDateTime"))
val to = parseDateTimeParam(toDateTime, endOfDay = true)
?: return ResponseEntity.badRequest().body(mapOf("error" to "Invalid toDateTime"))
if (from.isAfter(to)) {
return ResponseEntity.badRequest().body(mapOf("error" to "fromDateTime must be before toDateTime"))
}
return ResponseEntity.ok(labelPrinterMonitorService.labelPrintStats(from, to))
}

private fun parseDateTimeParam(raw: String, endOfDay: Boolean): LocalDateTime? {
val text = raw.trim()
if (text.isEmpty()) return null
return try {
when {
text.length <= 10 -> {
val d = LocalDate.parse(text.take(10))
if (endOfDay) d.atTime(23, 59, 59) else d.atStartOfDay()
}
else -> LocalDateTime.parse(text.replace(" ", "T").take(19))
}
} catch (_: DateTimeParseException) {
null
}
}
}

+ 121
- 0
src/main/java/com/ffii/fpsms/modules/monitoring/zebra/ZebraLinkOsClient.kt Parādīt failu

@@ -0,0 +1,121 @@
package com.ffii.fpsms.modules.monitoring.zebra

import java.io.IOException
import java.net.InetSocketAddress
import java.net.Socket
import java.nio.charset.StandardCharsets

data class ZebraOdometerResult(
val totalLabelCount: Long?,
val hostStatusSnippet: String?,
val errorMessage: String?,
)

object ZebraLinkOsClient {

private const val ODOMETER_VAR = "odometer.total_label_count"

fun isZebraBrand(brand: String?): Boolean =
brand?.contains("zebra", ignoreCase = true) == true

fun queryOdometer(
ip: String,
port: Int,
connectTimeoutMs: Int,
queryTimeoutMs: Int,
includeHostStatus: Boolean = true,
): ZebraOdometerResult {
val safeIp = ip.trim()
if (safeIp.isEmpty()) {
return ZebraOdometerResult(null, null, "IP is blank")
}
return try {
Socket().use { socket ->
socket.tcpNoDelay = true
socket.soTimeout = queryTimeoutMs.coerceAtLeast(500)
socket.connect(InetSocketAddress(safeIp, port), connectTimeoutMs.coerceAtLeast(500))
val out = socket.getOutputStream()
val cmd = """! U1 getvar "$ODOMETER_VAR"""".toByteArray(StandardCharsets.US_ASCII) + "\r\n".toByteArray()
out.write(cmd)
out.flush()
val odometerBytes = readAvailable(socket, queryTimeoutMs)
val odometerText = String(odometerBytes, StandardCharsets.UTF_8).trim()
val total = parseOdometerValue(odometerText)

val hostSnippet = if (includeHostStatus) {
out.write("~HS\r\n".toByteArray(StandardCharsets.US_ASCII))
out.flush()
val hsBytes = readAvailable(socket, queryTimeoutMs)
val hs = String(hsBytes, StandardCharsets.UTF_8).trim()
hs.take(500).ifBlank { null }
} else {
null
}

if (total == null) {
ZebraOdometerResult(
totalLabelCount = null,
hostStatusSnippet = hostSnippet,
errorMessage = odometerText.ifBlank { "No odometer response" }.take(255),
)
} else {
ZebraOdometerResult(
totalLabelCount = total,
hostStatusSnippet = hostSnippet,
errorMessage = null,
)
}
}
} catch (e: IOException) {
ZebraOdometerResult(null, null, e.message?.take(255) ?: e.javaClass.simpleName)
} catch (e: Exception) {
ZebraOdometerResult(null, null, e.message?.take(255) ?: e.javaClass.simpleName)
}
}

private fun readAvailable(socket: Socket, waitMs: Int): ByteArray {
val input = socket.getInputStream()
val buffer = ByteArray(4096)
val chunks = ArrayList<ByteArray>()
var total = 0
val deadline = System.currentTimeMillis() + waitMs.coerceAtLeast(300)
while (System.currentTimeMillis() < deadline && total < 16384) {
val available = try {
input.available()
} catch (_: IOException) {
break
}
if (available > 0) {
val n = input.read(buffer, 0, minOf(buffer.size, available))
if (n <= 0) break
chunks.add(buffer.copyOf(n))
total += n
continue
}
if (total > 0) break
try {
Thread.sleep(50)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
break
}
}
if (chunks.isEmpty()) return ByteArray(0)
val merged = ByteArray(total)
var offset = 0
for (chunk in chunks) {
System.arraycopy(chunk, 0, merged, offset, chunk.size)
offset += chunk.size
}
return merged
}

internal fun parseOdometerValue(raw: String): Long? {
val text = raw.trim()
if (text.isEmpty()) return null
val unquoted = text.trim('"', '\'', ' ')
val digits = unquoted.filter { it.isDigit() }
if (digits.isEmpty()) return null
return digits.toLongOrNull()
}
}

+ 104
- 62
src/main/resources/DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml Parādīt failu

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.17.0.final using JasperReports Library version 6.17.0-6d93193241dd8cc42629e188b94f9e0bc5722efd -->
<!-- Created with Jaspersoft Studio version 6.20.6.final using JasperReports Library version 6.20.6-5c96b6aa8a39ac1dc6b6bea4b81168e16dd39231 -->
<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="405" pageHeight="283" columnWidth="405" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="0" uuid="baa9f270-b398-4f1c-b01e-ba216b7997e9">
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
@@ -16,167 +16,209 @@
<parameter name="deliveryNoteCode" class="java.lang.String"/>
<parameter name="truckNo" class="java.lang.String"/>
<parameter name="shopCode" class="java.lang.String"/>
<parameter name="shopCodeAbbr" class="java.lang.String"/>
<queryString>
<![CDATA[]]>
</queryString>
<field name="cartonIndex" class="java.lang.Integer"/>
<field name="cartonTotal" class="java.lang.Integer"/>
<background>
<band splitType="Stretch"/>
<band height="7" splitType="Stretch"/>
</background>
<detail>
<band height="243" splitType="Stretch">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<textField>
<reportElement x="155" y="210" width="240" height="30" uuid="8fac39f8-4936-43a5-8e1f-1afbc8ccca9c">
<textField textAdjust="ScaleFont">
<reportElement x="126" y="211" width="164" 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>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16"/>
<font fontName="微軟正黑體" size="14"/>
</textElement>
<textFieldExpression><![CDATA[$P{shopPurchaseOrderNo}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="15" y="210" width="140" height="30" uuid="e03fcb92-259c-4427-a68e-60fe5924d763">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<reportElement x="14" y="211" width="112" height="30" uuid="e03fcb92-259c-4427-a68e-60fe5924d763">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16"/>
<font fontName="微軟正黑體" size="14"/>
</textElement>
<text><![CDATA[店鋪採購單編號:
]]></text>
</staticText>
<staticText>
<reportElement x="15" y="180" width="140" height="30" uuid="f3ffd4ee-0513-41a5-94d7-f1fdb9966a76">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<reportElement x="14" y="166" width="85" height="45" uuid="f3ffd4ee-0513-41a5-94d7-f1fdb9966a76">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<box>
<leftPen lineWidth="1.0" lineStyle="Solid"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16"/>
<font fontName="微軟正黑體" size="14"/>
</textElement>
<text><![CDATA[送貨單編號:]]></text>
</staticText>
<textField>
<reportElement x="155" y="180" width="240" height="30" uuid="4319059b-9096-4c49-8275-287be93d3e6a">
<reportElement x="99" y="166" width="191" height="45" uuid="4319059b-9096-4c49-8275-287be93d3e6a">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<box>
<rightPen lineWidth="1.0" lineStyle="Solid"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16"/>
<font fontName="微軟正黑體" size="14"/>
</textElement>
<textFieldExpression><![CDATA[$P{deliveryNoteCode}]]></textFieldExpression>
</textField>
<textField textAdjust="ScaleFont">
<reportElement x="13" y="0" width="220" height="50" uuid="9a440925-1bd4-4001-9b4b-7163ac27551e">
<reportElement x="200" y="6" width="194" height="130" uuid="ed6f8ce7-e351-4eeb-9f95-49d64e7ed2dd">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="38" isBold="true" isUnderline="false"/>
<box>
<pen lineWidth="1.0"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="300" isBold="true" isUnderline="false"/>
</textElement>
<textFieldExpression><![CDATA[$P{shopCode}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="15" y="150" width="140" height="30" uuid="c8b9fafb-9e8b-479f-9a9f-dadda7854f95">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16"/>
</textElement>
<text><![CDATA[貨車班次:
]]></text>
</staticText>
<textField>
<reportElement x="155" y="150" width="240" height="30" uuid="57f8e4fa-cea0-42c5-b9e5-a33f0a2710b8">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{truckNo}]]></textFieldExpression>
</textField>
<line>
<reportElement x="15" y="140" width="380" height="1" uuid="3e37c027-d6e9-4a88-b64d-58ba1dd3b22e">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
</line>
<textField textAdjust="ScaleFont">
<reportElement x="243" y="0" width="152" height="99" uuid="ed6f8ce7-e351-4eeb-9f95-49d64e7ed2dd">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<reportElement x="14" y="6" width="186" height="130" uuid="75a47bc6-5830-4636-9c62-1285163bf0b6">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<box>
<pen lineWidth="4.0"/>
<pen lineWidth="1.0" lineStyle="Solid"/>
<topPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="100" isBold="true" isUnderline="false"/>
</textElement>
<textFieldExpression><![CDATA[$P{shopCodeAbbr}]]></textFieldExpression>
</textField>
<textField textAdjust="ScaleFont">
<reportElement x="13" y="50" width="220" height="49" uuid="75a47bc6-5830-4636-9c62-1285163bf0b6">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="22" isBold="true" isUnderline="false"/>
<font fontName="微軟正黑體" size="30" isBold="true" isUnderline="false"/>
</textElement>
<textFieldExpression><![CDATA[$P{shopName}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="15" y="106" width="140" height="30" uuid="0ccaeebc-681b-449e-b547-97fc86c35662">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<reportElement x="14" y="136" width="55" height="30" uuid="0ccaeebc-681b-449e-b547-97fc86c35662">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<box>
<pen lineWidth="0.25" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineColor="#000000"/>
<leftPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16"/>
</textElement>
<text><![CDATA[箱數:]]></text>
</staticText>
<staticText>
<reportElement x="275" y="106" width="120" height="30" uuid="05bc180b-a58d-4ad8-95f6-bc3090ee2c2d">
<reportElement x="170" y="136" width="120" height="30" uuid="05bc180b-a58d-4ad8-95f6-bc3090ee2c2d">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<box>
<bottomPen lineWidth="1.0" lineStyle="Solid"/>
<rightPen lineWidth="1.0" lineStyle="Solid"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16"/>
</textElement>
<text><![CDATA[ 箱(蛋類除外)]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="155" y="106" width="40" height="30" uuid="dab335a5-e253-498c-a9bf-b9707d8e1099">
<reportElement x="69" y="136" width="40" height="30" uuid="dab335a5-e253-498c-a9bf-b9707d8e1099">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<box>
<bottomPen lineWidth="1.0" lineStyle="Solid"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font size="16"/>
</textElement>
<textFieldExpression><![CDATA[$F{cartonIndex}]]></textFieldExpression>
</textField>
<textField evaluationTime="Report" isBlankWhenNull="true">
<reportElement x="230" y="106" width="40" height="30" uuid="66d50bad-7b39-49c1-b127-4d763133ee0c">
<reportElement x="130" y="136" width="40" height="30" uuid="66d50bad-7b39-49c1-b127-4d763133ee0c">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<box>
<bottomPen lineWidth="1.0" lineStyle="Solid"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font size="16"/>
</textElement>
<textFieldExpression><![CDATA[$F{cartonTotal}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="195" y="106" width="35" height="30" uuid="73c18ae5-a07b-4215-a753-fa72d6db87eb">
<reportElement x="109" y="136" width="21" height="30" uuid="73c18ae5-a07b-4215-a753-fa72d6db87eb">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<box>
<bottomPen lineWidth="1.0" lineStyle="Solid"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16"/>
</textElement>
<text><![CDATA[/]]></text>
</staticText>
<staticText>
<reportElement x="290" y="136" width="104" height="19" uuid="c8b9fafb-9e8b-479f-9a9f-dadda7854f95">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<box>
<pen lineWidth="1.0"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="14"/>
</textElement>
<text><![CDATA[車線
]]></text>
</staticText>
<textField textAdjust="ScaleFont">
<reportElement x="290" y="155" width="104" height="86" uuid="57f8e4fa-cea0-42c5-b9e5-a33f0a2710b8">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<box>
<pen lineWidth="1.0" lineStyle="Solid"/>
<topPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="40" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{truckNo}]]></textFieldExpression>
</textField>
</band>
</detail>
</jasperReport>

+ 7
- 0
src/main/resources/application.yml Parādīt failu

@@ -65,6 +65,13 @@ fpsms:
default-port: 9100
offline-event-sample-sec: 300
history-max-range-days: 30
label-printer-monitor:
zebra-odometer-enabled: true
connect-timeout-ms: 3000
zebra-query-timeout-ms: 2500
default-port: 9100
check-interval-ms: 120000
history-max-range-days: 30

spring:
servlet:


+ 29
- 0
src/main/resources/db/changelog/changes/20260617_label_printer_monitor/01_label_printer_monitor.sql Parādīt failu

@@ -0,0 +1,29 @@
--liquibase formatted sql

--changeset fpsms:printer_label_odometer_cache_columns
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'printer' AND column_name = 'last_label_odometer'
--comment: Cache latest Zebra label odometer reading for label-printer-monitor
ALTER TABLE `printer`
ADD COLUMN `last_label_odometer` bigint DEFAULT NULL,
ADD COLUMN `last_label_odometer_at` datetime DEFAULT NULL;

--changeset fpsms:printer_label_odometer_log
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'printer_label_odometer_log'
--comment: Per-check label odometer snapshot for Zebra label printers
CREATE TABLE `printer_label_odometer_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`printer_id` int NOT NULL,
`odometer_total` bigint DEFAULT NULL,
`delta_since_previous` bigint DEFAULT NULL,
`reachable` tinyint(1) NOT NULL DEFAULT 0,
`latency_ms` int DEFAULT NULL,
`error_message` varchar(255) DEFAULT NULL,
`host_status_snippet` varchar(500) DEFAULT NULL,
`recorded_at` datetime NOT NULL,
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_plol_printer_recorded` (`printer_id`, `recorded_at`),
KEY `idx_plol_recorded_at` (`recorded_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

+ 37
- 16
src/main/resources/jasper/StockTakeVarianceReport.jrxml Parādīt failu

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.17.0.final using JasperReports Library version 6.17.0-6d93193241dd8cc42629e188b94f9e0bc5722efd -->
<!-- Created with Jaspersoft Studio version 6.20.6.final using JasperReports Library version 6.20.6-5c96b6aa8a39ac1dc6b6bea4b81168e16dd39231 -->
<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="StockTakeVarianceReport" pageWidth="842" pageHeight="595" orientation="Landscape" columnWidth="802" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="f00b4a24-af11-4263-9e82-3966acf01017">
<parameter name="stockSubCategory" class="java.lang.String">
<defaultValueExpression><![CDATA["stockSubCategory"]]></defaultValueExpression>
@@ -65,10 +65,11 @@
<field name="variance" class="java.lang.String"/>
<field name="variancePercentage" class="java.lang.String"/>
<field name="totalStockTakeQty" class="java.lang.String"/>
<field name="type" class="java.lang.String"/>
<group name="Group1" keepTogether="true" preventOrphanFooter="true">
<groupExpression><![CDATA[$F{itemNo}]]></groupExpression>
<groupHeader>
<band height="19">
<band height="18">
<textField textAdjust="StretchHeight">
<reportElement x="10" y="0" width="579" height="18" uuid="89a0d4b3-860b-4fa9-b8b4-cb1d9a19b053"/>
<textElement textAlignment="Left" verticalAlignment="Top" markup="none">
@@ -235,7 +236,7 @@
<columnHeader>
<band height="20">
<staticText>
<reportElement isPrintRepeatedValues="false" x="10" y="1" width="118" height="18" isPrintInFirstWholeBand="true" uuid="12b01171-4800-444d-b50a-e9b204f526a7">
<reportElement isPrintRepeatedValues="false" x="10" y="1" width="100" height="18" isPrintInFirstWholeBand="true" uuid="12b01171-4800-444d-b50a-e9b204f526a7">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
@@ -246,7 +247,7 @@
<text><![CDATA[批號]]></text>
</staticText>
<staticText>
<reportElement isPrintRepeatedValues="false" x="130" y="1" width="98" height="18" isPrintInFirstWholeBand="true" uuid="8d6b48f0-38f2-45b9-b306-9e7c81b99d5f">
<reportElement isPrintRepeatedValues="false" x="110" y="1" width="98" height="18" isPrintInFirstWholeBand="true" uuid="8d6b48f0-38f2-45b9-b306-9e7c81b99d5f">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
@@ -257,7 +258,7 @@
<text><![CDATA[到期日]]></text>
</staticText>
<staticText>
<reportElement isPrintRepeatedValues="false" x="344" y="1" width="64" height="18" isPrintInFirstWholeBand="true" uuid="38b97df1-2013-4b2d-a48d-06015c159953">
<reportElement isPrintRepeatedValues="false" x="324" y="1" width="64" height="18" isPrintInFirstWholeBand="true" uuid="38b97df1-2013-4b2d-a48d-06015c159953">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
@@ -268,7 +269,7 @@
<text><![CDATA[盤點前存量]]></text>
</staticText>
<staticText>
<reportElement isPrintRepeatedValues="false" x="228" y="1" width="110" height="18" isPrintInFirstWholeBand="true" uuid="e95a755d-4ecb-4900-ac9a-3a6e3b9b3470">
<reportElement isPrintRepeatedValues="false" x="208" y="1" width="110" height="18" isPrintInFirstWholeBand="true" uuid="e95a755d-4ecb-4900-ac9a-3a6e3b9b3470">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
@@ -290,7 +291,7 @@
<text><![CDATA[審核時間]]></text>
</staticText>
<staticText>
<reportElement isPrintRepeatedValues="false" x="558" y="1" width="100" height="18" isPrintInFirstWholeBand="true" uuid="921c16b3-172b-43b9-a090-24d2ee88a1b2">
<reportElement isPrintRepeatedValues="false" x="520" y="1" width="82" height="18" isPrintInFirstWholeBand="true" uuid="921c16b3-172b-43b9-a090-24d2ee88a1b2">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
@@ -301,7 +302,7 @@
<text><![CDATA[盤盈虧百分比]]></text>
</staticText>
<staticText>
<reportElement isPrintRepeatedValues="false" x="481" y="1" width="74" height="18" isPrintInFirstWholeBand="true" uuid="5dfc210f-b576-472b-a8a2-9db870c19b92">
<reportElement isPrintRepeatedValues="false" x="461" y="1" width="59" height="18" isPrintInFirstWholeBand="true" uuid="5dfc210f-b576-472b-a8a2-9db870c19b92">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
@@ -312,7 +313,7 @@
<text><![CDATA[盤盈虧]]></text>
</staticText>
<staticText>
<reportElement isPrintRepeatedValues="false" x="408" y="1" width="72" height="18" isPrintInFirstWholeBand="true" uuid="1590e6c1-5f5a-46ca-84ae-a45e2d4c5a0b">
<reportElement isPrintRepeatedValues="false" x="388" y="1" width="72" height="18" isPrintInFirstWholeBand="true" uuid="1590e6c1-5f5a-46ca-84ae-a45e2d4c5a0b">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
@@ -327,12 +328,23 @@
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
</line>
<staticText>
<reportElement isPrintRepeatedValues="false" x="602" y="1" width="86" height="18" isPrintInFirstWholeBand="true" uuid="0b8d0574-7a70-46f4-a622-384e0c32f0de">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[類型]]></text>
</staticText>
</band>
</columnHeader>
<detail>
<band height="18" splitType="Stretch">
<textField textAdjust="StretchHeight">
<reportElement x="130" y="0" width="98" height="18" uuid="608e1ba1-d37b-492e-a6c5-c8e97dfaf14a">
<reportElement x="110" y="0" width="98" height="18" uuid="608e1ba1-d37b-492e-a6c5-c8e97dfaf14a">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
@@ -341,7 +353,7 @@
<textFieldExpression><![CDATA[$F{expiryDate}]]></textFieldExpression>
</textField>
<textField textAdjust="StretchHeight">
<reportElement x="10" y="0" width="118" height="18" uuid="1d7293e1-dab1-473e-bdb9-f13cc2b29e19">
<reportElement x="10" y="0" width="100" height="18" uuid="1d7293e1-dab1-473e-bdb9-f13cc2b29e19">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
@@ -351,7 +363,7 @@
<textFieldExpression><![CDATA[$F{lotNo}]]></textFieldExpression>
</textField>
<textField textAdjust="StretchHeight">
<reportElement x="344" y="0" width="64" height="18" uuid="48601d57-e240-4390-9ec7-71c77773ee86">
<reportElement x="324" y="0" width="64" height="18" uuid="48601d57-e240-4390-9ec7-71c77773ee86">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
@@ -360,7 +372,7 @@
<textFieldExpression><![CDATA[$F{currentBookBalance}]]></textFieldExpression>
</textField>
<textField textAdjust="StretchHeight">
<reportElement x="228" y="0" width="110" height="18" uuid="f8664cfc-0eb6-497a-bce6-2171e3d9e43a">
<reportElement x="208" y="0" width="110" height="18" uuid="f8664cfc-0eb6-497a-bce6-2171e3d9e43a">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
@@ -378,7 +390,7 @@
<textFieldExpression><![CDATA[$F{stockTakeDate}]]></textFieldExpression>
</textField>
<textField textAdjust="StretchHeight">
<reportElement x="408" y="0" width="72" height="18" uuid="02d11283-5166-45fd-a900-6ae62315ac0a">
<reportElement x="388" y="0" width="72" height="18" uuid="02d11283-5166-45fd-a900-6ae62315ac0a">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
@@ -387,7 +399,7 @@
<textFieldExpression><![CDATA[$F{stockTakeQty}]]></textFieldExpression>
</textField>
<textField textAdjust="StretchHeight">
<reportElement x="481" y="0" width="74" height="18" uuid="68b8a311-ac96-4df6-9b9f-fb0db60c8a2d">
<reportElement x="461" y="0" width="59" height="18" uuid="68b8a311-ac96-4df6-9b9f-fb0db60c8a2d">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
@@ -396,7 +408,7 @@
<textFieldExpression><![CDATA[$F{variance}]]></textFieldExpression>
</textField>
<textField textAdjust="StretchHeight">
<reportElement x="558" y="0" width="100" height="18" uuid="c49f3615-0417-4724-a048-127fafce1d10">
<reportElement x="520" y="0" width="82" height="18" uuid="c49f3615-0417-4724-a048-127fafce1d10">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
@@ -404,6 +416,15 @@
</textElement>
<textFieldExpression><![CDATA[$F{variancePercentage}]]></textFieldExpression>
</textField>
<textField textAdjust="StretchHeight">
<reportElement x="602" y="0" width="86" height="18" uuid="ce9b9eb3-2260-4b06-852b-21fc42b56e9a">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體"/>
</textElement>
<textFieldExpression><![CDATA[$F{type}]]></textFieldExpression>
</textField>
</band>
</detail>
</jasperReport>

Notiek ielāde…
Atcelt
Saglabāt