| @@ -12,5 +12,7 @@ import java.time.LocalDateTime | |||
| @Repository | |||
| interface JoBagConsumptionRepository : AbstractRepository<JoBagConsumption, Long> { | |||
| fun findAllByDeletedIsFalse(): List<JoBagConsumption> | |||
| fun findAllByBagLotLineIdAndDeletedIsFalse(bagLotLineId: Long): List<JoBagConsumption> | |||
| fun findAllByBagId(bagId: Long): List<BagLotLine> | |||
| } | |||
| @@ -14,13 +14,15 @@ import java.time.LocalTime | |||
| import java.time.LocalDateTime | |||
| import java.time.LocalDate | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository | |||
| import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository | |||
| @Service | |||
| open class BagService( | |||
| private val bagRepository: BagRepository, | |||
| private val bagLotLineRepository: BagLotLineRepository, | |||
| private val joBagConsumptionRepository: JoBagConsumptionRepository, | |||
| private val inventoryLotRepository: InventoryLotRepository, | |||
| private val jobOrderRepository: JobOrderRepository | |||
| private val jobOrderRepository: JobOrderRepository, | |||
| private val productProcessRepository: ProductProcessRepository | |||
| ) { | |||
| open fun createBagLotLinesByBagId(request: CreateBagLotLineRequest): MessageResponse { | |||
| val bag = bagRepository.findById(request.bagId).orElse(null) | |||
| @@ -124,6 +126,14 @@ fun createJoBagConsumption(request: CreateJoBagConsumptionRequest): MessageRespo | |||
| this.time = LocalDateTime.now() | |||
| } | |||
| joBagConsumptionRepository.save(joBagConsumption) | |||
| // 更新 ProductProcess 的 submitedBagRecord 为 true | |||
| val productProcesses = productProcessRepository.findByJobOrder_Id(request.jobId) | |||
| productProcesses.forEach { productProcess -> | |||
| productProcess.submitedBagRecord = true | |||
| productProcessRepository.save(productProcess) | |||
| } | |||
| return MessageResponse( | |||
| id = null, | |||
| code = null, | |||
| @@ -153,4 +163,81 @@ open fun getAllBagInfo(): List<BagInfo> { | |||
| } | |||
| } | |||
| } | |||
| open fun getAllBagUsageRecords(): List<BagUsageRecordResponse> { | |||
| val allRecords = joBagConsumptionRepository.findAllByDeletedIsFalse() | |||
| return allRecords.map { record -> | |||
| val bag = bagRepository.findById(record.bagId ?: 0L).orElse(null) | |||
| val bagLotLine = bagLotLineRepository.findById(record.bagLotLineId ?: 0L).orElse(null) | |||
| BagUsageRecordResponse( | |||
| id = record.id ?: 0L, | |||
| bagId = record.bagId ?: 0L, | |||
| bagLotLineId = record.bagLotLineId ?: 0L, | |||
| jobId = record.jobId ?: 0L, | |||
| jobOrderCode = record.jobOrderCode ?: "", | |||
| stockOutLineId = record.stockOutLineId ?: 0L, | |||
| startQty = record.startQty ?: 0, | |||
| consumedQty = record.consumedQty ?: 0, | |||
| scrapQty = record.scrapQty ?: 0, | |||
| endQty = record.endQty ?: 0, | |||
| date = record.date ?: LocalDate.now(), | |||
| time = record.time ?: LocalDateTime.now(), | |||
| bagName = bag?.itemName, | |||
| bagCode = bag?.itemCode, | |||
| lotNo = bagLotLine?.lotNo | |||
| ) | |||
| } | |||
| } | |||
| open fun getBagSummaries(): List<BagSummaryResponse> { | |||
| return bagRepository.findAllByDeletedIsFalse().map { bag -> | |||
| BagSummaryResponse( | |||
| id = bag.id ?: 0L, | |||
| bagName = bag.itemName, | |||
| bagCode = bag.itemCode, | |||
| takenBagBalance = bag.takenBagBalance, | |||
| deleted = bag.deleted | |||
| ) | |||
| } | |||
| } | |||
| open fun getBagLotLines(bagId: Long): List<BagLotLineResponse> { | |||
| val lines = bagLotLineRepository.findAllByBagId(bagId) | |||
| return lines.map { line -> | |||
| BagLotLineResponse( | |||
| id = line.id ?: 0L, | |||
| bagId = line.bagId ?: 0L, | |||
| lotNo = line.lotNo, | |||
| stockOutLineId = line.stockOutLineId, | |||
| startQty = line.startQty, | |||
| consumedQty = line.consumedQty, | |||
| scrapQty = line.scrapQty, | |||
| balanceQty = line.balanceQty, | |||
| firstUseDate = line.firstUseDate, | |||
| lastUseDate = line.lastUseDate | |||
| ) | |||
| } | |||
| } | |||
| open fun getBagConsumptions(bagLotLineId: Long): List<BagConsumptionResponse> { | |||
| val records = joBagConsumptionRepository.findAllByBagLotLineIdAndDeletedIsFalse(bagLotLineId) | |||
| return records.map { r -> | |||
| BagConsumptionResponse( | |||
| id = r.id ?: 0L, | |||
| bagId = r.bagId ?: 0L, | |||
| bagLotLineId = r.bagLotLineId ?: 0L, | |||
| jobId = r.jobId ?: 0L, | |||
| jobOrderCode = r.jobOrderCode, | |||
| stockOutLineId = r.stockOutLineId, | |||
| startQty = r.startQty, | |||
| consumedQty = r.consumedQty, | |||
| scrapQty = r.scrapQty, | |||
| endQty = r.endQty, | |||
| date = r.date, | |||
| time = r.time | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| @@ -47,4 +47,19 @@ class BagController( | |||
| fun createJoBagConsumption(@RequestBody request: CreateJoBagConsumptionRequest): MessageResponse { | |||
| return bagService.createJoBagConsumption(request) | |||
| } | |||
| @GetMapping("/bagUsageRecords") | |||
| fun getBagUsageRecords(): List<BagUsageRecordResponse> { | |||
| return bagService.getAllBagUsageRecords() | |||
| } | |||
| @GetMapping("/bags") | |||
| fun getBags(): List<BagSummaryResponse> = | |||
| bagService.getBagSummaries() | |||
| @GetMapping("/bags/{bagId}/lot-lines") | |||
| fun getBagLotLines(@PathVariable bagId: Long): List<BagLotLineResponse> = | |||
| bagService.getBagLotLines(bagId) | |||
| @GetMapping("/lot-lines/{bagLotLineId}/consumptions") | |||
| fun getBagConsumptions(@PathVariable bagLotLineId: Long): List<BagConsumptionResponse> = | |||
| bagService.getBagConsumptions(bagLotLineId) | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| package com.ffii.fpsms.modules.bag.web.model | |||
| import java.time.LocalDate | |||
| import java.time.LocalDateTime | |||
| data class GetAllBagInfoResponse( | |||
| val bagId: Long, | |||
| val bagName: String, | |||
| @@ -16,4 +17,58 @@ data class BagInfo( | |||
| val code: String, | |||
| val balanceQty: Int, | |||
| ) | |||
| data class BagUsageRecordResponse( | |||
| val id: Long, | |||
| val bagId: Long, | |||
| val bagLotLineId: Long, | |||
| val jobId: Long, | |||
| val jobOrderCode: String, | |||
| val stockOutLineId: Long, | |||
| val startQty: Int, | |||
| val consumedQty: Int, | |||
| val scrapQty: Int, | |||
| val endQty: Int, | |||
| val date: LocalDate, | |||
| val time: LocalDateTime, | |||
| val bagName: String?, | |||
| val bagCode: String?, | |||
| val lotNo: String? | |||
| ) | |||
| data class BagSummaryResponse( | |||
| val id: Long, // bag.id | |||
| val bagName: String?, | |||
| val bagCode: String?, | |||
| val takenBagBalance: Int?, // 如有 | |||
| val deleted: Boolean? | |||
| ) | |||
| data class BagLotLineResponse( | |||
| val id: Long, | |||
| val bagId: Long, | |||
| val lotNo: String?, | |||
| val stockOutLineId: Long?, | |||
| val startQty: Int?, | |||
| val consumedQty: Int?, | |||
| val scrapQty: Int?, | |||
| val balanceQty: Int?, | |||
| val firstUseDate: LocalDate?, | |||
| val lastUseDate: LocalDate? | |||
| ) | |||
| data class BagConsumptionResponse( | |||
| val id: Long, | |||
| val bagId: Long, | |||
| val bagLotLineId: Long, | |||
| val jobId: Long, | |||
| val jobOrderCode: String?, | |||
| val stockOutLineId: Long?, | |||
| val startQty: Int?, | |||
| val consumedQty: Int?, | |||
| val scrapQty: Int?, | |||
| val endQty: Int?, | |||
| val date: LocalDate?, | |||
| val time: LocalDateTime? | |||
| ) | |||
| @@ -28,5 +28,4 @@ interface DeliveryOrderInfo{ | |||
| val deliveryOrderLines: List<DeliveryOrderLineInfo> | |||
| } | |||
| @@ -421,19 +421,19 @@ open class DeliveryOrderService( | |||
| } | |||
| deliveryOrderRepository.save(deliveryOrder) | |||
| val pols = deliveryOrder.deliveryOrderLines.map { | |||
| SavePickOrderLineRequest( | |||
| itemId = it.item?.id, | |||
| qty = it.qty ?: BigDecimal.ZERO, | |||
| uomId = it.uom?.id, | |||
| val pols = deliveryOrder.deliveryOrderLines.map { | |||
| SavePickOrderLineRequest( | |||
| itemId = it.item?.id, | |||
| qty = it.qty ?: BigDecimal.ZERO, | |||
| uomId = it.uom?.id, | |||
| ) | |||
| } | |||
| val po = SavePickOrderRequest( | |||
| doId = deliveryOrder.id, | |||
| type = PickOrderType.DELIVERY_ORDER, | |||
| targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(), | |||
| pickOrderLine = pols | |||
| ) | |||
| } | |||
| val po = SavePickOrderRequest( | |||
| doId = deliveryOrder.id, | |||
| type = PickOrderType.DELIVERY_ORDER, | |||
| targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(), | |||
| pickOrderLine = pols | |||
| ) | |||
| val createdPickOrder = pickOrderService.create(po) | |||
| println("🔍 DEBUG: Created pick order - ID: ${createdPickOrder.id}") | |||
| @@ -608,64 +608,64 @@ open class DeliveryOrderService( | |||
| println(" DEBUG: Truck matches preferred floor - Truck Store: $truckStoreId, Preferred: $preferredFloor") | |||
| // storeId 基于 items 的 preferredFloor | |||
| val storeId = "$preferredFloor/F" | |||
| val loadingSequence = truck.loadingSequence ?: 999 | |||
| println("🔍 DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${truck.id}") | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = truck.id, | |||
| doOrderId = deliveryOrder.id, | |||
| pickOrderId = createdPickOrder.id, | |||
| truckDepartureTime = truck.departureTime, | |||
| shopId = deliveryOrder.shop?.id, | |||
| handledBy = null, | |||
| pickOrderCode = createdPickOrder.code, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| loadingSequence = loadingSequence, | |||
| ticketReleaseTime = null, | |||
| truckLanceCode = truck.truckLanceCode, | |||
| shopCode = deliveryOrder.shop?.code, | |||
| shopName = deliveryOrder.shop?.name, | |||
| requiredDeliveryDate = targetDate | |||
| ) | |||
| val savedDoPickOrder = doPickOrderService.save(doPickOrder) | |||
| println("🔍 DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}") | |||
| truck | |||
| } | |||
| return MessageResponse( | |||
| id = deliveryOrder.id, | |||
| code = deliveryOrder.code, | |||
| name = deliveryOrder.shop?.name, | |||
| type = null, | |||
| message = null, | |||
| errorPosition = null, | |||
| entity = mapOf("status" to deliveryOrder.status?.value) | |||
| // storeId 基于 items 的 preferredFloor | |||
| val storeId = "$preferredFloor/F" | |||
| val loadingSequence = truck.loadingSequence ?: 999 | |||
| println("🔍 DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${truck.id}") | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = truck.id, | |||
| doOrderId = deliveryOrder.id, | |||
| pickOrderId = createdPickOrder.id, | |||
| truckDepartureTime = truck.departureTime, | |||
| shopId = deliveryOrder.shop?.id, | |||
| handledBy = null, | |||
| pickOrderCode = createdPickOrder.code, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| loadingSequence = loadingSequence, | |||
| ticketReleaseTime = null, | |||
| truckLanceCode = truck.truckLanceCode, | |||
| shopCode = deliveryOrder.shop?.code, | |||
| shopName = deliveryOrder.shop?.name, | |||
| requiredDeliveryDate = targetDate | |||
| ) | |||
| val savedDoPickOrder = doPickOrderService.save(doPickOrder) | |||
| println("🔍 DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}") | |||
| truck | |||
| } | |||
| return MessageResponse( | |||
| id = deliveryOrder.id, | |||
| code = deliveryOrder.code, | |||
| name = deliveryOrder.shop?.name, | |||
| type = null, | |||
| message = null, | |||
| errorPosition = null, | |||
| entity = mapOf("status" to deliveryOrder.status?.value) | |||
| ) | |||
| } | |||
| open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String { | |||
| try { | |||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId) | |||
| val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } | |||
| val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||
| val lotNumbers = suggestedPickLots | |||
| .filter { it.pickOrderLine?.item?.id == itemId } | |||
| .mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo } | |||
| .distinct() | |||
| return lotNumbers.joinToString(", ") | |||
| } catch (e: Exception) { | |||
| println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}") | |||
| return "" | |||
| } | |||
| open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String { | |||
| try { | |||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId) | |||
| val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } | |||
| val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||
| val lotNumbers = suggestedPickLots | |||
| .filter { it.pickOrderLine?.item?.id == itemId } | |||
| .mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo } | |||
| .distinct() | |||
| return lotNumbers.joinToString(", ") | |||
| } catch (e: Exception) { | |||
| println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}") | |||
| return "" | |||
| } | |||
| } | |||
| // Delivery Note | |||
| @@ -794,7 +794,8 @@ open class DeliveryOrderService( | |||
| params["shopName"] = doPickOrder.shopName ?: deliveryNoteInfo[0].shopName ?: "" | |||
| params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: "" | |||
| params["deliveryDate"] = deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| params["deliveryDate"] = | |||
| deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| params["truckNo"] = truckNo | |||
| params["ShopPurchaseOrderNo"] = doPickOrder.deliveryOrderCode ?: deliveryNoteInfo.joinToString(", ") { it.code } | |||
| params["FGPickOrderNo"] = doPickOrder.pickOrderCode ?: selectedPickOrder?.code ?: "" | |||
| @@ -810,7 +811,7 @@ open class DeliveryOrderService( | |||
| deliveryNote: net.sf.jasperreports.engine.JasperReport, | |||
| fields: MutableList<MutableMap<String, Any>>, | |||
| params: MutableMap<String, Any> | |||
| ) : Map<String, Any> { | |||
| ): Map<String, Any> { | |||
| val doPickOrderRecord = doPickOrderRecordRepository.findById(request.doPickOrderId).orElseThrow { | |||
| NoSuchElementException("DoPickOrderRecord not found with ID: ${request.doPickOrderId}") | |||
| @@ -819,12 +820,12 @@ open class DeliveryOrderService( | |||
| val doPickOrderLineRecords = doPickOrderLineRecordRepository.findByDoPickOrderId(doPickOrderRecord.recordId) | |||
| val pickOrderIds = doPickOrderLineRecords.mapNotNull { it.pickOrderId }.distinct() | |||
| if(pickOrderIds.isEmpty()){ | |||
| if (pickOrderIds.isEmpty()) { | |||
| throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated pick orders") | |||
| } | |||
| val deliveryOrderIds = doPickOrderLineRecords.mapNotNull { it.doOrderId }.distinct() | |||
| if(deliveryOrderIds.isEmpty()){ | |||
| if (deliveryOrderIds.isEmpty()) { | |||
| throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated delivery orders") | |||
| } | |||
| @@ -920,9 +921,11 @@ open class DeliveryOrderService( | |||
| params["shopName"] = doPickOrderRecord.shopName ?: deliveryNoteInfo[0].shopName ?: "" | |||
| params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: "" | |||
| params["deliveryDate"] = deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| params["deliveryDate"] = | |||
| deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| params["truckNo"] = truckNo | |||
| params["ShopPurchaseOrderNo"] = doPickOrderRecord.deliveryOrderCode ?: deliveryNoteInfo.joinToString(", ") { it.code } | |||
| params["ShopPurchaseOrderNo"] = | |||
| doPickOrderRecord.deliveryOrderCode ?: deliveryNoteInfo.joinToString(", ") { it.code } | |||
| params["FGPickOrderNo"] = doPickOrderRecord.pickOrderCode ?: selectedPickOrder?.code ?: "" | |||
| return mapOf( | |||
| @@ -934,7 +937,8 @@ open class DeliveryOrderService( | |||
| //Print Delivery Note | |||
| @Transactional | |||
| open fun printDeliveryNote(request: PrintDeliveryNoteRequest) { | |||
| val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||
| val printer = | |||
| printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||
| val pdf = exportDeliveryNote( | |||
| ExportDeliveryNoteRequest( | |||
| @@ -993,7 +997,7 @@ open class DeliveryOrderService( | |||
| val doPickOrderLineRecords = doPickOrderLineRecordRepository.findByDoPickOrderId(doPickOrderRecord.recordId) | |||
| val deliveryOrderIds = doPickOrderLineRecords.mapNotNull { it.doOrderId }.distinct() | |||
| if(deliveryOrderIds.isEmpty()){ | |||
| if (deliveryOrderIds.isEmpty()) { | |||
| throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated delivery orders") | |||
| } | |||
| @@ -1010,7 +1014,7 @@ open class DeliveryOrderService( | |||
| val field = mutableMapOf<String, Any>() | |||
| } | |||
| if(cartonLabelInfo.size > 1){ | |||
| if (cartonLabelInfo.size > 1) { | |||
| } | |||
| @@ -1036,245 +1040,375 @@ open class DeliveryOrderService( | |||
| } | |||
| //Print Carton Labels | |||
| @Transactional | |||
| open fun printDNLabels(request: PrintDNLabelsRequest) { | |||
| val printer = | |||
| printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||
| //Print Carton Labels | |||
| @Transactional | |||
| open fun printDNLabels(request: PrintDNLabelsRequest) { | |||
| val printer = | |||
| printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||
| val pdf = exportDNLabels( | |||
| ExportDNLabelsRequest( | |||
| doPickOrderId = request.doPickOrderId, | |||
| numOfCarton = request.numOfCarton | |||
| ) | |||
| val pdf = exportDNLabels( | |||
| ExportDNLabelsRequest( | |||
| doPickOrderId = request.doPickOrderId, | |||
| numOfCarton = request.numOfCarton | |||
| ) | |||
| ) | |||
| val jasperPrint = pdf["report"] as JasperPrint | |||
| val jasperPrint = pdf["report"] as JasperPrint | |||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||
| try { | |||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||
| try { | |||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||
| printer.ip?.let { ip -> | |||
| printer.port?.let { port -> | |||
| ZebraPrinterUtil.printPdfToZebra( | |||
| tempPdfFile, | |||
| ip, | |||
| port, | |||
| printQty, | |||
| ZebraPrinterUtil.PrintDirection.ROTATED | |||
| ) | |||
| } | |||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||
| printer.ip?.let { ip -> | |||
| printer.port?.let { port -> | |||
| ZebraPrinterUtil.printPdfToZebra( | |||
| tempPdfFile, | |||
| ip, | |||
| port, | |||
| printQty, | |||
| ZebraPrinterUtil.PrintDirection.ROTATED | |||
| ) | |||
| } | |||
| println("Test PDF saved to: ${tempPdfFile.absolutePath}") | |||
| } finally { | |||
| //tempPdfFile.delete() | |||
| } | |||
| } | |||
| @Transactional(rollbackFor = [Exception::class]) | |||
| open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult { | |||
| println("🔍 DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}") | |||
| println("Test PDF saved to: ${tempPdfFile.absolutePath}") | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | |||
| ?: throw NoSuchElementException("Delivery Order not found") | |||
| } finally { | |||
| //tempPdfFile.delete() | |||
| } | |||
| // 检查状态,跳过已完成或已发布的DO | |||
| if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) { | |||
| throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release") | |||
| } | |||
| // 更新状态为released (使用RECEIVING表示已发布) | |||
| deliveryOrder.apply { | |||
| status = DeliveryOrderStatus.RECEIVING // 使用RECEIVING表示已发布状态 | |||
| } | |||
| deliveryOrderRepository.save(deliveryOrder) | |||
| // 创建 pick order | |||
| val pols = deliveryOrder.deliveryOrderLines.map { | |||
| SavePickOrderLineRequest( | |||
| itemId = it.item?.id, | |||
| qty = it.qty ?: BigDecimal.ZERO, | |||
| uomId = it.uom?.id, | |||
| ) | |||
| } | |||
| val po = SavePickOrderRequest( | |||
| doId = deliveryOrder.id, | |||
| type = PickOrderType.DELIVERY_ORDER, | |||
| targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(), | |||
| pickOrderLine = pols | |||
| ) | |||
| 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) | |||
| // 创建 suggestions 和 hold inventory | |||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | |||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLines( | |||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | |||
| ) | |||
| val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | |||
| val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null } | |||
| if (insufficientCount > 0) { | |||
| println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)") | |||
| @Transactional(rollbackFor = [Exception::class]) | |||
| open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult { | |||
| println("🔍 DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}") | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | |||
| ?: throw NoSuchElementException("Delivery Order not found") | |||
| // 检查状态,跳过已完成或已发布的DO | |||
| if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) { | |||
| throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release") | |||
| } | |||
| // 更新状态为released (使用RECEIVING表示已发布) | |||
| deliveryOrder.apply { | |||
| status = DeliveryOrderStatus.RECEIVING // 使用RECEIVING表示已发布状态 | |||
| } | |||
| deliveryOrderRepository.save(deliveryOrder) | |||
| // 创建 pick order | |||
| val pols = deliveryOrder.deliveryOrderLines.map { | |||
| SavePickOrderLineRequest( | |||
| itemId = it.item?.id, | |||
| qty = it.qty ?: BigDecimal.ZERO, | |||
| uomId = it.uom?.id, | |||
| ) | |||
| } | |||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | |||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | |||
| val po = SavePickOrderRequest( | |||
| doId = deliveryOrder.id, | |||
| type = PickOrderType.DELIVERY_ORDER, | |||
| targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(), | |||
| pickOrderLine = pols | |||
| ) | |||
| 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) | |||
| 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) | |||
| // 创建 suggestions 和 hold inventory | |||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | |||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLines( | |||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | |||
| ) | |||
| val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | |||
| val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null } | |||
| if (insufficientCount > 0) { | |||
| println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)") | |||
| } | |||
| 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) | |||
| // 创建 stock out | |||
| val stockOut = StockOut().apply { | |||
| this.type = "do" | |||
| this.consoPickOrderCode = consoCode | |||
| this.status = StockOutStatus.PENDING.status | |||
| this.handler = request.userId | |||
| } | |||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||
| saveSuggestedPickLots.forEach { lot -> | |||
| val polId = lot.pickOrderLine?.id | |||
| val illId = lot.suggestedLotLine?.id | |||
| if (polId != null) { | |||
| val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null) | |||
| val inventoryLotLine = illId?.let { inventoryLotLineRepository.findById(it).orElse(null) } | |||
| if (pickOrderLine != null) { | |||
| val line = StockOutLine().apply { | |||
| this.stockOut = savedStockOut | |||
| this.pickOrderLine = pickOrderLine | |||
| this.inventoryLotLine = inventoryLotLine // 可能为 null | |||
| this.item = pickOrderLine.item | |||
| // 修复:根据是否有 inventoryLotLine 设置状态 | |||
| this.status = if (inventoryLotLine == null) { | |||
| StockOutLineStatus.PARTIALLY_COMPLETE.status // 没有库存批次时使用 PARTIALLY_COMPLETE | |||
| } else { | |||
| StockOutLineStatus.PENDING.status // 有正常库存批次时使用 PENDING | |||
| } | |||
| this.qty = 0.0 | |||
| } | |||
| stockOutLineRepository.save(line) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // 分析楼层分布 | |||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | |||
| val itemsStoreIdQuery = """ | |||
| SELECT | |||
| i.store_id, | |||
| COUNT(DISTINCT i.id) as item_count | |||
| FROM items i | |||
| WHERE i.id IN (${itemIds.joinToString(",")}) | |||
| AND i.deleted = 0 | |||
| GROUP BY i.store_id | |||
| """.trimIndent() | |||
| val itemsStoreIdResults = jdbcDao.queryForList(itemsStoreIdQuery) | |||
| val storeIdItemCount = mutableMapOf<String, Int>() | |||
| var totalItemsWithStoreId = 0 // 统计有 store_id 的商品总数 | |||
| itemsStoreIdResults.forEach { row -> | |||
| val rawStoreId = row["store_id"] as? String | |||
| if (rawStoreId != null) { // 只统计非 NULL 的 store_id | |||
| val normalizedStoreId = when (rawStoreId) { | |||
| "3F" -> "4F" | |||
| else -> rawStoreId | |||
| } | |||
| val count = (row["item_count"] as? Number)?.toInt() ?: 0 | |||
| storeIdItemCount[normalizedStoreId] = | |||
| (storeIdItemCount[normalizedStoreId] ?: 0) + count | |||
| totalItemsWithStoreId += count | |||
| } | |||
| } | |||
| inventoryLotLineRepository.saveAll(inventoryLotLines) | |||
| // 创建 stock out | |||
| val stockOut = StockOut().apply { | |||
| this.type = "do" | |||
| this.consoPickOrderCode = consoCode | |||
| this.status = StockOutStatus.PENDING.status | |||
| this.handler = request.userId | |||
| val count2F = storeIdItemCount["2F"] ?: 0 | |||
| val count4F = storeIdItemCount["4F"] ?: 0 | |||
| val preferredFloor = when { | |||
| totalItemsWithStoreId == 0 -> { | |||
| println("⚠️ WARNING: All items have NULL store_id, defaulting to 2F") | |||
| "2F" | |||
| } | |||
| count4F > count2F -> "4F" | |||
| count2F > count4F -> "2F" | |||
| count4F == totalItemsWithStoreId && count2F == 0 -> "4F" | |||
| else -> "2F" // 默认 2F | |||
| } | |||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||
| saveSuggestedPickLots.forEach { lot -> | |||
| val polId = lot.pickOrderLine?.id | |||
| val illId = lot.suggestedLotLine?.id | |||
| if (polId != null) { | |||
| val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null) | |||
| val inventoryLotLine = illId?.let { inventoryLotLineRepository.findById(it).orElse(null) } | |||
| if (pickOrderLine != null) { | |||
| val line = StockOutLine().apply { | |||
| this.stockOut = savedStockOut | |||
| this.pickOrderLine = pickOrderLine | |||
| this.inventoryLotLine = inventoryLotLine // 可能为 null | |||
| this.item = pickOrderLine.item | |||
| // 修复:根据是否有 inventoryLotLine 设置状态 | |||
| this.status = if (inventoryLotLine == null) { | |||
| StockOutLineStatus.PARTIALLY_COMPLETE.status // 没有库存批次时使用 PARTIALLY_COMPLETE | |||
| println("🔍 DEBUG: Floor calculation for DO ${deliveryOrder.id}") | |||
| println(" - Total items: ${itemIds.size}") | |||
| println(" - Items with store_id: $totalItemsWithStoreId") | |||
| println(" - Items without store_id: ${itemIds.size - totalItemsWithStoreId}") | |||
| println(" - 2F items: $count2F") | |||
| println(" - 4F items: $count4F") | |||
| println(" - Preferred floor: $preferredFloor") | |||
| val truck = deliveryOrder.shop?.id?.let { shopId -> | |||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||
| val preferredStoreId = when (preferredFloor) { | |||
| "2F" -> "2F" | |||
| "4F" -> "4F" | |||
| "3F" -> "3F" | |||
| else -> "2F" | |||
| } | |||
| val matchedTrucks = trucks.filter { it.storeId == preferredStoreId } | |||
| if (matchedTrucks.isEmpty()) { | |||
| null | |||
| } else { | |||
| if (preferredStoreId == "4F" && matchedTrucks.size > 1) { | |||
| deliveryOrder.estimatedArrivalDate?.let { estimatedArrivalDate -> | |||
| val targetDate = estimatedArrivalDate.toLocalDate() | |||
| val dayOfWeek = targetDate.dayOfWeek | |||
| val dayAbbr = when (dayOfWeek) { | |||
| java.time.DayOfWeek.MONDAY -> "Mon" | |||
| java.time.DayOfWeek.TUESDAY -> "Tue" | |||
| java.time.DayOfWeek.WEDNESDAY -> "Wed" | |||
| java.time.DayOfWeek.THURSDAY -> "Thu" | |||
| java.time.DayOfWeek.FRIDAY -> "Fri" | |||
| java.time.DayOfWeek.SATURDAY -> "Sat" | |||
| java.time.DayOfWeek.SUNDAY -> "Sun" | |||
| } | |||
| println("🔍 DEBUG: DO ${deliveryOrder.id} - Target date: $targetDate ($dayAbbr), Shop: $shopId") | |||
| println("🔍 DEBUG: Found ${matchedTrucks.size} matched 4F trucks") | |||
| val dayMatchedTrucks = matchedTrucks.filter { | |||
| it.truckLanceCode?.contains(dayAbbr, ignoreCase = true) == true | |||
| } | |||
| println("🔍 DEBUG: Found ${dayMatchedTrucks.size} trucks matching $dayAbbr") | |||
| dayMatchedTrucks.forEach { t -> | |||
| println(" - Truck ID=${t.id}, Code=${t.truckLanceCode}, Time=${t.departureTime}") | |||
| } | |||
| if (dayMatchedTrucks.isNotEmpty()) { | |||
| val selected = dayMatchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| println("✅ DEBUG: Selected truck matching $dayAbbr - ID=${selected?.id}, Code=${selected?.truckLanceCode}") | |||
| selected | |||
| } else { | |||
| StockOutLineStatus.PENDING.status // 有正常库存批次时使用 PENDING | |||
| println("⚠️ WARNING: No truck matching $dayAbbr, using first available truck") | |||
| matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| } | |||
| this.qty = 0.0 | |||
| } ?: run { | |||
| matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| } | |||
| stockOutLineRepository.save(line) | |||
| } else { | |||
| matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // 分析楼层分布 | |||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | |||
| val itemsStoreIdQuery = """ | |||
| SELECT | |||
| i.store_id, | |||
| COUNT(DISTINCT i.id) as item_count | |||
| FROM items i | |||
| WHERE i.id IN (${itemIds.joinToString(",")}) | |||
| AND i.deleted = 0 | |||
| GROUP BY i.store_id | |||
| """.trimIndent() | |||
| val itemsStoreIdResults = jdbcDao.queryForList(itemsStoreIdQuery) | |||
| val storeIdItemCount = mutableMapOf<String, Int>() | |||
| itemsStoreIdResults.forEach { row -> | |||
| val rawStoreId = row["store_id"] as? String | |||
| if (rawStoreId != null) { | |||
| val normalizedStoreId = when (rawStoreId) { | |||
| "3F" -> "4F" | |||
| else -> rawStoreId | |||
| } | |||
| storeIdItemCount[normalizedStoreId] = | |||
| (storeIdItemCount[normalizedStoreId] ?: 0) + | |||
| ((row["item_count"] as? Number)?.toInt() ?: 0) | |||
| // 如果没有匹配的 truck,抛出异常跳过 | |||
| if (truck == null) { | |||
| val errorMsg = | |||
| "No matching truck found for preferredFloor ($preferredFloor). Skipping DO ${deliveryOrder.id}." | |||
| println("⚠️ $errorMsg") | |||
| throw IllegalStateException(errorMsg) | |||
| } | |||
| } | |||
| val preferredFloor = if ((storeIdItemCount["4F"] ?: 0) == itemIds.size && | |||
| (storeIdItemCount["2F"] ?: 0) == 0) { | |||
| "4F" | |||
| } else { | |||
| "2F" | |||
| println(" DEBUG: Matched truck - ID=${truck.id}, Store=${truck.storeId}, Floor=$preferredFloor") | |||
| return ReleaseDoResult( | |||
| deliveryOrderId = deliveryOrder.id!!, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| pickOrderId = createdPickOrder.id!!, | |||
| pickOrderCode = pickOrderEntity?.code, | |||
| shopId = deliveryOrder.shop?.id, | |||
| shopCode = deliveryOrder.shop?.code, | |||
| shopName = deliveryOrder.shop?.name, | |||
| estimatedArrivalDate = targetDate, | |||
| preferredFloor = preferredFloor, | |||
| truckId = truck.id, | |||
| truckDepartureTime = truck.departureTime, | |||
| truckLanceCode = truck.truckLanceCode, | |||
| loadingSequence = truck.loadingSequence // 直接使用 truck 的值 | |||
| ) | |||
| } | |||
| // 查找匹配 preferred floor 的 truck | |||
| val truck = deliveryOrder.shop?.id?.let { shopId -> | |||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||
| val preferredStoreId = when (preferredFloor) { | |||
| "2F" -> "2F" | |||
| "4F" -> "4F" | |||
| "3F" -> "3F" | |||
| else -> "2F" | |||
| private fun getDayOfWeekAbbr(date: LocalDate): String = | |||
| when (date.dayOfWeek) { | |||
| java.time.DayOfWeek.MONDAY -> "Mon" | |||
| java.time.DayOfWeek.TUESDAY -> "Tue" | |||
| java.time.DayOfWeek.WEDNESDAY -> "Wed" | |||
| java.time.DayOfWeek.THURSDAY -> "Thu" | |||
| java.time.DayOfWeek.FRIDAY -> "Fri" | |||
| java.time.DayOfWeek.SATURDAY -> "Sat" | |||
| java.time.DayOfWeek.SUNDAY -> "Sun" | |||
| } | |||
| // 只选择 store_id 匹配的 truck | |||
| val matchedTrucks = trucks.filter { it.storeId == preferredStoreId } | |||
| if (matchedTrucks.isEmpty()) { | |||
| null // 没有匹配的 truck | |||
| } else { | |||
| matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| } | |||
| } | |||
| // 如果没有匹配的 truck,抛出异常跳过 | |||
| if (truck == null) { | |||
| val errorMsg = "No matching truck found for preferredFloor ($preferredFloor). Skipping DO ${deliveryOrder.id}." | |||
| println("⚠️ $errorMsg") | |||
| throw IllegalStateException(errorMsg) | |||
| } | |||
| open fun check4FTruckAvailabilityForDoList(doIds: List<Long>): Check4FTruckBatchResponse { | |||
| val problems = mutableListOf<ProblemDoDto>() | |||
| println(" DEBUG: Matched truck - ID=${truck.id}, Store=${truck.storeId}, Floor=$preferredFloor") | |||
| return ReleaseDoResult( | |||
| deliveryOrderId = deliveryOrder.id!!, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| pickOrderId = createdPickOrder.id!!, | |||
| pickOrderCode = pickOrderEntity?.code, | |||
| shopId = deliveryOrder.shop?.id, | |||
| shopCode = deliveryOrder.shop?.code, | |||
| shopName = deliveryOrder.shop?.name, | |||
| estimatedArrivalDate = targetDate, | |||
| preferredFloor = preferredFloor, | |||
| truckId = truck.id, | |||
| truckDepartureTime = truck.departureTime, | |||
| truckLanceCode = truck.truckLanceCode, | |||
| loadingSequence = truck.loadingSequence // 直接使用 truck 的值 | |||
| ) | |||
| } | |||
| doIds.forEach { doId -> | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId) ?: return@forEach | |||
| val shopId = deliveryOrder.shop?.id ?: return@forEach | |||
| val eta = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: return@forEach | |||
| @Transactional | |||
| open fun syncDeliveryOrderStatusFromDoPickOrder(): Int { | |||
| // 1) Check if this DO is a 4F order (your “all items on 4F, no 2F” rule) | |||
| val sql = """ | |||
| UPDATE fpsmsdb.delivery_order do | |||
| INNER JOIN fpsmsdb.do_pick_order dpo ON dpo.do_order_id = do.id | |||
| SET do.status = 'completed' | |||
| WHERE dpo.ticket_status = 'completed' | |||
| AND do.status != 'completed' | |||
| AND do.deleted = 0 | |||
| AND dpo.deleted = 0 | |||
| """.trimIndent() | |||
| return jdbcDao.executeUpdate(sql, emptyMap<String, Any>()) | |||
| SELECT | |||
| COUNT(DISTINCT CASE WHEN i.store_id = '4F' THEN dol.itemId END) AS count_4f, | |||
| COUNT(DISTINCT CASE WHEN i.store_id = '2F' THEN dol.itemId END) AS count_2f, | |||
| COUNT(DISTINCT dol.itemId) AS total_items | |||
| FROM fpsmsdb.delivery_order_line dol | |||
| INNER JOIN fpsmsdb.items i ON i.id = dol.itemId AND i.deleted = 0 | |||
| WHERE dol.deliveryOrderId = :doId AND dol.deleted = 0 | |||
| """.trimIndent() | |||
| val res = jdbcDao.queryForList(sql, mapOf("doId" to doId)).firstOrNull() ?: return@forEach | |||
| val count4F = (res["count_4f"] as? Number)?.toInt() ?: 0 | |||
| val count2F = (res["count_2f"] as? Number)?.toInt() ?: 0 | |||
| val totalItems = (res["total_items"] as? Number)?.toInt() ?: 0 | |||
| val is4FOrder = (totalItems > 0 && count4F == totalItems && count2F == 0) | |||
| if (!is4FOrder) { | |||
| // Not a 4F order → no problem, skip | |||
| return@forEach | |||
| } | |||
| // 2) Find 4F trucks for that shop on that weekday | |||
| val dayAbbr = getDayOfWeekAbbr(eta) | |||
| val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, "4F", dayAbbr) | |||
| if (trucks.isEmpty()) { | |||
| // Problem DO: 4F order but no matching 4F truck that day | |||
| val availableTrucksAll4F = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||
| .filter { it.storeId == "4F" } | |||
| val truckDtos = availableTrucksAll4F.map { t -> | |||
| TruckInfoDto( | |||
| id = t.id, | |||
| truckLanceCode = t.truckLanceCode, | |||
| departureTime = t.departureTime?.toString(), | |||
| storeId = t.storeId, | |||
| shopCode = t.shopCode, | |||
| shopName = t.shopName | |||
| ) | |||
| } | |||
| problems += ProblemDoDto( | |||
| deliveryOrderId = deliveryOrder.id!!, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| targetDate = eta, | |||
| availableTrucks = truckDtos | |||
| ) | |||
| } | |||
| } | |||
| return Check4FTruckBatchResponse( | |||
| hasProblem = problems.isNotEmpty(), | |||
| problems = problems | |||
| ) | |||
| } | |||
| } | |||
| @@ -19,6 +19,8 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||
| import com.ffii.fpsms.modules.pickOrder.service.PickExecutionIssueService | |||
| import java.time.LocalDate | |||
| import java.time.DayOfWeek | |||
| data class BatchReleaseJobStatus( | |||
| val jobId: String, | |||
| val total: Int, | |||
| @@ -41,9 +43,37 @@ class DoReleaseCoordinatorService( | |||
| private val poolSize = Runtime.getRuntime().availableProcessors() | |||
| private val executor = Executors.newFixedThreadPool(min(poolSize, 4)) | |||
| private val jobs = ConcurrentHashMap<String, BatchReleaseJobStatus>() | |||
| private fun getDayOfWeekAbbr(date: LocalDate?): String? { | |||
| if (date == null) return null | |||
| return when (date.dayOfWeek) { | |||
| DayOfWeek.MONDAY -> "Mon" | |||
| DayOfWeek.TUESDAY -> "Tue" | |||
| DayOfWeek.WEDNESDAY -> "Wed" | |||
| DayOfWeek.THURSDAY -> "Thu" | |||
| DayOfWeek.FRIDAY -> "Fri" | |||
| DayOfWeek.SATURDAY -> "Sat" | |||
| DayOfWeek.SUNDAY -> "Sun" | |||
| } | |||
| } | |||
| // 辅助方法:在 SQL 中获取星期几的缩写(MySQL) | |||
| private fun getDayOfWeekAbbrSql(dateColumn: String): String { | |||
| return """ | |||
| CASE DAYNAME($dateColumn) | |||
| WHEN 'Monday' THEN 'Mon' | |||
| WHEN 'Tuesday' THEN 'Tue' | |||
| WHEN 'Wednesday' THEN 'Wed' | |||
| WHEN 'Thursday' THEN 'Thu' | |||
| WHEN 'Friday' THEN 'Fri' | |||
| WHEN 'Saturday' THEN 'Sat' | |||
| WHEN 'Sunday' THEN 'Sun' | |||
| ELSE '' | |||
| END | |||
| """.trimIndent() | |||
| } | |||
| private fun updateBatchTicketNumbers() { | |||
| try { | |||
| val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate") | |||
| val updateSql = """ | |||
| UPDATE fpsmsdb.do_pick_order dpo | |||
| INNER JOIN ( | |||
| @@ -53,7 +83,7 @@ class DoReleaseCoordinatorService( | |||
| CASE | |||
| WHEN i.store_id = '3F' THEN '4F' | |||
| ELSE i.store_id | |||
| END AS store_id, -- 这里做 3F → 4F | |||
| END AS store_id, | |||
| COUNT(DISTINCT dol.itemId) AS item_count | |||
| FROM fpsmsdb.delivery_order_line dol | |||
| INNER JOIN fpsmsdb.items i ON i.id = dol.itemId | |||
| @@ -95,7 +125,26 @@ class DoReleaseCoordinatorService( | |||
| do.id AS delivery_order_id, | |||
| do.estimatedArrivalDate, | |||
| pf.preferred_floor, | |||
| $dayOfWeekSql AS day_of_week_abbr, | |||
| CASE | |||
| WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1 | |||
| AND pf.preferred_store_id = 4 | |||
| AND do.estimatedArrivalDate IS NOT NULL THEN | |||
| COALESCE( | |||
| (SELECT t.DepartureTime FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%') | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| (SELECT t.DepartureTime FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| (SELECT t.DepartureTime FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| '23:59:59' | |||
| ) | |||
| WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN | |||
| COALESCE( | |||
| (SELECT t.DepartureTime FROM fpsmsdb.truck t | |||
| @@ -116,6 +165,24 @@ class DoReleaseCoordinatorService( | |||
| ) | |||
| END AS selected_departure_time, | |||
| CASE | |||
| WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1 | |||
| AND pf.preferred_store_id = 4 | |||
| AND do.estimatedArrivalDate IS NOT NULL THEN | |||
| COALESCE( | |||
| (SELECT t.TruckLanceCode FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%') | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| (SELECT t.TruckLanceCode FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| (SELECT t.TruckLanceCode FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| 'ZZ' | |||
| ) | |||
| WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN | |||
| COALESCE( | |||
| (SELECT t.TruckLanceCode FROM fpsmsdb.truck t | |||
| @@ -136,6 +203,12 @@ class DoReleaseCoordinatorService( | |||
| ) | |||
| END AS selected_truck_lance, | |||
| COALESCE( | |||
| (SELECT t.LoadingSequence FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| AND (pf.preferred_store_id != 4 OR do.estimatedArrivalDate IS NULL OR | |||
| t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')) | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| (SELECT t.LoadingSequence FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| @@ -193,6 +266,7 @@ class DoReleaseCoordinatorService( | |||
| try { | |||
| println("🔍 DEBUG: Getting ordered IDs for ${ids.size} orders") | |||
| println("🔍 DEBUG: First 5 IDs: ${ids.take(5)}") | |||
| val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate") | |||
| val sql = """ | |||
| WITH DoFloorCounts AS ( | |||
| SELECT | |||
| @@ -251,13 +325,39 @@ class DoReleaseCoordinatorService( | |||
| FROM DoFloorSummary | |||
| ), | |||
| TruckSelection AS ( | |||
| SELECT | |||
| SELECT | |||
| do.id AS delivery_order_id, | |||
| do.shopId, | |||
| do.estimatedArrivalDate, | |||
| pf.preferred_floor, | |||
| pf.preferred_store_id, | |||
| $dayOfWeekSql AS day_of_week_abbr, | |||
| CASE | |||
| WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1 | |||
| AND pf.preferred_store_id = 4 | |||
| AND do.estimatedArrivalDate IS NOT NULL THEN | |||
| COALESCE( | |||
| (SELECT t.DepartureTime | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId | |||
| AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%') | |||
| ORDER BY t.DepartureTime ASC | |||
| LIMIT 1), | |||
| (SELECT t.DepartureTime | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| ORDER BY t.DepartureTime ASC | |||
| LIMIT 1), | |||
| (SELECT t.DepartureTime | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| ORDER BY t.DepartureTime ASC | |||
| LIMIT 1), | |||
| '23:59:59' | |||
| ) | |||
| WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN | |||
| COALESCE( | |||
| (SELECT t.DepartureTime | |||
| @@ -284,6 +384,31 @@ class DoReleaseCoordinatorService( | |||
| ) | |||
| END AS selected_departure_time, | |||
| CASE | |||
| WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1 | |||
| AND pf.preferred_store_id = 4 | |||
| AND do.estimatedArrivalDate IS NOT NULL THEN | |||
| COALESCE( | |||
| (SELECT t.TruckLanceCode | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId | |||
| AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%') | |||
| ORDER BY t.DepartureTime ASC | |||
| LIMIT 1), | |||
| (SELECT t.TruckLanceCode | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| ORDER BY t.DepartureTime ASC | |||
| LIMIT 1), | |||
| (SELECT t.TruckLanceCode | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| ORDER BY t.DepartureTime ASC | |||
| LIMIT 1), | |||
| 'ZZ' | |||
| ) | |||
| WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN | |||
| COALESCE( | |||
| (SELECT t.TruckLanceCode | |||
| @@ -310,17 +435,24 @@ class DoReleaseCoordinatorService( | |||
| ) | |||
| END AS selected_truck_lance, | |||
| COALESCE( | |||
| (SELECT t.LoadingSequence | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| (SELECT t.LoadingSequence | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| 999 | |||
| ) AS loading_sequence | |||
| (SELECT t.LoadingSequence | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| AND (pf.preferred_store_id != 4 OR do.estimatedArrivalDate IS NULL OR | |||
| t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')) | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| (SELECT t.LoadingSequence | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| AND t.Store_id = pf.preferred_store_id | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| (SELECT t.LoadingSequence | |||
| FROM fpsmsdb.truck t | |||
| WHERE t.shopId = do.shopId AND t.deleted = 0 | |||
| ORDER BY t.DepartureTime ASC LIMIT 1), | |||
| 999 | |||
| ) AS loading_sequence | |||
| FROM fpsmsdb.delivery_order do | |||
| LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id | |||
| WHERE do.id IN (${ids.joinToString(",")}) | |||
| @@ -338,35 +470,34 @@ class DoReleaseCoordinatorService( | |||
| """.trimIndent() | |||
| println("🔍 DEBUG: SQL length: ${sql.length} characters") // 添加这行 | |||
| println("🔍 DEBUG: SQL first 500 chars: ${sql.take(500)}") // 添加这行 | |||
| println("🔍 DEBUG: SQL length: ${sql.length} characters") | |||
| println("🔍 DEBUG: SQL first 500 chars: ${sql.take(500)}") | |||
| val results = jdbcDao.queryForList(sql) | |||
| println("🔍 DEBUG: Results type: ${results.javaClass.name}") // 添加这行 | |||
| println("🔍 DEBUG: Results size: ${results.size}") // 添加这行 | |||
| if (results.isNotEmpty()) { | |||
| println("🔍 DEBUG: First result keys: ${results[0].keys}") // 添加这行 | |||
| println("🔍 DEBUG: First result: ${results[0]}") // 添加这行 | |||
| } | |||
| val sortedIds = results.mapNotNull { row -> | |||
| (row["id"] as? Number)?.toLong() | |||
| } | |||
| println("🔍 DEBUG: Query returned ${sortedIds.size} sorted IDs") | |||
| println("🔍 DEBUG: First 10 sorted IDs: ${sortedIds.take(10)}") | |||
| return if (sortedIds.isEmpty()) { | |||
| println("⚠️ WARNING: No sorted IDs, using original order") | |||
| ids | |||
| } else { | |||
| sortedIds | |||
| } | |||
| println("🔍 DEBUG: Results type: ${results.javaClass.name}") | |||
| println("🔍 DEBUG: Results size: ${results.size}") | |||
| if (results.isNotEmpty()) { | |||
| println("🔍 DEBUG: First result keys: ${results[0].keys}") | |||
| println("🔍 DEBUG: First result: ${results[0]}") | |||
| } | |||
| val sortedIds = results.mapNotNull { row -> | |||
| (row["id"] as? Number)?.toLong() | |||
| } | |||
| println("🔍 DEBUG: Query returned ${sortedIds.size} sorted IDs") | |||
| println("🔍 DEBUG: First 10 sorted IDs: ${sortedIds.take(10)}") | |||
| return if (sortedIds.isEmpty()) { | |||
| println("⚠️ WARNING: No sorted IDs, using original order") | |||
| ids | |||
| } else { | |||
| sortedIds | |||
| } | |||
| } catch (e: Exception) { | |||
| println("❌ ERROR: ${e.message}") | |||
| println("❌ ERROR Stack Trace:") // 添加这行 | |||
| println("❌ ERROR: ${e.message}") | |||
| println("❌ ERROR Stack Trace:") | |||
| e.printStackTrace() | |||
| return ids | |||
| } | |||
| @@ -38,6 +38,7 @@ import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderService | |||
| import com.ffii.core.response.RecordsRes | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest | |||
| import com.ffii.fpsms.modules.stock.service.StockInLineService | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.Check4FTruckBatchResponse | |||
| @RequestMapping("/do") | |||
| @RestController | |||
| @@ -228,4 +229,8 @@ class DeliveryOrderController( | |||
| fun printQrCodeForDeliveryOrder(@ModelAttribute request: PrintQrCodeForDoRequest) { | |||
| stockInLineService.printQrCodeForDeliveryOrder(request) | |||
| } | |||
| @PostMapping("/check-4f-trucks-batch") | |||
| fun check4FTrucksBatch(@RequestBody doIds: List<Long>): Check4FTruckBatchResponse { | |||
| return deliveryOrderService.check4FTruckAvailabilityForDoList(doIds) | |||
| } | |||
| } | |||
| @@ -26,4 +26,24 @@ data class TicketReleaseTableResponse( | |||
| val requiredDeliveryDate: LocalDate?, | |||
| val handlerName: String?, | |||
| val numberOfFGItems: Int = 0 | |||
| ) | |||
| data class TruckInfoDto( | |||
| val id: Long?, | |||
| val truckLanceCode: String?, | |||
| val departureTime: String?, // or LocalTime? | |||
| val storeId: String?, | |||
| val shopCode: String?, | |||
| val shopName: String? | |||
| ) | |||
| data class ProblemDoDto( | |||
| val deliveryOrderId: Long, | |||
| val deliveryOrderCode: String?, | |||
| val targetDate: LocalDate, | |||
| val availableTrucks: List<TruckInfoDto> | |||
| ) | |||
| data class Check4FTruckBatchResponse( | |||
| val hasProblem: Boolean, | |||
| val problems: List<ProblemDoDto> | |||
| ) | |||
| @@ -26,8 +26,9 @@ import com.google.gson.reflect.TypeToken | |||
| import org.springframework.data.domain.PageRequest | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.transaction.annotation.Transactional | |||
| import java.math.BigDecimal | |||
| import java.math.BigDecimal | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderBomMaterialRepository | |||
| import java.time.format.DateTimeFormatter | |||
| import kotlin.jvm.optionals.getOrNull | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JobTypeRepository | |||
| @@ -62,7 +63,7 @@ import kotlinx.serialization.encodeToString | |||
| import kotlinx.serialization.json.Json | |||
| import kotlin.math.exp | |||
| import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository | |||
| import java.math.RoundingMode | |||
| import java.time.LocalDate | |||
| import java.time.LocalDateTime | |||
| @Service | |||
| @@ -83,7 +84,8 @@ open class JobOrderService( | |||
| val jobTypeRepository: JobTypeRepository, | |||
| val inventoryRepository: InventoryRepository, | |||
| val stockInLineRepository: StockInLineRepository, | |||
| val productProcessRepository: ProductProcessRepository | |||
| val productProcessRepository: ProductProcessRepository, | |||
| val jobOrderBomMaterialRepository: JobOrderBomMaterialRepository | |||
| ) { | |||
| open fun allJobOrdersByPage(request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfo> { | |||
| @@ -803,4 +805,40 @@ open class JobOrderService( | |||
| ) | |||
| } | |||
| */ | |||
| open fun updateJoReqQty(request: UpdateJoReqQtyRequest): MessageResponse { | |||
| val jobOrder = jobOrderRepository.findById(request.id).orElse(null) | |||
| ?: throw NoSuchElementException("Job Order not found with id: ${request.id}") | |||
| val newReqQty = BigDecimal.valueOf(request.reqQty.toLong()) | |||
| // 更新 JobOrder 的 reqQty | |||
| jobOrder.reqQty = newReqQty | |||
| jobOrderRepository.save(jobOrder) | |||
| // 更新相关的 JobOrderBomMaterial 的 reqQty(根据新的比例重新计算) | |||
| val bom = jobOrder.bom | |||
| if (bom != null && bom.outputQty != null && bom.outputQty!! > BigDecimal.ZERO) { | |||
| val proportion = newReqQty.divide(bom.outputQty!!, 5, RoundingMode.HALF_UP) | |||
| val jobOrderBomMaterials = jobOrderBomMaterialRepository.findAllByJobOrderId(jobOrder.id) | |||
| jobOrderBomMaterials.forEach { jobm -> | |||
| // 找到对应的 BOM Material | |||
| val bomMaterial = bom.bomMaterials?.find { it.item?.id == jobm.item?.id } | |||
| if (bomMaterial != null && bomMaterial.qty != null) { | |||
| jobm.reqQty = (bomMaterial.qty!!.times(proportion)).setScale(0, RoundingMode.CEILING) | |||
| jobOrderBomMaterialRepository.save(jobm) | |||
| } | |||
| } | |||
| } | |||
| return MessageResponse( | |||
| id = jobOrder.id, | |||
| code = jobOrder.code, | |||
| name = jobOrder.bom?.name, | |||
| type = "success", | |||
| message = "Job Order ReqQty Updated", | |||
| errorPosition = null, | |||
| entity = mapOf("reqQty" to request.reqQty) | |||
| ) | |||
| } | |||
| } | |||
| @@ -316,4 +316,8 @@ fun checkJobOrderCreated(@Valid @RequestBody request: CheckJobOrderCreatedReques | |||
| return jobOrderService.checkJobOrderCreated(request) | |||
| } | |||
| */ | |||
| @PostMapping("/updateReqQty") | |||
| fun updateJoReqQty(@Valid @RequestBody request: UpdateJoReqQtyRequest): MessageResponse { | |||
| return jobOrderService.updateJoReqQty(request) | |||
| } | |||
| } | |||
| @@ -149,4 +149,8 @@ data class JobOrderInfoResponse( | |||
| val code: String, | |||
| val itemName: String, | |||
| val reqQty: BigDecimal, | |||
| ) | |||
| data class UpdateJoReqQtyRequest( | |||
| val id: Long, | |||
| val reqQty: Int | |||
| ) | |||
| @@ -14,4 +14,9 @@ interface WarehouseRepository : AbstractRepository<Warehouse, Long> { | |||
| fun findByCodeAndDeletedIsFalse(code: String): Warehouse?; | |||
| fun findAllByDeletedIsFalse(): List<Warehouse>; | |||
| fun findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection: String): List<Warehouse>; | |||
| fun findDistinctStockTakeSectionsByDeletedIsFalse(): List<String>; | |||
| fun findAllByIdIn(ids: List<Long>): List<Warehouse>; | |||
| fun findAllByCodeAndDeletedIsFalse(code: String): List<Warehouse> | |||
| } | |||
| @@ -67,10 +67,10 @@ fun findMaterialIssues(): List<PickExecutionIssue> | |||
| fun getBadItemList_statusIn(@Param("statuses") statuses: List<PickExecutionIssueEnum>): List<PickExecutionIssue> | |||
| @Query(""" | |||
| SELECT p FROM PickExecutionIssue p | |||
| WHERE p.badItemQty IS NOT NULL | |||
| AND p.badItemQty > 0 | |||
| AND p.deleted = false | |||
| SELECT p FROM PickExecutionIssue p | |||
| WHERE (p.badItemQty IS NOT NULL AND p.badItemQty > 0) | |||
| OR (p.missQty IS NOT NULL AND p.missQty > 0) | |||
| AND p.deleted = false | |||
| ORDER BY p.created DESC | |||
| """) | |||
| fun getBadItemList(): List<PickExecutionIssue> | |||
| @@ -38,4 +38,17 @@ interface TruckRepository : AbstractRepository<Truck, Long> { | |||
| fun findAllByShopId(shopId :Long):List<Truck> | |||
| //fun findByTruckLanceCode(truckLanceCode: String):List<Truck> | |||
| //fun deleteByTruckLanceCodeAndDepartureTimeAndLoadingSequenceAndDistrictReferenceAndStoreId(truckLanceCode: String, departureTime: LocalTime, loadingSequence: Long, districtReference: Long, storeId: Long): String | |||
| @Query(""" | |||
| SELECT t FROM Truck t | |||
| WHERE t.shop.id = :shopId | |||
| AND t.storeId = :storeId | |||
| AND t.deleted = false | |||
| AND t.truckLanceCode LIKE CONCAT('%', :dayOfWeekAbbr, '%') | |||
| ORDER BY t.departureTime ASC | |||
| """) | |||
| fun findByShopIdAndStoreIdAndDayOfWeek( | |||
| @Param("shopId") shopId: Long, | |||
| @Param("storeId") storeId: String, | |||
| @Param("dayOfWeekAbbr") dayOfWeekAbbr: String | |||
| ): List<Truck> | |||
| } | |||
| @@ -53,5 +53,8 @@ open class ProductProcess : BaseEntity<Long>() { | |||
| @Column(name = "productionPriority") | |||
| open var productionPriority: Int? = 50 | |||
| @Column(name = "submitedBagRecord") | |||
| open var submitedBagRecord: Boolean? = false | |||
| } | |||
| @@ -39,7 +39,12 @@ open class ProductProcessLine : BaseEntity<Long>() { | |||
| @Column(name = "equipment_name", length = 100) | |||
| open var equipmentType: String? = null | |||
| // 添加 @ManyToOne | |||
| @Column(name = "processingTime") | |||
| open var processingTime: Int? = null | |||
| @Column(name = "setupTime") | |||
| open var setupTime: Int? = null | |||
| @Column(name = "changeoverTime") | |||
| open var changeoverTime: Int? = null | |||
| @ManyToOne(fetch = FetchType.LAZY) | |||
| @JoinColumn(name = "byproductId") | |||
| open var byproduct: Items? = null | |||
| @@ -5,7 +5,7 @@ import java.time.LocalDate | |||
| import com.ffii.fpsms.modules.productProcess.enums.ProductProcessStatus | |||
| import com.fasterxml.jackson.annotation.JsonFormat | |||
| import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus | |||
| import java.math.BigDecimal | |||
| data class ProductProcessInfo( | |||
| val id: Long?, | |||
| val productProcessCode: String?, | |||
| @@ -33,11 +33,13 @@ data class ProductProcessInfo( | |||
| val itemId: Long?, | |||
| val itemCode: String?, | |||
| val itemName: String?, | |||
| val bomBaseQty: BigDecimal?, | |||
| val outputQtyUom: String?, | |||
| val outputQty: Int?, | |||
| val timeSequence: Int?, | |||
| val complexity: Int?, | |||
| val productionPriority: Int?, | |||
| val submitedBagRecord: Boolean?, | |||
| val productProcessLines: List<ProductProcessLineInfo>?, | |||
| val totalStockQty: Int?, | |||
| val insufficientStockQty: Int?, | |||
| @@ -76,7 +76,7 @@ open class ProductProcessService( | |||
| ) { | |||
| open fun findAll(pageable: Pageable): Page<ProductProcess> { | |||
| println("📋 Service: Finding all ProductProcess with page: ${pageable.pageNumber}, size: ${pageable.pageSize}") | |||
| println(" Service: Finding all ProductProcess with page: ${pageable.pageNumber}, size: ${pageable.pageSize}") | |||
| val result = productProcessRepository.findAll(pageable) | |||
| println(" Service: Found ${result.totalElements} records") | |||
| return result | |||
| @@ -161,7 +161,7 @@ open class ProductProcessService( | |||
| // 添加:查询工序的所有步骤 | |||
| open fun getLines(productProcessId: Long): List<ProductProcessLine> { | |||
| println("📋 Service: Getting lines for process ID: $productProcessId") | |||
| println(" Service: Getting lines for process ID: $productProcessId") | |||
| val lines = productProcessLineRepository.findByProductProcess_Id(productProcessId) | |||
| println(" Service: Found ${lines.size} lines") | |||
| return lines | |||
| @@ -268,7 +268,7 @@ open class ProductProcessService( | |||
| } | |||
| open fun getIssues(productProcessId: Long): List<ProductionProcessIssue> { | |||
| println("📋 Service: Getting issues for ProductProcess ID: $productProcessId") | |||
| println(" Service: Getting issues for ProductProcess ID: $productProcessId") | |||
| val issues = productionProcessIssueRepository.findByProductProcess_Id(productProcessId) | |||
| println(" Service: Found ${issues.size} issues") | |||
| return issues | |||
| @@ -317,7 +317,7 @@ open class ProductProcessService( | |||
| return result | |||
| } | |||
| open fun findAllAsDto(pageable: Pageable): Page<ProductProcessSimpleResponse> { | |||
| println("📋 Service: Finding all ProductProcess as DTO with page: ${pageable.pageNumber}, size: ${pageable.pageSize}") | |||
| println(" Service: Finding all ProductProcess as DTO with page: ${pageable.pageNumber}, size: ${pageable.pageSize}") | |||
| val entityPage = productProcessRepository.findAll(pageable) | |||
| val dtoList = entityPage.content.map { entity -> | |||
| @@ -580,6 +580,7 @@ open class ProductProcessService( | |||
| itemName = bom?.item?.name?:"", | |||
| timeSequence = bom?.timeSequence?:0, | |||
| complexity = bom?.complexity?:0, | |||
| bomBaseQty = bom.outputQty ?: BigDecimal.ZERO, | |||
| isDark = calculateColourScore(bom?.isDark?:0), | |||
| isDense = bom?.isDense?:0, | |||
| isFloat = calculateFloatScore(bom?.isFloat?:0), | |||
| @@ -594,7 +595,8 @@ open class ProductProcessService( | |||
| startTime = process.startTime?:LocalDateTime.now(), | |||
| endTime = process.endTime?:LocalDateTime.now(), | |||
| date = process.date?:LocalDate.now(), | |||
| productionPriority = process.productionPriority?:50, | |||
| productionPriority = process.productionPriority?:50, | |||
| submitedBagRecord = process.submitedBagRecord?:false, | |||
| totalStockQty = totalStockQty, | |||
| insufficientStockQty = insufficientStockQty, | |||
| sufficientStockQty = sufficientStockQty, | |||
| @@ -617,9 +619,9 @@ open class ProductProcessService( | |||
| equipment_name = line.equipmentType?:"", | |||
| equipmentDetailCode = equipmentDetail?.code?:"", | |||
| status = line.status?:"", | |||
| durationInMinutes = line.bomProcess?.durationInMinute?:0, | |||
| prepTimeInMinutes = line.bomProcess?.prepTimeInMinute?:0, | |||
| postProdTimeInMinutes = line.bomProcess?.postProdTimeInMinute?:0, | |||
| durationInMinutes = line.processingTime?:0, | |||
| prepTimeInMinutes = line.setupTime?:0, | |||
| postProdTimeInMinutes = line.changeoverTime?:0, | |||
| byproductId = line.byproduct?.id?:0, | |||
| byproductName = line.byproduct?.name?:"", | |||
| byproductQty = line.byproductQty?:0, | |||
| @@ -640,7 +642,7 @@ open class ProductProcessService( | |||
| endTime = line.endTime | |||
| ) | |||
| }, | |||
| }.sortedBy { it.seqNo }, | |||
| jobOrderLines = bomMaterials.map { line -> | |||
| val itemId = line.item?.id ?: 0L | |||
| val stockQty = stockQtyMap[itemId]?.toInt() ?: 0 | |||
| @@ -719,6 +721,9 @@ open class ProductProcessService( | |||
| this.description = bomProcess.description?:"" | |||
| this.equipmentType = equipment?.code?:"" | |||
| this.status = "Pending" | |||
| this.processingTime = bomProcess.durationInMinute | |||
| this.setupTime = bomProcess.prepTimeInMinute | |||
| this.changeoverTime = bomProcess.postProdTimeInMinute | |||
| } | |||
| productProcessLineRepository.save(productProcessLine) | |||
| } | |||
| @@ -921,7 +926,7 @@ open class ProductProcessService( | |||
| } | |||
| open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse { | |||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | |||
| val productProcess = productProcessRepository.findById(productProcessLine?.productProcess?.id?:0L).orElse(null) | |||
| val bomProcessId = productProcessLine?.bomProcess?.id | |||
| println("bomProcessId ${bomProcessId}") | |||
| val bomProcess: BomProcess? = bomProcessId?.let { | |||
| @@ -961,6 +966,7 @@ open class ProductProcessService( | |||
| startTime = productProcessLine.startTime?:LocalDateTime.now(), | |||
| endTime = productProcessLine.endTime?:LocalDateTime.now(), | |||
| stopTime = productProcessIssue?.stopTime, | |||
| submitedBagRecord = productProcess.submitedBagRecord?:false, | |||
| // ✅ 添加总暂停时间(毫秒) | |||
| totalPausedTimeMs = totalPausedTimeMs.toLong(), | |||
| status = productProcessLine.status?:"", | |||
| @@ -985,13 +991,13 @@ open class ProductProcessService( | |||
| } | |||
| open fun updateProductProcessLineStatus(productProcessLineId: Long, status: String): MessageResponse { | |||
| println("📋 Service: Updating ProductProcessLine Status: $productProcessLineId") | |||
| println(" Service: Updating ProductProcessLine Status: $productProcessLineId") | |||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | |||
| println("📋 Service: ProductProcessLine: $productProcessLine") | |||
| println(" Service: ProductProcessLine: $productProcessLine") | |||
| productProcessLine.status = status | |||
| // productProcessLine.endTime = LocalDateTime.now() | |||
| productProcessLineRepository.save(productProcessLine) | |||
| println("📋 Service: ProductProcessLine Status Updated: ${productProcessLine.status}") | |||
| println(" Service: ProductProcessLine Status Updated: ${productProcessLine.status}") | |||
| CompleteProductProcessStatusIfAllLinesCompleted(productProcessLine.productProcess?.id?:0) | |||
| @@ -1008,8 +1014,8 @@ open class ProductProcessService( | |||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | |||
| val productProcess = productProcessRepository.findById(productProcessLine?.productProcess?.id?:0L).orElse(null) | |||
| // 如果 startTime 为空,才设置它(保留已存在的 startTime) | |||
| println("📋 Service: ProductProcessLine StartTime: ${productProcessLine.startTime}") | |||
| println("📋 Service: ProductProcess StartTime: ${productProcess.startTime}") | |||
| println(" Service: ProductProcessLine StartTime: ${productProcessLine.startTime}") | |||
| println(" Service: ProductProcess StartTime: ${productProcess.startTime}") | |||
| if (productProcessLine.startTime == null) { | |||
| productProcessLine.startTime = LocalDateTime.now() | |||
| productProcessLineRepository.save(productProcessLine) | |||
| @@ -1022,7 +1028,7 @@ open class ProductProcessService( | |||
| // 总是设置 endTime(因为 Pass 意味着完成) | |||
| productProcessLine.endTime = LocalDateTime.now() | |||
| productProcessLineRepository.save(productProcessLine) | |||
| println("📋 Service: ProductProcessLine EndTime: ${productProcessLine.endTime}") | |||
| println(" Service: ProductProcessLine EndTime: ${productProcessLine.endTime}") | |||
| // 更新状态为 "Pass" | |||
| updateProductProcessLineStatus(productProcessLineId, "Pass") | |||
| @@ -1042,20 +1048,20 @@ open class ProductProcessService( | |||
| } | |||
| open fun CompleteProductProcessStatusIfAllLinesCompleted(productProcessId: Long): MessageResponse { | |||
| val productProcess = productProcessRepository.findById(productProcessId).orElse(null) | |||
| println("📋 Service: ProductProcess: $productProcess") | |||
| println(" Service: ProductProcess: $productProcess") | |||
| val productProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) | |||
| println("📋 Service: ProductProcessLines: $productProcessLines") | |||
| println(" Service: ProductProcessLines: $productProcessLines") | |||
| if(productProcessLines.all { it.status == "Completed" || it.status == "Pass" }) { | |||
| productProcess.status = ProductProcessStatus.COMPLETED | |||
| if (productProcess.endTime == null) { | |||
| productProcess.endTime = LocalDateTime.now() | |||
| } | |||
| productProcessRepository.save(productProcess) | |||
| println("📋 Service: ProductProcess Status Updated: ${productProcess.status}") | |||
| println(" Service: ProductProcess Status Updated: ${productProcess.status}") | |||
| } | |||
| else { | |||
| println("📋 Service: ProductProcess Lines are not completed") | |||
| println(" Service: ProductProcess Lines are not completed") | |||
| } | |||
| return MessageResponse( | |||
| id = null, | |||
| @@ -1296,9 +1302,9 @@ open class ProductProcessService( | |||
| } | |||
| else{ | |||
| updateProductProcessLineStatus(productProcessLineId, "InProgress") | |||
| println("📋 Service: ProductProcess Lines are not Pending") | |||
| println("📋 Service: ProductProcess Lines: ${allproductProcessLines.map { it.status }}") | |||
| println("📋 Service: ProductProcess Line: ${productProcessLine.status}") | |||
| println(" Service: ProductProcess Lines are not Pending") | |||
| println(" Service: ProductProcess Lines: ${allproductProcessLines.map { it.status }}") | |||
| println(" Service: ProductProcess Line: ${productProcessLine.status}") | |||
| } | |||
| return MessageResponse( | |||
| id = productProcessLine.id, | |||
| @@ -1390,16 +1396,16 @@ open class ProductProcessService( | |||
| } | |||
| open fun SaveProductProcessIssueTime(request: SaveProductProcessIssueTimeRequest): MessageResponse { | |||
| println("📋 Service: Saving ProductProcess Issue Time: ${request.productProcessLineId}") | |||
| val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null) | |||
| val productProcess = productProcessRepository.findById(productProcessLine.productProcess?.id?:0L).orElse(null) | |||
| //val productProcessLines=productProcessLineRepository.findByProductProcess_Id(productProcessId) | |||
| val startTime=productProcessLine?.startTime | |||
| println("📋 Service: Start Time: $startTime") | |||
| val stopTime=LocalDateTime.now() | |||
| println("📋 Service: Stop Time: $stopTime") | |||
| val operatorId=productProcessLine.operator?.id | |||
| val Operator=userRepository.findById(operatorId).orElse(null) | |||
| val reason = request.reason | |||
| @@ -1429,13 +1435,13 @@ open class ProductProcessService( | |||
| open fun SaveProductProcessResumeTime(productProcessIssueId: Long): MessageResponse { | |||
| println("📋 Service: Saving ProductProcess Resume Time: $productProcessIssueId") | |||
| println(" Service: Saving ProductProcess Resume Time: $productProcessIssueId") | |||
| val productProcessLineIssue = productionProcessIssueRepository.findById(productProcessIssueId).orElse(null) | |||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineIssue.productProcessLineId?:0L).orElse(null) | |||
| val resumeTime = LocalDateTime.now() | |||
| println("📋 Service: Resume Time: $resumeTime") | |||
| println(" Service: Resume Time: $resumeTime") | |||
| productProcessLineIssue?.resumeTime = resumeTime | |||
| println("📋 Service: Resume Time: $resumeTime") | |||
| println(" Service: Resume Time: $resumeTime") | |||
| val totalTime = resumeTime.toEpochSecond(ZoneOffset.UTC) - (productProcessLineIssue.stopTime?.toEpochSecond(ZoneOffset.UTC) ?: 0L) | |||
| productProcessLineIssue?.totalTime = totalTime.toInt() | |||
| productProcessLineIssue?.status = "Resumed" | |||
| @@ -1453,7 +1459,32 @@ open class ProductProcessService( | |||
| ) | |||
| } | |||
| open fun UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest): MessageResponse { | |||
| val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null) | |||
| val processingTime = request.processingTime | |||
| val setupTime = request.setupTime | |||
| val changeoverTime = request.changeoverTime | |||
| if(processingTime != null) { | |||
| productProcessLine.processingTime = processingTime | |||
| productProcessLineRepository.save(productProcessLine) | |||
| } | |||
| if(setupTime != null) { | |||
| productProcessLine.setupTime = setupTime | |||
| productProcessLineRepository.save(productProcessLine) | |||
| } | |||
| if(changeoverTime != null) { | |||
| productProcessLine.changeoverTime = changeoverTime | |||
| productProcessLineRepository.save(productProcessLine) | |||
| } | |||
| return MessageResponse( | |||
| id = request.productProcessLineId, | |||
| code = "200", | |||
| name = "ProductProcess ProcessingTimeSetupTimeChangeoverTime Updated", | |||
| type = "success", | |||
| message = "ProductProcess ProcessingTimeSetupTimeChangeoverTime Updated", | |||
| errorPosition = null, | |||
| ) | |||
| } | |||
| open fun UpdateProductProcessPriority(productProcessId: Long, productionPriority: Int): MessageResponse { | |||
| val productProcess = productProcessRepository.findById(productProcessId).orElse(null) | |||
| productProcess.productionPriority = productionPriority | |||
| @@ -1467,5 +1498,94 @@ open class ProductProcessService( | |||
| errorPosition = null, | |||
| ) | |||
| } | |||
| } | |||
| open fun createNewProductProcessLine(productProcessLineId: Long): MessageResponse { | |||
| val sourceLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | |||
| ?: return MessageResponse( | |||
| id = productProcessLineId, | |||
| code = "404", | |||
| name = "ProductProcess Line Not Found", | |||
| type = "error", | |||
| message = "ProductProcess Line with ID $productProcessLineId not found", | |||
| errorPosition = null, | |||
| ) | |||
| val productProcessId = sourceLine.productProcess?.id ?: return MessageResponse( | |||
| id = productProcessLineId, | |||
| code = "400", | |||
| name = "Invalid ProductProcess", | |||
| type = "error", | |||
| message = "ProductProcess Line has no associated ProductProcess", | |||
| errorPosition = null, | |||
| ) | |||
| val originalSeqNo = sourceLine.seqNo ?: return MessageResponse( | |||
| id = productProcessLineId, | |||
| code = "400", | |||
| name = "Invalid SeqNo", | |||
| type = "error", | |||
| message = "ProductProcess Line has no seqNo", | |||
| errorPosition = null, | |||
| ) | |||
| // 先获取同一 productProcess 的所有 lines(在更新前) | |||
| val allLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) | |||
| // 更新所有其他 seqNo >= originalSeqNo + 1 的 lines(不包括原 line) | |||
| allLines.filter { | |||
| it.id != sourceLine.id && | |||
| it.seqNo != null && | |||
| it.seqNo!! >= originalSeqNo + 1 | |||
| }.forEach { line -> | |||
| line.seqNo = (line.seqNo ?: 0) + 1 | |||
| productProcessLineRepository.save(line) | |||
| } | |||
| // 创建新的 line,复制所有字段 | |||
| val newLine = ProductProcessLine().apply { | |||
| // 复制基本字段 | |||
| this.productProcess = sourceLine.productProcess | |||
| this.bomProcess = sourceLine.bomProcess | |||
| this.operator = sourceLine.operator | |||
| this.equipment = sourceLine.equipment | |||
| this.equipmentDetailId = sourceLine.equipmentDetailId | |||
| this.handler = sourceLine.handler | |||
| this.seqNo = originalSeqNo + 1 // 新 line 的 seqNo = originalSeqNo + 1 | |||
| this.name = sourceLine.name | |||
| this.description = sourceLine.description | |||
| this.equipmentType = sourceLine.equipmentType | |||
| this.status = "Pending" // 新创建的 line 状态总是设为 Pending | |||
| this.byproduct = sourceLine.byproduct | |||
| this.byproductName = sourceLine.byproductName | |||
| this.byproductQty = sourceLine.byproductQty | |||
| this.byproductUom = sourceLine.byproductUom | |||
| this.scrapQty = sourceLine.scrapQty | |||
| this.scrapUom = sourceLine.scrapUom | |||
| this.defectQty = sourceLine.defectQty | |||
| this.defectUom = sourceLine.defectUom | |||
| this.defectDescription = sourceLine.defectDescription | |||
| this.defectQty2 = sourceLine.defectQty2 | |||
| this.defectUom2 = sourceLine.defectUom2 | |||
| this.defectDescription2 = sourceLine.defectDescription2 | |||
| this.defectQty3 = sourceLine.defectQty3 | |||
| this.defectUom3 = sourceLine.defectUom3 | |||
| this.defectDescription3 = sourceLine.defectDescription3 | |||
| this.outputFromProcessQty = sourceLine.outputFromProcessQty | |||
| this.outputFromProcessUom = sourceLine.outputFromProcessUom | |||
| // 不复制时间字段,新 line 应该没有开始和结束时间 | |||
| this.startTime = null | |||
| this.endTime = null | |||
| } | |||
| // 保存新 line(原 line 的 seqNo 保持不变,不需要更新) | |||
| val savedNewLine = productProcessLineRepository.save(newLine) | |||
| return MessageResponse( | |||
| id = savedNewLine.id ?: productProcessLineId, | |||
| code = "200", | |||
| name = "ProductProcess Line Created", | |||
| type = "success", | |||
| message = "ProductProcess Line Created successfully with seqNo ${originalSeqNo + 1}", | |||
| errorPosition = null, | |||
| ) | |||
| }} | |||
| @@ -217,4 +217,12 @@ class ProductProcessController( | |||
| fun passProductProcessLine(@PathVariable lineId: Long): MessageResponse { | |||
| return productProcessService.passProductProcessLine(lineId) | |||
| } | |||
| @PostMapping("/Demo/ProcessLine/new/{lineId}") | |||
| fun newProductProcessLine(@PathVariable lineId: Long): MessageResponse { | |||
| return productProcessService.createNewProductProcessLine(lineId) | |||
| } | |||
| @PostMapping("/Demo/ProcessLine/update/processingTimeSetupTimeChangeoverTime/{lineId}") | |||
| fun updateProductProcessLineProcessingTimeSetupTimeChangeoverTime(@PathVariable lineId: Long, @RequestBody request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest): MessageResponse { | |||
| return productProcessService.UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request) | |||
| } | |||
| } | |||
| @@ -152,7 +152,8 @@ data class JobOrderProcessLineDetailResponse( | |||
| val byproductId: Long?, | |||
| val byproductName: String?, | |||
| val byproductQty: Int?, | |||
| val byproductUom: String? | |||
| val byproductUom: String?, | |||
| val submitedBagRecord: Boolean? | |||
| ) | |||
| data class AllJoborderProductProcessInfoResponse( | |||
| @@ -207,4 +208,14 @@ data class SaveProductProcessIssueTimeRequest( | |||
| data class SaveProductProcessResumeTimeRequest( | |||
| val productProcessLineId: Long, | |||
| val resumeTime: LocalDateTime? | |||
| ) | |||
| data class UpdateJoReqQtyRequest( | |||
| val id: Long, | |||
| val reqQty: Int | |||
| ) | |||
| data class UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest( | |||
| val productProcessLineId: Long, | |||
| val processingTime: Int?, | |||
| val setupTime: Int?, | |||
| val changeoverTime: Int? | |||
| ) | |||
| @@ -52,4 +52,15 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||
| AND ill.deleted = false | |||
| """) | |||
| fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLotLine? | |||
| // InventoryLotLineRepository.kt 中添加 | |||
| @Query("SELECT ill FROM InventoryLotLine ill WHERE ill.warehouse.id IN :warehouseIds AND ill.deleted = false") | |||
| fun findAllByWarehouseIdInAndDeletedIsFalse(@Param("warehouseIds") warehouseIds: List<Long>): List<InventoryLotLine> | |||
| @Query(""" | |||
| SELECT ill FROM InventoryLotLine ill | |||
| WHERE ill.warehouse.code = :warehouseCode | |||
| AND ill.deleted = false | |||
| ORDER BY ill.inventoryLot.item.code, ill.inventoryLot.lotNo | |||
| """) | |||
| fun findAllByWarehouseCodeAndDeletedIsFalse(@Param("warehouseCode") warehouseCode: String): List<InventoryLotLine> | |||
| } | |||
| @@ -23,4 +23,5 @@ interface InventoryLotRepository: AbstractRepository<InventoryLot, Long> { | |||
| AND il.deleted = false | |||
| """) | |||
| fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLot? | |||
| fun findByIdAndDeletedFalse(id: Serializable): InventoryLot? | |||
| } | |||
| @@ -39,4 +39,7 @@ open class StockTake: BaseEntity<Long>() { | |||
| @Size(max = 500) | |||
| @Column(name = "remarks", length = 500) | |||
| open var remarks: String? = null | |||
| @Size(max = 255) | |||
| @Column(name = "stockTakeSection", length = 255) | |||
| open var stockTakeSection: String? = null | |||
| } | |||
| @@ -7,4 +7,5 @@ import java.io.Serializable | |||
| @Repository | |||
| interface StockTakeLineRepository : AbstractRepository<StockTakeLine, Long> { | |||
| fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeLine?; | |||
| fun findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId: Serializable, stockTakeId: Serializable): StockTakeLine?; | |||
| } | |||
| @@ -0,0 +1,102 @@ | |||
| // StockTakeRecord.kt | |||
| package com.ffii.fpsms.modules.stock.entity | |||
| import com.ffii.core.entity.BaseEntity | |||
| import com.ffii.fpsms.modules.master.entity.Warehouse | |||
| import jakarta.persistence.* | |||
| import java.math.BigDecimal | |||
| import java.time.LocalDate | |||
| import java.time.LocalDateTime | |||
| @Entity | |||
| @Table(name = "stocktakerecord") | |||
| open class StockTakeRecord : BaseEntity<Long>() { | |||
| @Column(name = "itemId", nullable = false) | |||
| open var itemId: Long? = null | |||
| @Column(name = "lotId", nullable = false) | |||
| open var lotId: Long? = null | |||
| @ManyToOne | |||
| @JoinColumn(name = "warehouseId", nullable = false) | |||
| open var warehouse: Warehouse? = null | |||
| @Column(name = "stockTakeSection", length = 255) | |||
| open var stockTakeSection: String? = null | |||
| @ManyToOne | |||
| @JoinColumn(name = "stockTakeId", nullable = false) | |||
| open var stockTake: StockTake? = null | |||
| @Column(name = "approverId") | |||
| open var approverId: Long? = null | |||
| @Column(name = "approverName", length = 100) | |||
| open var approverName: String? = null | |||
| @Column(name = "stockTakerId", nullable = false) | |||
| open var stockTakerId: Long? = null | |||
| @Column(name = "stockTakerName", length = 100) | |||
| open var stockTakerName: String? = null | |||
| @Column(name = "pickerFirstStockTakeQty", precision = 14, scale = 2) | |||
| open var pickerFirstStockTakeQty: BigDecimal? = null | |||
| @Column(name = "pickerSecondStockTakeQty", precision = 14, scale = 2) | |||
| open var pickerSecondStockTakeQty: BigDecimal? = null | |||
| @Column(name = "approverStockTakeQty", precision = 14, scale = 2) | |||
| open var approverStockTakeQty: BigDecimal? = null | |||
| @Column(name = "approverSecondStockTakeQty", precision = 14, scale = 2) | |||
| open var approverSecondStockTakeQty: BigDecimal? = null | |||
| @Column(name = "bookQty", nullable = false, precision = 14, scale = 2) | |||
| open var bookQty: BigDecimal? = null | |||
| @Column(name = "badQty", precision = 14, scale = 2) | |||
| open var badQty: BigDecimal? = null | |||
| @Column(name = "pickerFirstBadQty", precision = 14, scale = 2) | |||
| open var pickerFirstBadQty: BigDecimal? = null | |||
| @Column(name = "pickerSecondBadQty", precision = 14, scale = 2) | |||
| open var pickerSecondBadQty: BigDecimal? = null | |||
| @Column(name = "approverBadQty", precision = 14, scale = 2) | |||
| open var approverBadQty: BigDecimal? = null | |||
| @Column(name = "varianceQty", precision = 14, scale = 2) | |||
| open var varianceQty: BigDecimal? = null | |||
| @Column(name = "uom", length = 30) | |||
| open var uom: String? = null | |||
| @Column(name = "stockTakeStartTime") | |||
| open var stockTakeStartTime: LocalDateTime? = null | |||
| @Column(name = "stockTakeEndTime") | |||
| open var stockTakeEndTime: LocalDateTime? = null | |||
| @Column(name = "date", nullable = false) | |||
| open var date: LocalDate? = null | |||
| @Column(name = "status", nullable = false, length = 30) | |||
| open var status: String? = null | |||
| @Column(name = "remarks", length = 500) | |||
| open var remarks: String? = null | |||
| @Column(name = "itemCode", length = 50) | |||
| open var itemCode: String? = null | |||
| @Column(name = "itemName", length = 200) | |||
| open var itemName: String? = null | |||
| @Column(name = "inventoryLotId") | |||
| open var inventoryLotId: Long? = null | |||
| @Column(name = "lotNo", length = 512) | |||
| open var lotNo: String? = null | |||
| @Column(name = "expiredDate") | |||
| open var expiredDate: LocalDate? = null | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| // 创建 StockTakeRecordRepository.kt | |||
| package com.ffii.fpsms.modules.stock.entity | |||
| import com.ffii.core.support.AbstractRepository | |||
| import org.springframework.stereotype.Repository | |||
| import java.io.Serializable | |||
| import org.springframework.data.jpa.repository.Query | |||
| @Repository | |||
| interface StockTakeRecordRepository : AbstractRepository<StockTakeRecord, Long> { | |||
| fun findAllByStockTakeIdAndDeletedIsFalse(stockTakeId: Long): List<StockTakeRecord>; | |||
| fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeRecord?; | |||
| @Query(""" | |||
| SELECT w.stockTakeSection, str.stockTake.id, COUNT(str.id) as count | |||
| FROM StockTakeRecord str | |||
| INNER JOIN Warehouse w ON str.warehouse.id = w.id | |||
| WHERE str.deleted = false | |||
| AND w.deleted = false | |||
| AND w.stockTakeSection IS NOT NULL | |||
| AND w.stockTakeSection != '' | |||
| AND str.stockTake.id IS NOT NULL | |||
| GROUP BY w.stockTakeSection, str.stockTake.id | |||
| """) | |||
| fun countStockTakeRecordsBySectionAndStockTakeId(): List<Array<Any>> | |||
| } | |||
| @@ -3,5 +3,6 @@ package com.ffii.fpsms.modules.stock.enums | |||
| enum class StockTakeStatus(val value: String) { | |||
| PENDING("pending"), | |||
| STOCKTAKING("stockTaking"), | |||
| APPROVING("approving"), | |||
| COMPLETED("completed"), | |||
| } | |||
| @@ -48,7 +48,7 @@ class StockTakeService( | |||
| fun saveStockTake(request: SaveStockTakeRequest): StockTake { | |||
| val stockTake = request.id?.let { stockTakeRepository.findByIdAndDeletedIsFalse(it) } ?: StockTake(); | |||
| val status = request.status?.let { _status -> StockTakeStatus.entries.find { it.value == _status } } | |||
| request.code?.let { stockTake.code = it } | |||
| if (stockTake.code == null) { | |||
| stockTake.code = assignStockTakeNo() | |||
| @@ -59,7 +59,8 @@ class StockTakeService( | |||
| request.actualEnd?.let { stockTake.actualEnd = it } | |||
| status?.let { stockTake.status = it } | |||
| request.remarks?.let { stockTake.remarks = it } | |||
| request.stockTakeSection?.let { stockTake.stockTakeSection = it } // 添加此行 | |||
| return stockTakeRepository.save(stockTake); | |||
| } | |||
| @@ -230,4 +231,67 @@ class StockTakeService( | |||
| logger.info("--------- End - Import Stock Take Excel -------") | |||
| return "Import Excel success"; | |||
| } | |||
| fun createStockTakeForSections(): Map<String, String> { | |||
| logger.info("--------- Start - Create Stock Take for Sections -------") | |||
| val result = mutableMapOf<String, String>() | |||
| // 1. 获取所有不同的 stockTakeSection(从 warehouse 表) | |||
| val allWarehouses = warehouseRepository.findAllByDeletedIsFalse() | |||
| val distinctSections = allWarehouses | |||
| .mapNotNull { it.stockTakeSection } | |||
| .distinct() | |||
| .filter { !it.isBlank() } | |||
| // 2. 获取所有 stock_take 记录(按 stockTakeSection 分组) | |||
| val allStockTakes = stockTakeRepository.findAll() | |||
| .filter { !it.deleted } | |||
| .groupBy { it.stockTakeSection } | |||
| // 3. 为每个 stockTakeSection 检查并创建 | |||
| distinctSections.forEach { section -> | |||
| val stockTakesForSection = allStockTakes[section] ?: emptyList() | |||
| // 检查:如果该 section 的所有记录都是 COMPLETED,才创建新的 | |||
| val allCompleted = stockTakesForSection.isEmpty() || | |||
| stockTakesForSection.all { it.status == StockTakeStatus.COMPLETED } | |||
| if (allCompleted) { | |||
| try { | |||
| val now = LocalDateTime.now() | |||
| val code = assignStockTakeNo() | |||
| val saveStockTakeReq = SaveStockTakeRequest( | |||
| code = code, | |||
| planStart = now, | |||
| planEnd = now.plusDays(1), | |||
| actualStart = null, | |||
| actualEnd = null, | |||
| status = StockTakeStatus.PENDING.value, | |||
| remarks = null, | |||
| stockTakeSection = section | |||
| ) | |||
| val savedStockTake = saveStockTake(saveStockTakeReq) | |||
| result[section] = "Created: ${savedStockTake.code}" | |||
| logger.info("Created stock take for section $section: ${savedStockTake.code}") | |||
| } catch (e: Exception) { | |||
| result[section] = "Error: ${e.message}" | |||
| logger.error("Error creating stock take for section $section: ${e.message}") | |||
| } | |||
| } else { | |||
| result[section] = "Skipped: Has non-completed records" | |||
| logger.info("Skipped section $section: Has non-completed records") | |||
| } | |||
| } | |||
| // 移除 null section 处理逻辑,因为 warehouse 表中没有 null 的 stockTakeSection | |||
| logger.info("--------- End - Create Stock Take for Sections -------") | |||
| return result | |||
| } | |||
| } | |||
| @@ -40,4 +40,10 @@ class StockTakeController( | |||
| return ResponseEntity.ok(stockTakeService.importExcel(workbook)) | |||
| } | |||
| @PostMapping("/createForSections") | |||
| fun createStockTakeForSections(): ResponseEntity<Map<String, String>> { | |||
| val result = stockTakeService.createStockTakeForSections() | |||
| return ResponseEntity.ok(result) | |||
| } | |||
| } | |||
| @@ -0,0 +1,180 @@ | |||
| package com.ffii.fpsms.modules.stock.web | |||
| import com.ffii.fpsms.modules.stock.service.StockTakeRecordService | |||
| import org.springframework.web.bind.annotation.GetMapping | |||
| import org.springframework.web.bind.annotation.PostMapping | |||
| import org.springframework.web.bind.annotation.RequestMapping | |||
| import org.springframework.web.bind.annotation.RestController | |||
| import org.springframework.web.bind.annotation.RequestParam | |||
| import org.springframework.web.bind.annotation.RequestBody | |||
| import org.springframework.http.ResponseEntity | |||
| import com.ffii.fpsms.modules.stock.web.model.* | |||
| import org.slf4j.LoggerFactory | |||
| @RestController | |||
| @RequestMapping("/stockTakeRecord") | |||
| class StockTakeRecordController( | |||
| private val stockOutRecordService: StockTakeRecordService | |||
| ) { | |||
| private val logger = LoggerFactory.getLogger(StockTakeRecordController::class.java) | |||
| @GetMapping("/AllPickedStockOutRecordList") | |||
| fun AllPickedStockOutRecordList(): List<AllPickedStockTakeListReponse> { | |||
| return stockOutRecordService.AllPickedStockTakeList() | |||
| } | |||
| @GetMapping("/AllApproverStockTakeList") | |||
| fun AllApproverStockTakeList(): List<AllPickedStockTakeListReponse> { | |||
| return stockOutRecordService.AllApproverStockTakeList() | |||
| } | |||
| @GetMapping("/inventoryLotDetailsBySectionNotMatch") | |||
| fun getInventoryLotDetailsByStockTakeSectionNotMatch( | |||
| @RequestParam stockTakeSection: String, | |||
| @RequestParam(required = false) stockTakeId: Long? | |||
| ): List<InventoryLotDetailResponse> { | |||
| return stockOutRecordService.getInventoryLotDetailsByStockTakeSectionNotMatch(stockTakeSection, stockTakeId) | |||
| } | |||
| @GetMapping("/inventoryLotDetails") | |||
| fun getInventoryLotDetailsByWarehouseCode( | |||
| @RequestParam warehouseCode: String | |||
| ): List<InventoryLotDetailResponse> { | |||
| return stockOutRecordService.getInventoryLotDetailsByWarehouseCode(warehouseCode) | |||
| } | |||
| @GetMapping("/inventoryLotDetailsBySection") | |||
| fun getInventoryLotDetailsByStockTakeSection( | |||
| @RequestParam stockTakeSection: String, | |||
| @RequestParam(required = false) stockTakeId: Long? | |||
| ): List<InventoryLotDetailResponse> { | |||
| return stockOutRecordService.getInventoryLotDetailsByStockTakeSection(stockTakeSection, stockTakeId) | |||
| } | |||
| @PostMapping("/saveStockTakeRecord") | |||
| fun saveStockTakeRecord( | |||
| @RequestBody request: SaveStockTakeRecordRequest, | |||
| @RequestParam stockTakeId: Long, | |||
| @RequestParam stockTakerId: Long | |||
| ): ResponseEntity<Any> { | |||
| return try { | |||
| val savedRecord = stockOutRecordService.saveStockTakeRecord(request, stockTakeId, stockTakerId) | |||
| logger.info("Successfully saved stock take record: ${savedRecord.id}") | |||
| ResponseEntity.ok(savedRecord) | |||
| } catch (e: IllegalArgumentException) { | |||
| logger.warn("Validation error: ${e.message}") | |||
| ResponseEntity.badRequest().body(mapOf( | |||
| "error" to "VALIDATION_ERROR", | |||
| "message" to (e.message ?: "Validation failed") | |||
| )) | |||
| } catch (e: Exception) { | |||
| logger.error("Error saving stock take record", e) | |||
| ResponseEntity.status(500).body(mapOf( | |||
| "error" to "INTERNAL_ERROR", | |||
| "message" to (e.message ?: "Failed to save stock take record") | |||
| )) | |||
| } | |||
| } | |||
| @PostMapping("/batchSaveStockTakeRecords") | |||
| fun batchSaveStockTakeRecords( | |||
| @RequestBody request: BatchSaveStockTakeRecordRequest | |||
| ): ResponseEntity<Any> { | |||
| return try { | |||
| val result = stockOutRecordService.batchSaveStockTakeRecords(request) | |||
| logger.info("Batch save completed: success=${result.successCount}, errors=${result.errorCount}") | |||
| ResponseEntity.ok(result) | |||
| } catch (e: IllegalArgumentException) { | |||
| logger.warn("Validation error: ${e.message}") | |||
| ResponseEntity.badRequest().body(mapOf( | |||
| "error" to "VALIDATION_ERROR", | |||
| "message" to (e.message ?: "Validation failed") | |||
| )) | |||
| } catch (e: Exception) { | |||
| logger.error("Error batch saving stock take records", e) | |||
| ResponseEntity.status(500).body(mapOf( | |||
| "error" to "INTERNAL_ERROR", | |||
| "message" to (e.message ?: "Failed to batch save stock take records") | |||
| )) | |||
| } | |||
| } | |||
| @GetMapping("/checkAndUpdateStockTakeStatus") | |||
| fun checkAndUpdateStockTakeStatus( | |||
| @RequestParam stockTakeId: Long, | |||
| @RequestParam stockTakeSection: String | |||
| ): ResponseEntity<Any> { | |||
| return try { | |||
| val result = stockOutRecordService.checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection) | |||
| ResponseEntity.ok(result) | |||
| } catch (e: Exception) { | |||
| logger.error("Error checking and updating stock take status", e) | |||
| ResponseEntity.status(500).body( | |||
| mapOf( | |||
| "error" to "INTERNAL_ERROR", | |||
| "message" to (e.message ?: "Failed to check and update stock take status") | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| @PostMapping("/saveApproverStockTakeRecord") | |||
| fun saveApproverStockTakeRecord( | |||
| @RequestBody request: SaveApproverStockTakeRecordRequest, | |||
| @RequestParam stockTakeId: Long | |||
| ): ResponseEntity<Any> { | |||
| return try { | |||
| val savedRecord = stockOutRecordService.saveApproverStockTakeRecord(request, stockTakeId) | |||
| logger.info("Successfully saved approver stock take record: ${savedRecord.id}") | |||
| ResponseEntity.ok(savedRecord) | |||
| } catch (e: IllegalArgumentException) { | |||
| logger.warn("Validation error: ${e.message}") | |||
| ResponseEntity.badRequest().body(mapOf( | |||
| "error" to "VALIDATION_ERROR", | |||
| "message" to (e.message ?: "Validation failed") | |||
| )) | |||
| } catch (e: Exception) { | |||
| logger.error("Error saving approver stock take record", e) | |||
| ResponseEntity.status(500).body(mapOf( | |||
| "error" to "INTERNAL_ERROR", | |||
| "message" to (e.message ?: "Failed to save approver stock take record") | |||
| )) | |||
| } | |||
| } | |||
| @PostMapping("/batchSaveApproverStockTakeRecords") | |||
| fun batchSaveApproverStockTakeRecords( | |||
| @RequestBody request: BatchSaveApproverStockTakeRecordRequest | |||
| ): ResponseEntity<Any> { | |||
| return try { | |||
| val result = stockOutRecordService.batchSaveApproverStockTakeRecords(request) | |||
| logger.info("Batch approver save completed: success=${result.successCount}, errors=${result.errorCount}") | |||
| ResponseEntity.ok(result) | |||
| } catch (e: IllegalArgumentException) { | |||
| logger.warn("Validation error: ${e.message}") | |||
| ResponseEntity.badRequest().body(mapOf( | |||
| "error" to "VALIDATION_ERROR", | |||
| "message" to (e.message ?: "Validation failed") | |||
| )) | |||
| } catch (e: Exception) { | |||
| logger.error("Error batch saving approver stock take records", e) | |||
| ResponseEntity.status(500).body(mapOf( | |||
| "error" to "INTERNAL_ERROR", | |||
| "message" to (e.message ?: "Failed to batch save approver stock take records") | |||
| )) | |||
| } | |||
| } | |||
| @PostMapping("/updateStockTakeRecordStatusToNotMatch") | |||
| fun updateStockTakeRecordStatusToNotMatch( | |||
| @RequestParam stockTakeRecordId: Long | |||
| ): ResponseEntity<Any> { | |||
| return try { | |||
| val result = stockOutRecordService.updateStockTakeRecordStatusToNotMatch(stockTakeRecordId) | |||
| ResponseEntity.ok(result) | |||
| } catch (e: Exception) { | |||
| logger.error("Error updating stock take record status to not match", e) | |||
| ResponseEntity.status(500).body(mapOf( | |||
| "error" to "INTERNAL_ERROR", | |||
| "message" to (e.message ?: "Failed to update stock take record status to not match") | |||
| )) | |||
| } | |||
| } | |||
| } | |||
| @@ -11,4 +11,5 @@ data class SaveStockTakeRequest( | |||
| var actualEnd: LocalDateTime?, | |||
| var status: String?, | |||
| val remarks: String?, | |||
| val stockTakeSection: String?=null, | |||
| ) | |||
| @@ -0,0 +1,90 @@ | |||
| // StockTakeRecordReponse.kt | |||
| package com.ffii.fpsms.modules.stock.web.model | |||
| import java.time.LocalDate | |||
| import java.math.BigDecimal | |||
| data class AllPickedStockTakeListReponse( | |||
| val id: Long, | |||
| val stockTakeSession: String, | |||
| val lastStockTakeDate: LocalDate?, | |||
| val status: String, | |||
| val currentStockTakeItemNumber: Int, | |||
| val totalInventoryLotNumber: Int, | |||
| val stockTakeId: Long, | |||
| ) | |||
| data class InventoryLotDetailResponse( | |||
| val id: Long, | |||
| val inventoryLotId: Long, | |||
| val itemId: Long, | |||
| val itemCode: String?, | |||
| val itemName: String?, | |||
| val lotNo: String?, | |||
| val expiryDate: LocalDate?, | |||
| val productionDate: java.time.LocalDateTime?, | |||
| val stockInDate: java.time.LocalDateTime?, | |||
| val inQty: BigDecimal?, | |||
| val outQty: BigDecimal?, | |||
| val holdQty: BigDecimal?, | |||
| val availableQty: BigDecimal?, | |||
| val uom: String?, | |||
| val warehouseCode: String?, | |||
| val warehouse: String?, | |||
| val warehouseSlot: String?, | |||
| val warehouseArea: String?, | |||
| val warehouseName: String?, | |||
| val varianceQty: BigDecimal? = null, | |||
| val status: String?, | |||
| val remarks: String?, | |||
| val stockTakeRecordStatus: String?, | |||
| val stockTakeRecordId: Long? = null, | |||
| val firstStockTakeQty: BigDecimal? = null, | |||
| val secondStockTakeQty: BigDecimal? = null, | |||
| val firstBadQty: BigDecimal? = null, | |||
| val secondBadQty: BigDecimal? = null, | |||
| val approverQty: BigDecimal? = null, | |||
| val approverBadQty: BigDecimal? = null, | |||
| val finalQty: BigDecimal? = null, | |||
| ) | |||
| data class InventoryLotLineListRequest( | |||
| val warehouseCode: String | |||
| ) | |||
| data class SaveStockTakeRecordRequest( | |||
| val stockTakeRecordId: Long? = null, // null = 创建,非 null = 更新 | |||
| val inventoryLotLineId: Long, // 创建时需要,用于识别 inventory lot line | |||
| val qty: BigDecimal, // QTY(第一次或第二次,后端判断) | |||
| val badQty: BigDecimal, | |||
| // val stockTakerName: String, | |||
| val remark: String? = null | |||
| ) | |||
| data class SaveApproverStockTakeRecordRequest( | |||
| val stockTakeRecordId: Long? = null, | |||
| val qty: BigDecimal, | |||
| val badQty: BigDecimal, | |||
| val approverId: Long? = null, | |||
| val approverQty: BigDecimal? = null, | |||
| val approverBadQty: BigDecimal? = null, | |||
| //val remark: String? = null | |||
| ) | |||
| data class BatchSaveStockTakeRecordRequest( | |||
| val stockTakeId: Long, | |||
| val stockTakeSection: String, | |||
| val stockTakerId: Long, | |||
| //val stockTakerName: String | |||
| ) | |||
| data class BatchSaveStockTakeRecordResponse( | |||
| val successCount: Int, | |||
| val errorCount: Int, | |||
| val errors: List<String> | |||
| ) | |||
| data class BatchSaveApproverStockTakeRecordRequest( | |||
| val stockTakeId: Long, | |||
| val stockTakeSection: String, | |||
| val approverId: Long, | |||
| ) | |||
| data class BatchSaveApproverStockTakeRecordResponse( | |||
| val successCount: Int, | |||
| val errorCount: Int, | |||
| val errors: List<String> | |||
| ) | |||
| @@ -0,0 +1,53 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset Enson:add_column | |||
| CREATE TABLE IF NOT EXISTS stockTakeRecord ( | |||
| id INT AUTO_INCREMENT PRIMARY KEY, | |||
| created DATETIME, | |||
| createdBy VARCHAR(30), | |||
| version INT, | |||
| modified DATETIME, | |||
| modifiedBy VARCHAR(30), | |||
| deleted TINYINT(1), | |||
| itemId INT NOT NULL, | |||
| lotId INT NOT NULL, | |||
| warehouseId INT NOT NULL, | |||
| stockTakeSection varchar(255), | |||
| stockTakeId INT NOT NULL, | |||
| stockTakerName VARCHAR(100), | |||
| approverId INT, | |||
| approverName VARCHAR(100), | |||
| stockTakerId INT NOT NULL, | |||
| pickerFirstStockTakeQty DECIMAL(14,2), | |||
| pickerSecondStockTakeQty DECIMAL(14,2), | |||
| approverStockTakeQty DECIMAL(14,2), | |||
| approverSecondStockTakeQty DECIMAL(14,2), | |||
| bookQty DECIMAL(14,2) NOT NULL, | |||
| badQty DECIMAL(14,2), | |||
| pickerFirstBadQty DECIMAL(14,2), | |||
| pickerSecondBadQty DECIMAL(14,2), | |||
| approverBadQty DECIMAL(14,2), | |||
| varianceQty DECIMAL(14,2), | |||
| uom VARCHAR(30), | |||
| stockTakeStartTime DATETIME, | |||
| stockTakeEndTime DATETIME, | |||
| date DATE NOT NULL, | |||
| status VARCHAR(30) NOT NULL, | |||
| remarks VARCHAR(500), | |||
| itemCode VARCHAR(50), | |||
| itemName VARCHAR(200), | |||
| inventoryLotId INT, | |||
| lotNo VARCHAR(512), | |||
| expiredDate DATE | |||
| ); | |||
| @@ -0,0 +1,5 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset Enson:add_column | |||
| ALTER TABLE `fpsmsdb`.`stock_take` | |||
| ADD COLUMN `stockTakeSection` varchar(255) NULL DEFAULT '' AFTER `remarks`; | |||
| @@ -0,0 +1,7 @@ | |||
| --liquibase formatted sql | |||
| --changeset author:add_time_fields_to_productprocessline | |||
| ALTER TABLE `productprocessline` | |||
| ADD COLUMN `processingTime` INT(11) NULL AFTER `endTime`, | |||
| ADD COLUMN `setupTime` INT(11) NULL AFTER `processingTime`, | |||
| ADD COLUMN `changeoverTime` INT(11) NULL AFTER `setupTime`; | |||