| @@ -207,4 +207,71 @@ class SetupController( | |||||
| ) | ) | ||||
| return ResponseEntity.ok(mapOf("success" to true, "message" to "Lot stock-in labels printed successfully", "printedCount" to printedCount)) | return ResponseEntity.ok(mapOf("success" to true, "message" to "Lot stock-in labels printed successfully", "printedCount" to printedCount)) | ||||
| } | } | ||||
| data class PrintProgressResult( | |||||
| val success: Boolean, | |||||
| val lastIndex: Int, | |||||
| val totalLots: Int, | |||||
| val errorMessage: String? = null | |||||
| ) | |||||
| @PostMapping("/inventory/print-lot-stockin-labels-by-item-ids-v2") | |||||
| fun printLotStockInLabelsByItemIdsV2(@RequestBody request: Map<String, Any>): ResponseEntity<Map<String, Any>> { | |||||
| val printerId = (request["printerId"] as? Number)?.toLong() | |||||
| val printQty = (request["printQty"] as? Number)?.toInt() ?: 1 | |||||
| val fromIndex = (request["fromIndex"] as? Number)?.toInt() | |||||
| val toIndex = (request["toIndex"] as? Number)?.toInt() | |||||
| val itemIds = (request["itemIds"] as? List<*>)?.mapNotNull { (it as? Number)?.toLong() } ?: emptyList() | |||||
| if (printerId == null) { | |||||
| return ResponseEntity.badRequest().body( | |||||
| mapOf( | |||||
| "success" to false, | |||||
| "message" to "printerId is required" | |||||
| ) | |||||
| ) | |||||
| } | |||||
| if (itemIds.isEmpty()) { | |||||
| return ResponseEntity.badRequest().body( | |||||
| mapOf( | |||||
| "success" to false, | |||||
| "message" to "itemIds is required" | |||||
| ) | |||||
| ) | |||||
| } | |||||
| return try { | |||||
| val result = inventorySetup.printLotStockInLabelsByItemIdsV2( | |||||
| printerId = printerId, | |||||
| itemIds = itemIds, | |||||
| printQty = printQty, | |||||
| fromIndex = fromIndex, | |||||
| toIndex = toIndex | |||||
| ) | |||||
| val body = if (result.success) { | |||||
| mapOf( | |||||
| "success" to true, | |||||
| "message" to "Lot stock-in labels printed successfully", | |||||
| "lastIndex" to result.lastIndex, | |||||
| "totalLots" to result.totalLots | |||||
| ) | |||||
| } else { | |||||
| mapOf( | |||||
| "success" to false, | |||||
| "message" to (result.errorMessage ?: "Printer error"), | |||||
| "lastIndex" to result.lastIndex, | |||||
| "totalLots" to result.totalLots | |||||
| ) | |||||
| } | |||||
| ResponseEntity.ok(body) | |||||
| } catch (e: Exception) { | |||||
| e.printStackTrace() | |||||
| ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( | |||||
| mapOf( | |||||
| "success" to false, | |||||
| "message" to (e.message ?: "Unknown error occurred while printing lot stock-in labels") | |||||
| ) | |||||
| ) | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -353,5 +353,117 @@ open class InventorySetup { | |||||
| } | } | ||||
| return printedCount | return printedCount | ||||
| } | } | ||||
| data class PrintProgressResult( | |||||
| val success: Boolean, | |||||
| val lastIndex: Int, | |||||
| val totalLots: Int, | |||||
| val errorMessage: String? = null | |||||
| ) | |||||
| fun printLotStockInLabelsByItemIdsV2( | |||||
| printerId: Long, | |||||
| itemIds: List<Long>, | |||||
| printQty: Int = 1, | |||||
| fromIndex: Int? = null, | |||||
| toIndex: Int? = null | |||||
| ): PrintProgressResult { | |||||
| if (itemIds.isEmpty()) { | |||||
| println("No itemIds provided, nothing to print") | |||||
| return PrintProgressResult( | |||||
| success = false, | |||||
| lastIndex = -1, | |||||
| totalLots = 0, | |||||
| errorMessage = "itemIds is empty" | |||||
| ) | |||||
| } | |||||
| // 1. 查出并过滤 lot(可以按你 v2 的逻辑过滤 inQty-outQty=0) | |||||
| val allInventoryLotLines = inventoryLotLineRepository | |||||
| .findAllByItemIdIn(itemIds) | |||||
| .filter { it.deleted == false && it.inventoryLot?.stockInLine != null } | |||||
| if (allInventoryLotLines.isEmpty()) { | |||||
| println("No inventory lot lines found for itemIds=$itemIds") | |||||
| return PrintProgressResult( | |||||
| success = false, | |||||
| lastIndex = -1, | |||||
| totalLots = 0, | |||||
| errorMessage = "no lots found for given itemIds" | |||||
| ) | |||||
| } | |||||
| val totalLots = allInventoryLotLines.size | |||||
| // 如果要过滤 inQty-outQty = 0,就在这里再 filter 一次(你已经写过类似逻辑了) | |||||
| val lotsToUse = allInventoryLotLines // 或者 nonZeroLots | |||||
| val effectiveTotal = lotsToUse.size // 这就是你的 totalLots(按你的业务决定) | |||||
| // 2. 计算本次要打的范围 | |||||
| val startIndex = (fromIndex ?: 0).coerceAtLeast(0) | |||||
| val endIndex = (toIndex ?: (effectiveTotal - 1)).coerceAtMost(effectiveTotal - 1) | |||||
| if (startIndex > endIndex || startIndex >= effectiveTotal) { | |||||
| println("Invalid range: fromIndex=$fromIndex, toIndex=$toIndex, totalLots=$effectiveTotal") | |||||
| return PrintProgressResult( | |||||
| success = false, | |||||
| lastIndex = startIndex - 1, | |||||
| totalLots = effectiveTotal, | |||||
| errorMessage = "invalid index range" | |||||
| ) | |||||
| } | |||||
| println("Printing range: $startIndex to $endIndex (out of $effectiveTotal)") | |||||
| // 3. 循环打印,实时更新 lastIndex,出现打印错误时立即返回 | |||||
| var lastIndex = startIndex - 1 // 还没成功打印任何一张时是 startIndex-1 | |||||
| for (globalIndex in startIndex..endIndex) { | |||||
| val inventoryLotLine = lotsToUse[globalIndex] | |||||
| val stockInLineId = inventoryLotLine.inventoryLot?.stockInLine?.id | |||||
| ?: return PrintProgressResult( | |||||
| success = false, | |||||
| lastIndex = lastIndex, | |||||
| totalLots = effectiveTotal, | |||||
| errorMessage = "Stock in line missing for inventoryLotLineId=${inventoryLotLine.id}" | |||||
| ) | |||||
| try { | |||||
| println("Processing lot ${globalIndex + 1}/$effectiveTotal: lotNo=${inventoryLotLine.inventoryLot?.lotNo}, stockInLineId=$stockInLineId") | |||||
| stockInLineService.printQrCode( | |||||
| PrintQrCodeForSilRequest( | |||||
| stockInLineId = stockInLineId, | |||||
| printerId = printerId, | |||||
| printQty = printQty | |||||
| ) | |||||
| ) | |||||
| lastIndex = globalIndex | |||||
| println("✓ Printed label for lotNo=${inventoryLotLine.inventoryLot?.lotNo}") | |||||
| } catch (e: Exception) { | |||||
| // 打印异常(最常见:打印机没纸 / 打印机错误) | |||||
| println("✗ Printer error at index=$globalIndex, lotLineId=${inventoryLotLine.id}: ${e.message}") | |||||
| e.printStackTrace() | |||||
| val msg = when { | |||||
| e.message?.contains("paper", ignoreCase = true) == true -> | |||||
| "Printer error (maybe out of paper): ${e.message}" | |||||
| else -> | |||||
| "Printer error: ${e.message}" | |||||
| } | |||||
| return PrintProgressResult( | |||||
| success = false, | |||||
| lastIndex = lastIndex, // 最后成功打印到哪一张 | |||||
| totalLots = effectiveTotal, | |||||
| errorMessage = msg | |||||
| ) | |||||
| } | |||||
| } | |||||
| // 能循环走完,说明这段全部打完 | |||||
| return PrintProgressResult( | |||||
| success = true, | |||||
| lastIndex = lastIndex, // 成功场景下,这里会等于 endIndex | |||||
| totalLots = effectiveTotal, | |||||
| errorMessage = null | |||||
| ) | |||||
| } | |||||
| } | } | ||||
| @@ -30,7 +30,8 @@ interface JobOrderInfo { | |||||
| @get:Value("#{target.stockInLines?.size() > 0 ? target.stockInLines[0].escalationLog.^[status.value == 'pending']?.handler?.id : null}") | @get:Value("#{target.stockInLines?.size() > 0 ? target.stockInLines[0].escalationLog.^[status.value == 'pending']?.handler?.id : null}") | ||||
| val silHandlerId: Long?; | val silHandlerId: Long?; | ||||
| @get:Value("#{target.stockInLines?.size() > 0 ? target.stockInLines[0].lotNo : null}") | |||||
| val lotNo: String? | |||||
| val planStart: LocalDateTime?; | val planStart: LocalDateTime?; | ||||
| // @get:Value("#{target.bom.item.itemUoms.^[salesUnit == true && deleted == false]?.uom}") | // @get:Value("#{target.bom.item.itemUoms.^[salesUnit == true && deleted == false]?.uom}") | ||||
| //// @get:Value("#{target.bom.item.itemUoms.^[salesUnit == true && deleted == false]?.uom.udfudesc}") | //// @get:Value("#{target.bom.item.itemUoms.^[salesUnit == true && deleted == false]?.uom.udfudesc}") | ||||
| @@ -66,7 +67,8 @@ data class JobOrderInfoWithTypeName( | |||||
| val productionPriority: Int?, | val productionPriority: Int?, | ||||
| val status: String, | val status: String, | ||||
| val jobTypeId: Long?, | val jobTypeId: Long?, | ||||
| val jobTypeName: String? | |||||
| val jobTypeName: String?, | |||||
| val lotNo: String? | |||||
| ) | ) | ||||
| // Job Order | // Job Order | ||||
| interface JobOrderDetailWithJsonString { | interface JobOrderDetailWithJsonString { | ||||
| @@ -1809,7 +1809,7 @@ open fun getAllJoPickOrders(isDrink: Boolean?): List<AllJoPickOrderResponse> { | |||||
| val bom = jobOrder.bom | val bom = jobOrder.bom | ||||
| // 按 isDrink 过滤:null 表示不过滤(全部) | // 按 isDrink 过滤:null 表示不过滤(全部) | ||||
| if (isDrink != null && bom?.isDrink != isDrink) return@mapNotNull null | |||||
| if (isDrink != null && bom?.isDrink != isDrink) return@mapNotNull null | |||||
| println("BOM found: ${bom?.id}") | println("BOM found: ${bom?.id}") | ||||
| val item = bom?.item | val item = bom?.item | ||||
| @@ -1831,7 +1831,10 @@ if (isDrink != null && bom?.isDrink != isDrink) return@mapNotNull null | |||||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id ?: return@mapNotNull null) | val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id ?: return@mapNotNull null) | ||||
| val finishedLines = pickOrderLines.count { it.status == PickOrderLineStatus.COMPLETED } | val finishedLines = pickOrderLines.count { it.status == PickOrderLineStatus.COMPLETED } | ||||
| val jobOrderType = jobOrder.jobTypeId?.let { jobTypeRepository.findById(it).orElse(null) } | val jobOrderType = jobOrder.jobTypeId?.let { jobTypeRepository.findById(it).orElse(null) } | ||||
| val stockInLine = stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse( | |||||
| jobOrder.id ?: return@mapNotNull null | |||||
| ) | |||||
| val lotNo = stockInLine?.lotNo | |||||
| println("✅ Building response for pick order ${pickOrder.id}") | println("✅ Building response for pick order ${pickOrder.id}") | ||||
| val floorPickCounts = floorCountsByPickOrderId[pickOrder.id]?.map { row -> | val floorPickCounts = floorCountsByPickOrderId[pickOrder.id]?.map { row -> | ||||
| com.ffii.fpsms.modules.jobOrder.web.model.FloorPickCountDto( | com.ffii.fpsms.modules.jobOrder.web.model.FloorPickCountDto( | ||||
| @@ -1854,6 +1857,7 @@ if (isDrink != null && bom?.isDrink != isDrink) return@mapNotNull null | |||||
| //uomId = bom.outputQtyUom?.id : 0L, | //uomId = bom.outputQtyUom?.id : 0L, | ||||
| uomId = 0, | uomId = 0, | ||||
| uomName = bom?.outputQtyUom?: "", | uomName = bom?.outputQtyUom?: "", | ||||
| lotNo = lotNo, | |||||
| jobOrderStatus = jobOrder.status?.value ?: "", | jobOrderStatus = jobOrder.status?.value ?: "", | ||||
| finishedPickOLineCount = finishedLines, | finishedPickOLineCount = finishedLines, | ||||
| floorPickCounts = floorPickCounts | floorPickCounts = floorPickCounts | ||||
| @@ -192,7 +192,8 @@ open class JobOrderService( | |||||
| productionPriority = info.productionPriority, | productionPriority = info.productionPriority, | ||||
| status = info.status, | status = info.status, | ||||
| jobTypeId = info.jobTypeId, | jobTypeId = info.jobTypeId, | ||||
| jobTypeName = info.jobTypeId?.let { jobTypes[it]?.name } | |||||
| jobTypeName = info.jobTypeId?.let { jobTypes[it]?.name }, | |||||
| lotNo = info.lotNo | |||||
| ) | ) | ||||
| } | } | ||||
| .filter { info -> | .filter { info -> | ||||
| @@ -52,6 +52,7 @@ data class AllJoPickOrderResponse( | |||||
| val reqQty: BigDecimal, | val reqQty: BigDecimal, | ||||
| val uomId: Long, | val uomId: Long, | ||||
| val uomName: String, | val uomName: String, | ||||
| val lotNo: String?, | |||||
| val jobOrderStatus: String, | val jobOrderStatus: String, | ||||
| val finishedPickOLineCount: Int, | val finishedPickOLineCount: Int, | ||||
| val floorPickCounts: List<FloorPickCountDto> = emptyList() | val floorPickCounts: List<FloorPickCountDto> = emptyList() | ||||
| @@ -1414,6 +1414,7 @@ open class ProductProcessService( | |||||
| }, | }, | ||||
| RequiredQty = jobOrder?.reqQty?.toInt() ?: 0, | RequiredQty = jobOrder?.reqQty?.toInt() ?: 0, | ||||
| Uom = bomUom?.udfudesc, | Uom = bomUom?.udfudesc, | ||||
| lotNo = stockInLine?.lotNo, | |||||
| productionPriority = productProcesses.productionPriority, | productionPriority = productProcesses.productionPriority, | ||||
| date = productProcesses.date, | date = productProcesses.date, | ||||
| bomId = productProcesses.bom?.id, | bomId = productProcesses.bom?.id, | ||||
| @@ -169,6 +169,7 @@ data class AllJoborderProductProcessInfoResponse( | |||||
| val matchStatus: String?, | val matchStatus: String?, | ||||
| val RequiredQty: Int?, | val RequiredQty: Int?, | ||||
| val Uom: String?, | val Uom: String?, | ||||
| val lotNo: String?, | |||||
| val jobOrderId: Long?, | val jobOrderId: Long?, | ||||
| val productionPriority: Int?, | val productionPriority: Int?, | ||||
| val jobOrderCode: String?, | val jobOrderCode: String?, | ||||
| @@ -78,4 +78,12 @@ fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine? | |||||
| ORDER BY sil.purchaseOrder.id | ORDER BY sil.purchaseOrder.id | ||||
| """) | """) | ||||
| fun findCompletedDnByReceiptDate(@Param("receiptDate") receiptDate: LocalDate): List<StockInLine> | fun findCompletedDnByReceiptDate(@Param("receiptDate") receiptDate: LocalDate): List<StockInLine> | ||||
| @Query(""" | |||||
| select max(sil.lotNo) | |||||
| from StockInLine sil | |||||
| where sil.deleted = false | |||||
| and sil.lotNo like concat(:prefix, '%') | |||||
| """) | |||||
| fun findLatestLotNoByPrefix(@Param("prefix") prefix: String): String? | |||||
| } | } | ||||
| @@ -110,7 +110,7 @@ open class StockInLineService( | |||||
| open fun getReceivedStockInLineInfo(stockInLineId: Long): StockInLineInfo { | open fun getReceivedStockInLineInfo(stockInLineId: Long): StockInLineInfo { | ||||
| return stockInLineRepository.findStockInLineInfoByIdAndStatusAndDeletedFalse(id = stockInLineId, status = StockInLineStatus.RECEIVED.status).orElseThrow() | return stockInLineRepository.findStockInLineInfoByIdAndStatusAndDeletedFalse(id = stockInLineId, status = StockInLineStatus.RECEIVED.status).orElseThrow() | ||||
| } | } | ||||
| /* | |||||
| open fun assignLotNo(): String { | open fun assignLotNo(): String { | ||||
| val prefix = "LT" | val prefix = "LT" | ||||
| // ✅ 每次调用都取今天的日期段 | // ✅ 每次调用都取今天的日期段 | ||||
| @@ -126,7 +126,32 @@ open class StockInLineService( | |||||
| latestCode = latestCode | latestCode = latestCode | ||||
| ) | ) | ||||
| } | } | ||||
| */ | |||||
| fun assignLotNo(): String { | |||||
| val prefix = "LT" | |||||
| // 建議統一用同一種日期格式,現在 StockInLineService 是 DEFAULT_FORMATTER (LocalDate.today) | |||||
| val midfix = LocalDate.now().format(CodeGenerator.DEFAULT_FORMATTER) | |||||
| val fullPrefix = "$prefix-$midfix" | |||||
| // 1) 今天在 inventory_lot 裡的最大 lotNo | |||||
| val latestFromInventory = | |||||
| inventoryLotRepository.findLatestLotNoByPrefix(fullPrefix) | |||||
| // 2) 今天在 stock_in_line 裡的最大 lotNo(不分來源:JO、PO、盤點...) | |||||
| val latestFromStockInLine = | |||||
| stockInLineRepository.findLatestLotNoByPrefix(fullPrefix) | |||||
| // 3) 兩邊取最大的一個當成 latestCode | |||||
| val latestCode = listOfNotNull(latestFromInventory, latestFromStockInLine) | |||||
| .maxOrNull() | |||||
| // 4) 丟給你現有的 CodeGenerator 產下一號 | |||||
| return CodeGenerator.generateNo( | |||||
| prefix = prefix, | |||||
| midfix = midfix, | |||||
| latestCode = latestCode, | |||||
| ) | |||||
| } | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| @Transactional | @Transactional | ||||
| open fun create(request: SaveStockInLineRequest): MessageResponse { | open fun create(request: SaveStockInLineRequest): MessageResponse { | ||||