| @@ -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, | |||
| @@ -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 { | |||
| @@ -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) | |||
| @@ -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)) | |||
| } | |||
| @@ -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)") | |||
| @@ -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( | |||
| @@ -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 | |||