| @@ -13,7 +13,7 @@ import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.transaction.annotation.Transactional | |||
| import java.time.LocalDate | |||
| import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | |||
| import java.time.LocalDateTime | |||
| import com.ffii.core.support.JdbcDao | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.SecondScanSubmitRequest | |||
| @@ -24,7 +24,11 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository | |||
| import java.math.BigDecimal | |||
| import com.ffii.fpsms.modules.pickOrder.entity.HandleStatus | |||
| import com.ffii.fpsms.modules.pickOrder.entity.IssueCategory | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository | |||
| import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository | |||
| @Service | |||
| open class JoPickOrderService( | |||
| private val joPickOrderRepository: JoPickOrderRepository, | |||
| @@ -33,7 +37,12 @@ open class JoPickOrderService( | |||
| private val userService: UserService, | |||
| private val pickOrderRepository: PickOrderRepository, | |||
| private val jdbcDao: JdbcDao, | |||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository | |||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository, | |||
| private val pickOrderLineRepository: PickOrderLineRepository, | |||
| private val suggestPickLotRepository: SuggestPickLotRepository, | |||
| private val inventoryLotLineRepository: InventoryLotLineRepository, | |||
| private val inventoryLotRepository: InventoryLotRepository, | |||
| private val stockOutLineRepository: StockOutLIneRepository | |||
| ) { | |||
| open fun save(record: JoPickOrder): JoPickOrder { | |||
| @@ -209,10 +218,8 @@ open class JoPickOrderService( | |||
| println("🔍 DEBUG: Found ${allAssignedPickOrders.size} job order pick orders assigned to user $userId") | |||
| val visiblePickOrders = allAssignedPickOrders | |||
| // Filter based on assignment and status | |||
| val filteredPickOrders = if (visiblePickOrders.isNotEmpty()) { | |||
| val filteredPickOrders = if (allAssignedPickOrders.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 | |||
| @@ -248,223 +255,201 @@ open class JoPickOrderService( | |||
| ) | |||
| } | |||
| // 修复:简化 SQL 查询,移除不存在的字段 | |||
| 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, | |||
| -- 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, | |||
| -- Simplified Router Information using warehouse | |||
| 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, | |||
| // 使用 Repository 获取数据 | |||
| try { | |||
| val pickOrders = pickOrderRepository.findAllByIdIn(pickOrderIds) | |||
| .filter { it.deleted == false && it.assignTo?.id == userId } | |||
| -- Stock out line information | |||
| sol.id as stockOutLineId, | |||
| sol.status as stockOutLineStatus, | |||
| COALESCE(sol.qty, 0) as stockOutLineQty, | |||
| if (pickOrders.isEmpty()) { | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| ) | |||
| } | |||
| -- 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, | |||
| val pickOrder = pickOrders.first() // 取第一个(应该只有一个) | |||
| val jobOrder = pickOrder.jobOrder ?: return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| ) | |||
| -- 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, | |||
| // 获取 pick order lines | |||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!) | |||
| .filter { it.deleted == false } | |||
| -- Calculate remaining available quantity correctly | |||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as remainingAfterAllPickOrders, | |||
| // 获取所有 pick order line IDs | |||
| val pickOrderLineIds = pickOrderLines.map { it.id!! } | |||
| -- 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, | |||
| // 获取 suggested pick lots | |||
| val suggestedPickLots = if (pickOrderLineIds.isNotEmpty()) { | |||
| suggestPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||
| .filter { it.deleted == false } | |||
| } else { | |||
| emptyList() | |||
| } | |||
| -- 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, | |||
| // 获取所有 inventory lot line IDs | |||
| val inventoryLotLineIds = suggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | |||
| -- JoPickOrder second scan status | |||
| jpo.match_status as match_status, | |||
| jpo.match_by as match_by, | |||
| jpo.match_qty as match_qty, | |||
| jpo.ticketReleaseTime as ticketReleaseTime, | |||
| jpo.ticketCompleteTime as ticketCompleteTime | |||
| // 获取 inventory lot lines | |||
| val inventoryLotLines = if (inventoryLotLineIds.isNotEmpty()) { | |||
| inventoryLotLineRepository.findAllByIdIn(inventoryLotLineIds) | |||
| .filter { it.deleted == false } | |||
| } else { | |||
| emptyList() | |||
| } | |||
| 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 -- 直接关联 warehouse | |||
| 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(w.`order`, 999999) ASC, -- 使用 warehouse.order 排序 | |||
| 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") | |||
| // 修复:直接执行 SQL 查询并构建分层结构 | |||
| try { | |||
| val results = jdbcDao.queryForList(sql, mapOf("userId" to userId)) | |||
| // 获取 inventory lots | |||
| val inventoryLotIds = inventoryLotLines.mapNotNull { it.inventoryLot?.id }.distinct() | |||
| val inventoryLots = if (inventoryLotIds.isNotEmpty()) { | |||
| inventoryLotRepository.findAllByIdIn(inventoryLotIds) | |||
| .filter { it.deleted == false } | |||
| } else { | |||
| emptyList() | |||
| } | |||
| if (results.isEmpty()) { | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| ) | |||
| // 获取 stock out lines | |||
| val stockOutLines = if (pickOrderLineIds.isNotEmpty() && inventoryLotLineIds.isNotEmpty()) { | |||
| pickOrderLineIds.flatMap { polId -> | |||
| inventoryLotLineIds.flatMap { illId -> | |||
| stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId) | |||
| } | |||
| } | |||
| } else { | |||
| emptyList() | |||
| } | |||
| // 取得所有 stock out line(含無 lot 情況) | |||
| val stockOutLinesByPickOrderLine = pickOrderLineIds.associateWith { polId -> | |||
| stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(polId) | |||
| } | |||
| // 获取 jo_pick_order 记录 | |||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!) | |||
| // 构建分层结构 | |||
| val pickOrderInfo = results.firstOrNull()?.let { firstRow: Map<String, Any?> -> | |||
| mapOf( | |||
| "id" to firstRow["pickOrderId"], | |||
| "code" to firstRow["pickOrderCode"], | |||
| "consoCode" to firstRow["pickOrderConsoCode"], | |||
| "targetDate" to firstRow["pickOrderTargetDate"], | |||
| "type" to firstRow["pickOrderType"], | |||
| "status" to firstRow["pickOrderStatus"], | |||
| "assignTo" to firstRow["pickOrderAssignTo"], | |||
| "jobOrder" to mapOf( | |||
| "id" to firstRow["jobOrderId"], | |||
| "code" to firstRow["jobOrderCode"], | |||
| "name" to "Job Order ${firstRow["jobOrderCode"]}" // 使用生成的名称 | |||
| ) | |||
| // 构建 pick order info | |||
| val pickOrderInfo = mapOf( | |||
| "id" to pickOrder.id, | |||
| "code" to pickOrder.code, | |||
| "consoCode" to pickOrder.consoCode, | |||
| "targetDate" to pickOrder.targetDate?.let { | |||
| "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}" | |||
| }, | |||
| "type" to pickOrder.type?.value, | |||
| "status" to pickOrder.status?.value, | |||
| "assignTo" to pickOrder.assignTo?.id, | |||
| "jobOrder" to mapOf( | |||
| "id" to jobOrder.id, | |||
| "code" to jobOrder.code, | |||
| "name" to "Job Order ${jobOrder.code}" | |||
| ) | |||
| } | |||
| ) | |||
| // 按 pick order line 分组 | |||
| val groupedByLine = results.groupBy { row: Map<String, Any?> -> row["pickOrderLineId"] } | |||
| val pickOrderLines = groupedByLine.map { (lineId: Any?, lineRows: List<Map<String, Any?>>) -> | |||
| val firstLineRow = lineRows.first() | |||
| mapOf( | |||
| "id" to lineId, | |||
| "itemId" to firstLineRow["itemId"], | |||
| "itemCode" to firstLineRow["itemCode"], | |||
| "itemName" to firstLineRow["itemName"], | |||
| "requiredQty" to firstLineRow["pickOrderLineRequiredQty"], | |||
| "uomCode" to firstLineRow["uomCode"], | |||
| "uomDesc" to firstLineRow["uomDesc"], | |||
| "lots" to lineRows.map { row: Map<String, Any?> -> | |||
| mapOf( | |||
| "lotId" to row["lotId"], | |||
| "lotNo" to row["lotNo"], | |||
| "expiryDate" to row["expiryDate"], | |||
| "location" to row["location"], | |||
| "availableQty" to row["availableQty"], | |||
| "requiredQty" to row["requiredQty"], | |||
| "actualPickQty" to row["actualPickQty"], | |||
| "processingStatus" to row["processingStatus"], | |||
| "lotAvailability" to row["lotAvailability"], | |||
| "pickOrderId" to row["pickOrderId"], | |||
| "pickOrderCode" to row["pickOrderCode"], | |||
| "pickOrderConsoCode" to row["pickOrderConsoCode"], | |||
| "pickOrderLineId" to row["pickOrderLineId"], | |||
| "stockOutLineId" to row["stockOutLineId"], | |||
| "suggestedPickLotId" to row["suggestedPickLotId"], | |||
| "stockOutLineQty" to row["stockOutLineQty"], | |||
| "stockOutLineStatus" to row["stockOutLineStatus"], | |||
| "routerIndex" to row["routerIndex"], | |||
| "routerArea" to row["routerArea"], | |||
| "routerRoute" to row["routerRoute"], | |||
| "uomShortDesc" to row["uomShortDesc"], | |||
| "matchStatus" to row["match_status"], | |||
| "matchBy" to row["match_by"], | |||
| "matchQty" to row["match_qty"] | |||
| ) | |||
| // 构建 pick order lines with lots | |||
| val pickOrderLinesResult = pickOrderLines.map { pol -> | |||
| val item = pol.item | |||
| val uom = pol.uom | |||
| val lineId = pol.id!! | |||
| val suggestions = suggestedPickLots.filter { it.pickOrderLine?.id == lineId } | |||
| val stockoutsForLine = stockOutLinesByPickOrderLine[lineId].orEmpty() | |||
| // 获取该 line 的 suggested pick lots | |||
| val lineSuggestedLots = suggestedPickLots.filter { it.pickOrderLine?.id == pol.id } | |||
| // 构建 lots 数据 | |||
| val lots = lineSuggestedLots.mapNotNull { spl -> | |||
| val ill = spl.suggestedLotLine | |||
| if (ill == null || ill.deleted == true) return@mapNotNull null | |||
| val il = ill.inventoryLot | |||
| if (il == null || il.deleted == true) return@mapNotNull null | |||
| val warehouse = ill.warehouse | |||
| // 获取对应的 stock out line | |||
| val sol = stockOutLines.firstOrNull { | |||
| it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id | |||
| } | |||
| // 获取对应的 jo_pick_order | |||
| val jpo = joPickOrders.firstOrNull { it.itemId == item?.id } | |||
| // 计算 available quantity | |||
| val availableQty = if (sol?.status == "rejected") { | |||
| null | |||
| } else { | |||
| (ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty ?: BigDecimal.ZERO) | |||
| } | |||
| // 计算 total picked by all pick orders | |||
| val totalPickedByAllPickOrders = stockOutLines | |||
| .filter { it.inventoryLotLine?.id == ill.id && it.deleted == false } | |||
| .filter { it.status in listOf("pending", "checked", "partially_completed", "completed") } | |||
| .sumOf { it.qty?.toBigDecimal() ?: BigDecimal.ZERO } | |||
| // 计算 lot availability | |||
| val lotAvailability = when { | |||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | |||
| sol?.status == "rejected" -> "rejected" | |||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||
| ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | |||
| else -> "available" | |||
| } | |||
| // 计算 processing status | |||
| val processingStatus = when (sol?.status) { | |||
| "completed" -> "completed" | |||
| "rejected" -> "rejected" | |||
| "created" -> "pending" | |||
| else -> "pending" | |||
| } | |||
| mapOf( | |||
| "lotId" to ill.id, | |||
| "lotNo" to il.lotNo, | |||
| "expiryDate" to il.expiryDate?.let { | |||
| "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}" | |||
| }, | |||
| "location" to warehouse?.name, | |||
| "availableQty" to availableQty?.toDouble(), | |||
| "requiredQty" to (spl.qty?.toDouble() ?: 0.0), | |||
| "actualPickQty" to (sol?.qty ?: 0.0), | |||
| "processingStatus" to processingStatus, | |||
| "lotAvailability" to lotAvailability, | |||
| "pickOrderId" to pickOrder.id, | |||
| "pickOrderCode" to pickOrder.code, | |||
| "pickOrderConsoCode" to pickOrder.consoCode, | |||
| "pickOrderLineId" to pol.id, | |||
| "stockOutLineId" to sol?.id, | |||
| "suggestedPickLotId" to spl.id, | |||
| "stockOutLineQty" to (sol?.qty ?: 0.0), | |||
| "stockOutLineStatus" to sol?.status, | |||
| "routerIndex" to warehouse?.order, | |||
| "routerArea" to warehouse?.code, | |||
| "routerRoute" to warehouse?.code, | |||
| "uomShortDesc" to uom?.udfShortDesc, | |||
| "matchStatus" to jpo?.matchStatus?.value, | |||
| "matchBy" to jpo?.matchBy, | |||
| "matchQty" to jpo?.matchQty | |||
| ) | |||
| } | |||
| mapOf( | |||
| "id" to pol.id, | |||
| "itemId" to item?.id, | |||
| "itemCode" to item?.code, | |||
| "itemName" to item?.name, | |||
| "requiredQty" to pol.qty?.toDouble(), | |||
| "uomCode" to uom?.code, | |||
| "uomDesc" to uom?.udfudesc, | |||
| "lots" to lots | |||
| ) | |||
| } | |||
| return mapOf( | |||
| "pickOrder" to pickOrderInfo as Any?, | |||
| "pickOrderLines" to pickOrderLines as Any? | |||
| "pickOrderLines" to pickOrderLinesResult as Any? | |||
| ) | |||
| } catch (e: Exception) { | |||
| println("❌ Error executing Job Order SQL: ${e.message}") | |||
| println("❌ Error executing Job Order hierarchical query: ${e.message}") | |||
| e.printStackTrace() | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| @@ -19,6 +19,7 @@ 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.productProcess.service.ProductProcessService | |||
| 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.PrintPickRecordRequest | |||
| @@ -40,7 +41,8 @@ class JobOrderController( | |||
| private val jobOrderService: JobOrderService, | |||
| private val jobOrderBomMaterialService: JobOrderBomMaterialService, | |||
| private val jobOrderProcessService: JobOrderProcessService, | |||
| private val joPickOrderService: JoPickOrderService | |||
| private val joPickOrderService: JoPickOrderService, | |||
| private val productProcessService: ProductProcessService | |||
| ) { | |||
| @GetMapping("/getRecordByPage") | |||
| @@ -87,7 +89,7 @@ class JobOrderController( | |||
| } | |||
| jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(jo.id) | |||
| jobOrderProcessService.createJobOrderProcessesByJoId(jo.id) | |||
| productProcessService.createProductProcessByJobOrderId(jo.id) | |||
| return jo | |||
| } | |||
| @GetMapping("/all-lots-hierarchical/{userId}") | |||
| @@ -39,4 +39,18 @@ interface ProductionScheduleLineRepository : AbstractRepository<ProductionSchedu | |||
| group by psl.id, bm.id, i.id, pp.proportion | |||
| """) | |||
| fun getBomMaterials(id: Long): List<DetailedProdScheduleLineBomMaterialInterface>? | |||
| @Query(""" | |||
| SELECT psl FROM ProductionScheduleLine psl | |||
| JOIN psl.productionSchedule ps | |||
| WHERE psl.item.id = :itemId | |||
| AND ps.produceAt = :produceAt | |||
| AND psl.type = :type | |||
| AND ps.deleted = false | |||
| ORDER BY psl.itemPriority ASC | |||
| """) | |||
| fun findByItemIdAndProduceAtAndType( | |||
| itemId: Long, | |||
| produceAt: LocalDateTime, | |||
| type: String | |||
| ): ProductionScheduleLine? | |||
| } | |||
| @@ -24,16 +24,24 @@ data class ProductProcessInfo( | |||
| val isDark: String?, | |||
| val isDense: Int?, | |||
| val isFloat: String?, | |||
| val itemId: Long?, | |||
| val itemCode: String?, | |||
| val itemName: String?, | |||
| val outputQtyUom: String?, | |||
| val outputQty: Int?, | |||
| val productProcessLines: List<ProductProcessLineInfo>? | |||
| val productionPriority: String?, | |||
| val productProcessLines: List<ProductProcessLineInfo>?, | |||
| val totalStockQty: Int?, | |||
| val insufficientStockQty: Int?, | |||
| val sufficientStockQty: Int?, | |||
| val jobOrderLines: List<jobOrderLineInfo>? | |||
| ) | |||
| data class ProductProcessLineInfo( | |||
| val id:Long?, | |||
| val bomprocessId: Long?, | |||
| val operatorId: Long?, | |||
| val operatorName: String?, | |||
| val equipmentId: Long?, | |||
| val handlerId: Long?, | |||
| val seqNo: Long?, | |||
| @@ -50,8 +58,22 @@ data class ProductProcessLineInfo( | |||
| val defectUom: String?, | |||
| val outputFromProcessQty: Int?, | |||
| val outputFromProcessUom: String?, | |||
| val durationInMinutes: Int?, | |||
| val prepTimeInMinutes: Int?, | |||
| val postProdTimeInMinutes: Int?, | |||
| @JsonFormat(pattern = "yyyy-MM-dd") | |||
| val startTime: LocalDateTime?, | |||
| @JsonFormat(pattern = "yyyy-MM-dd") | |||
| val endTime: LocalDateTime? | |||
| ) | |||
| data class jobOrderLineInfo( | |||
| val id: Long?, | |||
| val itemId: Long?, | |||
| val itemCode: String?, | |||
| val itemName: String?, | |||
| val reqQty: Int?, | |||
| val stockQty: Int?, | |||
| val uom: String?, | |||
| val shortUom: String?, | |||
| val availableStatus: String? | |||
| ) | |||
| @@ -7,6 +7,7 @@ import com.ffii.fpsms.modules.productProcess.enums.ProductProcessStatus | |||
| import com.ffii.fpsms.modules.productProcess.web.model.* | |||
| import org.springframework.data.domain.Page | |||
| import org.springframework.data.domain.Pageable | |||
| import com.ffii.fpsms.modules.productProcess.entity.projections.jobOrderLineInfo | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.transaction.annotation.Transactional | |||
| import java.time.LocalDate | |||
| @@ -27,6 +28,14 @@ import com.ffii.fpsms.modules.master.entity.Equipment | |||
| import com.ffii.fpsms.modules.master.entity.BomProcess | |||
| import com.ffii.fpsms.modules.master.entity.Bom | |||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderBomMaterialRepository | |||
| import com.ffii.fpsms.modules.master.service.ProductionScheduleService | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | |||
| import com.ffii.fpsms.modules.master.entity.ProductionScheduleLineRepository | |||
| import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | |||
| import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus | |||
| import com.ffii.fpsms.modules.stock.service.StockInLineService | |||
| import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest | |||
| @Service | |||
| @Transactional | |||
| open class ProductProcessService( | |||
| @@ -40,6 +49,10 @@ open class ProductProcessService( | |||
| private val userRepository: UserRepository, | |||
| private val processRepository: ProcessRepository, | |||
| private val equipmentRepository: EquipmentRepository, | |||
| private val jobOrderBomMaterialRepository: JobOrderBomMaterialRepository, | |||
| private val inventoryLotLineRepository: InventoryLotLineRepository, | |||
| private val productionScheduleLineRepository: ProductionScheduleLineRepository, | |||
| private val stockInLineService: StockInLineService, | |||
| ) { | |||
| open fun findAll(pageable: Pageable): Page<ProductProcess> { | |||
| @@ -457,6 +470,62 @@ open class ProductProcessService( | |||
| val productProcesses = productProcessRepository.findByJobOrder_Id(joid) | |||
| val jobOrder = jobOrderRepository.findById(joid).orElse(null) | |||
| val bom = bomRepository.findById(jobOrder?.bom?.id?:0L).orElse(null) | |||
| val bomProcess = bomProcessRepository.findByBomId(jobOrder?.bom?.id?:0L) | |||
| // get bom materials | |||
| val bomMaterials = jobOrderBomMaterialRepository.findAllByJobOrderId(jobOrder?.id?:0) | |||
| val itemIds = bomMaterials.mapNotNull { it.item?.id } | |||
| // calculate each item's available stock | |||
| val stockQtyMap = bomMaterials.mapNotNull { material -> | |||
| val itemId = material.item?.id ?: return@mapNotNull null | |||
| val availableLots = inventoryLotLineRepository | |||
| .findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) | |||
| val stockQty = availableLots.sumOf { lot -> | |||
| (lot.inQty ?: BigDecimal.ZERO) | |||
| .minus(lot.outQty ?: BigDecimal.ZERO) | |||
| .minus(lot.holdQty ?: BigDecimal.ZERO) | |||
| } | |||
| itemId to stockQty | |||
| }.toMap() | |||
| // calculate statistics | |||
| val totalStockQty = stockQtyMap.values.sumOf { it.toInt() } | |||
| val insufficientStockQty = bomMaterials | |||
| .filter { material -> | |||
| val itemId = material.item?.id ?: 0L | |||
| val stockQty = stockQtyMap[itemId] ?: BigDecimal.ZERO | |||
| stockQty < (material.reqQty ?: BigDecimal.ZERO) | |||
| } | |||
| .sumOf { material -> | |||
| val itemId = material.item?.id ?: 0L | |||
| stockQtyMap[itemId]?.toInt() ?: 0 | |||
| } | |||
| val sufficientStockQty = bomMaterials | |||
| .filter { material -> | |||
| val itemId = material.item?.id ?: 0L | |||
| val stockQty = stockQtyMap[itemId] ?: BigDecimal.ZERO | |||
| stockQty >= (material.reqQty ?: BigDecimal.ZERO) | |||
| } | |||
| .sumOf { material -> | |||
| val itemId = material.item?.id ?: 0L | |||
| stockQtyMap[itemId]?.toInt() ?: 0 | |||
| } | |||
| // 获取 productionPriority | |||
| val itemId = jobOrder?.bom?.item?.id | |||
| val planEndDate = jobOrder?.planEnd?.toLocalDate() | |||
| val productionPriority = if (itemId != null && planEndDate != null) { | |||
| val scheduleLine = productionScheduleLineRepository | |||
| .findByItemIdAndProduceAtAndType( | |||
| itemId, | |||
| planEndDate.atStartOfDay(), | |||
| "detailed" | |||
| ) | |||
| scheduleLine?.itemPriority?.toString() ?: "0" | |||
| } else { | |||
| "0" | |||
| } | |||
| fun calculateColourScore(value: Int?): String { | |||
| return when (value) { | |||
| 0 -> "淺" | |||
| @@ -471,13 +540,16 @@ open class ProductProcessService( | |||
| else -> "" | |||
| } | |||
| } | |||
| //val processId = productProcess.id ?: 0 | |||
| return productProcesses.map { process -> | |||
| ProductProcessInfo( | |||
| id = process.id?:0, | |||
| bomId = process.bom?.id?:0, | |||
| jobOrderId = process.jobOrder?.id?:0, | |||
| jobOrderCode = jobOrder?.code?:"", | |||
| itemId = bom?.item?.id?:0, | |||
| itemCode = bom?.item?.code?:"", | |||
| itemName = bom?.item?.name?:"", | |||
| isDark = calculateColourScore(bom?.isDark?:0), | |||
| isDense = bom?.isDense?:0, | |||
| isFloat = calculateFloatScore(bom?.isFloat?:0), | |||
| @@ -488,11 +560,16 @@ open class ProductProcessService( | |||
| startTime = process.startTime?:LocalDateTime.now(), | |||
| endTime = process.endTime?:LocalDateTime.now(), | |||
| date = process.date?:LocalDate.now(), | |||
| productionPriority = productionPriority, // 已经是 String,不需要 ?:0 | |||
| totalStockQty = totalStockQty, | |||
| insufficientStockQty = insufficientStockQty, | |||
| sufficientStockQty = sufficientStockQty, | |||
| productProcessLines = productProcessLineRepository.findByProductProcess_Id(process.id?:0).map { line -> | |||
| ProductProcessLineInfo( | |||
| id = line.id?:0, | |||
| bomprocessId = line.bomProcess?.id?:0, | |||
| operatorId = line.operator?.id?:0, | |||
| operatorName = line.operator?.name?:"", | |||
| equipmentId = line.equipment?.id?:0, | |||
| handlerId = line.handler?.id?:0, | |||
| seqNo = line.seqNo?:0, | |||
| @@ -500,6 +577,9 @@ open class ProductProcessService( | |||
| description = line.description?:"", | |||
| equipment_name = line.equipmentType?:"", | |||
| status = line.status?:"", | |||
| durationInMinutes = line.bomProcess?.durationInMinute?:0, | |||
| prepTimeInMinutes = line.bomProcess?.prepTimeInMinute?:0, | |||
| postProdTimeInMinutes = line.bomProcess?.postProdTimeInMinute?:0, | |||
| byproductId = line.byproduct?.id?:0, | |||
| byproductName = line.byproduct?.name?:"", | |||
| byproductQty = line.byproductQty?:0, | |||
| @@ -512,54 +592,36 @@ open class ProductProcessService( | |||
| startTime = line.startTime, | |||
| endTime = line.endTime | |||
| ) | |||
| }, | |||
| jobOrderLines = bomMaterials.map { line -> | |||
| val itemId = line.item?.id ?: 0L | |||
| val stockQty = stockQtyMap[itemId]?.toInt() ?: 0 | |||
| val uom = line.uom | |||
| val shortUom = uom?.udfShortDesc ?: uom?.udfudesc ?: "" | |||
| // 计算 availableStatus | |||
| val availableStatus = if (stockQty >= (line.reqQty?.toInt() ?: 0)) { | |||
| "available" | |||
| } else { | |||
| "insufficient" | |||
| } | |||
| jobOrderLineInfo( | |||
| id = line.id?:0, | |||
| itemId = itemId, | |||
| itemCode = line.item?.code?:"", | |||
| itemName = line.item?.name?:"", | |||
| reqQty = line.reqQty?.toInt() ?: 0, | |||
| stockQty = stockQty, | |||
| uom = uom?.udfudesc ?: "", | |||
| shortUom = shortUom, | |||
| availableStatus = availableStatus | |||
| ) | |||
| } | |||
| ) | |||
| } | |||
| } | |||
| /* | |||
| open fun productProcessDetailfindbyidtest(id: Long): ProductProcessInfo { | |||
| val zero = BigDecimal.ZERO | |||
| val productProcess = productProcessRepository.findById(id).orElse(null) | |||
| val processId = productProcess.id ?: 0 | |||
| val productProcessLines = productProcessLineRepository.findByProductProcess_Id(processId) | |||
| return ProductProcessInfo( | |||
| id = productProcess.id?:0, | |||
| bomId = productProcess.bom?.id?:0, | |||
| jobOrderId = productProcess.jobOrder?.id?:0, | |||
| productProcessCode = productProcess.productProcessCode?:"", | |||
| status = productProcess.status?:ProductProcessStatus.PENDING, | |||
| startTime = productProcess.startTime?:LocalDateTime.now(), | |||
| endTime = productProcess.endTime?:LocalDateTime.now(), | |||
| date = productProcess.date?:LocalDate.now(), | |||
| productProcessLines = productProcessLines.map { line -> | |||
| ProductProcessLineInfo( | |||
| id = line.id?:0, | |||
| bomprocessId = line.bomProcess?.id?:0, | |||
| operatorId = line.operator?.id?:0, | |||
| equipmentId = line.equipment?.id?:0, | |||
| handlerId = line.handler?.id?:0, | |||
| seqNo = line.seqNo?:0, | |||
| name = line.name?:"", | |||
| description = line.description?:"", | |||
| equipment_name = line.equipment?.name?:"", | |||
| status = line.status?:"", | |||
| byproductId = line.byproduct?.id?:0, | |||
| byproductName = line.byproduct?.name?:"", | |||
| byproductQty = line.byproductQty?:0, | |||
| byproductUom = line.byproductUom?:"", | |||
| scrapQty = line.scrapQty?:0, | |||
| defectQty = line.defectQty?:0, | |||
| defectUom = line.defectUom?:"", | |||
| outputFromProcessQty = line.outputFromProcessQty?:0, | |||
| outputFromProcessUom = line.outputFromProcessUom?:"", | |||
| startTime = line.startTime?:LocalDateTime.now(), | |||
| endTime = line.endTime?:LocalDateTime.now() | |||
| ) | |||
| } | |||
| ) | |||
| } | |||
| */ | |||
| open fun createProductProcessByJobOrderId(jobOrderId: Long): MessageResponse { | |||
| val jobOrder = jobOrderRepository.findById(jobOrderId).orElse(null) | |||
| val bom = bomRepository.findById(jobOrder?.bom?.id ?: 0L).orElse(null) | |||
| @@ -792,6 +854,7 @@ open fun updateProductProcessLineQty(request: UpdateProductProcessLineQtyRequest | |||
| productProcessLine.byproductUom = byproductUom | |||
| } | |||
| productProcessLineRepository.save(productProcessLine) | |||
| CompleteProductProcessLine(request.productProcessLineId) | |||
| return MessageResponse( | |||
| id = request.productProcessLineId, | |||
| code = "200", | |||
| @@ -806,11 +869,14 @@ open fun updateProductProcessLineQty(request: UpdateProductProcessLineQtyRequest | |||
| val productProcesses = productProcessRepository.findAllByDeletedIsFalse() | |||
| val productProcessIds = productProcesses.map { it.id } | |||
| return productProcesses.map { productProcesses -> | |||
| val productProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcesses.id?:0L) | |||
| val jobOrder = jobOrderRepository.findById(productProcesses.jobOrder?.id?:0L).orElse(null) | |||
| val FinishedProductProcessLineCount = productProcessLineRepository.findByProductProcess_Id(productProcesses.id?:0L).count { it.status == "Completed" } | |||
| val stockInLine = jobOrder?.stockInLines?.firstOrNull() | |||
| val stockInLineId = stockInLine?.id | |||
| //val silHandlerId = stockInLine?.escalationLog?.firstOrNull { it.status == "pending" }?.handler?.id | |||
| AllJoborderProductProcessInfoResponse( | |||
| id = productProcesses.id ?: 0L, | |||
| productProcessCode = productProcesses.productProcessCode, | |||
| @@ -821,6 +887,7 @@ open fun updateProductProcessLineQty(request: UpdateProductProcessLineQtyRequest | |||
| bomId = productProcesses.bom?.id, | |||
| itemName = productProcesses.item?.name, | |||
| jobOrderId = productProcesses.jobOrder?.id, | |||
| stockInLineId = stockInLineId, | |||
| jobOrderCode = jobOrder?.code, | |||
| productProcessLineCount = productProcessLines.size, | |||
| FinishedProductProcessLineCount = FinishedProductProcessLineCount, | |||
| @@ -936,10 +1003,30 @@ open fun updateProductProcessLineQty(request: UpdateProductProcessLineQtyRequest | |||
| updateProductProcessLineEndTime(productProcessLineId) | |||
| updateProductProcessLineStatus(productProcessLineId, "Completed") | |||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | |||
| val allproductProcessLines=productProcessLineRepository.findByProductProcess_Id(productProcessLine.productProcess?.id?:0L) | |||
| val productProcessId = productProcessLine?.productProcess?.id ?: 0L | |||
| val allproductProcessLines=productProcessLineRepository.findByProductProcess_Id(productProcessId) | |||
| if(allproductProcessLines.all { it.status == "Completed" }) { | |||
| updateProductProcessEndTime(productProcessLine.id) | |||
| updateProductProcessStatus(productProcessLine.id, ProductProcessStatus.COMPLETED) | |||
| updateProductProcessEndTime(productProcessId) | |||
| updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) | |||
| val productProcess = productProcessRepository.findById(productProcessId).orElse(null) | |||
| val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id?:0L).orElse(null) | |||
| if(jobOrder != null) { | |||
| jobOrder.status = JobOrderStatus.STORING | |||
| jobOrderRepository.save(jobOrder) | |||
| stockInLineService.create( | |||
| SaveStockInLineRequest( | |||
| itemId = productProcess?.item?.id?:0L, | |||
| acceptedQty = jobOrder?.reqQty?:BigDecimal.ZERO, | |||
| productLotNo = jobOrder?.code, | |||
| productionDate = LocalDate.now(), | |||
| jobOrderId = jobOrder.id, | |||
| acceptQty =jobOrder?.reqQty?:BigDecimal.ZERO , | |||
| expiryDate=null, | |||
| status="", | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| return MessageResponse( | |||
| id = productProcessLine.id, | |||
| @@ -147,7 +147,7 @@ data class AllJoborderProductProcessInfoResponse( | |||
| val jobOrderCode: String?, | |||
| val productProcessLineCount: Int, | |||
| val FinishedProductProcessLineCount: Int, | |||
| val stockInLineId: Long?, | |||
| val lines: List<ProductProcessInfoResponse> | |||
| ) | |||
| data class ProductProcessInfoResponse( | |||
| @@ -15,4 +15,5 @@ interface InventoryLotRepository: AbstractRepository<InventoryLot, Long> { | |||
| select il.lotNo from InventoryLot il where il.lotNo like :prefix% order by il.lotNo desc limit 1 | |||
| """) | |||
| fun findLatestLotNoByPrefix(prefix: String): String? | |||
| fun findAllByIdIn(ids: List<Long>): List<InventoryLot> | |||
| } | |||
| @@ -21,4 +21,5 @@ interface StockOutLIneRepository: AbstractRepository<StockOutLine, Long> { | |||
| inventoryLotLineId: Long | |||
| ): List<StockOutLine> | |||
| fun existsByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(pickOrderLineId: Long, inventoryLotLineId: Long): Boolean | |||
| //fun findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId: Long): List<StockOutLine> | |||
| } | |||