소스 검색

added po number syn m18, added qtyM18 and uomIdM18 in purchase order line, and added conversion m18 to purchase unit qty

master
Fai Luk 1 일 전
부모
커밋
ff5218ae58
10개의 변경된 파일223개의 추가작업 그리고 65개의 파일을 삭제
  1. +101
    -20
      src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt
  2. +6
    -0
      src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt
  3. +57
    -39
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt
  4. +10
    -5
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt
  5. +21
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt
  6. +7
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLine.kt
  7. +3
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderLineService.kt
  8. +3
    -1
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/model/SavePurchaseOrderLineRequest.kt
  9. +8
    -0
      src/main/resources/db/changelog/changes/20260319_01_codex/01_update_purchase_order_line_add_uomidm18.sql
  10. +7
    -0
      src/main/resources/db/changelog/changes/20260319_02_codex/01_update_purchase_order_line_add_qtym18.sql

+ 101
- 20
src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt 파일 보기

@@ -54,6 +54,7 @@ open class M18PurchaseOrderService(

// M18 API
val M18_LOAD_PURCHASE_ORDER_API = "/root/api/read/po"
val M18_LOAD_PURCHASE_ORDER_BY_CODE_API = "/root/api/read/po"
val M18_FETCH_PURCHASE_ORDER_LIST_API = "/search/search"

// Include material po, oem po
@@ -204,16 +205,82 @@ open class M18PurchaseOrderService(
return purchaseOrder
}

open fun getPurchaseOrderByCode(code: String): M18PurchaseOrderResponse? {
var purchaseOrder: M18PurchaseOrderResponse? = null
try {
purchaseOrder = apiCallerService.get<M18PurchaseOrderResponse>(
M18_LOAD_PURCHASE_ORDER_BY_CODE_API,
mapOf("code" to code)
).block()
} catch (e: Exception) {
logger.error("(Getting Po Detail By Code) Error on Function - ${e.stackTrace}")
logger.error(e.message)
}
return purchaseOrder
}

private fun resolvePoTypeByBeId(beId: Long?): PurchaseOrderType {
return if (beId?.toString() == m18Config.BEID_TOA) PurchaseOrderType.SHOP else PurchaseOrderType.MATERIAL
}

open fun savePurchaseOrderByCode(code: String): SyncResult {
// Follow the scheduler's original approach:
// 1) use /search/search to find the PO id by code
// 2) then sync by PO id (this reuses the same create/update logic)
val searchRequest = M18PurchaseOrderListRequest(
stSearch = "po",
params = null,
conds = "(code=equal=$code)"
)
val poListResponse = try {
apiCallerService.get<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>(
M18_FETCH_PURCHASE_ORDER_LIST_API,
searchRequest
).block()
} catch (e: Exception) {
logger.error("(Getting Po List By Code) Error on Function - ${e.stackTrace}")
logger.error(e.message)
null
}

val poValues = poListResponse?.values
if (poValues.isNullOrEmpty()) {
return SyncResult(
totalProcessed = 1,
totalSuccess = 0,
totalFail = 1,
query = "code=equal=$code"
)
}

val request = M18PurchaseOrderListResponseWithType(
valuesWithType = mutableListOf(Pair(PurchaseOrderType.MATERIAL, poListResponse)),
query = "code=equal=$code"
)

return savePurchaseOrdersWithPreparedList(purchaseOrdersWithType = request)
}

open fun savePurchaseOrders(request: M18CommonRequest) : SyncResult{
logger.info("--------------------------------------------Start - Saving M18 Purchase Order--------------------------------------------")
val purchaseOrdersWithType = getPurchaseOrdersWithType(request)
val examplePurchaseOrders = listOf<Long>(4764034L)
return savePurchaseOrdersWithPreparedList(
purchaseOrdersWithType = purchaseOrdersWithType,
preloadPurchaseOrderDetails = null
)
}

private fun savePurchaseOrdersWithPreparedList(
purchaseOrdersWithType: M18PurchaseOrderListResponseWithType?,
preloadPurchaseOrderDetails: Map<Long, M18PurchaseOrderResponse?>? = null
): SyncResult {
logger.info("--------------------------------------------Start - Saving M18 Purchase Order--------------------------------------------")

val successList = mutableListOf<Long>()
val successDetailList = mutableListOf<Long>()
val failList = mutableListOf<Long>()
val failDetailList = mutableListOf<Long>()
val affectedItemIds = mutableSetOf<Long>()
val uomByM18IdCache = mutableMapOf<Long, com.ffii.fpsms.modules.master.entity.ItemUom?>()

val poRefType = "Purchase Order"
val poLineRefType = "Purchase Order Line"
@@ -221,7 +288,6 @@ open class M18PurchaseOrderService(
if (purchaseOrdersWithType != null) {
// Loop for Purchase Orders (values)
purchaseOrdersWithType.valuesWithType.forEach { purchaseOrderWithType ->
val type = purchaseOrderWithType.first
// if success
val purchaseOrdersValues = purchaseOrderWithType.second?.values
// if fail
@@ -229,7 +295,8 @@ open class M18PurchaseOrderService(

if (purchaseOrdersValues != null) {
purchaseOrdersValues.forEach { purchaseOrder ->
val purchaseOrderDetail = getPurchaseOrder(purchaseOrder.id)
val purchaseOrderDetail =
preloadPurchaseOrderDetails?.get(purchaseOrder.id) ?: getPurchaseOrder(purchaseOrder.id)

var purchaseOrderId: Long? = null //FP-MTMS

@@ -242,10 +309,11 @@ open class M18PurchaseOrderService(

// purchase_order + m18_data_log table
if (mainpo != null) {
val m18PurchaseOrderId = mainpo.id
// Find the latest m18 data log by m18 id & type
// logger.info("${poRefType}: Finding For Latest M18 Data Log...")
val latestPurchaseOrderLog =
m18DataLogService.findLatestM18DataLogWithSuccess(purchaseOrder.id, poRefType)
m18DataLogService.findLatestM18DataLogWithSuccess(m18PurchaseOrderId, poRefType)

// logger.info(latestPurchaseOrderLog.toString())
// Save to m18_data_log table
@@ -257,7 +325,7 @@ open class M18PurchaseOrderService(
val saveM18PurchaseOrderLogRequest = SaveM18DataLogRequest(
id = null,
refType = poRefType,
m18Id = purchaseOrder.id,
m18Id = m18PurchaseOrderId,
m18LastModifyDate = commonUtils.timestampToLocalDateTime(mainpo.lastModifyDate),
// dataLog = mainpoJson,
statusEnum = M18DataLogStatus.NOT_PROCESS
@@ -286,7 +354,7 @@ open class M18PurchaseOrderService(
estimatedArrivalDate = commonUtils.timestampToLocalDateTime(mainpo.dDate),
completeDate = null,
status = PurchaseOrderStatus.PENDING.value,
type = type.value,
type = resolvePoTypeByBeId(mainpo.beId).value,
m18DataLogId = saveM18PurchaseOrderLog.id,
m18BeId = mainpo.beId
)
@@ -305,13 +373,13 @@ open class M18PurchaseOrderService(
m18DataLogService.saveM18DataLog(successSaveM18PurchaseOrderLogRequest)

// log success info
successList.add(purchaseOrder.id)
logger.info("${poRefType}: Saved purchase order. ID: ${savePurchaseOrderResponse.id} | M18 ${poRefType} ID: ${purchaseOrder.id}")
successList.add(m18PurchaseOrderId)
logger.info("${poRefType}: Saved purchase order. ID: ${savePurchaseOrderResponse.id} | M18 ${poRefType} ID: ${m18PurchaseOrderId}")

} catch (e: Exception) {
failList.add(purchaseOrder.id)
failList.add(m18PurchaseOrderId)
// logger.error("${poRefType}: Saving Failure!")
logger.error("Error on Function - ${e.stackTrace} | Type: ${poRefType} | M18 ID: ${purchaseOrder.id} | Different? ${mainpo.id}")
logger.error("Error on Function - ${e.stackTrace} | Type: ${poRefType} | M18 ID: ${m18PurchaseOrderId} | Different? ${mainpo.id}")
logger.error(e.message)

val errorSaveM18PurchaseOrderLogRequest = SaveM18DataLogRequest(
@@ -359,11 +427,10 @@ open class M18PurchaseOrderService(
// logger.info("${poLineRefType}: Saved M18 Data Log. ID: ${saveM18PurchaseOrderLineLog.id}")
// logger.info("${poLineRefType}: Finding item...")
val item = itemsService.findByM18Id(line.proId)
var itemId: Long? = null
if (item == null) {
itemId = m18MasterDataService.saveProduct(line.proId)?.id
val itemId: Long? = if (item == null) {
m18MasterDataService.saveProduct(line.proId)?.id
} else {
itemId = item.id
item.id
}
logger.info("${poLineRefType}: Item ID: ${itemId} | M18 Item ID: ${line.proId}")

@@ -377,13 +444,26 @@ open class M18PurchaseOrderService(

// Save to purchase_order_line table
// logger.info("${poLineRefType}: Saving purchase order line...")
val itemUom = itemId?.let { itemUomService.findPurchaseUnitByItemId(it) }
val purchaseItemUom = itemId?.let { itemUomService.findPurchaseUnitByItemId(it) }
val m18ItemUom = uomByM18IdCache.getOrPut(line.unitId) {
itemUomService.findByM18Id(line.unitId)
}
val qtyM18 = line.qty
val sourceUomId = m18ItemUom?.uom?.id
// qtyM18 is the original qty in M18 line uom.
// System PO line.qty must be qty in purchase unit.
val convertedQty = if (itemId != null && sourceUomId != null) {
itemUomService.convertQtyToPurchaseQty(itemId, sourceUomId, qtyM18)
} else {
qtyM18
}
val savePurchaseOrderLineRequest = SavePurchaseOrderLineRequest(
id = existingPurchaseOrderLine?.id,
itemId = itemId,
uomId = itemUom?.uom?.id,
uomId = purchaseItemUom?.uom?.id,
purchaseOrderId = purchaseOrderId,
qty = line.qty,
qty = convertedQty,
qtyM18 = qtyM18,
up = line.up,
price = line.amt,
// m18CurrencyId = mainpo.curId,
@@ -391,7 +471,8 @@ open class M18PurchaseOrderService(
?: PurchaseOrderLineStatus.PENDING.value,
m18DataLogId = saveM18PurchaseOrderLineLog.id,
m18Discount = line.disc,
m18Lot = line.lot
m18Lot = line.lot,
uomIdM18 = sourceUomId
)

val savePurchaseOrderLineResponse =
@@ -443,7 +524,7 @@ open class M18PurchaseOrderService(
val saveM18PurchaseOrderLineLogRequest = SaveM18DataLogRequest(
id = null,
refType = "${poLineRefType}",
m18Id = purchaseOrder.id,
m18Id = m18PurchaseOrderId,
m18LastModifyDate = commonUtils.timestampToLocalDateTime(mainpo.lastModifyDate),
// dataLog = mutableMapOf(Pair("Error Message", "${poLineRefType} is null")),
dataLog = mutableMapOf(


+ 6
- 0
src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt 파일 보기

@@ -2,6 +2,7 @@ package com.ffii.fpsms.m18.web

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.web.models.M18CommonRequest
import com.ffii.fpsms.modules.common.SettingNames
@@ -63,6 +64,11 @@ class M18TestController (
fun test4(): Any {
return schedulerService.getM18Pos();
}

@GetMapping("/test/po-by-code")
fun testSyncPoByCode(@RequestParam code: String): SyncResult {
return m18PurchaseOrderService.savePurchaseOrderByCode(code)
}
// --------------------------------------------- Scheduler --------------------------------------------- ///
// @GetMapping("/schedule/po")
// fun schedulePo(@RequestParam @Valid newCron: String) {


+ 57
- 39
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt 파일 보기

@@ -581,54 +581,72 @@ open class PlasticBagPrinterService(
}

fun sendDataFlex6330Zpl(request: PrintRequest) {
Socket().use { socket ->
try {
// Connect with timeout
socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 5000)
socket.soTimeout = 5000 // read timeout if expecting response (optional)

val out = socket.getOutputStream()
fun zplEscape(s: String): String {
// Match python Bag2.py: escape "\" as "\\" and "^" as "\^"
return s.replace("\\", "\\\\").replace("^", "\\^")
}

// Build ZPL dynamically
val zpl = buildString {
append("^XA\n")
append("^PW500\n") // Print width ~42mm @300dpi; adjust 400-630 based on head
append("^LL280\n") // Label height ~23mm; tune for your pouch
append("^PON\n") // Normal orientation
append("^CI28\n") // UTF-8 / extended char set for Chinese
fun buildZplDataFlex(): String {
val desc = zplEscape((request.itemName.takeIf { it.isNotBlank() } ?: "—").trim())
val code = zplEscape((request.itemCode.takeIf { it.isNotBlank() } ?: "—").trim())

// Chinese product name / description (top)
append("^FO20,20^A@N,36,36,E:SIMSUN.FNT^FD${request.itemName}^FS\n") // Assumes font loaded
val lot = request.lotNo.trim()
val batchNo = if (lot.isBlank()) "—" else lot

// Item code
append("^FO20,80^A0N,32,32^FD${request.itemCode}^FS\n")
val labelLine = (if (lot.isBlank()) null else lot) ?: batchNo
val labelEsc = zplEscape(labelLine)

// Expiry date
append("^FO20,120^A0N,28,28^FDEXP: ${request.expiryDate}^FS\n")
val qrPayload = if (request.itemId != null && request.stockInLineId != null) {
// Keep same spacing style as python json.dumps (default).
"{\"itemId\": ${request.itemId}, \"stockInLineId\": ${request.stockInLineId}}"
} else {
if (labelLine.isNotBlank()) labelLine else batchNo
}
val qrValue = zplEscape(qrPayload)

// Must match python Bag2.py generate_zpl_dataflex()
val fontRegular = "E:STXihei.ttf"
val fontBold = "E:STXihei.ttf"

return """
^XA
^CI28
^PW700
^LL500
^PO N
^FO10,20
^BQN,2,4^FDQA,$qrValue^FS
^FO170,20
^A@R,72,72,$fontRegular^FD$desc^FS
^FO0,200
^A@R,72,72,$fontRegular^FD$labelEsc^FS
^FO55,200
^A@R,88,88,$fontBold^FD$code^FS
^XZ
""".trimIndent()
}

// Lot / Batch No.
append("^FO20,160^A0N,28,28^FDLOT: ${request.lotNo}^FS\n")
val rawQty = request.printQty
val qty = if (rawQty == -1) 100 else rawQty.coerceAtLeast(1)

// QR code encoding lotNo (or combine: item|lot|exp)
// Position right side, mag 6 (~good size), model 2
val qrData = request.lotNo // or "${request.itemCode}|${request.lotNo}|${request.expiryDate}"
append("^FO320,20^BQN,2,6^FDQA,$qrData^FS\n") // QA, prefix for alphanumeric mode
val zpl = buildZplDataFlex()

append("^XZ\n")
repeat(qty) { idx ->
Socket().use { socket ->
try {
socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 5000)
val out = socket.getOutputStream()
out.write(zpl.toByteArray(Charsets.UTF_8))
out.flush()
} catch (e: Exception) {
throw RuntimeException(
"DataFlex ZPL communication failed (sent ${idx} of ${qty}) to ${request.printerIp}:${request.printerPort}: ${e.message}",
e
)
}

// Send as bytes (UTF-8 safe)
out.write(zpl.toByteArray(Charsets.UTF_8))
out.flush()

println("DataFlex 6330 ZPL: Print job sent to ${request.printerIp}:${request.printerPort}")
// Optional: read response if printer echoes anything (rare in raw ZPL)
// val response = socket.getInputStream().readBytes().decodeToString()
// println("Response: $response")

} catch (e: Exception) {
throw RuntimeException("DataFlex 6330 ZPL communication failed: ${e.message}", e)
}
}

println("DataFlex ZPL: sent ${qty} labels to ${request.printerIp}:${request.printerPort}")
}
}

+ 10
- 5
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt 파일 보기

@@ -1,12 +1,17 @@
package com.ffii.fpsms.modules.jobOrder.web.model

data class PrintRequest(
val itemCode: String,
val itemName: String,
val lotNo: String,
val expiryDate: String,
val itemCode: String = "—",
val itemName: String = "—",
val lotNo: String = "—",
val expiryDate: String? = null,
val printerIp: String,
val printerPort: Int
val printerPort: Int,
// Used for DataFlex QR payload (same semantics as python Bag2.py)
val itemId: Long? = null,
val stockInLineId: Long? = null,
// For UI "+50/+10/+5/+1/C" printing. When -1 means continuous mode (backend prints up to 100).
val printQty: Int = 1
)

data class LaserRequest(


+ 21
- 0
src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt 파일 보기

@@ -138,6 +138,27 @@ open class ItemUomService(
return purchaseQty.setScale(0, RoundingMode.UP)
}

/**
* Convert source quantity from a specific UOM to this item's purchase unit quantity.
* Returns source qty when source/purchase unit mapping is not found.
*/
open fun convertQtyToPurchaseQty(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal {
val sourceItemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return sourceQty
val purchaseUnit = findPurchaseUnitByItemId(itemId) ?: return sourceQty
val one = BigDecimal.ONE
val calcScale = 10

val baseQty = sourceQty
.multiply(sourceItemUom.ratioN ?: one)
.divide(sourceItemUom.ratioD ?: one, calcScale, RoundingMode.HALF_UP)

val purchaseQty = baseQty
.multiply(purchaseUnit.ratioD ?: one)
.divide(purchaseUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP)

return purchaseQty.setScale(0, RoundingMode.UP)
}

open fun convertQtyToStockQty(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal {
val itemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return sourceQty;



+ 7
- 0
src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLine.kt 파일 보기

@@ -36,6 +36,9 @@ open class PurchaseOrderLine : BaseEntity<Long>() {
@Column(name = "qty", precision = 14, scale = 2)
open var qty: BigDecimal? = null

@Column(name = "qtyM18", precision = 14, scale = 2)
open var qtyM18: BigDecimal? = null

@Column(name = "up", precision = 14, scale = 2)
open var up: BigDecimal? = null

@@ -63,6 +66,10 @@ open class PurchaseOrderLine : BaseEntity<Long>() {
@Column(name = "m18Lot", length = 20)
open var m18Lot: String? = null

@ManyToOne
@JoinColumn(name = "uomIdM18")
open var uomM18: UomConversion? = null

@JsonManagedReference
@OneToMany(mappedBy = "purchaseOrderLine", cascade = [CascadeType.ALL], orphanRemoval = true)
open var stockInLines: MutableList<StockInLine> = mutableListOf()

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderLineService.kt 파일 보기

@@ -71,15 +71,18 @@ open class PurchaseOrderLineService(
// val currency = request.m18CurrencyId?.let { currencyService.findByM18Id(it) }
// ?: request.currencyId?.let { currencyService.findById(it) }
val uom = request.uomId?.let { uomConversionService.find(it).getOrNull() }
val uomM18 = request.uomIdM18?.let { uomConversionService.find(it).getOrNull() }

purchaseOrderLine.apply {
this.item = item
itemNo = item?.code
this.purchaseOrder = purchaseOrder
qty = request.qty
qtyM18 = request.qtyM18
up = request.up
price = request.price
this.uom = uom
this.uomM18 = uomM18
// this.currency = currency
this.status = status
this.m18DataLog = m18DataLog ?: this.m18DataLog


+ 3
- 1
src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/model/SavePurchaseOrderLineRequest.kt 파일 보기

@@ -8,6 +8,7 @@ data class SavePurchaseOrderLineRequest(
val uomId: Long?,
val purchaseOrderId: Long?,
val qty: BigDecimal?,
val qtyM18: BigDecimal?,
val up: BigDecimal?, // unit price
val price: BigDecimal?,
// val currencyId: Long? = null,
@@ -16,4 +17,5 @@ data class SavePurchaseOrderLineRequest(
val m18DataLogId: Long?,
val m18Discount: BigDecimal?,
val m18Lot: String?,
)
val uomIdM18: Long?,
)

+ 8
- 0
src/main/resources/db/changelog/changes/20260319_01_codex/01_update_purchase_order_line_add_uomidm18.sql 파일 보기

@@ -0,0 +1,8 @@
-- liquibase formatted sql
-- changeset codex:add_uomidm18_to_po_line
-- preconditions onFail:MARK_RAN
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'purchase_order_line' AND COLUMN_NAME = 'uomIdM18'

ALTER TABLE `purchase_order_line`
ADD COLUMN `uomIdM18` INT NULL AFTER `uomId`,
ADD CONSTRAINT `FK_PURCHASE_ORDER_LINE_ON_UOMIDM18` FOREIGN KEY (`uomIdM18`) REFERENCES `uom_conversion` (`id`);

+ 7
- 0
src/main/resources/db/changelog/changes/20260319_02_codex/01_update_purchase_order_line_add_qtym18.sql 파일 보기

@@ -0,0 +1,7 @@
-- liquibase formatted sql
-- changeset codex:add_qtym18_to_po_line
-- preconditions onFail:MARK_RAN
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'purchase_order_line' AND COLUMN_NAME = 'qtyM18'

ALTER TABLE `purchase_order_line`
ADD COLUMN `qtyM18` DECIMAL(14,2) NULL AFTER `qty`;

불러오는 중...
취소
저장