From 5facc1c50c86f270d05f4e63b34836527f974d0f Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Fri, 11 Sep 2026 13:38:11 +0800 Subject: [PATCH] update --- .../service/DoWorkbenchMainService.kt | 9 +- .../modules/master/service/ItemUomService.kt | 28 +++ .../master/service/StockLedgerFixService.kt | 51 +++-- .../fpsms/modules/stock/entity/Inventory.kt | 5 + .../stock/entity/InventoryRepository.kt | 3 + .../stock/entity/projection/InventoryInfo.kt | 4 + .../service/StockOutLineWorkbenchService.kt | 9 +- .../01_add_inventory_stock_uom_id.sql | 44 ++++ .../02_drop_inventory_lot_line_triggers.sql | 8 + .../03_inventory_lot_line_trigger_insert.sql | 170 +++++++++++++++ .../04_inventory_lot_line_trigger_update.sql | 198 ++++++++++++++++++ 11 files changed, 501 insertions(+), 28 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/01_add_inventory_stock_uom_id.sql create mode 100644 src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/02_drop_inventory_lot_line_triggers.sql create mode 100644 src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/03_inventory_lot_line_trigger_insert.sql create mode 100644 src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/04_inventory_lot_line_trigger_update.sql diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt index e1563c5..7437645 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt @@ -2595,15 +2595,18 @@ return MessageResponse( private fun createWorkbenchPickLedger(sol: StockOutLine, deltaQty: BigDecimal) { if (deltaQty <= BigDecimal.ZERO) return val solItem = sol.item ?: return - val inventory = itemUomService.findInventoryForItemBaseUom(solItem.id!!) ?: return + val ill = sol.inventoryLotLine?.id?.let { inventoryLotLineRepository.findById(it).orElse(sol.inventoryLotLine) } + ?: sol.inventoryLotLine + val inventory = ( + ill?.let { itemUomService.resolveInventoryForLot(it) } + ?: itemUomService.findInventoryForItemBaseUom(solItem.id!!) + ) ?: return // Fast path (batch-submit style): avoid querying latest ledger. // At this point inventory.onHandQty has already been updated by DB triggers after lot outQty change. // So previousBalance can be reconstructed as (onHandAfter + deltaQty). val onHandAfter = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() val previousBalance = onHandAfter + deltaQty.toDouble() val newBalance = previousBalance - deltaQty.toDouble() - val ill = sol.inventoryLotLine?.id?.let { inventoryLotLineRepository.findById(it).orElse(sol.inventoryLotLine) } - ?: sol.inventoryLotLine val lotAfter = ill?.let { StockLedgerLotSnapshot.availableQty(it) } val ledger = StockLedger().apply { this.stockOutLine = sol diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt index 275b80c..03fd32c 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt @@ -13,6 +13,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import com.ffii.fpsms.modules.master.entity.ItemsRepository import com.ffii.fpsms.modules.stock.entity.Inventory +import com.ffii.fpsms.modules.stock.entity.InventoryLotLine import com.ffii.fpsms.modules.stock.entity.InventoryRepository @Service @@ -86,6 +87,33 @@ open class ItemUomService( return inventoryRepository.findFirstByItemIdAndDeletedIsFalseOrderByIdAsc(itemId) } + /** + * Qty bucket for a lot line (ChangeList No.34). Same key as lot-line triggers: + * `ill.stockItemUomId` → `item_uom.uomId` = `inventory.stockUomId`. + * Does not create inventory; trigger is the writer. + */ + open fun resolveInventoryForLot(lotLine: InventoryLotLine): Inventory? { + val itemId = lotLine.inventoryLot?.item?.id ?: return null + val stockUomId = resolveLotStockUomId(lotLine, itemId) + ?: return findInventoryForItemBaseUom(itemId) + return findInventoryForItemStockUom(itemId, stockUomId) + } + + open fun findInventoryForItemStockUom(itemId: Long, stockUomId: Long): Inventory? { + inventoryRepository.findFirstByItemIdAndStockUomIdAndDeletedIsFalseOrderByIdAsc(itemId, stockUomId) + ?.let { return it } + return inventoryRepository.findFirstByItemIdAndUomIdAndDeletedIsFalseOrderByIdAsc(itemId, stockUomId) + } + + private fun resolveLotStockUomId(lotLine: InventoryLotLine, itemId: Long): Long? { + lotLine.stockUom?.uom?.id?.let { return it } + val itemUomId = lotLine.stockUom?.id + if (itemUomId != null) { + itemUomRespository.findById(itemUomId).getOrNull()?.uom?.id?.let { return it } + } + return findStockUnitByItemId(itemId)?.uom?.id + } + open fun findPickingUnitByItemId(itemId: Long): ItemUom? { return itemUomRespository.findByItemIdAndPickingUnitIsTrueAndDeletedIsFalse(itemId) } diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/StockLedgerFixService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/StockLedgerFixService.kt index 7ef5a34..dca6705 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/StockLedgerFixService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/StockLedgerFixService.kt @@ -190,7 +190,7 @@ open class StockLedgerFixService( WHERE inv.id = sl.inventoryId AND IFNULL(inv.deleted, 0) = 0 AND inv.itemId = sl.itemId - AND inv.uomId = sl.uomId + AND COALESCE(inv.stockUomId, inv.uomId) = sl.uomId ) ) AS invIncorrect, SUM(sl.lotQtyAfter IS NULL OR sl.lotQtyBefore IS NULL) AS lotQtyMiss, @@ -964,13 +964,13 @@ open class StockLedgerFixService( ) } - /** 2.3 inventory by (itemId, uomId) from 1.0 MySQL step. */ + /** 2.3 inventory by (itemId, stockUomId bucket) from 1.0 MySQL step. */ private fun fillInventoryId(args: Map): Int { return jdbcDao.executeUpdate( """ UPDATE stock_ledger sl INNER JOIN ( - SELECT i.itemId, i.uomId, MIN(i.id) AS inventoryId + SELECT i.itemId, COALESCE(i.stockUomId, i.uomId) AS bucketUomId, MIN(i.id) AS inventoryId FROM inventory i INNER JOIN ( SELECT DISTINCT itemId, uomId @@ -979,10 +979,10 @@ open class StockLedgerFixService( AND date >= :day AND date < :dayNext AND itemId IS NOT NULL AND uomId IS NOT NULL - ) t ON t.itemId = i.itemId AND t.uomId = i.uomId + ) t ON t.itemId = i.itemId AND t.uomId = COALESCE(i.stockUomId, i.uomId) WHERE IFNULL(i.deleted, 0) = 0 - GROUP BY i.itemId, i.uomId - ) inv ON inv.itemId = sl.itemId AND inv.uomId = sl.uomId + GROUP BY i.itemId, COALESCE(i.stockUomId, i.uomId) + ) inv ON inv.itemId = sl.itemId AND inv.bucketUomId = sl.uomId SET sl.inventoryId = inv.inventoryId WHERE sl.deleted = 0 AND sl.date >= :day AND sl.date < :dayNext @@ -1556,11 +1556,11 @@ open class StockLedgerFixService( """ UPDATE stock_ledger sl INNER JOIN ( - SELECT i.itemId, i.uomId, MIN(i.id) AS inventoryId + SELECT i.itemId, COALESCE(i.stockUomId, i.uomId) AS bucketUomId, MIN(i.id) AS inventoryId FROM inventory i WHERE IFNULL(i.deleted, 0) = 0 - GROUP BY i.itemId, i.uomId - ) inv ON inv.itemId = sl.itemId AND inv.uomId = sl.uomId + GROUP BY i.itemId, COALESCE(i.stockUomId, i.uomId) + ) inv ON inv.itemId = sl.itemId AND inv.bucketUomId = sl.uomId SET sl.inventoryId = inv.inventoryId WHERE sl.deleted = 0 AND sl.date < :today @@ -1612,11 +1612,11 @@ open class StockLedgerFixService( UPDATE stock_ledger sl INNER JOIN tmp_sl_fix_inv_ids t ON t.id = sl.id INNER JOIN ( - SELECT i.itemId, i.uomId, MIN(i.id) AS inventoryId + SELECT i.itemId, COALESCE(i.stockUomId, i.uomId) AS bucketUomId, MIN(i.id) AS inventoryId FROM inventory i WHERE IFNULL(i.deleted, 0) = 0 - GROUP BY i.itemId, i.uomId - ) inv ON inv.itemId = sl.itemId AND inv.uomId = sl.uomId + GROUP BY i.itemId, COALESCE(i.stockUomId, i.uomId) + ) inv ON inv.itemId = sl.itemId AND inv.bucketUomId = sl.uomId SET sl.inventoryId = inv.inventoryId """.trimIndent(), ) @@ -2193,7 +2193,7 @@ open class StockLedgerFixService( jdbc.query( """ SELECT id, created, createdBy, version, modified, modifiedBy, deleted, - itemId, uomId, onHandQty, onHoldQty, unavailableQty, + itemId, uomId, stockUomId, onHandQty, onHoldQty, unavailableQty, price, currencyId, cpu, cpuUnit, cpm, cpmUnit, status FROM inventory WHERE IFNULL(deleted, 0) = 0 @@ -2205,7 +2205,7 @@ open class StockLedgerFixService( writer.write( "INSERT INTO inventory (\n" + " id, created, createdBy, version, modified, modifiedBy, deleted,\n" + - " itemId, uomId, onHandQty, onHoldQty, unavailableQty,\n" + + " itemId, uomId, stockUomId, onHandQty, onHoldQty, unavailableQty,\n" + " price, currencyId, cpu, cpuUnit, cpm, cpmUnit, status\n" + ") VALUES\n", ) @@ -2215,7 +2215,7 @@ open class StockLedgerFixService( writer.write( "(${rsSql(rs, "id")}, ${rsSql(rs, "created")}, ${rsSql(rs, "createdBy")}, ${rsSql(rs, "version")}, " + "${rsSql(rs, "modified")}, ${rsSql(rs, "modifiedBy")}, ${rsSql(rs, "deleted")}, " + - "${rsSql(rs, "itemId")}, ${rsSql(rs, "uomId")}, ${rsSql(rs, "onHandQty")}, " + + "${rsSql(rs, "itemId")}, ${rsSql(rs, "uomId")}, ${rsSql(rs, "stockUomId")}, ${rsSql(rs, "onHandQty")}, " + "${rsSql(rs, "onHoldQty")}, ${rsSql(rs, "unavailableQty")}, ${rsSql(rs, "price")}, " + "${rsSql(rs, "currencyId")}, ${rsSql(rs, "cpu")}, ${rsSql(rs, "cpuUnit")}, " + "${rsSql(rs, "cpm")}, ${rsSql(rs, "cpmUnit")}, ${rsSql(rs, "status")})", @@ -2226,6 +2226,7 @@ open class StockLedgerFixService( "\nON DUPLICATE KEY UPDATE " + "onHandQty=VALUES(onHandQty), onHoldQty=VALUES(onHoldQty), " + "unavailableQty=VALUES(unavailableQty), status=VALUES(status), " + + "uomId=VALUES(uomId), stockUomId=VALUES(stockUomId), " + "price=VALUES(price), currencyId=VALUES(currencyId), " + "cpu=VALUES(cpu), cpuUnit=VALUES(cpuUnit), cpm=VALUES(cpm), cpmUnit=VALUES(cpmUnit), " + "modified=VALUES(modified), modifiedBy=VALUES(modifiedBy), " + @@ -2240,6 +2241,7 @@ open class StockLedgerFixService( "\nON DUPLICATE KEY UPDATE " + "onHandQty=VALUES(onHandQty), onHoldQty=VALUES(onHoldQty), " + "unavailableQty=VALUES(unavailableQty), status=VALUES(status), " + + "uomId=VALUES(uomId), stockUomId=VALUES(stockUomId), " + "price=VALUES(price), currencyId=VALUES(currencyId), " + "cpu=VALUES(cpu), cpuUnit=VALUES(cpuUnit), cpm=VALUES(cpm), cpmUnit=VALUES(cpmUnit), " + "modified=VALUES(modified), modifiedBy=VALUES(modifiedBy), " + @@ -2652,11 +2654,11 @@ open class StockLedgerFixService( LEFT JOIN items it ON it.id = il.itemId LEFT JOIN item_uom iu ON iu.id = ill.stockItemUomId LEFT JOIN ( - SELECT itemId, uomId, MIN(id) AS inventoryId + SELECT itemId, COALESCE(stockUomId, uomId) AS bucketUomId, MIN(id) AS inventoryId FROM inventory WHERE IFNULL(deleted, 0) = 0 - GROUP BY itemId, uomId - ) inv ON inv.itemId = il.itemId AND inv.uomId = iu.uomId + GROUP BY itemId, COALESCE(stockUomId, uomId) + ) inv ON inv.itemId = il.itemId AND inv.bucketUomId = iu.uomId WHERE IFNULL(ill.deleted, 0) = 0 AND ( COALESCE(ill.inQty, 0) <> 0 @@ -2982,7 +2984,7 @@ open class StockLedgerFixService( ON iu.id = ill.stockItemUomId LEFT JOIN inventory inv ON inv.itemId = il.itemId - AND inv.uomId = iu.uomId + AND COALESCE(inv.stockUomId, inv.uomId) = iu.uomId AND IFNULL(inv.deleted, 0) = 0 WHERE IFNULL(ill.deleted, 0) = 0 AND il.itemId IS NOT NULL @@ -2994,12 +2996,13 @@ open class StockLedgerFixService( private const val INSERT_MISSING_INVENTORY_SQL = """ INSERT INTO inventory ( - itemId, uomId, onHandQty, onHoldQty, unavailableQty, + itemId, uomId, stockUomId, onHandQty, onHoldQty, unavailableQty, price, currencyId, cpu, cpuUnit, cpm, cpmUnit, status, created, createdBy, modified, modifiedBy, version, deleted ) SELECT src.itemId, + COALESCE(baseIu.uomId, src.uomId), src.uomId, 0, 0, 0, COALESCE(tpl.price, 0), @@ -3022,9 +3025,13 @@ open class StockLedgerFixService( AND iu.uomId IS NOT NULL GROUP BY il.itemId, iu.uomId ) src + LEFT JOIN item_uom baseIu + ON baseIu.itemId = src.itemId + AND baseIu.baseUnit = 1 + AND IFNULL(baseIu.deleted, 0) = 0 LEFT JOIN inventory existing ON existing.itemId = src.itemId - AND existing.uomId = src.uomId + AND COALESCE(existing.stockUomId, existing.uomId) = src.uomId AND IFNULL(existing.deleted, 0) = 0 LEFT JOIN inventory tpl ON tpl.id = ( @@ -3060,7 +3067,7 @@ open class StockLedgerFixService( AND il.itemId IS NOT NULL AND iu.uomId IS NOT NULL GROUP BY il.itemId, iu.uomId - ) agg ON agg.itemId = i.itemId AND agg.uomId = i.uomId + ) agg ON agg.itemId = i.itemId AND agg.uomId = COALESCE(i.stockUomId, i.uomId) SET i.onHandQty = COALESCE(agg.onHand, 0), i.onHoldQty = COALESCE(agg.onHold, 0), diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/Inventory.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/Inventory.kt index 503d478..566a6c9 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/Inventory.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/Inventory.kt @@ -58,6 +58,11 @@ open class Inventory: BaseEntity(){ @JoinColumn(name = "uomId") open var uom: UomConversion? = null + /** Qty bucket key (lot stock UOM → uom_conversion.id). [uom] is base and is not overwritten by triggers. */ + @ManyToOne + @JoinColumn(name = "stockUomId") + open var stockUom: UomConversion? = null + // @NotNull @Column(name = "status") open var status: String? = null diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt index 89d7754..f45f6ba 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt @@ -112,4 +112,7 @@ interface InventoryRepository: AbstractRepository { /** 與 item_uom 中 baseUnit=true 之 uomId 對齊時使用,避免同一 item 多筆 inventory 時 NonUniqueResult */ fun findFirstByItemIdAndUomIdAndDeletedIsFalseOrderByIdAsc(itemId: Long, uomId: Long): Inventory? + + /** Qty bucket: inventory.stockUomId = lot stock UOM (uom_conversion.id). */ + fun findFirstByItemIdAndStockUomIdAndDeletedIsFalseOrderByIdAsc(itemId: Long, stockUomId: Long): Inventory? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryInfo.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryInfo.kt index be8d777..6c4528e 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryInfo.kt @@ -27,6 +27,10 @@ interface InventoryInfo{ // @get:Value("#{(target.onHandQty - target.onHoldQty - target.unavailableQty) / (target.item.itemUoms.^[stockUnit == true && deleted == false]?.ratioN / target.item.itemUoms.^[stockUnit == true && deleted == false]?.ratioD)}") @get:Value("#{(target.onHandQty - target.onHoldQty - target.unavailableQty)}") val availableQty: BigDecimal? + @get:Value("#{target.stockUom?.id}") + val stockUomId: Long? + @get:Value("#{target.stockUom?.code}") + val stockUomCode: String? @get:Value("#{target.item.itemUoms.^[stockUnit == true && deleted == false]?.uom.code}") val uomCode: String? @get:Value("#{target.item.itemUoms.^[stockUnit == true && deleted == false]?.uom?.udfudesc}") diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt index efcf4c3..f9a8d86 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt @@ -512,13 +512,16 @@ if (updated == 0) { if (deltaQty <= BigDecimal.ZERO) return val sol = stockOutLIneRepository.findById(stockOutLineId).orElse(null) ?: return val solItem = sol.item ?: return - val inventory = itemUomService.findInventoryForItemBaseUom(solItem.id!!) ?: return + val ill = sol.inventoryLotLine?.id?.let { inventoryLotLineRepository.findById(it).orElse(sol.inventoryLotLine) } + ?: sol.inventoryLotLine + val inventory = ( + ill?.let { itemUomService.resolveInventoryForLot(it) } + ?: itemUomService.findInventoryForItemBaseUom(solItem.id!!) + ) ?: return // Fast path: use inventory onHand after update + delta to reconstruct previous balance. val onHandAfter = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() val previousBalance = onHandAfter + deltaQty.toDouble() val newBalance = previousBalance - deltaQty.toDouble() - val ill = sol.inventoryLotLine?.id?.let { inventoryLotLineRepository.findById(it).orElse(sol.inventoryLotLine) } - ?: sol.inventoryLotLine val lotAfter = ill?.let { StockLedgerLotSnapshot.availableQty(it) } val ledger = StockLedger().apply { this.stockOutLine = sol diff --git a/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/01_add_inventory_stock_uom_id.sql b/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/01_add_inventory_stock_uom_id.sql new file mode 100644 index 0000000..4e17d6d --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/01_add_inventory_stock_uom_id.sql @@ -0,0 +1,44 @@ +--liquibase formatted sql + +--changeset fpsms:20260911_inventory_add_stock_uom_id +--comment: No.1 inventory.stockUomId is the qty bucket; uomId stays base. No.35 unique among non-deleted rows. +--precondition onFail:MARK_RAN +--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'inventory' AND COLUMN_NAME = 'stockUomId' + +ALTER TABLE `inventory` + ADD COLUMN `stockUomId` INT NULL AFTER `uomId`; + +UPDATE `inventory` i +INNER JOIN ( + SELECT `itemId`, `uomId`, MIN(`id`) AS keepId + FROM `inventory` + WHERE IFNULL(`deleted`, 0) = 0 + AND `uomId` IS NOT NULL + GROUP BY `itemId`, `uomId` +) k ON k.keepId = i.`id` +SET i.`stockUomId` = i.`uomId` +WHERE i.`stockUomId` IS NULL + AND i.`uomId` IS NOT NULL; + +UPDATE `inventory` i +INNER JOIN `item_uom` iu + ON iu.`itemId` = i.`itemId` + AND iu.`baseUnit` = 1 + AND IFNULL(iu.`deleted`, 0) = 0 +SET i.`uomId` = iu.`uomId` +WHERE IFNULL(i.`deleted`, 0) = 0 + AND iu.`uomId` IS NOT NULL; + +ALTER TABLE `inventory` + ADD COLUMN `stockUomActiveKey` VARCHAR(40) + GENERATED ALWAYS AS ( + IF(IFNULL(`deleted`, 0) = 0 AND `stockUomId` IS NOT NULL, + CONCAT(`itemId`, ':', `stockUomId`), + NULL) + ) STORED; + +CREATE UNIQUE INDEX `uk_inventory_item_stock_uom_active` + ON `inventory` (`stockUomActiveKey`); + +CREATE INDEX `idx_inventory_item_stock_uom` + ON `inventory` (`itemId`, `stockUomId`); diff --git a/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/02_drop_inventory_lot_line_triggers.sql b/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/02_drop_inventory_lot_line_triggers.sql new file mode 100644 index 0000000..2124410 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/02_drop_inventory_lot_line_triggers.sql @@ -0,0 +1,8 @@ +--liquibase formatted sql + +--changeset fpsms:20260911_drop_inventory_lot_line_triggers +--comment: Replace lot-line inventory triggers with stockUomId bucket lookup. + +DROP TRIGGER IF EXISTS `inventory_lot_line_AFTER_INSERT`; + +DROP TRIGGER IF EXISTS `inventory_lot_line_AFTER_UPDATE`; diff --git a/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/03_inventory_lot_line_trigger_insert.sql b/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/03_inventory_lot_line_trigger_insert.sql new file mode 100644 index 0000000..f81edcb --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/03_inventory_lot_line_trigger_insert.sql @@ -0,0 +1,170 @@ +-- liquibase formatted sql + +-- changeset fpsms:20260911_inventory_lot_line_trigger_after_insert splitStatements:false +-- comment: No.2 AFTER INSERT: bucket by lot stock UOM (stockItemUomId → inventory.stockUomId). New rows set stockUomId + base uomId. UPDATE existing qty only. + +create definer = current_user trigger `inventory_lot_line_AFTER_insert` + after insert + on `inventory_lot_line` + for each row +begin + declare inventoryId int default -1; + declare itemId int default null; + declare stockUomId int default null; + declare baseUomId int default null; + declare error_msg TEXT default null; + declare continue handler for sqlexception, sqlwarning + begin + get diagnostics condition 1 error_msg = message_text; + set error_msg = coalesce(error_msg, 'Unknown error occurred'); + end; + + select il.itemId + into itemId + from `inventory_lot` il + where il.id = new.inventoryLotId; + + select iu.uomId + into stockUomId + from `item_uom` iu + where iu.id = new.stockItemUomId + limit 1; + + if stockUomId is null and itemId is not null then + select iu.uomId + into stockUomId + from `item_uom` iu + where iu.itemId = itemId + and iu.stockUnit = true + and ifnull(iu.deleted, 0) = 0 + limit 1; + end if; + + if itemId is not null then + select iu.uomId + into baseUomId + from `item_uom` iu + where iu.itemId = itemId + and iu.baseUnit = true + and ifnull(iu.deleted, 0) = 0 + limit 1; + end if; + + if itemId is not null and stockUomId is not null then + select coalesce( + (select i.id + from `inventory` i + where i.itemId = itemId + and i.stockUomId = stockUomId + and ifnull(i.deleted, 0) = 0 + order by i.id + limit 1), + (select i.id + from `inventory` i + where i.itemId = itemId + and i.uomId = stockUomId + and i.stockUomId is null + and ifnull(i.deleted, 0) = 0 + order by i.id + limit 1), + -1 + ) + into inventoryId; + end if; + + drop temporary table if exists `temp_inventory`; + + create temporary table `temp_inventory` + ( + onHandQty decimal(14, 2), + onHoldQty decimal(14, 2), + unavailableQty decimal(14, 2), + currencyId int, + currencyName varchar(30), + itemId int, + price decimal(14, 2) + ); + + insert into `temp_inventory` (onHandQty, onHoldQty, unavailableQty, currencyId, currencyName, itemId, price) + select coalesce(new.inQty, 0) - coalesce(new.outQty, 0) AS onHandQty, + coalesce(new.holdQty, 0) AS onHoldQty, + IF( + new.status = 'unavailable', + coalesce(new.inQty, 0) - coalesce(new.outQty, 0) - coalesce(new.holdQty, 0), + 0 + ) AS unavailableQty, + c.id AS currencyId, + coalesce(c.name, 'HKD') AS currencyName, + il.itemId AS itemId, + coalesce(pql.price, 0) AS price + from `inventory_lot` il + left join `stock_in_line` sil on sil.id = il.stockInLineId + left join `stock_in` si on si.id = sil.stockInId + left join `purchase_order_line` pol on pol.id = sil.purchaseOrderLineId + left join `purchase_order` po on po.id = pol.purchaseOrderId + left join `purchase_quotation_line` pql on pql.itemId = il.itemId and pql.uomId = pol.uomId + left join `purchase_quotation` pq on pq.id = pql.purchaseQuotationId + left join `currency` c on c.id = pq.currencyId + where il.id = new.inventoryLotId; + + begin + declare inventoryErrorMsg text default null; + declare continue handler for sqlexception, sqlwarning + begin + get diagnostics condition 1 inventoryErrorMsg = message_text; + set inventoryErrorMsg = coalesce(inventoryErrorMsg, 'Unknown error occurred'); + end; + + if inventoryId < 0 and stockUomId is not null then + insert into `inventory` (itemId, onHandQty, onHoldQty, unavailableQty, price, currencyId, cpu, cpuUnit, cpm, + cpmUnit, uomId, stockUomId, status) + select ti.itemId AS itemId, + ti.onHandQty AS onHandQty, + ti.onHoldQty AS onHoldQty, + ti.unavailableQty AS unavailableQty, + ti.price AS price, + ti.currencyId AS currencyId, + 0 AS cpu, + ti.currencyName AS cpuUnit, + 0 AS cpm, + ti.currencyName AS cpmUnit, + coalesce(baseUomId, stockUomId) AS uomId, + stockUomId AS stockUomId, + if(coalesce(ti.onHandQty - ti.onHoldQty - ti.unavailableQty, 0) > 0, 'available', + 'unavailable') AS status + from `temp_inventory` ti; + else + update `inventory` i + join `temp_inventory` ti on i.id = inventoryId + set i.onHandQty = (i.onHandQty + ti.onHandQty), + i.onHoldQty = (i.onHoldQty + ti.onHoldQty), + i.unavailableQty = (i.unavailableQty + ti.unavailableQty), + i.price = ti.price, + i.currencyId = ti.currencyId, + i.cpu = 0, + i.cpuUnit = ti.currencyName, + i.cpm = 0, + i.cpmUnit = ti.currencyName, + i.stockUomId = coalesce(i.stockUomId, stockUomId), + i.status = if(coalesce((i.onHandQty + ti.onHandQty) - (i.onHoldQty + ti.onHoldQty) - + (i.unavailableQty + ti.unavailableQty), 0) > 0, 'available', + 'unavailable') + where i.id = inventoryId; + end if; + + if inventoryErrorMsg is null then + insert into `trigger_debug_log` (tableName, triggerName, description, status) + VALUES ('inventory_lot_line', 'after insert', 'Insert / Update Inventory Table', 'success'); + ELSE + insert into `trigger_debug_log` (tableName, triggerName, description, errorMessages, status) + VALUES ('inventory_lot_line', 'after insert', 'Insert / Update Inventory Table', inventoryErrorMsg, 'fail'); + end if; + end; + + drop temporary table if exists `temp_inventory`; + + if error_msg is NOT null then + insert into `trigger_debug_log` (tableName, triggerName, `description`, errorMessages, `status`) + VALUES ('inventory_lot_line', 'after insert', 'Unhandled error in trigger', error_msg, 'fail'); + end if; +end; diff --git a/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/04_inventory_lot_line_trigger_update.sql b/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/04_inventory_lot_line_trigger_update.sql new file mode 100644 index 0000000..94fed21 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260911_inventory_stock_uom/04_inventory_lot_line_trigger_update.sql @@ -0,0 +1,198 @@ +-- liquibase formatted sql + +-- changeset fpsms:20260911_inventory_lot_line_trigger_after_update splitStatements:false +-- comment: No.3 AFTER UPDATE: same stockUomId bucket as INSERT. Qty/hold/unavailable only; do not overwrite uomId. + +create definer = current_user trigger `inventory_lot_line_AFTER_UPDATE` + after update + on `inventory_lot_line` + for each row +begin + declare inventoryId int default -1; + declare itemId int default null; + declare stockUomId int default null; + declare baseUomId int default null; + declare error_msg text default null; + declare continue handler for sqlexception, sqlwarning + begin + get diagnostics condition 1 error_msg = message_text; + SET error_msg = coalesce(error_msg, 'Unknown error occurred'); + end; + + select il.itemId + into itemId + from `inventory_lot` il + where il.id = new.inventoryLotId; + + select iu.uomId + into stockUomId + from `item_uom` iu + where iu.id = new.stockItemUomId + limit 1; + + if stockUomId is null and itemId is not null then + select iu.uomId + into stockUomId + from `item_uom` iu + where iu.itemId = itemId + and iu.stockUnit = true + and ifnull(iu.deleted, 0) = 0 + limit 1; + end if; + + if itemId is not null then + select iu.uomId + into baseUomId + from `item_uom` iu + where iu.itemId = itemId + and iu.baseUnit = true + and ifnull(iu.deleted, 0) = 0 + limit 1; + end if; + + if itemId is not null and stockUomId is not null then + select coalesce( + (select i.id + from `inventory` i + where i.itemId = itemId + and i.stockUomId = stockUomId + and ifnull(i.deleted, 0) = 0 + order by i.id + limit 1), + (select i.id + from `inventory` i + where i.itemId = itemId + and i.uomId = stockUomId + and i.stockUomId is null + and ifnull(i.deleted, 0) = 0 + order by i.id + limit 1), + -1 + ) + into inventoryId; + end if; + + drop temporary table if exists temp_inventory; + + create temporary table temp_inventory + ( + oldOnHandQty decimal(14, 2), + currentOnHandQty decimal(14, 2), + oldOnHoldQty decimal(14, 2), + currentOnHoldQty decimal(14, 2), + oldUnavailableQty decimal(14, 2), + currentUnavailableQty decimal(14, 2), + currencyId INT, + currencyName varchar(30), + itemId INT, + price decimal(14, 2) + ); + + insert into temp_inventory (oldOnHandQty, currentOnHandQty, oldOnHoldQty, currentOnHoldQty, oldUnavailableQty, + currentUnavailableQty, currencyId, currencyName, itemId, price) + select coalesce(coalesce(old.inQty, 0) - coalesce(old.outQty, 0), 0) as oldOnHandQty, + coalesce(coalesce(new.inQty, 0) - coalesce(new.outQty, 0), 0) as currentOnHandQty, + coalesce(old.holdQty, 0) as oldOnHoldQty, + coalesce(new.holdQty, 0) as currentOnHoldQty, + IF( + old.status = 'unavailable', + coalesce(old.inQty, 0) - coalesce(old.outQty, 0) - coalesce(old.holdQty, 0), + 0 + ) as oldUnavailableQty, + IF( + new.status = 'unavailable', + coalesce(new.inQty, 0) - coalesce(new.outQty, 0) - coalesce(new.holdQty, 0), + 0 + ) as currentUnavailableQty, + c.id as currencyId, + coalesce(c.name, 'HKD') as currencyName, + il.itemId as itemId, + coalesce(pql.price, 0) as price + from `inventory_lot` il + left join `stock_in_line` sil on sil.id = il.stockInLineId + left join `stock_in` si on si.id = sil.stockInId + left join `purchase_order_line` pol on pol.id = sil.purchaseOrderLineId + left join `purchase_order` po on po.id = pol.purchaseOrderId + left join `purchase_quotation_line` pql on pql.itemId = il.itemId and pql.uomId = pol.uomId + left join `purchase_quotation` pq on pq.id = pql.purchaseQuotationId + left join `currency` c on c.id = pq.currencyId + where il.id = new.inventoryLotId; + + begin + declare inventoryErrorMsg text default null; + declare continue handler for sqlexception, sqlwarning + begin + GET diagnostics condition 1 inventoryErrorMsg = message_text; + SET inventoryErrorMsg = coalesce(inventoryErrorMsg, 'Unknown error occurred'); + end; + + if inventoryId < 0 and stockUomId is not null then + insert into `inventory` (itemId, onHandQty, onHoldQty, unavailableQty, price, currencyId, cpu, cpuUnit, cpm, + cpmUnit, uomId, stockUomId, status) + select ti.itemId as itemId, + ti.currentOnHandQty as onHandQty, + ti.currentOnHoldQty as onHoldQty, + ti.currentUnavailableQty as unavailableQty, + ti.price as price, + ti.currencyId as currencyId, + 0 as cpu, + ti.currencyName as cpuUnit, + 0 as cpm, + ti.currencyName as cpmUnit, + coalesce(baseUomId, stockUomId) as uomId, + stockUomId as stockUomId, + if(coalesce(ti.currentOnHandQty - ti.currentOnHoldQty - ti.currentUnavailableQty, 0) > 0, + 'available', 'unavailable') as status + from `temp_inventory` ti; + else + update `inventory` i + join `temp_inventory` ti on i.id = inventoryId + set i.onHandQty = i.onHandQty - (ti.oldOnHandQty - ti.currentOnHandQty), + i.onHoldQty = i.onHoldQty - (ti.oldOnHoldQty - ti.currentOnHoldQty), + i.unavailableQty = case + when old.status = 'available' and new.status = 'unavailable' + then i.unavailableQty + ti.currentUnavailableQty + when old.status = 'unavailable' and new.status = 'available' + then i.unavailableQty - ti.oldUnavailableQty + when old.status = 'unavailable' and new.status = 'unavailable' + then i.unavailableQty + (ti.currentUnavailableQty - ti.oldUnavailableQty) + else i.unavailableQty + end, + i.price = ti.price, + i.currencyId = ti.currencyId, + i.cpu = 0, + i.cpuUnit = ti.currencyName, + i.cpm = 0, + i.cpmUnit = ti.currencyName, + i.stockUomId = coalesce(i.stockUomId, stockUomId), + i.status = if((i.onHandQty - (ti.oldOnHandQty - ti.currentOnHandQty)) - (i.onHoldQty - + (ti.oldOnHoldQty - ti.currentOnHoldQty)) + - (case + when old.status = 'available' and new.status = 'unavailable' + then i.unavailableQty + ti.currentUnavailableQty + when old.status = 'unavailable' and new.status = 'available' + then i.unavailableQty - ti.oldUnavailableQty + when old.status = 'unavailable' and new.status = 'unavailable' + then i.unavailableQty + (ti.currentUnavailableQty - ti.oldUnavailableQty) + else i.unavailableQty + end) > 0, + 'available', 'unavailable') + where i.id = inventoryId; + end if; + + if inventoryErrorMsg is null then + insert into `trigger_debug_log` (tableName, triggerName, description, status) + values ('inventory_lot_line', 'after update', 'Insert / Update Inventory Table', 'success'); + else + insert into `trigger_debug_log` (tableName, triggerName, description, errorMessages, status) + values ('inventory_lot_line', 'after update', 'Insert / Update Inventory Table', inventoryErrorMsg, 'fail'); + end if; + end; + + drop temporary table if exists `temp_inventory`; + + if error_msg is NOT null then + insert into `trigger_debug_log` (tableName, triggerName, `description`, errorMessages, `status`) + VALUES ('inventory_lot_line', 'after update', 'Unhandled error in trigger', error_msg, 'fail'); + end if; +end;