Преглед изворни кода

remove the preview of 2.7 and correct 2,7 to fix data correct

fix負數倉
CANCERYS\kw093 пре 6 часа
родитељ
комит
5f65764892
3 измењених фајлова са 261 додато и 219 уклоњено
  1. +235
    -216
      src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixAdjService.kt
  2. +7
    -3
      src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixBackfillService.kt
  3. +19
    -0
      src/main/resources/db/changelog/changes/20260930_ledger_inv_date_index/01_ledger_inv_date_index.sql

+ 235
- 216
src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixAdjService.kt Прегледај датотеку

@@ -20,48 +20,40 @@ open class StockLedgerFixAdjService(
private val log = LoggerFactory.getLogger(StockLedgerFixAdjService::class.java)

/**
* Line in/out vs Σ ledger in/out. Any non-zero gap becomes ADJ on [adjDate]
* (default yesterday; pass today on freeze-night dump).
* Step 1: over-issue (`lineIn < lineOut`) sets lot inQty = outQty (trigger updates inventory)
* and writes SIL + ledger ADJ in. Step 2: miss ADJ (no further lot qty write).
* Positive miss → ADJ in/out; negative miss → reverse ADJ out/in.
* One ADJ per inventory bucket on [adjDate] (default yesterday; pass today on freeze-night dump).
* Quantity is onHandQty minus the bucket ledger net, including rows with no lot.
* The row is attached to the lot line whose own net is furthest from its ledger.
* Lot lines and inventory are not updated, so the lot-line trigger does not run.
* A bucket whose ledger net already equals onHandQty is skipped. A second run inserts nothing.
* All inbound lines share one stock_in; all outbound lines share one stock_out (per adjDate).
* Ledger gap sum uses `date < adjDate+1`.
* Ledger net uses `date < adjDate+1`.
*/
open fun adjPreview(rowLimit: Int = 20, adjDateRaw: String? = null): StockLedgerFixAdjPreview {
val (adjDate, asOfExclusive) = resolveAdjWindow(adjDateRaw)
val t0 = System.nanoTime()
val gaps = loadAdjGaps(asOfExclusive)
val work = gaps.filter { it.hasWork() }
val over = work.filter { it.hasOverIssue() }
val miss = work.filter { it.hasMiss() }
val sumIn = miss.fold(BigDecimal.ZERO) { a, g -> a + g.missIn }
val sumOut = miss.fold(BigDecimal.ZERO) { a, g -> a + g.missOut }
val sumOver = over.fold(BigDecimal.ZERO) { a, g -> a + g.overIssue }
val plans = planBucketAdjs(asOfExclusive, resolveHost = false)
val sumIn = plans.fold(BigDecimal.ZERO) { a, g -> a + g.missIn }
val sumOut = plans.fold(BigDecimal.ZERO) { a, g -> a + g.missOut }
val limit = rowLimit.coerceIn(0, 50)
log.info(
"stock-ledger-fix adj preview lots={} over={} in={} out={} revIn={} revOut={} {}ms",
work.size,
over.size,
miss.count { it.missIn > BigDecimal.ZERO },
miss.count { it.missOut > BigDecimal.ZERO },
miss.count { it.missIn < BigDecimal.ZERO },
miss.count { it.missOut < BigDecimal.ZERO },
"stock-ledger-fix adj preview buckets={} in={} out={} {}ms",
plans.size,
plans.count { it.missIn > BigDecimal.ZERO },
plans.count { it.missOut > BigDecimal.ZERO },
(System.nanoTime() - t0) / 1_000_000,
)
val rows = (over + miss.filter { !it.hasOverIssue() }).distinctBy { it.lotLineId }.take(limit)
return StockLedgerFixAdjPreview(
adjDate = adjDate.toString(),
lotCount = work.size,
adjInCount = miss.count { it.missIn > BigDecimal.ZERO },
adjOutCount = miss.count { it.missOut > BigDecimal.ZERO },
skippedNegCount = miss.count { it.missIn < BigDecimal.ZERO || it.missOut < BigDecimal.ZERO },
lotCount = plans.size,
adjInCount = plans.count { it.missIn > BigDecimal.ZERO },
adjOutCount = plans.count { it.missOut > BigDecimal.ZERO },
skippedNegCount = 0,
sumMissIn = sumIn.stripTrailingZeros().toPlainString(),
sumMissOut = sumOut.stripTrailingZeros().toPlainString(),
skuNet = (sumIn - sumOut).stripTrailingZeros().toPlainString(),
overIssueCount = over.size,
sumOverIssue = sumOver.stripTrailingZeros().toPlainString(),
rows = rows.map { it.toRow() },
overIssueCount = 0,
sumOverIssue = "0",
rows = plans.take(limit).map { it.toRow() },
)
}

@@ -72,14 +64,11 @@ open class StockLedgerFixAdjService(
val (adjDate, asOfExclusive) = resolveAdjWindow(adjDateRaw)
val t0 = System.nanoTime()
try {
val gaps = loadAdjGaps(asOfExclusive)
val work = gaps.filter { it.hasWork() }
val over = work.filter { it.hasOverIssue() }
val miss = work.filter { it.hasMiss() }
val plans = planBucketAdjs(asOfExclusive, resolveHost = true)
var insertedIn = 0
var insertedOut = 0
var overIssuePatched = 0
if (work.isNotEmpty()) {
val overIssuePatched = 0
if (plans.isNotEmpty()) {
val insTx = TransactionTemplate(support.transactionManager)
insTx.timeout = 300
insTx.execute {
@@ -89,34 +78,20 @@ open class StockLedgerFixAdjService(
stockInId ?: ensureAdjStockInHeader(adjDate).also { stockInId = it }
fun outHeader(): Long =
stockOutId ?: ensureAdjStockOutHeader(adjDate).also { stockOutId = it }
over.forEach { g ->
if (patchLotRemainToZero(g.lotLineId)) {
insertAdjLedger(
g, adjDate, inQty = g.overIssue, outQty = null,
remarks = OVER_ISSUE_REMARKS, stockInId = inHeader(),
)
insertedIn++
overIssuePatched++
}
}
miss.forEach { g ->
if (g.missIn.compareTo(BigDecimal.ZERO) > 0) {
insertAdjLedger(g, adjDate, inQty = g.missIn, outQty = null, stockInId = inHeader())
insertedIn++
} else if (g.missIn.compareTo(BigDecimal.ZERO) < 0) {
insertAdjLedger(
g, adjDate, inQty = null, outQty = g.missIn.negate(), stockOutId = outHeader(),
)
insertedOut++
}
if (g.missOut.compareTo(BigDecimal.ZERO) > 0) {
insertAdjLedger(g, adjDate, inQty = null, outQty = g.missOut, stockOutId = outHeader())
insertedOut++
} else if (g.missOut.compareTo(BigDecimal.ZERO) < 0) {
insertAdjLedger(
g, adjDate, inQty = g.missOut.negate(), outQty = null, stockInId = inHeader(),
)
insertedIn++
plans.forEach { g ->
when {
g.missIn > BigDecimal.ZERO -> {
insertAdjLedger(
g, adjDate, inQty = g.missIn, outQty = null, stockInId = inHeader(),
)
insertedIn++
}
g.missOut > BigDecimal.ZERO -> {
insertAdjLedger(
g, adjDate, inQty = null, outQty = g.missOut, stockOutId = outHeader(),
)
insertedOut++
}
}
}
}
@@ -125,7 +100,7 @@ open class StockLedgerFixAdjService(
fun addLot(lotId: Long, invId: Long?) {
if (lotId > 0) lotIds.add(lotId)
}
work.forEach { addLot(it.lotLineId, it.inventoryId) }
plans.forEach { addLot(it.lotLineId, it.inventoryId) }
support.jdbcDao.queryForList(
"""
SELECT DISTINCT inventoryLotLineId AS lotId, inventoryId AS invId
@@ -147,8 +122,9 @@ open class StockLedgerFixAdjService(
support.timed("adj 2.6") { backfill.upsertStockLotDayRange(lotIds.toList(), range.first, range.second) }
} else 0
log.info(
"stock-ledger-fix adj {} over={} in={} out={} lotQty={} bal={} day={} total={}ms",
adjDate, overIssuePatched, insertedIn, insertedOut, filledLotQty, filledBalance, dayRows,
"stock-ledger-fix adj {} buckets={} in={} out={} lotQty={} bal={} day={} total={}ms",
adjDate, plans.size, insertedIn, insertedOut,
filledLotQty, filledBalance, dayRows,
(System.nanoTime() - t0) / 1_000_000,
)
return StockLedgerFixAdjResponse(
@@ -185,141 +161,206 @@ open class StockLedgerFixAdjService(
return adjDate to adjDate.plusDays(1)
}

private fun loadAdjGaps(today: LocalDate): List<AdjGap> {
val tx = TransactionTemplate(support.transactionManager)
tx.timeout = 300
val args = mapOf("today" to today)
return tx.execute {
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_adj_line")
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_adj_led")
support.jdbcDao.executeUpdate(
"""
CREATE TEMPORARY TABLE tmp_adj_line (
lotLineId BIGINT NOT NULL PRIMARY KEY,
inventoryId INT NULL,
itemId INT NULL,
itemCode VARCHAR(64) NULL,
uomId INT NULL,
lineIn DECIMAL(14,2) NOT NULL,
lineOut DECIMAL(14,2) NOT NULL
/**
* One ADJ per bucket whose latest balance differs from onHandQty.
* After 2.5 that balance equals the bucket ledger net, so the ADJ qty is onHandQty − balance.
* Preview does not scan lot history. Apply resolves which lot line the ADJ is attached to.
*/
private fun planBucketAdjs(today: LocalDate, resolveHost: Boolean): List<AdjGap> {
val buckets = loadMismatchedByLastBalance(today)
if (buckets.isEmpty()) return emptyList()
if (!resolveHost) {
val lines = loadLotLines(buckets.mapNotNull { it.lotLineId }.distinct())
return buckets.map { bucket -> previewGap(bucket, lines[bucket.lotLineId]) }
}
val lotsByInv = loadLotsOnBuckets(today, buckets.map { it.inventoryId })
return buckets.mapNotNull { bucket ->
val host = chooseAdjHost(lotsByInv[bucket.inventoryId].orEmpty(), bucket.delta)
if (host == null) {
log.warn(
"stock-ledger-fix adj skip inventoryId={} delta={}: no lot line to attach",
bucket.inventoryId, bucket.delta,
)
""".trimIndent(),
return@mapNotNull null
}
val needIn = bucket.delta > BigDecimal.ZERO
AdjGap(
lotLineId = host.lotLineId,
inventoryId = bucket.inventoryId,
itemId = host.itemId,
itemCode = host.itemCode,
uomId = host.uomId,
lineIn = host.lineIn,
lineOut = host.lineOut,
ledgerIn = host.ledgerIn,
ledgerOut = host.ledgerOut,
missIn = if (needIn) bucket.delta else BigDecimal.ZERO,
missOut = if (needIn) BigDecimal.ZERO else bucket.delta.negate(),
)
support.jdbcDao.executeUpdate(
"""
CREATE TEMPORARY TABLE tmp_adj_led (
inventoryLotLineId BIGINT NOT NULL PRIMARY KEY,
ledger_in DECIMAL(14,2) NOT NULL,
ledger_out DECIMAL(14,2) NOT NULL
)
""".trimIndent(),
}
}

private data class BucketDelta(
val inventoryId: Long,
val delta: BigDecimal,
val lotLineId: Long?,
val itemId: Long?,
val itemCode: String?,
)

/** Preview row. Ledger in/out stay 0; the bucket delta is missIn / missOut. */
private fun previewGap(bucket: BucketDelta, line: Map<String, Any?>?) = AdjGap(
lotLineId = bucket.lotLineId ?: 0L,
inventoryId = bucket.inventoryId,
itemId = support.toLongOrNull(line?.get("itemId") ?: bucket.itemId),
itemCode = (line?.get("itemCode") ?: bucket.itemCode)?.toString(),
uomId = support.toLongOrNull(line?.get("uomId")),
lineIn = support.toDecimal(line?.get("lineIn")),
lineOut = support.toDecimal(line?.get("lineOut")),
ledgerIn = BigDecimal.ZERO,
ledgerOut = BigDecimal.ZERO,
missIn = if (bucket.delta > BigDecimal.ZERO) bucket.delta else BigDecimal.ZERO,
missOut = if (bucket.delta > BigDecimal.ZERO) BigDecimal.ZERO else bucket.delta.negate(),
)

private data class LotOnBucket(
val lotLineId: Long,
val itemId: Long?,
val itemCode: String?,
val uomId: Long?,
val lineIn: BigDecimal,
val lineOut: BigDecimal,
val ledgerIn: BigDecimal,
val ledgerOut: BigDecimal,
) {
val lotGap: BigDecimal
get() = lineIn.subtract(lineOut).subtract(ledgerIn.subtract(ledgerOut))
}

/**
* Latest balance per bucket (date, then id). After 2.5 this equals the ledger net,
* so onHand − balance is the ADJ qty. One index seek per inventory, not a full sum.
*/
private fun loadMismatchedByLastBalance(today: LocalDate): List<BucketDelta> {
return support.jdbcDao.queryForList(
"""
SELECT i.id AS inventoryId,
i.itemId AS itemId,
it.code AS itemCode,
CAST(COALESCE(i.onHandQty, 0) AS DECIMAL(14,2)) AS onHand,
x.balance AS balance,
x.lotLineId AS lotLineId
FROM inventory i
LEFT JOIN items it ON it.id = i.itemId
INNER JOIN LATERAL (
SELECT s.balance AS balance,
s.inventoryLotLineId AS lotLineId
FROM stock_ledger s FORCE INDEX (idx_ledger_inv_last)
WHERE s.inventoryId = i.id
AND s.deleted = 0
AND s.date < :today
AND s.balance IS NOT NULL
ORDER BY s.date DESC, s.id DESC
LIMIT 1
) x ON TRUE
WHERE IFNULL(i.deleted, 0) = 0
AND ABS(CAST(x.balance AS DECIMAL(14,2))
- CAST(COALESCE(i.onHandQty, 0) AS DECIMAL(14,2))) > 0.01
""".trimIndent(),
mapOf("today" to today),
).mapNotNull { row ->
val delta = support.toDecimal(row["onHand"]).subtract(support.toDecimal(row["balance"]))
if (delta.abs().compareTo(ADJ_TOLERANCE) <= 0) null
else BucketDelta(
inventoryId = support.toLong(row["inventoryId"]),
delta = delta,
lotLineId = support.toLongOrNull(row["lotLineId"]),
itemId = support.toLongOrNull(row["itemId"]),
itemCode = row["itemCode"]?.toString(),
)
try {
support.timed("adj line-all") {
support.jdbcDao.executeUpdate(
"""
INSERT INTO tmp_adj_line (
lotLineId, inventoryId, itemId, itemCode, uomId, lineIn, lineOut
)
SELECT
ill.id,
inv.inventoryId,
il.itemId,
it.code,
iu.uomId,
CAST(COALESCE(ill.inQty, 0) AS DECIMAL(14,2)),
CAST(COALESCE(ill.outQty, 0) AS DECIMAL(14,2))
FROM inventory_lot_line ill
INNER JOIN inventory_lot il
ON il.id = ill.inventoryLotId AND IFNULL(il.deleted, 0) = 0
LEFT JOIN items it ON it.id = il.itemId
LEFT JOIN item_uom iu ON iu.id = ill.stockItemUomId
LEFT JOIN (
SELECT itemId, COALESCE(stockUomId, uomId) AS bucketUomId, MIN(id) AS inventoryId
FROM inventory
WHERE IFNULL(deleted, 0) = 0
GROUP BY itemId, COALESCE(stockUomId, uomId)
) inv ON inv.itemId = il.itemId AND inv.bucketUomId = iu.uomId
WHERE IFNULL(ill.deleted, 0) = 0
AND (
COALESCE(ill.inQty, 0) <> 0
OR COALESCE(ill.outQty, 0) <> 0
)
""".trimIndent(),
)
}
val lineIds = support.jdbcDao.queryForList(
"SELECT lotLineId FROM tmp_adj_line",
).map { support.toLong(it["lotLineId"]) }.filter { it > 0 }
log.info("stock-ledger-fix adj line rows={}", lineIds.size)
if (lineIds.isEmpty()) return@execute emptyList()
support.timed("adj ledger-by-line") {
lineIds.chunked(40).sumOf { chunk ->
support.jdbcDao.executeUpdate(
"""
INSERT INTO tmp_adj_led (inventoryLotLineId, ledger_in, ledger_out)
SELECT
sl.inventoryLotLineId,
CAST(SUM(COALESCE(sl.inQty, 0)) AS DECIMAL(14,2)),
CAST(SUM(COALESCE(sl.outQty, 0)) AS DECIMAL(14,2))
FROM stock_ledger sl FORCE INDEX (idx_ledger_lot_date_id)
WHERE sl.deleted = 0
AND sl.date < :today
AND sl.inventoryLotLineId IN (:ids)
GROUP BY sl.inventoryLotLineId
""".trimIndent(),
mapOf("today" to today, "ids" to chunk),
)
}
}
}
}

/** Lot nets only for buckets that need an ADJ, so the host lot can be chosen. */
private fun loadLotsOnBuckets(today: LocalDate, inventoryIds: List<Long>): Map<Long, List<LotOnBucket>> {
val ledgerRows = mutableListOf<Map<String, Any?>>()
inventoryIds.chunked(50).forEach { chunk ->
ledgerRows.addAll(
support.jdbcDao.queryForList(
"""
SELECT
l.lotLineId,
l.inventoryId,
l.itemId,
l.itemCode,
l.uomId,
l.lineIn,
l.lineOut,
CAST(COALESCE(agg.ledger_in, 0) AS DECIMAL(14,2)) AS ledgerIn,
CAST(COALESCE(agg.ledger_out, 0) AS DECIMAL(14,2)) AS ledgerOut
FROM tmp_adj_line l
LEFT JOIN tmp_adj_led agg ON agg.inventoryLotLineId = l.lotLineId
WHERE l.lineIn <> CAST(COALESCE(agg.ledger_in, 0) AS DECIMAL(14,2))
OR l.lineOut <> CAST(COALESCE(agg.ledger_out, 0) AS DECIMAL(14,2))
OR l.lineIn < l.lineOut
SELECT sl.inventoryId,
sl.inventoryLotLineId AS lotLineId,
CAST(SUM(COALESCE(sl.inQty, 0)) AS DECIMAL(14,2)) AS ledgerIn,
CAST(SUM(COALESCE(sl.outQty, 0)) AS DECIMAL(14,2)) AS ledgerOut,
MIN(sl.itemId) AS itemId,
MIN(sl.itemCode) AS itemCode,
MIN(sl.uomId) AS uomId
FROM stock_ledger sl FORCE INDEX (idx_ledger_inv_date)
WHERE sl.deleted = 0
AND sl.date < :today
AND sl.inventoryId IN (:ids)
AND sl.inventoryLotLineId IS NOT NULL
GROUP BY sl.inventoryId, sl.inventoryLotLineId
""".trimIndent(),
).map { row ->
val lineIn = support.toDecimal(row["lineIn"])
val lineOut = support.toDecimal(row["lineOut"])
val ledgerIn = support.toDecimal(row["ledgerIn"])
val ledgerOut = support.toDecimal(row["ledgerOut"])
AdjGap(
lotLineId = support.toLong(row["lotLineId"]),
inventoryId = support.toLongOrNull(row["inventoryId"]),
itemId = support.toLongOrNull(row["itemId"]),
itemCode = row["itemCode"]?.toString(),
uomId = support.toLongOrNull(row["uomId"]),
lineIn = lineIn,
lineOut = lineOut,
ledgerIn = ledgerIn,
ledgerOut = ledgerOut,
missIn = lineIn.subtract(ledgerIn),
missOut = lineOut.subtract(ledgerOut),
)
}
} finally {
try {
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_adj_led")
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_adj_line")
} catch (e: Exception) {
log.warn("stock-ledger-fix drop tmp_adj skipped: {}", e.message)
}
}
} ?: emptyList()
mapOf("today" to today, "ids" to chunk),
),
)
}
if (ledgerRows.isEmpty()) return emptyMap()
val lines = loadLotLines(ledgerRows.map { support.toLong(it["lotLineId"]) }.distinct())
return ledgerRows.map { row ->
val lotLineId = support.toLong(row["lotLineId"])
val line = lines[lotLineId]
LotOnBucket(
lotLineId = lotLineId,
itemId = support.toLongOrNull(line?.get("itemId") ?: row["itemId"]),
itemCode = (line?.get("itemCode") ?: row["itemCode"])?.toString(),
uomId = support.toLongOrNull(row["uomId"] ?: line?.get("uomId")),
lineIn = support.toDecimal(line?.get("lineIn")),
lineOut = support.toDecimal(line?.get("lineOut")),
ledgerIn = support.toDecimal(row["ledgerIn"]),
ledgerOut = support.toDecimal(row["ledgerOut"]),
) to support.toLong(row["inventoryId"])
}.groupBy({ it.second }, { it.first })
}

private fun loadLotLines(lotIds: List<Long>): Map<Long, Map<String, Any?>> {
if (lotIds.isEmpty()) return emptyMap()
val lines = HashMap<Long, Map<String, Any?>>()
lotIds.chunked(200).forEach { chunk ->
support.jdbcDao.queryForList(
"""
SELECT ill.id AS lotLineId,
CAST(COALESCE(ill.inQty, 0) AS DECIMAL(14,2)) AS lineIn,
CAST(COALESCE(ill.outQty, 0) AS DECIMAL(14,2)) AS lineOut,
il.itemId AS itemId,
it.code AS itemCode,
iu.uomId AS uomId
FROM inventory_lot_line ill
INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId
LEFT JOIN items it ON it.id = il.itemId
LEFT JOIN item_uom iu ON iu.id = ill.stockItemUomId
WHERE ill.id IN (:ids)
""".trimIndent(),
mapOf("ids" to chunk),
).forEach { lines[support.toLong(it["lotLineId"])] = it }
}
return lines
}

/** Prefer the lot whose own gap has the same sign as the bucket delta, largest first. */
private fun chooseAdjHost(lots: List<LotOnBucket>, delta: BigDecimal): LotOnBucket? {
if (lots.isEmpty()) return null
val sameSign = lots.filter {
it.lotGap.abs().compareTo(ADJ_TOLERANCE) > 0 && it.lotGap.signum() == delta.signum()
}
if (sameSign.isNotEmpty()) return sameSign.maxBy { it.lotGap.abs() }
val anyGap = lots.filter { it.lotGap.abs().compareTo(ADJ_TOLERANCE) > 0 }
if (anyGap.isNotEmpty()) return anyGap.maxBy { it.lotGap.abs() }
return lots.maxBy { it.ledgerIn.add(it.ledgerOut) }
}


private fun insertAdjLedger(
g: AdjGap,
adjDate: LocalDate,
@@ -369,28 +410,6 @@ open class StockLedgerFixAdjService(
)
}

/** Sets lot inQty = outQty so remain = 0; trigger recomputes inventory. */
private fun patchLotRemainToZero(lotLineId: Long): Boolean {
val n = support.jdbcDao.executeUpdate(
"""
UPDATE inventory_lot_line
SET inQty = CAST(COALESCE(outQty, 0) AS DECIMAL(14,2)),
status = 'unavailable',
modified = NOW(),
modifiedBy = 'stock-ledger-fix',
version = IFNULL(version, 0) + 1
WHERE id = :lotLineId
AND IFNULL(deleted, 0) = 0
AND CAST(COALESCE(inQty, 0) AS DECIMAL(14,2))
< CAST(COALESCE(outQty, 0) AS DECIMAL(14,2))
""".trimIndent(),
mapOf("lotLineId" to lotLineId),
)
if (n == 0) {
log.warn("stock-ledger-fix over-issue skip lot update: lotLineId={}", lotLineId)
}
return n > 0
}

private fun adjStockInCode(adjDate: LocalDate): String = "SLF-I-$adjDate"

@@ -609,8 +628,8 @@ open class StockLedgerFixAdjService(
}

companion object {
private const val ADJ_DOC_REMARKS = "stock-ledger-fix miss ADJ (no lot qty change)"
private const val OVER_ISSUE_REMARKS = "stock-ledger-fix over-issue remain→0 (lot inQty patched)"
private val ADJ_TOLERANCE = BigDecimal("0.01")
private const val ADJ_DOC_REMARKS = "stock-ledger-fix bucket ADJ (lot line unchanged)"
private const val ADJ_HEADER_REMARKS = "stock-ledger-fix 2.7 ADJ"
}
}

+ 7
- 3
src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixBackfillService.kt Прегледај датотеку

@@ -1301,7 +1301,7 @@ open class StockLedgerFixBackfillService(
- CAST(COALESCE(sl2.outQty, 0) AS DECIMAL(14,2))
) OVER (
PARTITION BY sl2.inventoryLotLineId
ORDER BY sl2.id
ORDER BY sl2.date, sl2.id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
AS DECIMAL(14,2)) AS lotAfter,
@@ -1330,7 +1330,10 @@ open class StockLedgerFixBackfillService(
}
}

/** Chain balance on new ADJ rows only; older rows for the same SKU are unchanged. */
/**
* Chain balance on new ADJ rows only; older rows for the same SKU are unchanged.
* Opening balance is the latest date, then the highest id on that date (same rule as 2.5 lookback).
*/
open internal fun fillBalanceOnNewAdjRows(adjDate: LocalDate, today: LocalDate): Int {
val rows = support.jdbcDao.queryForList(
"""
@@ -1363,7 +1366,8 @@ open class StockLedgerFixBackfillService(
AND inventoryId = :invId
AND date < :today
AND id < :adjId
ORDER BY id DESC
AND balance IS NOT NULL
ORDER BY date DESC, id DESC
LIMIT 1
""".trimIndent(),
mapOf("invId" to invId, "today" to today, "adjId" to id),


+ 19
- 0
src/main/resources/db/changelog/changes/20260930_ledger_inv_date_index/01_ledger_inv_date_index.sql Прегледај датотеку

@@ -0,0 +1,19 @@
--liquibase formatted sql

--changeset fpsms:ledger_inv_date_covering_index
--comment: Cover 2.7 bucket net by inventoryId + date without reading the full stock_ledger row
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'stock_ledger' AND INDEX_NAME = 'idx_ledger_inv_date'

ALTER TABLE `stock_ledger`
ADD INDEX `idx_ledger_inv_date` (`inventoryId`, `date`, `inventoryLotLineId`, `inQty`, `outQty`, `deleted`),
ALGORITHM=INPLACE, LOCK=NONE;

--changeset fpsms:ledger_inv_last_balance_index
--comment: Last ledger balance per inventory for 2.7 preview (date desc, id desc, limit 1)
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'stock_ledger' AND INDEX_NAME = 'idx_ledger_inv_last'

ALTER TABLE `stock_ledger`
ADD INDEX `idx_ledger_inv_last` (`inventoryId`, `deleted`, `date`, `id`, `balance`),
ALGORITHM=INPLACE, LOCK=NONE;

Loading…
Откажи
Сачувај