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.core.io.ClassPathResource | ||||
| import org.springframework.data.domain.PageRequest | import org.springframework.data.domain.PageRequest | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import org.springframework.http.HttpStatus | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||
| import org.springframework.web.server.ResponseStatusException | |||||
| import java.io.FileNotFoundException | import java.io.FileNotFoundException | ||||
| import java.io.IOException | import java.io.IOException | ||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| @@ -190,7 +192,21 @@ open class InventoryLotLineService( | |||||
| val stockUom = request.stockUomId?.let { itemUomRespository.findById(it).getOrNull() } | val stockUom = request.stockUomId?.let { itemUomRespository.findById(it).getOrNull() } | ||||
| ?: existing?.stockUom | ?: existing?.stockUom | ||||
| if (stockUom == null) { | 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 } } | 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.ss.usermodel.Workbook | ||||
| import org.apache.poi.xssf.usermodel.XSSFWorkbook | import org.apache.poi.xssf.usermodel.XSSFWorkbook | ||||
| import org.springframework.core.io.support.PathMatchingResourcePatternResolver | import org.springframework.core.io.support.PathMatchingResourcePatternResolver | ||||
| import org.springframework.http.HttpStatus | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import org.springframework.web.server.ResponseStatusException | |||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| import java.sql.Timestamp | import java.sql.Timestamp | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| @@ -649,8 +651,9 @@ open class InventoryService( | |||||
| val warehouse = warehouseRepository.findAll().find { it.code == request.warehouseCode }!! | val warehouse = warehouseRepository.findAll().find { it.code == request.warehouseCode }!! | ||||
| // val salesUnit = itemUomService.findSalesUnitByItemId(itemId = inventoryLot.item!!.id!!) | // val salesUnit = itemUomService.findSalesUnitByItemId(itemId = inventoryLot.item!!.id!!) | ||||
| val stockUnit = itemUomService.findStockUnitByItemId(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. | // ChangeList #8: lot-line stockItemUomId must be set so trigger writes inventory.stockUomId. | ||||
| InventoryLotLine().apply { | InventoryLotLine().apply { | ||||
| @@ -539,8 +539,9 @@ open class StockInLineService( | |||||
| this.inQty = convertedBaseQty | this.inQty = convertedBaseQty | ||||
| this.status = InventoryLotLineStatus.AVAILABLE | this.status = InventoryLotLineStatus.AVAILABLE | ||||
| this.stockUom = stockItemUom | this.stockUom = stockItemUom | ||||
| ?: throw IllegalArgumentException( | |||||
| "stockItemUomId is required: no stockUnit item_uom for itemId=${request.itemId}", | |||||
| ?: throw ResponseStatusException( | |||||
| HttpStatus.BAD_REQUEST, | |||||
| "入倉明細缺少庫存單位,無法入帳", | |||||
| ) | ) | ||||
| } | } | ||||
| saveLines.add(inventoryLotLine) | saveLines.add(inventoryLotLine) | ||||
| @@ -1535,9 +1536,9 @@ open class StockInLineService( | |||||
| ) | ) | ||||
| } | } | ||||
| return itemUomRepository.findFirstByItemIdAndStockUnitIsTrueAndDeletedIsFalseOrderByIdAsc(itemId) | 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; | |||||