Преглед на файлове

2.1 fix and 2.7 fix improve

report update
fix負數倉
CANCERYS\kw093 преди 3 седмици
родител
ревизия
04783a2e74
променени са 7 файла, в които са добавени 583 реда и са изтрити 27 реда
  1. +53
    -3
      src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixAdjService.kt
  2. +233
    -10
      src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixBackfillService.kt
  3. +189
    -7
      src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixExportService.kt
  4. +80
    -6
      src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixQueryService.kt
  5. +12
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixSupport.kt
  6. +12
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/models/StockLedgerFixModels.kt
  7. +4
    -1
      src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt

+ 53
- 3
src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixAdjService.kt Целия файл

@@ -24,7 +24,8 @@ open class StockLedgerFixAdjService(
* 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.
* Each ADJ line also gets one stock_adjustment_record. A later run fills any ADJ line that still has none.
* A bucket whose ledger net already equals onHandQty is skipped. A second run inserts no new ADJ.
* All inbound lines share one stock_in; all outbound lines share one stock_out (per adjDate).
* Ledger net uses `date < adjDate+1`.
*/
@@ -96,6 +97,10 @@ open class StockLedgerFixAdjService(
}
}
}
val adjustmentRecords = TransactionTemplate(support.transactionManager).let { tx ->
tx.timeout = 300
tx.execute { insertMissingAdjustmentRecords() } ?: 0
}
val lotIds = linkedSetOf<Long>()
fun addLot(lotId: Long, invId: Long?) {
if (lotId > 0) lotIds.add(lotId)
@@ -122,8 +127,8 @@ open class StockLedgerFixAdjService(
support.timed("adj 2.6") { backfill.upsertStockLotDayRange(lotIds.toList(), range.first, range.second) }
} else 0
log.info(
"stock-ledger-fix adj {} buckets={} in={} out={} lotQty={} bal={} day={} total={}ms",
adjDate, plans.size, insertedIn, insertedOut,
"stock-ledger-fix adj {} buckets={} in={} out={} adjRecord={} lotQty={} bal={} day={} total={}ms",
adjDate, plans.size, insertedIn, insertedOut, adjustmentRecords,
filledLotQty, filledBalance, dayRows,
(System.nanoTime() - t0) / 1_000_000,
)
@@ -135,6 +140,7 @@ open class StockLedgerFixAdjService(
filledLotQty = filledLotQty,
filledBalance = filledBalance,
dayRowsWritten = dayRows,
adjustmentRecords = adjustmentRecords,
)
} catch (e: Exception) {
log.warn("stock-ledger-fix adj FAILED after {}ms: {}", (System.nanoTime() - t0) / 1_000_000, e.message)
@@ -475,6 +481,50 @@ open class StockLedgerFixAdjService(
)
}

/** One remarks row per stock-ledger-fix ADJ line that does not already have one. */
private fun insertMissingAdjustmentRecords(): Int {
return support.jdbcDao.executeUpdate(
"""
INSERT INTO stock_adjustment_record (
created, createdBy, version, modified, modifiedBy, deleted,
itemId, itemCode, itemName, lotNo, inQty, outQty, remarks,
stockInLineId, stockOutLineId
)
SELECT
NOW(), 'stock-ledger-fix', 0, NOW(), 'stock-ledger-fix', 0,
sl.itemId,
LEFT(COALESCE(sl.itemCode, it.code), 50),
LEFT(it.name, 200),
LEFT(il.lotNo, 512),
CASE WHEN sl.stockInLineId IS NOT NULL THEN sl.inQty ELSE NULL END,
CASE WHEN sl.stockOutLineId IS NOT NULL THEN sl.outQty ELSE NULL END,
LEFT(COALESCE(sil.remarks, so.remarks, 'stock-ledger-fix 2.7 ADJ'), 500),
sl.stockInLineId,
sl.stockOutLineId
FROM stock_ledger sl
INNER JOIN items it ON it.id = sl.itemId
LEFT JOIN inventory_lot_line ill ON ill.id = sl.inventoryLotLineId
LEFT JOIN inventory_lot il ON il.id = ill.inventoryLotId
LEFT JOIN stock_in_line sil ON sil.id = sl.stockInLineId
LEFT JOIN stock_out_line sol ON sol.id = sl.stockOutLineId
LEFT JOIN stock_out so ON so.id = sol.stockOutId
WHERE sl.deleted = 0
AND sl.type = 'ADJ'
AND sl.createdBy = 'stock-ledger-fix'
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
AND NOT EXISTS (
SELECT 1
FROM stock_adjustment_record r
WHERE IFNULL(r.deleted, 0) = 0
AND (
(sl.stockInLineId IS NOT NULL AND r.stockInLineId = sl.stockInLineId)
OR (sl.stockOutLineId IS NOT NULL AND r.stockOutLineId = sl.stockOutLineId)
)
)
""".trimIndent(),
)
}

