Bläddra i källkod

Replace inventory-bucket errors with wording that names the lot and stock unit.

Co-authored-by: Cursor <[email protected]>
fix負數倉
kelvin.yau 3 veckor sedan
förälder
incheckning
70695549d5
2 ändrade filer med 381 tillägg och 1 borttagningar
  1. +1
    -1
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  2. +380
    -0
      src/main/resources/db/changelog/changes/20260910_inventory_stock_uom/08_signal_stock_account_wording.sql

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Visa fil

@@ -2736,7 +2736,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
if (lotLine != null && inventoryBeforeUpdate == null) {
throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"找不到對應庫存單位的庫存桶,無法提交不良品/短少。",
"此批次的庫存單位與貨品不符,無法提交不良品/短少。",
)
}


+ 380
- 0
src/main/resources/db/changelog/changes/20260910_inventory_stock_uom/08_signal_stock_account_wording.sql Visa fil

@@ -0,0 +1,380 @@
-- liquibase formatted sql

-- changeset kelvin:20261013-drop-ill-triggers-stock-account
-- comment: Recreate lot-line triggers so the customer-facing SIGNAL names the lot and stock unit, without 庫存桶.
DROP TRIGGER IF EXISTS `inventory_lot_line_AFTER_insert`;
DROP TRIGGER IF EXISTS `inventory_lot_line_AFTER_update`;

-- changeset kelvin:20261013-ill-after-insert-stock-account splitStatements:false
-- comment: AFTER INSERT trigger. Same logic as 20260921-ill-after-insert-signal-zh. SIGNAL names the lot and stock unit.
CREATE DEFINER = CURRENT_USER TRIGGER `inventory_lot_line_AFTER_insert`
AFTER INSERT
ON `inventory_lot_line`
FOR EACH ROW
BEGIN
DECLARE v_itemId INT;
DECLARE v_stockUomId INT; -- bucket key = item_uom.uomId for NEW.stockItemUomId
DECLARE v_baseUomId INT; -- baseUnit uomId; written to inventory.uomId only on CREATE
DECLARE v_inventoryId INT DEFAULT NULL;
DECLARE v_onHand DECIMAL(14, 2) DEFAULT 0;
DECLARE v_unavailable DECIMAL(14, 2) DEFAULT 0;
DECLARE v_currencyId INT DEFAULT NULL;
DECLARE v_currencyName VARCHAR(30) DEFAULT 'HKD';
DECLARE v_price DECIMAL(14, 2) DEFAULT 0;

-- Step 1: stockItemUomId is required to know which stock-UOM bucket this line belongs to.
IF NEW.stockItemUomId IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '入倉明細缺少庫存單位,無法入帳';
END IF;

-- Step 2: Resolve bucket (itemId, stockUomId) from lot + item_uom.
-- stockItemUomId -> item_uom.id; stockUomId := item_uom.uomId (must belong to the same item).
SELECT il.itemId, iu.uomId
INTO v_itemId, v_stockUomId
FROM `inventory_lot` il
INNER JOIN `item_uom` iu
ON iu.id = NEW.stockItemUomId
AND iu.itemId = il.itemId
AND IFNULL(iu.deleted, 0) = 0
WHERE il.id = NEW.inventoryLotId
AND IFNULL(il.deleted, 0) = 0
LIMIT 1;

IF v_itemId IS NULL OR v_stockUomId IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '此批次的庫存單位與貨品不符,請檢查貨品單位與批次';
END IF;

-- Step 3: Resolve base UOM for inventory.uomId snapshot (used only if we INSERT a new inventory row).
SELECT iu.uomId
INTO v_baseUomId
FROM `item_uom` iu
WHERE iu.itemId = v_itemId
AND iu.baseUnit = TRUE
AND IFNULL(iu.deleted, 0) = 0
LIMIT 1;

IF v_baseUomId IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '此貨品未設定基本單位,無法建立庫存';
END IF;

-- Step 4: Find existing inventory row for this stock-UOM bucket.
SELECT i.id
INTO v_inventoryId
FROM `inventory` i
WHERE i.itemId = v_itemId
AND i.stockUomId = v_stockUomId
AND IFNULL(i.deleted, 0) = 0
LIMIT 1;

