From f216a3d4a4facfc7272de8bda6d02a611255ae3d Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 24 Mar 2026 02:24:20 +0800 Subject: [PATCH] update --- .../fpsms/modules/common/SettingNames.java | 2 + .../scheduler/service/SchedulerService.kt | 30 +++++++++++ .../scheduler/web/SchedulerController.kt | 6 +++ .../deliveryOrder/entity/DoPickOrderRecord.kt | 2 + .../service/DeliveryOrderService.kt | 9 ++++ .../entity/InventoryLotLineRepository.kt | 19 +++++++ .../stock/service/InventoryLotLineService.kt | 5 ++ .../stock/service/StockInLineService.kt | 2 +- .../stock/service/StockOutLineService.kt | 51 ++++++++----------- .../stock/service/StockTakeRecordService.kt | 8 ++- .../stock/web/model/SaveStockOutRequest.kt | 3 +- .../stock/web/model/StockTakeRecordReponse.kt | 2 + src/main/resources/application-prod.yml | 2 + src/main/resources/application.yml | 2 + 14 files changed, 111 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java index fa550d7..186823e 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java +++ b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java @@ -35,6 +35,8 @@ public abstract class SettingNames { /** Backfill M18 AN document code (grn_code) via GET /root/api/read/an (default 1:20 AM daily) */ public static final String SCHEDULE_GRN_CODE_SYNC = "SCHEDULE.grn.grnCode.m18"; + /** Mark expired inventory lot lines as unavailable (default 00:05 daily) */ + public static final String SCHEDULE_INVENTORY_LOT_EXPIRY = "SCHEDULE.inventoryLot.expiry"; public static final String SCHEDULE_PROD_ROUGH = "SCHEDULE.prod.rough"; diff --git a/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt b/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt index 3877855..50e7c52 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt +++ b/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt @@ -12,6 +12,7 @@ import com.ffii.fpsms.m18.model.SyncResult import com.ffii.fpsms.modules.common.SettingNames import com.ffii.fpsms.modules.master.service.ProductionScheduleService import com.ffii.fpsms.modules.stock.service.SearchCompletedDnService +import com.ffii.fpsms.modules.stock.service.InventoryLotLineService import com.ffii.fpsms.modules.settings.service.SettingsService import jakarta.annotation.PostConstruct import org.springframework.beans.factory.annotation.Value @@ -37,6 +38,7 @@ open class SchedulerService( * missing `grn_code`. Example: 4 = from 4 days ago 00:00 to now. */ @Value("\${scheduler.grnCodeSync.syncOffsetDays:0}") val grnCodeSyncSyncOffsetDays: Int, + @Value("\${scheduler.inventoryLotExpiry.enabled:true}") val inventoryLotExpiryEnabled: Boolean, val settingsService: SettingsService, val taskScheduler: TaskScheduler, val productionScheduleService: ProductionScheduleService, @@ -46,6 +48,7 @@ open class SchedulerService( val schedulerSyncLogRepository: SchedulerSyncLogRepository, val searchCompletedDnService: SearchCompletedDnService, val m18GrnCodeSyncService: M18GrnCodeSyncService, + val inventoryLotLineService: InventoryLotLineService, ) { var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd") @@ -64,6 +67,7 @@ open class SchedulerService( var scheduledPostCompletedDnGrn: ScheduledFuture<*>? = null var scheduledGrnCodeSync: ScheduledFuture<*>? = null + var scheduledInventoryLotExpiry: ScheduledFuture<*>? = null //@Volatile //var scheduledRoughProd: ScheduledFuture<*>? = null @@ -110,6 +114,7 @@ open class SchedulerService( scheduleM18MasterData(); schedulePostCompletedDnGrn(); scheduleGrnCodeSync(); + scheduleInventoryLotExpiry(); //scheduleRoughProd(); //scheduleDetailedProd(); } @@ -193,6 +198,31 @@ open class SchedulerService( ) } + /** Mark expired inventory lot lines as unavailable daily. Set scheduler.inventoryLotExpiry.enabled=false to disable. */ + fun scheduleInventoryLotExpiry() { + if (!inventoryLotExpiryEnabled) { + scheduledInventoryLotExpiry?.cancel(false) + scheduledInventoryLotExpiry = null + logger.info("Inventory lot expiry scheduler disabled (scheduler.inventoryLotExpiry.enabled=false)") + return + } + scheduledInventoryLotExpiry = commonSchedule( + scheduledInventoryLotExpiry, + SettingNames.SCHEDULE_INVENTORY_LOT_EXPIRY, + "0 5 0 * * *", + { markExpiredInventoryLotLinesAsUnavailable() } + ) + } + + open fun markExpiredInventoryLotLinesAsUnavailable() { + try { + val updatedRows = inventoryLotLineService.markExpiredLotLinesAsUnavailable() + logger.info("Scheduler - Inventory lot expiry update done, rows updated=$updatedRows") + } catch (e: Exception) { + logger.error("Scheduler - Inventory lot expiry update failed: ${e.message}", e) + } + } + // Function for schedule // --------------------------- FP-MTMS --------------------------- // diff --git a/src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt b/src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt index 9bddc6e..a98b2b5 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt +++ b/src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt @@ -61,6 +61,12 @@ class SchedulerController( return "M18 GRN code sync (read/an) triggered" } + @GetMapping("/trigger/inventory-lot-expiry") + fun triggerInventoryLotExpiry(): String { + schedulerService.markExpiredInventoryLotLinesAsUnavailable() + return "Inventory lot expiry status update triggered" + } + @GetMapping("/trigger/post-completed-dn-grn") fun triggerPostCompletedDnGrn( @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) receiptDate: LocalDate? = null, diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecord.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecord.kt index 2c91be0..379eb13 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecord.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecord.kt @@ -96,6 +96,8 @@ class DoPickOrderRecord { var handlerName: String? = null @Column(name = "release_type", length = 100) var releaseType: String? = null + @Column(name = "cartonQty") + var cartonQty: Int? = null // Default constructor for Hibernate constructor() diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index 0a6638d..0a879c0 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt @@ -1308,6 +1308,7 @@ open class DeliveryOrderService( val driver = A4PrintDriverRegistry.getDriver(printer.brand) driver.print(tempPdfFile, ip, port, printQty) } + updateRecordCartonQty(request.doPickOrderId, request.numOfCarton) } finally { //tempPdfFile.delete() } @@ -1406,6 +1407,7 @@ open class DeliveryOrderService( ) } } + updateRecordCartonQty(request.doPickOrderId, request.numOfCarton) println("Test PDF saved to: ${tempPdfFile.absolutePath}") @@ -1415,6 +1417,13 @@ open class DeliveryOrderService( } + private fun updateRecordCartonQty(doPickOrderRecordId: Long, cartonQty: Int) { + if (cartonQty <= 0) return + val record = doPickOrderRecordRepository.findById(doPickOrderRecordId).orElse(null) ?: return + record.cartonQty = cartonQty + doPickOrderRecordRepository.save(record) + } + @Transactional(rollbackFor = [Exception::class]) open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult { println(" DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}") diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt index ee2e815..a1dd4a3 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt @@ -5,8 +5,10 @@ import com.ffii.fpsms.modules.stock.entity.projection.CurrentInventoryItemInfo import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional import java.io.Serializable import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus import org.springframework.data.repository.query.Param @@ -145,4 +147,21 @@ interface InventoryLotLineRepository : AbstractRepository ): List + + @Modifying + @Transactional + @Query( + value = """ + UPDATE inventory_lot_line ill + INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId + SET ill.status = 'unavailable' + WHERE il.deleted = 0 + AND ill.deleted = 0 + AND il.expiryDate IS NOT NULL + AND il.expiryDate < CURRENT_DATE + AND (ill.status IS NULL OR LOWER(ill.status) <> 'unavailable') + """, + nativeQuery = true + ) + fun markExpiredLotLinesAsUnavailable(): Int } \ 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 173f1f1..c74b61a 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 @@ -60,6 +60,11 @@ open class InventoryLotLineService( @Lazy private val jobOrderService: JobOrderService ) { + @Transactional + open fun markExpiredLotLinesAsUnavailable(): Int { + return inventoryLotLineRepository.markExpiredLotLinesAsUnavailable() + } + open fun findById(id: Long): Optional { return inventoryLotLineRepository.findById(id) } 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 e76092a..8e2a8cb 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 @@ -1162,7 +1162,7 @@ open class StockInLineService( this.type = stockInLine.type this.itemId = item.id this.itemCode = item.code - this.uomId = inventory.uom?.id ?: itemUomService.findStockUnitByItemId(item.id!!)?.uom?.id + this.uomId = itemUomService.findStockUnitByItemId(item.id!!)?.uom?.id ?: inventory.uom?.id this.date = LocalDate.now() } 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 799eb81..9b80cf3 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 @@ -656,31 +656,22 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { val statusLower = request.status.trim().lowercase() val isPickEnd = statusLower == "completed" || statusLower == "partially_completed" val deltaQty = request.qty + val skipLedgerWrite = request.skipLedgerWrite == true - // non jo/do: 手工拣货增量扣减时,補寫出库 ledger(幂等 + 跳过 jo/do,避免 double write) - if (isPickEnd && deltaQty != null && deltaQty > 0 && savedStockOutLine.id != null) { + // 手工拣货增量扣减时补写出库 ledger(DO/JO 也需要写) + // 批量提交流程会显式传 skipLedgerWrite=true,避免重复写入。 + if (!skipLedgerWrite && isPickEnd && deltaQty != null && deltaQty > 0 && savedStockOutLine.id != null) { val itemId = savedStockOutLine.item?.id - val po = savedStockOutLine.pickOrderLine?.pickOrder - - if (itemId != null && po != null) { - val isJoOrDo = (po.jobOrder != null || po.deliveryOrder != null) - if (!isJoOrDo) { - val exists = jdbcDao.queryForInt( - "SELECT COUNT(*) FROM stock_ledger WHERE deleted = false AND stockOutLineId = :id", - mapOf("id" to savedStockOutLine.id) - ) > 0 - - if (!exists) { - val invBefore = inventoryRepository.findByItemId(itemId).orElse(null) - val onHandBefore = invBefore?.onHandQty?.toDouble() ?: 0.0 - - createStockLedgerForPickDelta( - stockOutLineId = savedStockOutLine.id!!, - deltaQty = BigDecimal(deltaQty.toString()), - onHandQtyBeforeUpdate = onHandBefore - ) - } - } + + if (itemId != null) { + val invBefore = inventoryRepository.findByItemId(itemId).orElse(null) + val onHandBefore = invBefore?.onHandQty?.toDouble() ?: 0.0 + + createStockLedgerForPickDelta( + stockOutLineId = savedStockOutLine.id!!, + deltaQty = BigDecimal(deltaQty.toString()), + onHandQtyBeforeUpdate = onHandBefore + ) } } println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}") @@ -1295,7 +1286,8 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { UpdateStockOutLineStatusRequest( id = line.stockOutLineId, status = newStatus, // 例如前端传来的 "completed" - qty = 0.0 // 不改变现有 qty + qty = 0.0, // 不改变现有 qty + skipLedgerWrite = true ) ) @@ -1308,7 +1300,8 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { UpdateStockOutLineStatusRequest( id = line.stockOutLineId, status = newStatus, - qty = submitQty.toDouble() + qty = submitQty.toDouble(), + skipLedgerWrite = true ) ) @@ -1509,8 +1502,8 @@ affectedConsoCodes.forEach { consoCode -> this.type = stockOutLine.type this.itemId = item.id this.itemCode = item.code - this.uomId = inventory.uom?.id - ?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id + this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id + ?: inventory.uom?.id this.date = LocalDate.now() } @@ -1607,8 +1600,8 @@ affectedConsoCodes.forEach { consoCode -> this.type = "NOR" this.itemId = item.id this.itemCode = item.code - this.uomId = inventory.uom?.id - ?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id + this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id + ?: inventory.uom?.id this.date = LocalDate.now() } 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 9050b2b..3db01ba 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 @@ -40,6 +40,7 @@ import java.sql.ResultSet import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository import com.ffii.fpsms.modules.stock.entity.InventoryRepository import com.ffii.fpsms.modules.stock.entity.StockLedger +import com.ffii.fpsms.modules.master.entity.UomConversionRepository import java.time.LocalDate import com.ffii.fpsms.modules.stock.entity.InventoryLotLine import java.math.RoundingMode @@ -59,7 +60,8 @@ class StockTakeRecordService( val stockInLineRepository: StockInLineRepository, val inventoryLotRepository: InventoryLotRepository, val stockLedgerRepository: StockLedgerRepository, - val inventoryRepository: InventoryRepository + val inventoryRepository: InventoryRepository, + val uomConversionRepository: UomConversionRepository ) { private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java) @@ -1706,6 +1708,10 @@ fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes< itemId = ledger.itemId ?: 0L, itemCode = ledger.itemCode, itemName = ledger.inventory?.item?.name, + uomId = ledger.uomId, + uomDesc = ledger.uomId?.let { uomId -> + uomConversionRepository.findByIdAndDeletedFalse(uomId)?.udfudesc + }, balanceQty = ledger.balance?.let { balance: Double -> BigDecimal(balance.toString()) }, qty = if (ledger.inQty != null && ledger.inQty!! > 0) { BigDecimal(ledger.inQty.toString()) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt index ee8fbaa..0c6fbc0 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt @@ -59,7 +59,8 @@ data class UpdateStockOutLineStatusRequest( val id: Long, val status: String, val qty: Double? = null, - val remarks: String? = null + val remarks: String? = null, + val skipLedgerWrite: Boolean? = false ) data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( val pickOrderLineId: Long, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt index e9aaf78..99b039b 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt @@ -127,6 +127,8 @@ data class StockTransactionResponse( val itemId: Long, val itemCode: String?, val itemName: String?, + val uomId: Long? = null, + val uomDesc: String? = null, val balanceQty: BigDecimal? = null, val qty: BigDecimal, val type: String?, diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 587684a..83f0b60 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -27,6 +27,8 @@ scheduler: grnCodeSync: enabled: true syncOffsetDays: 10 # from (today − 10) 00:00 to now, rows missing grn_code + inventoryLotExpiry: + enabled: true m18: config: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e39a204..e23e183 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,6 +19,8 @@ scheduler: enabled: false # set true in prod; backfills grn_code from M18 GET /root/api/read/an # Lookback: created from start of (today − N days) through now, missing grn_code. E.g. 4 = last 4 days + today. syncOffsetDays: 0 + inventoryLotExpiry: + enabled: true spring: servlet: