| @@ -114,6 +114,13 @@ open class StockInLine : BaseEntity<Long>() { | |||
| @Column(name = "remarks") | |||
| open var remarks: String? = null | |||
| /** Set when job-order auto putaway is waiting for an open stock-take round to close. */ | |||
| @Column(name = "pendingAutoPutawayWarehouseId") | |||
| open var pendingAutoPutawayWarehouseId: Long? = null | |||
| @Column(name = "pendingAutoPutawayQty", precision = 14, scale = 2) | |||
| open var pendingAutoPutawayQty: BigDecimal? = null | |||
| @Column(name = "type") | |||
| open var type: String? = null | |||
| @@ -63,7 +63,9 @@ fun searchStockInLines( | |||
| //AND sil.type IS NOT NULL | |||
| @Query("SELECT sil FROM StockInLine sil WHERE sil.item.id IN :itemIds AND sil.deleted = false") | |||
| fun findAllByItemIdInAndDeletedFalse(itemIds: List<Long>): List<StockInLine> | |||
| fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine? | |||
| fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine? | |||
| fun findAllByPendingAutoPutawayWarehouseIdIsNotNullAndDeletedIsFalse(): List<StockInLine> | |||
| /** Loads stock-in lines with item and purchaseOrderLine.item so bDesc/bDesc_en (item name) are available for M18 GRN. */ | |||
| @Query(""" | |||
| @@ -21,4 +21,40 @@ interface StockTakeRepository : AbstractRepository<StockTake, Long> { | |||
| "SELECT COALESCE(MAX(st.stockTakeRoundId), 0) FROM StockTake st WHERE st.deleted = false" | |||
| ) | |||
| fun findMaxStockTakeRoundId(): Long | |||
| @Query( | |||
| value = """ | |||
| SELECT DISTINCT stockTakeSection FROM stock_take | |||
| WHERE deleted = 0 | |||
| AND status IN ('pending', 'stockTaking', 'approving') | |||
| AND stockTakeSection IS NOT NULL | |||
| AND stockTakeSection <> '' | |||
| """, | |||
| nativeQuery = true, | |||
| ) | |||
| fun findOpenStockTakeSections(): List<String> | |||
| @Query( | |||
| value = """ | |||
| SELECT | |||
| id AS id, | |||
| stockTakeSection AS stockTakeSection, | |||
| stockTakeRoundMode AS stockTakeRoundMode, | |||
| stockTakeRoundId AS stockTakeRoundId | |||
| FROM stock_take | |||
| WHERE deleted = 0 | |||
| AND status IN ('pending', 'stockTaking', 'approving') | |||
| AND stockTakeSection IS NOT NULL | |||
| AND stockTakeSection <> '' | |||
| """, | |||
| nativeQuery = true, | |||
| ) | |||
| fun findOpenStockTakeRows(): List<OpenStockTakeRow> | |||
| } | |||
| interface OpenStockTakeRow { | |||
| fun getId(): Long | |||
| fun getStockTakeSection(): String? | |||
| fun getStockTakeRoundMode(): String? | |||
| fun getStockTakeRoundId(): Long? | |||
| } | |||
| @@ -61,7 +61,8 @@ open class InventoryLotLineService( | |||
| private val inventoryRepository: InventoryRepository, | |||
| private val printerService: PrinterService, | |||
| @Lazy | |||
| private val jobOrderService: JobOrderService | |||
| private val jobOrderService: JobOrderService, | |||
| private val stockTakeLocationGuard: StockTakeLocationGuard, | |||
| ) { | |||
| @Transactional | |||
| open fun markExpiredLotLinesAsUnavailable(): Int { | |||
| @@ -387,6 +388,7 @@ open class InventoryLotLineService( | |||
| open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantitiesRequest): MessageResponse { | |||
| try { | |||
| val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() | |||
| stockTakeLocationGuard.assertLotLineMovable(inventoryLotLine) | |||
| // Handle quantity updates based on operation | |||
| var newHoldQty = inventoryLotLine.holdQty ?: BigDecimal.ZERO | |||
| @@ -111,6 +111,7 @@ open class StockInLineService( | |||
| @Value("\${scheduler.m18Grn.createEnabled:false}") private val m18GrnCreateEnabled: Boolean, | |||
| /** Recent window for PO stock-in alerts (pending / receiving) in nav / list UI. */ | |||
| @Value("\${fpsms.purchase-stock-in-alert.lookback-days:7}") private val purchaseStockInAlertLookbackDays: Int, | |||
| private val stockTakeLocationGuard: StockTakeLocationGuard, | |||
| // private val bomOutputQtyService: BomOutputQtyService, | |||
| ) : AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) { | |||
| @@ -1032,6 +1033,10 @@ open class StockInLineService( | |||
| } | |||
| _logStep("apply_fields", _tApplyFields) | |||
| if (request.status == StockInLineStatus.RECEIVED.status) { | |||
| val held = holdAutoPutawayIfSectionInStockTake(request, stockInLine) | |||
| if (held != null) { | |||
| return held | |||
| } | |||
| val _tPutAwayTotal = System.nanoTime() | |||
| // Putaway | |||
| val _tSaveLotLines = System.nanoTime() | |||
| @@ -1439,8 +1444,99 @@ open class StockInLineService( | |||
| logger.info("[SIL.ledger timing] TOTAL took ${String.format("%.3f", (System.nanoTime() - _t0) / 1_000_000.0)} ms") | |||
| } | |||
| /** | |||
| * Job-order WIP / FG / FA auto putaway into a section that is still in an open stock-take round | |||
| * is stored on the stock-in line and posted when that round closes. | |||
| * Any other putaway into that section is rejected. | |||
| */ | |||
| private fun holdAutoPutawayIfSectionInStockTake( | |||
| request: SaveStockInLineRequest, | |||
| stockInLine: StockInLine, | |||
| ): MessageResponse? { | |||
| if (request.releaseHeldPutaway) return null | |||
| if (request.stockTakeLineId != null || stockInLine.stockTakeLine != null) return null | |||
| if (stockInLine.type.equals("TKE", ignoreCase = true)) return null | |||
| val targets = (request.inventoryLotLines ?: emptyList()) | |||
| .mapNotNull { line -> | |||
| val warehouseId = line.warehouseId ?: return@mapNotNull null | |||
| if (line.qty <= BigDecimal.ZERO) return@mapNotNull null | |||
| warehouseId to line.qty | |||
| } | |||
| if (targets.isEmpty()) return null | |||
| val itemId = stockInLine.item?.id ?: request.itemId | |||
| val blocked = targets.firstOrNull { stockTakeLocationGuard.blockMessage(it.first, itemId) != null } | |||
| ?: return null | |||
| if (isJobOrderAutoPutaway(stockInLine) && targets.size == 1) { | |||
| val (warehouseId, qty) = targets.first() | |||
| stockInLine.pendingAutoPutawayWarehouseId = warehouseId | |||
| stockInLine.pendingAutoPutawayQty = qty | |||
| stockInLine.jobOrder?.let { jo -> | |||
| jo.status = JobOrderStatus.COMPLETED | |||
| jobOrderRepository.save(jo) | |||
| } | |||
| val saved = saveAndFlush(stockInLine) | |||
| val lineInfo = stockInLineRepository.findStockInLineInfoByIdAndDeletedFalse(saved.id!!) | |||
| return MessageResponse( | |||
| id = saved.id, | |||
| code = "PUTAWAY_HELD", | |||
| name = null, | |||
| type = "putaway_held", | |||
| message = "盤點進行中,自動上架將於盤點結束後入庫", | |||
| errorPosition = null, | |||
| entity = listOf(lineInfo), | |||
| ) | |||
| } | |||
| stockTakeLocationGuard.assertWarehouseMovable(blocked.first, itemId) | |||
| return null | |||
| } | |||
| private fun isJobOrderAutoPutaway(stockInLine: StockInLine): Boolean { | |||
| if (stockInLine.jobOrder == null) return false | |||
| val bom = stockInLine.jobOrder?.bom?.description | |||
| if (bom == "WIP" || bom == "FG") return true | |||
| val itemNo = (stockInLine.itemNo ?: stockInLine.item?.code ?: "").uppercase() | |||
| return itemNo.contains("FA") | |||
| } | |||
| /** Post auto putaways that were waiting on sections closed with this round. */ | |||
| @Transactional | |||
| open fun releasePendingAutoPutaways(sections: Collection<String>) { | |||
| val wanted = sections.map { it.trim().lowercase() }.filter { it.isNotEmpty() }.toSet() | |||
| if (wanted.isEmpty()) return | |||
| val pending = stockInLineRepository.findAllByPendingAutoPutawayWarehouseIdIsNotNullAndDeletedIsFalse() | |||
| for (sil in pending) { | |||
| val warehouseId = sil.pendingAutoPutawayWarehouseId ?: continue | |||
| val warehouse = warehouseRepository.findById(warehouseId).orElse(null) ?: continue | |||
| val section = warehouse.stockTakeSection?.trim()?.lowercase() ?: continue | |||
| if (section !in wanted) continue | |||
| val qty = sil.pendingAutoPutawayQty ?: sil.acceptedQty ?: continue | |||
| val itemId = sil.item?.id ?: continue | |||
| sil.pendingAutoPutawayWarehouseId = null | |||
| sil.pendingAutoPutawayQty = null | |||
| saveAndFlush(sil) | |||
| update( | |||
| SaveStockInLineRequest( | |||
| id = sil.id, | |||
| itemId = itemId, | |||
| acceptedQty = sil.acceptedQty ?: qty, | |||
| acceptQty = qty, | |||
| status = StockInLineStatus.RECEIVED.status, | |||
| expiryDate = sil.expiryDate, | |||
| receiptDate = sil.receiptDate?.toLocalDate(), | |||
| productionDate = sil.productionDate?.toLocalDate(), | |||
| warehouseId = warehouseId, | |||
| releaseHeldPutaway = true, | |||
| inventoryLotLines = listOf( | |||
| SaveInventoryLotLineForSil(qty = qty, warehouseId = warehouseId), | |||
| ), | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| @Transactional | |||
| open fun createStockIn(request: StockInRequest): StockInLine { | |||
| stockTakeLocationGuard.assertWarehouseMovable(request.warehouseId, request.itemId) | |||
| // Step 1: Create a row of stock_in | |||
| val prefix = "SI" | |||
| val midfix = CodeGenerator.DEFAULT_MIDFIX | |||
| @@ -1531,6 +1627,9 @@ open class StockInLineService( | |||
| request: StockInRequest, | |||
| existingInventoryLotLine: InventoryLotLine | |||
| ): StockInLine { | |||
| val movingItemId = existingInventoryLotLine.inventoryLot?.item?.id ?: request.itemId | |||
| stockTakeLocationGuard.assertWarehouseMovable(existingInventoryLotLine.warehouse?.id, movingItemId) | |||
| stockTakeLocationGuard.assertWarehouseMovable(request.warehouseId, movingItemId) | |||
| // Step 1: Create StockIn | |||
| val prefix = "SI" | |||
| val midfix = CodeGenerator.DEFAULT_MIDFIX | |||
| @@ -82,6 +82,7 @@ private val inventoryLotLineService: InventoryLotLineService, | |||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository, | |||
| private val itemUomService: ItemUomService, | |||
| @Lazy private val doReplenishmentService: DoReplenishmentService, | |||
| private val stockTakeLocationGuard: StockTakeLocationGuard, | |||
| ): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) { | |||
| @PersistenceContext | |||
| @@ -208,6 +209,7 @@ val existingStockOutLine = stockOutLineRepository.findByPickOrderLineIdAndInvent | |||
| ) | |||
| val item = itemRepository.findById(pickOrderLine.item!!.id!!).orElseThrow() | |||
| val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() | |||
| stockTakeLocationGuard.assertLotLineMovable(inventoryLotLine) | |||
| val stockOutLine = StockOutLine() | |||
| .apply { | |||
| @@ -257,6 +259,7 @@ fun handleQc(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): Li | |||
| val zero = BigDecimal.ZERO | |||
| val one = BigDecimal.ONE | |||
| val targetLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId!!).orElseThrow() | |||
| stockTakeLocationGuard.assertLotLineMovable(targetLotLine) | |||
| val salesUnit = inventoryLotLine?.inventoryLot?.item?.id?.let {_itemId -> itemUomRespository.findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(_itemId) } | |||
| val ratio = 1.0 //(salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one).toDouble() | |||
| val targetLotLineEntry = targetLotLine.apply { | |||
| @@ -306,6 +309,7 @@ open fun createWithoutConso(request: CreateStockOutLineWithoutConsoRequest): Mes | |||
| val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow { | |||
| IllegalArgumentException("InventoryLotLine not found with ID: ${request.inventoryLotLineId}") | |||
| } | |||
| stockTakeLocationGuard.assertLotLineMovable(inventoryLotLine) | |||
| println("Found inventoryLotLine: ${inventoryLotLine.id}") | |||
| val stockOutLine = StockOutLine() | |||
| @@ -490,6 +494,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { | |||
| /** Batch pick: same qty rules as [InventoryLotLineService.updateInventoryLotLineQuantities] pick, without double findById / extra service layers. */ | |||
| private fun applyPickToInventoryLotLineInBatch(ill: InventoryLotLine, submitQty: BigDecimal) { | |||
| stockTakeLocationGuard.assertLotLineMovable(ill) | |||
| val zero = BigDecimal.ZERO | |||
| val newHold = (ill.holdQty ?: zero).minus(submitQty) | |||
| val newOut = (ill.outQty ?: zero).plus(submitQty) | |||
| @@ -1673,6 +1678,7 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||
| */ | |||
| open fun createStockOutForStockTransfer(inventoryLotLineId: Long, qty: Double): StockOutLine { | |||
| val inventoryLotLine = inventoryLotLineRepository.findById(inventoryLotLineId).orElseThrow() | |||
| stockTakeLocationGuard.assertLotLineMovable(inventoryLotLine) | |||
| val qtyBd = BigDecimal.valueOf(qty) | |||
| validateOutOnlyIncrease(inventoryLotLine, qtyBd, OutOnlyLotLineContext.STOCK_TRANSFER) | |||
| applyOutOnlyIncreaseToLotLine(inventoryLotLine, qtyBd) | |||
| @@ -1688,6 +1694,9 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||
| */ | |||
| open fun createStockOutForAdjustment(request: StockOutRequest): StockOutLine { | |||
| val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() | |||
| if (!request.type.equals("TKE", ignoreCase = true)) { | |||
| stockTakeLocationGuard.assertLotLineMovable(inventoryLotLine) | |||
| } | |||
| val qtyBd = BigDecimal.valueOf(request.qty) | |||
| validateOutOnlyIncrease(inventoryLotLine, qtyBd, OutOnlyLotLineContext.ADJUSTMENT) | |||
| applyOutOnlyIncreaseToLotLine(inventoryLotLine, qtyBd) | |||
| @@ -1706,6 +1715,9 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||
| */ | |||
| open fun createStockOutForRemoval(inventoryLotLineId: Long, type: String = "ADJ"): StockOutLine { | |||
| val inventoryLotLine = inventoryLotLineRepository.findById(inventoryLotLineId).orElseThrow() | |||
| if (!type.equals("TKE", ignoreCase = true)) { | |||
| stockTakeLocationGuard.assertLotLineMovable(inventoryLotLine) | |||
| } | |||
| val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO | |||
| val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO | |||
| val deltaOut = inQty.subtract(outQty).coerceAtLeast(BigDecimal.ZERO) | |||
| @@ -2159,6 +2171,9 @@ fun applyStockOutLineDelta( | |||
| if (!skipInventoryWrite) { | |||
| val lotLine = savedSol.inventoryLotLine | |||
| if (lotLine != null) { | |||
| if (!postingType.equals("tke", ignoreCase = true)) { | |||
| stockTakeLocationGuard.assertLotLineMovable(lotLine) | |||
| } | |||
| if (isIssuePosting) { | |||
| val latestLotLine = inventoryLotLineRepository.findById(lotLine.id!!).orElse(null) | |||
| if (latestLotLine != null) { | |||
| @@ -0,0 +1,73 @@ | |||
| package com.ffii.fpsms.modules.stock.service | |||
| import com.ffii.fpsms.modules.master.entity.WarehouseRepository | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | |||
| import com.ffii.fpsms.modules.stock.entity.StockTakeRecordRepository | |||
| import com.ffii.fpsms.modules.stock.entity.StockTakeRepository | |||
| import com.ffii.fpsms.modules.stock.enums.StockTakeRoundMode | |||
| import org.springframework.http.HttpStatus | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.web.server.ResponseStatusException | |||
| /** | |||
| * Section rounds block every item in that stock-take section. | |||
| * Item rounds block only the items on that round's records. | |||
| * Stock-take's own variance posting does not call this guard. | |||
| */ | |||
| @Service | |||
| class StockTakeLocationGuard( | |||
| private val stockTakeRepository: StockTakeRepository, | |||
| private val stockTakeRecordRepository: StockTakeRecordRepository, | |||
| private val warehouseRepository: WarehouseRepository, | |||
| ) { | |||
| /** Null when this warehouse and item can still move. */ | |||
| fun blockMessage(warehouseId: Long?, itemId: Long?): String? { | |||
| if (warehouseId == null || warehouseId <= 0L) return null | |||
| val warehouse = warehouseRepository.findById(warehouseId).orElse(null) ?: return null | |||
| val section = warehouse.stockTakeSection?.trim()?.takeIf { it.isNotBlank() } ?: return null | |||
| val open = stockTakeRepository.findOpenStockTakeRows().filter { | |||
| section.equals(it.getStockTakeSection()?.trim(), ignoreCase = true) | |||
| } | |||
| if (open.isEmpty()) return null | |||
| if (open.any { !StockTakeRoundMode.ITEM.equals(it.getStockTakeRoundMode(), ignoreCase = true) }) { | |||
| return "盤點進行中,區域 $section 暫停出入庫" | |||
| } | |||
| if (itemId == null || itemId <= 0L) return null | |||
| val roundIds = open.mapNotNull { it.getStockTakeRoundId()?.takeIf { id -> id > 0L } }.distinct() | |||
| val headerIds = open.map { it.getId() }.distinct() | |||
| val records = buildList { | |||
| if (roundIds.isNotEmpty()) { | |||
| addAll(stockTakeRecordRepository.findAllByStockTakeRoundIdInAndDeletedIsFalse(roundIds)) | |||
| } | |||
| if (headerIds.isNotEmpty()) { | |||
| addAll(stockTakeRecordRepository.findAllByStockTakeIdInAndDeletedIsFalse(headerIds)) | |||
| } | |||
| } | |||
| val hit = records.firstOrNull { record -> | |||
| record.itemId == itemId && | |||
| section.equals(record.stockTakeSection?.trim(), ignoreCase = true) | |||
| } ?: return null | |||
| val itemLabel = recordLabel(hit.itemCode, hit.itemName) | |||
| return "盤點進行中,貨品 $itemLabel 暫停出入庫" | |||
| } | |||
| fun assertWarehouseMovable(warehouseId: Long?, itemId: Long? = null) { | |||
| val message = blockMessage(warehouseId, itemId) ?: return | |||
| throw ResponseStatusException(HttpStatus.CONFLICT, message) | |||
| } | |||
| fun assertLotLineMovable(lotLine: InventoryLotLine?) { | |||
| assertWarehouseMovable(lotLine?.warehouse?.id, lotLine?.inventoryLot?.item?.id) | |||
| } | |||
| private fun recordLabel(code: String?, name: String?): String { | |||
| val itemCode = code?.trim().orEmpty() | |||
| val itemName = name?.trim().orEmpty() | |||
| return when { | |||
| itemCode.isNotEmpty() && itemName.isNotEmpty() -> "$itemCode $itemName" | |||
| itemCode.isNotEmpty() -> itemCode | |||
| itemName.isNotEmpty() -> itemName | |||
| else -> "此貨品" | |||
| } | |||
| } | |||
| } | |||
| @@ -11,6 +11,7 @@ import com.ffii.fpsms.modules.stock.enums.StockTakeRoundMode | |||
| import com.ffii.fpsms.modules.stock.web.model.* | |||
| import org.slf4j.Logger | |||
| import org.slf4j.LoggerFactory | |||
| import org.springframework.context.annotation.Lazy | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.transaction.annotation.Transactional | |||
| import java.time.LocalDateTime | |||
| @@ -66,7 +67,8 @@ open class StockTakeRecordService( | |||
| val stockLedgerRepository: StockLedgerRepository, | |||
| val inventoryRepository: InventoryRepository, | |||
| val uomConversionRepository: UomConversionRepository, | |||
| val itemsRepository: ItemsRepository | |||
| val itemsRepository: ItemsRepository, | |||
| @Lazy private val stockInLineService: StockInLineService, | |||
| ) { | |||
| private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java) | |||
| @@ -1213,7 +1215,7 @@ open class StockTakeRecordService( | |||
| } | |||
| toRoundCard(roundKey, rows, records, floorsBySection) | |||
| } | |||
| .sortedByDescending { it.stockTakeRoundId } | |||
| .sortedBy { it.stockTakeRoundId } | |||
| val thisMonth = java.time.YearMonth.now().toString() | |||
| val hasOpenRounds = cards.any { it.open } | |||
| val completedYearMonths = ( | |||
| @@ -1248,6 +1250,8 @@ open class StockTakeRecordService( | |||
| } | |||
| stockTakeRepository.save(st) | |||
| } | |||
| val sections = rows.mapNotNull { it.stockTakeSection?.trim()?.takeIf { section -> section.isNotBlank() } } | |||
| stockInLineService.releasePendingAutoPutaways(sections) | |||
| return mapOf( | |||
| "success" to true, | |||
| "stockTakeRoundId" to roundId, | |||
| @@ -60,6 +60,7 @@ open class SuggestedPickLotService( | |||
| val itemRepository: ItemsRepository, | |||
| val stockOutLineRepository: StockOutLIneRepository, | |||
| private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | |||
| private val stockTakeLocationGuard: StockTakeLocationGuard, | |||
| ) { | |||
| // Calculation Available Qty / Remaining Qty | |||
| @@ -1035,6 +1036,7 @@ val basePickOrders = (listOf(pickOrder) + allCompetingPickOrders).distinctBy { i | |||
| if (lotId != null && newHoldQty != null && newHoldQty > BigDecimal.ZERO) { | |||
| val lot = inventoryLotLineRepository.findById(lotId).orElse(null) | |||
| lot?.let { | |||
| stockTakeLocationGuard.assertLotLineMovable(it) | |||
| val currentHoldQty = it.holdQty ?: BigDecimal.ZERO | |||
| val existingHoldQty = existingHoldQtyMap[lotId] ?: BigDecimal.ZERO | |||
| val additionalHoldQty = newHoldQty.minus(existingHoldQty) | |||
| @@ -1892,6 +1894,7 @@ open fun updateSuggestedLotLineId(suggestedPickLotId: Long, newLotLineId: Long): | |||
| } | |||
| // Add hold quantity to new lot | |||
| stockTakeLocationGuard.assertLotLineMovable(newInventoryLotLine) | |||
| val newHoldQty = (newInventoryLotLine.holdQty ?: BigDecimal.ZERO).plus(suggestedQty) | |||
| newInventoryLotLine.holdQty = newHoldQty | |||
| inventoryLotLineRepository.save(newInventoryLotLine) | |||
| @@ -63,7 +63,9 @@ data class SaveStockInLineRequest( | |||
| var escalationLog: SaveEscalationLogRequest? = null, | |||
| var warehouseId: Long? = null, | |||
| var rejectQty: BigDecimal? = null, | |||
| var inventoryLotLines: List<SaveInventoryLotLineForSil>? = null | |||
| var inventoryLotLines: List<SaveInventoryLotLineForSil>? = null, | |||
| /** True when closing a stock-take round posts a putaway that was held for that round. */ | |||
| var releaseHeldPutaway: Boolean = false, | |||
| ) | |||
| data class SaveInventoryLotLineForSil ( | |||
| @@ -0,0 +1,18 @@ | |||
| --liquibase formatted sql | |||
| --changeset fpsms:20261013_close_march_open_stock_take_rounds | |||
| -- Close leftover open stock_take headers (planStart Mar 2026). | |||
| -- Apr–Sep 2026 was closed by 20261007_close_open_stock_take_rounds. | |||
| -- Same as Admin close round: status → completed; fill actualEnd if null. | |||
| -- Does not change stock_take_record / stock_take_line. | |||
| UPDATE stock_take | |||
| SET | |||
| status = 'completed', | |||
| actualEnd = IFNULL(actualEnd, NOW()), | |||
| modified = NOW(), | |||
| modifiedBy = 'sql-close-rounds' | |||
| WHERE deleted = 0 | |||
| AND status IN ('pending', 'stockTaking', 'approving') | |||
| AND planStart >= '2026-03-01 00:00:00' | |||
| AND planStart < '2026-04-01 00:00:00'; | |||
| @@ -0,0 +1,6 @@ | |||
| --liquibase formatted sql | |||
| --changeset fpsms:20261013_stock_in_line_pending_auto_putaway | |||
| ALTER TABLE stock_in_line | |||
| ADD COLUMN pendingAutoPutawayWarehouseId BIGINT NULL, | |||
| ADD COLUMN pendingAutoPutawayQty DECIMAL(14, 2) NULL; | |||