-- Step 5: Create inventory row if missing (qty starts at 0; Step 6 overwrites from lot lines).
-- uomId = base snapshot once; stockUomId = bucket key. onHoldQty left at 0 and never maintained by this trigger.
IF v_inventoryId IS NULL THEN
-- Prefer currency/price from the lot's stock-in / PO / quotation path; fall back to HKD / first currency.
SELECT c.id, COALESCE(c.name, 'HKD'), COALESCE(pql.price, 0)
INTO v_currencyId, v_currencyName, v_price
FROM `inventory_lot` il
LEFT JOIN `stock_in_line` sil ON sil.id = il.stockInLineId
LEFT JOIN `purchase_order_line` pol ON pol.id = sil.purchaseOrderLineId
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
LIMIT 1;

IF v_currencyId IS NULL THEN
SELECT c.id, COALESCE(c.name, 'HKD')
INTO v_currencyId, v_currencyName
FROM `currency` c
WHERE c.code = 'HKD'
AND IFNULL(c.deleted, 0) = 0
LIMIT 1;
END IF;

IF v_currencyId IS NULL THEN
SELECT c.id, COALESCE(c.name, 'HKD')
INTO v_currencyId, v_currencyName
FROM `currency` c
WHERE IFNULL(c.deleted, 0) = 0
ORDER BY c.id
LIMIT 1;
END IF;

IF v_currencyId IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '無法取得幣別,無法建立庫存';
END IF;

INSERT INTO `inventory` (`itemId`,
`onHandQty`,
`onHoldQty`,
`unavailableQty`,
`price`,
`currencyId`,
`cpu`,
`cpuUnit`,
`cpm`,
`cpmUnit`,
`uomId`,
`stockUomId`,
`status`)
VALUES (v_itemId,
0,
0,
0,
COALESCE(v_price, 0),
v_currencyId,
0,
COALESCE(v_currencyName, 'HKD'),
0,
COALESCE(v_currencyName, 'HKD'),
v_baseUomId,
v_stockUomId,
'unavailable');

SET v_inventoryId = LAST_INSERT_ID();
END IF;

-- Step 6: Full recompute for THIS bucket only (not a delta on existing inventory qty).
-- onHand = SUM(inQty - outQty) for all statuses
-- unavailable = SUM(inQty - outQty) for status = unavailable only (holdQty ignored)
SELECT COALESCE(SUM(ill.inQty - ill.outQty), 0),
COALESCE(SUM(CASE
WHEN LOWER(ill.status) = 'unavailable'
THEN ill.inQty - ill.outQty
ELSE 0 END), 0)
INTO v_onHand, v_unavailable
FROM `inventory_lot_line` ill
INNER JOIN `inventory_lot` il ON il.id = ill.inventoryLotId AND IFNULL(il.deleted, 0) = 0
INNER JOIN `item_uom` iu ON iu.id = ill.stockItemUomId AND IFNULL(iu.deleted, 0) = 0
WHERE il.itemId = v_itemId
AND iu.uomId = v_stockUomId
AND IFNULL(ill.deleted, 0) = 0;

-- Step 7: Overwrite inventory qty/status from the SUM (source of truth = lot lines).
-- Do not touch onHoldQty, uomId, or stockUomId on an existing row.
UPDATE `inventory`
SET `onHandQty` = v_onHand,
`unavailableQty` = v_unavailable,
`status` = IF(v_onHand - v_unavailable > 0, 'available', 'unavailable'),
`modified` = CURRENT_TIMESTAMP
WHERE `id` = v_inventoryId;
END;

