From a4a6e818336a271ce0cedd2e7b66298c8eb4881d Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 22 Apr 2026 16:32:59 +0800 Subject: [PATCH] update job order record --- .../jobOrder/service/JoPickOrderService.kt | 228 +++++++++++++++++- .../jobOrder/service/JobOrderService.kt | 2 +- .../jobOrder/web/JobOrderController.kt | 52 ++++ .../pickOrder/entity/PickOrderRepository.kt | 17 ++ .../20260420_01_Enson/02_alter_stock_take.sql | 10 + 5 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20260420_01_Enson/02_alter_stock_take.sql diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt index 269f09d..b41122a 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt @@ -1702,7 +1702,233 @@ open fun getCompletedJobOrderPickOrders(completedDate: LocalDate?): List> { + println("=== getJobOrderPickOrders ===") + println("date: $date, status: $status") + + return try { + val allPickOrders = + when { + date != null -> { + val from = date.atStartOfDay() + val toExclusive = date.plusDays(1).atStartOfDay() + pickOrderRepository.findByJobOrderPlanStartOnDayAndOptionalStatus(from, toExclusive, status) + } + status != null -> { + pickOrderRepository.findAllByStatusAndDeletedFalse(status) + .filter { it.jobOrder != null && it.jobOrder?.isHidden != true } + } + else -> { + pickOrderRepository.findAllByStatusIn(PickOrderStatus.entries.toList()) + .filter { !it.deleted && it.jobOrder != null && it.jobOrder?.isHidden != true } + } + } + + val pickOrderIds = allPickOrders.mapNotNull { it.id }.distinct() + val joByPickOrderId: Map> = + if (pickOrderIds.isNotEmpty()) { + joPickOrderRepository.findByPickOrderIdIn(pickOrderIds) + .filterNot { it.deleted } + .groupBy { it.pickOrderId ?: 0L } + .filterKeys { it != 0L } + } else { + emptyMap() + } + + allPickOrders.mapNotNull { pickOrder -> + val jobOrder = pickOrder.jobOrder ?: return@mapNotNull null + val poId = pickOrder.id ?: return@mapNotNull null + val joPickOrders = joByPickOrderId[poId].orEmpty() + + val secondScanCompleted = joPickOrders.isNotEmpty() && + joPickOrders.all { it.matchStatus == com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus.completed } + + mapOf( + "id" to pickOrder.id, + "pickOrderId" to pickOrder.id, + "pickOrderCode" to pickOrder.code, + "pickOrderConsoCode" to pickOrder.consoCode, + "pickOrderTargetDate" to pickOrder.targetDate?.let { + "${it.year}-${"%02d".format(it.monthValue)}-${"%02d".format(it.dayOfMonth)}" + }, + "pickOrderStatus" to pickOrder.status, + "completedDate" to jobOrder.planStart, + "jobOrderId" to jobOrder.id, + "jobOrderCode" to jobOrder.code, + "jobOrderName" to jobOrder.bom?.name, + "reqQty" to jobOrder.reqQty, + "uom" to jobOrder.bom?.uom?.code, + "planStart" to jobOrder.planStart, + "planEnd" to jobOrder.planEnd, + "secondScanCompleted" to secondScanCompleted, + "totalItems" to joPickOrders.size, + "completedItems" to joPickOrders.count { + it.matchStatus == com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus.completed + }, + ) + } + } catch (e: Exception) { + println("❌ Error in getJobOrderPickOrders: ${e.message}") + e.printStackTrace() + emptyList() + } + } + open fun getJobOrderPickOrderLotDetails( + pickOrderId: Long, + status: String? = null, // 可選,不傳=全部狀態 + ): List> { + println("=== getJobOrderPickOrderLotDetails (all status) ===") + println("pickOrderId: $pickOrderId, status: $status") + + return try { + val sql = """ + SELECT + -- Pick Order Information + po.id as pickOrderId, + po.code as pickOrderCode, + po.consoCode as pickOrderConsoCode, + DATE_FORMAT(po.targetDate, '%Y-%m-%d') as pickOrderTargetDate, + po.status as pickOrderStatus, + + -- Job Order Information + jo.id as jobOrderId, + jo.code as jobOrderCode, + + -- Pick Order Line Information + pol.id as pickOrderLineId, + pol.qty as pickOrderLineRequiredQty, + pol.status as pickOrderLineStatus, + + -- Item Information + i.id as itemId, + i.code as itemCode, + i.name as itemName, + uc.code as uomCode, + uc.udfudesc as uomDesc, + uc.udfShortDesc as uomShortDesc, + + -- Lot Information + ill.id as lotId, + il.lotNo, + DATE_FORMAT(il.expiryDate, '%Y-%m-%d') as expiryDate, + w.name as location, + COALESCE(uc.udfudesc, 'N/A') as stockUnit, + + -- Router Information + w.id as routerId, + w.`order` as routerIndex, + w.code as routerRoute, + w.code as routerArea, + + -- Set quantities to NULL for rejected lots + CASE + WHEN sol.status = 'rejected' THEN NULL + ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) + END as availableQty, + + -- Required quantity for this lot + COALESCE(spl.qty, 0) as requiredQty, + + -- Actual picked quantity + COALESCE(sol.qty, 0) as actualPickQty, + + -- Suggested pick lot information + spl.id as suggestedPickLotId, + ill.status as lotStatus, + + -- Stock out line information + sol.id as stockOutLineId, + sol.status as stockOutLineStatus, + COALESCE(sol.qty, 0) as stockOutLineQty, + + -- Additional detailed fields + COALESCE(ill.inQty, 0) as inQty, + COALESCE(ill.outQty, 0) as outQty, + COALESCE(ill.holdQty, 0) as holdQty, + + -- Calculate total picked quantity by ALL pick orders for this lot + COALESCE(( + SELECT SUM(sol_all.qty) + FROM fpsmsdb.stock_out_line sol_all + WHERE sol_all.inventoryLotLineId = ill.id + AND sol_all.deleted = false + AND sol_all.status IN ('pending', 'checked', 'partially_completed', 'completed', 'picking') + ), 0) as totalPickedByAllPickOrders, + + -- Calculate remaining available quantity correctly + (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as remainingAfterAllPickOrders, + + -- Lot availability status + CASE + WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' + WHEN sol.status = 'rejected' THEN 'rejected' + WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock' + WHEN ill.status = 'unavailable' THEN 'status_unavailable' + ELSE 'available' + END as lotAvailability, + + -- Processing status (all-status friendly) + CASE + WHEN sol.status = 'completed' THEN 'completed' + WHEN sol.status = 'rejected' THEN 'rejected' + WHEN sol.status IN ('checked', 'partially_completed', 'picking') THEN 'in_progress' + WHEN sol.status IN ('created', 'pending') THEN 'pending' + ELSE COALESCE(sol.status, 'pending') + END as processingStatus, + + -- JoPickOrder second scan status + jpo.match_status as match_status, + jpo.match_by as match_by, + jpo.match_qty as match_qty + + FROM fpsmsdb.pick_order po + JOIN fpsmsdb.job_order jo ON jo.id = po.joId + JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id + JOIN fpsmsdb.items i ON i.id = pol.itemId + LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId + LEFT JOIN fpsmsdb.suggested_pick_lot spl ON pol.id = spl.pickOrderLineId + LEFT JOIN fpsmsdb.inventory_lot_line ill ON spl.suggestedLotLineId = ill.id + LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId + LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId + -- no-lot:ill.id 為 null 時,匹配 sol.inventoryLotLineId IS NULL + LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false + AND ( + sol.inventoryLotLineId = ill.id + OR (sol.inventoryLotLineId IS NULL AND ill.id IS NULL) + ) + LEFT JOIN fpsmsdb.jo_pick_order jpo ON jpo.pick_order_id = po.id AND jpo.item_id = pol.itemId + WHERE po.deleted = false + AND po.id = :pickOrderId + AND pol.deleted = false + AND (:status IS NULL OR po.status = :status) + -- ill / il 可能為 null(no-lot),避免過濾掉 no-lot 記錄 + AND (ill.id IS NULL OR ill.deleted = false) + AND (il.id IS NULL OR il.deleted = false) + AND (spl.pickOrderLineId IS NOT NULL OR sol.pickOrderLineId IS NOT NULL) + ORDER BY + CASE WHEN sol.status = 'rejected' THEN 0 ELSE 1 END, + COALESCE(w.`order`, 999999) ASC, + po.code ASC, + i.code ASC, + il.expiryDate ASC, + il.lotNo ASC + """.trimIndent() + + val params = mapOf( + "pickOrderId" to pickOrderId, + "status" to status?.trim()?.takeIf { it.isNotEmpty() }, // null = all status + ) + + println("With parameters: $params") + val results = jdbcDao.queryForList(sql, params) + println("Found ${results.size} lot details for pickOrderId=$pickOrderId") + results + } catch (e: Exception) { + println("Error in getJobOrderPickOrderLotDetails: ${e.message}") + e.printStackTrace() + emptyList() + } + } open fun getCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId: Long): List> { println("=== getCompletedJobOrderPickOrderLotDetailsForCompletedPick ===") diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 6168c61..e8ec7c0 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -731,7 +731,7 @@ open class JobOrderService( } val inputStream = resource.inputStream val pickRecord = JasperCompileManager.compileReport(inputStream) - val pickRecordInfo = joPickOrderService.getCompletedJobOrderPickOrderLotDetails(request.pickOrderIds).toMutableList() + val pickRecordInfo = joPickOrderService.getJobOrderPickOrderLotDetails(request.pickOrderIds).toMutableList() val fields = mutableListOf>() diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt index f7369cb..4c3d199 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt @@ -20,6 +20,11 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import com.ffii.fpsms.modules.jobOrder.service.JoPickOrderService +import com.ffii.fpsms.modules.jobOrder.service.JoWorkbenchMainService +import com.ffii.fpsms.modules.jobOrder.service.JoWorkbenchReleaseService +import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus + +import org.springframework.format.annotation.DateTimeFormat import com.ffii.fpsms.modules.productProcess.service.ProductProcessService import com.ffii.fpsms.modules.jobOrder.web.model.* import com.ffii.fpsms.modules.jobOrder.web.model.ExportPickRecordRequest @@ -50,6 +55,8 @@ class JobOrderController( private val jobOrderBomMaterialService: JobOrderBomMaterialService, private val jobOrderProcessService: JobOrderProcessService, private val joPickOrderService: JoPickOrderService, + private val joWorkbenchMainService: JoWorkbenchMainService, + private val joWorkbenchReleaseService: JoWorkbenchReleaseService, private val productProcessService: ProductProcessService, private val jobOrderCreationService: com.ffii.fpsms.modules.jobOrder.service.JobOrderCreationService ) { @@ -82,6 +89,18 @@ class JobOrderController( return jobOrderService.releaseJobOrder(request) } + /** Workbench: same search as [allJobOrdersByPage] but dedicated route for future divergence. */ + @GetMapping("/workbench/getRecordByPage") + fun allJobOrdersByPageForWorkbench(@ModelAttribute request: SearchJobOrderInfoRequest): RecordsRes { + return jobOrderService.allJobOrdersByPageWithTypeName(request) + } + + /** Workbench: release without stock out / SPL / SOL until first pick-order assign. */ + @PostMapping("/workbench/release") + fun releaseJobOrderForWorkbench(@Valid @RequestBody request: JobOrderCommonActionRequest): MessageResponse { + return joWorkbenchReleaseService.releaseJobOrderForWorkbench(request) + } + @PostMapping("/set-hidden") fun setJobOrderHidden(@Valid @RequestBody request: SetJobOrderHiddenRequest): MessageResponse { return jobOrderService.setJobOrderHidden(request) @@ -128,6 +147,15 @@ class JobOrderController( ): MessageResponse { return joPickOrderService.assignJobOrderPickOrderToUser(pickOrderId, userId) } + + /** Workbench: assign + prime SPL/SOL when release was deferred (see [JoWorkbenchMainService]). */ + @PostMapping("/workbench/assign-job-order-pick-order/{pickOrderId}/{userId}") + fun assignJobOrderPickOrderToUserForWorkbench( + @PathVariable pickOrderId: Long, + @PathVariable userId: Long + ): MessageResponse { + return joWorkbenchMainService.assignJobOrderPickOrderToUserForWorkbench(pickOrderId, userId) + } @PostMapping("/unassign-job-order-pick-order/{pickOrderId}") fun unAssignJobOrderPickOrderToUser( @PathVariable pickOrderId: Long @@ -241,6 +269,24 @@ fun recordSecondScanIssue( ): List> { return joPickOrderService.getCompletedJobOrderPickOrders(completedDate) } + @GetMapping("/job-order-pick-orders") +fun getJobOrderPickOrders( + @RequestParam(name = "date", required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + date: LocalDate?, + @RequestParam(name = "status", required = false) + status: PickOrderStatus?, +): List> { + return joPickOrderService.getJobOrderPickOrders(date, status) +} + +@GetMapping("/job-order-pick-order-lot-details/{pickOrderId}") +fun getJobOrderPickOrderLotDetails( + @PathVariable pickOrderId: Long, + @RequestParam(name = "status", required = false) status: String?, +): List> { + return joPickOrderService.getJobOrderPickOrderLotDetails(pickOrderId, status) +} @GetMapping("/joForPrintQrCode/{date}") fun getJoForPrintQrCode(@PathVariable date: String): List { return joPickOrderService.getJobOrderListForPrintQrCode(LocalDate.parse(date)) @@ -264,6 +310,12 @@ fun recordSecondScanIssue( fun getJobOrderLotsHierarchicalByPickOrderId(@PathVariable pickOrderId: Long): JobOrderLotsHierarchicalResponse { return joPickOrderService.getJobOrderLotsHierarchicalByPickOrderId(pickOrderId) } + + /** Workbench: available qty uses in−out (matches scan-pick); stockouts include suggested SPL qty when matched. */ + @GetMapping("/all-lots-hierarchical-by-pick-order-workbench/{pickOrderId}") + fun getJobOrderLotsHierarchicalByPickOrderIdWorkbench(@PathVariable pickOrderId: Long): JobOrderLotsHierarchicalResponse { + return joWorkbenchMainService.getJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderId) + } @PostMapping("/update-jo-pick-order-handled-by") fun updateJoPickOrderHandledBy(@Valid @RequestBody request: UpdateJoPickOrderHandledByRequest): MessageResponse { try { diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt index 6616cdc..cea2874 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt @@ -179,4 +179,21 @@ fun findReleasedIdsForPickExecution( @Param("status") status: PickOrderStatus, @Param("excludedTypes") excludedTypes: List, ): List + +@Query(""" +SELECT po +FROM PickOrder po +JOIN po.jobOrder jo +WHERE po.deleted = false + AND po.jobOrder IS NOT NULL + AND (jo.isHidden = false OR jo.isHidden IS NULL) + AND jo.planStart >= :from + AND jo.planStart < :toExclusive + AND (:status IS NULL OR po.status = :status) +""") +fun findByJobOrderPlanStartOnDayAndOptionalStatus( + @Param("from") from: LocalDateTime, + @Param("toExclusive") toExclusive: LocalDateTime, + @Param("status") status: PickOrderStatus?, +): List } \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20260420_01_Enson/02_alter_stock_take.sql b/src/main/resources/db/changelog/changes/20260420_01_Enson/02_alter_stock_take.sql new file mode 100644 index 0000000..d42e96a --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260420_01_Enson/02_alter_stock_take.sql @@ -0,0 +1,10 @@ +--liquibase formatted sql + +--changeset Enson:20260422-01 +CREATE INDEX idx_jo_planstart_ishidden ON job_order (planStart, isHidden, id); + +--changeset Enson:20260422-02 +CREATE INDEX idx_po_status_deleted_joid ON pick_order (status, deleted, joId); + +--changeset Enson:20260422-03 +CREATE INDEX idx_jpo_pickorder_deleted_match ON jo_pick_order (pick_order_id, deleted, match_status); \ No newline at end of file