Kaynağa Gözat

fix for exp date

fix負數倉
Fai Luk 4 hafta önce
ebeveyn
işleme
112adad863
6 değiştirilmiş dosya ile 140 ekleme ve 33 silme
  1. +25
    -11
      python/Bag4.py
  2. +37
    -19
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt
  3. +10
    -2
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt
  4. +33
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/FlexibleExpiryDateDeserializer.kt
  5. +7
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendRequest.kt
  6. +28
    -0
      src/test/kotlin/com/ffii/fpsms/modules/jobOrder/service/LaserBag2PayloadTest.kt

+ 25
- 11
python/Bag4.py Dosyayı Görüntüle

@@ -3,7 +3,8 @@
Bag4 v4.0 – FPSMS job orders by plan date, with DataFlex expiry under Lot.

Uses the public API GET /py/job-orders and POST /py/job-order-print-submit (no login required).
Same as Bag3, plus expiry (today + defaultShelfLifeDays) on DataFlex ZPL and as the 4th laser TCP param.
Same as Bag3, plus expiry (today + defaultShelfLifeDays) on DataFlex ZPL
and as the 4th laser TCP param (EZCAD job must bind that field).

Bag3 remains the line without expiry on DataFlex; do not share settings files.

@@ -434,6 +435,26 @@ def format_expiry_print_label(d: date) -> str:
return f"Expiry Date {d.strftime('%Y%m%d')}"


def _parse_api_expiry_date(raw) -> Optional[date]:
"""ISO `yyyy-MM-dd` or Jackson date-array `[yyyy,M,d]` from @EnableWebMvc."""
if raw is None or raw == "":
return None
if isinstance(raw, (list, tuple)) and len(raw) >= 3:
try:
return date(int(raw[0]), int(raw[1]), int(raw[2]))
except (TypeError, ValueError):
return None
s = str(raw).strip()
if s.startswith("[") and "," in s:
return None
iso = s[:10]
try:
y, m, day = (int(p) for p in iso.split("-"))
return date(y, m, day)
except ValueError:
return None


def job_expiry_zpl_text(jo: dict) -> Optional[str]:
"""Expiry yyyyMMdd from print-time shelf life days, else API expiryDate."""
days = jo.get("defaultShelfLifeDays")
@@ -443,15 +464,8 @@ def job_expiry_zpl_text(jo: dict) -> Optional[str]:
days = int(days)
if isinstance(days, int) and days > 0:
return format_expiry_print_label(date.today() + timedelta(days=days))
iso = jo.get("expiryDate")
if iso:
s = str(iso).strip()[:10]
try:
y, m, day = (int(p) for p in s.split("-"))
return format_expiry_print_label(date(y, m, day))
except ValueError:
return None
return None
parsed = _parse_api_expiry_date(jo.get("expiryDate"))
return format_expiry_print_label(parsed) if parsed else None


