| @@ -1702,7 +1702,233 @@ open fun getCompletedJobOrderPickOrders(completedDate: LocalDate?): List<Map<Str | |||||
| emptyList() | emptyList() | ||||
| } | } | ||||
| } | } | ||||
| // ... rest of the code ... | |||||
| open fun getJobOrderPickOrders(date: LocalDate?, status: PickOrderStatus?): List<Map<String, Any?>> { | |||||
| 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<Long, List<JoPickOrder>> = | |||||
| 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<Map<String, Any?>> { | |||||
| 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<Map<String, Any?>> { | open fun getCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId: Long): List<Map<String, Any?>> { | ||||
| println("=== getCompletedJobOrderPickOrderLotDetailsForCompletedPick ===") | println("=== getCompletedJobOrderPickOrderLotDetailsForCompletedPick ===") | ||||
| @@ -731,7 +731,7 @@ open class JobOrderService( | |||||
| } | } | ||||
| val inputStream = resource.inputStream | val inputStream = resource.inputStream | ||||
| val pickRecord = JasperCompileManager.compileReport(inputStream) | val pickRecord = JasperCompileManager.compileReport(inputStream) | ||||
| val pickRecordInfo = joPickOrderService.getCompletedJobOrderPickOrderLotDetails(request.pickOrderIds).toMutableList() | |||||
| val pickRecordInfo = joPickOrderService.getJobOrderPickOrderLotDetails(request.pickOrderIds).toMutableList() | |||||
| val fields = mutableListOf<MutableMap<String, Any>>() | val fields = mutableListOf<MutableMap<String, Any>>() | ||||
| @@ -20,6 +20,11 @@ import org.springframework.web.bind.annotation.RequestBody | |||||
| import org.springframework.web.bind.annotation.RequestMapping | import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | import org.springframework.web.bind.annotation.RestController | ||||
| import com.ffii.fpsms.modules.jobOrder.service.JoPickOrderService | 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.productProcess.service.ProductProcessService | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.* | import com.ffii.fpsms.modules.jobOrder.web.model.* | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.ExportPickRecordRequest | import com.ffii.fpsms.modules.jobOrder.web.model.ExportPickRecordRequest | ||||
| @@ -50,6 +55,8 @@ class JobOrderController( | |||||
| private val jobOrderBomMaterialService: JobOrderBomMaterialService, | private val jobOrderBomMaterialService: JobOrderBomMaterialService, | ||||
| private val jobOrderProcessService: JobOrderProcessService, | private val jobOrderProcessService: JobOrderProcessService, | ||||
| private val joPickOrderService: JoPickOrderService, | private val joPickOrderService: JoPickOrderService, | ||||
| private val joWorkbenchMainService: JoWorkbenchMainService, | |||||
| private val joWorkbenchReleaseService: JoWorkbenchReleaseService, | |||||
| private val productProcessService: ProductProcessService, | private val productProcessService: ProductProcessService, | ||||
| private val jobOrderCreationService: com.ffii.fpsms.modules.jobOrder.service.JobOrderCreationService | private val jobOrderCreationService: com.ffii.fpsms.modules.jobOrder.service.JobOrderCreationService | ||||
| ) { | ) { | ||||
| @@ -82,6 +89,18 @@ class JobOrderController( | |||||
| return jobOrderService.releaseJobOrder(request) | return jobOrderService.releaseJobOrder(request) | ||||
| } | } | ||||
| /** Workbench: same search as [allJobOrdersByPage] but dedicated route for future divergence. */ | |||||
| @GetMapping("/workbench/getRecordByPage") | |||||
| fun allJobOrdersByPageForWorkbench(@ModelAttribute request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfoWithTypeName> { | |||||
| 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") | @PostMapping("/set-hidden") | ||||
| fun setJobOrderHidden(@Valid @RequestBody request: SetJobOrderHiddenRequest): MessageResponse { | fun setJobOrderHidden(@Valid @RequestBody request: SetJobOrderHiddenRequest): MessageResponse { | ||||
| return jobOrderService.setJobOrderHidden(request) | return jobOrderService.setJobOrderHidden(request) | ||||
| @@ -128,6 +147,15 @@ class JobOrderController( | |||||
| ): MessageResponse { | ): MessageResponse { | ||||
| return joPickOrderService.assignJobOrderPickOrderToUser(pickOrderId, userId) | 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}") | @PostMapping("/unassign-job-order-pick-order/{pickOrderId}") | ||||
| fun unAssignJobOrderPickOrderToUser( | fun unAssignJobOrderPickOrderToUser( | ||||
| @PathVariable pickOrderId: Long | @PathVariable pickOrderId: Long | ||||
| @@ -241,6 +269,24 @@ fun recordSecondScanIssue( | |||||
| ): List<Map<String, Any?>> { | ): List<Map<String, Any?>> { | ||||
| return joPickOrderService.getCompletedJobOrderPickOrders(completedDate) | 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<Map<String, Any?>> { | |||||
| 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<Map<String, Any?>> { | |||||
| return joPickOrderService.getJobOrderPickOrderLotDetails(pickOrderId, status) | |||||
| } | |||||
| @GetMapping("/joForPrintQrCode/{date}") | @GetMapping("/joForPrintQrCode/{date}") | ||||
| fun getJoForPrintQrCode(@PathVariable date: String): List<JobOrderListForPrintQrCodeResponse> { | fun getJoForPrintQrCode(@PathVariable date: String): List<JobOrderListForPrintQrCodeResponse> { | ||||
| return joPickOrderService.getJobOrderListForPrintQrCode(LocalDate.parse(date)) | return joPickOrderService.getJobOrderListForPrintQrCode(LocalDate.parse(date)) | ||||
| @@ -264,6 +310,12 @@ fun recordSecondScanIssue( | |||||
| fun getJobOrderLotsHierarchicalByPickOrderId(@PathVariable pickOrderId: Long): JobOrderLotsHierarchicalResponse { | fun getJobOrderLotsHierarchicalByPickOrderId(@PathVariable pickOrderId: Long): JobOrderLotsHierarchicalResponse { | ||||
| return joPickOrderService.getJobOrderLotsHierarchicalByPickOrderId(pickOrderId) | 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") | @PostMapping("/update-jo-pick-order-handled-by") | ||||
| fun updateJoPickOrderHandledBy(@Valid @RequestBody request: UpdateJoPickOrderHandledByRequest): MessageResponse { | fun updateJoPickOrderHandledBy(@Valid @RequestBody request: UpdateJoPickOrderHandledByRequest): MessageResponse { | ||||
| try { | try { | ||||
| @@ -179,4 +179,21 @@ fun findReleasedIdsForPickExecution( | |||||
| @Param("status") status: PickOrderStatus, | @Param("status") status: PickOrderStatus, | ||||
| @Param("excludedTypes") excludedTypes: List<PickOrderType>, | @Param("excludedTypes") excludedTypes: List<PickOrderType>, | ||||
| ): List<Long> | ): List<Long> | ||||
| @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<PickOrder> | |||||
| } | } | ||||
| @@ -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); | |||||