diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLine.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLine.kt index 12c90309..c8cec07f 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLine.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLine.kt @@ -114,6 +114,13 @@ open class StockInLine : BaseEntity() { @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 diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt index 98454337..f31c8519 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt @@ -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): List -fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine? + fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine? + + fun findAllByPendingAutoPutawayWarehouseIdIsNotNullAndDeletedIsFalse(): List /** Loads stock-in lines with item and purchaseOrderLine.item so bDesc/bDesc_en (item name) are available for M18 GRN. */ @Query(""" diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRepository.kt index da09eae2..bc7aae92 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRepository.kt @@ -21,4 +21,40 @@ interface StockTakeRepository : AbstractRepository { "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 + + @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 +} + +interface OpenStockTakeRow { + fun getId(): Long + fun getStockTakeSection(): String? + fun getStockTakeRoundMode(): String? + fun getStockTakeRoundId(): Long? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt index 377e8b48..8ec1cd18 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt @@ -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 diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index fa4c1abc..434030c8 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -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(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) { + 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 diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index e32492dc..db6cece1 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt @@ -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(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) { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeLocationGuard.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeLocationGuard.kt new file mode 100644 index 00000000..0a7e042b --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeLocationGuard.kt @@ -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 -> "此貨品" + } + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt index 1d13fc9e..d09c0e6b 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt @@ -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, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index 507b100a..bbdb0e14 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt @@ -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) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockInRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockInRequest.kt index ff261531..74355227 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockInRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockInRequest.kt @@ -63,7 +63,9 @@ data class SaveStockInLineRequest( var escalationLog: SaveEscalationLogRequest? = null, var warehouseId: Long? = null, var rejectQty: BigDecimal? = null, - var inventoryLotLines: List? = null + var inventoryLotLines: List? = null, + /** True when closing a stock-take round posts a putaway that was held for that round. */ + var releaseHeldPutaway: Boolean = false, ) data class SaveInventoryLotLineForSil ( diff --git a/src/main/resources/db/changelog/changes/20261013_close_stock_take_rounds/01_close_march_open_rounds.sql b/src/main/resources/db/changelog/changes/20261013_close_stock_take_rounds/01_close_march_open_rounds.sql new file mode 100644 index 00000000..0205cc68 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20261013_close_stock_take_rounds/01_close_march_open_rounds.sql @@ -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'; diff --git a/src/main/resources/db/changelog/changes/20261013_pending_auto_putaway/01_stock_in_line_pending_auto_putaway.sql b/src/main/resources/db/changelog/changes/20261013_pending_auto_putaway/01_stock_in_line_pending_auto_putaway.sql new file mode 100644 index 00000000..d769f16e --- /dev/null +++ b/src/main/resources/db/changelog/changes/20261013_pending_auto_putaway/01_stock_in_line_pending_auto_putaway.sql @@ -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;