def job_expiry_laser_param(jo: dict) -> str:
@@ -1251,7 +1265,7 @@ def send_job_to_laser(
{"itemId": itemId, "stockInLineId": stockInLineId} ; itemCode ; itemName ; [Expiry Date yyyyMMdd] ;;
conn_ref: [socket or None] - reused across calls; closed only when switching printer.
When both item_id and stock_in_line_id present, sends JSON first param; else fallback: 0;item_code;item_name;;
Expiry is the 4th field when present (same wording as DataFlex).
Expiry is the 4th field when present (same wording as DataFlex). The EZCAD job must bind it.
Returns (success, message).
"""
code_str = (item_code or "").strip().replace(";", ",")


+ 37
- 19
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt Dosyayı Görüntüle

@@ -120,9 +120,9 @@ class PlasticBagPrinterService(
private const val ONPACK_EXPIRY_BMP_HEIGHT = 79

/**
* Bag2/Bag4 laser TCP payload. When [expiryDate] is present:
* `json;itemCode;itemName;Expiry Date yyyyMMdd;;`
* otherwise the original 3-field form `json;itemCode;itemName;;`.
* Laser TCP: `json;itemCode;itemName;;` or, when [expiryDate] is set,
* `json;itemCode;itemName;Expiry Date yyyyMMdd;;`.
* The lemon EZCAD job must bind a 4th text param or it replies invalid.
*/
fun buildLaserBag2Payload(
itemId: Long?,
@@ -146,6 +146,9 @@ class PlasticBagPrinterService(
}
}

fun laserAckLooksInvalid(ack: String?): Boolean =
ack?.contains("invalid", ignoreCase = true) == true

/** ISO `yyyy-MM-dd`, compact `yyyyMMdd`, or already `Expiry Date yyyyMMdd`. */
fun formatLaserExpiryParam(expiryDate: String?): String {
val raw = expiryDate?.trim().orEmpty()
@@ -286,6 +289,18 @@ class PlasticBagPrinterService(
* Bag2.py [send_job_to_laser] / [send_job_to_laser_with_retry]: UTF-8 TCP payload and optional ack read.
*/
fun sendLaserBag2Job(request: LaserBag2SendRequest): LaserBag2SendResponse {
return try {
sendLaserBag2JobInner(request)
} catch (e: Exception) {
logger.error("sendLaserBag2Job unexpected error", e)
LaserBag2SendResponse(
success = false,
message = "送出失敗:${e.message ?: e.javaClass.simpleName}",
)
}
}

private fun sendLaserBag2JobInner(request: LaserBag2SendRequest): LaserBag2SendResponse {
val ip = (request.printerIp?.trim()?.takeIf { it.isNotEmpty() }
?: resolveLaserBag2Host()).trim()
val port = request.printerPort ?: resolveLaserBag2Port()
@@ -298,16 +313,10 @@ class PlasticBagPrinterService(
itemName = request.itemName,
expiryDate = request.expiryDate,
)
val response = if (first.success) {
LaserBag2SendResponse(
success = true,
message = first.message,
payloadSent = first.payload,
printerAck = first.printerAck,
receiveAcknowledged = first.receiveAcknowledged,
)
val chosen = if (first.success) {
first
} else {
val second = sendLaserBag2TcpOnce(
sendLaserBag2TcpOnce(
ip = ip,
port = port,
itemId = request.itemId,
@@ -316,14 +325,14 @@ class PlasticBagPrinterService(
itemName = request.itemName,
expiryDate = request.expiryDate,
)
LaserBag2SendResponse(
success = second.success,
message = second.message,
payloadSent = second.payload,
printerAck = second.printerAck,
receiveAcknowledged = second.receiveAcknowledged,
)
}
val response = LaserBag2SendResponse(
success = chosen.success,
message = chosen.message,
payloadSent = chosen.payload,
printerAck = chosen.printerAck,
receiveAcknowledged = chosen.receiveAcknowledged,
)
if (response.success && response.receiveAcknowledged) {
try {
persistLaserLastReceiveSuccess(request, response.printerAck)
@@ -436,6 +445,15 @@ class PlasticBagPrinterService(
} catch (_: SocketTimeoutException) {
// Same as Python Bag3: ignore read timeout, payload was still sent
}
if (laserAckLooksInvalid(ackRaw)) {
return LaserBag2TcpResult(
false,
"檸檬機回覆 invalid,未接受指令。",
payload,
ackRaw,
false,
)
}
val msg = if (receiveAck) {
"已送出激光機:$payload(已確認)"
} else {


+ 10
- 2
src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt Dosyayı Görüntüle

@@ -61,12 +61,20 @@ class PlasticBagPrinterController(
}

/**
* Bag2/Bag4 laser TCP protocol: `{"itemId":n,"stockInLineId":m};code;name;;` or
* Bag2/Bag4 laser TCP: `{"itemId":n,"stockInLineId":m};code;name;;` or
* `{"itemId":n,"stockInLineId":m};code;name;Expiry Date yyyyMMdd;;` when expiryDate is set.
*/
@PostMapping("/print-laser-bag2")
fun printLaserBag2(@RequestBody request: LaserBag2SendRequest): ResponseEntity<LaserBag2SendResponse> {
val resp = plasticBagPrinterService.sendLaserBag2Job(request)
val resp = try {
plasticBagPrinterService.sendLaserBag2Job(request)
} catch (e: Exception) {
logger.error("print-laser-bag2 failed", e)
LaserBag2SendResponse(
success = false,
message = "送出失敗:${e.message ?: e.javaClass.simpleName}",
)
}
return if (resp.success) {
ResponseEntity.ok(resp)
} else {


+ 33
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/FlexibleExpiryDateDeserializer.kt Dosyayı Görüntüle

@@ -0,0 +1,33 @@
package com.ffii.fpsms.modules.jobOrder.web.model

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer

/**
* Accepts `yyyy-MM-dd`, compact `yyyyMMdd`, `Expiry Date yyyyMMdd`,
* or Jackson date-array `[2026,8,27]` (happens when `@EnableWebMvc` uses a raw ObjectMapper).
*/
class FlexibleExpiryDateDeserializer : JsonDeserializer<String>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): String? {
return when (p.currentToken) {
JsonToken.VALUE_NULL -> null
JsonToken.VALUE_STRING -> p.valueAsString?.trim()?.takeIf { it.isNotEmpty() }
JsonToken.VALUE_NUMBER_INT -> p.valueAsString
JsonToken.START_ARRAY -> {
val y = p.nextIntValue(0)
val m = p.nextIntValue(0)
val d = p.nextIntValue(0)
while (p.nextToken() != JsonToken.END_ARRAY && p.currentToken != null) {
// skip extra tokens (e.g. nanoseconds on a datetime array)
}
if (y <= 0 || m <= 0 || d <= 0) null else "%04d-%02d-%02d".format(y, m, d)
}
else -> {
p.skipChildren()
null
}
}
}
}

+ 7
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendRequest.kt Dosyayı Görüntüle

@@ -1,13 +1,18 @@
package com.ffii.fpsms.modules.jobOrder.web.model

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.databind.annotation.JsonDeserialize

/**
* Body for Bag2/Bag4-style laser TCP send: `json;itemCode;itemName;;` or
* `json;itemCode;itemName;Expiry Date yyyyMMdd;;` when [expiryDate] is set (UTF-8).
* The lemon EZCAD job must bind the 4th `;` field or it replies invalid.
* Optional [printerIp] / [printerPort] override system settings [LASER_PRINT.host] / [LASER_PRINT.port].
*
* Optional job metadata is used to persist [com.ffii.fpsms.modules.common.SettingNames.LASER_PRINT_LAST_RECEIVE_SUCCESS]
* when the printer returns a receive ack.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
data class LaserBag2SendRequest(
val itemId: Long? = null,
val stockInLineId: Long? = null,
@@ -20,8 +25,9 @@ data class LaserBag2SendRequest(
val lotNo: String? = null,
/**
* Print-time expiry from the job-order list (`yyyy-MM-dd`, compact `yyyyMMdd`,
* or already `Expiry Date yyyyMMdd`). Sent as the 4th TCP field.
* Jackson `[yyyy,M,d]`, or already `Expiry Date yyyyMMdd`). Sent as the 4th TCP field.
*/
@JsonDeserialize(using = FlexibleExpiryDateDeserializer::class)
val expiryDate: String? = null,
/** AUTO (auto-send) or MANUAL (/laserPrint); optional. */
val source: String? = null,


+ 28
- 0
src/test/kotlin/com/ffii/fpsms/modules/jobOrder/service/LaserBag2PayloadTest.kt Dosyayı Görüntüle

@@ -1,6 +1,9 @@
package com.ffii.fpsms.modules.jobOrder.service

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendRequest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test

class LaserBag2PayloadTest {
@@ -25,6 +28,31 @@ class LaserBag2PayloadTest {
)
}

@Test
fun laserAckLooksInvalid_detects_plugin_reject() {
assertEquals(false, PlasticBagPrinterService.laserAckLooksInvalid(null))
assertEquals(false, PlasticBagPrinterService.laserAckLooksInvalid("receive;;"))
assertEquals(true, PlasticBagPrinterService.laserAckLooksInvalid("errorinvalid data"))
assertEquals(true, PlasticBagPrinterService.laserAckLooksInvalid("INVALID"))
}

@Test
fun sendRequest_accepts_expiry_as_iso_or_jackson_array() {
val mapper = jacksonObjectMapper()
val fromIso = mapper.readValue(
"""{"expiryDate":"2026-08-27","source":"MANUAL"}""",
LaserBag2SendRequest::class.java,
)
assertEquals("2026-08-27", fromIso.expiryDate)
val fromArray = mapper.readValue(
"""{"expiryDate":[2026,8,27],"source":"MANUAL"}""",
LaserBag2SendRequest::class.java,
)
assertEquals("2026-08-27", fromArray.expiryDate)
val missing = mapper.readValue("{}", LaserBag2SendRequest::class.java)
assertNull(missing.expiryDate)
}

@Test
fun formatLaserExpiryParam_accepts_iso_compact_and_print_label() {
assertEquals("Expiry Date 20260821", PlasticBagPrinterService.formatLaserExpiryParam("2026-08-21"))


Yükleniyor…
İptal
Kaydet