| @@ -207,4 +207,71 @@ class SetupController( | |||
| ) | |||
| 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 | |||
| } | |||
| 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}") | |||
| val silHandlerId: Long?; | |||
| @get:Value("#{target.stockInLines?.size() > 0 ? target.stockInLines[0].lotNo : null}") | |||
| val lotNo: String? | |||
| 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.udfudesc}") | |||
| @@ -66,7 +67,8 @@ data class JobOrderInfoWithTypeName( | |||
| val productionPriority: Int?, | |||
| val status: String, | |||
| val jobTypeId: Long?, | |||
| val jobTypeName: String? | |||
| val jobTypeName: String?, | |||
| val lotNo: String? | |||
| ) | |||
| // Job Order | |||
| interface JobOrderDetailWithJsonString { | |||
| @@ -1809,7 +1809,7 @@ open fun getAllJoPickOrders(isDrink: Boolean?): List<AllJoPickOrderResponse> { | |||
| val bom = jobOrder.bom | |||
| // 按 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}") | |||
| 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 finishedLines = pickOrderLines.count { it.status == PickOrderLineStatus.COMPLETED } | |||
| 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}") | |||
| val floorPickCounts = floorCountsByPickOrderId[pickOrder.id]?.map { row -> | |||
| 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 = 0, | |||
| uomName = bom?.outputQtyUom?: "", | |||
| lotNo = lotNo, | |||
| jobOrderStatus = jobOrder.status?.value ?: "", | |||
| finishedPickOLineCount = finishedLines, | |||
| floorPickCounts = floorPickCounts | |||
| @@ -194,7 +194,8 @@ open class JobOrderService( | |||
| productionPriority = info.productionPriority, | |||
| status = info.status, | |||
| jobTypeId = info.jobTypeId, | |||
| jobTypeName = info.jobTypeId?.let { jobTypes[it]?.name } | |||
| jobTypeName = info.jobTypeId?.let { jobTypes[it]?.name }, | |||
| lotNo = info.lotNo | |||
| ) | |||
| } | |||
| .filter { info -> | |||
| @@ -52,6 +52,7 @@ data class AllJoPickOrderResponse( | |||
| val reqQty: BigDecimal, | |||
| val uomId: Long, | |||
| val uomName: String, | |||
| val lotNo: String?, | |||
| val jobOrderStatus: String, | |||
| val finishedPickOLineCount: Int, | |||
| val floorPickCounts: List<FloorPickCountDto> = emptyList() | |||
| @@ -1414,6 +1414,7 @@ open class ProductProcessService( | |||
| }, | |||
| RequiredQty = jobOrder?.reqQty?.toInt() ?: 0, | |||
| Uom = bomUom?.udfudesc, | |||
| lotNo = stockInLine?.lotNo, | |||
| productionPriority = productProcesses.productionPriority, | |||
| date = productProcesses.date, | |||
| bomId = productProcesses.bom?.id, | |||
| @@ -169,6 +169,7 @@ data class AllJoborderProductProcessInfoResponse( | |||
| val matchStatus: String?, | |||
| val RequiredQty: Int?, | |||
| val Uom: String?, | |||
| val lotNo: String?, | |||
| val jobOrderId: Long?, | |||
| val productionPriority: Int?, | |||
| val jobOrderCode: String?, | |||
| @@ -78,4 +78,12 @@ fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine? | |||
| ORDER BY sil.purchaseOrder.id | |||
| """) | |||
| 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 { | |||
| return stockInLineRepository.findStockInLineInfoByIdAndStatusAndDeletedFalse(id = stockInLineId, status = StockInLineStatus.RECEIVED.status).orElseThrow() | |||
| } | |||
| /* | |||
| open fun assignLotNo(): String { | |||
| val prefix = "LT" | |||
| // ✅ 每次调用都取今天的日期段 | |||
| @@ -126,7 +126,32 @@ open class StockInLineService( | |||
| 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) | |||
| @Transactional | |||
| open fun create(request: SaveStockInLineRequest): MessageResponse { | |||