|
|
@@ -20,24 +20,22 @@ open class StockLedgerFixAdjService( |
|
|
private val log = LoggerFactory.getLogger(StockLedgerFixAdjService::class.java) |
|
|
private val log = LoggerFactory.getLogger(StockLedgerFixAdjService::class.java) |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 2.7b. 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. |
|
|
|
|
|
* 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). |
|
|
|
|
|
|
|
|
* 2.7b. One ADJ per lot line whose remain (in − out) differs from its ledger net. |
|
|
|
|
|
* Quantity is that lot's own gap. Out greater than in is left to 2.7a. |
|
|
|
|
|
* Lot lines and inventory are not updated. After the ADJ, the bucket balance is |
|
|
|
|
|
* rebuilt from live in/out so a stale balance is not copied forward. |
|
|
|
|
|
* A lot that already matches is skipped. A second run inserts no new ADJ. |
|
|
* Ledger net uses `date < adjDate+1`. |
|
|
* Ledger net uses `date < adjDate+1`. |
|
|
*/ |
|
|
*/ |
|
|
open fun adjPreview(rowLimit: Int = 20, adjDateRaw: String? = null): StockLedgerFixAdjPreview { |
|
|
open fun adjPreview(rowLimit: Int = 20, adjDateRaw: String? = null): StockLedgerFixAdjPreview { |
|
|
val (adjDate, asOfExclusive) = resolveAdjWindow(adjDateRaw) |
|
|
val (adjDate, asOfExclusive) = resolveAdjWindow(adjDateRaw) |
|
|
val t0 = System.nanoTime() |
|
|
val t0 = System.nanoTime() |
|
|
val plans = planBucketAdjs(asOfExclusive, resolveHost = false) |
|
|
|
|
|
|
|
|
val plans = planLotAdjs(asOfExclusive) |
|
|
val sumIn = plans.fold(BigDecimal.ZERO) { a, g -> a + g.missIn } |
|
|
val sumIn = plans.fold(BigDecimal.ZERO) { a, g -> a + g.missIn } |
|
|
val sumOut = plans.fold(BigDecimal.ZERO) { a, g -> a + g.missOut } |
|
|
val sumOut = plans.fold(BigDecimal.ZERO) { a, g -> a + g.missOut } |
|
|
val limit = rowLimit.coerceIn(0, 50) |
|
|
val limit = rowLimit.coerceIn(0, 50) |
|
|
log.info( |
|
|
log.info( |
|
|
"stock-ledger-fix adj preview buckets={} in={} out={} {}ms", |
|
|
|
|
|
|
|
|
"stock-ledger-fix 2.7b preview lots={} in={} out={} {}ms", |
|
|
plans.size, |
|
|
plans.size, |
|
|
plans.count { it.missIn > BigDecimal.ZERO }, |
|
|
plans.count { it.missIn > BigDecimal.ZERO }, |
|
|
plans.count { it.missOut > BigDecimal.ZERO }, |
|
|
plans.count { it.missOut > BigDecimal.ZERO }, |
|
|
@@ -65,7 +63,7 @@ open class StockLedgerFixAdjService( |
|
|
val (adjDate, asOfExclusive) = resolveAdjWindow(adjDateRaw) |
|
|
val (adjDate, asOfExclusive) = resolveAdjWindow(adjDateRaw) |
|
|
val t0 = System.nanoTime() |
|
|
val t0 = System.nanoTime() |
|
|
try { |
|
|
try { |
|
|
val plans = planBucketAdjs(asOfExclusive, resolveHost = true) |
|
|
|
|
|
|
|
|
val plans = planLotAdjs(asOfExclusive) |
|
|
var insertedIn = 0 |
|
|
var insertedIn = 0 |
|
|
var insertedOut = 0 |
|
|
var insertedOut = 0 |
|
|
val overIssuePatched = 0 |
|
|
val overIssuePatched = 0 |
|
|
@@ -118,7 +116,12 @@ open class StockLedgerFixAdjService( |
|
|
mapOf("adjDate" to adjDate, "asOf" to asOfExclusive), |
|
|
mapOf("adjDate" to adjDate, "asOf" to asOfExclusive), |
|
|
).forEach { addLot(support.toLong(it["lotId"]), support.toLongOrNull(it["invId"])) } |
|
|
).forEach { addLot(support.toLong(it["lotId"]), support.toLongOrNull(it["invId"])) } |
|
|
val filledLotQty = support.timed("adj 2.4") { backfill.fillLotQtyForLotIds(lotIds.toList(), asOfExclusive) } |
|
|
val filledLotQty = support.timed("adj 2.4") { backfill.fillLotQtyForLotIds(lotIds.toList(), asOfExclusive) } |
|
|
val filledBalance = support.timed("adj 2.5") { backfill.fillBalanceOnNewAdjRows(adjDate, asOfExclusive) } |
|
|
|
|
|
|
|
|
val filledBalance = support.timed("adj 2.5") { |
|
|
|
|
|
backfill.fillBalanceForInventoryIds( |
|
|
|
|
|
plans.mapNotNull { it.inventoryId }.distinct(), |
|
|
|
|
|
asOfExclusive, |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
val range = if (lotIds.isEmpty()) null else backfill.dateRangeForFilter( |
|
|
val range = if (lotIds.isEmpty()) null else backfill.dateRangeForFilter( |
|
|
"inventoryLotLineId IN (:lotIds)", |
|
|
"inventoryLotLineId IN (:lotIds)", |
|
|
mapOf("today" to asOfExclusive, "lotIds" to lotIds.toList()), |
|
|
mapOf("today" to asOfExclusive, "lotIds" to lotIds.toList()), |
|
|
@@ -127,7 +130,7 @@ open class StockLedgerFixAdjService( |
|
|
support.timed("adj 2.6") { backfill.upsertStockLotDayRange(lotIds.toList(), range.first, range.second) } |
|
|
support.timed("adj 2.6") { backfill.upsertStockLotDayRange(lotIds.toList(), range.first, range.second) } |
|
|
} else 0 |
|
|
} else 0 |
|
|
log.info( |
|
|
log.info( |
|
|
"stock-ledger-fix adj {} buckets={} in={} out={} adjRecord={} lotQty={} bal={} day={} total={}ms", |
|
|
|
|
|
|
|
|
"stock-ledger-fix 2.7b {} lots={} in={} out={} adjRecord={} lotQty={} bal={} day={} total={}ms", |
|
|
adjDate, plans.size, insertedIn, insertedOut, adjustmentRecords, |
|
|
adjDate, plans.size, insertedIn, insertedOut, adjustmentRecords, |
|
|
filledLotQty, filledBalance, dayRows, |
|
|
filledLotQty, filledBalance, dayRows, |
|
|
(System.nanoTime() - t0) / 1_000_000, |
|
|
(System.nanoTime() - t0) / 1_000_000, |
|
|
@@ -295,202 +298,80 @@ open class StockLedgerFixAdjService( |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 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, |
|
|
|
|
|
) |
|
|
|
|
|
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(), |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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. |
|
|
|
|
|
|
|
|
* One ADJ per lot whose remain differs from its own ledger net. |
|
|
|
|
|
* Gap = (line in − line out) − (ledger in − ledger out). Positive gap is an inbound ADJ. |
|
|
|
|
|
* Lots with out greater than in are skipped; 2.7a owns those. |
|
|
*/ |
|
|
*/ |
|
|
private fun loadMismatchedByLastBalance(today: LocalDate): List<BucketDelta> { |
|
|
|
|
|
return support.jdbcDao.queryForList( |
|
|
|
|
|
|
|
|
private fun planLotAdjs(today: LocalDate): List<AdjGap> { |
|
|
|
|
|
val lines = support.jdbcDao.queryForList( |
|
|
""" |
|
|
""" |
|
|
SELECT i.id AS inventoryId, |
|
|
|
|
|
i.itemId AS itemId, |
|
|
|
|
|
|
|
|
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, |
|
|
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 |
|
|
|
|
|
|
|
|
iu.uomId AS uomId, |
|
|
|
|
|
i.id AS inventoryId |
|
|
|
|
|
FROM inventory_lot_line ill |
|
|
|
|
|
INNER JOIN inventory_lot il |
|
|
|
|
|
ON il.id = ill.inventoryLotId AND IFNULL(il.deleted, 0) = 0 |
|
|
|
|
|
INNER JOIN items it ON it.id = il.itemId |
|
|
|
|
|
INNER JOIN item_uom iu ON iu.id = ill.stockItemUomId |
|
|
|
|
|
LEFT JOIN inventory i |
|
|
|
|
|
ON i.itemId = il.itemId |
|
|
|
|
|
AND i.stockUomId = iu.uomId |
|
|
|
|
|
AND IFNULL(i.deleted, 0) = 0 |
|
|
|
|
|
WHERE IFNULL(ill.deleted, 0) = 0 |
|
|
|
|
|
AND COALESCE(ill.outQty, 0) <= COALESCE(ill.inQty, 0) |
|
|
""".trimIndent(), |
|
|
""".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(), |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** 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 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(), |
|
|
|
|
|
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 -> |
|
|
|
|
|
|
|
|
) |
|
|
|
|
|
if (lines.isEmpty()) return emptyList() |
|
|
|
|
|
val byId = HashMap<Long, Map<String, Any?>>(lines.size) |
|
|
|
|
|
lines.forEach { byId[support.toLong(it["lotLineId"])] = it } |
|
|
|
|
|
val gaps = ArrayList<AdjGap>() |
|
|
|
|
|
byId.keys.chunked(400).forEach { chunk -> |
|
|
|
|
|
val netByLot = HashMap<Long, Map<String, Any?>>(chunk.size) |
|
|
support.jdbcDao.queryForList( |
|
|
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) |
|
|
|
|
|
|
|
|
SELECT 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 |
|
|
|
|
|
FROM stock_ledger sl |
|
|
|
|
|
WHERE sl.deleted = 0 |
|
|
|
|
|
AND sl.date < :today |
|
|
|
|
|
AND sl.inventoryLotLineId IN (:ids) |
|
|
|
|
|
GROUP BY sl.inventoryLotLineId |
|
|
""".trimIndent(), |
|
|
""".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() |
|
|
|
|
|
|
|
|
mapOf("today" to today, "ids" to chunk), |
|
|
|
|
|
).forEach { netByLot[support.toLong(it["lotLineId"])] = it } |
|
|
|
|
|
chunk.forEach lotLoop@{ lotId -> |
|
|
|
|
|
val line = byId.getValue(lotId) |
|
|
|
|
|
val lineIn = support.toDecimal(line["lineIn"]) |
|
|
|
|
|
val lineOut = support.toDecimal(line["lineOut"]) |
|
|
|
|
|
val net = netByLot[lotId] |
|
|
|
|
|
val ledgerIn = support.toDecimal(net?.get("ledgerIn")) |
|
|
|
|
|
val ledgerOut = support.toDecimal(net?.get("ledgerOut")) |
|
|
|
|
|
val gap = lineIn.subtract(lineOut).subtract(ledgerIn.subtract(ledgerOut)) |
|
|
|
|
|
if (gap.abs().compareTo(ADJ_TOLERANCE) <= 0) return@lotLoop |
|
|
|
|
|
val needIn = gap.signum() > 0 |
|
|
|
|
|
gaps.add( |
|
|
|
|
|
AdjGap( |
|
|
|
|
|
lotLineId = lotId, |
|
|
|
|
|
inventoryId = support.toLongOrNull(line["inventoryId"]), |
|
|
|
|
|
itemId = support.toLongOrNull(line["itemId"]), |
|
|
|
|
|
itemCode = line["itemCode"]?.toString(), |
|
|
|
|
|
uomId = support.toLongOrNull(line["uomId"]), |
|
|
|
|
|
lineIn = lineIn, |
|
|
|
|
|
lineOut = lineOut, |
|
|
|
|
|
ledgerIn = ledgerIn, |
|
|
|
|
|
ledgerOut = ledgerOut, |
|
|
|
|
|
missIn = if (needIn) gap else BigDecimal.ZERO, |
|
|
|
|
|
missOut = if (needIn) BigDecimal.ZERO else gap.negate(), |
|
|
|
|
|
), |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
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) } |
|
|
|
|
|
|
|
|
return gaps |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -806,7 +687,7 @@ open class StockLedgerFixAdjService( |
|
|
|
|
|
|
|
|
companion object { |
|
|
companion object { |
|
|
private val ADJ_TOLERANCE = BigDecimal("0.01") |
|
|
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_DOC_REMARKS = "stock-ledger-fix 2.7b ADJ" |
|
|
private const val ADJ_LOT_LINE_REMARKS = "stock-ledger-fix 2.7a ADJ" |
|
|
private const val ADJ_LOT_LINE_REMARKS = "stock-ledger-fix 2.7a ADJ" |
|
|
private const val ADJ_HEADER_REMARKS = "stock-ledger-fix 2.7 ADJ" |
|
|
private const val ADJ_HEADER_REMARKS = "stock-ledger-fix 2.7 ADJ" |
|
|
} |
|
|
} |
|
|
|