-- changeset kelvin:20261013-ill-after-update-stock-account splitStatements:false
-- comment: AFTER UPDATE trigger. Same logic as 20260921-ill-after-update-signal-zh. SIGNAL names the lot and stock unit.
CREATE DEFINER = CURRENT_USER TRIGGER `inventory_lot_line_AFTER_update`
AFTER UPDATE
ON `inventory_lot_line`
FOR EACH ROW
BEGIN
DECLARE v_itemId INT;
DECLARE v_stockUomId INT; -- NEW bucket key
DECLARE v_baseUomId INT; -- baseUnit uomId; used only when creating inventory
DECLARE v_inventoryId INT DEFAULT NULL;
DECLARE v_onHand DECIMAL(14, 2) DEFAULT 0;
DECLARE v_unavailable DECIMAL(14, 2) DEFAULT 0;
DECLARE v_currencyId INT DEFAULT NULL;
DECLARE v_currencyName VARCHAR(30) DEFAULT 'HKD';
DECLARE v_price DECIMAL(14, 2) DEFAULT 0;

DECLARE v_oldItemId INT DEFAULT NULL;
DECLARE v_oldStockUomId INT DEFAULT NULL;
DECLARE v_oldInventoryId INT DEFAULT NULL;
DECLARE v_oldOnHand DECIMAL(14, 2) DEFAULT 0;
DECLARE v_oldUnavailable DECIMAL(14, 2) DEFAULT 0;

-- Step 1: NEW.stockItemUomId is required.
IF NEW.stockItemUomId IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '入倉明細缺少庫存單位,無法入帳';
END IF;

-- Step 2: Resolve NEW bucket (itemId, stockUomId).
SELECT il.itemId, iu.uomId
INTO v_itemId, v_stockUomId
FROM `inventory_lot` il
INNER JOIN `item_uom` iu
ON iu.id = NEW.stockItemUomId
AND iu.itemId = il.itemId
AND IFNULL(iu.deleted, 0) = 0
WHERE il.id = NEW.inventoryLotId
AND IFNULL(il.deleted, 0) = 0
LIMIT 1;

IF v_itemId IS NULL OR v_stockUomId IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '此批次的庫存單位與貨品不符,請檢查貨品單位與批次';
END IF;

-- Step 3: If stock UOM or lot changed, recompute the OLD bucket so qty does not stick on the wrong row.
-- After this UPDATE, the line already belongs to NEW; OLD-bucket SUM naturally excludes it.
IF OLD.stockItemUomId IS NOT NULL
AND (OLD.stockItemUomId <> NEW.stockItemUomId
OR OLD.inventoryLotId <> NEW.inventoryLotId) THEN

SELECT il.itemId, iu.uomId
INTO v_oldItemId, v_oldStockUomId
FROM `inventory_lot` il
INNER JOIN `item_uom` iu
ON iu.id = OLD.stockItemUomId
AND iu.itemId = il.itemId
AND IFNULL(iu.deleted, 0) = 0
WHERE il.id = OLD.inventoryLotId
AND IFNULL(il.deleted, 0) = 0
LIMIT 1;

IF v_oldItemId IS NOT NULL
AND v_oldStockUomId IS NOT NULL
AND (v_oldItemId <> v_itemId OR v_oldStockUomId <> v_stockUomId) THEN

SELECT i.id
INTO v_oldInventoryId
FROM `inventory` i
WHERE i.itemId = v_oldItemId
AND i.stockUomId = v_oldStockUomId
AND IFNULL(i.deleted, 0) = 0
LIMIT 1;

IF v_oldInventoryId IS NOT NULL THEN
-- Full SUM for OLD bucket (same formulas as NEW).
SELECT COALESCE(SUM(ill.inQty - ill.outQty), 0),
COALESCE(SUM(CASE
WHEN LOWER(ill.status) = 'unavailable'
THEN ill.inQty - ill.outQty
ELSE 0 END), 0)
INTO v_oldOnHand, v_oldUnavailable
FROM `inventory_lot_line` ill
INNER JOIN `inventory_lot` il
ON il.id = ill.inventoryLotId AND IFNULL(il.deleted, 0) = 0
INNER JOIN `item_uom` iu
ON iu.id = ill.stockItemUomId AND IFNULL(iu.deleted, 0) = 0
WHERE il.itemId = v_oldItemId
AND iu.uomId = v_oldStockUomId
AND IFNULL(ill.deleted, 0) = 0;

