From 4b83633f28162543a422cbe852c051fc5100e07f Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Wed, 13 May 2026 15:20:32 +0800 Subject: [PATCH] updated Bag3, adding m18 BOM syn, delete the DO2_EXTRA syn, move the DO2 sync to 1pm --- python/Bag3.py | 97 ++++++-- python/__pycache__/Bag3.cpython-313.pyc | Bin 0 -> 135895 bytes .../fpsms/m18/entity/M18BomShopSyncLog.kt | 39 +++ .../m18/entity/M18BomShopSyncLogRepository.kt | 5 + .../m18/model/M18BomForShopSaveRequest.kt | 86 +++++++ .../m18/model/M18BomShopSyncTriggerResult.kt | 14 ++ .../fpsms/m18/service/M18BomForShopService.kt | 223 ++++++++++++++++++ .../ffii/fpsms/m18/web/M18TestController.kt | 12 +- .../fpsms/modules/common/SettingNames.java | 5 + .../scheduler/service/SchedulerService.kt | 42 +--- .../service/PlasticBagPrinterService.kt | 9 +- .../modules/master/service/BomService.kt | 141 +++++++++++ .../modules/master/service/ItemUomService.kt | 14 ++ .../fpsms/modules/master/web/BomController.kt | 12 + .../web/models/BomIdByItemCodeResponse.kt | 14 ++ .../20260118_fai/01_insert_scheduler.sql | 2 +- .../01_m18_bom_shop_sync_settings.sql | 20 ++ .../20260512_fai/02_m18_bom_shop_sync_log.sql | 24 ++ .../01_update_do2_schedule_to_13.sql | 4 + src/main/resources/log4j2-prod-linux.yml | 1 + src/main/resources/log4j2-prod-win.yml | 1 + src/main/resources/log4j2.yml | 1 + 22 files changed, 712 insertions(+), 54 deletions(-) create mode 100644 python/__pycache__/Bag3.cpython-313.pyc create mode 100644 src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt create mode 100644 src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt create mode 100644 src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt create mode 100644 src/main/java/com/ffii/fpsms/m18/model/M18BomShopSyncTriggerResult.kt create mode 100644 src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/master/web/models/BomIdByItemCodeResponse.kt create mode 100644 src/main/resources/db/changelog/changes/20260512_fai/01_m18_bom_shop_sync_settings.sql create mode 100644 src/main/resources/db/changelog/changes/20260512_fai/02_m18_bom_shop_sync_log.sql create mode 100644 src/main/resources/db/changelog/changes/20260513_do2_1pm/01_update_do2_schedule_to_13.sql diff --git a/python/Bag3.py b/python/Bag3.py index 63b5d83..887c401 100644 --- a/python/Bag3.py +++ b/python/Bag3.py @@ -16,6 +16,7 @@ Bag2 is kept as a separate legacy v2.x line; do not assume Bag2 matches Bag3. Run: python Bag3.py """ +import errno import json import os import select @@ -344,6 +345,25 @@ DATAFLEX_UI_PROGRESS_EVERY = max( DATAFLEX_SINGLE_TCP_JOB = _dataflex_bool_env( "FPSMS_DATAFLEX_SINGLE_TCP_JOB", False ) +# Link-OS SGD: raw-ZPL job shows this host id instead of a generic name (e.g. "ZPL. EMULATION"). +# Same id when the same job order is printed again. Disable: FPSMS_DATAFLEX_HOST_IDENTIFICATION_SGD=0 +DATAFLEX_HOST_IDENTIFICATION_SGD = _dataflex_bool_env( + "FPSMS_DATAFLEX_HOST_IDENTIFICATION_SGD", True +) +# Bag ZPL size (dots). ^PW700 matched little content (mostly vertical ^A@R), so previews showed a wide strip with empty right margin. +DATAFLEX_LABEL_PW = max( + 280, + _dataflex_int_env("FPSMS_DATAFLEX_LABEL_PW", 400), +) +DATAFLEX_LABEL_LL = max( + 200, + _dataflex_int_env("FPSMS_DATAFLEX_LABEL_LL", 500), +) +# Some Zebra/DataFlex units RST the socket on host half-close; Windows surfaces WinError 10054. +# Set FPSMS_DATAFLEX_SKIP_SHUTDOWN_WR=1 to omit shutdown(SHUT_WR) and only close() (often avoids RST). +DATAFLEX_SKIP_SHUTDOWN_WR = _dataflex_bool_env( + "FPSMS_DATAFLEX_SKIP_SHUTDOWN_WR", False +) # Full recovery (~JR soft reset) — used by「打袋重設」only; longer delay for firmware DATAFLEX_POST_FULL_RECOVERY_DELAY_SEC = 1.2 # Zebra ~RO only (used when FPSMS_DATAFLEX_NO_JR is set for full recovery) @@ -364,12 +384,56 @@ def _zpl_escape(s: str) -> str: return s.replace("\\", "\\\\").replace("^", "\\^") +def _dataflex_host_identification_sgd_prefix(job_order_id: Optional[int]) -> str: + """ + Optional ASCII prefix before ^XA: set zpl.host_identification so the printer lists the job + under the job order id instead of a generic raw-ZPL label. + """ + if not DATAFLEX_HOST_IDENTIFICATION_SGD or job_order_id is None: + return "" + try: + jid = str(int(job_order_id)) + except (TypeError, ValueError): + return "" + if not jid.isdigit(): + return "" + return f'! U1 setvar "zpl.host_identification" "{jid}"\r\n' + + def _dataflex_zpl_bytes(zpl: str) -> bytes: """UTF-8 ZPL with one trailing CRLF so the printer sees a clear job boundary.""" s = (zpl or "").rstrip("\r\n") return (s + "\r\n").encode("utf-8") +def _dataflex_is_benign_tcp_reset(err: BaseException) -> bool: + """True when peer closed with RST/FIN in a way that is normal for raw printer TCP (Windows 10054).""" + if isinstance(err, (BrokenPipeError, ConnectionResetError, ConnectionAbortedError)): + return True + if isinstance(err, OSError): + if getattr(err, "winerror", None) == 10054: # WSAECONNRESET + return True + if err.errno in ( + errno.ECONNRESET, + errno.EPIPE, + errno.ECONNABORTED, + ): + return True + return False + + +def _dataflex_shutdown_write_maybe(sock: socket.socket) -> None: + """Half-close write side; ignore printer RST (common after ZPL on port 9100-style links).""" + if DATAFLEX_SKIP_SHUTDOWN_WR: + return + try: + sock.shutdown(socket.SHUT_WR) + except OSError as e: + if _dataflex_is_benign_tcp_reset(e): + return + raise + + def generate_zpl_dataflex( batch_no: str, item_code: str, @@ -377,6 +441,7 @@ def generate_zpl_dataflex( item_id: Optional[int] = None, stock_in_line_id: Optional[int] = None, lot_no: Optional[str] = None, + job_order_id: Optional[int] = None, font_regular: str = "E:STXihei.ttf", font_bold: str = "E:STXihei.ttf", ) -> str: @@ -398,11 +463,12 @@ def generate_zpl_dataflex( qr_value = _zpl_escape(qr_payload) # Explicit ^PQ1: each ^XA…^XZ is exactly one bag. Avoids E1005 "over quantity" on some Zebra/DataFlex # firmware when many labels are sent on one TCP session without a per-job quantity. - return f"""^XA + host_id = _dataflex_host_identification_sgd_prefix(job_order_id) + return host_id + f"""^XA ^PQ1,0,1,N ^CI28 -^PW700 -^LL500 +^PW{DATAFLEX_LABEL_PW} +^LL{DATAFLEX_LABEL_LL} ^PO N ^FO10,20 ^BQN,2,4^FDQA,{qr_value}^FS @@ -447,10 +513,7 @@ def send_dataflex_preprint_reset(ip: str, port: int, *, force: bool = False) -> sock.connect((ip, port)) sock.sendall(DATAFLEX_PREPRINT_BYTES) time.sleep(DATAFLEX_POST_PREPRINT_DELAY_SEC) - try: - sock.shutdown(socket.SHUT_WR) - except OSError: - pass + _dataflex_shutdown_write_maybe(sock) finally: sock.close() @@ -472,10 +535,7 @@ def send_dataflex_job_counter_reset(ip: str, port: int, *, force: bool = False) sock.connect((ip, port)) sock.sendall(_dataflex_full_recovery_payload()) time.sleep(DATAFLEX_POST_FULL_RECOVERY_DELAY_SEC) - try: - sock.shutdown(socket.SHUT_WR) - except OSError: - pass + _dataflex_shutdown_write_maybe(sock) finally: sock.close() @@ -527,10 +587,7 @@ def send_dataflex_reset_and_labels( time.sleep(DATAFLEX_POST_LABEL_SETTLE_SEC) if i < copies - 1: time.sleep(delay_sec) - try: - sock.shutdown(socket.SHUT_WR) - except OSError: - pass + _dataflex_shutdown_write_maybe(sock) finally: sock.close() @@ -879,10 +936,7 @@ def send_zpl_to_dataflex(ip: str, port: int, zpl: str) -> None: sock.connect((ip, port)) sock.sendall(_dataflex_zpl_bytes(zpl)) time.sleep(DATAFLEX_POST_LABEL_SETTLE_SEC) - try: - sock.shutdown(socket.SHUT_WR) - except OSError: - pass + _dataflex_shutdown_write_maybe(sock) finally: sock.close() @@ -907,6 +961,10 @@ def query_dataflex_host_status(ip: str, port: int) -> str: data = sock.recv(4096) except socket.timeout: break + except OSError as ex: + if _dataflex_is_benign_tcp_reset(ex): + break + raise if not data: break chunks.append(data) @@ -2451,6 +2509,7 @@ def main() -> None: item_id=item_id, stock_in_line_id=stock_in_line_id, lot_no=lot_no, + job_order_id=j.get("id"), ) label_text = (lot_no or b).strip() if continuous: diff --git a/python/__pycache__/Bag3.cpython-313.pyc b/python/__pycache__/Bag3.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a01ec70f11042aa6ae472c4dfdffdae5b2685cbc GIT binary patch literal 135895 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 M_bTQ8sNezrKdrMNYXATM literal 0 HcmV?d00001 diff --git a/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt b/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt new file mode 100644 index 0000000..5d85df5 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt @@ -0,0 +1,39 @@ +package com.ffii.fpsms.m18.entity + +import com.ffii.core.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull + +/** + * Audit log for FPSMS → M18 udfBomForShop sync (request / response bodies). + */ +@Entity +@Table(name = "m18_bom_shop_sync_log") +open class M18BomShopSyncLog : BaseEntity() { + + @NotNull + @Column(name = "bom_id", nullable = false) + open var bomId: Long? = null + + @Column(name = "m18_record_id") + open var m18RecordId: Long? = null + + @NotNull + @Column(name = "m18_api_status", nullable = false) + open var m18ApiStatus: Boolean = false + + @NotNull + @Column(name = "synced", nullable = false) + open var synced: Boolean = false + + @Column(name = "message", length = 4000) + open var message: String? = null + + @Column(name = "request_json", columnDefinition = "LONGTEXT") + open var requestJson: String? = null + + @Column(name = "response_json", columnDefinition = "LONGTEXT") + open var responseJson: String? = null +} diff --git a/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt b/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt new file mode 100644 index 0000000..bf6c04c --- /dev/null +++ b/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt @@ -0,0 +1,5 @@ +package com.ffii.fpsms.m18.entity + +import com.ffii.core.support.AbstractRepository + +interface M18BomShopSyncLogRepository : AbstractRepository diff --git a/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt b/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt new file mode 100644 index 0000000..7b83edc --- /dev/null +++ b/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt @@ -0,0 +1,86 @@ +package com.ffii.fpsms.m18.model + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * M18 save payload for Shop BOM (udfBomForShop). + * PUT /root/api/save/udfbomforshop?menuCode=udfbomforshop + * + * Same idea as GRN (`mainan` + `ant`): header and lines each wrapped as `{ "values": [ ... ] }`. + * Root keys: **`udfbomforshop`** and **`udfproduct`** (same as M18 read [M18BomData]). + * (Spelling is **udf**, not "uni".) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +data class M18BomForShopSaveRequest( + @JsonProperty("udfbomforshop") + val udfbomforshop: M18MainUdfBomForShopWrapper, + @JsonProperty("udfproduct") + val udfproduct: M18UdfProductWrapper, +) + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class M18MainUdfBomForShopWrapper( + val values: List, +) + +/** + * Header row for udfBomForShop. Field names match M18 read/sample JSON. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +data class M18MainUdfBomForShopValue( + /** + * Existing M18 udfBomForShop header id for **update** (same as FPSMS [Bom.m18Id] after first sync). + * Omit or null for **create**. Sent as JSON string for M18 compatibility (like GRN mainan `id`). + */ + val id: String? = null, + val code: String? = null, + val beId: Int? = null, + val desc: String? = null, + @JsonProperty("desc_en") + val descEn: String? = null, + @JsonProperty("udfBOMCode") + val udfBomCode: String? = null, + val rev: String? = null, + val udfUnit: Long? = null, + /** Harvest qty: [Bom.outputQty] × pack multiple from header item stock UOM code (e.g. PACK2LB → ×2), else plain output qty. */ + val udfHarvest: String? = null, + /** Trailing unit letters from that code (e.g. LB); null if code not parsed. */ + val udfHarvestUnit: String? = null, + /** Epoch milliseconds (M18-style; same as read `lastModifyDate`). From FPSMS [com.ffii.core.entity.BaseEntity.created] in Asia/Hong_Kong. */ + @JsonProperty("udfeffectivedate") + val udfEffectiveDate: Long? = null, + @JsonProperty("udfYieldratePP") + val udfYieldratePP: Number? = null, + val udftypeoffood: String? = null, + val staffId: Int? = null, + val flowTypeId: Int? = null, + val virDeptId: Int? = null, + val status: String? = null, +) + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class M18UdfProductWrapper( + val values: List, +) + +/** + * Line payload for `udfproduct.values[]`. **`udfBaseUnit`** is the FPSMS UOM **code** for the line. + * **`udfpurchaseUnit`** / **`udfPackingUnit`** / **`udfPackingQty`** / **`udfproremark`** are not sent. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +data class M18UdfProductSaveValue( + /** Line id in M18 when updating */ + val id: Long? = null, + val udfqty: Number? = null, + val udfProduct: Long? = null, + val udfIngredients: String? = null, + /** Line UOM: [com.ffii.fpsms.modules.master.entity.UomConversion.code] (same unit as [udfqty]). */ + val udfBaseUnit: String? = null, + val udfSupplier: Long? = null, + /** Line sequence, e.g. " 1" */ + val itemNo: String? = null, + val udfoptions: String? = null, + val udfoption: Number? = null, + val udfYieldRate: Number? = null, +) diff --git a/src/main/java/com/ffii/fpsms/m18/model/M18BomShopSyncTriggerResult.kt b/src/main/java/com/ffii/fpsms/m18/model/M18BomShopSyncTriggerResult.kt new file mode 100644 index 0000000..8f78e08 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/m18/model/M18BomShopSyncTriggerResult.kt @@ -0,0 +1,14 @@ +package com.ffii.fpsms.m18.model + +/** + * Result of [com.ffii.fpsms.modules.master.service.BomService.pushBomToM18ShopIfAllowed] + * (e.g. POST /m18/test/bom-shop-sync/{bomId}). + */ +data class M18BomShopSyncTriggerResult( + val bomId: Long, + val synced: Boolean, + val skippedReason: String? = null, + val recordId: Long? = null, + val status: Boolean? = null, + val messageSummary: String? = null, +) diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt new file mode 100644 index 0000000..4ab7dbf --- /dev/null +++ b/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt @@ -0,0 +1,223 @@ +package com.ffii.fpsms.m18.service + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.ffii.fpsms.api.service.ApiCallerService +import com.ffii.fpsms.m18.M18Config +import com.ffii.fpsms.m18.model.GoodsReceiptNoteResponse +import com.ffii.fpsms.m18.model.M18BomForShopSaveRequest +import com.ffii.fpsms.m18.model.M18MainUdfBomForShopValue +import com.ffii.fpsms.m18.model.M18MainUdfBomForShopWrapper +import com.ffii.fpsms.m18.model.M18UdfProductSaveValue +import com.ffii.fpsms.m18.model.M18UdfProductWrapper +import com.ffii.fpsms.modules.master.entity.Bom +import com.ffii.fpsms.modules.master.entity.BomMaterial +import com.ffii.fpsms.modules.master.service.ItemUomService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.util.LinkedMultiValueMap +import reactor.core.publisher.Mono +import java.math.BigDecimal +import java.math.RoundingMode +import java.nio.charset.StandardCharsets +import java.time.ZoneId + +/** + * Push FPSMS BOM + materials to M18 udfBomForShop (similar to GRN save/an). + * PUT /root/api/save/udfbomforshop?menuCode=udfbomforshop + */ +@Service +open class M18BomForShopService( + private val m18Config: M18Config, + private val apiCallerService: ApiCallerService, + private val itemUomService: ItemUomService, +) { + private val logger: Logger = LoggerFactory.getLogger(M18BomForShopService::class.java) + + private val savePath = "/root/api/save/udfbomforshop" + private val menuCode = "udfbomforshop" + + /** M18 business entity id for udfBomForShop header (`udfbomforshop.values[0].beId`). */ + private val bomShopMainBeId: Int = 29 + + /** + * Stock UOM `code` on the **BOM header item** (e.g. PACK2LB = prefix + pack multiple + unit suffix). + * [udfHarvest] = [Bom.outputQty] × middle number; [udfHarvestUnit] = trailing unit (e.g. LB). + */ + private val bomItemStockUomPackCodeRegex = Regex("^([A-Za-z]+)(\\d+)([A-Za-z]+)$") + + companion object { + private const val HARVEST_CALC_SCALE = 10 + private val m18Tz: ZoneId = ZoneId.of("Asia/Hong_Kong") + } + + @Suppress("DEPRECATION") + private val objectMapper: ObjectMapper = jacksonObjectMapper().apply { + disable(JsonGenerator.Feature.ESCAPE_NON_ASCII) + } + + /** + * Builds M18 save body from a persisted BOM (materials loaded). + * [headerM18IdOverride] optional M18 header record id (e.g. from `/bom/by-item-code` `bomM18Id`) when DB column is stale. + * Otherwise uses [Bom.m18Id] when set for **update**; omitted for **create**. + * Returns null if required M18 ids are missing (caller should log and skip). + */ + open fun buildSaveRequest(bom: Bom, headerM18IdOverride: Long? = null): M18BomForShopSaveRequest? { + val code = bom.code ?: return null + val flowTypeId = resolveFlowTypeId(code) + val udfUnit = bom.uom?.m18Id?.takeIf { it > 0 } ?: return null + val outputQty = bom.outputQty ?: BigDecimal.ZERO + val (udfHarvest, udfHarvestUnit) = resolveUdfHarvestFields(bom, outputQty) + val udfEffectiveDate = bom.created?.atZone(m18Tz)?.toInstant()?.toEpochMilli() + + val effectiveHeaderM18Id = + headerM18IdOverride?.takeIf { it > 0 } ?: bom.m18Id?.takeIf { it > 0 } + val header = M18MainUdfBomForShopValue( + id = effectiveHeaderM18Id?.toString(), + code = code, + beId = bomShopMainBeId, + desc = bom.name ?: bom.description, + descEn = bom.name ?: bom.description, + udfBomCode = deriveUdfBomCode(code), + rev = deriveRev(code), + udfUnit = udfUnit, + udfHarvest = udfHarvest, + udfHarvestUnit = udfHarvestUnit, + udfEffectiveDate = udfEffectiveDate, + udfYieldratePP = bom.yield, + udftypeoffood = "半成品", + staffId = 232, + flowTypeId = flowTypeId, + virDeptId = 117, + status = "Y", + ) + + val lines = bom.bomMaterials + .filter { it.deleted != true } + .sortedBy { it.id ?: 0L } + .mapIndexedNotNull { idx, mat -> toProductLine(mat, idx + 1) } + + if (lines.isEmpty()) { + logger.warn("[M18 BOM] BOM id=${bom.id} code=$code has no materials; skipping M18 save") + return null + } + + logger.info( + "[M18 BOM] buildSaveRequest fpsmsBomId=${bom.id} code=$code mainM18Id=$effectiveHeaderM18Id " + + "(m18HeaderIdOverride=$headerM18IdOverride, bom.m18Id=${bom.m18Id})", + ) + + return M18BomForShopSaveRequest( + udfbomforshop = M18MainUdfBomForShopWrapper(values = listOf(header)), + udfproduct = M18UdfProductWrapper(values = lines), + ) + } + + /** + * From the **finished-good** [Bom.item] stock unit [com.ffii.fpsms.modules.master.entity.UomConversion.code] + * (pattern `LETTER_PREFIX` + `DIGITS` + `UNIT_SUFFIX`, e.g. PACK2LB): harvest qty = outputQty × digits, unit = suffix. + * Falls back to plain [outputQty] and null unit when item/stock UOM/code is missing or does not match. + */ + private fun resolveUdfHarvestFields(bom: Bom, outputQty: BigDecimal): Pair { + val itemId = bom.item?.id + if (itemId == null) { + logger.warn("[M18 BOM] bom.item id missing; udfHarvest=outputQty only. bomId=${bom.id}") + return outputQty.stripTrailingZeros().toPlainString() to null + } + val stockCode = itemUomService.findStockUnitByItemId(itemId)?.uom?.code?.trim().orEmpty() + if (stockCode.isEmpty()) { + logger.warn("[M18 BOM] stock UOM code missing for bom itemId=$itemId; udfHarvest=outputQty only. bomId=${bom.id}") + return outputQty.stripTrailingZeros().toPlainString() to null + } + val match = bomItemStockUomPackCodeRegex.matchEntire(stockCode) + if (match == null) { + logger.warn( + "[M18 BOM] stock UOM code '$stockCode' does not match PREFIX+NUMBER+SUFFIX; " + + "udfHarvest=outputQty only. bomId=${bom.id} itemId=$itemId", + ) + return outputQty.stripTrailingZeros().toPlainString() to null + } + val mult = match.groupValues[2].toBigDecimalOrNull() + if (mult == null || mult.compareTo(BigDecimal.ZERO) <= 0) { + logger.warn( + "[M18 BOM] invalid pack multiple in stock UOM code '$stockCode'; udfHarvest=outputQty only. bomId=${bom.id}", + ) + return outputQty.stripTrailingZeros().toPlainString() to null + } + val unitSuffix = match.groupValues[3] + val harvestQty = outputQty.multiply(mult).setScale(HARVEST_CALC_SCALE, RoundingMode.HALF_UP).stripTrailingZeros() + return harvestQty.toPlainString() to unitSuffix + } + + private fun toProductLine(mat: BomMaterial, lineNo: Int): M18UdfProductSaveValue? { + val proId = mat.item?.m18Id?.takeIf { it > 0 } ?: run { + logger.warn("[M18 BOM] material item m18Id missing bomMaterialId=${mat.id} itemId=${mat.item?.id}") + return null + } + val udfBaseUnit = mat.uom?.code?.trim()?.takeIf { it.isNotEmpty() } ?: run { + logger.warn("[M18 BOM] material UOM code missing bomMaterialId=${mat.id}") + return null + } + val udfqty = (mat.qty ?: BigDecimal.ZERO).setScale(8, RoundingMode.HALF_UP).toDouble() + return M18UdfProductSaveValue( + id = mat.m18Id?.takeIf { it > 0 }, + udfqty = udfqty, + udfProduct = proId, + udfIngredients = mat.itemName ?: mat.item?.name, + udfBaseUnit = udfBaseUnit, + udfSupplier = 0L, + itemNo = String.format("%6d", lineNo), + udfoptions = "", + udfoption = 0.0, + udfYieldRate = 0.0, + ) + } + + private fun deriveUdfBomCode(fullCode: String): String { + val v = Regex("^(.*)V(\\d+)$").find(fullCode) + return if (v != null) v.groupValues[1] else fullCode + } + + private fun deriveRev(fullCode: String): String? { + val v = Regex("^.*V(\\d+)$").find(fullCode) ?: return null + return v.groupValues[1] + } + + private fun resolveFlowTypeId(code: String): Int = when { + code.startsWith("TOA") -> 1 + code.startsWith("BOMPP") || code.startsWith("PP") -> 3 + code.startsWith("BOMPF") || code.startsWith("PF") || code.startsWith("PFP") -> 2 + else -> 1 + } + + open fun toJson(request: M18BomForShopSaveRequest): String = + objectMapper.writeValueAsString(request) + + open fun toJson(response: GoodsReceiptNoteResponse): String = + objectMapper.writeValueAsString(response) + + open fun saveBomForShop(request: M18BomForShopSaveRequest): GoodsReceiptNoteResponse? = + saveBomForShopMono(request).block() + + open fun saveBomForShopMono(request: M18BomForShopSaveRequest): Mono { + val queryParams = LinkedMultiValueMap().apply { + add("menuCode", menuCode) + } + val qs = queryParams.entries.flatMap { (k, v) -> v.map { "$k=$it" } }.joinToString("&") + val fullUrl = "${m18Config.BASE_URL}$savePath?$qs" + val bodyJson = objectMapper.writeValueAsString(request) + logger.info("[M18 BOM udfBomForShop] PUT url=$fullUrl bodyUtf8Bytes=${bodyJson.toByteArray(StandardCharsets.UTF_8).size}") + logger.debug("[M18 BOM udfBomForShop] PUT body=$bodyJson") + return apiCallerService.putWithJsonString( + urlPath = savePath, + queryParams = queryParams, + bodyJson = bodyJson, + ).doOnSuccess { r -> + logger.info("[M18 BOM udfBomForShop] response status=${r.status} recordId=${r.recordId} messages=${r.messages}") + }.doOnError { e -> + logger.error("[M18 BOM udfBomForShop] failed: ${e.message}", e) + } + } +} diff --git a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt index 5fbafb9..4da2df9 100644 --- a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt +++ b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt @@ -4,8 +4,9 @@ import com.ffii.core.utils.JwtTokenUtil import com.ffii.fpsms.m18.M18Config import com.ffii.fpsms.m18.model.SyncResult import com.ffii.fpsms.m18.service.* +import com.ffii.fpsms.m18.model.M18BomShopSyncTriggerResult import com.ffii.fpsms.m18.web.models.M18CommonRequest -import com.ffii.fpsms.modules.common.SettingNames +import com.ffii.fpsms.modules.master.service.BomService import com.ffii.fpsms.modules.common.scheduler.service.SchedulerService import com.ffii.fpsms.modules.master.entity.ItemUom import com.ffii.fpsms.modules.master.entity.Items @@ -35,6 +36,7 @@ class M18TestController ( private val m18DeliveryOrderService: M18DeliveryOrderService, val schedulerService: SchedulerService, private val settingsService: SettingsService, + private val bomService: BomService, ) { var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) @@ -65,6 +67,14 @@ class M18TestController ( return schedulerService.getM18Pos(); } + @PostMapping("/test/bom-shop-sync/{bomId}") + fun testBomShopSync( + @PathVariable bomId: Long, + @RequestParam(required = false) m18HeaderId: Long?, + ): M18BomShopSyncTriggerResult { + return bomService.pushBomToM18ShopIfAllowed(bomId, m18HeaderId) + } + @GetMapping("/test/po-by-code") fun testSyncPoByCode(@RequestParam code: String): SyncResult { return m18PurchaseOrderService.savePurchaseOrderByCode(code) diff --git a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java index 61cb2b7..e0fa020 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java +++ b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java @@ -41,6 +41,11 @@ public abstract class SettingNames { */ public static final String M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE = "M18.units.sync.initialFullSyncDone"; + /** + * When "true", FPSMS may push BOM header + materials to M18 udfBomForShop. + */ + public static final String M18_BOM_SHOP_SYNC_ENABLED = "M18.bom.shop.sync.enabled"; + /** Post completed DN and process M18 GRN (cron, e.g. "0 40 23 * * *" for 23:40 daily) */ public static final String SCHEDULE_POST_COMPLETED_DN_GRN = "SCHEDULE.postCompletedDn.grn"; diff --git a/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt b/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt index ba3b879..beaabe3 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt +++ b/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt @@ -57,6 +57,12 @@ open class SchedulerService( val m18GrnCodeSyncService: M18GrnCodeSyncService, val inventoryLotLineService: InventoryLotLineService, ) { + companion object { + /** DO2: Spring 6-field cron default and M18 `lastModifyDate` upper bound hour (1pm local). */ + const val DO2_MODIFIED_TO_HOUR: Int = 13 + const val DO2_DEFAULT_CRON: String = "0 0 13 * * *" + } + 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") @@ -206,7 +212,7 @@ open class SchedulerService( logger.info("M18 DO2 scheduler disabled (scheduler.m18Sync.enabled=false)") return } - scheduledM18Do2 = commonSchedule(scheduledM18Do2, SettingNames.SCHEDULE_M18_DO2, ::getM18Dos2) + scheduledM18Do2 = commonSchedule(scheduledM18Do2, SettingNames.SCHEDULE_M18_DO2, DO2_DEFAULT_CRON, ::getM18Dos2) } fun scheduleM18MasterData() { @@ -455,7 +461,7 @@ open class SchedulerService( val ysd = today.minusDays(1L) val tmr = today.plusDays(1L) - // Default: lastModified from yesterday 19:00 (aligns with nightly DO2 expectation). + // Default: lastModified from yesterday 19:00 through today's DO2 run hour (1pm; aligns with SCHEDULE.m18.do2). // On Sunday, yesterday is Saturday: use 03:00 instead so we include DO changed after Sat 03:10 DO1 // (otherwise Sat 03:00–18:59 would be skipped until a much later sync). val isSundayDo2 = runDate.dayOfWeek == DayOfWeek.SUNDAY @@ -465,21 +471,21 @@ open class SchedulerService( ysd.withHour(19).withMinute(0).withSecond(0) } - // Set to 11:00:00 of today - val todayEleven = today.withHour(11).withMinute(0).withSecond(0) + val modifiedDateToEnd = + today.withHour(DO2_MODIFIED_TO_HOUR).withMinute(0).withSecond(0) logger.info( "DO2 modifiedDateFrom={} ({}), modifiedDateTo={}", modifiedFromStart.format(dateTimeStringFormat), if (isSundayDo2) "Sunday window from Sat 03:00" else "from yesterday 19:00", - todayEleven.format(dateTimeStringFormat), + modifiedDateToEnd.format(dateTimeStringFormat), ) 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 + modifiedDateTo = modifiedDateToEnd.format(dateTimeStringFormat), modifiedDateFrom = modifiedFromStart.format(dateTimeStringFormat), ) @@ -491,30 +497,6 @@ open class SchedulerService( result = result, start = currentTime ) - - // Extra DO sync window: after DO2, also sync ETA = today or tomorrow (normal sync; does NOT set isEtra). - try { - val extraStart = LocalDateTime.now() - val requestExtra = M18CommonRequest( - dDateFrom = today.format(dateTimeStringFormat), - dDateTo = tmr.format(dateTimeStringFormat), - ) - val extraResult = m18DeliveryOrderService.saveDeliveryOrders(requestExtra) - saveSyncLog( - type = "DO2_EXTRA", - status = "SUCCESS", - result = extraResult, - start = extraStart, - ) - } catch (e: Exception) { - logger.error("DO2_EXTRA sync failed: ${e.message}", e) - saveSyncLog( - type = "DO2_EXTRA", - status = "FAIL", - error = e.message, - start = LocalDateTime.now(), - ) - } } open fun getPostCompletedDnAndProcessGrn( diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt index fcad435..78dcb59 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt @@ -1367,15 +1367,18 @@ class PlasticBagPrinterService( } val qrValue = zplEscape(qrPayload) - // Must match python Bag2.py generate_zpl_dataflex() + // Must match python Bag3.py generate_zpl_dataflex() field layout / fonts. val fontRegular = "E:STXihei.ttf" val fontBold = "E:STXihei.ttf" + // Match python Bag3.py DataFlex defaults: narrower ^PW so job preview is not mostly empty on the right (^A@R fields are tall, not wide). + val labelPw = 400 + val labelLl = 500 return """ ^XA ^CI28 - ^PW700 - ^LL500 + ^PW$labelPw + ^LL$labelLl ^PO N ^FO10,20 ^BQN,2,4^FDQA,$qrValue^FS diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt index 47bc322..efa8f93 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt @@ -34,6 +34,15 @@ import java.util.Comparator import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository import org.springframework.transaction.annotation.Transactional +import com.fasterxml.jackson.databind.ObjectMapper +import com.ffii.fpsms.m18.service.M18BomForShopService +import com.ffii.fpsms.m18.model.M18BomShopSyncTriggerResult +import com.ffii.fpsms.m18.model.GoodsReceiptNoteResponse +import com.ffii.fpsms.m18.entity.M18BomShopSyncLog +import com.ffii.fpsms.m18.entity.M18BomShopSyncLogRepository +import com.ffii.fpsms.modules.common.SettingNames +import com.ffii.fpsms.modules.settings.entity.Settings +import com.ffii.fpsms.modules.settings.service.SettingsService @Service open class BomService( @@ -52,6 +61,10 @@ open class BomService( private val itemUomService: ItemUomService, private val jobOrderRepository: JobOrderRepository, private val productProcessRepository: ProductProcessRepository, + private val m18BomForShopService: M18BomForShopService, + private val m18BomShopSyncLogRepository: M18BomShopSyncLogRepository, + private val objectMapper: ObjectMapper, + private val settingsService: SettingsService, @Value("\${bom.import.temp-dir:\${java.io.tmpdir}/fpsms-bom-import}") private val bomImportTempDir: String, ) { open fun uploadBomFiles(files: List): BomUploadResponse { @@ -119,6 +132,29 @@ open class BomService( ?: bomRepository.findAllByItemIdAndDeletedIsFalse(itemId).firstOrNull() } + /** Resolve BOM header for a finished-good item code ([Items.code] on [Bom.item]). */ + open fun findBomSummaryByItemCode(itemCodeTrimmed: String): BomIdByItemCodeResponse { + val code = itemCodeTrimmed.trim() + val item = itemsRepository.findByCodeAndDeletedFalse(code) + ?: return BomIdByItemCodeResponse( + itemCode = code, + message = "Item not found for code", + ) + val bom = findByItemId(item.id!!) + ?: return BomIdByItemCodeResponse( + itemCode = code, + itemId = item.id, + message = "No BOM linked to this item", + ) + return BomIdByItemCodeResponse( + itemCode = code, + itemId = item.id, + bomId = bom.id, + bomCode = bom.code, + bomM18Id = bom.m18Id, + ) + } + open fun saveBom(request: SaveBomRequest): SaveBomResponse { val item = request.code.let { itemsService.findByM18BomCode(it) } ?: request.itemId?.let { itemsService.findById(it) } @@ -371,6 +407,111 @@ open class BomService( return getBomDetail(bom.id!!) } + /** + * When {@link SettingNames#M18_BOM_SHOP_SYNC_ENABLED} is true, push BOM to M18 udfBomForShop. + * Use {@code POST /m18/test/bom-shop-sync/{bomId}} (or future UI) to trigger explicitly. + * Optional [m18HeaderId]: M18 udfBomForShop **header** record id (e.g. [BomIdByItemCodeResponse.bomM18Id]) + * to force **update** when [Bom.m18Id] is missing or stale. When null, uses [Bom.m18Id] if set. + */ + open fun pushBomToM18ShopIfAllowed(bomId: Long, m18HeaderId: Long? = null): M18BomShopSyncTriggerResult { + if (!isM18BomShopSyncEnabled()) { + return M18BomShopSyncTriggerResult( + bomId = bomId, + synced = false, + skippedReason = "M18 BOM shop sync disabled (${SettingNames.M18_BOM_SHOP_SYNC_ENABLED} is not true)", + ) + } + val bom = bomRepository.findByIdAndDeletedIsFalse(bomId) + ?: return M18BomShopSyncTriggerResult(bomId, false, skippedReason = "BOM not found") + val req = m18BomForShopService.buildSaveRequest(bom, m18HeaderId) + ?: return M18BomShopSyncTriggerResult(bomId, false, skippedReason = "Cannot build M18 payload (missing m18 UOM/item ids or no materials)") + + val requestJsonPayload = m18BomForShopService.toJson(req) + var resp: GoodsReceiptNoteResponse? = null + var callError: Throwable? = null + try { + resp = m18BomForShopService.saveBomForShop(req) + } catch (e: Exception) { + callError = e + } + + val responseJsonPayload = when { + resp != null -> m18BomForShopService.toJson(resp) + callError != null -> + runCatching { + objectMapper.writeValueAsString( + mapOf( + "exceptionType" to callError.javaClass.name, + "message" to (callError.message ?: ""), + ), + ) + }.getOrElse { """{"error":"failed to serialize exception"}""" } + else -> """{"error":"M18 API returned null"}""" + } + + val msgSummary = resp?.messages?.joinToString("; ") { it.msgDetail ?: it.msgCode ?: "" }.orEmpty() + val apiStatus = resp?.status == true + val recordId = resp?.recordId ?: 0L + + val result = when { + callError != null -> + M18BomShopSyncTriggerResult( + bomId = bomId, + synced = false, + skippedReason = callError.message ?: "M18 API call failed", + status = false, + messageSummary = callError.message, + ) + resp == null -> + M18BomShopSyncTriggerResult(bomId, false, skippedReason = "M18 API returned null") + resp.status == true && resp.recordId > 0L -> { + bom.m18Id = resp.recordId + bomRepository.saveAndFlush(bom) + M18BomShopSyncTriggerResult( + bomId = bomId, + synced = true, + recordId = resp.recordId, + status = true, + messageSummary = msgSummary.ifBlank { null }, + ) + } + else -> + M18BomShopSyncTriggerResult( + bomId = bomId, + synced = false, + skippedReason = "M18 save failed or status=false", + recordId = resp.recordId.takeIf { it > 0 }, + status = resp.status, + messageSummary = msgSummary.ifBlank { null }, + ) + } + + val logMessage = listOfNotNull( + msgSummary.ifBlank { null }, + callError?.message, + result.skippedReason?.takeIf { !result.synced }, + ).joinToString("; ").take(4000) + + m18BomShopSyncLogRepository.save( + M18BomShopSyncLog().apply { + this.bomId = bomId + m18RecordId = recordId.takeIf { it > 0 } + m18ApiStatus = apiStatus + synced = result.synced + message = logMessage.ifBlank { null } + requestJson = requestJsonPayload + responseJson = responseJsonPayload + }, + ) + + return result + } + + private fun isM18BomShopSyncEnabled(): Boolean = + settingsService.findByName(SettingNames.M18_BOM_SHOP_SYNC_ENABLED) + .map { Settings.VALUE_BOOLEAN_TRUE == it.value } + .orElse(false) + private fun resolveEquipmentForBomProcess(pReq: EditBomProcessRequest): Equipment { val equipmentId = pReq.equipmentId val equipmentCode = pReq.equipmentCode?.trim().orEmpty() diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt index adc2179..275b80c 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt @@ -282,6 +282,20 @@ open class ItemUomService( return finalizePreciseStockQty(stockUnit, stockQty) } + /** + * Convert quantity from [uomId] (must exist on `item_uom` for [itemId]) to the item's **base unit** quantity. + * Returns null when no `item_uom` row links the item to that UOM. + */ + open fun convertQtyToBaseQtyPrecise(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal? { + val itemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return null + val one = BigDecimal.ONE + val calcScale = 10 + return sourceQty + .multiply(itemUom.ratioN ?: one) + .divide(itemUom.ratioD ?: one, calcScale, RoundingMode.HALF_UP) + .stripTrailingZeros() + } + // See if need to update the response open fun saveItemUom(request: ItemUomRequest): ItemUom { val itemUom = request.m18Id?.let { itemUomRespository.findFirstByM18IdOrderByIdDesc(it) } diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt index 79be196..5e97b94 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt @@ -29,6 +29,8 @@ import com.ffii.fpsms.modules.master.web.models.ImportBomRequestPayload import com.ffii.fpsms.modules.master.web.models.BomDetailResponse import com.ffii.fpsms.modules.master.web.models.EditBomRequest import com.ffii.fpsms.modules.master.web.models.BomExcelCheckProgress +import com.ffii.fpsms.modules.master.web.models.BomIdByItemCodeResponse +import com.ffii.core.exception.BadRequestException import java.util.logging.Logger import java.nio.file.Files import org.springframework.core.io.FileSystemResource @@ -120,6 +122,16 @@ fun downloadBomFormatIssueLog( // fun exportProblematicBom() { // return bomService.importBOM() // } + + /** Testing: FPSMS BOM id by finished-good item code (same item as BOM header). */ + @GetMapping("/by-item-code") + fun getBomByItemCode(@RequestParam code: String): BomIdByItemCodeResponse { + if (code.isBlank()) { + throw BadRequestException("query parameter code is required") + } + return bomService.findBomSummaryByItemCode(code.trim()) + } + @GetMapping("/{id}/detail") fun getBomDetail(@PathVariable id: Long): BomDetailResponse { return bomService.getBomDetail(id) diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/models/BomIdByItemCodeResponse.kt b/src/main/java/com/ffii/fpsms/modules/master/web/models/BomIdByItemCodeResponse.kt new file mode 100644 index 0000000..da7f821 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/web/models/BomIdByItemCodeResponse.kt @@ -0,0 +1,14 @@ +package com.ffii.fpsms.modules.master.web.models + +/** + * Testing / lookup: resolve FPSMS BOM from finished-good [item] code (bom.item → [Items.code]). + */ +data class BomIdByItemCodeResponse( + val itemCode: String, + val itemId: Long? = null, + val bomId: Long? = null, + val bomCode: String? = null, + val bomM18Id: Long? = null, + /** e.g. item not found, or no BOM for item */ + val message: String? = null, +) diff --git a/src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql b/src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql index 0b3cee9..ad4287b 100644 --- a/src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql +++ b/src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql @@ -9,7 +9,7 @@ WHERE NOT EXISTS ( ); INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`) -SELECT 'SCHEDULE.m18.do2', '0 0 11 * * *', 'SCHEDULE', 'string' +SELECT 'SCHEDULE.m18.do2', '0 0 13 * * *', 'SCHEDULE', 'string' FROM DUAL WHERE NOT EXISTS ( SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'SCHEDULE.m18.do2' diff --git a/src/main/resources/db/changelog/changes/20260512_fai/01_m18_bom_shop_sync_settings.sql b/src/main/resources/db/changelog/changes/20260512_fai/01_m18_bom_shop_sync_settings.sql new file mode 100644 index 0000000..cf1102c --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260512_fai/01_m18_bom_shop_sync_settings.sql @@ -0,0 +1,20 @@ +--liquibase formatted sql +--changeset fai:20260512_m18_bom_shop_sync_settings + +INSERT INTO `settings` (`name`, `value`, `category`, `type`) +SELECT v.name, v.value, v.category, v.type +FROM ( + SELECT 'M18.bom.shop.sync.enabled' AS name, 'false' AS value, 'M18' AS category, 'boolean' AS type +) v +WHERE NOT EXISTS ( + SELECT 1 FROM `settings` s WHERE s.name = 'M18.bom.shop.sync.enabled' +); + +INSERT INTO `settings` (`name`, `value`, `category`, `type`) +SELECT v.name, v.value, v.category, v.type +FROM ( + SELECT 'M18.bom.shop.sync.allowedBomIds' AS name, '78,274' AS value, 'M18' AS category, 'string' AS type +) v +WHERE NOT EXISTS ( + SELECT 1 FROM `settings` s WHERE s.name = 'M18.bom.shop.sync.allowedBomIds' +); diff --git a/src/main/resources/db/changelog/changes/20260512_fai/02_m18_bom_shop_sync_log.sql b/src/main/resources/db/changelog/changes/20260512_fai/02_m18_bom_shop_sync_log.sql new file mode 100644 index 0000000..e0e1805 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260512_fai/02_m18_bom_shop_sync_log.sql @@ -0,0 +1,24 @@ +--liquibase formatted sql +--changeset fai:20260512_m18_bom_shop_sync_log + +CREATE TABLE IF NOT EXISTS `m18_bom_shop_sync_log` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `createdBy` VARCHAR(30) NULL DEFAULT NULL, + `version` INT NOT NULL DEFAULT '0', + `modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `modifiedBy` VARCHAR(30) NULL DEFAULT NULL, + `deleted` TINYINT(1) NOT NULL DEFAULT '0', + + `bom_id` BIGINT NOT NULL COMMENT 'FPSMS bom.id', + `m18_record_id` BIGINT NULL DEFAULT NULL COMMENT 'M18 udfBomForShop record id when returned', + `m18_api_status` TINYINT(1) NOT NULL COMMENT 'M18 response status field', + `synced` TINYINT(1) NOT NULL COMMENT 'FPSMS treat as success (e.g. updated bom.m18Id)', + `message` VARCHAR(4000) NULL DEFAULT NULL COMMENT 'Summary / errors', + `request_json` LONGTEXT NULL COMMENT 'PUT body sent to M18', + `response_json` LONGTEXT NULL COMMENT 'Parsed M18 response or error JSON', + + CONSTRAINT pk_m18_bom_shop_sync_log PRIMARY KEY (`id`), + KEY `idx_m18_bom_shop_sync_log_bom_id` (`bom_id`), + KEY `idx_m18_bom_shop_sync_log_created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/main/resources/db/changelog/changes/20260513_do2_1pm/01_update_do2_schedule_to_13.sql b/src/main/resources/db/changelog/changes/20260513_do2_1pm/01_update_do2_schedule_to_13.sql new file mode 100644 index 0000000..0c849bb --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260513_do2_1pm/01_update_do2_schedule_to_13.sql @@ -0,0 +1,4 @@ +--liquibase formatted sql +--changeset fpsms:20260513_do2_schedule_1pm + +UPDATE `settings` SET `value` = '0 0 13 * * *' WHERE `name` = 'SCHEDULE.m18.do2'; diff --git a/src/main/resources/log4j2-prod-linux.yml b/src/main/resources/log4j2-prod-linux.yml index c90a291..f19e1f1 100644 --- a/src/main/resources/log4j2-prod-linux.yml +++ b/src/main/resources/log4j2-prod-linux.yml @@ -11,6 +11,7 @@ Configutation: filePattern: ${log_location}fpsms-all.log.%i.gz PatternLayout: Pattern: "%d %p [%l] - %m%n" + charset: UTF-8 Policies: SizeBasedTriggeringPolicy: size: 4096KB diff --git a/src/main/resources/log4j2-prod-win.yml b/src/main/resources/log4j2-prod-win.yml index d4f8a75..3152168 100644 --- a/src/main/resources/log4j2-prod-win.yml +++ b/src/main/resources/log4j2-prod-win.yml @@ -11,6 +11,7 @@ Configutation: filePattern: ${log_location}fpsms-all.log.%i.gz PatternLayout: Pattern: "%d %p [%l] - %m%n" + charset: UTF-8 Policies: SizeBasedTriggeringPolicy: size: 4096KB diff --git a/src/main/resources/log4j2.yml b/src/main/resources/log4j2.yml index e3b20ff..8977b61 100644 --- a/src/main/resources/log4j2.yml +++ b/src/main/resources/log4j2.yml @@ -10,6 +10,7 @@ Configutation: target: SYSTEM_OUT PatternLayout: pattern: ${log_pattern} + charset: UTF-8 Loggers: Root: level: info