| @@ -0,0 +1,80 @@ | |||
| package com.ffii.fpsms.modules.jobOrder.entity | |||
| import jakarta.persistence.* | |||
| import java.time.LocalDateTime | |||
| import java.time.LocalTime | |||
| import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus | |||
| import org.hibernate.annotations.CreationTimestamp | |||
| import org.hibernate.annotations.UpdateTimestamp | |||
| @Entity | |||
| @Table(name = "jo_pick_order") | |||
| class JoPickOrder { | |||
| @Id | |||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | |||
| var id: Long? = null | |||
| @Column(name = "pick_order_id") | |||
| var pickOrderId: Long? = null | |||
| @Column(name = "item_id") | |||
| var itemId: Long? = null | |||
| @Enumerated(EnumType.STRING) | |||
| @Column(name = "second_qr_scan_status") | |||
| var second_qr_scan_status: JoPickOrderStatus? = null | |||
| @Column(name = "second_qr_scan_by") | |||
| var second_qr_scan_by: Long? = null | |||
| @Column(name = "second_qr_scan_qty") | |||
| var second_qr_scan_qty: Int? = null | |||
| @Column(name = "handled_by") | |||
| var handledBy: Long? = null | |||
| @CreationTimestamp | |||
| @Column(name = "created") | |||
| var created: LocalDateTime? = null | |||
| @Column(name = "createdBy", length = 30) | |||
| var createdBy: String? = null | |||
| @Version | |||
| var version: Int = 0 | |||
| @UpdateTimestamp | |||
| @Column(name = "modified") | |||
| var modified: LocalDateTime? = null | |||
| @Column(name = "modifiedBy", length = 30) | |||
| var modifiedBy: String? = null | |||
| @Column(name = "deleted") | |||
| var deleted: Boolean = false | |||
| @Column(name = "hide", nullable = false) | |||
| var hide: Boolean = false | |||
| // Default constructor for Hibernate | |||
| constructor() | |||
| // Constructor for creating new instances | |||
| constructor( | |||
| pickOrderId: Long? = null, | |||
| itemId: Long? = null, | |||
| second_qr_scan_status: JoPickOrderStatus? = null, | |||
| second_qr_scan_by: Long? = null, | |||
| second_qr_scan_qty: Int? = null, | |||
| handledBy: Long? = null, | |||
| createdBy: String? = null, | |||
| modifiedBy: String? = null | |||
| ) { | |||
| this.pickOrderId = pickOrderId | |||
| this.itemId = itemId | |||
| this.second_qr_scan_status = second_qr_scan_status | |||
| this.second_qr_scan_by = second_qr_scan_by | |||
| this.second_qr_scan_qty = second_qr_scan_qty | |||
| this.handledBy = handledBy | |||
| this.createdBy = createdBy | |||
| this.modifiedBy = modifiedBy | |||
| } | |||
| } | |||
| @@ -0,0 +1,80 @@ | |||
| package com.ffii.fpsms.modules.jobOrder.entity | |||
| import jakarta.persistence.* | |||
| import java.time.LocalDateTime | |||
| import java.time.LocalTime | |||
| import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus | |||
| import org.hibernate.annotations.CreationTimestamp | |||
| import org.hibernate.annotations.UpdateTimestamp | |||
| @Entity | |||
| @Table(name = "jo_pick_order_record") | |||
| class JoPickOrderRecord { | |||
| @Id | |||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | |||
| var id: Long? = null | |||
| @Column(name = "pick_order_id") | |||
| var pickOrderId: Long? = null | |||
| @Column(name = "item_id") | |||
| var itemId: Long? = null | |||
| @Enumerated(EnumType.STRING) | |||
| @Column(name = "second_qr_scan_status") | |||
| var second_qr_scan_status: JoPickOrderStatus? = null | |||
| @Column(name = "second_qr_scan_by") | |||
| var second_qr_scan_by: Long? = null | |||
| @Column(name = "second_qr_scan_qty") | |||
| var second_qr_scan_qty: Int? = null | |||
| @Column(name = "handled_by") | |||
| var handledBy: Long? = null | |||
| @CreationTimestamp | |||
| @Column(name = "created") | |||
| var created: LocalDateTime? = null | |||
| @Column(name = "createdBy", length = 30) | |||
| var createdBy: String? = null | |||
| @Version | |||
| var version: Int = 0 | |||
| @UpdateTimestamp | |||
| @Column(name = "modified") | |||
| var modified: LocalDateTime? = null | |||
| @Column(name = "modifiedBy", length = 30) | |||
| var modifiedBy: String? = null | |||
| @Column(name = "deleted") | |||
| var deleted: Boolean = false | |||
| @Column(name = "hide", nullable = false) | |||
| var hide: Boolean = false | |||
| // Default constructor for Hibernate | |||
| constructor() | |||
| // Constructor for creating new instances | |||
| constructor( | |||
| pickOrderId: Long? = null, | |||
| itemId: Long? = null, | |||
| second_qr_scan_status: JoPickOrderStatus? = null, | |||
| second_qr_scan_by: Long? = null, | |||
| second_qr_scan_qty: Int? = null, | |||
| handledBy: Long? = null, | |||
| createdBy: String? = null, | |||
| modifiedBy: String? = null | |||
| ) { | |||
| this.pickOrderId = pickOrderId | |||
| this.itemId = itemId | |||
| this.second_qr_scan_status = second_qr_scan_status | |||
| this.second_qr_scan_by = second_qr_scan_by | |||
| this.second_qr_scan_qty = second_qr_scan_qty | |||
| this.handledBy = handledBy | |||
| this.createdBy = createdBy | |||
| this.modifiedBy = modifiedBy | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| package com.ffii.fpsms.modules.jobOrder.entity | |||
| import com.ffii.core.support.AbstractRepository | |||
| import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus | |||
| import com.ffii.fpsms.modules.master.entity.projections.SearchId | |||
| import org.springframework.data.jpa.repository.JpaRepository | |||
| import org.springframework.data.jpa.repository.Query | |||
| import org.springframework.stereotype.Repository | |||
| import java.io.Serializable | |||
| import java.time.LocalDateTime | |||
| @Repository | |||
| interface JoPickOrderRecordRepository : JpaRepository<JoPickOrderRecord, Long> { | |||
| fun findByPickOrderId(pickOrderId: Long): List<JoPickOrderRecord> | |||
| //fun findByTicketNoStartingWith(ticketPrefix: String): List<JoPickOrderRecord> | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| package com.ffii.fpsms.modules.jobOrder.entity | |||
| import com.ffii.core.support.AbstractRepository | |||
| import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrder | |||
| import com.ffii.fpsms.modules.master.entity.projections.SearchId | |||
| import org.springframework.data.jpa.repository.JpaRepository | |||
| import org.springframework.data.jpa.repository.Query | |||
| import org.springframework.stereotype.Repository | |||
| import java.io.Serializable | |||
| import java.time.LocalDateTime | |||
| @Repository | |||
| interface JoPickOrderRepository : JpaRepository<JoPickOrder, Long> { | |||
| fun findByPickOrderId(pickOrderId: Long): List<JoPickOrder> | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| package com.ffii.fpsms.modules.jobOrder.enums | |||
| enum class JoPickOrderStatus(val value: String) { | |||
| pending("pending"), | |||
| scanned("scanned"), | |||
| completed("completed") | |||
| } | |||
| @@ -0,0 +1,447 @@ | |||
| package com.ffii.fpsms.modules.jobOrder.service | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrder | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecord | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository | |||
| import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus | |||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||
| import com.ffii.fpsms.modules.pickOrder.service.PickOrderService | |||
| import com.ffii.fpsms.modules.user.service.UserService | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | |||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.transaction.annotation.Transactional | |||
| import java.time.LocalDate | |||
| import java.time.LocalDateTime | |||
| @Service | |||
| open class JoPickOrderService( | |||
| private val joPickOrderRepository: JoPickOrderRepository, | |||
| private val joPickOrderRecordRepository: JoPickOrderRecordRepository, | |||
| private val pickOrderService: PickOrderService, | |||
| private val userService: UserService, | |||
| private val pickOrderRepository: PickOrderRepository | |||
| ) { | |||
| open fun save(record: JoPickOrder): JoPickOrder { | |||
| return joPickOrderRepository.save(record) | |||
| } | |||
| open fun saveRecord(record: JoPickOrderRecord): JoPickOrderRecord { | |||
| return joPickOrderRecordRepository.save(record) | |||
| } | |||
| // ✅ Update JoPickOrder status to released | |||
| open fun updateHandledByForPickOrder(pickOrderId: Long, userId: Long): List<JoPickOrder> { | |||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| joPickOrders.forEach { | |||
| it.handledBy = userId | |||
| it.second_qr_scan_status = JoPickOrderStatus.pending // Set initial status | |||
| } | |||
| return joPickOrderRepository.saveAll(joPickOrders) | |||
| } | |||
| // ✅ Complete JoPickOrder | |||
| open fun completeJoPickOrdersForPickOrder(pickOrderId: Long): List<JoPickOrder> { | |||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| joPickOrders.forEach { | |||
| it.second_qr_scan_status = JoPickOrderStatus.completed | |||
| } | |||
| return joPickOrderRepository.saveAll(joPickOrders) | |||
| } | |||
| // ✅ Update JoPickOrderRecord status to released | |||
| open fun updateRecordHandledByForPickOrder(pickOrderId: Long, userId: Long): List<JoPickOrderRecord> { | |||
| val joPickOrderRecords = joPickOrderRecordRepository.findByPickOrderId(pickOrderId) | |||
| joPickOrderRecords.forEach { | |||
| it.handledBy = userId | |||
| it.second_qr_scan_status = JoPickOrderStatus.pending // Set initial status | |||
| } | |||
| return joPickOrderRecordRepository.saveAll(joPickOrderRecords) | |||
| } | |||
| // ✅ Complete JoPickOrderRecord | |||
| open fun completeJoPickOrderRecordsForPickOrder(pickOrderId: Long): List<JoPickOrderRecord> { | |||
| val joPickOrderRecords = joPickOrderRecordRepository.findByPickOrderId(pickOrderId) | |||
| joPickOrderRecords.forEach { | |||
| it.second_qr_scan_status = JoPickOrderStatus.completed | |||
| } | |||
| return joPickOrderRecordRepository.saveAll(joPickOrderRecords) | |||
| } | |||
| // ✅ Find JoPickOrder records by pick order ID | |||
| open fun findByPickOrderId(pickOrderId: Long): List<JoPickOrder> { | |||
| return joPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| } | |||
| // ✅ Find JoPickOrderRecord records by pick order ID | |||
| open fun findRecordsByPickOrderId(pickOrderId: Long): List<JoPickOrderRecord> { | |||
| return joPickOrderRecordRepository.findByPickOrderId(pickOrderId) | |||
| } | |||
| // ✅ Create JoPickOrder records for a pick order | |||
| @Transactional | |||
| open fun createJoPickOrdersForPickOrder(pickOrderId: Long, itemIds: List<Long>): List<JoPickOrder> { | |||
| val joPickOrders = itemIds.map { itemId -> | |||
| JoPickOrder( | |||
| pickOrderId = pickOrderId, | |||
| itemId = itemId, | |||
| second_qr_scan_status = JoPickOrderStatus.pending, | |||
| handledBy = null | |||
| ) | |||
| } | |||
| return joPickOrderRepository.saveAll(joPickOrders) | |||
| } | |||
| // ✅ Create JoPickOrderRecord records for a pick order | |||
| @Transactional | |||
| open fun createJoPickOrderRecordsForPickOrder(pickOrderId: Long, itemIds: List<Long>): List<JoPickOrderRecord> { | |||
| val joPickOrderRecords = itemIds.map { itemId -> | |||
| JoPickOrderRecord( | |||
| pickOrderId = pickOrderId, | |||
| itemId = itemId, | |||
| second_qr_scan_status = JoPickOrderStatus.pending, | |||
| handledBy = null | |||
| ) | |||
| } | |||
| return joPickOrderRecordRepository.saveAll(joPickOrderRecords) | |||
| } | |||
| // ✅ Get all job order lots with details hierarchical | |||
| open fun getAllJobOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | |||
| println("=== Debug: getAllJobOrderLotsWithDetailsHierarchical ===") | |||
| println("today: ${LocalDate.now()}") | |||
| println("userId filter: $userId") | |||
| // Get all pick order IDs assigned to the user (both RELEASED and PENDING with joId) | |||
| val user = userService.find(userId).orElse(null) | |||
| if (user == null) { | |||
| println("❌ User not found: $userId") | |||
| return emptyMap() | |||
| } | |||
| val statusList = listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED) | |||
| // Get all pick orders assigned to user with PENDING or RELEASED status that have joId | |||
| val allAssignedPickOrders = pickOrderRepository.findAllByAssignToIdAndStatusIn( | |||
| userId, | |||
| statusList | |||
| ).filter { it.jobOrder != null } // Only pick orders with joId | |||
| println("🔍 DEBUG: Found ${allAssignedPickOrders.size} job order pick orders assigned to user $userId") | |||
| val visiblePickOrders = allAssignedPickOrders.filter { pickOrder -> | |||
| val joPickOrders = findByPickOrderId(pickOrder.id!!) | |||
| joPickOrders.none { it.hide } // Only show hide = false orders | |||
| } | |||
| // Filter based on assignment and status | |||
| val filteredPickOrders = if (visiblePickOrders.isNotEmpty()) { | |||
| // Check if there are any RELEASED orders assigned to this user (active work) | |||
| val assignedReleasedOrders = allAssignedPickOrders.filter { | |||
| it.status == PickOrderStatus.RELEASED && it.assignTo?.id == userId | |||
| } | |||
| if (assignedReleasedOrders.isNotEmpty()) { | |||
| // If there are assigned RELEASED orders, show only those | |||
| println("🔍 DEBUG: Found ${assignedReleasedOrders.size} assigned RELEASED job orders, showing only those") | |||
| assignedReleasedOrders | |||
| } else { | |||
| // If no assigned RELEASED orders, show only the latest COMPLETED order | |||
| val completedOrders = allAssignedPickOrders.filter { it.status == PickOrderStatus.COMPLETED } | |||
| if (completedOrders.isNotEmpty()) { | |||
| val latestCompleted = completedOrders.maxByOrNull { it.completeDate ?: it.modified ?: LocalDateTime.MIN } | |||
| println("🔍 DEBUG: No assigned RELEASED job orders, showing latest completed order: ${latestCompleted?.code}") | |||
| listOfNotNull(latestCompleted) | |||
| } else { | |||
| println("🔍 DEBUG: No job orders found") | |||
| emptyList() | |||
| } | |||
| } | |||
| } else { | |||
| emptyList() | |||
| } | |||
| val pickOrderIds = filteredPickOrders.map { it.id!! } | |||
| println("🎯 Job Order Pick order IDs to fetch: $pickOrderIds") | |||
| if (pickOrderIds.isEmpty()) { | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| ) | |||
| } | |||
| // Use the same SQL query but transform the results into hierarchical structure | |||
| val pickOrderIdsStr = pickOrderIds.joinToString(",") | |||
| 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.type as pickOrderType, | |||
| po.status as pickOrderStatus, | |||
| po.assignTo as pickOrderAssignTo, | |||
| -- Job Order Information | |||
| jo.id as jobOrderId, | |||
| jo.code as jobOrderCode, | |||
| jo.name as jobOrderName, | |||
| -- 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 | |||
| r.id as routerId, | |||
| r.index as routerIndex, | |||
| r.route as routerRoute, | |||
| r.area 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, | |||
| COALESCE(spl.suggestedLotLineId, ill.id) as debugSuggestedLotLineId, | |||
| ill.inventoryLotId as debugInventoryLotId, | |||
| -- 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') | |||
| ), 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 | |||
| CASE | |||
| WHEN sol.status = 'completed' THEN 'completed' | |||
| WHEN sol.status = 'rejected' THEN 'rejected' | |||
| WHEN sol.status = 'created' THEN 'pending' | |||
| ELSE 'pending' | |||
| END as processingStatus, | |||
| -- JoPickOrder second scan status | |||
| jpo.second_qr_scan_status as secondQrScanStatus, | |||
| jpo.second_qr_scan_by as secondQrScanBy, | |||
| jpo.second_qr_scan_qty as secondQrScanQty | |||
| 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.router r ON r.inventoryLotId = ill.id AND r.deleted = false | |||
| LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||
| LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||
| LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ill.id AND sol.deleted = false | |||
| 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 IN ($pickOrderIdsStr) | |||
| AND pol.deleted = false | |||
| AND po.status IN ('PENDING', 'RELEASED', 'COMPLETED') | |||
| AND po.assignTo = :userId | |||
| AND ill.deleted = false | |||
| AND 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(r.index, 0) ASC, | |||
| po.code ASC, | |||
| i.code ASC, | |||
| il.expiryDate ASC, | |||
| il.lotNo ASC | |||
| """.trimIndent() | |||
| println("🔍 Executing SQL for job order hierarchical structure: $sql") | |||
| println("🔍 With parameters: userId = $userId, pickOrderIds = $pickOrderIdsStr") | |||
| // ✅ 修复:使用 PickOrderService 的现有方法 | |||
| val results = pickOrderService.getAllPickOrderLotsWithDetailsHierarchical(userId) | |||
| // 如果结果为空,返回空结构 | |||
| if (results.isEmpty()) { | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| ) | |||
| } | |||
| // 获取 pick order 信息 | |||
| val pickOrderInfo = results["pickOrder"] as? Map<String, Any?> | |||
| val pickOrderLines = results["pickOrderLines"] as? List<Map<String, Any?>> | |||
| // 添加 Job Order 信息到 pick order info | |||
| val enhancedPickOrderInfo = pickOrderInfo?.toMutableMap() ?: mutableMapOf() | |||
| enhancedPickOrderInfo["jobOrder"] = mapOf( | |||
| "id" to pickOrderInfo?.get("id"), | |||
| "code" to "JO-${pickOrderInfo?.get("code")}", | |||
| "name" to "Job Order ${pickOrderInfo?.get("code")}" | |||
| ) | |||
| return mapOf( | |||
| "pickOrder" to enhancedPickOrderInfo as Any?, | |||
| "pickOrderLines" to (pickOrderLines ?: emptyList()) as Any? | |||
| ) | |||
| } | |||
| // Get completed job order pick orders (for second tab) | |||
| open fun getCompletedJobOrderPickOrders(userId: Long): List<Map<String, Any?>> { | |||
| println("=== getCompletedJobOrderPickOrders ===") | |||
| println("userId: $userId") | |||
| return try { | |||
| // Get completed pick orders for job orders | |||
| val completedPickOrders = pickOrderRepository.findAllByAssignToIdAndStatusIn( | |||
| userId, | |||
| listOf(PickOrderStatus.COMPLETED) | |||
| ).filter { it.jobOrder != null } // Only job order pick orders | |||
| println("Found ${completedPickOrders.size} completed job order pick orders for user $userId") | |||
| val jobOrderPickOrders = completedPickOrders.mapNotNull { pickOrder -> | |||
| // Get job order details | |||
| val jobOrder = pickOrder.jobOrder | |||
| if (jobOrder != null) { | |||
| val joPickOrders = findByPickOrderId(pickOrder.id!!) | |||
| val isHidden = joPickOrders.any { it.hide } | |||
| if (!isHidden) { | |||
| mapOf( | |||
| "pickOrderId" to pickOrder.id, | |||
| "pickOrderCode" to pickOrder.code, | |||
| "pickOrderConsoCode" to pickOrder.consoCode, | |||
| "pickOrderTargetDate" to pickOrder.targetDate, | |||
| "pickOrderStatus" to pickOrder.status, | |||
| "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, | |||
| "completeDate" to pickOrder.completeDate | |||
| ) | |||
| } else { | |||
| println("❌ Pick order ${pickOrder.id} is hidden, skipping.") | |||
| null | |||
| } | |||
| } else { | |||
| println("❌ Pick order ${pickOrder.id} has no job order, skipping.") | |||
| null | |||
| } | |||
| } | |||
| println("Returning ${jobOrderPickOrders.size} completed job order pick orders") | |||
| jobOrderPickOrders | |||
| } catch (e: Exception) { | |||
| println("❌ Error in getCompletedJobOrderPickOrders: ${e.message}") | |||
| e.printStackTrace() | |||
| emptyList() | |||
| } | |||
| } | |||
| // Get completed job order pick order records (for third tab) | |||
| open fun getCompletedJobOrderPickOrderRecords(userId: Long): List<Map<String, Any?>> { | |||
| println("=== getCompletedJobOrderPickOrderRecords ===") | |||
| println("userId: $userId") | |||
| return try { | |||
| // Get all completed JoPickOrderRecord records for the user | |||
| val allRecords = joPickOrderRecordRepository.findAll() | |||
| val completedRecords = allRecords.filter { | |||
| it.second_qr_scan_status == JoPickOrderStatus.completed && | |||
| it.handledBy == userId | |||
| } | |||
| println("Found ${completedRecords.size} completed job order pick order records for user $userId") | |||
| val recordDetails = completedRecords.mapNotNull { record -> | |||
| // Get pick order details | |||
| val pickOrder = pickOrderRepository.findById(record.pickOrderId!!).orElse(null) | |||
| if (pickOrder != null && pickOrder.jobOrder != null) { | |||
| val jobOrder = pickOrder.jobOrder | |||
| mapOf( | |||
| "recordId" to record.id, | |||
| "pickOrderId" to pickOrder.id, | |||
| "pickOrderCode" to pickOrder.code, | |||
| "jobOrderId" to jobOrder?.id, | |||
| "jobOrderCode" to jobOrder?.code, | |||
| "jobOrderName" to jobOrder?.bom?.name, | |||
| "itemId" to record.itemId, | |||
| "secondQrScanStatus" to record.second_qr_scan_status, | |||
| "secondQrScanBy" to record.second_qr_scan_by, | |||
| "secondQrScanQty" to record.second_qr_scan_qty, | |||
| "handledBy" to record.handledBy, | |||
| "created" to record.created, | |||
| "completeDate" to pickOrder.completeDate | |||
| ) | |||
| } else { | |||
| println("❌ Record ${record.id} has no valid pick order or job order, skipping.") | |||
| null | |||
| } | |||
| } | |||
| println("Returning ${recordDetails.size} completed job order pick order records") | |||
| recordDetails | |||
| } catch (e: Exception) { | |||
| println("❌ Error in getCompletedJobOrderPickOrderRecords: ${e.message}") | |||
| e.printStackTrace() | |||
| emptyList() | |||
| } | |||
| } | |||
| } | |||
| @@ -30,7 +30,18 @@ import java.time.LocalDateTime | |||
| import java.time.format.DateTimeFormatter | |||
| import kotlin.jvm.optionals.getOrNull | |||
| import com.ffii.fpsms.modules.jobOrder.service.JoPickOrderService | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository | |||
| import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutRepository | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository | |||
| import com.ffii.fpsms.modules.stock.entity.StockOut | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutLine | |||
| import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | |||
| import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus | |||
| import com.ffii.fpsms.modules.stock.web.model.SuggestedPickLotForPolRequest | |||
| @Service | |||
| open class JobOrderService( | |||
| val jobOrderRepository: JobOrderRepository, | |||
| @@ -38,6 +49,13 @@ open class JobOrderService( | |||
| val userService: UserService, | |||
| val productionScheduleLineRepository: ProductionScheduleLineRepository, | |||
| val pickOrderService: PickOrderService, | |||
| val joPickOrderService: JoPickOrderService, | |||
| val pickOrderRepository: PickOrderRepository, | |||
| val pickOrderLineRepository: PickOrderLineRepository, | |||
| val suggestedPickLotService: SuggestedPickLotService, | |||
| val inventoryLotLineRepository: InventoryLotLineRepository, | |||
| val stockOutRepository: StockOutRepository, | |||
| val stockOutLineRepository: StockOutLIneRepository | |||
| ) { | |||
| open fun allJobOrdersByPage(request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfo> { | |||
| val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10); | |||
| @@ -161,7 +179,81 @@ open class JobOrderService( | |||
| pickOrderLine = pols | |||
| ) | |||
| pickOrderService.create(po) | |||
| val createdPickOrder = pickOrderService.create(po) | |||
| val consoCode = pickOrderService.assignConsoCode() | |||
| val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null) | |||
| if (pickOrderEntity != null) { | |||
| pickOrderEntity.consoCode = consoCode | |||
| pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED | |||
| pickOrderRepository.saveAndFlush(pickOrderEntity) | |||
| // ✅ 添加 suggested pick lots 创建逻辑 | |||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | |||
| if (lines.isNotEmpty()) { | |||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLines( | |||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | |||
| ) | |||
| val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | |||
| // ✅ Hold inventory quantities | |||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | |||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | |||
| ) | |||
| saveSuggestedPickLots.forEach { lot -> | |||
| if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) { | |||
| val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine) | |||
| if (lineIndex >= 0) { | |||
| inventoryLotLines[lineIndex].holdQty = | |||
| (inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO) | |||
| } | |||
| } | |||
| } | |||
| inventoryLotLineRepository.saveAll(inventoryLotLines) | |||
| // ✅ Create stock out record and pre-create stock out lines | |||
| val stockOut = StockOut().apply { | |||
| this.type = "job" | |||
| this.consoPickOrderCode = consoCode | |||
| this.status = StockOutStatus.PENDING.status | |||
| this.handler = SecurityUtils.getUser().getOrNull()?.id | |||
| } | |||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||
| // ✅ Pre-create stock out lines for suggested lots | |||
| saveSuggestedPickLots.forEach { lot -> | |||
| val polId = lot.pickOrderLine?.id | |||
| val illId = lot.suggestedLotLine?.id | |||
| if (polId != null && illId != null) { | |||
| val existingLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId) | |||
| if (existingLines.isEmpty()) { | |||
| val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null) | |||
| val inventoryLotLine = inventoryLotLineRepository.findById(illId).orElse(null) | |||
| if (pickOrderLine != null && inventoryLotLine != null) { | |||
| val line = StockOutLine().apply { | |||
| this.stockOut = savedStockOut | |||
| this.pickOrderLine = pickOrderLine | |||
| this.inventoryLotLine = inventoryLotLine | |||
| this.item = pickOrderLine.item | |||
| this.status = StockOutLineStatus.PENDING.status | |||
| this.qty = 0.0 | |||
| } | |||
| stockOutLineRepository.save(line) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| val itemIds = pols.mapNotNull { it.itemId } | |||
| if (itemIds.isNotEmpty()) { | |||
| joPickOrderService.createJoPickOrdersForPickOrder(createdPickOrder.id!!, itemIds) | |||
| joPickOrderService.createJoPickOrderRecordsForPickOrder(createdPickOrder.id!!, itemIds) | |||
| } | |||
| return MessageResponse( | |||
| id = jo.id, | |||
| @@ -18,13 +18,14 @@ import org.springframework.web.bind.annotation.PostMapping | |||
| 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 | |||
| @RestController | |||
| @RequestMapping("/jo") | |||
| class JobOrderController( | |||
| private val jobOrderService: JobOrderService, | |||
| private val jobOrderBomMaterialService: JobOrderBomMaterialService, | |||
| private val jobOrderProcessService: JobOrderProcessService | |||
| private val jobOrderProcessService: JobOrderProcessService, | |||
| private val joPickOrderService: JoPickOrderService | |||
| ) { | |||
| @GetMapping("/getRecordByPage") | |||
| @@ -58,4 +59,18 @@ class JobOrderController( | |||
| return jo | |||
| } | |||
| @GetMapping("/all-lots-hierarchical/{userId}") | |||
| fun getAllJobOrderLotsHierarchical(@PathVariable userId: Long): Map<String, Any?> { | |||
| return joPickOrderService.getAllJobOrderLotsWithDetailsHierarchical(userId) | |||
| } | |||
| @GetMapping("/completed-job-order-pick-orders/{userId}") | |||
| fun getCompletedJobOrderPickOrders(@PathVariable userId: Long): List<Map<String, Any?>> { | |||
| return joPickOrderService.getCompletedJobOrderPickOrders(userId) | |||
| } | |||
| @GetMapping("/completed-job-order-pick-order-records/{userId}") | |||
| fun getCompletedJobOrderPickOrderRecords(@PathVariable userId: Long): List<Map<String, Any?>> { | |||
| return joPickOrderService.getCompletedJobOrderPickOrderRecords(userId) | |||
| } | |||
| } | |||
| @@ -2777,7 +2777,122 @@ if (existingRecords.isNotEmpty()) { | |||
| println("❌ Pick order is not of type 'do': ${pickOrder.type?.value}") | |||
| return emptyList() | |||
| } | |||
| val allowedstatuses= listOf("assigned", "released", "picking", "completed") | |||
| val allowedstatuses= listOf("assigned", "released", "picking" | |||
| //, "completed" | |||
| ) | |||
| if (pickOrder.status?.value !in allowedstatuses) { | |||
| println("❌ Pick order status is not in allowed states: ${pickOrder.status?.value}") | |||
| return emptyList() | |||
| } | |||
| val deliveryOrder = pickOrder.deliveryOrder | |||
| val shop = deliveryOrder?.shop | |||
| val supplier = deliveryOrder?.supplier | |||
| println(" Delivery order: ${deliveryOrder?.code}, Shop: ${shop?.name}, Supplier: ${supplier?.code}") | |||
| println("🔍 Shop ID: ${shop?.id}") | |||
| // ✅ Get truck information using repository with detailed debugging | |||
| val truck = if (shop?.id != null) { | |||
| try { | |||
| println("🔍 Querying truck repository for shopId: ${shop.id}") | |||
| // Get all trucks for this shop | |||
| val trucksForShop = truckRepository.findByShopIdAndDeletedFalse(shop.id) | |||
| println("🔍 Trucks for shop ${shop.id}: ${trucksForShop.size}") | |||
| trucksForShop.forEach { t -> | |||
| println(" - Truck ID: ${t.id}, TruckNo: ${t.truckNo}, ShopId: ${t.shop?.id}, Deleted: ${t.deleted}") | |||
| } | |||
| // Take the first truck (or null if none found) | |||
| val selectedTruck = trucksForShop.firstOrNull() | |||
| if (selectedTruck != null) { | |||
| println("✅ Selected truck: ID=${selectedTruck.id}, TruckNo=${selectedTruck.truckNo}, DepartureTime=${selectedTruck.departureTime}") | |||
| } else { | |||
| println("❌ No truck found for shopId ${shop.id}") | |||
| } | |||
| selectedTruck | |||
| } catch (e: Exception) { | |||
| println("⚠️ Error querying truck repository for shop ${shop.id}: ${e.message}") | |||
| e.printStackTrace() | |||
| null | |||
| } | |||
| } else { | |||
| println("⚠️ Shop ID is null") | |||
| null | |||
| } | |||
| val ticketNo = try { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| val ticketNo = doPickOrders.firstOrNull()?.ticketNo ?: "" | |||
| println("🔍 Found ticket number: $ticketNo for pick order $pickOrderId") | |||
| ticketNo | |||
| } catch (e: Exception) { | |||
| println("⚠️ Error getting ticket number for pick order $pickOrderId: ${e.message}") | |||
| "" | |||
| } | |||
| val dpoStoreId = try { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| doPickOrders.firstOrNull()?.storeId ?: "" | |||
| } catch (e: Exception) { | |||
| println("⚠️ Error getting storeId for pick order $pickOrderId: ${e.message}") | |||
| "" | |||
| } | |||
| val result = mapOf( | |||
| "pickOrderId" to (pickOrder.id ?: 0L), | |||
| "pickOrderCode" to (pickOrder.code ?: ""), | |||
| "pickOrderConsoCode" to (pickOrder.consoCode ?: ""), | |||
| "pickOrderTargetDate" to (pickOrder.targetDate?.toString() ?: ""), | |||
| "pickOrderStatus" to (pickOrder.status?.value ?: ""), | |||
| "deliveryOrderId" to (deliveryOrder?.id ?: 0L), | |||
| "deliveryNo" to (deliveryOrder?.code ?: ""), | |||
| "deliveryDate" to (deliveryOrder?.orderDate?.toString() ?: ""), | |||
| "shopId" to (shop?.id ?: 0L), | |||
| "shopCode" to (shop?.code ?: ""), | |||
| "shopName" to (shop?.name ?: ""), | |||
| "shopAddress" to buildShopAddress(shop), | |||
| "shopPoNo" to (supplier?.code ?: ""), | |||
| "numberOfCartons" to (pickOrder.pickOrderLines.size), | |||
| "truckNo" to (truck?.truckNo ?: ""), // ✅ Use entity property | |||
| "DepartureTime" to (truck?.departureTime?.toString() ?: ""), | |||
| "ticketNo" to ticketNo, | |||
| "storeId" to dpoStoreId, | |||
| "qrCodeData" to (pickOrder.id ?: 0L) | |||
| ) | |||
| println("✅ FG Pick Orders by ID result count: 1") | |||
| return listOf(result) | |||
| } catch (e: Exception) { | |||
| println("❌ Error in getFgPickOrdersByPickOrderId: ${e.message}") | |||
| e.printStackTrace() | |||
| return emptyList() | |||
| } | |||
| } | |||
| open fun getnewFgPickOrdersByPickOrderId(pickOrderId: Long): List<Map<String, Any?>> { | |||
| try { | |||
| println("🔍 Starting getnewFgPickOrdersByPickOrderId method with pickOrderId: $pickOrderId") | |||
| val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) | |||
| if (pickOrder == null) { | |||
| println("❌ Pick order not found with ID: $pickOrderId") | |||
| return emptyList() | |||
| } | |||
| if (doPickOrderRepository.findByPickOrderId(pickOrderId).firstOrNull()?.hide == true) { | |||
| println("🔍 Pick order $pickOrderId is hidden, returning empty list") | |||
| return emptyList() | |||
| } | |||
| println("🔍 Found pick order: ${pickOrder.code}, type: ${pickOrder.type?.value}, status: ${pickOrder.status?.value}") | |||
| if (pickOrder.type?.value != "do") { | |||
| println("❌ Pick order is not of type 'do': ${pickOrder.type?.value}") | |||
| return emptyList() | |||
| } | |||
| val allowedstatuses= listOf("assigned", "released", "picking","completed" | |||
| //, "completed" | |||
| ) | |||
| if (pickOrder.status?.value !in allowedstatuses) { | |||
| println("❌ Pick order status is not in allowed states: ${pickOrder.status?.value}") | |||
| return emptyList() | |||
| @@ -3169,7 +3284,9 @@ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, A | |||
| println("❌ User not found: $userId") | |||
| return emptyMap() | |||
| } | |||
| val statusList = listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED, PickOrderStatus.COMPLETED) | |||
| val statusList = listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED, | |||
| //PickOrderStatus.COMPLETED | |||
| ) | |||
| // Get all pick orders assigned to user with PENDING or RELEASED status that have doId | |||
| val allAssignedPickOrders = pickOrderRepository.findAllByAssignToAndStatusIn( | |||
| @@ -3749,7 +3866,7 @@ open fun getCompletedDoPickOrders( | |||
| for (pickOrder in completedPickOrders) { | |||
| // 获取该 pick order 的 FG pick orders | |||
| val fgPickOrders = getFgPickOrdersByPickOrderId(pickOrder.id!!) | |||
| val fgPickOrders = getnewFgPickOrdersByPickOrderId(pickOrder.id!!) | |||
| if (fgPickOrders.isNotEmpty()) { | |||
| val firstFgOrder = fgPickOrders[0] as Map<String, Any?> // ✅ 修复:转换为 Map | |||
| @@ -3801,4 +3918,168 @@ open fun getCompletedDoPickOrders( | |||
| emptyList() | |||
| } | |||
| } | |||
| // ... existing code ... | |||
| open fun getLotDetailsByPickOrderId(pickOrderId: Long): List<Map<String, Any>> { | |||
| println("=== Debug: getLotDetailsByPickOrderId ===") | |||
| println("pickOrderId: $pickOrderId") | |||
| // Get the pick order | |||
| val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) | |||
| if (pickOrder == null) { | |||
| println("❌ Pick order not found: $pickOrderId") | |||
| return emptyList() | |||
| } | |||
| // ✅ 修复:使用正确的方法名 | |||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId) | |||
| if (pickOrderLines.isEmpty()) { | |||
| println("❌ No pick order lines found for pick order: $pickOrderId") | |||
| return emptyList() | |||
| } | |||
| // Build the SQL query similar to getAllPickOrderLotsWithDetailsWithoutAutoAssign | |||
| // but filtered by specific pickOrderId | |||
| 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.type as pickOrderType, | |||
| po.status as pickOrderStatus, | |||
| po.assignTo as pickOrderAssignTo, | |||
| -- 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 | |||
| r.id as routerId, | |||
| r.index as routerIndex, | |||
| r.route as routerRoute, | |||
| -- 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, | |||
| COALESCE(spl.suggestedLotLineId, ill.id) as debugSuggestedLotLineId, | |||
| ill.inventoryLotId as debugInventoryLotId, | |||
| -- 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') | |||
| ), 0) as totalPickedByAllPickOrders, | |||
| -- Calculate remaining available quantity correctly | |||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as remainingAfterAllPickOrders, | |||
| -- Add detailed debug fields for lotAvailability calculation | |||
| ill.status as debug_ill_status, | |||
| (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) as debug_is_expired, | |||
| sol.status as debug_sol_status, | |||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as debug_remaining_stock, | |||
| (COALESCE(spl.qty, 0) - COALESCE(sol.qty, 0)) as debug_required_after_picked, | |||
| -- Lot availability status | |||
| CASE | |||
| -- Check if lot is expired | |||
| WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' | |||
| -- Check if lot has rejected stock out line for this pick order | |||
| WHEN sol.status = 'rejected' THEN 'rejected' | |||
| -- Check if lot is unavailable due to insufficient stock | |||
| WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock' | |||
| -- Check if lot status is unavailable | |||
| WHEN ill.status = 'unavailable' THEN 'status_unavailable' | |||
| -- Default to available | |||
| ELSE 'available' | |||
| END as lotAvailability, | |||
| -- Processing status | |||
| CASE | |||
| WHEN sol.status = 'completed' THEN 'completed' | |||
| WHEN sol.status = 'rejected' THEN 'rejected' | |||
| WHEN sol.status = 'created' THEN 'pending' | |||
| ELSE 'pending' | |||
| END as processingStatus | |||
| FROM fpsmsdb.pick_order po | |||
| 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.router r ON r.inventoryLotId = ill.id AND r.deleted = false | |||
| LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||
| LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||
| LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ill.id AND sol.deleted = false | |||
| WHERE po.deleted = false | |||
| AND po.id = :pickOrderId | |||
| AND pol.deleted = false | |||
| AND ill.deleted = false | |||
| AND 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, -- Show rejected lots first | |||
| COALESCE(r.index, 0) ASC, | |||
| po.code ASC, | |||
| i.code ASC, | |||
| il.expiryDate ASC, | |||
| il.lotNo ASC | |||
| """.trimIndent() | |||
| // ✅ 使用 jdbcDao 而不是 entityManager | |||
| println("🔍 Executing SQL for lot details by pick order: $sql") | |||
| println("🔍 With parameters: pickOrderId = $pickOrderId") | |||
| val results = jdbcDao.queryForList(sql, mapOf("pickOrderId" to pickOrderId)) | |||
| println("✅ Total result count: ${results.size}") | |||
| return results | |||
| } | |||
| } | |||
| @@ -304,4 +304,8 @@ fun getCompletedDoPickOrders( | |||
| ): List<Map<String, Any?>> { | |||
| return pickOrderService.getCompletedDoPickOrders(userId, pickOrderCode, shopName, deliveryNo, ticketNo) | |||
| } | |||
| @GetMapping("/lot-details-by-pick-order/{pickOrderId}") | |||
| fun getLotDetailsByPickOrderId(@PathVariable pickOrderId: Long): List<Map<String, Any>> { | |||
| return pickOrderService.getLotDetailsByPickOrderId(pickOrderId); | |||
| } | |||
| } | |||
| @@ -1,5 +1,5 @@ | |||
| spring: | |||
| datasource: | |||
| jdbc-url: jdbc:mysql://127.0.0.1:3306/fpsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 | |||
| jdbc-url: jdbc:mysql://127.0.0.1:3308/fpsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 | |||
| username: root | |||
| password: secret | |||
| @@ -0,0 +1,45 @@ | |||
| --liquibase formatted sql | |||
| --changeset enson:update | |||
| CREATE TABLE jo_pick_order ( | |||
| id int AUTO_INCREMENT PRIMARY KEY, | |||
| pick_order_id int NOT NULL, | |||
| item_id int NOT NULL, | |||
| handled_by int, | |||
| second_qr_scan_status enum('pending', 'scanned', 'completed') DEFAULT 'pending', | |||
| second_qr_scan_by int NULL, | |||
| second_qr_scan_qty int NULL, | |||
| created datetime DEFAULT CURRENT_TIMESTAMP, | |||
| createdBy varchar(30), | |||
| version int DEFAULT 1, | |||
| modified datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |||
| modifiedBy varchar(30), | |||
| deleted tinyint(1) DEFAULT 0, | |||
| FOREIGN KEY (pick_order_id) REFERENCES pick_order(id), | |||
| FOREIGN KEY (item_id) REFERENCES items(id), | |||
| FOREIGN KEY (handled_by) REFERENCES user(id), | |||
| FOREIGN KEY (second_qr_scan_by) REFERENCES user(id) | |||
| ); | |||
| CREATE TABLE jo_pick_order_record ( | |||
| id int AUTO_INCREMENT PRIMARY KEY, | |||
| pick_order_id int NOT NULL, | |||
| item_id int NOT NULL, | |||
| handled_by int, | |||
| second_qr_scan_status enum('pending', 'scanned', 'completed') DEFAULT 'pending', | |||
| second_qr_scan_by int NULL, | |||
| second_qr_scan_qty int NULL, | |||
| created datetime DEFAULT CURRENT_TIMESTAMP, | |||
| createdBy varchar(30), | |||
| version int DEFAULT 1, | |||
| modified datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |||
| modifiedBy varchar(30), | |||
| deleted tinyint(1) DEFAULT 0, | |||
| FOREIGN KEY (pick_order_id) REFERENCES pick_order(id), | |||
| FOREIGN KEY (item_id) REFERENCES items(id), | |||
| FOREIGN KEY (handled_by) REFERENCES user(id), | |||
| FOREIGN KEY (second_qr_scan_by) REFERENCES user(id) | |||
| ); | |||