UPDATE `inventory`
SET `onHandQty` = v_oldOnHand,
`unavailableQty` = v_oldUnavailable,
`status` = IF(v_oldOnHand - v_oldUnavailable > 0, 'available', 'unavailable'),
`modified` = CURRENT_TIMESTAMP
WHERE `id` = v_oldInventoryId;
END IF;
END IF;
END IF;

-- Step 4: Resolve base UOM snapshot (only needed when creating a new inventory row).
SELECT iu.uomId
INTO v_baseUomId
FROM `item_uom` iu
WHERE iu.itemId = v_itemId
AND iu.baseUnit = TRUE
AND IFNULL(iu.deleted, 0) = 0
LIMIT 1;

IF v_baseUomId IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '此貨品未設定基本單位,無法建立庫存';
END IF;

-- Step 5: Find or create inventory row for the NEW bucket.
SELECT i.id
INTO v_inventoryId
FROM `inventory` i
WHERE i.itemId = v_itemId
AND i.stockUomId = v_stockUomId
AND IFNULL(i.deleted, 0) = 0
LIMIT 1;

IF v_inventoryId IS NULL THEN
SELECT c.id, COALESCE(c.name, 'HKD'), COALESCE(pql.price, 0)
INTO v_currencyId, v_currencyName, v_price
FROM `inventory_lot` il
LEFT JOIN `stock_in_line` sil ON sil.id = il.stockInLineId
LEFT JOIN `purchase_order_line` pol ON pol.id = sil.purchaseOrderLineId
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
LIMIT 1;

IF v_currencyId IS NULL THEN
SELECT c.id, COALESCE(c.name, 'HKD')
INTO v_currencyId, v_currencyName
FROM `currency` c
WHERE c.code = 'HKD'
AND IFNULL(c.deleted, 0) = 0
LIMIT 1;
END IF;

IF v_currencyId IS NULL THEN
SELECT c.id, COALESCE(c.name, 'HKD')
INTO v_currencyId, v_currencyName
FROM `currency` c
WHERE IFNULL(c.deleted, 0) = 0
ORDER BY c.id
LIMIT 1;
END IF;

IF v_currencyId IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '無法取得幣別,無法建立庫存';
END IF;

INSERT INTO `inventory` (`itemId`,
`onHandQty`,
`onHoldQty`,
`unavailableQty`,
`price`,
`currencyId`,
`cpu`,
`cpuUnit`,
`cpm`,
`cpmUnit`,
`uomId`,
`stockUomId`,
`status`)
VALUES (v_itemId,
0,
0,
0,
COALESCE(v_price, 0),
v_currencyId,
0,
COALESCE(v_currencyName, 'HKD'),
0,
COALESCE(v_currencyName, 'HKD'),
v_baseUomId,
v_stockUomId,
'unavailable');

SET v_inventoryId = LAST_INSERT_ID();
END IF;

-- Step 6: Full SUM for NEW bucket + overwrite.
-- onHand = all statuses; unavailable = unavailable lines only; holdQty ignored.
SELECT COALESCE(SUM(ill.inQty - ill.outQty), 0),
COALESCE(SUM(CASE
WHEN LOWER(ill.status) = 'unavailable'
THEN ill.inQty - ill.outQty
ELSE 0 END), 0)
INTO v_onHand, v_unavailable
FROM `inventory_lot_line` ill
INNER JOIN `inventory_lot` il ON il.id = ill.inventoryLotId AND IFNULL(il.deleted, 0) = 0
INNER JOIN `item_uom` iu ON iu.id = ill.stockItemUomId AND IFNULL(iu.deleted, 0) = 0
WHERE il.itemId = v_itemId
AND iu.uomId = v_stockUomId
AND IFNULL(ill.deleted, 0) = 0;

-- Do not touch onHoldQty, uomId, or stockUomId on an existing row.
UPDATE `inventory`
SET `onHandQty` = v_onHand,
`unavailableQty` = v_unavailable,
`status` = IF(v_onHand - v_unavailable > 0, 'available', 'unavailable'),
`modified` = CURRENT_TIMESTAMP
WHERE `id` = v_inventoryId;
END;

Laddar…
Avbryt
Spara