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 857bec3..235e533 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,6 +85,10 @@ 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}") @@ -1246,6 +1250,7 @@ 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) @@ -1254,6 +1259,12 @@ 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( @@ -1308,7 +1319,7 @@ open class ProductProcessService( val productProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) println(" Service: ProductProcessLines: $productProcessLines") - if (productProcessLines.all { it.status == "Completed" || it.status == "Pass" }) { + if (productProcessLines.all { isLineDone(it.status) }) { productProcess.status = ProductProcessStatus.COMPLETED if (productProcess.endTime == null) { productProcess.endTime = LocalDateTime.now() @@ -1500,8 +1511,9 @@ open class ProductProcessService( } /** - * FP-MTMS Version Checklist | Functions Ref. No. 41 | v1.0.1 | 2026-08-06 - * QC 列表:按 jobOrder 粒度分页,qcReady 由该 jobOrder 下所有 productProcessLine 是否全部 Completed/Pass 决定。 + * 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,因此不再需要「任一包裝即可」特例。) * * 注意:date/itemCode/jobOrderCode/bomIds 用来筛选“候选 jobOrder”,但 qcReady 判断会用该 jobOrder 下全部 productProcessLine。 */ @@ -1651,10 +1663,6 @@ 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] @@ -1676,20 +1684,12 @@ open class ProductProcessService( val lineAggregate = lineStatusByJobOrderId[jobOrderId] val totalLines = lineAggregate?.totalLines ?: 0L - val done = { s: String? -> - val x = s?.trim()?.lowercase() - x == "completed" || x == "pass" - } - - - val jobLines = linesByJobOrder[jobOrderId].orEmpty() - val packing = jobLines.filter { (it.name ?: "").trim() == "包裝" } - val nonPacking = jobLines.filter { (it.name ?: "").trim() != "包裝" } - val nonPackingOk = nonPacking.isEmpty() || nonPacking.all { done(it.status) } - val packingOk = packing.isEmpty() || packing.any { done(it.status) } + // After packaging Complete/Pass autoPasses sibling 包裝 lines, qcReady is simply all lines done. + val jobLines = linesByJobOrder[jobOrderId].orEmpty() + val allLinesDone = jobLines.isNotEmpty() && jobLines.all { isLineDone(it.status) } - val ready = includedInList && stockInLine != null && nonPackingOk && packingOk + val ready = includedInList && stockInLine != null && allLinesDone JobOrderQcKey( jobOrderId = jobOrderId, @@ -1877,10 +1877,7 @@ open class ProductProcessService( val joPickOrdersList = if (pickOrderId != null) joPickOrdersByPickOrderId[pickOrderId].orEmpty() else emptyList() val productProcessLines = linesByProcessId[productProcess.id ?: 0L].orEmpty() - val finishedCount = productProcessLines.count { - val s = it.status?.trim()?.lowercase() - s == "completed" || s == "pass" - } + val finishedCount = productProcessLines.count { isLineDone(it.status) } val bomIsDrink = productProcess.bom?.isDrink val matchStatus = if (joPickOrdersList.isNotEmpty() && @@ -1954,7 +1951,7 @@ open class ProductProcessService( } /** - * FG QC / 上架 reminders: same eligibility as 完成QC工單 (qcReady): all lines Completed/Pass, stock-in exists and not completed/rejected; + * FG QC / 上架 reminders: same eligibility as 完成QC工單 (qcReady): all lines Completed/Pass/autoPass, 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 { @@ -2008,7 +2005,7 @@ open class ProductProcessService( val jobOrderLines = processes.flatMap { p -> linesByProcessId[p.id ?: 0L].orEmpty() } val allLinesDone = jobOrderLines.isNotEmpty() && - jobOrderLines.all { it.status == "Completed" || it.status == "Pass" } + jobOrderLines.all { isLineDone(it.status) } if (!allLinesDone) continue val maxDate = processes.mapNotNull { it.date }.maxOrNull() @@ -2110,6 +2107,12 @@ 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" } @@ -2119,9 +2122,54 @@ 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 -> completed (via [ifAllLinesCompletedOrPassed]) + * - All Completed/Pass/autoPass -> completed (via [ifAllLinesCompletedOrPassed]) * - Any line started but not all done -> in_progress * - Does not override STOPPED or CANCELLED on the parent. */ @@ -2201,8 +2249,8 @@ open class ProductProcessService( // 获取所有 product process lines val allproductProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) - // 检查是否所有 lines 都是 "Completed" 或 "Pass" - if (allproductProcessLines.all { it.status == "Completed" || it.status == "Pass" }) { + // 检查是否所有 lines 都是 Completed / Pass / autoPass + if (allproductProcessLines.all { isLineDone(it.status) }) { // 更新 product process 的 endTime 和状态 updateProductProcessEndTime(productProcessId) updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) @@ -2631,9 +2679,6 @@ 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( @@ -3047,7 +3092,7 @@ open class ProductProcessService( } val currentProcesses = operatorLines - .filter { it.endTime == null || (it.status != null && it.status != "Completed" && it.status != "Pass") } + .filter { it.endTime == null || (it.status != null && !isLineDone(it.status)) } .map { line -> val productProcess = line.productProcess val jobOrder = productProcess.jobOrder 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 781a450..01afa35 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, stock-in not completed/rejected; + * Nav alerts aligned with 完成QC工單 list: all product process lines Completed/Pass/autoPass, 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(