| @@ -2464,6 +2464,172 @@ open class ProductProcessService( | |||||
| } | } | ||||
| // ===== Drink Production Qty Dashboard ===== | |||||
| open fun getDrinkProductionQty(date: LocalDate?): List<DrinkProductionQtyResponse> { | |||||
| val targetDate = date ?: LocalDate.now() | |||||
| val startOfDay = targetDate.atStartOfDay() | |||||
| val endOfDayExclusive = targetDate.plusDays(1).atStartOfDay() | |||||
| // 依「生產日」彙總:與既有 dashboard 的用法一致,採半開區間 [startOfDay, endExclusive) | |||||
| val jobOrders = jobOrderRepository.findByDeletedFalseAndPlanStartFromBeforeExclusiveOrderByIdAsc( | |||||
| planStartFrom = startOfDay, | |||||
| planStartToExclusive = endOfDayExclusive, | |||||
| ) | |||||
| if (jobOrders.isEmpty()) { | |||||
| return emptyList() | |||||
| } | |||||
| data class GroupKey( | |||||
| val itemCode: String?, | |||||
| val itemName: String?, | |||||
| ) | |||||
| data class DrinkOrderRow( | |||||
| val itemCode: String?, | |||||
| val itemName: String?, | |||||
| val uom: String?, | |||||
| val reqQty: BigDecimal, | |||||
| val productionQty: BigDecimal, | |||||
| val jobOrderId: Long?, | |||||
| val jobOrderCode: String?, | |||||
| val productionDate: LocalDate?, | |||||
| ) | |||||
| val drinkOrders = jobOrders | |||||
| .filter { it.bom?.isDrink == true } | |||||
| .filter { it.status != JobOrderStatus.PLANNING } | |||||
| if (drinkOrders.isEmpty()) { | |||||
| return emptyList() | |||||
| } | |||||
| val jobOrderIds = drinkOrders.mapNotNull { it.id } | |||||
| val itemIds = drinkOrders.mapNotNull { it.bom?.item?.id }.distinct() | |||||
| val processesByJobOrderId = productProcessRepository | |||||
| .findByJobOrder_IdInAndDeletedIsFalse(jobOrderIds) | |||||
| .groupBy { it.jobOrder?.id } | |||||
| .mapValues { (_, processes) -> processes.firstOrNull() } | |||||
| val processIds = processesByJobOrderId.values.mapNotNull { it?.id }.distinct() | |||||
| val linesByProcessId = if (processIds.isNotEmpty()) { | |||||
| productProcessLineRepository.findByProductProcess_IdIn(processIds) | |||||
| .groupBy { it.productProcess?.id ?: 0L } | |||||
| } else { | |||||
| emptyMap() | |||||
| } | |||||
| val stockInLineByJobOrderId = if (jobOrderIds.isNotEmpty()) { | |||||
| stockInLineRepository.findAllByJobOrder_IdInAndDeletedFalse(jobOrderIds) | |||||
| .associateBy { it.jobOrder?.id } | |||||
| } else { | |||||
| emptyMap() | |||||
| } | |||||
| val stockItemUoms = if (itemIds.isNotEmpty()) { | |||||
| itemUomRepository.findAllByItemIdInAndStockUnitIsTrueAndDeletedIsFalse(itemIds) | |||||
| } else { | |||||
| emptyList() | |||||
| } | |||||
| val itemUomByItemId = mutableMapOf<Long, com.ffii.fpsms.modules.master.entity.ItemUom>() | |||||
| stockItemUoms.forEach { iu -> | |||||
| val itemId = iu.item?.id ?: return@forEach | |||||
| itemUomByItemId.putIfAbsent(itemId, iu) | |||||
| } | |||||
| val uomIds = itemUomByItemId.values.mapNotNull { it.uom?.id }.distinct() | |||||
| val uomById = if (uomIds.isNotEmpty()) { | |||||
| uomConversionRepository.findAllByIdInAndDeletedFalse(uomIds).associateBy { it.id } | |||||
| } else { | |||||
| emptyMap() | |||||
| } | |||||
| val uomDescByItemId = itemUomByItemId.mapValues { (_, iu) -> | |||||
| uomById[iu.uom?.id]?.udfudesc | |||||
| } | |||||
| val drinkRows = drinkOrders.map { jo -> | |||||
| val bom = jo.bom | |||||
| val itemId = bom?.item?.id | |||||
| val process = processesByJobOrderId[jo.id] | |||||
| val lines = process?.id?.let { linesByProcessId[it].orEmpty() }.orEmpty() | |||||
| val stockInLine = stockInLineByJobOrderId[jo.id] | |||||
| DrinkOrderRow( | |||||
| itemCode = bom?.item?.code, | |||||
| itemName = bom?.item?.name, | |||||
| uom = resolveDrinkProductionUom(bom, itemId, uomDescByItemId), | |||||
| reqQty = jo.reqQty ?: BigDecimal.ZERO, | |||||
| productionQty = resolveDrinkProductionQty(jo, lines, stockInLine), | |||||
| jobOrderId = jo.id, | |||||
| jobOrderCode = jo.code, | |||||
| productionDate = jo.planStart?.toLocalDate(), | |||||
| ) | |||||
| } | |||||
| return drinkRows | |||||
| .groupBy { GroupKey(it.itemCode, it.itemName) } | |||||
| .map { (key, rows) -> | |||||
| DrinkProductionQtyResponse( | |||||
| itemCode = key.itemCode, | |||||
| itemName = key.itemName, | |||||
| uom = rows.firstNotNullOfOrNull { it.uom?.takeIf { u -> u.isNotBlank() } }, | |||||
| totalReqQty = rows.fold(BigDecimal.ZERO) { acc, row -> | |||||
| acc + row.reqQty | |||||
| }, | |||||
| totalQty = rows.fold(BigDecimal.ZERO) { acc, row -> | |||||
| acc + row.productionQty | |||||
| }, | |||||
| jobOrders = rows.map { row -> | |||||
| DrinkProductionQtyJobOrderDetail( | |||||
| jobOrderId = row.jobOrderId ?: 0L, | |||||
| jobOrderCode = row.jobOrderCode, | |||||
| productionDate = row.productionDate, | |||||
| reqQty = row.reqQty, | |||||
| productionQty = row.productionQty, | |||||
| ) | |||||
| }.sortedBy { it.jobOrderCode ?: "" }, | |||||
| ) | |||||
| } | |||||
| .sortedWith( | |||||
| compareBy<DrinkProductionQtyResponse> { it.itemCode ?: "" } | |||||
| .thenBy { it.itemName ?: "" } | |||||
| ) | |||||
| } | |||||
| private fun resolveDrinkProductionUom( | |||||
| bom: Bom?, | |||||
| itemId: Long?, | |||||
| uomDescByItemId: Map<Long, String?>, | |||||
| ): String? { | |||||
| bom?.outputQtyUom?.trim()?.takeIf { it.isNotEmpty() }?.let { return it } | |||||
| bom?.excelUom?.trim()?.takeIf { it.isNotEmpty() }?.let { return it } | |||||
| itemId?.let { uomDescByItemId[it] }?.trim()?.takeIf { it.isNotEmpty() }?.let { return it } | |||||
| return null | |||||
| } | |||||
| /** 實際生產數量:優先工序產出 → QC 入庫接受量 → 工單 reqQty(當日計劃生產量) */ | |||||
| private fun resolveDrinkProductionQty( | |||||
| jobOrder: com.ffii.fpsms.modules.jobOrder.entity.JobOrder, | |||||
| lines: List<ProductProcessLine>, | |||||
| stockInLine: com.ffii.fpsms.modules.stock.entity.StockInLine?, | |||||
| ): BigDecimal { | |||||
| val lineOutputQty = lines | |||||
| .sortedByDescending { it.seqNo ?: 0L } | |||||
| .firstNotNullOfOrNull { line -> | |||||
| line.outputFromProcessQty?.takeIf { it > 0 }?.toBigDecimal() | |||||
| } | |||||
| if (lineOutputQty != null) { | |||||
| return lineOutputQty | |||||
| } | |||||
| val acceptedQty = stockInLine?.acceptedQty | |||||
| if (acceptedQty != null && acceptedQty > BigDecimal.ZERO) { | |||||
| return acceptedQty | |||||
| } | |||||
| return jobOrder.reqQty ?: BigDecimal.ZERO | |||||
| } | |||||
| // ===== Operator KPI Dashboard ===== | // ===== Operator KPI Dashboard ===== | ||||
| open fun getOperatorKpi(date: LocalDate?): List<OperatorKpiResponse> { | open fun getOperatorKpi(date: LocalDate?): List<OperatorKpiResponse> { | ||||
| val targetDate = date ?: LocalDate.now() | val targetDate = date ?: LocalDate.now() | ||||
| @@ -306,4 +306,12 @@ class ProductProcessController( | |||||
| fun getEquipmentStatus(): List<EquipmentStatusByTypeResponse> { | fun getEquipmentStatus(): List<EquipmentStatusByTypeResponse> { | ||||
| return productProcessService.getEquipmentStatusByType() | return productProcessService.getEquipmentStatusByType() | ||||
| } | } | ||||
| @GetMapping("/Demo/DrinkProductionQty") | |||||
| fun getDrinkProductionQty(@RequestParam(required = false) date: String?): List<DrinkProductionQtyResponse> { | |||||
| val parsedDate = date?.takeIf { it.isNotBlank() }?.let { | |||||
| LocalDate.parse(it, DateTimeFormatter.ISO_DATE) | |||||
| } | |||||
| return productProcessService.getDrinkProductionQty(parsedDate) | |||||
| } | |||||
| } | } | ||||
| @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.productProcess.web.model | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import java.math.BigDecimal | |||||
| import com.fasterxml.jackson.annotation.JsonProperty | import com.fasterxml.jackson.annotation.JsonProperty | ||||
| import com.fasterxml.jackson.annotation.JsonFormat | import com.fasterxml.jackson.annotation.JsonFormat | ||||
| data class SaveProductProcessRequest( | data class SaveProductProcessRequest( | ||||
| @@ -335,4 +336,23 @@ data class EquipmentStatusByTypeResponse( | |||||
| val equipmentTypeId: Long, | val equipmentTypeId: Long, | ||||
| val equipmentTypeName: String?, | val equipmentTypeName: String?, | ||||
| val details: List<EquipmentStatusPerDetail>, | val details: List<EquipmentStatusPerDetail>, | ||||
| ) | |||||
| // ===== Drink Production Qty Dashboard ===== | |||||
| data class DrinkProductionQtyJobOrderDetail( | |||||
| val jobOrderId: Long, | |||||
| val jobOrderCode: String?, | |||||
| @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") | |||||
| val productionDate: LocalDate?, | |||||
| val reqQty: BigDecimal, | |||||
| val productionQty: BigDecimal, | |||||
| ) | |||||
| data class DrinkProductionQtyResponse( | |||||
| val itemCode: String?, | |||||
| val itemName: String?, | |||||
| val uom: String?, | |||||
| val totalReqQty: BigDecimal, | |||||
| val totalQty: BigDecimal, | |||||
| val jobOrders: List<DrinkProductionQtyJobOrderDetail> = emptyList(), | |||||
| ) | ) | ||||
| @@ -223,6 +223,7 @@ open class ItemLotTraceOrchestrator( | |||||
| relationshipId = meta.relationshipId, | relationshipId = meta.relationshipId, | ||||
| doOutboundIsExtra = meta.isExtra, | doOutboundIsExtra = meta.isExtra, | ||||
| doOutboundIsReplenish = meta.isReplenish, | doOutboundIsReplenish = meta.isReplenish, | ||||
| doOutboundQtyChanged = meta.qtyChanged, | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| @@ -223,18 +223,42 @@ open class TraceInboundLoader( | |||||
| } | } | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 22 | v1.0.0 | 2026-07-20 */ | |||||
| open fun loadPutawayEvents(inventoryLotId: Long): List<ItemLotTracePutawayEvent> = | open fun loadPutawayEvents(inventoryLotId: Long): List<ItemLotTracePutawayEvent> = | ||||
| loadPutawayEventsByLotIds(listOf(inventoryLotId))[inventoryLotId].orEmpty() | loadPutawayEventsByLotIds(listOf(inventoryLotId))[inventoryLotId].orEmpty() | ||||
| /** | |||||
| * Putaway card qty must be the original shelf-in amount, not the live [InventoryLotLine.inQty]. | |||||
| * Stock-take 盤盈 adds variance onto the same lot line inQty (see applyVarianceAdjustment), | |||||
| * which would otherwise inflate the 已上架 node (e.g. 150 → 1500). | |||||
| */ | |||||
| private fun putawayQtySql(illIdExpr: String): String = """ | |||||
| GREATEST( | |||||
| COALESCE(ill.inQty, 0) - COALESCE(( | |||||
| SELECT SUM(COALESCE(sil_tke.acceptedQty, 0)) | |||||
| FROM stock_in_line sil_tke | |||||
| INNER JOIN stock_take_line stl_tke | |||||
| ON sil_tke.stockTakeLineId = stl_tke.id AND stl_tke.deleted = 0 | |||||
| WHERE stl_tke.inventoryLotLineId = $illIdExpr | |||||
| AND sil_tke.deleted = 0 | |||||
| AND UPPER(COALESCE(sil_tke.type, '')) = 'TKE' | |||||
| AND COALESCE(sil_tke.acceptedQty, 0) > 0 | |||||
| ), 0), | |||||
| 0 | |||||
| ) | |||||
| """.trimIndent() | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 22 | v1.0.0 | 2026-07-20 */ | |||||
| open fun loadPutawayEventsByLotIds(lotIds: List<Long>): Map<Long, List<ItemLotTracePutawayEvent>> { | open fun loadPutawayEventsByLotIds(lotIds: List<Long>): Map<Long, List<ItemLotTracePutawayEvent>> { | ||||
| if (lotIds.isEmpty()) return emptyMap() | if (lotIds.isEmpty()) return emptyMap() | ||||
| val illPutawayQty = putawayQtySql("ill.id") | |||||
| val fromIllSql = """ | val fromIllSql = """ | ||||
| SELECT | SELECT | ||||
| il.id AS inventoryLotId, | il.id AS inventoryLotId, | ||||
| ill.id AS inventoryLotLineId, | ill.id AS inventoryLotLineId, | ||||
| COALESCE(sil_link.id, sil_primary.id) AS stockInLineId, | COALESCE(sil_link.id, sil_primary.id) AS stockInLineId, | ||||
| COALESCE(wh.code, '') AS warehouseCode, | COALESCE(wh.code, '') AS warehouseCode, | ||||
| COALESCE(ill.inQty, 0) AS qty, | |||||
| $illPutawayQty AS qty, | |||||
| COALESCE(ill.createdBy, sil_link.createdBy, sil_primary.createdBy, '') AS handledBy, | COALESCE(ill.createdBy, sil_link.createdBy, sil_primary.createdBy, '') AS handledBy, | ||||
| COALESCE(ill.created, sil_link.receiptDate, sil_primary.receiptDate, sil_link.created, sil_primary.created) AS timestamp, | COALESCE(ill.created, sil_link.receiptDate, sil_primary.receiptDate, sil_link.created, sil_primary.created) AS timestamp, | ||||
| CASE | CASE | ||||
| @@ -266,7 +290,10 @@ open class TraceInboundLoader( | |||||
| COALESCE(ill.id, sil.inventoryLotLineId) AS inventoryLotLineId, | COALESCE(ill.id, sil.inventoryLotLineId) AS inventoryLotLineId, | ||||
| sil.id AS stockInLineId, | sil.id AS stockInLineId, | ||||
| COALESCE(wh.code, '') AS warehouseCode, | COALESCE(wh.code, '') AS warehouseCode, | ||||
| COALESCE(ill.inQty, sil.acceptedQty, 0) AS qty, | |||||
| CASE | |||||
| WHEN ill.id IS NOT NULL THEN $illPutawayQty | |||||
| ELSE COALESCE(sil.acceptedQty, 0) | |||||
| END AS qty, | |||||
| COALESCE(ill.createdBy, sil.createdBy, '') AS handledBy, | COALESCE(ill.createdBy, sil.createdBy, '') AS handledBy, | ||||
| COALESCE(ill.created, sil.receiptDate, sil.created) AS timestamp, | COALESCE(ill.created, sil.receiptDate, sil.created) AS timestamp, | ||||
| CASE | CASE | ||||
| @@ -154,9 +154,9 @@ open class TraceMovementLoader( | |||||
| COALESCE(NULLIF(st.stockTakeSection, ''), NULLIF(str.stockTakeSection, ''), '') AS stockTakeSection, | COALESCE(NULLIF(st.stockTakeSection, ''), NULLIF(str.stockTakeSection, ''), '') AS stockTakeSection, | ||||
| COALESCE(st.stockTakeRoundId, str.stockTakeRoundId) AS stockTakeRoundId, | COALESCE(st.stockTakeRoundId, str.stockTakeRoundId) AS stockTakeRoundId, | ||||
| COALESCE(NULLIF(st.stockTakeRoundName, ''), '') AS stockTakeRoundName, | COALESCE(NULLIF(st.stockTakeRoundName, ''), '') AS stockTakeRoundName, | ||||
| COALESCE(str.varianceQty, stl.finalQty - stl.initialQty, 0) AS varianceQty, | |||||
| COALESCE(stl.initialQty, 0) AS beforeQty, | |||||
| COALESCE(stl.finalQty, 0) AS afterQty, | |||||
| COALESCE(str.varianceQty, stl.finalQty - stl.initialQty) AS varianceQty, | |||||
| stl.initialQty AS beforeQty, | |||||
| stl.finalQty AS afterQty, | |||||
| COALESCE(NULLIF(str.approverName, ''), NULLIF(str.stockTakerName, ''), '') AS approver, | COALESCE(NULLIF(str.approverName, ''), NULLIF(str.stockTakerName, ''), '') AS approver, | ||||
| COALESCE(str.approverTime, stl.completeDate, stl.modified) AS timestamp, | COALESCE(str.approverTime, stl.completeDate, stl.modified) AS timestamp, | ||||
| COALESCE(str.bookQty, 0) AS bookQty, | COALESCE(str.bookQty, 0) AS bookQty, | ||||
| @@ -197,15 +197,15 @@ open class TraceMovementLoader( | |||||
| stockTakeSection = sql.str(row["stockTakeSection"]), | stockTakeSection = sql.str(row["stockTakeSection"]), | ||||
| stockTakeRoundId = sql.longVal(row["stockTakeRoundId"]), | stockTakeRoundId = sql.longVal(row["stockTakeRoundId"]), | ||||
| stockTakeRoundName = sql.str(row["stockTakeRoundName"]), | stockTakeRoundName = sql.str(row["stockTakeRoundName"]), | ||||
| varianceQty = sql.decimal(row["varianceQty"]), | |||||
| beforeQty = sql.decimal(row["beforeQty"]), | |||||
| afterQty = sql.decimal(row["afterQty"]), | |||||
| varianceQty = sql.decimalOrNull(row["varianceQty"]) ?: java.math.BigDecimal.ZERO, | |||||
| beforeQty = sql.decimalOrNull(row["beforeQty"]) ?: java.math.BigDecimal.ZERO, | |||||
| afterQty = sql.decimalOrNull(row["afterQty"]), | |||||
| approver = sql.str(row["approver"]), | approver = sql.str(row["approver"]), | ||||
| timestamp = sql.formatDateTime(row["timestamp"]), | timestamp = sql.formatDateTime(row["timestamp"]), | ||||
| recordDetail = if (hasRecordDetail) { | recordDetail = if (hasRecordDetail) { | ||||
| ItemLotTraceStockTakeRecordDetail( | ItemLotTraceStockTakeRecordDetail( | ||||
| stockTakeLineId = stockTakeLineId ?: 0L, | stockTakeLineId = stockTakeLineId ?: 0L, | ||||
| bookQty = sql.decimal(row["bookQty"]), | |||||
| bookQty = sql.decimalOrNull(row["bookQty"]) ?: java.math.BigDecimal.ZERO, | |||||
| pickerFirstQty = sql.decimalOrNull(row["pickerFirstQty"]), | pickerFirstQty = sql.decimalOrNull(row["pickerFirstQty"]), | ||||
| pickerFirstBadQty = sql.decimalOrNull(row["pickerFirstBadQty"]), | pickerFirstBadQty = sql.decimalOrNull(row["pickerFirstBadQty"]), | ||||
| pickerSecondQty = sql.decimalOrNull(row["pickerSecondQty"]), | pickerSecondQty = sql.decimalOrNull(row["pickerSecondQty"]), | ||||
| @@ -97,6 +97,13 @@ open class TraceOutboundLoader( | |||||
| do_tbl.id AS deliveryOrderId, | do_tbl.id AS deliveryOrderId, | ||||
| COALESCE(do_tbl.isExtra, 0) AS deliveryOrderIsExtra, | COALESCE(do_tbl.isExtra, 0) AS deliveryOrderIsExtra, | ||||
| pol.id AS pickOrderLineId, | pol.id AS pickOrderLineId, | ||||
| pol.qty AS pickOrderLineQty, | |||||
| ( | |||||
| SELECT COALESCE(SUM(sol_all.qty), 0) | |||||
| FROM stock_out_line sol_all | |||||
| WHERE sol_all.pickOrderLineId = pol.id | |||||
| AND sol_all.deleted = 0 | |||||
| ) AS pickOrderLinePickedQty, | |||||
| il.itemId AS itemId, | il.itemId AS itemId, | ||||
| COALESCE(dop_direct.ticketNo, dop_conso.ticketNo) AS dopTicketNo, | COALESCE(dop_direct.ticketNo, dop_conso.ticketNo) AS dopTicketNo, | ||||
| COALESCE(dpo.ticket_no, dpor.ticket_no, '') AS legacyTicketNo, | COALESCE(dpo.ticket_no, dpor.ticket_no, '') AS legacyTicketNo, | ||||
| @@ -213,6 +220,14 @@ open class TraceOutboundLoader( | |||||
| ) | ) | ||||
| val isReplenish = deliveryOrderId != null && | val isReplenish = deliveryOrderId != null && | ||||
| replenishIndex.matches(deliveryOrderId, itemId, pickOrderLineId) | replenishIndex.matches(deliveryOrderId, itemId, pickOrderLineId) | ||||
| // Multi-lot picks: compare POL.qty to SUM(SOL.qty) for that line, not a single SOL. | |||||
| val pickOrderLineQty = sql.decimalOrNull(row["pickOrderLineQty"]) | |||||
| val pickOrderLinePickedQty = sql.decimalOrNull(row["pickOrderLinePickedQty"]) | |||||
| val qtyChanged = | |||||
| pickOrderLineId != null && | |||||
| pickOrderLineQty != null && | |||||
| pickOrderLinePickedQty != null && | |||||
| pickOrderLineQty.compareTo(pickOrderLinePickedQty) != 0 | |||||
| return ItemLotTraceDoDelivery( | return ItemLotTraceDoDelivery( | ||||
| stockOutLineId = sql.longVal(row["stockOutLineId"]) ?: 0L, | stockOutLineId = sql.longVal(row["stockOutLineId"]) ?: 0L, | ||||
| qty = sql.decimal(row["qty"]), | qty = sql.decimal(row["qty"]), | ||||
| @@ -233,6 +248,7 @@ open class TraceOutboundLoader( | |||||
| doPickOrderRecordId = sql.longVal(row["doPickOrderRecordId"]), | doPickOrderRecordId = sql.longVal(row["doPickOrderRecordId"]), | ||||
| isExtra = isExtra, | isExtra = isExtra, | ||||
| isReplenish = isReplenish, | isReplenish = isReplenish, | ||||
| qtyChanged = qtyChanged, | |||||
| ) | ) | ||||
| } | } | ||||
| @@ -89,6 +89,8 @@ data class ItemLotTraceMovement( | |||||
| val doOutboundIsExtra: Boolean = false, | val doOutboundIsExtra: Boolean = false, | ||||
| @get:JsonProperty("doOutboundIsReplenish") | @get:JsonProperty("doOutboundIsReplenish") | ||||
| val doOutboundIsReplenish: Boolean = false, | val doOutboundIsReplenish: Boolean = false, | ||||
| @get:JsonProperty("doOutboundQtyChanged") | |||||
| val doOutboundQtyChanged: Boolean = false, | |||||
| val processingStatus: String = "", | val processingStatus: String = "", | ||||
| val matchStatus: String = "", | val matchStatus: String = "", | ||||
| ) | ) | ||||
| @@ -152,7 +154,7 @@ data class ItemLotTraceStockTakeEvent( | |||||
| val stockTakeRoundName: String = "", | val stockTakeRoundName: String = "", | ||||
| val varianceQty: BigDecimal, | val varianceQty: BigDecimal, | ||||
| val beforeQty: BigDecimal, | val beforeQty: BigDecimal, | ||||
| val afterQty: BigDecimal, | |||||
| val afterQty: BigDecimal? = null, | |||||
| val approver: String, | val approver: String, | ||||
| val timestamp: String?, | val timestamp: String?, | ||||
| val recordDetail: ItemLotTraceStockTakeRecordDetail? = null, | val recordDetail: ItemLotTraceStockTakeRecordDetail? = null, | ||||
| @@ -375,6 +377,12 @@ data class ItemLotTraceDoDelivery( | |||||
| /** Matches delivery note sequence suffix (補貨). */ | /** Matches delivery note sequence suffix (補貨). */ | ||||
| @get:JsonProperty("isReplenish") | @get:JsonProperty("isReplenish") | ||||
| val isReplenish: Boolean = false, | val isReplenish: Boolean = false, | ||||
| /** | |||||
| * True when pick_order_line.qty differs from SUM(stock_out_line.qty) for that line | |||||
| * (改數). Multi-lot picks are aggregated so splitting across lots alone is not 改數. | |||||
| */ | |||||
| @get:JsonProperty("qtyChanged") | |||||
| val qtyChanged: Boolean = false, | |||||
| ) | ) | ||||
| /** DO replenishment created for a source delivery order line (建立補貨). */ | /** DO replenishment created for a source delivery order line (建立補貨). */ | ||||