App and Liquibase BEFORE triggers reject one lot spanning two stock-UOM buckets so API/UI match M-X-01 and M-E-01.fix負數倉
| @@ -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 } } | |||
| @@ -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 { | |||
| @@ -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, | |||
| "入倉明細缺少庫存單位,無法入帳", | |||
| ) | |||
| } | |||
| @@ -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; | |||