瀏覽代碼

Liquibase 20260910:#1 stockUomId、#2–3 Option A trigger、#35 index

上線 app:#5 entity、#7 available = onHand − unavailable、#8 搜尋全 bucket、#34 Resolver(#10/#11 已接)
Admin 1.0:補 stockUomId、uomId=base(這就是 #1 的 data patch)
2.3 把 ledger 綁到該 bucket
2.7:先超發 remain→0(改 lot),再 miss;共用一張入倉單、一張出倉單
2.6 日結
fix負數倉
CANCERYS\kw093 6 天之前
父節點
當前提交
193a8cbab5
共有 4 個文件被更改,包括 741 次插入151 次删除
  1. +646
    -113
      src/main/java/com/ffii/fpsms/modules/master/service/StockLedgerFixService.kt
  2. +7
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/models/StockLedgerFixModels.kt
  3. +87
    -37
      src/main/java/com/ffii/fpsms/modules/stock/service/StockLotDayCloseService.kt
  4. +1
    -1
      src/main/resources/db/changelog/changes/20260814_stock_lot_day/01_create_stock_lot_day.sql

+ 646
- 113
src/main/java/com/ffii/fpsms/modules/master/service/StockLedgerFixService.kt
文件差異過大導致無法顯示
查看文件


+ 7
- 0
src/main/java/com/ffii/fpsms/modules/master/web/models/StockLedgerFixModels.kt 查看文件

@@ -60,12 +60,15 @@ data class StockLedgerFixInventoryPreview(
val inventoryRows: Int,
val lotUomPairs: Int,
val missingUomPairs: Int,
val nullStockUomId: Int,
)

data class StockLedgerFixInventoryResponse(
val inserted: Int,
val updated: Int,
val missingUomPairsAfter: Int,
val patchedStockUomId: Int,
val nullStockUomIdAfter: Int,
)

data class StockLedgerFixSearchInventoryHit(
@@ -99,6 +102,7 @@ data class StockLedgerFixAdjRow(
val ledgerOut: String,
val missIn: String,
val missOut: String,
val overIssue: String,
)

data class StockLedgerFixAdjPreview(
@@ -110,6 +114,8 @@ data class StockLedgerFixAdjPreview(
val sumMissIn: String,
val sumMissOut: String,
val skuNet: String,
val overIssueCount: Int,
val sumOverIssue: String,
val rows: List<StockLedgerFixAdjRow>,
)

@@ -117,6 +123,7 @@ data class StockLedgerFixAdjResponse(
val adjDate: String,
val insertedIn: Int,
val insertedOut: Int,
val overIssuePatched: Int,
val filledLotQty: Int,
val filledBalance: Int,
val dayRowsWritten: Int,


+ 87
- 37
src/main/java/com/ffii/fpsms/modules/stock/service/StockLotDayCloseService.kt 查看文件

@@ -3,6 +3,8 @@ package com.ffii.fpsms.modules.stock.service
import com.ffii.core.support.JdbcDao
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDate
import java.util.concurrent.ConcurrentHashMap

@@ -12,10 +14,12 @@ import java.util.concurrent.ConcurrentHashMap
* - carry: previous closing > 0 with no ledger today → opening = closing = prev.closing
*
* Never closes today or future dates. Idempotent via ON DUPLICATE KEY UPDATE.
* Unchanged rows are skipped so a second close of the same day does not rewrite.
*/
@Service
open class StockLotDayCloseService(
private val jdbcDao: JdbcDao,
private val transactionManager: PlatformTransactionManager,
) {
private val log = LoggerFactory.getLogger(StockLotDayCloseService::class.java)
private val inFlight = ConcurrentHashMap.newKeySet<String>()
@@ -129,13 +133,29 @@ open class StockLotDayCloseService(
"actor" to actor,
)
val t0 = System.nanoTime()
val moveRows = jdbcDao.executeUpdate(MOVES_SQL, args)
val carryRows = jdbcDao.executeUpdate(CARRY_SQL, args)
val result = TransactionTemplate(transactionManager).execute {
jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sld_lots_today")
jdbcDao.executeUpdate(
"""
CREATE TEMPORARY TABLE tmp_sld_lots_today (
inventoryLotLineId BIGINT NOT NULL PRIMARY KEY
)
""".trimIndent(),
)
try {
jdbcDao.executeUpdate(FILL_LOTS_TODAY_SQL, args)
val moveRows = jdbcDao.executeUpdate(MOVES_SQL, args)
val carryRows = jdbcDao.executeUpdate(CARRY_SQL, args)
CloseDayResult(date = day, moveRows = moveRows, carryRows = carryRows)
} finally {
jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sld_lots_today")
}
} ?: CloseDayResult(date = day, moveRows = 0, carryRows = 0)
log.info(
"stock_lot_day close {} moves={} carry={} {}ms actor={}",
day, moveRows, carryRows, (System.nanoTime() - t0) / 1_000_000, actor,
day, result.moveRows, result.carryRows, (System.nanoTime() - t0) / 1_000_000, actor,
)
return CloseDayResult(date = day, moveRows = moveRows, carryRows = carryRows)
return result
}

private fun maxClosedDate(): LocalDate? {
@@ -160,6 +180,31 @@ open class StockLotDayCloseService(
const val ACTOR_FIX = "ledger-fix-api"
private const val LOCK = "stock-lot-day-close"

private const val ON_DUP = """
ON DUPLICATE KEY UPDATE
itemId = VALUES(itemId),
itemCode = VALUES(itemCode),
uomId = VALUES(uomId),
lotNo = VALUES(lotNo),
opening = VALUES(opening),
inQty = VALUES(inQty),
outQty = VALUES(outQty),
closing = VALUES(closing),
modified = NOW(),
modifiedBy = VALUES(modifiedBy),
version = stock_lot_day.version + 1,
deleted = 0
"""

private val FILL_LOTS_TODAY_SQL = """
INSERT INTO tmp_sld_lots_today (inventoryLotLineId)
SELECT DISTINCT sl.inventoryLotLineId
FROM stock_ledger sl
WHERE sl.deleted = 0
AND sl.date >= :day AND sl.date < :dayNext
AND sl.inventoryLotLineId IS NOT NULL
"""

private val MOVES_SQL = """
INSERT INTO stock_lot_day (
created, createdBy, version, modified, modifiedBy, deleted,
@@ -199,20 +244,23 @@ open class StockLotDayCloseService(
INNER JOIN stock_ledger lastSl ON lastSl.id = x.lastId
LEFT JOIN inventory_lot_line ill ON ill.id = x.inventoryLotLineId
LEFT JOIN inventory_lot il ON il.id = ill.inventoryLotId
ON DUPLICATE KEY UPDATE
itemId = VALUES(itemId),
itemCode = VALUES(itemCode),
uomId = VALUES(uomId),
lotNo = VALUES(lotNo),
opening = VALUES(opening),
inQty = VALUES(inQty),
outQty = VALUES(outQty),
closing = VALUES(closing),
modified = NOW(),
modifiedBy = VALUES(modifiedBy),
version = stock_lot_day.version + 1,
deleted = 0
""".trimIndent()
LEFT JOIN stock_lot_day cur
ON cur.inventoryLotLineId = x.inventoryLotLineId
AND cur.date = :day
WHERE cur.inventoryLotLineId IS NULL
OR IFNULL(cur.deleted, 0) <> 0
OR NOT (
cur.opening <=> firstSl.lotQtyBefore
AND cur.inQty <=> x.inQty
AND cur.outQty <=> x.outQty
AND cur.closing <=> lastSl.lotQtyAfter
AND cur.itemId <=> x.itemId
AND cur.uomId <=> x.uomId
AND IFNULL(cur.itemCode, '') <=> IFNULL(x.itemCode, '')
AND IFNULL(cur.lotNo, '') <=> IFNULL(il.lotNo, '')
)
$ON_DUP
"""

private val CARRY_SQL = """
INSERT INTO stock_lot_day (
@@ -233,28 +281,30 @@ open class StockLotDayCloseService(
0,
prev.closing
FROM stock_lot_day prev
LEFT JOIN tmp_sld_lots_today t
ON t.inventoryLotLineId = prev.inventoryLotLineId
LEFT JOIN stock_lot_day cur
ON cur.inventoryLotLineId = prev.inventoryLotLineId
AND cur.date = :day
WHERE prev.date = :prevDay
AND IFNULL(prev.deleted, 0) = 0
AND prev.closing > 0
AND NOT EXISTS (
SELECT 1 FROM stock_ledger sl
WHERE sl.deleted = 0
AND sl.date >= :day AND sl.date < :dayNext
AND sl.inventoryLotLineId = prev.inventoryLotLineId
AND t.inventoryLotLineId IS NULL
AND (
cur.inventoryLotLineId IS NULL
OR IFNULL(cur.deleted, 0) <> 0
OR NOT (
cur.opening <=> prev.closing
AND cur.inQty <=> 0
AND cur.outQty <=> 0
AND cur.closing <=> prev.closing
AND cur.itemId <=> prev.itemId
AND cur.uomId <=> prev.uomId
AND IFNULL(cur.itemCode, '') <=> IFNULL(prev.itemCode, '')
AND IFNULL(cur.lotNo, '') <=> IFNULL(prev.lotNo, '')
)
)
ON DUPLICATE KEY UPDATE
itemId = VALUES(itemId),
itemCode = VALUES(itemCode),
uomId = VALUES(uomId),
lotNo = VALUES(lotNo),
opening = VALUES(opening),
inQty = VALUES(inQty),
outQty = VALUES(outQty),
closing = VALUES(closing),
modified = NOW(),
modifiedBy = VALUES(modifiedBy),
version = stock_lot_day.version + 1,
deleted = 0
""".trimIndent()
$ON_DUP
"""
}
}

+ 1
- 1
src/main/resources/db/changelog/changes/20260814_stock_lot_day/01_create_stock_lot_day.sql 查看文件

@@ -2,7 +2,7 @@

--changeset fpsms:create_stock_lot_day
--comment: Daily lot stock card (主錨). Month-end report aggregates this table.
--precondition onFail:MARK_RAN
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'stock_lot_day'

CREATE TABLE `stock_lot_day` (


Loading…
取消
儲存