diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/service/DrinkShipmentQtyService.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/service/DrinkShipmentQtyService.kt new file mode 100644 index 0000000..bafd7c3 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/service/DrinkShipmentQtyService.kt @@ -0,0 +1,400 @@ +package com.ffii.fpsms.modules.productProcess.service + +import com.ffii.core.support.JdbcDao +import com.ffii.fpsms.modules.productProcess.web.model.DrinkShipmentQtyDeliveryDetail +import com.ffii.fpsms.modules.productProcess.web.model.DrinkShipmentQtyResponse +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.sql.Date +import java.sql.Timestamp +import java.time.LocalDate + +@Service +@Transactional(readOnly = true) +open class DrinkShipmentQtyService( + private val jdbcDao: JdbcDao, +) { + /** 訂單列:預計送貨日=選取日,或當日有掃出。實際出貨=當日 stock_ledger.outQty。 */ + open fun getDrinkShipmentQty(date: LocalDate?): List { + val targetDate = date ?: LocalDate.now() + val params = mapOf("targetDate" to targetDate.toString()) + val rows = jdbcDao.queryForList(dolSql, params) + + jdbcDao.queryForList(replenishmentOnlySql, params) + if (rows.isEmpty()) { + return emptyList() + } + + data class GroupKey( + val itemCode: String?, + val itemName: String?, + ) + + data class DeliveryKey( + val deliveryOrderId: Long, + val itemCode: String?, + ) + + val deliveriesByItem = linkedMapOf>>() + rows.forEach { row -> + val itemCode = jdbcString(row, "itemCode") + val itemName = jdbcString(row, "itemName") + val uom = jdbcString(row, "uom") + val itemKey = GroupKey(itemCode, itemName) + val detail = DrinkShipmentQtyDeliveryDetail( + deliveryOrderId = jdbcLong(row, "deliveryOrderId"), + deliveryOrderCode = jdbcString(row, "deliveryOrderCode"), + deliveryDate = jdbcDate(row, "deliveryDate"), + shopCode = jdbcString(row, "shopCode"), + shopName = shopLabel( + jdbcString(row, "shopCode"), + jdbcString(row, "shopName"), + ), + deliveryOrderStatus = jdbcString(row, "deliveryOrderStatus"), + orderQty = jdbcDecimal(row, "orderQty"), + shippedQty = jdbcDecimal(row, "shippedQty"), + ) + val deliveryKey = DeliveryKey(detail.deliveryOrderId, itemCode) + val byDelivery = deliveriesByItem.getOrPut(itemKey) { linkedMapOf() } + val existing = byDelivery[deliveryKey] + if (existing == null) { + byDelivery[deliveryKey] = detail to uom + } else { + val merged = existing.first.copy( + orderQty = existing.first.orderQty + detail.orderQty, + shippedQty = existing.first.shippedQty + detail.shippedQty, + shopName = existing.first.shopName ?: detail.shopName, + deliveryOrderCode = existing.first.deliveryOrderCode ?: detail.deliveryOrderCode, + deliveryOrderStatus = existing.first.deliveryOrderStatus ?: detail.deliveryOrderStatus, + ) + byDelivery[deliveryKey] = merged to (existing.second ?: uom) + } + } + + return deliveriesByItem + .map { (key, byDelivery) -> + val deliveries = byDelivery.values + .map { it.first } + .sortedBy { it.deliveryOrderCode ?: "" } + DrinkShipmentQtyResponse( + itemCode = key.itemCode, + itemName = key.itemName, + uom = byDelivery.values.firstNotNullOfOrNull { it.second }, + totalOrderQty = deliveries.fold(BigDecimal.ZERO) { acc, row -> acc + row.orderQty }, + totalShippedQty = deliveries.fold(BigDecimal.ZERO) { acc, row -> acc + row.shippedQty }, + deliveries = deliveries, + ) + } + .sortedWith( + compareBy { it.itemCode ?: "" } + .thenBy { it.itemName ?: "" }, + ) + } + + private fun shopLabel(shopCode: String?, shopName: String?): String? { + val code = shopCode?.trim()?.takeIf { it.isNotEmpty() } + val name = shopName?.trim()?.takeIf { it.isNotEmpty() } + return when { + code != null && name != null && name.startsWith("$code -") -> name + code != null && name != null -> "$code - $name" + name != null -> name + else -> code + } + } + + private fun jdbcString(row: Map, key: String): String? { + val value = row[key] ?: row[key.lowercase()] ?: return null + val text = value.toString().trim() + return text.takeIf { it.isNotEmpty() && !it.equals("null", ignoreCase = true) } + } + + private fun jdbcLong(row: Map, key: String): Long { + val value = row[key] ?: row[key.lowercase()] ?: return 0L + return when (value) { + is Number -> value.toLong() + else -> value.toString().toLongOrNull() ?: 0L + } + } + + private fun jdbcDecimal(row: Map, key: String): BigDecimal { + val value = row[key] ?: row[key.lowercase()] ?: return BigDecimal.ZERO + return when (value) { + is BigDecimal -> value + is Number -> BigDecimal(value.toString()) + else -> value.toString().toBigDecimalOrNull() ?: BigDecimal.ZERO + } + } + + private fun jdbcDate(row: Map, key: String): LocalDate? { + val value = row[key] ?: row[key.lowercase()] ?: return null + return when (value) { + is LocalDate -> value + is Date -> value.toLocalDate() + is Timestamp -> value.toLocalDateTime().toLocalDate() + else -> runCatching { LocalDate.parse(value.toString().take(10)) }.getOrNull() + } + } + + companion object { + private const val DRINK_ITEM_JOIN = """ + INNER JOIN ( + SELECT itemId, MIN(id) AS bomId + FROM bom + WHERE deleted = 0 + AND IFNULL(isDrink, 0) = 1 + AND itemId IS NOT NULL + GROUP BY itemId + ) drink_item ON drink_item.itemId = it.id + LEFT JOIN bom b ON b.id = drink_item.bomId + LEFT JOIN item_uom iu + ON iu.id = ( + SELECT MIN(iu2.id) + FROM item_uom iu2 + WHERE iu2.itemId = it.id + AND iu2.stockUnit = 1 + AND iu2.deleted = 0 + ) + LEFT JOIN uom_conversion uc ON uc.id = iu.uomId + """ + + private const val SOL_DO_TYPE_FILTER = """ + AND LOWER(IFNULL(sol.type, 'nor')) NOT IN ('trf', 'adj', 'tke', 'stocktake', 'jo') + AND ( + so.id IS NULL + OR ( + IFNULL(so.deleted, 0) = 0 + AND LOWER(IFNULL(so.type, 'do')) = 'do' + ) + ) + """ + + /** + * 當天掃出去的量:優先 stock_ledger.outQty(跨日提料只計當日增量)。 + * 無 ledger 的舊資料才 fallback 到當日 pickTime 的 sol.qty。 + */ + private const val SHIPPED_QTY_FOR_DO = """ + IFNULL(( + SELECT SUM( + CASE + WHEN sl.id IS NOT NULL THEN IFNULL(sl.outQty, 0) + WHEN DATE(IFNULL(sol.pickTime, sol.created)) = :targetDate + AND NOT EXISTS ( + SELECT 1 + FROM stock_ledger slx + WHERE slx.stockOutLineId = sol.id + AND IFNULL(slx.deleted, 0) = 0 + ) + THEN IFNULL(sol.qty, 0) + ELSE 0 + END + ) + FROM pick_order po + INNER JOIN pick_order_line pol + ON pol.poId = po.id + AND pol.deleted = 0 + AND pol.itemId = it.id + INNER JOIN stock_out_line sol + ON sol.pickOrderLineId = pol.id + AND sol.deleted = 0 + LEFT JOIN stock_out so + ON so.id = sol.stockOutId + LEFT JOIN stock_ledger sl + ON sl.stockOutLineId = sol.id + AND IFNULL(sl.deleted, 0) = 0 + AND DATE(sl.date) = :targetDate + AND IFNULL(sl.outQty, 0) > 0 + WHERE po.deleted = 0 + AND po.doId = do.id + AND LOWER(IFNULL(po.type, 'do')) = 'do' + $SOL_DO_TYPE_FILTER + ), 0) + """ + + private const val SCANNED_TODAY_FOR_DO = """ + EXISTS ( + SELECT 1 + FROM pick_order po + INNER JOIN pick_order_line pol + ON pol.poId = po.id + AND pol.deleted = 0 + AND pol.itemId = it.id + INNER JOIN stock_out_line sol + ON sol.pickOrderLineId = pol.id + AND sol.deleted = 0 + LEFT JOIN stock_out so + ON so.id = sol.stockOutId + LEFT JOIN stock_ledger sl + ON sl.stockOutLineId = sol.id + AND IFNULL(sl.deleted, 0) = 0 + AND DATE(sl.date) = :targetDate + AND IFNULL(sl.outQty, 0) > 0 + WHERE po.deleted = 0 + AND po.doId = do.id + AND LOWER(IFNULL(po.type, 'do')) = 'do' + $SOL_DO_TYPE_FILTER + AND ( + sl.id IS NOT NULL + OR ( + IFNULL(sol.qty, 0) > 0 + AND DATE(IFNULL(sol.pickTime, sol.created)) = :targetDate + AND NOT EXISTS ( + SELECT 1 + FROM stock_ledger slx + WHERE slx.stockOutLineId = sol.id + AND IFNULL(slx.deleted, 0) = 0 + ) + ) + ) + ) + """ + + private val dolSql = """ + SELECT + it.code AS itemCode, + it.name AS itemName, + COALESCE( + NULLIF(TRIM(b.outputQtyUom), ''), + NULLIF(TRIM(b.excelUom), ''), + uc.udfudesc + ) AS uom, + do.id AS deliveryOrderId, + do.code AS deliveryOrderCode, + do.status AS deliveryOrderStatus, + DATE(IFNULL(do.estimatedArrivalDate, do.orderDate)) AS deliveryDate, + IFNULL(sp.code, '') AS shopCode, + IFNULL(sp.name, '') AS shopName, + SUM(IFNULL(dol.qty, 0)) + + IFNULL(( + SELECT SUM(IFNULL(r.replenishQty, 0)) + FROM do_replenishment r + WHERE IFNULL(r.deleted, 0) = 0 + AND r.targetDoId = do.id + AND r.itemId = it.id + ), 0) AS orderQty, + $SHIPPED_QTY_FOR_DO AS shippedQty + FROM delivery_order_line dol + INNER JOIN delivery_order do + ON dol.deliveryOrderId = do.id + AND do.deleted = 0 + INNER JOIN items it + ON dol.itemId = it.id + AND it.deleted = 0 + $DRINK_ITEM_JOIN + LEFT JOIN shop sp + ON do.shopId = sp.id + AND sp.deleted = 0 + WHERE dol.deleted = 0 + AND ( + DATE(IFNULL(do.estimatedArrivalDate, do.orderDate)) = :targetDate + OR $SCANNED_TODAY_FOR_DO + ) + GROUP BY + it.id, + it.code, + it.name, + b.outputQtyUom, + b.excelUom, + uc.udfudesc, + do.id, + do.code, + do.status, + DATE(IFNULL(do.estimatedArrivalDate, do.orderDate)), + sp.code, + sp.name + ORDER BY it.code, do.code + """.trimIndent() + + private val replenishmentOnlySql = """ + SELECT + it.code AS itemCode, + it.name AS itemName, + COALESCE( + NULLIF(TRIM(b.outputQtyUom), ''), + NULLIF(TRIM(b.excelUom), ''), + uc.udfudesc + ) AS uom, + COALESCE(r.targetDoId, r.sourceDoId, 0) AS deliveryOrderId, + COALESCE(do.code, r.targetDoCode, r.sourceDoCode) AS deliveryOrderCode, + do.status AS deliveryOrderStatus, + r.deliveryDate AS deliveryDate, + IFNULL(r.shopCode, IFNULL(sp.code, '')) AS shopCode, + IFNULL(r.shopName, IFNULL(sp.name, '')) AS shopName, + SUM(IFNULL(r.replenishQty, 0)) AS orderQty, + IFNULL(MAX(ship.shippedQty), 0) AS shippedQty + FROM do_replenishment r + INNER JOIN items it + ON r.itemId = it.id + AND it.deleted = 0 + $DRINK_ITEM_JOIN + LEFT JOIN delivery_order do + ON do.id = COALESCE(r.targetDoId, r.sourceDoId) + AND do.deleted = 0 + LEFT JOIN shop sp + ON do.shopId = sp.id + AND sp.deleted = 0 + LEFT JOIN ( + SELECT + COALESCE(r2.targetDoId, r2.sourceDoId, 0) AS deliveryOrderId, + r2.itemId AS itemId, + SUM( + CASE + WHEN sl.id IS NOT NULL THEN IFNULL(sl.outQty, 0) + WHEN DATE(IFNULL(sol.pickTime, sol.created)) = :targetDate + AND NOT EXISTS ( + SELECT 1 + FROM stock_ledger slx + WHERE slx.stockOutLineId = sol.id + AND IFNULL(slx.deleted, 0) = 0 + ) + THEN IFNULL(sol.qty, 0) + ELSE 0 + END + ) AS shippedQty + FROM do_replenishment r2 + INNER JOIN stock_out_line sol + ON sol.pickOrderLineId = r2.pickOrderLineId + AND sol.deleted = 0 + LEFT JOIN stock_out so + ON so.id = sol.stockOutId + LEFT JOIN stock_ledger sl + ON sl.stockOutLineId = sol.id + AND IFNULL(sl.deleted, 0) = 0 + AND DATE(sl.date) = :targetDate + AND IFNULL(sl.outQty, 0) > 0 + WHERE IFNULL(r2.deleted, 0) = 0 + $SOL_DO_TYPE_FILTER + GROUP BY + COALESCE(r2.targetDoId, r2.sourceDoId, 0), + r2.itemId + ) ship + ON ship.deliveryOrderId = COALESCE(r.targetDoId, r.sourceDoId, 0) + AND ship.itemId = it.id + WHERE IFNULL(r.deleted, 0) = 0 + AND ( + r.deliveryDate = :targetDate + OR IFNULL(ship.shippedQty, 0) > 0 + ) + AND NOT EXISTS ( + SELECT 1 + FROM delivery_order_line dol + WHERE dol.deleted = 0 + AND dol.itemId = r.itemId + AND dol.deliveryOrderId = COALESCE(r.targetDoId, r.sourceDoId) + ) + GROUP BY + it.id, + it.code, + it.name, + b.outputQtyUom, + b.excelUom, + uc.udfudesc, + COALESCE(r.targetDoId, r.sourceDoId, 0), + COALESCE(do.code, r.targetDoCode, r.sourceDoCode), + do.status, + r.deliveryDate, + IFNULL(r.shopCode, IFNULL(sp.code, '')), + IFNULL(r.shopName, IFNULL(sp.name, '')) + ORDER BY it.code, COALESCE(do.code, r.targetDoCode, r.sourceDoCode) + """.trimIndent() + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt index 72e7aca..857bec3 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt @@ -85,10 +85,6 @@ open class ProductProcessService( private val stockInLineRepository: StockInLineRepository, private val qcResultRepository: QcResultRepository, ) { - companion object { - private const val PACKAGING_PROCESS_NAME = "包裝" - private const val DRINK_PLANNED_LOOKBACK_DAYS = 30L - } open fun findAll(pageable: Pageable): Page { println(" Service: Finding all ProductProcess with page: ${pageable.pageNumber}, size: ${pageable.pageSize}") @@ -1250,7 +1246,6 @@ open class ProductProcessService( ) } - /** FP-MTMS Version Checklist | Functions Ref. No. 58 | v1.0.1 | 2026-08-10 */ open fun updateProductProcessLineStatus(productProcessLineId: Long, status: String): MessageResponse { println(" Service: Updating ProductProcessLine Status: $productProcessLineId") val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) @@ -1259,12 +1254,6 @@ open class ProductProcessService( // productProcessLine.endTime = LocalDateTime.now() productProcessLineRepository.save(productProcessLine) println(" Service: ProductProcessLine Status Updated: ${productProcessLine.status}") - - // One packaging Complete/Pass → autoPass remaining packaging lines on the same JO. - if (isManualLineDone(status) && isPackagingLine(productProcessLine)) { - autoPassSiblingPackagingLines(productProcessLineId) - } - syncProductProcessStatusFromLines(productProcessLine.productProcess?.id ?: 0L) return MessageResponse( @@ -1319,7 +1308,7 @@ open class ProductProcessService( val productProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) println(" Service: ProductProcessLines: $productProcessLines") - if (productProcessLines.all { isLineDone(it.status) }) { + if (productProcessLines.all { it.status == "Completed" || it.status == "Pass" }) { productProcess.status = ProductProcessStatus.COMPLETED if (productProcess.endTime == null) { productProcess.endTime = LocalDateTime.now() @@ -1511,9 +1500,8 @@ open class ProductProcessService( } /** - * FP-MTMS Version Checklist | Functions Ref. No. 41 | v1.0.3 | 2026-08-10 - * QC 列表:按 jobOrder 粒度分页,qcReady 由该 jobOrder 下所有 productProcessLine 是否全部 Completed/Pass/autoPass 决定。 - * (包裝 Complete/Pass 時會自動 autoPass 同 JO 其餘包裝 line,因此不再需要「任一包裝即可」特例。) + * FP-MTMS Version Checklist | Functions Ref. No. 41 | v1.0.1 | 2026-08-06 + * QC 列表:按 jobOrder 粒度分页,qcReady 由该 jobOrder 下所有 productProcessLine 是否全部 Completed/Pass 决定。 * * 注意:date/itemCode/jobOrderCode/bomIds 用来筛选“候选 jobOrder”,但 qcReady 判断会用该 jobOrder 下全部 productProcessLine。 */ @@ -1663,6 +1651,10 @@ open class ProductProcessService( val linesByJobOrder = candidateLines.groupBy { it.productProcess.jobOrder?.id ?: 0L } + val done = { s: String? -> + val x = s?.trim()?.lowercase() + x == "completed" || x == "pass" + } val qcKeys = candidateAggregates.mapNotNull { agg -> val jobOrderId = agg.jobOrderId val stockInLine = stockInLineByJobOrderId[jobOrderId] @@ -1684,12 +1676,20 @@ open class ProductProcessService( val lineAggregate = lineStatusByJobOrderId[jobOrderId] val totalLines = lineAggregate?.totalLines ?: 0L - - // After packaging Complete/Pass autoPasses sibling 包裝 lines, qcReady is simply all lines done. + val done = { s: String? -> + val x = s?.trim()?.lowercase() + x == "completed" || x == "pass" + } + + val jobLines = linesByJobOrder[jobOrderId].orEmpty() - val allLinesDone = jobLines.isNotEmpty() && jobLines.all { isLineDone(it.status) } + val packing = jobLines.filter { (it.name ?: "").trim() == "包裝" } + val nonPacking = jobLines.filter { (it.name ?: "").trim() != "包裝" } - val ready = includedInList && stockInLine != null && allLinesDone + val nonPackingOk = nonPacking.isEmpty() || nonPacking.all { done(it.status) } + val packingOk = packing.isEmpty() || packing.any { done(it.status) } + + val ready = includedInList && stockInLine != null && nonPackingOk && packingOk JobOrderQcKey( jobOrderId = jobOrderId, @@ -1877,7 +1877,10 @@ open class ProductProcessService( val joPickOrdersList = if (pickOrderId != null) joPickOrdersByPickOrderId[pickOrderId].orEmpty() else emptyList() val productProcessLines = linesByProcessId[productProcess.id ?: 0L].orEmpty() - val finishedCount = productProcessLines.count { isLineDone(it.status) } + val finishedCount = productProcessLines.count { + val s = it.status?.trim()?.lowercase() + s == "completed" || s == "pass" + } val bomIsDrink = productProcess.bom?.isDrink val matchStatus = if (joPickOrdersList.isNotEmpty() && @@ -1951,7 +1954,7 @@ open class ProductProcessService( } /** - * FG QC / 上架 reminders: same eligibility as 完成QC工單 (qcReady): all lines Completed/Pass/autoPass, stock-in exists and not completed/rejected; + * FG QC / 上架 reminders: same eligibility as 完成QC工單 (qcReady): all lines Completed/Pass, stock-in exists and not completed/rejected; * only job orders that have a product process dated **today or yesterday** (server local date). */ open fun findJobOrderFgQcAndPutAwayAlertsForTodayYesterday(): JobOrderFgAlertsResponse { @@ -2005,7 +2008,7 @@ open class ProductProcessService( val jobOrderLines = processes.flatMap { p -> linesByProcessId[p.id ?: 0L].orEmpty() } val allLinesDone = jobOrderLines.isNotEmpty() && - jobOrderLines.all { isLineDone(it.status) } + jobOrderLines.all { it.status == "Completed" || it.status == "Pass" } if (!allLinesDone) continue val maxDate = processes.mapNotNull { it.date }.maxOrNull() @@ -2107,12 +2110,6 @@ open class ProductProcessService( status?.trim()?.lowercase()?.replace(" ", "") ?: "" private fun isLineDone(status: String?): Boolean { - val n = normalizeLineStatus(status) - return n == "completed" || n == "pass" || n == "autopass" - } - - /** Manual Complete/Pass only — does not include autoPass (avoids cascade loops). */ - private fun isManualLineDone(status: String?): Boolean { val n = normalizeLineStatus(status) return n == "completed" || n == "pass" } @@ -2122,54 +2119,9 @@ open class ProductProcessService( return n == "inprogress" || n == "paused" } - /** FP-MTMS Version Checklist | Functions Ref. No. 58 | v1.0.1 | 2026-08-10 */ - private fun isPackagingLine(line: ProductProcessLine): Boolean = - (line.name ?: "").trim() == PACKAGING_PROCESS_NAME - - /** - * FP-MTMS Version Checklist | Functions Ref. No. 58 | v1.0.1 | 2026-08-10 - * When one 包裝 line is Completed/Pass, set other unfinished 包裝 lines on the same job order to autoPass, - * then sync each affected ProductProcess.status (autoPass counts as done → COMPLETED). - */ - private fun autoPassSiblingPackagingLines(triggerLineId: Long) { - val trigger = productProcessLineRepository.findById(triggerLineId).orElse(null) ?: return - if (!isPackagingLine(trigger)) return - - val jobOrderId = trigger.productProcess?.jobOrder?.id ?: return - val processes = productProcessRepository.findByJobOrder_Id(jobOrderId) - .filter { it.deleted != true } - val processIds = processes.mapNotNull { it.id } - if (processIds.isEmpty()) return - - val allLines = productProcessLineRepository.findByProductProcess_IdIn(processIds) - val siblings = allLines.filter { line -> - line.id != null && - line.id != triggerLineId && - isPackagingLine(line) && - !isLineDone(line.status) - } - if (siblings.isEmpty()) return - - val now = LocalDateTime.now() - val affectedProcessIds = linkedSetOf() - for (line in siblings) { - if (line.startTime == null) { - line.startTime = now - } - line.endTime = now - line.status = "autoPass" - productProcessLineRepository.save(line) - line.productProcess?.id?.let { affectedProcessIds.add(it) } - } - - for (processId in affectedProcessIds) { - syncProductProcessStatusFromLines(processId) - } - } - /** * Align parent [ProductProcess.status] with all line states. - * - All Completed/Pass/autoPass -> completed (via [ifAllLinesCompletedOrPassed]) + * - All Completed/Pass -> completed (via [ifAllLinesCompletedOrPassed]) * - Any line started but not all done -> in_progress * - Does not override STOPPED or CANCELLED on the parent. */ @@ -2249,8 +2201,8 @@ open class ProductProcessService( // 获取所有 product process lines val allproductProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) - // 检查是否所有 lines 都是 Completed / Pass / autoPass - if (allproductProcessLines.all { isLineDone(it.status) }) { + // 检查是否所有 lines 都是 "Completed" 或 "Pass" + if (allproductProcessLines.all { it.status == "Completed" || it.status == "Pass" }) { // 更新 product process 的 endTime 和状态 updateProductProcessEndTime(productProcessId) updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) @@ -2679,6 +2631,9 @@ open class ProductProcessService( } // ===== Drink Production Qty Dashboard ===== + private companion object { + const val DRINK_PLANNED_LOOKBACK_DAYS = 30L + } /** FP-MTMS Version Checklist | Functions Ref. No. 27 | v1.0.0 | 2026-07-20 */ open fun getDrinkProductionQty( @@ -2765,6 +2720,7 @@ open class ProductProcessService( } val drinkOrders = candidateJobOrders + .filter { it.isHidden != true } .filter { it.bom?.isDrink == true } .filter { jo -> isPlannedView || jo.status != JobOrderStatus.PLANNING @@ -3091,7 +3047,7 @@ open class ProductProcessService( } val currentProcesses = operatorLines - .filter { it.endTime == null || (it.status != null && !isLineDone(it.status)) } + .filter { it.endTime == null || (it.status != null && it.status != "Completed" && it.status != "Pass") } .map { line -> val productProcess = line.productProcess val jobOrder = productProcess.jobOrder diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt index 748c661..10843be 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt @@ -3,6 +3,7 @@ package com.ffii.fpsms.modules.productProcess.web import com.ffii.fpsms.modules.productProcess.entity.ProductProcess import com.ffii.fpsms.modules.productProcess.entity.ProductProcessLine import com.ffii.fpsms.modules.productProcess.enums.ProductProcessStatus +import com.ffii.fpsms.modules.productProcess.service.DrinkShipmentQtyService import com.ffii.fpsms.modules.productProcess.service.ProductProcessService import com.ffii.fpsms.modules.productProcess.web.model.* import org.springframework.data.domain.Page @@ -16,7 +17,8 @@ import java.time.format.DateTimeFormatter @RestController @RequestMapping("/product-process") class ProductProcessController( - private val productProcessService: ProductProcessService + private val productProcessService: ProductProcessService, + private val drinkShipmentQtyService: DrinkShipmentQtyService, ) { @GetMapping @@ -333,4 +335,14 @@ class ProductProcessController( } return productProcessService.getDrinkProductionQty(parsedDate, viewMode) } + + @GetMapping("/Demo/DrinkShipmentQty") + fun getDrinkShipmentQty( + @RequestParam(required = false) date: String?, + ): List { + val parsedDate = date?.takeIf { it.isNotBlank() }?.let { + LocalDate.parse(it, DateTimeFormatter.ISO_DATE) + } + return drinkShipmentQtyService.getDrinkShipmentQty(parsedDate) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt index ab9e59e..781a450 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt @@ -218,7 +218,7 @@ data class JobOrderProductProcessPageResponse( ) /** - * Nav alerts aligned with 完成QC工單 list: all product process lines Completed/Pass/autoPass, stock-in not completed/rejected; + * Nav alerts aligned with 完成QC工單 list: all product process lines Completed/Pass, stock-in not completed/rejected; * job order has at least one process dated today or yesterday. [qc] = before received; [putAway] = received / partially_completed. */ data class JobOrderFgAlertRowResponse( @@ -400,4 +400,25 @@ data class DrinkProductionQtyResponse( val totalReqQty: BigDecimal, val totalQty: BigDecimal, val jobOrders: List = emptyList(), +) + +data class DrinkShipmentQtyDeliveryDetail( + val deliveryOrderId: Long, + val deliveryOrderCode: String?, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + val deliveryDate: LocalDate?, + val shopCode: String?, + val shopName: String?, + val deliveryOrderStatus: String?, + val orderQty: BigDecimal, + val shippedQty: BigDecimal, +) + +data class DrinkShipmentQtyResponse( + val itemCode: String?, + val itemName: String?, + val uom: String?, + val totalOrderQty: BigDecimal, + val totalShippedQty: BigDecimal, + val deliveries: List = emptyList(), ) \ No newline at end of file