diff --git a/src/main/java/com/ffii/fpsms/modules/bag/entity/joBagConsumptionRepository.kt b/src/main/java/com/ffii/fpsms/modules/bag/entity/joBagConsumptionRepository.kt index 4e5a7b9..97afb23 100644 --- a/src/main/java/com/ffii/fpsms/modules/bag/entity/joBagConsumptionRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/bag/entity/joBagConsumptionRepository.kt @@ -12,5 +12,7 @@ import java.time.LocalDateTime @Repository interface JoBagConsumptionRepository : AbstractRepository { - + fun findAllByDeletedIsFalse(): List + fun findAllByBagLotLineIdAndDeletedIsFalse(bagLotLineId: Long): List + fun findAllByBagId(bagId: Long): List } diff --git a/src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt b/src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt index 9546713..d467a01 100644 --- a/src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt +++ b/src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt @@ -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 { } } } +open fun getAllBagUsageRecords(): List { + 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 { + 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 { + 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 { + 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 + ) + } +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/bag/web/bagController.kt b/src/main/java/com/ffii/fpsms/modules/bag/web/bagController.kt index 0fb62dc..13b2698 100644 --- a/src/main/java/com/ffii/fpsms/modules/bag/web/bagController.kt +++ b/src/main/java/com/ffii/fpsms/modules/bag/web/bagController.kt @@ -47,4 +47,19 @@ class BagController( fun createJoBagConsumption(@RequestBody request: CreateJoBagConsumptionRequest): MessageResponse { return bagService.createJoBagConsumption(request) } + @GetMapping("/bagUsageRecords") + fun getBagUsageRecords(): List { + return bagService.getAllBagUsageRecords() + } + @GetMapping("/bags") + fun getBags(): List = + bagService.getBagSummaries() + + @GetMapping("/bags/{bagId}/lot-lines") + fun getBagLotLines(@PathVariable bagId: Long): List = + bagService.getBagLotLines(bagId) + + @GetMapping("/lot-lines/{bagLotLineId}/consumptions") + fun getBagConsumptions(@PathVariable bagLotLineId: Long): List = + bagService.getBagConsumptions(bagLotLineId) } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/bag/web/model/bagReponse.kt b/src/main/java/com/ffii/fpsms/modules/bag/web/model/bagReponse.kt index b61c3d4..ac7c8a8 100644 --- a/src/main/java/com/ffii/fpsms/modules/bag/web/model/bagReponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/bag/web/model/bagReponse.kt @@ -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? ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt index 1d87fba..dfe0b63 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt @@ -28,5 +28,4 @@ interface DeliveryOrderInfo{ val deliveryOrderLines: List - } diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index 380edf0..d34082b 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt @@ -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>, params: MutableMap - ) : Map { + ): Map { 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() } - 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() + 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() - 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): Check4FTruckBatchResponse { + val problems = mutableListOf() - 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()) + 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 + ) } +} + \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt index 067a710..509c76c 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt @@ -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() + 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 } diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt index 7a26695..9b63738 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt @@ -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): Check4FTruckBatchResponse { + return deliveryOrderService.check4FTruckAvailabilityForDoList(doIds) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt index 6c736c3..f5d9f86 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt @@ -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 +) + +data class Check4FTruckBatchResponse( + val hasProblem: Boolean, + val problems: List ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 3ed0e53..abc59f5 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -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 { @@ -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) + ) +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt index 264d05e..108b442 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt @@ -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) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt index 5ba740e..d9d653e 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt @@ -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 ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt index adfcd0e..5abf7d6 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt @@ -14,4 +14,9 @@ interface WarehouseRepository : AbstractRepository { fun findByCodeAndDeletedIsFalse(code: String): Warehouse?; + fun findAllByDeletedIsFalse(): List; + fun findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection: String): List; + fun findDistinctStockTakeSectionsByDeletedIsFalse(): List; + fun findAllByIdIn(ids: List): List; + fun findAllByCodeAndDeletedIsFalse(code: String): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt index 552417a..6e5a270 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt @@ -67,10 +67,10 @@ fun findMaterialIssues(): List fun getBadItemList_statusIn(@Param("statuses") statuses: List): List @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 diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt index 0a87914..cabcf13 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt @@ -38,4 +38,17 @@ interface TruckRepository : AbstractRepository { fun findAllByShopId(shopId :Long):List //fun findByTruckLanceCode(truckLanceCode: String):List //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 } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcess.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcess.kt index 832c656..0089d85 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcess.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcess.kt @@ -53,5 +53,8 @@ open class ProductProcess : BaseEntity() { @Column(name = "productionPriority") open var productionPriority: Int? = 50 - + + @Column(name = "submitedBagRecord") + open var submitedBagRecord: Boolean? = false + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLine.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLine.kt index d277cd8..c2bd4ea 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLine.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLine.kt @@ -39,7 +39,12 @@ open class ProductProcessLine : BaseEntity() { @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 diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt index 74b9fb1..9c846b2 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt @@ -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?, val totalStockQty: Int?, val insufficientStockQty: Int?, diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt index db504da..9729b63 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt @@ -76,7 +76,7 @@ open class ProductProcessService( ) { open fun findAll(pageable: Pageable): Page { - 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 { - 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 { - 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 { - 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, + ) +}} diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt index eed6bbb..4cb8682 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt @@ -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) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt index ef8bf03..0273e8d 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt @@ -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? ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt index c26c454..c002537 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt @@ -52,4 +52,15 @@ interface InventoryLotLineRepository : AbstractRepository): List + +@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 } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotRepository.kt index 4dcfb55..f4dbaf6 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotRepository.kt @@ -23,4 +23,5 @@ interface InventoryLotRepository: AbstractRepository { AND il.deleted = false """) fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLot? + fun findByIdAndDeletedFalse(id: Serializable): InventoryLot? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt index a255704..a3f3d94 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt @@ -39,4 +39,7 @@ open class StockTake: BaseEntity() { @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 } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt index 1a78615..b1321fc 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt @@ -7,4 +7,5 @@ import java.io.Serializable @Repository interface StockTakeLineRepository : AbstractRepository { fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeLine?; + fun findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId: Serializable, stockTakeId: Serializable): StockTakeLine?; } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecord.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecord.kt new file mode 100644 index 0000000..9258f59 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecord.kt @@ -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() { + @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 +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt new file mode 100644 index 0000000..b7176ad --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt @@ -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 { + fun findAllByStockTakeIdAndDeletedIsFalse(stockTakeId: Long): List; + 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> +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/enums/StockTakeEnum.kt b/src/main/java/com/ffii/fpsms/modules/stock/enums/StockTakeEnum.kt index 54d8326..8417ac9 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/enums/StockTakeEnum.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/enums/StockTakeEnum.kt @@ -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"), } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt new file mode 100644 index 0000000..716e165 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt @@ -0,0 +1,999 @@ +// StockTakeRecordService.kt +package com.ffii.fpsms.modules.stock.service + +import com.ffii.fpsms.modules.master.entity.WarehouseRepository +import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository +import com.ffii.fpsms.modules.stock.entity.StockTakeRepository +import com.ffii.fpsms.modules.stock.entity.StockTakeRecordRepository +import com.ffii.fpsms.modules.stock.entity.StockTakeRecord +import com.ffii.fpsms.modules.stock.enums.StockTakeStatus +import com.ffii.fpsms.modules.stock.web.model.* +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.math.BigDecimal +import com.ffii.fpsms.modules.user.entity.UserRepository + +import com.ffii.fpsms.modules.stock.service.InventoryLotLineService +import com.ffii.fpsms.modules.stock.entity.StockTakeLine +import com.ffii.fpsms.modules.stock.entity.StockTakeLineRepository +import com.ffii.fpsms.modules.stock.enums.StockTakeLineStatus +import com.ffii.fpsms.modules.stock.entity.StockOut +import com.ffii.fpsms.modules.stock.entity.StockOutRepository +import com.ffii.fpsms.modules.stock.entity.StockOutLine +import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository +import com.ffii.fpsms.modules.stock.entity.StockIn +import com.ffii.fpsms.modules.stock.entity.StockInRepository +import com.ffii.fpsms.modules.stock.entity.StockInLine +import com.ffii.fpsms.modules.stock.entity.StockInLineRepository +import com.ffii.fpsms.modules.stock.entity.InventoryLot +import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository +@Service +class StockTakeRecordService( + val stockTakeRepository: StockTakeRepository, + val warehouseRepository: WarehouseRepository, + val inventoryLotLineRepository: InventoryLotLineRepository, + val stockTakeRecordRepository: StockTakeRecordRepository, + val userRepository: UserRepository, + val stockTakeLineRepository: StockTakeLineRepository, + val inventoryLotLineService: InventoryLotLineService, + val stockOutRepository: StockOutRepository, + val stockOutLineRepository: StockOutLIneRepository, + val stockInRepository: StockInRepository, + val stockInLineRepository: StockInLineRepository, + val inventoryLotRepository: InventoryLotRepository +) { + private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java) + + open fun AllPickedStockTakeList(): List { + // 1. 获取所有不同的 stockTakeSection(从 warehouse 表) + val allWarehouses = warehouseRepository.findAllByDeletedIsFalse() + val distinctSections = allWarehouses + .mapNotNull { it.stockTakeSection } + .distinct() + .filter { !it.isBlank() } + + if (distinctSections.isEmpty()) { + return emptyList() + } + + + val warehousesBySection = allWarehouses + .filter { it.stockTakeSection != null && !it.stockTakeSection!!.isBlank() } + .groupBy { it.stockTakeSection!! } + + val allStockTakes = stockTakeRepository.findAll() + .filter { !it.deleted } + .groupBy { it.stockTakeSection } + + + val allStockTakeRecords = stockTakeRecordRepository.findAll() + .filter { !it.deleted } + + + val recordsByWarehouseId = allStockTakeRecords + .filter { it.warehouse?.id != null } + .groupBy { it.warehouse!!.id } + + val result = mutableListOf() + var idCounter = 1L + + // 3. 为每个 stockTakeSection 创建一个卡片 + distinctSections.forEach { stockTakeSection -> + // 4. 获取该 section 下的所有 warehouse + val warehouses = warehousesBySection[stockTakeSection] ?: emptyList() + val warehouseIds = warehouses.mapNotNull { it.id } + + if (warehouseIds.isEmpty()) { + return@forEach + } + + // 5. 获取该 section 相关的所有 stock_take 记录 + val stockTakesForSection = allStockTakes[stockTakeSection] ?: emptyList() + + // 6. 获取 lastStockTakeDate:从 completed 状态的记录中,按 actualEnd 排序,取最新的 + val completedStockTakes = stockTakesForSection + .filter { it.status == StockTakeStatus.COMPLETED && it.actualEnd != null } + .sortedByDescending { it.actualEnd } + + val lastStockTakeDate = completedStockTakes.firstOrNull()?.actualEnd?.toLocalDate() + + // 7. 获取 status:获取最新的 stock_take 记录(按 actualStart 或 planStart 排序) + val latestStockTake = stockTakesForSection + .maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } + + val status = if (latestStockTake != null) { + latestStockTake.status?.value ?: "pending" + } else { + "no_cycle" + } + + // 8. 使用 stockTakeSection 作为 stockTakeSession + result.add( + AllPickedStockTakeListReponse( + id = idCounter++, + stockTakeSession = stockTakeSection, + lastStockTakeDate = lastStockTakeDate, + status = status, + currentStockTakeItemNumber = 0, // 临时设为 0,测试性能 + totalInventoryLotNumber = 0, // 临时设为 0,测试性能 + stockTakeId = latestStockTake?.id ?: 0 + ) + ) + } + + return result.sortedBy { it.stockTakeSession } + } + open fun AllApproverStockTakeList(): List { + // 1. 获取所有不同的 stockTakeSection(从 warehouse 表) + val allWarehouses = warehouseRepository.findAllByDeletedIsFalse() + val distinctSections = allWarehouses + .mapNotNull { it.stockTakeSection } + .distinct() + .filter { !it.isBlank() } + + if (distinctSections.isEmpty()) { + return emptyList() + } + + // 2. 批量获取所有相关的数据(优化性能 - 只查询一次) + // 2.1 按 section 分组 warehouse + val warehousesBySection = allWarehouses + .filter { it.stockTakeSection != null && !it.stockTakeSection!!.isBlank() } + .groupBy { it.stockTakeSection!! } + + // 2.2 批量获取所有 stock_take 记录(只获取一次),按 stockTakeSection 分组 + val allStockTakes = stockTakeRepository.findAll() + .filter { !it.deleted } + .groupBy { it.stockTakeSection } + + // 2.3 批量获取所有 stocktakeRecord(只获取一次) + val allStockTakeRecords = stockTakeRecordRepository.findAll() + .filter { !it.deleted } + + // 2.4 按 warehouseId 分组 stocktakeRecord,便于快速查找 + val recordsByWarehouseId = allStockTakeRecords + .filter { it.warehouse?.id != null } + .groupBy { it.warehouse!!.id } + + val result = mutableListOf() + var idCounter = 1L + + // 3. 为每个 stockTakeSection 创建一个卡片 + distinctSections.forEach { stockTakeSection -> + // 4. 获取该 section 下的所有 warehouse + val warehouses = warehousesBySection[stockTakeSection] ?: emptyList() + val warehouseIds = warehouses.mapNotNull { it.id } + + if (warehouseIds.isEmpty()) { + return@forEach + } + + // 5. 获取该 section 相关的所有 stock_take 记录 + val stockTakesForSection = allStockTakes[stockTakeSection] ?: emptyList() + + // 6. 获取 lastStockTakeDate:从 completed 状态的记录中,按 actualEnd 排序,取最新的 + val completedStockTakes = stockTakesForSection + .filter { it.status == StockTakeStatus.COMPLETED && it.actualEnd != null } + .sortedByDescending { it.actualEnd } + + val lastStockTakeDate = completedStockTakes.firstOrNull()?.actualEnd?.toLocalDate() + + // 7. 获取 status:获取最新的 stock_take 记录(按 actualStart 或 planStart 排序) + val latestStockTake = stockTakesForSection + .maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } + + // 8. 确定 status:只有 APPROVING 或 COMPLETED 状态才输出,其他为 null + val status = if (latestStockTake != null) { + val stockTakeStatus = latestStockTake.status + // 只有 APPROVING 或 COMPLETED 状态才输出值,其他返回 null + if (stockTakeStatus == StockTakeStatus.APPROVING || stockTakeStatus == StockTakeStatus.COMPLETED) { + stockTakeStatus.value + } else { + null + } + } else { + null + } + + // 9. 使用 stockTakeSection 作为 stockTakeSession + result.add( + AllPickedStockTakeListReponse( + id = idCounter++, + stockTakeSession = stockTakeSection, + lastStockTakeDate = lastStockTakeDate, + status = status?:"", + currentStockTakeItemNumber = 0, // 临时设为 0,测试性能 + totalInventoryLotNumber = 0, // 临时设为 0,测试性能 + stockTakeId = latestStockTake?.id ?: 0 + ) + ) + } + + return result.sortedBy { it.stockTakeSession } + } + open fun getInventoryLotDetailsByWarehouseCode(warehouseCode: String): List { + println("getInventoryLotDetailsByWarehouseCode called with code: $warehouseCode") + + // 1. 先查找 warehouse,可能有多个相同 code 的记录,选择 id 最小的(最早的) + val warehouses = warehouseRepository.findAllByCodeAndDeletedIsFalse(warehouseCode) + if (warehouses.isEmpty()) { + logger.warn("Warehouse not found with code: $warehouseCode") + return emptyList() + } + + // 按 id 排序,取第一个(id 最小的,即最早的) + val warehouse = warehouses.minByOrNull { it.id ?: Long.MAX_VALUE } + if (warehouse == null) { + logger.warn("Warehouse found but id is null for code: $warehouseCode") + return emptyList() + } + println("Found warehouse: id=${warehouse.id}, code=${warehouse.code} (total ${warehouses.size} found, using earliest)") + + // 2. 根据 warehouse id 查找所有相关的 InventoryLotLine + val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(listOf(warehouse.id!!)) + println("Found ${inventoryLotLines.size} inventory lot lines") + + // 3. 转换为 Response + return inventoryLotLines.map { ill -> + val inventoryLot = ill.inventoryLot + val item = inventoryLot?.item + val warehouse = ill.warehouse + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + .subtract(ill.holdQty ?: BigDecimal.ZERO) + + InventoryLotDetailResponse( + id = ill.id ?: 0L, + inventoryLotId = inventoryLot?.id ?: 0L, + itemId = item?.id ?: 0L, + itemCode = item?.code, + itemName = item?.name, + lotNo = inventoryLot?.lotNo, + expiryDate = inventoryLot?.expiryDate, + productionDate = inventoryLot?.productionDate, + stockInDate = inventoryLot?.stockInDate, + inQty = ill.inQty, + outQty = ill.outQty, + holdQty = ill.holdQty, + availableQty = availableQty, + uom = ill.stockUom?.uom?.udfudesc, + warehouseCode = warehouse?.code, + warehouseName = warehouse?.name, + status = ill.status?.name, + stockTakeRecordStatus = null, + stockTakeRecordId = null, + firstStockTakeQty = null, + secondStockTakeQty = null, + approverQty = null, + varianceQty = null, + approverBadQty = null, + finalQty = null, + firstBadQty = null, + secondBadQty = null, + remarks = null, + warehouseSlot = warehouse?.slot, + warehouseArea = warehouse?.area, + warehouse = warehouse?.warehouse + + ) + } + } + open fun getInventoryLotDetailsByStockTakeSection(stockTakeSection: String, stockTakeId: Long? = null): List { + println("getInventoryLotDetailsByStockTakeSection called with section: $stockTakeSection, stockTakeId: $stockTakeId") + + + val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection) + if (warehouses.isEmpty()) { + logger.warn("No warehouses found for stockTakeSection: $stockTakeSection") + return emptyList() + } + + val warehouseIds = warehouses.mapNotNull { it.id } + println("Found ${warehouses.size} warehouses for section $stockTakeSection") + + + val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) + println("Found ${inventoryLotLines.size} inventory lot lines") + + + val stockTakeRecordsMap = if (stockTakeId != null) { + val allStockTakeRecords = stockTakeRecordRepository.findAll() + .filter { + !it.deleted && + it.stockTake?.id == stockTakeId && + it.warehouse?.id in warehouseIds + } + // 按 lotId 和 warehouseId 建立映射 + allStockTakeRecords.associateBy { + Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L) + } + } else { + emptyMap() + } + + + return inventoryLotLines.map { ill -> + val inventoryLot = ill.inventoryLot + val item = inventoryLot?.item + val warehouse = ill.warehouse + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + .subtract(ill.holdQty ?: BigDecimal.ZERO) + + + val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) { + stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)] + } else { + null + } + val inventoryLotLineId = ill.id + val stockTakeLine = stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId, stockTakeId!!) + InventoryLotDetailResponse( + id = ill.id ?: 0L, + inventoryLotId = inventoryLot?.id ?: 0L, + itemId = item?.id ?: 0L, + itemCode = item?.code, + itemName = item?.name, + lotNo = inventoryLot?.lotNo, + expiryDate = inventoryLot?.expiryDate, + productionDate = inventoryLot?.productionDate, + stockInDate = inventoryLot?.stockInDate, + inQty = ill.inQty, + remarks = stockTakeRecord?.remarks, + outQty = ill.outQty, + holdQty = ill.holdQty, + availableQty = availableQty, + uom = ill.stockUom?.uom?.udfudesc, + warehouseCode = warehouse?.code, + warehouseName = warehouse?.name, + status = ill.status?.name, + warehouseSlot = warehouse?.slot, + warehouseArea = warehouse?.area, + warehouse = warehouse?.warehouse, + varianceQty = stockTakeRecord?.varianceQty, + stockTakeRecordId = stockTakeRecord?.id, + stockTakeRecordStatus = stockTakeRecord?.status, + firstStockTakeQty = stockTakeRecord?.pickerFirstStockTakeQty, + secondStockTakeQty = stockTakeRecord?.pickerSecondStockTakeQty, + firstBadQty = stockTakeRecord?.pickerFirstBadQty, + secondBadQty = stockTakeRecord?.pickerSecondBadQty, + approverQty = stockTakeRecord?.approverStockTakeQty , + approverBadQty = stockTakeRecord?.approverBadQty, + finalQty = stockTakeLine?.finalQty, + //finalQty = null, + ) + } + } + open fun saveStockTakeRecord( + request: SaveStockTakeRecordRequest, + stockTakeId: Long, + stockTakerId: Long + ): StockTakeRecord { + println("saveStockTakeRecord called with stockTakeRecordId: ${request.stockTakeRecordId}, inventoryLotLineId: ${request.inventoryLotLineId}") + val user = userRepository.findById(stockTakerId).orElse(null) + // 1. 获取 inventory lot line + val inventoryLotLine = inventoryLotLineRepository.findByIdAndDeletedIsFalse(request.inventoryLotLineId) + ?: throw IllegalArgumentException("Inventory lot line not found: ${request.inventoryLotLineId}") + + val inventoryLot = inventoryLotLine.inventoryLot + ?: throw IllegalArgumentException("Inventory lot not found") + val item = inventoryLot.item + ?: throw IllegalArgumentException("Item not found") + val warehouse = inventoryLotLine.warehouse + ?: throw IllegalArgumentException("Warehouse not found") + val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) + ?: throw IllegalArgumentException("Stock take not found: $stockTakeId") + + // 2. 计算 availableQty + val availableQty = (inventoryLotLine.inQty ?: BigDecimal.ZERO) + .subtract(inventoryLotLine.outQty ?: BigDecimal.ZERO) + .subtract(inventoryLotLine.holdQty ?: BigDecimal.ZERO) + + // 3. 判断是创建还是更新 + val stockTakeRecord = if (request.stockTakeRecordId != null) { + // 更新现有记录(第二次盘点) + val existingRecord = stockTakeRecordRepository.findByIdAndDeletedIsFalse(request.stockTakeRecordId) + ?: throw IllegalArgumentException("Stock take record not found: ${request.stockTakeRecordId}") + + // 第二次盘点:允许不匹配,但根据匹配情况设置状态 + val totalInputQty = request.qty.add(request.badQty) + val isMatched = totalInputQty.compareTo(availableQty) == 0 + val varianceQty = availableQty-request.qty-request.badQty + // 更新字段(第二次盘点) + existingRecord.apply { + this.pickerSecondStockTakeQty = request.qty + this.pickerSecondBadQty = request.badQty // 更新 badQty + this.status ="pass" + this.remarks = request.remark ?: this.remarks + this.stockTakerName = user?.name + this.stockTakeEndTime = java.time.LocalDateTime.now() + + this.varianceQty = varianceQty + } + existingRecord + } else { + // 创建新记录(第一次盘点) + // 第一次盘点不验证 QTY + Bad QTY 是否匹配,允许不匹配的情况 + // 如果匹配,status = "1",如果不匹配,status = "2" + val totalInputQty = request.qty.add(request.badQty) + val isMatched = totalInputQty.compareTo(availableQty) == 0 + val varianceQty = availableQty-request.qty-request.badQty + StockTakeRecord().apply { + this.itemId = item.id + this.lotId = inventoryLot.id + this.warehouse = warehouse + this.stockTake = stockTake + this.stockTakeSection = warehouse.stockTakeSection + this.inventoryLotId = inventoryLot.id + this.stockTakerId = stockTakerId + this.stockTakerName = user?.name + this.pickerFirstStockTakeQty = request.qty + this.pickerFirstBadQty = request.badQty + this.bookQty = availableQty + this.varianceQty = varianceQty + this.uom = inventoryLotLine.stockUom?.uom?.udfudesc + this.date = java.time.LocalDate.now() + this.status = if (isMatched) "pass" else "notMatch" + this.remarks = request.remark + this.itemCode = item.code + this.itemName = item.name + this.lotNo = inventoryLot.lotNo + this.expiredDate = inventoryLot.expiryDate + this.stockTakeStartTime = java.time.LocalDateTime.now() + } + } + + + val savedRecord = stockTakeRecordRepository.save(stockTakeRecord) + + + val stockTakeSection = savedRecord.stockTakeSection + if (stockTakeSection != null) { + checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection) + } + + return savedRecord +} + + + +open fun batchSaveStockTakeRecords( + request: BatchSaveStockTakeRecordRequest +): BatchSaveStockTakeRecordResponse { + + println("batchSaveStockTakeRecords called for section: ${request.stockTakeSection}, stockTakeId: ${request.stockTakeId}") + val user = userRepository.findById(request.stockTakerId).orElse(null) + // 1. 获取 stock take + val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) + ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") + + // 2. 使用 stockTakeSection 获取所有 inventory lot lines(类似 getInventoryLotDetailsByStockTakeSection) + val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(request.stockTakeSection) + if (warehouses.isEmpty()) { + logger.warn("No warehouses found for stockTakeSection: ${request.stockTakeSection}") + return BatchSaveStockTakeRecordResponse(0, 0, listOf("No warehouses found for section: ${request.stockTakeSection}")) + } + + val warehouseIds = warehouses.mapNotNull { it.id } + println("Found ${warehouses.size} warehouses for section ${request.stockTakeSection}") + + // 3. 批量获取所有相关的 InventoryLotLine + val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) + println("Found ${inventoryLotLines.size} inventory lot lines") + + // 4. 使用 stockTakeId 获取已创建的记录,建立映射以排除它们 + val existingRecordsMap = stockTakeRecordRepository.findAll() + .filter { + !it.deleted && + it.stockTake?.id == request.stockTakeId && + it.warehouse?.id in warehouseIds + } + .associateBy { + Pair(it.inventoryLotId ?: 0L, it.warehouse?.id ?: 0L) + } + + println("Found ${existingRecordsMap.size} existing stock take records") + + // 5. 找出未创建的记录 + val uncreatedLines = inventoryLotLines.filter { ill -> + val inventoryLot = ill.inventoryLot + val warehouse = ill.warehouse + if (inventoryLot?.id == null || warehouse?.id == null) { + false + } else { + val key = Pair(inventoryLot.id, warehouse.id) + !existingRecordsMap.containsKey(key) + } + } + + println("Found ${uncreatedLines.size} uncreated inventory lot lines to process") + + if (uncreatedLines.isEmpty()) { + return BatchSaveStockTakeRecordResponse(0, 0, listOf("No uncreated records found")) + } + + // 6. 批量创建记录 + var successCount = 0 + var errorCount = 0 + val errors = mutableListOf() + + uncreatedLines.forEach { ill -> + try { + val inventoryLot = ill.inventoryLot + ?: throw IllegalArgumentException("Inventory lot not found") + val itemEntity = inventoryLot.item + ?: throw IllegalArgumentException("Item not found") + val warehouse = ill.warehouse + ?: throw IllegalArgumentException("Warehouse not found") + + // 计算 availableQty + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + .subtract(ill.holdQty ?: BigDecimal.ZERO) + + // 使用 availableQty 作为 qty,badQty 为 0 + val qty = availableQty + val badQty = BigDecimal.ZERO + + // 判断是否匹配 + val totalInputQty = qty.add(badQty) + val isMatched = totalInputQty.compareTo(availableQty) == 0 + val varianceQty = availableQty-qty-badQty + // 创建新记录 + val stockTakeRecord = StockTakeRecord().apply { + this.itemId = itemEntity.id + this.lotId = inventoryLot.id + this.warehouse = warehouse + this.stockTake = stockTake + this.stockTakeSection = request.stockTakeSection + this.inventoryLotId = inventoryLot.id + this.stockTakerId = request.stockTakerId + this.stockTakerName = user?.name + this.pickerFirstStockTakeQty = qty + this.pickerFirstBadQty = badQty + this.bookQty = availableQty + this.varianceQty = varianceQty + this.uom = ill.stockUom?.uom?.udfudesc + this.date = java.time.LocalDate.now() + this.status = if (isMatched) "pass" else "notMatch" + this.remarks = null + this.itemCode = itemEntity.code + this.itemName = itemEntity.name + this.lotNo = inventoryLot.lotNo + this.expiredDate = inventoryLot.expiryDate + this.stockTakeStartTime = java.time.LocalDateTime.now() + this.stockTakeEndTime = java.time.LocalDateTime.now() + } + + stockTakeRecordRepository.save(stockTakeRecord) + successCount++ + } catch (e: Exception) { + errorCount++ + val errorMsg = "Error saving inventoryLotLineId ${ill.id}: ${e.message}" + errors.add(errorMsg) + logger.error(errorMsg, e) + } + } + if (successCount > 0) { + checkAndUpdateStockTakeStatus(request.stockTakeId, request.stockTakeSection) + } + println("batchSaveStockTakeRecords completed: success=$successCount, errors=$errorCount") + return BatchSaveStockTakeRecordResponse( + successCount = successCount, + errorCount = errorCount, + errors = errors + ) +} +// ... existing code ... + +// 检查并更新 stock take 状态 +// 检查并更新 stock take 状态 +open fun checkAndUpdateStockTakeStatus(stockTakeId: Long, stockTakeSection: String): Map { + try { + val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) + ?: return mapOf("success" to false, "message" to "Stock take not found") + + // 1. 获取该 section 下的所有 warehouse + val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection) + if (warehouses.isEmpty()) { + return mapOf("success" to false, "message" to "No warehouses found for section") + } + + val warehouseIds = warehouses.mapNotNull { it.id } + + // 2. 获取该 section 下的所有 inventory lot lines + val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) + + // 3. 获取该 stock take 下该 section 的所有记录 + val stockTakeRecords = stockTakeRecordRepository.findAll() + .filter { + !it.deleted && + it.stockTake?.id == stockTakeId && + it.warehouse?.id in warehouseIds && + it.stockTakeSection == stockTakeSection + } + + // 4. 检查是否所有 inventory lot lines 都有对应的记录 + val allLinesHaveRecords = inventoryLotLines.all { ill -> + val inventoryLot = ill.inventoryLot + val warehouse = ill.warehouse + if (inventoryLot?.id == null || warehouse?.id == null) { + false + } else { + stockTakeRecords.any { record -> + record.inventoryLotId == inventoryLot.id && + record.warehouse?.id == warehouse.id + } + } + } + + // 5. 检查是否所有记录的状态都是 "pass" + val allRecordsPassed = stockTakeRecords.isNotEmpty() && + stockTakeRecords.all { it.status == "pass" } + + // 6. 如果所有记录都已创建且都是 "pass",更新 stock take 状态为 "approving" + if (allLinesHaveRecords && allRecordsPassed) { + stockTake.status = StockTakeStatus.APPROVING + stockTakeRepository.save(stockTake) + println("Stock take $stockTakeId status updated to APPROVING - all records are pass") + return mapOf( + "success" to true, + "message" to "Stock take status updated to APPROVING", + "updated" to true + ) + } else { + return mapOf( + "success" to true, + "message" to "Conditions not met for status update", + "updated" to false, + "allLinesHaveRecords" to allLinesHaveRecords, + "allRecordsPassed" to allRecordsPassed + ) + } + } catch (e: Exception) { + logger.error("Error checking and updating stock take status for stockTakeId: $stockTakeId", e) + return mapOf("success" to false, "message" to "Error: ${e.message}") + } +} +open fun saveApproverStockTakeRecord( + request: SaveApproverStockTakeRecordRequest, + stockTakeId: Long +): StockTakeRecord { + println("saveApproverStockTakeRecord called with stockTakeRecordId: ${request.stockTakeRecordId}") + val user = userRepository.findById(request.approverId ?: 0L).orElse(null) + + + val stockTakeRecord = if (request.stockTakeRecordId != null) { + stockTakeRecordRepository.findByIdAndDeletedIsFalse(request.stockTakeRecordId) + ?: throw IllegalArgumentException("Stock take record not found: ${request.stockTakeRecordId}") + } else { + throw IllegalArgumentException("stockTakeRecordId is required for approver") + } + + val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) + ?: throw IllegalArgumentException("Stock take not found: $stockTakeId") + + + val finalQty = if (request.approverQty != null && request.approverBadQty != null) { + request.approverQty + } else { + request.qty + } + + val finalBadQty = if (request.approverQty != null && request.approverBadQty != null) { + request.approverBadQty + } else { + request.badQty + } + val varianceQty = (finalQty ?: BigDecimal.ZERO) + .subtract( stockTakeRecord.bookQty?: BigDecimal.ZERO) + println("finalQty: $finalQty") + println("stockTakeRecord.bookQty: ${stockTakeRecord.bookQty}") + println("varianceQty: $varianceQty") + stockTakeRecord.apply { + this.approverId = request.approverId + this.approverName = user?.name + this.approverStockTakeQty = finalQty + this.approverBadQty = finalBadQty + this.varianceQty = varianceQty + this.status = "completed" + } + + + val savedRecord = stockTakeRecordRepository.save(stockTakeRecord) + + + val inventoryLotLine = inventoryLotLineRepository.findByIdAndDeletedIsFalse( + stockTakeRecord.inventoryLotId ?: throw IllegalArgumentException("Inventory lot ID not found") + ) ?: throw IllegalArgumentException("Inventory lot line not found") + val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse( + inventoryLotLine?.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found") + ) ?: throw IllegalArgumentException("Inventory lot not found") + + if (varianceQty !=BigDecimal.ZERO ) { + val stockTakeLine = StockTakeLine().apply { + this.stockTake = stockTake + this.inventoryLotLine = inventoryLotLine + this.initialQty = stockTakeRecord.bookQty + this.finalQty = finalQty + this.status = StockTakeLineStatus.COMPLETED + this.completeDate = java.time.LocalDateTime.now() + + } + stockTakeLineRepository.save(stockTakeLine) + if (varianceQty < BigDecimal.ZERO ) { + val stockOut=StockOut().apply { + this.type="stockTake" + this.status="completed" + this.handler=request.approverId + } + stockOutRepository.save(stockOut) + + val stockOutLine = StockOutLine().apply { + this.item=inventoryLot.item + this.qty=(-varianceQty)?.toDouble() + this.stockOut = stockOut + this.inventoryLotLine = inventoryLotLine + this.status = "completed" + + + } + stockOutLineRepository.save(stockOutLine) + } + if (varianceQty > BigDecimal.ZERO ) { + val stockIn=StockIn().apply { + this.code=stockTake.code + this.status="completed" + this.stockTake=stockTake + } + stockInRepository.save(stockIn) + val stockInLine = StockInLine().apply { + this.stockTakeLine=stockTakeLine + this.item=inventoryLot.item + this.itemNo=stockTakeRecord.itemCode + this.stockIn = stockIn + this.demandQty=finalQty + this.acceptedQty=finalQty + this.expiryDate=inventoryLot.expiryDate + this.inventoryLot=inventoryLot + this.inventoryLotLine=inventoryLotLine + this.lotNo=inventoryLot.lotNo + this.status = "completed" + + + } + stockInLineRepository.save(stockInLine) + } + // val currentInQty = inventoryLotLine.inQty ?: BigDecimal.ZERO + // val newInQty = currentInQty.add(variance) + + val updateRequest = SaveInventoryLotLineRequest( + id = inventoryLotLine.id, + inventoryLotId = inventoryLotLine.inventoryLot?.id, + warehouseId = inventoryLotLine.warehouse?.id, + stockUomId = inventoryLotLine.stockUom?.id, + inQty = finalQty, + outQty = inventoryLotLine.outQty, + holdQty = inventoryLotLine.holdQty, + status = inventoryLotLine.status?.value, + remarks = inventoryLotLine.remarks + ) + inventoryLotLineService.saveInventoryLotLine(updateRequest) +} + + return savedRecord +} + +open fun batchSaveApproverStockTakeRecords( + request: BatchSaveApproverStockTakeRecordRequest +): BatchSaveApproverStockTakeRecordResponse { + println("batchSaveApproverStockTakeRecords called for section: ${request.stockTakeSection}, stockTakeId: ${request.stockTakeId}") + val user = userRepository.findById(request.approverId).orElse(null) + + // 1. Get stock take + val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) + ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") + + // 2. Get all stock take records for this section where: + // - secondStockTakeQty is null/0 (no second input) + // - pickerFirstStockTakeQty is not 0 + val stockTakeRecords = stockTakeRecordRepository.findAll() + .filter { + !it.deleted && + it.stockTake?.id == request.stockTakeId && + it.stockTakeSection == request.stockTakeSection && + (it.pickerSecondStockTakeQty == null || it.pickerSecondStockTakeQty == BigDecimal.ZERO) && + it.pickerFirstStockTakeQty != null && it.pickerFirstStockTakeQty!! > BigDecimal.ZERO + } + + println("Found ${stockTakeRecords.size} records to process") + + if (stockTakeRecords.isEmpty()) { + return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No records found matching criteria")) + } + + // 3. Process each record + var successCount = 0 + var errorCount = 0 + val errors = mutableListOf() + + stockTakeRecords.forEach { record -> + try { + // Use first qty/badQty as approver selection + val qty = record.pickerFirstStockTakeQty ?: BigDecimal.ZERO + val badQty = record.pickerFirstBadQty ?: BigDecimal.ZERO + + // Update approver fields + record.apply { + this.approverId = request.approverId + this.approverName = user?.name + this.approverStockTakeQty = qty + this.approverBadQty = badQty + } + + stockTakeRecordRepository.save(record) + + // Create stock_take_line and update inventoryLotLine + val inventoryLotLine = record.inventoryLotId?.let { + inventoryLotLineRepository.findByIdAndDeletedIsFalse(it) + } ?: throw IllegalArgumentException("Inventory lot line not found") + + // Create stock take line + val stockTakeLine = StockTakeLine().apply { + this.stockTake = stockTake + this.inventoryLotLine = inventoryLotLine + this.initialQty = record.bookQty + this.finalQty = qty.add(badQty) + this.status = StockTakeLineStatus.COMPLETED + this.completeDate = java.time.LocalDateTime.now() + this.remarks = "Batch approved by approver" + } + stockTakeLineRepository.save(stockTakeLine) + + // Update inventoryLotLine + val variance = qty.add(badQty).subtract(record.bookQty ?: BigDecimal.ZERO) + if (variance.compareTo(BigDecimal.ZERO) != 0) { + val currentInQty = inventoryLotLine.inQty ?: BigDecimal.ZERO + val newInQty = currentInQty.add(variance) + + val updateRequest = SaveInventoryLotLineRequest( + id = inventoryLotLine.id, + inventoryLotId = inventoryLotLine.inventoryLot?.id, + warehouseId = inventoryLotLine.warehouse?.id, + stockUomId = inventoryLotLine.stockUom?.id, + inQty = newInQty, + outQty = inventoryLotLine.outQty, + holdQty = inventoryLotLine.holdQty, + status = inventoryLotLine.status?.value, + remarks = inventoryLotLine.remarks + ) + inventoryLotLineService.saveInventoryLotLine(updateRequest) + } + + successCount++ + } catch (e: Exception) { + errorCount++ + val errorMsg = "Error processing record ${record.id}: ${e.message}" + errors.add(errorMsg) + logger.error(errorMsg, e) + } + } + + println("batchSaveApproverStockTakeRecords completed: success=$successCount, errors=$errorCount") + return BatchSaveApproverStockTakeRecordResponse( + successCount = successCount, + errorCount = errorCount, + errors = errors + ) +} +// 在文件末尾,batchSaveApproverStockTakeRecords 方法之后添加 + +open fun updateStockTakeRecordStatusToNotMatch(stockTakeRecordId: Long): StockTakeRecord { + println("updateStockTakeRecordStatusToNotMatch called with stockTakeRecordId: $stockTakeRecordId") + + val stockTakeRecord = stockTakeRecordRepository.findByIdAndDeletedIsFalse(stockTakeRecordId) + ?: throw IllegalArgumentException("Stock take record not found: $stockTakeRecordId") + + stockTakeRecord.apply { + this.status = "notMatch" + } + + return stockTakeRecordRepository.save(stockTakeRecord) +} + +open fun getInventoryLotDetailsByStockTakeSectionNotMatch(stockTakeSection: String, stockTakeId: Long? = null): List { + println("getInventoryLotDetailsByStockTakeSectionNotMatch called with section: $stockTakeSection, stockTakeId: $stockTakeId") + + val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection) + if (warehouses.isEmpty()) { + logger.warn("No warehouses found for stockTakeSection: $stockTakeSection") + return emptyList() + } + + val warehouseIds = warehouses.mapNotNull { it.id } + println("Found ${warehouses.size} warehouses for section $stockTakeSection") + + val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) + println("Found ${inventoryLotLines.size} inventory lot lines") + + val stockTakeRecordsMap = if (stockTakeId != null) { + val allStockTakeRecords = stockTakeRecordRepository.findAll() + .filter { + !it.deleted && + it.stockTake?.id == stockTakeId && + it.warehouse?.id in warehouseIds && + it.status == "notMatch" // 只获取 notMatch 状态的记录 + } + // 按 lotId 和 warehouseId 建立映射 + allStockTakeRecords.associateBy { + Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L) + } + } else { + emptyMap() + } + + // 只返回有 notMatch 记录的 inventory lot lines + return inventoryLotLines.mapNotNull { ill -> + val inventoryLot = ill.inventoryLot + val item = inventoryLot?.item + val warehouse = ill.warehouse + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + .subtract(ill.holdQty ?: BigDecimal.ZERO) + + val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) { + stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)] + } else { + null + } + + // 只返回状态为 notMatch 的记录 + if (stockTakeRecord == null || stockTakeRecord.status != "notMatch") { + return@mapNotNull null + } + + val inventoryLotLineId = ill.id + val stockTakeLine = if (stockTakeId != null) { + stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId, stockTakeId) + } else { + null + } + + InventoryLotDetailResponse( + id = ill.id ?: 0L, + inventoryLotId = inventoryLot?.id ?: 0L, + itemId = item?.id ?: 0L, + itemCode = item?.code, + itemName = item?.name, + lotNo = inventoryLot?.lotNo, + expiryDate = inventoryLot?.expiryDate, + productionDate = inventoryLot?.productionDate, + stockInDate = inventoryLot?.stockInDate, + inQty = ill.inQty, + remarks = stockTakeRecord.remarks, + outQty = ill.outQty, + holdQty = ill.holdQty, + availableQty = availableQty, + uom = ill.stockUom?.uom?.udfudesc, + warehouseCode = warehouse?.code, + warehouseName = warehouse?.name, + status = ill.status?.name, + warehouseSlot = warehouse?.slot, + warehouseArea = warehouse?.area, + warehouse = warehouse?.warehouse, + varianceQty = stockTakeRecord.varianceQty, + stockTakeRecordId = stockTakeRecord.id, + stockTakeRecordStatus = stockTakeRecord.status, + firstStockTakeQty = stockTakeRecord.pickerFirstStockTakeQty, + secondStockTakeQty = stockTakeRecord.pickerSecondStockTakeQty, + firstBadQty = stockTakeRecord.pickerFirstBadQty, + secondBadQty = stockTakeRecord.pickerSecondBadQty, + approverQty = stockTakeRecord.approverStockTakeQty, + approverBadQty = stockTakeRecord.approverBadQty, + finalQty = stockTakeLine?.finalQty, + ) + } +} +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt index cb54ac8..8ab4a3e 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt @@ -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 { + logger.info("--------- Start - Create Stock Take for Sections -------") + + val result = mutableMapOf() + + // 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 + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt index 3aef8aa..30c0ce6 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt @@ -40,4 +40,10 @@ class StockTakeController( return ResponseEntity.ok(stockTakeService.importExcel(workbook)) } + @PostMapping("/createForSections") + fun createStockTakeForSections(): ResponseEntity> { + val result = stockTakeService.createStockTakeForSections() + return ResponseEntity.ok(result) + } + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt new file mode 100644 index 0000000..498596c --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt @@ -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 { + return stockOutRecordService.AllPickedStockTakeList() + } + @GetMapping("/AllApproverStockTakeList") + fun AllApproverStockTakeList(): List { + return stockOutRecordService.AllApproverStockTakeList() + } + @GetMapping("/inventoryLotDetailsBySectionNotMatch") + fun getInventoryLotDetailsByStockTakeSectionNotMatch( + @RequestParam stockTakeSection: String, + @RequestParam(required = false) stockTakeId: Long? + ): List { + return stockOutRecordService.getInventoryLotDetailsByStockTakeSectionNotMatch(stockTakeSection, stockTakeId) + } + + @GetMapping("/inventoryLotDetails") + fun getInventoryLotDetailsByWarehouseCode( + @RequestParam warehouseCode: String + ): List { + return stockOutRecordService.getInventoryLotDetailsByWarehouseCode(warehouseCode) + } + + @GetMapping("/inventoryLotDetailsBySection") + fun getInventoryLotDetailsByStockTakeSection( + @RequestParam stockTakeSection: String, + @RequestParam(required = false) stockTakeId: Long? + ): List { + return stockOutRecordService.getInventoryLotDetailsByStockTakeSection(stockTakeSection, stockTakeId) + } + + @PostMapping("/saveStockTakeRecord") + fun saveStockTakeRecord( + @RequestBody request: SaveStockTakeRecordRequest, + @RequestParam stockTakeId: Long, + @RequestParam stockTakerId: Long + ): ResponseEntity { + 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 { + 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 { + 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 { + 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 { + 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 { + 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") + )) + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt index 6ce4798..a58e928 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt @@ -11,4 +11,5 @@ data class SaveStockTakeRequest( var actualEnd: LocalDateTime?, var status: String?, val remarks: String?, + val stockTakeSection: String?=null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt new file mode 100644 index 0000000..30f3b9d --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt @@ -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 +) +data class BatchSaveApproverStockTakeRecordRequest( + val stockTakeId: Long, + val stockTakeSection: String, + val approverId: Long, +) + +data class BatchSaveApproverStockTakeRecordResponse( + val successCount: Int, + val errorCount: Int, + val errors: List +) \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20251230_01_Enson/02_alter_table.sql b/src/main/resources/db/changelog/changes/20251230_01_Enson/02_alter_table.sql new file mode 100644 index 0000000..811ca83 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20251230_01_Enson/02_alter_table.sql @@ -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 + +); \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20251230_01_Enson/04_alter_table.sql b/src/main/resources/db/changelog/changes/20251230_01_Enson/04_alter_table.sql new file mode 100644 index 0000000..c3af042 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20251230_01_Enson/04_alter_table.sql @@ -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`; diff --git a/src/main/resources/db/changelog/changes/20260105_enson/01_alter_table.sql b/src/main/resources/db/changelog/changes/20260105_enson/01_alter_table.sql new file mode 100644 index 0000000..893fb24 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260105_enson/01_alter_table.sql @@ -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`; \ No newline at end of file