/** Line on shared stock_in; does not UPDATE inventory_lot_line or inventory. */
private fun insertAdjStockInLine(
g: AdjGap,


+ 233
- 10
src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixBackfillService.kt Целия файл

@@ -79,7 +79,8 @@ open class StockLedgerFixBackfillService(
"uomId" to (inv["uomId"] ?: 0),
"today" to toExclusive,
)
val filledLotLineId = support.timed("inv 2.1") { fillLotLineIdScope(args, byInventory = true) }
val lotStep = support.timedValue("inv 2.1") { fillLotLineIdScope(args, byInventory = true) }
val filledLotLineId = lotStep.filled
val filledUomId = support.timed("inv 2.2") { fillUomIdScope(args, byInventory = true) }
val filledInventoryId = support.timed("inv 2.3") { fillInventoryIdScope(args, byInventory = true) }
val filledLotQty = support.timed("inv 2.4") { fillLotQtyHistory(args, byInventory = true) }
@@ -107,6 +108,11 @@ open class StockLedgerFixBackfillService(
stillMissUom = still.missUom,
stillMissInventoryId = still.missInventoryId,
stillMissLotQty = still.missLotQty,
deletedUnlinkedLot = lotStep.deletedUnlinked,
existingLotMatch = lotStep.existingMatch,
existingLotMismatch = lotStep.existingMismatch,
deletedBlankOutLine = lotStep.deletedBlankOutLine,
alignedOutLine = lotStep.alignedOutLine,
)
} catch (e: Exception) {
log.warn("stock-ledger-fix inventory {} FAILED after {}ms: {}", inventoryId, (System.nanoTime() - t0) / 1_000_000, e.message)
@@ -130,7 +136,8 @@ open class StockLedgerFixBackfillService(
)
if (exists == 0) throw IllegalArgumentException("lot line not found: $lotLineId")
val args = mapOf("lotId" to lotLineId, "today" to today)
val filledLotLineId = support.timed("lot 2.1") { fillLotLineIdScope(args, byInventory = false) }
val lotStep = support.timedValue("lot 2.1") { fillLotLineIdScope(args, byInventory = false) }
val filledLotLineId = lotStep.filled
val filledUomId = support.timed("lot 2.2") { fillUomIdScope(args, byInventory = false) }
val filledInventoryId = support.timed("lot 2.3") { fillInventoryIdScope(args, byInventory = false) }
val filledLotQty = support.timed("lot 2.4") { fillLotQtyHistory(args, byInventory = false) }
@@ -156,6 +163,11 @@ open class StockLedgerFixBackfillService(
stillMissUom = still.missUom,
stillMissInventoryId = still.missInventoryId,
stillMissLotQty = still.missLotQty,
deletedUnlinkedLot = lotStep.deletedUnlinked,
existingLotMatch = lotStep.existingMatch,
existingLotMismatch = lotStep.existingMismatch,
deletedBlankOutLine = lotStep.deletedBlankOutLine,
alignedOutLine = lotStep.alignedOutLine,
)
} catch (e: Exception) {
log.warn("stock-ledger-fix lot {} FAILED after {}ms: {}", lotLineId, (System.nanoTime() - t0) / 1_000_000, e.message)
@@ -184,7 +196,8 @@ open class StockLedgerFixBackfillService(
val t0 = System.nanoTime()
try {
val args = support.dayArgs(day)
val filledLotLineId = if ("2.1" in steps) support.timed("2.1 fillLotLineId") { fillLotLineId(args) } else 0
val lotStep = if ("2.1" in steps) support.timedValue("2.1 fillLotLineId") { fillLotLineId(args) } else LotLineStep(0, 0, 0, 0)
val filledLotLineId = lotStep.filled
val filledUomId = if ("2.2" in steps) support.timed("2.2 fillUomId") { fillUomId(args) } else 0
val filledInventoryId = if ("2.3" in steps) support.timed("2.3 fillInventoryId") { fillInventoryId(args) } else 0
val filledLotQty = if ("2.4" in steps) support.timed("2.4 fillLotQty") { fillLotQty(args) } else 0
@@ -215,6 +228,11 @@ open class StockLedgerFixBackfillService(
stillMissUom = still.missUom,
stillMissInventoryId = still.missInventoryId,
stillMissLotQty = still.missLotQty,
deletedUnlinkedLot = lotStep.deletedUnlinked,
existingLotMatch = lotStep.existingMatch,
existingLotMismatch = lotStep.existingMismatch,
deletedBlankOutLine = lotStep.deletedBlankOutLine,
alignedOutLine = lotStep.alignedOutLine,
)
} catch (e: Exception) {
log.warn(
@@ -251,6 +269,11 @@ open class StockLedgerFixBackfillService(
val t0 = System.nanoTime()
try {
var filledLotLineId = 0
var deletedUnlinkedLot = 0
var existingLotMatch = 0
var existingLotMismatch = 0
var deletedBlankOutLine = 0
var alignedOutLine = 0
var filledUomId = 0
var filledInventoryId = 0
var filledLotQty = 0
@@ -259,7 +282,15 @@ open class StockLedgerFixBackfillService(
var d = from
while (!d.isAfter(to)) {
val args = support.dayArgs(d)
if ("2.1" in steps) filledLotLineId += support.timed("range $d 2.1") { fillLotLineId(args) }
if ("2.1" in steps) {
val lotStep = support.timedValue("range $d 2.1") { fillLotLineId(args) }
filledLotLineId += lotStep.filled
deletedUnlinkedLot += lotStep.deletedUnlinked
existingLotMatch += lotStep.existingMatch
existingLotMismatch += lotStep.existingMismatch
deletedBlankOutLine += lotStep.deletedBlankOutLine
alignedOutLine += lotStep.alignedOutLine
}
if ("2.2" in steps) filledUomId += support.timed("range $d 2.2") { fillUomId(args) }
if ("2.3" in steps) filledInventoryId += support.timed("range $d 2.3") { fillInventoryId(args) }
if ("2.4" in steps) filledLotQty += support.timed("range $d 2.4") { fillLotQty(args) }
@@ -290,6 +321,11 @@ open class StockLedgerFixBackfillService(
stillMissUom = still.missUom,
stillMissInventoryId = still.missInventoryId,
stillMissLotQty = still.missLotQty,
deletedUnlinkedLot = deletedUnlinkedLot,
existingLotMatch = existingLotMatch,
existingLotMismatch = existingLotMismatch,
deletedBlankOutLine = deletedBlankOutLine,
alignedOutLine = alignedOutLine,
)
} catch (e: Exception) {
log.warn(
@@ -302,8 +338,22 @@ open class StockLedgerFixBackfillService(
}
}

/** 2.1 fill, then soft-delete rows whose in/out line still cannot point at a lot line. */
private fun fillLotLineId(args: Map<String, Any>): LotLineStep {
val filled = fillLotLineIdOnly(args)
val where = "sl.date >= :day AND sl.date < :dayNext"
val deleted = softDeleteUnlinkedLot(where, args)
val outLine = alignExistingOutLine(where, args)
val check = countExistingLotLineMatch(where, args)
return LotLineStep(
filled, deleted, check.first, check.second,
deletedBlankOutLine = outLine.first,
alignedOutLine = outLine.second,
)
}

/** 2.1 SOL / SIL lot line; one-line fallback when SIL has lot header only. */
private fun fillLotLineId(args: Map<String, Any>): Int {
private fun fillLotLineIdOnly(args: Map<String, Any>): Int {
val fromSolSil = support.timed("2.1a solSil") {
support.jdbcDao.executeUpdate(
"""
@@ -358,6 +408,136 @@ open class StockLedgerFixBackfillService(
return fromSolSil + fromOneLine
}

/**
* Rows that already have a lot line stay. Soft-delete only when a related
* stock in/out line exists and 2.1a/2.1b still could not set inventoryLotLineId.
*/
private fun softDeleteUnlinkedLot(
dateWhere: String,
args: Map<String, Any>,
extraAnd: String = "1 = 1",
): Int {
return support.jdbcDao.executeUpdate(
"""
UPDATE stock_ledger sl
LEFT JOIN stock_in_line sil
ON sil.id = sl.stockInLineId AND IFNULL(sil.deleted, 0) = 0
SET
sl.deleted = 1,
sl.modified = NOW(),
sl.modifiedBy = 'stock-ledger-fix'
WHERE sl.deleted = 0
AND ($dateWhere)
AND sl.inventoryLotLineId IS NULL
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
AND ($extraAnd)
""".trimIndent(),
args,
)
}

/** Ledger lot line vs live SIL/SOL lot line. Does not update those rows. */
private fun countExistingLotLineMatch(dateWhere: String, args: Map<String, Any>): Pair<Int, Int> {
val row = support.jdbcDao.queryForList(
"""
SELECT
SUM(
sl.inventoryLotLineId IS NOT NULL
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
AND COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) = sl.inventoryLotLineId
) AS matched,
SUM(
sl.inventoryLotLineId IS NOT NULL
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
AND (
COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) IS NULL
OR COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) <> sl.inventoryLotLineId
)
) AS mismatched
FROM stock_ledger sl
LEFT JOIN stock_out_line sol
ON sol.id = sl.stockOutLineId AND IFNULL(sol.deleted, 0) = 0
LEFT JOIN stock_in_line sil
ON sil.id = sl.stockInLineId AND IFNULL(sil.deleted, 0) = 0
WHERE sl.deleted = 0
AND ($dateWhere)
""".trimIndent(),
args,
).firstOrNull()
return support.toInt(row?.get("matched")) to support.toInt(row?.get("mismatched"))
}

private data class LotLineStep(
val filled: Int,
val deletedUnlinked: Int,
val existingMatch: Int,
val existingMismatch: Int,
val deletedBlankOutLine: Int = 0,
val alignedOutLine: Int = 0,
)

/**
* Ledger already has a lot line.
* Stock-out line lot is null: clear the ledger lot and soft-delete the ledger.
* Stock-out line lot differs: set the line to the ledger lot. Skip a line whose live ledgers disagree.
*/
private fun alignExistingOutLine(
dateWhere: String,
args: Map<String, Any>,
extraAnd: String = "1 = 1",
): Pair<Int, Int> {
val aligned = support.jdbcDao.executeUpdate(
"""
UPDATE stock_out_line sol
INNER JOIN (
SELECT sl.stockOutLineId AS lineId, MIN(sl.inventoryLotLineId) AS lotLineId
FROM stock_ledger sl
WHERE sl.deleted = 0
AND ($dateWhere)
AND sl.stockOutLineId IS NOT NULL
AND sl.inventoryLotLineId IS NOT NULL
AND ($extraAnd)
AND NOT EXISTS (
SELECT 1
FROM stock_ledger sl2
WHERE sl2.deleted = 0
AND sl2.stockOutLineId = sl.stockOutLineId
AND sl2.inventoryLotLineId IS NOT NULL
AND sl2.inventoryLotLineId <> sl.inventoryLotLineId
)
GROUP BY sl.stockOutLineId
) src ON src.lineId = sol.id
SET
sol.inventoryLotLineId = src.lotLineId,
sol.modified = NOW(),
sol.modifiedBy = 'stock-ledger-fix'
WHERE IFNULL(sol.deleted, 0) = 0
AND sol.inventoryLotLineId IS NOT NULL
AND sol.inventoryLotLineId <> src.lotLineId
""".trimIndent(),
args,
)
val deletedBlank = support.jdbcDao.executeUpdate(
"""
UPDATE stock_ledger sl
INNER JOIN stock_out_line sol
ON sol.id = sl.stockOutLineId AND IFNULL(sol.deleted, 0) = 0
SET
sl.inventoryLotLineId = NULL,
sl.deleted = 1,
sl.modified = NOW(),
sl.modifiedBy = 'stock-ledger-fix'
WHERE sl.deleted = 0
AND ($dateWhere)
AND sl.inventoryLotLineId IS NOT NULL
AND sol.inventoryLotLineId IS NULL
AND ($extraAnd)
""".trimIndent(),
args,
)
return deletedBlank to aligned
}

/** 2.2 lot line stock UOM → uom_conversion id. Include deleted item_uom (historical lots still point there). */
private fun fillUomId(args: Map<String, Any>): Int {
return support.jdbcDao.executeUpdate(
@@ -758,7 +938,7 @@ open class StockLedgerFixBackfillService(
} ?: 0
}

private fun fillLotLineIdScope(args: Map<String, Any>, byInventory: Boolean): Int {
private fun fillLotLineIdScope(args: Map<String, Any>, byInventory: Boolean): LotLineStep {
if (byInventory) return fillLotLineIdScopeByInventory(args)
val lotFilter = """
AND (
@@ -818,12 +998,34 @@ open class StockLedgerFixBackfillService(
args,
)
}
return fromSolSil + fromOneLine
val filled = fromSolSil + fromOneLine
val deleted = softDeleteUnlinkedLot(
"sl.date < :today",
args,
"sil.inventoryLotId IN (SELECT inventoryLotId FROM inventory_lot_line WHERE id = :lotId)",
)
val outLine = alignExistingOutLine("sl.date < :today", args, "sl.inventoryLotLineId = :lotId")
val check = countExistingLotLineMatch(
"""
sl.date < :today
AND (
sl.inventoryLotLineId = :lotId
OR sil.inventoryLotId IN (SELECT inventoryLotId FROM inventory_lot_line WHERE id = :lotId)
)
""".trimIndent(),
args,
)
return LotLineStep(
filled, deleted, check.first, check.second,
deletedBlankOutLine = outLine.first,
alignedOutLine = outLine.second,
)
}

/** Collect candidate ledger ids first — do not UPDATE with date < today OR …. */
private fun fillLotLineIdScopeByInventory(args: Map<String, Any>): Int {
return support.withTempTx {
private fun fillLotLineIdScopeByInventory(args: Map<String, Any>): LotLineStep {
var step = LotLineStep(0, 0, 0, 0)
support.withTempTx {
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sl_fix_lot_ids")
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sl_fix_sil_lots")
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sl_fix_one_line")
@@ -927,13 +1129,34 @@ open class StockLedgerFixBackfillService(
""".trimIndent(),
)
}
fromSolSil + fromOneLine
val deleted = support.jdbcDao.executeUpdate(
"""
UPDATE stock_ledger sl
INNER JOIN tmp_sl_fix_lot_ids t ON t.id = sl.id
SET
sl.deleted = 1,
sl.modified = NOW(),
sl.modifiedBy = 'stock-ledger-fix'
WHERE sl.deleted = 0
AND sl.inventoryLotLineId IS NULL
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
""".trimIndent(),
)
val outLine = alignExistingOutLine("sl.date < :today", args, "sl.inventoryId = :inventoryId")
val check = countExistingLotLineMatch("sl.inventoryId = :inventoryId", args)
step = LotLineStep(
fromSolSil + fromOneLine, deleted, check.first, check.second,
deletedBlankOutLine = outLine.first,
alignedOutLine = outLine.second,
)
0
} finally {
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sl_fix_one_line")
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sl_fix_sil_lots")
support.jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sl_fix_lot_ids")
}
}
return step
}

private fun fillUomIdScope(args: Map<String, Any>, byInventory: Boolean): Int {


+ 189
- 7
src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixExportService.kt Целия файл

@@ -21,8 +21,9 @@ open class StockLedgerFixExportService(
/**
* Export SQL patch for target DB (same dump lineage / freeze).
* [partsRaw] omit / empty / all = ledger + 2.6 (legacy). Else subset:
* 1.0 inventory, 2.3 inventoryId, ledger (lot/uom/lotQty/balance), 2.6 day, 2.7 SIL/SOL + ADJ INSERT.
* Apply order in file: 1.0 → ledger(+2.3) → 2.7 docs → 2.7 ADJ → 2.6.
* 1.0 inventory, 2.1 soft-deleted ledger + stock-out lot, 2.3 inventoryId,
* ledger (lot/uom/lotQty/balance), 2.6 day, 2.7 SIL/SOL + adjustment record + ADJ INSERT.
* Apply order in file: 1.0 → 2.1 → ledger(+2.3) → 2.7 docs → 2.7 ADJ → 2.6.
*/
open fun writeExportSql(
from: LocalDate,
@@ -44,12 +45,13 @@ open class StockLedgerFixExportService(
}
val parts = parseExportParts(partsRaw)
val want10 = "1.0" in parts
val want21 = "2.1" in parts
val want23 = "2.3" in parts
val wantLedger = "ledger" in parts
val want26 = "2.6" in parts
val want27 = "2.7" in parts
if (!want10 && !want23 && !wantLedger && !want26 && !want27) {
throw IllegalArgumentException("parts must include at least one of: 1.0, 2.3, ledger, 2.6, 2.7")
if (!want10 && !want21 && !want23 && !wantLedger && !want26 && !want27) {
throw IllegalArgumentException("parts must include at least one of: 1.0, 2.1, 2.3, ledger, 2.6, 2.7")
}
val args = mapOf("from" to from, "toExclusive" to toExclusive)
val ledgerCnt = if (wantLedger || want23 || want27) {
@@ -110,7 +112,7 @@ open class StockLedgerFixExportService(
writer.write("-- stock-ledger-fix patch\n")
writer.write("-- from $from inclusive to ${toExclusive.minusDays(1)} inclusive (date < $toExclusive)\n")
writer.write("-- parts=${parts.joinToString(",")}\n")
writer.write("-- apply order: 1.0 inventory → ledger(+2.3) UPDATE → 2.7 SIL/SOL then ADJ INSERT → 2.6 stock_lot_day\n")
writer.write("-- apply order: 1.0 inventory → 2.1 delete ledger + stock_out lot → ledger(+2.3) UPDATE → 2.7 SIL/SOL/adjustment record then ADJ INSERT → 2.6 stock_lot_day\n")
writer.write("-- INNER JOIN id: target rows without matching id are skipped on UPDATE\n")
if (want23 && !want10) {
writer.write("-- WARN: 2.3 without 1.0 — new inventory ids may be missing on target; prefer include 1.0\n")
@@ -124,6 +126,9 @@ open class StockLedgerFixExportService(
if (want10) {
writeExportInventory10(writer, jdbc)
}
if (want21) {
writeExport21(writer, jdbc, args)
}
if (wantLedger || want23) {
writeExportLedgerPatch(writer, jdbc, args, wantLedger, want23)
}
@@ -209,6 +214,146 @@ open class StockLedgerFixExportService(
} else {
writer.write("-- (no inventory rows)\n\n")
}
writeTempUpdate(
writer,
jdbc,
emptyMap(),
title = "-- === 1.0 soft-delete orphan inventory (stockUomId still null) ===\n",
selectSql = """
SELECT id, modified, modifiedBy
FROM inventory
WHERE deleted = 1
AND modifiedBy = 'stock-ledger-fix'
ORDER BY id
""".trimIndent(),
tempName = "tmp_sl_fix_inv_del",
columnDefs = "modified DATETIME NULL, modifiedBy VARCHAR(255) NULL",
columns = listOf("id", "modified", "modifiedBy"),
updateSql = """
UPDATE inventory i
INNER JOIN tmp_sl_fix_inv_del t ON t.id = i.id
SET
i.deleted = 1,
i.modified = t.modified,
i.modifiedBy = t.modifiedBy;
""".trimIndent(),
emptyComment = "-- (no stock-ledger-fix deleted inventory rows)",
)
}

/**
* 2.1 results the live-row ledger patch cannot carry:
* soft-deleted ledger rows (including lot cleared when the out line has no lot),
* and stock_out_line.inventoryLotLineId set to the ledger lot.
*/
private fun writeExport21(
writer: BufferedWriter,
jdbc: NamedParameterJdbcTemplate,
args: Map<String, Any>,
) {
writeTempUpdate(
writer,
jdbc,
args,
title = "-- === 2.1 soft-delete stock_ledger (id missing on target is skipped) ===\n",
selectSql = """
SELECT id, inventoryLotLineId, modified, modifiedBy
FROM stock_ledger
WHERE deleted = 1
AND modifiedBy = 'stock-ledger-fix'
AND date >= :from AND date < :toExclusive
ORDER BY id
""".trimIndent(),
tempName = "tmp_sl_fix_ledger_del",
columnDefs = "inventoryLotLineId BIGINT NULL, modified DATETIME NULL, modifiedBy VARCHAR(255) NULL",
columns = listOf("id", "inventoryLotLineId", "modified", "modifiedBy"),
updateSql = """
UPDATE stock_ledger sl
INNER JOIN tmp_sl_fix_ledger_del t ON t.id = sl.id
SET
sl.deleted = 1,
sl.inventoryLotLineId = t.inventoryLotLineId,
sl.modified = t.modified,
sl.modifiedBy = t.modifiedBy;
""".trimIndent(),
emptyComment = "-- (no stock-ledger-fix deleted ledger rows in range)",
)
writeTempUpdate(
writer,
jdbc,
args,
title = "-- === 2.1 stock_out_line lot = ledger lot (existing lines only) ===\n",
selectSql = """
SELECT sol.id, sol.inventoryLotLineId, sol.modified, sol.modifiedBy
FROM stock_out_line sol
WHERE sol.modifiedBy = 'stock-ledger-fix'
AND IFNULL(sol.createdBy, '') <> 'stock-ledger-fix'
AND EXISTS (
SELECT 1
FROM stock_ledger sl
WHERE sl.stockOutLineId = sol.id
AND sl.date >= :from AND sl.date < :toExclusive
)
ORDER BY sol.id
""".trimIndent(),
tempName = "tmp_sl_fix_sol_lot",
columnDefs = "inventoryLotLineId BIGINT NULL, modified DATETIME NULL, modifiedBy VARCHAR(255) NULL",
columns = listOf("id", "inventoryLotLineId", "modified", "modifiedBy"),
updateSql = """
UPDATE stock_out_line sol
INNER JOIN tmp_sl_fix_sol_lot t ON t.id = sol.id
SET
sol.inventoryLotLineId = t.inventoryLotLineId,
sol.modified = t.modified,
sol.modifiedBy = t.modifiedBy;
""".trimIndent(),
emptyComment = "-- (no stock-ledger-fix stock_out_line lot updates in range)",
)
}

private fun writeTempUpdate(
writer: BufferedWriter,
jdbc: NamedParameterJdbcTemplate,
args: Map<String, Any>,
title: String,
selectSql: String,
tempName: String,
columnDefs: String,
columns: List<String>,
updateSql: String,
emptyComment: String,
) {
writer.write(title)
writer.write("DROP TEMPORARY TABLE IF EXISTS $tempName;\n")
writer.write("CREATE TEMPORARY TABLE $tempName (\n")
writer.write(" id INT NOT NULL PRIMARY KEY,\n $columnDefs\n);\n\n")
var batch = 0
var any = false
jdbc.query(
selectSql,
args,
RowCallbackHandler { rs ->
if (batch == 0) {
writer.write("INSERT INTO $tempName (${columns.joinToString(", ")}) VALUES\n")
} else {
writer.write(",\n")
}
writer.write("(" + columns.joinToString(", ") { rsSql(rs, it) } + ")")
any = true
batch++
if (batch >= 500) {
writer.write(";\n\n")
batch = 0
}
},
)
if (batch > 0) writer.write(";\n\n")
if (any) {
writer.write("$updateSql\n\n")
} else {
writer.write("$emptyComment\n\n")
}
writer.write("DROP TEMPORARY TABLE IF EXISTS $tempName;\n\n")
}

private fun writeExportLedgerPatch(
@@ -397,6 +542,42 @@ open class StockLedgerFixExportService(
"modifiedBy=VALUES(modifiedBy), version=stock_out_line.version+1, deleted=0",
emptyComment = "-- (no ADJ stock_out_line in range)",
)
writer.write("-- === 2.7 stock_adjustment_record (after SIL/SOL; FK to those lines) ===\n")
writeExportRows(
writer,
jdbc,
"""
SELECT DISTINCT r.id, r.created, r.createdBy, r.version, r.modified, r.modifiedBy, r.deleted,
r.itemId, r.itemCode, r.itemName, r.lotNo, r.inQty, r.outQty, r.remarks,
r.stockInLineId, r.stockOutLineId
FROM stock_adjustment_record r
INNER JOIN stock_ledger sl
ON (
(r.stockInLineId IS NOT NULL AND sl.stockInLineId = r.stockInLineId)
OR (r.stockOutLineId IS NOT NULL AND sl.stockOutLineId = r.stockOutLineId)
)
WHERE IFNULL(r.deleted, 0) = 0
AND r.createdBy = 'stock-ledger-fix'
AND sl.deleted = 0
AND sl.date >= :from AND sl.date < :toExclusive
AND sl.type = 'ADJ'
AND sl.createdBy = 'stock-ledger-fix'
ORDER BY r.id
""".trimIndent(),
args,
table = "stock_adjustment_record",
columns = listOf(
"id", "created", "createdBy", "version", "modified", "modifiedBy", "deleted",
"itemId", "itemCode", "itemName", "lotNo", "inQty", "outQty", "remarks",
"stockInLineId", "stockOutLineId",
),
onDup = "itemCode=VALUES(itemCode), itemName=VALUES(itemName), lotNo=VALUES(lotNo), " +
"inQty=VALUES(inQty), outQty=VALUES(outQty), remarks=VALUES(remarks), " +
"stockInLineId=VALUES(stockInLineId), stockOutLineId=VALUES(stockOutLineId), " +
"modified=VALUES(modified), modifiedBy=VALUES(modifiedBy), " +
"version=stock_adjustment_record.version+1, deleted=0",
emptyComment = "-- (no ADJ stock_adjustment_record in range)",
)
}

private fun writeExportAdjInsert(
@@ -557,11 +738,12 @@ open class StockLedgerFixExportService(
val key = when (t) {
"1.0", "10", "inventory" -> "1.0"
"2.3", "23", "inventoryid" -> "2.3"
"ledger", "2.1", "2.2", "2.4", "2.5" -> "ledger"
"2.1", "21" -> "2.1"
"ledger", "2.2", "2.4", "2.5" -> "ledger"
"2.6", "26", "day", "stock_lot_day" -> "2.6"
"2.7", "27", "adj" -> "2.7"
else -> throw IllegalArgumentException(
"unknown export part '$token' (use 1.0, 2.3, ledger, 2.6, 2.7, or all)",
"unknown export part '$token' (use 1.0, 2.1, 2.3, ledger, 2.6, 2.7, or all)",
)
}
out.add(key)


+ 80
- 6
src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixQueryService.kt Целия файл

@@ -180,10 +180,36 @@ open class StockLedgerFixQueryService(
sl.inventoryLotLineId IS NULL
AND COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) IS NULL
AND (sil.inventoryLotId IS NULL OR IFNULL(lc.lineCnt, 0) = 0)
) AS cannotNoSource
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
) AS cannotNoSource,
SUM(
sl.inventoryLotLineId IS NOT NULL
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
AND COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) = sl.inventoryLotLineId
) AS existingLotMatch,
SUM(
sl.inventoryLotLineId IS NOT NULL
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
AND (
COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) IS NULL
OR COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) <> sl.inventoryLotLineId
)
) AS existingLotMismatch,
SUM(
sl.inventoryLotLineId IS NOT NULL
AND sol.id IS NOT NULL
AND sol.inventoryLotLineId IS NULL
) AS outLineBlank,
SUM(
sl.inventoryLotLineId IS NOT NULL
AND sol.inventoryLotLineId IS NOT NULL
AND sol.inventoryLotLineId <> sl.inventoryLotLineId
) AS outLineDiffers
FROM stock_ledger sl
LEFT JOIN stock_out_line sol ON sol.id = sl.stockOutLineId
LEFT JOIN stock_in_line sil ON sil.id = sl.stockInLineId
LEFT JOIN stock_out_line sol
ON sol.id = sl.stockOutLineId AND IFNULL(sol.deleted, 0) = 0
LEFT JOIN stock_in_line sil
ON sil.id = sl.stockInLineId AND IFNULL(sil.deleted, 0) = 0
LEFT JOIN inventory_lot_line ill ON ill.id = sl.inventoryLotLineId
LEFT JOIN item_uom iu ON iu.id = ill.stockItemUomId
LEFT JOIN (
@@ -315,6 +341,10 @@ open class StockLedgerFixQueryService(
cannotNoSource = support.toInt(ledger["cannotNoSource"]),
earlyTkeOver = 0,
otherOver = 0,
existingLotMatch = support.toInt(ledger["existingLotMatch"]),
existingLotMismatch = support.toInt(ledger["existingLotMismatch"]),
outLineBlank = support.toInt(ledger["outLineBlank"]),
outLineDiffers = support.toInt(ledger["outLineDiffers"]),
extra = listOf(
part("dayTable", "stock_lot_day", dayTableMiss, dayIncorrect, dayTableTotal),
),
@@ -541,7 +571,31 @@ open class StockLedgerFixQueryService(
sl.inventoryLotLineId IS NULL
AND COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) IS NULL
AND (sil.inventoryLotId IS NULL OR IFNULL(lc.lineCnt, 0) = 0)
) AS cannotNoSource
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
) AS cannotNoSource,
SUM(
sl.inventoryLotLineId IS NOT NULL
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
AND COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) = sl.inventoryLotLineId
) AS existingLotMatch,
SUM(
sl.inventoryLotLineId IS NOT NULL
AND (sl.stockInLineId IS NOT NULL OR sl.stockOutLineId IS NOT NULL)
AND (
COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) IS NULL
OR COALESCE(sol.inventoryLotLineId, sil.inventoryLotLineId) <> sl.inventoryLotLineId
)
) AS existingLotMismatch,
SUM(
sl.inventoryLotLineId IS NOT NULL
AND sol.id IS NOT NULL
AND sol.inventoryLotLineId IS NULL
) AS outLineBlank,
SUM(
sl.inventoryLotLineId IS NOT NULL
AND sol.inventoryLotLineId IS NOT NULL
AND sol.inventoryLotLineId <> sl.inventoryLotLineId
) AS outLineDiffers
FROM stock_ledger sl
LEFT JOIN stock_out_line sol
ON sol.id = sl.stockOutLineId AND IFNULL(sol.deleted, 0) = 0
@@ -599,6 +653,10 @@ open class StockLedgerFixQueryService(
cannotNoSource = support.toInt(ledger["cannotNoSource"]),
earlyTkeOver = earlyTke,
otherOver = (overIssue - earlyTke).coerceAtLeast(0),
existingLotMatch = support.toInt(ledger["existingLotMatch"]),
existingLotMismatch = support.toInt(ledger["existingLotMismatch"]),
outLineBlank = support.toInt(ledger["outLineBlank"]),
outLineDiffers = support.toInt(ledger["outLineDiffers"]),
),
)
}
@@ -621,6 +679,10 @@ open class StockLedgerFixQueryService(
cannotNoSource: Int,
earlyTkeOver: Int,
otherOver: Int,
existingLotMatch: Int = 0,
existingLotMismatch: Int = 0,
outLineBlank: Int = 0,
outLineDiffers: Int = 0,
extra: List<StockLedgerFixCheckPart> = emptyList(),
): List<StockLedgerFixCheckPart> {
fun field(key: String, label: String, miss: Int, incorrect: Int) =
@@ -643,12 +705,24 @@ open class StockLedgerFixQueryService(
add(field("lotQty", "lotQtyBefore/After(lot 已有)", lotQtyMissWithLot, lotQtyIncorrect))
add(field("overIssue", "over-issue (lotQty < 0)", 0, overIssue))
add(field("balance", "balance", balanceMiss, 0))
add(
StockLedgerFixCheckPart(
key = "existingLotLine",
label = "已有 lot line:入出倉列是否同一條",
ok = existingLotMatch,
miss = 0,
incorrect = existingLotMismatch,
group = "field",
),
)
addAll(extra)
add(canFix("outLineBlank", "2.1:出倉列批號是空,流水帳改空並軟刪", outLineBlank))
add(canFix("outLineDiffers", "2.1:出倉列批號不同,改出倉列跟流水帳", outLineDiffers))
add(canFix("fixable21a", "可修 2.1a:SIL/SOL 已有 lot line", fixable21a))
add(canFix("fixable21b", "可修 2.1b:inventoryLotId 僅一條 line", fixable21b))
add(canFix("fixableLotQty", "可修:lot 已有但缺 lotQty", lotQtyMissWithLot))
add(cannotFix("cannotNoSource", "不能修:沒有 SIL/SOL lot 來源", cannotNoSource))
add(cannotFix("cannotMultiLine", "不能修:同一 lot 有多條 warehouse line", cannotMultiLine))
add(cannotFix("cannotNoSource", "2.1 軟刪:入出倉列對不到批號列", cannotNoSource))
add(cannotFix("cannotMultiLine", "2.1 軟刪:同一 lot 有多條 warehouse line", cannotMultiLine))
add(cannotFix("earlyTke", "不能修:舊 TKE 開新批(sibling 盤盈)", earlyTkeOver))
add(cannotFix("realOver", "不能修:over-issue 非舊 TKE(真超發/帳不一致)", otherOver))
}


+ 12
- 0
src/main/java/com/ffii/fpsms/modules/master/service/ledgerfix/StockLedgerFixSupport.kt Целия файл

@@ -64,6 +64,18 @@ open class StockLedgerFixSupport(
}
}

fun <T> timedValue(name: String, block: () -> T): T {
val t0 = System.nanoTime()
return try {
val value = block()
log.info("stock-ledger-fix step {} {}ms", name, (System.nanoTime() - t0) / 1_000_000)
value
} catch (e: Exception) {
log.warn("stock-ledger-fix step {} FAILED {}ms: {}", name, (System.nanoTime() - t0) / 1_000_000, e.message)
throw e
}
}

fun parseDay(raw: String): LocalDate {
val trimmed = raw.trim()
if (trimmed.isEmpty()) throw IllegalArgumentException("date is required (yyyy-MM-dd)")


+ 12
- 0
src/main/java/com/ffii/fpsms/modules/master/web/models/StockLedgerFixModels.kt Целия файл

@@ -39,6 +39,16 @@ data class StockLedgerFixRunResponse(
val stillMissUom: Int,
val stillMissInventoryId: Int,
val stillMissLotQty: Int,
/** 2.1: related SIL/SOL cannot point at a lot line, so the ledger row is soft-deleted. */
val deletedUnlinkedLot: Int = 0,
/** Ledger already had inventoryLotLineId and the live SIL/SOL lot line is the same. */
val existingLotMatch: Int = 0,
/** Ledger already had inventoryLotLineId; the live SIL/SOL lot line is missing or different. Not deleted. */
val existingLotMismatch: Int = 0,
/** Ledger had a lot line but the live stock-out line lot was null: lot cleared and ledger soft-deleted. */
val deletedBlankOutLine: Int = 0,
/** Live stock-out line lot differed; the line was set to the ledger lot. Ledger lot was not changed. */
val alignedOutLine: Int = 0,
)

data class StockLedgerFixCheckPart(
@@ -128,6 +138,8 @@ data class StockLedgerFixAdjResponse(
val filledLotQty: Int,
val filledBalance: Int,
val dayRowsWritten: Int,
/** Rows inserted into stock_adjustment_record for stock-ledger-fix ADJ lines, including ones already on the ledger. */
val adjustmentRecords: Int = 0,
)

data class StockLedgerFixScopeDetail(


+ 4
- 1
src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt Целия файл

@@ -320,7 +320,10 @@ open class StockLedgerReportService(
"""
SELECT
sol.id AS lineId,
COALESCE(TRIM(do.code), TRIM(jo_po.code)) AS orderRefNo,
CASE
WHEN po_out.type = 'consumable' THEN TRIM(po_out.code)
ELSE COALESCE(TRIM(do.code), TRIM(jo_po.code))
END AS orderRefNo,
CASE
WHEN so.stockTakeId IS NOT NULL OR so.type = 'stockTake' THEN 1
ELSE 0


Зареждане…
Отказ
Запис