From 017e2d21b9e24e1b07555b9cc371127c4d18da28 Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Tue, 22 Sep 2026 19:02:57 +0800 Subject: [PATCH] Return Chinese HTTP 400 for missing stock UOM and block mixed-UOM lots. App and Liquibase BEFORE triggers reject one lot spanning two stock-UOM buckets so API/UI match M-X-01 and M-E-01. --- .../stock/service/InventoryLotLineService.kt | 18 +++++- .../modules/stock/service/InventoryService.kt | 7 ++- .../stock/service/StockInLineService.kt | 11 ++-- .../07_one_lot_one_stock_uom.sql | 56 +++++++++++++++++++ 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20260910_inventory_stock_uom/07_one_lot_one_stock_uom.sql diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt index 0fdab33..0082e85 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt @@ -25,8 +25,10 @@ import net.sf.jasperreports.engine.JasperCompileManager import org.springframework.core.io.ClassPathResource import org.springframework.data.domain.PageRequest import java.time.LocalDate +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.server.ResponseStatusException import java.io.FileNotFoundException import java.io.IOException import java.math.BigDecimal @@ -190,7 +192,21 @@ open class InventoryLotLineService( val stockUom = request.stockUomId?.let { itemUomRespository.findById(it).getOrNull() } ?: existing?.stockUom if (stockUom == null) { - throw IllegalArgumentException("stockItemUomId is required for inventory_lot_line") + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "入倉明細缺少庫存單位,無法入帳") + } + // M-E-01: one inventory lot must not mix two stock-UOM buckets (item_uom.uomId). + val lotId = inventoryLot?.id ?: existing?.inventoryLot?.id + val newBucketUomId = stockUom.uom?.id + if (lotId != null && newBucketUomId != null) { + val conflict = inventoryLotLineRepository.findAllByInventoryLotId(lotId).any { sibling -> + sibling.deleted != true && + sibling.id != existing?.id && + sibling.stockUom?.uom?.id != null && + sibling.stockUom!!.uom!!.id != newBucketUomId + } + if (conflict) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "同一批次不可混用不同庫存單位") + } } val status = request.status?.let { _status -> InventoryLotLineStatus.entries.find { it.value == _status } } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt index 71744df..6570d0c 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt @@ -25,7 +25,9 @@ import org.apache.poi.ss.usermodel.Sheet import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.core.io.support.PathMatchingResourcePatternResolver +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException import java.math.BigDecimal import java.sql.Timestamp import java.time.LocalDate @@ -649,8 +651,9 @@ open class InventoryService( val warehouse = warehouseRepository.findAll().find { it.code == request.warehouseCode }!! // val salesUnit = itemUomService.findSalesUnitByItemId(itemId = inventoryLot.item!!.id!!) val stockUnit = itemUomService.findStockUnitByItemId(itemId = inventoryLot.item!!.id!!) - ?: throw IllegalArgumentException( - "stockItemUomId is required: no stockUnit item_uom for itemId=${inventoryLot.item!!.id}", + ?: throw ResponseStatusException( + HttpStatus.BAD_REQUEST, + "入倉明細缺少庫存單位,無法入帳", ) // ChangeList #8: lot-line stockItemUomId must be set so trigger writes inventory.stockUomId. InventoryLotLine().apply { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index 7283ead..abcb493 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -539,8 +539,9 @@ open class StockInLineService( this.inQty = convertedBaseQty this.status = InventoryLotLineStatus.AVAILABLE this.stockUom = stockItemUom - ?: throw IllegalArgumentException( - "stockItemUomId is required: no stockUnit item_uom for itemId=${request.itemId}", + ?: throw ResponseStatusException( + HttpStatus.BAD_REQUEST, + "入倉明細缺少庫存單位,無法入帳", ) } saveLines.add(inventoryLotLine) @@ -1535,9 +1536,9 @@ open class StockInLineService( ) } return itemUomRepository.findFirstByItemIdAndStockUnitIsTrueAndDeletedIsFalseOrderByIdAsc(itemId) - ?: throw IllegalArgumentException( - "Stock UOM not found for item: id=$itemId, itemNo=$itemNo. " + - "Ensure at least one item_uom row has stockUnit=1, deleted=0 for this item." + ?: throw ResponseStatusException( + HttpStatus.BAD_REQUEST, + "入倉明細缺少庫存單位,無法入帳", ) } diff --git a/src/main/resources/db/changelog/changes/20260910_inventory_stock_uom/07_one_lot_one_stock_uom.sql b/src/main/resources/db/changelog/changes/20260910_inventory_stock_uom/07_one_lot_one_stock_uom.sql new file mode 100644 index 0000000..37845e1 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260910_inventory_stock_uom/07_one_lot_one_stock_uom.sql @@ -0,0 +1,56 @@ +-- liquibase formatted sql + +-- changeset kelvin:20260922-ill-before-insert-one-lot-one-stock-uom splitStatements:false +-- comment: M-E-01 One inventory_lot must not mix two stock UOM buckets. +DROP TRIGGER IF EXISTS `inventory_lot_line_BEFORE_insert_one_stock_uom`; +CREATE DEFINER = CURRENT_USER TRIGGER `inventory_lot_line_BEFORE_insert_one_stock_uom` + BEFORE INSERT + ON `inventory_lot_line` + FOR EACH ROW +BEGIN + IF NEW.stockItemUomId IS NOT NULL + AND NEW.inventoryLotId IS NOT NULL + AND IFNULL(NEW.deleted, 0) = 0 + AND EXISTS ( + SELECT 1 + FROM `inventory_lot_line` ill + INNER JOIN `item_uom` iu_old + ON iu_old.id = ill.stockItemUomId AND IFNULL(iu_old.deleted, 0) = 0 + INNER JOIN `item_uom` iu_new + ON iu_new.id = NEW.stockItemUomId AND IFNULL(iu_new.deleted, 0) = 0 + WHERE ill.inventoryLotId = NEW.inventoryLotId + AND IFNULL(ill.deleted, 0) = 0 + AND iu_old.uomId <> iu_new.uomId + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '同一批次不可混用不同庫存單位'; + END IF; +END; + +-- changeset kelvin:20260922-ill-before-update-one-lot-one-stock-uom splitStatements:false +-- comment: M-E-01 Also block UPDATE that would mix stock UOMs on the same lot. +DROP TRIGGER IF EXISTS `inventory_lot_line_BEFORE_update_one_stock_uom`; +CREATE DEFINER = CURRENT_USER TRIGGER `inventory_lot_line_BEFORE_update_one_stock_uom` + BEFORE UPDATE + ON `inventory_lot_line` + FOR EACH ROW +BEGIN + IF NEW.stockItemUomId IS NOT NULL + AND NEW.inventoryLotId IS NOT NULL + AND IFNULL(NEW.deleted, 0) = 0 + AND EXISTS ( + SELECT 1 + FROM `inventory_lot_line` ill + INNER JOIN `item_uom` iu_old + ON iu_old.id = ill.stockItemUomId AND IFNULL(iu_old.deleted, 0) = 0 + INNER JOIN `item_uom` iu_new + ON iu_new.id = NEW.stockItemUomId AND IFNULL(iu_new.deleted, 0) = 0 + WHERE ill.inventoryLotId = NEW.inventoryLotId + AND IFNULL(ill.deleted, 0) = 0 + AND ill.id <> NEW.id + AND iu_old.uomId <> iu_new.uomId + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '同一批次不可混用不同庫存單位'; + END IF; +END;