| @@ -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 | |||
| @@ -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) | |||
| } | |||
| @@ -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<String, Any>): 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), | |||
| @@ -58,6 +58,11 @@ open class Inventory: BaseEntity<Long>(){ | |||
| @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 | |||
| @@ -112,4 +112,7 @@ interface InventoryRepository: AbstractRepository<Inventory, Long> { | |||
| /** 與 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? | |||
| } | |||
| @@ -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}") | |||
| @@ -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 | |||
| @@ -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`); | |||
| @@ -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`; | |||
| @@ -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; | |||
| @@ -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; | |||