|
|
|
@@ -7,6 +7,7 @@ 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.enums.StockTakeRoundMode |
|
|
|
import com.ffii.fpsms.modules.stock.web.model.* |
|
|
|
import org.slf4j.Logger |
|
|
|
import org.slf4j.LoggerFactory |
|
|
|
@@ -81,7 +82,11 @@ open class StockTakeRecordService( |
|
|
|
* 建立該 section 的 stock_take 後,依當下「可用」庫存行預先建立本輪全部盤點明細(pending),盤點員儲存時改為更新。 |
|
|
|
*/ |
|
|
|
@Transactional |
|
|
|
open fun createPlaceholderStockTakeRecordsForStockTake(stockTake: StockTake, placeholderStockTakerId: Long) { |
|
|
|
open fun createPlaceholderStockTakeRecordsForStockTake( |
|
|
|
stockTake: StockTake, |
|
|
|
placeholderStockTakerId: Long, |
|
|
|
itemIds: Set<Long>? = null, |
|
|
|
) { |
|
|
|
val section = stockTake.stockTakeSection ?: return |
|
|
|
val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(section) |
|
|
|
val warehouseIds = warehouses.mapNotNull { it.id } |
|
|
|
@@ -92,6 +97,9 @@ open class StockTakeRecordService( |
|
|
|
|
|
|
|
val inventoryLotLines = |
|
|
|
inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalseAndStatusIsAvailable(warehouseIds) |
|
|
|
.filter { ill -> |
|
|
|
itemIds == null || (ill.inventoryLot?.item?.id?.let { id -> id in itemIds } == true) |
|
|
|
} |
|
|
|
if (inventoryLotLines.isEmpty()) return |
|
|
|
|
|
|
|
val toSave = ArrayList<StockTakeRecord>(inventoryLotLines.size) |
|
|
|
@@ -401,7 +409,13 @@ open class StockTakeRecordService( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
open fun AllPickedStockTakeList(): List<AllPickedStockTakeListReponse> { |
|
|
|
open fun AllPickedStockTakeList(targetRoundKey: Long? = null): List<AllPickedStockTakeListReponse> { |
|
|
|
if (targetRoundKey != null && targetRoundKey > 0L) { |
|
|
|
val roundHeaders = stockTakesForRound(targetRoundKey) |
|
|
|
if (roundHeaders.any { it.stockTakeRoundMode == StockTakeRoundMode.ITEM }) { |
|
|
|
return listItemCardsForRound(targetRoundKey, roundHeaders) |
|
|
|
} |
|
|
|
} |
|
|
|
// 1. 获取所有不同的 stockTakeSection(从 warehouse 表) |
|
|
|
val allWarehouses = warehouseRepository.findAllByDeletedIsFalse() |
|
|
|
val distinctSections = allWarehouses |
|
|
|
@@ -418,13 +432,20 @@ open class StockTakeRecordService( |
|
|
|
.filter { it.stockTakeSection != null && !it.stockTakeSection!!.isBlank() } |
|
|
|
.groupBy { it.stockTakeSection!! } |
|
|
|
|
|
|
|
val allStockTakes = stockTakeRepository.findAll() |
|
|
|
.filter { !it.deleted } |
|
|
|
.groupBy { it.stockTakeSection } |
|
|
|
|
|
|
|
val scopedRound = targetRoundKey != null && targetRoundKey > 0L |
|
|
|
val allStockTakes = ( |
|
|
|
if (scopedRound) { |
|
|
|
stockTakeRepository.findAllByStockTakeRoundIdAndDeletedIsFalse(targetRoundKey!!) |
|
|
|
} else { |
|
|
|
stockTakeRepository.findAll().filter { !it.deleted } |
|
|
|
} |
|
|
|
).groupBy { it.stockTakeSection } |
|
|
|
|
|
|
|
val allStockTakeRecords = stockTakeRecordRepository.findAll() |
|
|
|
.filter { !it.deleted } |
|
|
|
val allStockTakeRecords = if (scopedRound) { |
|
|
|
stockTakeRecordRepository.findAllByStockTakeRoundIdAndDeletedIsFalse(targetRoundKey!!) |
|
|
|
} else { |
|
|
|
stockTakeRecordRepository.findAll().filter { !it.deleted } |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
val recordsByWarehouseId = allStockTakeRecords |
|
|
|
@@ -433,7 +454,7 @@ open class StockTakeRecordService( |
|
|
|
|
|
|
|
val result = mutableListOf<AllPickedStockTakeListReponse>() |
|
|
|
var idCounter = 1L |
|
|
|
val globalLatestRoundKey = resolveGlobalLatestStockTakeRoundKey() |
|
|
|
val globalLatestRoundKey = targetRoundKey ?: resolveGlobalLatestStockTakeRoundKey() |
|
|
|
|
|
|
|
// 3. 为每个 stockTakeSection 创建一个卡片 |
|
|
|
distinctSections.forEach { stockTakeSection -> |
|
|
|
@@ -456,10 +477,22 @@ open class StockTakeRecordService( |
|
|
|
val lastStockTakeDate = completedStockTakes.firstOrNull()?.actualEnd |
|
|
|
|
|
|
|
|
|
|
|
val latestStockTake = resolveLatestStockTakeForSection( |
|
|
|
stockTakesForSection, |
|
|
|
globalLatestRoundKey |
|
|
|
) |
|
|
|
val latestStockTake = if (targetRoundKey != null && targetRoundKey > 0L) { |
|
|
|
stockTakesForSection.filter { st -> |
|
|
|
(st.stockTakeRoundId ?: st.id) == targetRoundKey |
|
|
|
}.maxWithOrNull( |
|
|
|
compareBy<StockTake> { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } |
|
|
|
.thenBy { it.id ?: 0L } |
|
|
|
) |
|
|
|
} else { |
|
|
|
resolveLatestStockTakeForSection( |
|
|
|
stockTakesForSection, |
|
|
|
globalLatestRoundKey |
|
|
|
) |
|
|
|
} |
|
|
|
if (targetRoundKey != null && targetRoundKey > 0L && latestStockTake == null) { |
|
|
|
return@forEach |
|
|
|
} |
|
|
|
|
|
|
|
val status = if (latestStockTake != null) { |
|
|
|
latestStockTake.status?.value ?: "pending" |
|
|
|
@@ -578,6 +611,91 @@ open class StockTakeRecordService( |
|
|
|
return result.sortedBy { it.stockTakeSession } |
|
|
|
} |
|
|
|
|
|
|
|
/** 只留仍對得到未刪除庫存行的明細,避免舊佔位列把貨品狀態和批數算歪。 */ |
|
|
|
private fun liveStockTakeRecords(records: List<StockTakeRecord>): List<StockTakeRecord> { |
|
|
|
val lotIds = records.mapNotNull { it.lotId }.distinct() |
|
|
|
val warehouseIds = records.mapNotNull { it.warehouse?.id }.distinct() |
|
|
|
if (lotIds.isEmpty() || warehouseIds.isEmpty()) return emptyList() |
|
|
|
val livePairs = inventoryLotLineRepository |
|
|
|
.findAllByWarehouseIdInAndInventoryLotIdInAndDeletedIsFalse(warehouseIds, lotIds) |
|
|
|
.map { (it.inventoryLot?.id ?: 0L) to (it.warehouse?.id ?: 0L) } |
|
|
|
.toSet() |
|
|
|
return records.filter { rec -> |
|
|
|
val lotId = rec.lotId ?: return@filter false |
|
|
|
val warehouseId = rec.warehouse?.id ?: return@filter false |
|
|
|
livePairs.contains(lotId to warehouseId) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun listItemCardsForRound( |
|
|
|
roundKey: Long, |
|
|
|
headers: List<StockTake>, |
|
|
|
): List<AllPickedStockTakeListReponse> { |
|
|
|
val records = liveStockTakeRecords( |
|
|
|
stockTakeRecordRepository.findAllByStockTakeRoundIdAndDeletedIsFalse(roundKey) |
|
|
|
.filter { !it.deleted } |
|
|
|
) |
|
|
|
val byItem = records.groupBy { it.itemId ?: 0L }.filterKeys { it > 0L } |
|
|
|
val planStartDate = headers.mapNotNull { it.planStart }.maxOrNull()?.toLocalDate() |
|
|
|
val headerBySection = headers.associateBy { it.stockTakeSection } |
|
|
|
return byItem.entries.mapIndexed { idx, (itemId, recs) -> |
|
|
|
val code = recs.firstOrNull { !it.itemCode.isNullOrBlank() }?.itemCode ?: "#$itemId" |
|
|
|
val name = recs.firstOrNull { !it.itemName.isNullOrBlank() }?.itemName |
|
|
|
val total = recs.size |
|
|
|
val taken = recs.count { it.pickerFirstStockTakeQty != null } |
|
|
|
val notMatch = recs.any { it.status == "notMatch" } |
|
|
|
val sections = recs.mapNotNull { it.stockTakeSection?.trim()?.takeIf { s -> s.isNotBlank() } }.distinct() |
|
|
|
val involvedHeaders = sections.mapNotNull { headerBySection[it] }.ifEmpty { headers } |
|
|
|
val open = involvedHeaders.any { it.status?.isOpen() == true } |
|
|
|
val allCounted = recs.isNotEmpty() && recs.all { |
|
|
|
it.pickerFirstStockTakeQty != null || it.status == "pass" || it.status == "completed" |
|
|
|
} |
|
|
|
val allApproved = recs.isNotEmpty() && recs.all { |
|
|
|
it.status == "completed" || it.approverTime != null |
|
|
|
} |
|
|
|
val anyCounted = recs.any { |
|
|
|
it.pickerFirstStockTakeQty != null || it.status == "pass" || it.status == "completed" |
|
|
|
} |
|
|
|
val statusValue = when { |
|
|
|
!open -> StockTakeStatus.COMPLETED.value |
|
|
|
notMatch -> StockTakeStatus.STOCKTAKING.value |
|
|
|
allApproved -> StockTakeStatus.COMPLETED.value |
|
|
|
allCounted -> StockTakeStatus.APPROVING.value |
|
|
|
anyCounted -> StockTakeStatus.STOCKTAKING.value |
|
|
|
else -> StockTakeStatus.PENDING.value |
|
|
|
} |
|
|
|
val floors = recs.mapNotNull { it.warehouse?.store_id?.trim()?.takeIf { f -> f.isNotBlank() } } |
|
|
|
val mainFloor = floors.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key |
|
|
|
val representative = involvedHeaders.maxByOrNull { it.id ?: 0L } ?: headers.first() |
|
|
|
AllPickedStockTakeListReponse( |
|
|
|
id = (idx + 1).toLong(), |
|
|
|
stockTakeSession = code, |
|
|
|
lastStockTakeDate = representative.actualStart?.toLocalDate(), |
|
|
|
status = statusValue, |
|
|
|
currentStockTakeItemNumber = taken, |
|
|
|
totalInventoryLotNumber = total, |
|
|
|
stockTakeId = representative.id ?: 0L, |
|
|
|
stockTakeRoundId = roundKey, |
|
|
|
stockTakerName = recs.mapNotNull { it.stockTakerName }.lastOrNull(), |
|
|
|
TotalItemNumber = 1, |
|
|
|
approverName = recs.mapNotNull { it.approverName }.lastOrNull(), |
|
|
|
startTime = representative.actualStart, |
|
|
|
endTime = if (allCounted) { |
|
|
|
recs.mapNotNull { it.stockTakeEndTime }.maxOrNull() ?: representative.actualEnd |
|
|
|
} else { |
|
|
|
representative.actualEnd |
|
|
|
}, |
|
|
|
ReStockTakeTrueFalse = notMatch, |
|
|
|
planStartDate = planStartDate, |
|
|
|
stockTakeSectionDescription = name, |
|
|
|
warehouseArea = recs.mapNotNull { it.warehouse?.area }.distinct().joinToString(", ").ifBlank { null }, |
|
|
|
storeId = mainFloor, |
|
|
|
listMode = StockTakeRoundMode.ITEM, |
|
|
|
itemId = itemId, |
|
|
|
) |
|
|
|
}.sortedBy { it.stockTakeSession } |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 區域內用於卡片展示的 stock_take:若該區域參與全系統最新輪次,優先取該輪次內最新一筆; |
|
|
|
* 否則退回區域內 planStart/actualStart 最新一筆(供未開 onlyLatestRound 時顯示舊輪)。 |
|
|
|
@@ -1019,6 +1137,299 @@ open class StockTakeRecordService( |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
open fun getApproverStockTakeHeaderForRound(roundId: Long): AllPickedStockTakeListReponse? { |
|
|
|
val rows = stockTakesForRound(roundId) |
|
|
|
if (rows.isEmpty()) return null |
|
|
|
val representative = rows.maxWithOrNull( |
|
|
|
compareBy<StockTake> { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } |
|
|
|
.thenBy { it.id ?: 0L } |
|
|
|
) ?: return null |
|
|
|
val roundKey = representative.stockTakeRoundId ?: representative.id ?: roundId |
|
|
|
val planStartDate = rows.mapNotNull { it.planStart }.maxOrNull()?.toLocalDate() |
|
|
|
val open = rows.any { it.status?.isOpen() == true } |
|
|
|
val statusValue = when { |
|
|
|
open && rows.any { it.status == StockTakeStatus.APPROVING } -> StockTakeStatus.APPROVING.value |
|
|
|
open && rows.any { it.status == StockTakeStatus.STOCKTAKING } -> StockTakeStatus.STOCKTAKING.value |
|
|
|
open -> StockTakeStatus.PENDING.value |
|
|
|
else -> StockTakeStatus.COMPLETED.value |
|
|
|
} |
|
|
|
return AllPickedStockTakeListReponse( |
|
|
|
id = 1L, |
|
|
|
stockTakeSession = representative.stockTakeRoundName ?: "", |
|
|
|
lastStockTakeDate = representative.actualStart?.toLocalDate(), |
|
|
|
status = statusValue, |
|
|
|
currentStockTakeItemNumber = 0, |
|
|
|
totalInventoryLotNumber = 0, |
|
|
|
stockTakeId = representative.id ?: 0, |
|
|
|
stockTakeRoundId = roundKey, |
|
|
|
stockTakerName = null, |
|
|
|
approverName = null, |
|
|
|
TotalItemNumber = 0, |
|
|
|
startTime = representative.actualStart, |
|
|
|
endTime = representative.actualEnd, |
|
|
|
ReStockTakeTrueFalse = false, |
|
|
|
planStartDate = planStartDate, |
|
|
|
stockTakeSectionDescription = null, |
|
|
|
warehouseArea = null, |
|
|
|
storeId = null |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
open fun busyStockTakeSections(): List<String> { |
|
|
|
return stockTakeRepository.findAll() |
|
|
|
.filter { !it.deleted && it.status?.isOpen() == true } |
|
|
|
.mapNotNull { it.stockTakeSection?.trim() } |
|
|
|
.filter { it.isNotBlank() } |
|
|
|
.distinct() |
|
|
|
.sorted() |
|
|
|
} |
|
|
|
|
|
|
|
open fun listStockTakeRoundCards(yearMonth: String?): StockTakeRoundCardsRes { |
|
|
|
val all = stockTakeRepository.findAll().filter { !it.deleted } |
|
|
|
if (all.isEmpty()) { |
|
|
|
val thisMonth = java.time.YearMonth.now().toString() |
|
|
|
return StockTakeRoundCardsRes( |
|
|
|
hasOpenRounds = false, |
|
|
|
rounds = emptyList(), |
|
|
|
completedYearMonths = listOf(thisMonth), |
|
|
|
) |
|
|
|
} |
|
|
|
val groups = all.groupBy { st -> st.stockTakeRoundId ?: st.id ?: 0L } |
|
|
|
.filterKeys { it > 0L } |
|
|
|
val recordsByRound = stockTakeRecordRepository.findAll() |
|
|
|
.filter { !it.deleted } |
|
|
|
.groupBy { rec -> rec.stockTakeRoundId ?: rec.stockTake?.id ?: 0L } |
|
|
|
val floorsBySection = warehouseRepository.findAllByDeletedIsFalse() |
|
|
|
.mapNotNull { w -> |
|
|
|
val section = w.stockTakeSection?.trim()?.takeIf { it.isNotBlank() } ?: return@mapNotNull null |
|
|
|
val floor = w.store_id?.trim()?.takeIf { it.isNotBlank() } |
|
|
|
section to floor |
|
|
|
} |
|
|
|
.groupBy({ it.first }, { it.second }) |
|
|
|
val cards = groups.map { (roundKey, rows) -> |
|
|
|
val headerIds = rows.mapNotNull { it.id }.toSet() |
|
|
|
val records = recordsByRound[roundKey].orEmpty().ifEmpty { |
|
|
|
recordsByRound.filterKeys { it in headerIds }.values.flatten() |
|
|
|
} |
|
|
|
toRoundCard(roundKey, rows, records, floorsBySection) |
|
|
|
} |
|
|
|
.sortedByDescending { it.stockTakeRoundId } |
|
|
|
val thisMonth = java.time.YearMonth.now().toString() |
|
|
|
val hasOpenRounds = cards.any { it.open } |
|
|
|
val completedYearMonths = ( |
|
|
|
cards.filter { !it.open }.map { it.yearMonth } + thisMonth |
|
|
|
).distinct().filter { it <= thisMonth }.sortedDescending() |
|
|
|
val rounds = if (hasOpenRounds) { |
|
|
|
val openCards = cards.filter { it.open } |
|
|
|
val thisMonthOpen = openCards.filter { it.yearMonth == thisMonth } |
|
|
|
thisMonthOpen + openCards.filter { it.yearMonth != thisMonth } |
|
|
|
} else { |
|
|
|
val ym = yearMonth?.trim()?.takeIf { it.matches(Regex("\\d{4}-\\d{2}")) } ?: thisMonth |
|
|
|
cards.filter { !it.open && it.yearMonth == ym } |
|
|
|
} |
|
|
|
return StockTakeRoundCardsRes( |
|
|
|
hasOpenRounds = hasOpenRounds, |
|
|
|
rounds = rounds, |
|
|
|
completedYearMonths = completedYearMonths, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
@Transactional |
|
|
|
open fun closeStockTakeRound(roundId: Long): Map<String, Any> { |
|
|
|
val rows = stockTakesForRound(roundId) |
|
|
|
if (rows.isEmpty()) { |
|
|
|
return mapOf("success" to false, "message" to "Stock take round not found: $roundId") |
|
|
|
} |
|
|
|
val now = LocalDateTime.now() |
|
|
|
rows.forEach { st -> |
|
|
|
st.status = StockTakeStatus.COMPLETED |
|
|
|
if (st.actualEnd == null) { |
|
|
|
st.actualEnd = now |
|
|
|
} |
|
|
|
stockTakeRepository.save(st) |
|
|
|
} |
|
|
|
return mapOf( |
|
|
|
"success" to true, |
|
|
|
"stockTakeRoundId" to roundId, |
|
|
|
"closedCount" to rows.size, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
private fun stockTakesForRound(roundId: Long): List<StockTake> { |
|
|
|
val byRound = stockTakeRepository.findAllByStockTakeRoundIdAndDeletedIsFalse(roundId) |
|
|
|
if (byRound.isNotEmpty()) return byRound |
|
|
|
return try { |
|
|
|
val one = stockTakeRepository.findByIdAndDeletedIsFalse(roundId) |
|
|
|
if (one != null) listOf(one) else emptyList() |
|
|
|
} catch (_: Exception) { |
|
|
|
emptyList() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun toRoundCard( |
|
|
|
roundKey: Long, |
|
|
|
rows: List<StockTake>, |
|
|
|
records: List<StockTakeRecord>, |
|
|
|
floorsBySection: Map<String, List<String?>>, |
|
|
|
): StockTakeRoundCardResponse { |
|
|
|
val representative = rows.maxWithOrNull( |
|
|
|
compareBy<StockTake> { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } |
|
|
|
.thenBy { it.id ?: 0L } |
|
|
|
) ?: rows.first() |
|
|
|
val planStart = rows.mapNotNull { it.planStart }.maxOrNull() |
|
|
|
val yearMonth = if (planStart != null) { |
|
|
|
"%04d-%02d".format(planStart.year, planStart.monthValue) |
|
|
|
} else { |
|
|
|
java.time.YearMonth.now().toString() |
|
|
|
} |
|
|
|
val open = rows.any { it.status?.isOpen() == true } |
|
|
|
val statusValue = when { |
|
|
|
open && rows.any { it.status == StockTakeStatus.APPROVING } -> StockTakeStatus.APPROVING.value |
|
|
|
open && rows.any { it.status == StockTakeStatus.STOCKTAKING } -> StockTakeStatus.STOCKTAKING.value |
|
|
|
open -> StockTakeStatus.PENDING.value |
|
|
|
else -> StockTakeStatus.COMPLETED.value |
|
|
|
} |
|
|
|
val name = rows.mapNotNull { it.stockTakeRoundName?.trim()?.takeIf { n -> n.isNotBlank() } }.firstOrNull() |
|
|
|
val sections = rows.mapNotNull { it.stockTakeSection?.trim()?.takeIf { s -> s.isNotBlank() } }.distinct() |
|
|
|
val floors = sections.flatMap { sec -> floorsBySection[sec].orEmpty().mapNotNull { it } } |
|
|
|
val mainFloor = floors.groupingBy { it }.eachCount().maxWithOrNull( |
|
|
|
compareBy<Map.Entry<String, Int>> { it.value }.thenByDescending { it.key } |
|
|
|
)?.key |
|
|
|
val itemMode = rows.any { it.stockTakeRoundMode == StockTakeRoundMode.ITEM } |
|
|
|
val countedRecords = if (itemMode) liveStockTakeRecords(records) else records |
|
|
|
val totalLots = countedRecords.size |
|
|
|
val stockTakenCount: Int |
|
|
|
val approvedCount: Int |
|
|
|
if (itemMode) { |
|
|
|
stockTakenCount = countedRecords.count { |
|
|
|
it.pickerFirstStockTakeQty != null || it.status == "pass" || it.status == "completed" |
|
|
|
} |
|
|
|
approvedCount = countedRecords.count { |
|
|
|
it.status == "completed" || it.approverStockTakeQty != null || it.approverTime != null |
|
|
|
} |
|
|
|
} else { |
|
|
|
val bySection = records.groupBy { it.stockTakeSection?.trim()?.lowercase() } |
|
|
|
fun sectionComplete(section: String, approved: Boolean): Boolean { |
|
|
|
val group = bySection[section.lowercase()].orEmpty() |
|
|
|
if (group.isEmpty()) return false |
|
|
|
return if (approved) { |
|
|
|
group.all { it.approverStockTakeQty != null || it.approverTime != null } |
|
|
|
} else { |
|
|
|
group.all { it.pickerFirstStockTakeQty != null } |
|
|
|
} |
|
|
|
} |
|
|
|
stockTakenCount = sections.count { sectionComplete(it, false) } |
|
|
|
approvedCount = sections.count { sectionComplete(it, true) } |
|
|
|
} |
|
|
|
return StockTakeRoundCardResponse( |
|
|
|
stockTakeRoundId = roundKey, |
|
|
|
stockTakeRoundName = name, |
|
|
|
stockTakeId = representative.id ?: 0L, |
|
|
|
planStartDate = planStart?.toLocalDate(), |
|
|
|
yearMonth = yearMonth, |
|
|
|
open = open, |
|
|
|
status = statusValue, |
|
|
|
sectionCount = sections.size, |
|
|
|
totalItemNumber = countedRecords.mapNotNull { it.itemId }.distinct().size, |
|
|
|
totalInventoryLotNumber = totalLots, |
|
|
|
stockTakenCount = stockTakenCount, |
|
|
|
approvedCount = approvedCount, |
|
|
|
mainFloor = mainFloor, |
|
|
|
stockTakeRoundMode = rows.mapNotNull { it.stockTakeRoundMode }.firstOrNull(), |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
open fun getInventoryLotDetailsByItem( |
|
|
|
itemId: Long, |
|
|
|
stockTakeRoundId: Long, |
|
|
|
pageNum: Int = 0, |
|
|
|
pageSize: Int = 10, |
|
|
|
notMatchOnly: Boolean = false, |
|
|
|
): RecordsRes<InventoryLotDetailResponse> { |
|
|
|
val records = stockTakeRecordRepository.findAllByStockTakeRoundIdAndDeletedIsFalse(stockTakeRoundId) |
|
|
|
.filter { |
|
|
|
!it.deleted && |
|
|
|
it.itemId == itemId && |
|
|
|
(!notMatchOnly || it.status == "notMatch") |
|
|
|
} |
|
|
|
if (records.isEmpty()) { |
|
|
|
return RecordsRes(emptyList(), 0) |
|
|
|
} |
|
|
|
val lotIds = records.mapNotNull { it.lotId }.toSet() |
|
|
|
val whIds = records.mapNotNull { it.warehouse?.id }.toSet() |
|
|
|
val illList = |
|
|
|
if (lotIds.isNotEmpty() && whIds.isNotEmpty()) { |
|
|
|
inventoryLotLineRepository.findAllByWarehouseIdInAndInventoryLotIdInAndDeletedIsFalse(whIds, lotIds) |
|
|
|
} else { |
|
|
|
emptyList() |
|
|
|
} |
|
|
|
val illByPair = illList.associateBy { |
|
|
|
Pair(it.inventoryLot?.id ?: 0L, it.warehouse?.id ?: 0L) |
|
|
|
} |
|
|
|
val allResults = records.mapNotNull { rec -> |
|
|
|
val ill = illByPair[Pair(rec.lotId ?: 0L, rec.warehouse?.id ?: 0L)] |
|
|
|
?: return@mapNotNull null |
|
|
|
val inventoryLot = ill.inventoryLot |
|
|
|
val item = inventoryLot?.item |
|
|
|
val warehouse = ill.warehouse |
|
|
|
val availableQty = (ill.inQty ?: BigDecimal.ZERO) |
|
|
|
.subtract(ill.outQty ?: BigDecimal.ZERO) |
|
|
|
val stId = rec.stockTake?.id |
|
|
|
val illId = ill.id |
|
|
|
val stockTakeLine = |
|
|
|
if (stId != null && illId != null) { |
|
|
|
stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(illId, stId) |
|
|
|
} else { |
|
|
|
null |
|
|
|
} |
|
|
|
InventoryLotDetailResponse( |
|
|
|
id = ill.id ?: 0L, |
|
|
|
inventoryLotId = inventoryLot?.id ?: 0L, |
|
|
|
itemId = item?.id ?: rec.itemId ?: 0L, |
|
|
|
itemCode = item?.code ?: rec.itemCode, |
|
|
|
itemName = item?.name ?: rec.itemName, |
|
|
|
lotNo = inventoryLot?.lotNo, |
|
|
|
expiryDate = inventoryLot?.expiryDate, |
|
|
|
storeId = warehouse?.store_id, |
|
|
|
productionDate = inventoryLot?.productionDate, |
|
|
|
stockInDate = inventoryLot?.stockInDate, |
|
|
|
inQty = ill.inQty, |
|
|
|
remarks = rec.remarks, |
|
|
|
outQty = ill.outQty, |
|
|
|
holdQty = ill.holdQty, |
|
|
|
availableQty = availableQty, |
|
|
|
uom = ill.stockUom?.uom?.udfudesc, |
|
|
|
uomShortDesc = ill.stockUom?.uom?.udfShortDesc, |
|
|
|
warehouseCode = warehouse?.code, |
|
|
|
warehouseArea = warehouse?.area, |
|
|
|
warehouseName = warehouse?.name, |
|
|
|
status = ill.status?.name, |
|
|
|
warehouseSlot = warehouse?.slot, |
|
|
|
warehouse = warehouse?.warehouse, |
|
|
|
varianceQty = rec.varianceQty, |
|
|
|
stockTakeRecordId = rec.id, |
|
|
|
stockTakeRecordStatus = rec.status, |
|
|
|
firstStockTakeQty = rec.pickerFirstStockTakeQty, |
|
|
|
secondStockTakeQty = rec.pickerSecondStockTakeQty, |
|
|
|
firstBadQty = rec.pickerFirstBadQty, |
|
|
|
secondBadQty = rec.pickerSecondBadQty, |
|
|
|
approverQty = rec.approverStockTakeQty, |
|
|
|
approverBadQty = rec.approverBadQty, |
|
|
|
finalQty = stockTakeLine?.finalQty, |
|
|
|
bookQty = rec.bookQty, |
|
|
|
stockTakeSection = rec.stockTakeSection ?: warehouse?.stockTakeSection, |
|
|
|
stockTakeSectionDescription = warehouse?.stockTakeSectionDescription, |
|
|
|
stockTakerName = rec.stockTakerName, |
|
|
|
stockTakeEndTime = rec.stockTakeEndTime, |
|
|
|
approverTime = rec.approverTime, |
|
|
|
) |
|
|
|
}.sortedWith(compareBy({ it.storeId }, { it.warehouseCode }, { it.lotNo })) |
|
|
|
val startIndex = pageNum * pageSize |
|
|
|
val endIndex = minOf(startIndex + pageSize, allResults.size) |
|
|
|
val page = if (startIndex < allResults.size) allResults.subList(startIndex, endIndex) else emptyList() |
|
|
|
return RecordsRes(page, allResults.size) |
|
|
|
} |
|
|
|
|
|
|
|
open fun getInventoryLotDetailsByWarehouseCode(warehouseCode: String): List<InventoryLotDetailResponse> { |
|
|
|
println("getInventoryLotDetailsByWarehouseCode called with code: $warehouseCode") |
|
|
|
|
|
|
|
@@ -1425,8 +1836,9 @@ open class StockTakeRecordService( |
|
|
|
} |
|
|
|
|
|
|
|
val stockTakeSection = savedRecord.stockTakeSection |
|
|
|
val statusStockTakeId = savedRecord.stockTake?.id ?: stockTakeId |
|
|
|
if (stockTakeSection != null) { |
|
|
|
checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection) |
|
|
|
checkAndUpdateStockTakeStatus(statusStockTakeId, stockTakeSection) |
|
|
|
} |
|
|
|
|
|
|
|
return toSaveStockTakeRecordResponse(savedRecord) |
|
|
|
@@ -1463,8 +1875,19 @@ open class StockTakeRecordService( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (successCount > 0 && request.stockTakeSection.isNotBlank()) { |
|
|
|
checkAndUpdateStockTakeStatus(request.stockTakeId, request.stockTakeSection) |
|
|
|
if (successCount > 0) { |
|
|
|
val recIds = request.records.mapNotNull { it.stockTakeRecordId } |
|
|
|
val recs = if (recIds.isEmpty()) emptyList() else stockTakeRecordRepository.findAllById(recIds) |
|
|
|
val headers = recs.mapNotNull { it.stockTake }.distinctBy { it.id } |
|
|
|
if (headers.isNotEmpty()) { |
|
|
|
headers.forEach { st -> |
|
|
|
val section = st.stockTakeSection ?: return@forEach |
|
|
|
val id = st.id ?: return@forEach |
|
|
|
checkAndUpdateStockTakeStatus(id, section) |
|
|
|
} |
|
|
|
} else if (request.stockTakeSection.isNotBlank()) { |
|
|
|
checkAndUpdateStockTakeStatus(request.stockTakeId, request.stockTakeSection) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return BatchSaveStockTakeRecordResponse( |
|
|
|
@@ -2762,6 +3185,7 @@ private inline fun <T : Any, K : Any> Iterable<T>.associateByNotNull(keySelector |
|
|
|
} |
|
|
|
return destination |
|
|
|
} |
|
|
|
@Transactional |
|
|
|
open fun updateStockTakeRecordStatusToNotMatch(stockTakeRecordId: Long): StockTakeRecord { |
|
|
|
println("updateStockTakeRecordStatusToNotMatch called with stockTakeRecordId: $stockTakeRecordId") |
|
|
|
|
|
|
|
@@ -2771,8 +3195,13 @@ open fun updateStockTakeRecordStatusToNotMatch(stockTakeRecordId: Long): StockTa |
|
|
|
stockTakeRecord.apply { |
|
|
|
this.status = "notMatch" |
|
|
|
} |
|
|
|
|
|
|
|
return stockTakeRecordRepository.save(stockTakeRecord) |
|
|
|
val saved = stockTakeRecordRepository.save(stockTakeRecord) |
|
|
|
val stockTake = saved.stockTake |
|
|
|
if (stockTake != null && !stockTake.deleted) { |
|
|
|
stockTake.status = StockTakeStatus.STOCKTAKING |
|
|
|
stockTakeRepository.save(stockTake) |
|
|
|
} |
|
|
|
return saved |
|
|
|
} |
|
|
|
|
|
|
|
open fun getInventoryLotDetailsByStockTakeSectionNotMatch( |
|
|
|
|