|
|
@@ -1339,6 +1339,38 @@ lot_root_origin AS ( |
|
|
.sorted() |
|
|
.sorted() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 建立區域輪前預覽:所選 section 目前沒有可預建明細的可用 lot |
|
|
|
|
|
*(與 [createPlaceholderStockTakeRecordsForStockTake] 規則一致:availableQty>0 且非 busy)。 |
|
|
|
|
|
* 建立後會變成「空區」單頭。 |
|
|
|
|
|
*/ |
|
|
|
|
|
open fun previewSectionsWithNoAvailableLots(sections: Collection<String>): List<String> { |
|
|
|
|
|
val wanted = sections.map { it.trim() }.filter { it.isNotBlank() }.distinct() |
|
|
|
|
|
if (wanted.isEmpty()) return emptyList() |
|
|
|
|
|
val busyLotKeys = busyOpenLotWarehouseKeys() |
|
|
|
|
|
val empty = ArrayList<String>() |
|
|
|
|
|
for (section in wanted) { |
|
|
|
|
|
val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(section) |
|
|
|
|
|
val warehouseIds = warehouses.mapNotNull { it.id } |
|
|
|
|
|
if (warehouseIds.isEmpty()) { |
|
|
|
|
|
empty.add(section) |
|
|
|
|
|
continue |
|
|
|
|
|
} |
|
|
|
|
|
val hasCreatable = inventoryLotLineRepository |
|
|
|
|
|
.findAllByWarehouseIdInAndDeletedIsFalseAndStatusIsAvailable(warehouseIds) |
|
|
|
|
|
.any { ill -> |
|
|
|
|
|
val lotId = ill.inventoryLot?.id ?: return@any false |
|
|
|
|
|
val warehouseId = ill.warehouse?.id ?: return@any false |
|
|
|
|
|
if ((lotId to warehouseId) in busyLotKeys) return@any false |
|
|
|
|
|
val availableQty = (ill.inQty ?: BigDecimal.ZERO) |
|
|
|
|
|
.subtract(ill.outQty ?: BigDecimal.ZERO) |
|
|
|
|
|
availableQty.compareTo(BigDecimal.ZERO) > 0 |
|
|
|
|
|
} |
|
|
|
|
|
if (!hasCreatable) empty.add(section) |
|
|
|
|
|
} |
|
|
|
|
|
return empty.sorted() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 建立區域輪前預覽:所選 section 內已被 open「依貨品」輪佔用的貨品 |
|
|
* 建立區域輪前預覽:所選 section 內已被 open「依貨品」輪佔用的貨品 |
|
|
*(Option B 會略過這些 lot,本輪不會盤到)。 |
|
|
*(Option B 會略過這些 lot,本輪不會盤到)。 |
|
|
@@ -1892,6 +1924,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
val totalLots = countedRecords.size |
|
|
val totalLots = countedRecords.size |
|
|
val stockTakenCount: Int |
|
|
val stockTakenCount: Int |
|
|
val approvedCount: Int |
|
|
val approvedCount: Int |
|
|
|
|
|
val sectionCountForProgress: Int |
|
|
if (itemMode) { |
|
|
if (itemMode) { |
|
|
stockTakenCount = countedRecords.count { |
|
|
stockTakenCount = countedRecords.count { |
|
|
it.pickerFirstStockTakeQty != null || it.status == "pass" || it.status == "completed" |
|
|
it.pickerFirstStockTakeQty != null || it.status == "pass" || it.status == "completed" |
|
|
@@ -1899,8 +1932,13 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
approvedCount = countedRecords.count { |
|
|
approvedCount = countedRecords.count { |
|
|
it.status == "completed" || it.approverStockTakeQty != null || it.approverTime != null |
|
|
it.status == "completed" || it.approverStockTakeQty != null || it.approverTime != null |
|
|
} |
|
|
} |
|
|
|
|
|
sectionCountForProgress = sections.size |
|
|
} else { |
|
|
} else { |
|
|
val bySection = records.groupBy { it.stockTakeSection?.trim()?.lowercase() } |
|
|
val bySection = records.groupBy { it.stockTakeSection?.trim()?.lowercase() } |
|
|
|
|
|
// Empty sections (header with 0 records) do not count toward progress denominator. |
|
|
|
|
|
val sectionsWithLots = sections.filter { sec -> |
|
|
|
|
|
bySection[sec.lowercase()].orEmpty().isNotEmpty() |
|
|
|
|
|
} |
|
|
fun sectionComplete(section: String, approved: Boolean): Boolean { |
|
|
fun sectionComplete(section: String, approved: Boolean): Boolean { |
|
|
val group = bySection[section.lowercase()].orEmpty() |
|
|
val group = bySection[section.lowercase()].orEmpty() |
|
|
if (group.isEmpty()) return false |
|
|
if (group.isEmpty()) return false |
|
|
@@ -1910,8 +1948,9 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
group.all { it.pickerFirstStockTakeQty != null } |
|
|
group.all { it.pickerFirstStockTakeQty != null } |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
stockTakenCount = sections.count { sectionComplete(it, false) } |
|
|
|
|
|
approvedCount = sections.count { sectionComplete(it, true) } |
|
|
|
|
|
|
|
|
stockTakenCount = sectionsWithLots.count { sectionComplete(it, false) } |
|
|
|
|
|
approvedCount = sectionsWithLots.count { sectionComplete(it, true) } |
|
|
|
|
|
sectionCountForProgress = sectionsWithLots.size |
|
|
} |
|
|
} |
|
|
return StockTakeRoundCardResponse( |
|
|
return StockTakeRoundCardResponse( |
|
|
stockTakeRoundId = roundKey, |
|
|
stockTakeRoundId = roundKey, |
|
|
@@ -1921,7 +1960,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
yearMonth = yearMonth, |
|
|
yearMonth = yearMonth, |
|
|
open = open, |
|
|
open = open, |
|
|
status = statusValue, |
|
|
status = statusValue, |
|
|
sectionCount = sections.size, |
|
|
|
|
|
|
|
|
sectionCount = sectionCountForProgress, |
|
|
totalItemNumber = countedRecords.mapNotNull { it.itemId }.distinct().size, |
|
|
totalItemNumber = countedRecords.mapNotNull { it.itemId }.distinct().size, |
|
|
totalInventoryLotNumber = totalLots, |
|
|
totalInventoryLotNumber = totalLots, |
|
|
stockTakenCount = stockTakenCount, |
|
|
stockTakenCount = stockTakenCount, |
|
|
@@ -2405,7 +2444,8 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
existingRecord |
|
|
existingRecord |
|
|
} |
|
|
} |
|
|
existingRecord.pickerFirstStockTakeQty == null -> { |
|
|
existingRecord.pickerFirstStockTakeQty == null -> { |
|
|
val isCompled = inventoryLotLine.inQty == inventoryLotLine.outQty |
|
|
|
|
|
|
|
|
// Always pass (await approver). Do not auto-complete when inQty==outQty — |
|
|
|
|
|
// that left status=completed with null approver and hid rows on approver UI. |
|
|
val varianceQty = availableQty - request.qty - request.badQty |
|
|
val varianceQty = availableQty - request.qty - request.badQty |
|
|
existingRecord.apply { |
|
|
existingRecord.apply { |
|
|
this.stockTakeRoundId = this.stockTakeRoundId ?: (stockTake.stockTakeRoundId ?: stockTake.id) |
|
|
this.stockTakeRoundId = this.stockTakeRoundId ?: (stockTake.stockTakeRoundId ?: stockTake.id) |
|
|
@@ -2413,7 +2453,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
this.pickerFirstBadQty = request.badQty |
|
|
this.pickerFirstBadQty = request.badQty |
|
|
this.bookQty = availableQty |
|
|
this.bookQty = availableQty |
|
|
this.varianceQty = varianceQty |
|
|
this.varianceQty = varianceQty |
|
|
this.status = if (isCompled) "completed" else "pass" |
|
|
|
|
|
|
|
|
this.status = "pass" |
|
|
this.remarks = request.remark ?: this.remarks |
|
|
this.remarks = request.remark ?: this.remarks |
|
|
this.stockTakerId = stockTakerId |
|
|
this.stockTakerId = stockTakerId |
|
|
this.stockTakerName = user?.name |
|
|
this.stockTakerName = user?.name |
|
|
@@ -2448,7 +2488,6 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
else -> throw IllegalArgumentException("Stock take record already has first and second counts") |
|
|
else -> throw IllegalArgumentException("Stock take record already has first and second counts") |
|
|
} |
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
val isCompled = inventoryLotLine.inQty == inventoryLotLine.outQty |
|
|
|
|
|
val varianceQty = availableQty - request.qty - request.badQty |
|
|
val varianceQty = availableQty - request.qty - request.badQty |
|
|
StockTakeRecord().apply { |
|
|
StockTakeRecord().apply { |
|
|
this.itemId = item.id |
|
|
this.itemId = item.id |
|
|
@@ -2466,7 +2505,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
this.varianceQty = varianceQty |
|
|
this.varianceQty = varianceQty |
|
|
this.uom = inventoryLotLine.stockUom?.uom?.udfudesc |
|
|
this.uom = inventoryLotLine.stockUom?.uom?.udfudesc |
|
|
this.date = java.time.LocalDate.now() |
|
|
this.date = java.time.LocalDate.now() |
|
|
this.status = if (isCompled) "completed" else "pass" |
|
|
|
|
|
|
|
|
this.status = "pass" |
|
|
this.remarks = request.remark |
|
|
this.remarks = request.remark |
|
|
this.itemCode = item.code |
|
|
this.itemCode = item.code |
|
|
this.itemName = item.name |
|
|
this.itemName = item.name |
|
|
|