Inventory & Warehouse Management

Level: Intermediate Module: Production 16 min read Lesson 25 of 47

Overview

  • What you’ll learn:
    • How iDempiere organizes warehouses and locators using MWarehouse and MLocator, including locator coordinates, priorities, and defaults
    • How to query on-hand inventory using MStorageOnHand static methods — by product, warehouse, locator, and attribute set instance
    • How to create and process inventory transfers between locators using MMovement and MMovementLine
    • How Material Policy (FIFO/LIFO) determines which inventory lots are consumed first during movements and production
  • Prerequisites: Lesson 24 — Production Overview
  • Estimated reading time: 18 minutes

Introduction

Inventory management is at the heart of any ERP system. Knowing what you have, where it is, and being able to move it efficiently between locations are fundamental capabilities that support purchasing, production, sales, and financial reporting.

iDempiere provides a comprehensive inventory management system built around four key classes: MWarehouse (warehouse master data), MLocator (bin/location within a warehouse), MStorageOnHand (real-time inventory quantities), and MMovement (inventory transfer documents). In this lesson, you’ll learn how these components work together to track and manage inventory across your organization.

Warehouse Structure: MWarehouse and MLocator

MWarehouse — The Warehouse Master

A warehouse represents a physical or logical storage location within an organization. Each warehouse belongs to a single organization and can contain multiple locators (bins).

Field Description
M_Warehouse_ID Primary key
AD_Org_ID Owning organization
Value / Name Warehouse code and description
C_Location_ID Physical address of the warehouse
Separator Character separating locator coordinates (default: *)
IsInTransit If true, this warehouse represents goods in transit between locations
IsDisallowNegativeInv If true, prevents inventory from going below zero
// Get a warehouse by ID (immutable, from cache)
MWarehouse warehouse = MWarehouse.get(ctx, warehouseId);
System.out.println("Warehouse: " + warehouse.getName());

// Get all warehouses for an organization
MWarehouse[] orgWarehouses = MWarehouse.getForOrg(ctx, orgId);
for (MWarehouse wh : orgWarehouses) {
    System.out.println("  " + wh.getValue() + " - " + wh.getName()
        + (wh.isInTransit() ? " [In Transit]" : ""));
}

// Get the default locator for a warehouse
MLocator defaultLocator = warehouse.getDefaultLocator();
System.out.println("Default locator: " + defaultLocator.getValue());

MLocator — Warehouse Bins and Locations

A locator represents a specific storage position within a warehouse. Locators use a three-dimensional coordinate system (X, Y, Z) to identify their physical location — think of it as aisle, rack, and shelf.

Field Description
M_Locator_ID Primary key
M_Warehouse_ID Parent warehouse
Value Locator code (typically auto-composed from coordinates)
X / Y / Z Three-dimensional coordinates (Aisle, Bin, Level)
IsDefault Default locator for the warehouse
PriorityNo Priority for locator selection (lower = higher priority)
// Get a locator by ID (immutable, from cache)
MLocator locator = MLocator.get(ctx, locatorId);
System.out.println("Locator: " + locator.getValue()
    + " in warehouse " + locator.getWarehouseName());

// Get or create a locator by coordinates
// If a locator with these coordinates exists, it's returned;
// otherwise a new one is created
MLocator newLocator = MLocator.get(ctx, warehouseId,
    "LOC-A1-R3", "A1", "R3", "S1");  // value, X, Y, Z
newLocator.saveEx();

// Get default locator for a warehouse (highest priority)
MLocator defaultLoc = MLocator.getDefault(warehouse);

// Get all locators for a warehouse
MLocator[] locators = warehouse.getLocators(false);  // false = use cache
for (MLocator loc : locators) {
    System.out.println("  " + loc.getValue()
        + " [" + loc.getX() + "," + loc.getY() + "," + loc.getZ() + "]"
        + (loc.isDefault() ? " (default)" : ""));
}

Inventory Tracking: MStorageOnHand

MStorageOnHand is the real-time inventory ledger. Each record represents the quantity of a specific product at a specific locator with a specific attribute set instance (lot/serial number). The composite key is: M_Locator_ID + M_Product_ID + M_AttributeSetInstance_ID.

Querying Inventory

MStorageOnHand provides numerous static methods for querying inventory quantities:

// Get total on-hand quantity for a product in a warehouse
BigDecimal qtyOnHand = MStorageOnHand.getQtyOnHand(
    productId, warehouseId, 0,  // 0 = any ASI
    trxName);
System.out.println("Total on hand: " + qtyOnHand);

// Get on-hand quantity at a specific locator
BigDecimal qtyAtLocator = MStorageOnHand.getQtyOnHandForLocator(
    productId, locatorId, 0, trxName);
System.out.println("At locator: " + qtyAtLocator);

// Get all storage records for a product at a locator (with qty > 0)
MStorageOnHand[] storage = MStorageOnHand.getAll(
    ctx, productId, locatorId, trxName);
for (MStorageOnHand soh : storage) {
    System.out.println("  ASI=" + soh.getM_AttributeSetInstance_ID()
        + " qty=" + soh.getQtyOnHand()
        + " date=" + soh.getDateMaterialPolicy());
}

// Get warehouse-wide storage sorted by material policy (FIFO or LIFO)
MStorageOnHand[] warehouseStorage = MStorageOnHand.getWarehouse(
    ctx, warehouseId, productId,
    0,        // ASI (0 = any)
    null,     // min guarantee date
    true,     // FIFO order (false = LIFO)
    true,     // positive only
    0,        // locator filter (0 = all)
    trxName);
for (MStorageOnHand soh : warehouseStorage) {
    System.out.println("  Locator=" + soh.getM_Locator_ID()
        + " qty=" + soh.getQtyOnHand());
}

Updating Inventory

The primary method for modifying on-hand quantities is MStorageOnHand.add(). This method is called internally by document completion processes (MProduction, MMovement, MInOut) — you rarely call it directly, but understanding it is important:

// Add (or subtract) inventory at a locator
// This is called internally by MProduction.completeIt(), MMovement.completeIt(), etc.
boolean success = MStorageOnHand.add(
    ctx,
    locatorId,                    // where to add/subtract
    productId,                    // which product
    attributeSetInstanceId,       // which lot/serial (0 = no tracking)
    qty,                          // positive to add, negative to subtract
    dateMaterialPolicy,           // FIFO/LIFO date
    trxName);

// Note: If the warehouse has IsDisallowNegativeInv = true and the
// subtraction would result in negative inventory, this throws
// NegativeInventoryDisallowedException

Inventory Movement: MMovement

When you need to transfer inventory between locators — whether within the same warehouse or across different warehouses — you create an MMovement document. Like MProduction, MMovement implements DocAction and follows the standard document workflow.

MMovement Structure

Field Description
M_Movement_ID Primary key
DocumentNo Auto-generated document number
MovementDate Date of the inventory transfer
DocStatus / DocAction Document workflow state
IsApproved Whether the movement has been approved
IsInTransit Whether goods are currently in transit

MMovementLine — Transfer Line Items

Field Description
M_Movement_ID Parent movement document
M_Product_ID Product being transferred
MovementQty Quantity to move
M_Locator_ID Source locator (move FROM)
M_LocatorTo_ID Destination locator (move TO)
M_AttributeSetInstance_ID Source lot/serial (if applicable)
M_AttributeSetInstanceTo_ID Destination lot/serial

Creating and Completing a Movement

// Step 1: Check source inventory
BigDecimal available = MStorageOnHand.getQtyOnHandForLocator(
    productId, fromLocatorId, 0, trxName);
BigDecimal transferQty = new BigDecimal("50");

if (available.compareTo(transferQty) < 0) {
    throw new AdempiereException("Insufficient stock: "
        + available + " available, " + transferQty + " required");
}

// Step 2: Create movement header
MMovement movement = new MMovement(ctx, 0, trxName);
movement.setAD_Org_ID(orgId);
movement.setMovementDate(new Timestamp(System.currentTimeMillis()));
movement.saveEx();

// Step 3: Create movement line
MMovementLine line = new MMovementLine(movement);
line.setM_Product_ID(productId);
line.setMovementQty(transferQty);
line.setM_Locator_ID(fromLocatorId);       // source
line.setM_LocatorTo_ID(toLocatorId);       // destination
line.saveEx();

// Step 4: Complete the movement
if (!movement.processIt(DocAction.ACTION_Complete)) {
    throw new AdempiereException("Movement failed: "
        + movement.getProcessMsg());
}
movement.saveEx();

System.out.println("Movement " + movement.getDocumentNo() + " completed");

// Step 5: Verify inventory updated
BigDecimal newSourceQty = MStorageOnHand.getQtyOnHandForLocator(
    productId, fromLocatorId, 0, trxName);
BigDecimal newDestQty = MStorageOnHand.getQtyOnHandForLocator(
    productId, toLocatorId, 0, trxName);

System.out.println("Source locator: " + newSourceQty);
System.out.println("Destination locator: " + newDestQty);

What Happens During Movement Completion

When completeIt() runs on an MMovement, the system:

  1. Validates that source and destination locators are different (either locator or ASI must differ)
  2. Checks for mandatory attribute set instances
  3. For lines with ASI=0, auto-generates MMovementLineMA records based on the material policy (FIFO/LIFO)
  4. Calls MStorageOnHand.add() to decrease inventory at the source locator
  5. Calls MStorageOnHand.add() to increase inventory at the destination locator
  6. Creates MTransaction records for the audit trail (MovementType: M+ for receipt, M- for shipment)

Material Policy: FIFO and LIFO

When consuming inventory — whether through production or movement — the system needs to know which specific inventory to consume. This is determined by the product's Material Policy, configured at the product category level.

FIFO (First In, First Out)

The oldest inventory (earliest DateMaterialPolicy) is consumed first. This is typical for perishable goods and is the most common policy. When querying storage with FIFO, records are sorted by DateMaterialPolicy ascending.

LIFO (Last In, First Out)

The newest inventory (latest DateMaterialPolicy) is consumed first. Less common, but used in some industries for tax optimization. Records are sorted by DateMaterialPolicy descending.

How Material Policy Affects Movements

// When a movement line has ASI = 0 (no specific lot selected),
// the system auto-allocates from available storage based on material policy.
//
// Example: Product has 3 storage records at the source locator:
//   Lot A: 30 units, received 2026-01-15
//   Lot B: 50 units, received 2026-02-01
//   Lot C: 20 units, received 2026-02-15
//
// Moving 60 units with FIFO policy:
//   - 30 units from Lot A (oldest)
//   - 30 units from Lot B (next oldest)
//
// Moving 60 units with LIFO policy:
//   - 20 units from Lot C (newest)
//   - 40 units from Lot B (next newest)

// Query storage in FIFO order
MStorageOnHand[] fifoStorage = MStorageOnHand.getWarehouse(
    ctx, warehouseId, productId,
    0,        // ASI
    null,     // guarantee date
    true,     // FiFo = true
    true,     // positive only
    0,        // locator filter
    trxName);

// Query storage in LIFO order
MStorageOnHand[] lifoStorage = MStorageOnHand.getWarehouse(
    ctx, warehouseId, productId,
    0, null,
    false,    // FiFo = false (LIFO)
    true, 0, trxName);

Key Takeaways

  • MWarehouse represents physical storage facilities, each containing multiple MLocator bins identified by X/Y/Z coordinates. Use MWarehouse.get() for cached access and getDefaultLocator() for the primary receiving location.
  • MStorageOnHand is the real-time inventory ledger, keyed by Locator + Product + ASI. Use its static query methods (getQtyOnHand, getAll, getWarehouse) to check stock levels, and understand that its add() method is called internally by document completion processes.
  • MMovement handles inventory transfers between locators, following the standard DocAction workflow. Each MMovementLine specifies a product, quantity, source locator, and destination locator. On completion, the system updates MStorageOnHand for both locations.
  • Material Policy (FIFO/LIFO) determines which inventory lots are consumed first. The DateMaterialPolicy field drives the consumption order, and the system auto-generates allocation records when no specific lot is selected. Note that FIFO and LIFO also serve as costing methods in the Accounting Schema — the material policy and costing method work together to determine both consumption order and cost valuation.
  • Always validate stock availability before creating movements to avoid errors, especially when IsDisallowNegativeInv is enabled on the warehouse.

What's Next

In the Advanced level, Advanced Production & BOM Management covers multi-product production plans (MProductionPlan), lot tracking with MProductionLineMA, BOM verification, and phantom assembly expansion in depth.

繁體中文

概述

  • 您將學到:
    • iDempiere 如何使用 MWarehouse 和 MLocator 組織倉庫和儲位,包括儲位座標、優先順序和預設值
    • 如何使用 MStorageOnHand 靜態方法查詢現有庫存 — 依產品、倉庫、儲位和屬性集實例進行查詢
    • 如何使用 MMovement 和 MMovementLine 建立和處理儲位之間的庫存移動
    • 物料策略(FIFO/LIFO)如何決定在移動和生產過程中優先消耗哪些庫存批次
  • 先修課程:第 24 課 — 生產概述
  • 預估閱讀時間:18 分鐘

簡介

庫存管理是任何 ERP 系統的核心。了解您擁有什麼、它在哪裡,以及能夠高效地在不同位置之間移動庫存,是支援採購、生產、銷售和財務報告的基本功能。

iDempiere 提供了一套完整的庫存管理系統,建構在四個關鍵類別之上:MWarehouse(倉庫主資料)、MLocator(倉庫內的儲位/位置)、MStorageOnHand(即時庫存數量)和 MMovement(庫存移動單據)。在本課程中,您將學習這些元件如何協同運作,以追蹤和管理整個組織的庫存。

倉庫結構:MWarehouse 和 MLocator

MWarehouse — 倉庫主檔

倉庫代表組織內的實體或邏輯儲存位置。每個倉庫隸屬於單一組織,並且可以包含多個儲位(料架)。

欄位 說明
M_Warehouse_ID 主鍵
AD_Org_ID 所屬組織
Value / Name 倉庫代碼與描述
C_Location_ID 倉庫的實體地址
Separator 分隔儲位座標的字元(預設:*)
IsInTransit 若為 true,此倉庫代表在不同位置之間運輸中的貨物
IsDisallowNegativeInv 若為 true,防止庫存降至零以下
// Get a warehouse by ID (immutable, from cache)
MWarehouse warehouse = MWarehouse.get(ctx, warehouseId);
System.out.println("Warehouse: " + warehouse.getName());

// Get all warehouses for an organization
MWarehouse[] orgWarehouses = MWarehouse.getForOrg(ctx, orgId);
for (MWarehouse wh : orgWarehouses) {
    System.out.println("  " + wh.getValue() + " - " + wh.getName()
        + (wh.isInTransit() ? " [In Transit]" : ""));
}

// Get the default locator for a warehouse
MLocator defaultLocator = warehouse.getDefaultLocator();
System.out.println("Default locator: " + defaultLocator.getValue());

MLocator — 倉庫儲位與位置

儲位代表倉庫內的特定儲存位置。儲位使用三維座標系統(X、Y、Z)來識別其實體位置 — 可以想像成走道、料架和層架。

欄位 說明
M_Locator_ID 主鍵
M_Warehouse_ID 上層倉庫
Value 儲位代碼(通常由座標自動組合)
X / Y / Z 三維座標(走道、料架、層架)
IsDefault 倉庫的預設儲位
PriorityNo 儲位選擇的優先順序(數值越小,優先順序越高)
// Get a locator by ID (immutable, from cache)
MLocator locator = MLocator.get(ctx, locatorId);
System.out.println("Locator: " + locator.getValue()
    + " in warehouse " + locator.getWarehouseName());

// Get or create a locator by coordinates
// If a locator with these coordinates exists, it's returned;
// otherwise a new one is created
MLocator newLocator = MLocator.get(ctx, warehouseId,
    "LOC-A1-R3", "A1", "R3", "S1");  // value, X, Y, Z
newLocator.saveEx();

// Get default locator for a warehouse (highest priority)
MLocator defaultLoc = MLocator.getDefault(warehouse);

// Get all locators for a warehouse
MLocator[] locators = warehouse.getLocators(false);  // false = use cache
for (MLocator loc : locators) {
    System.out.println("  " + loc.getValue()
        + " [" + loc.getX() + "," + loc.getY() + "," + loc.getZ() + "]"
        + (loc.isDefault() ? " (default)" : ""));
}

庫存追蹤:MStorageOnHand

MStorageOnHand 是即時庫存帳冊。每筆記錄代表特定產品在特定儲位、特定屬性集實例(批號/序號)下的數量。複合鍵為:M_Locator_ID + M_Product_ID + M_AttributeSetInstance_ID

查詢庫存

MStorageOnHand 提供了多種靜態方法來查詢庫存數量:

// Get total on-hand quantity for a product in a warehouse
BigDecimal qtyOnHand = MStorageOnHand.getQtyOnHand(
    productId, warehouseId, 0,  // 0 = any ASI
    trxName);
System.out.println("Total on hand: " + qtyOnHand);

// Get on-hand quantity at a specific locator
BigDecimal qtyAtLocator = MStorageOnHand.getQtyOnHandForLocator(
    productId, locatorId, 0, trxName);
System.out.println("At locator: " + qtyAtLocator);

// Get all storage records for a product at a locator (with qty > 0)
MStorageOnHand[] storage = MStorageOnHand.getAll(
    ctx, productId, locatorId, trxName);
for (MStorageOnHand soh : storage) {
    System.out.println("  ASI=" + soh.getM_AttributeSetInstance_ID()
        + " qty=" + soh.getQtyOnHand()
        + " date=" + soh.getDateMaterialPolicy());
}

// Get warehouse-wide storage sorted by material policy (FIFO or LIFO)
MStorageOnHand[] warehouseStorage = MStorageOnHand.getWarehouse(
    ctx, warehouseId, productId,
    0,        // ASI (0 = any)
    null,     // min guarantee date
    true,     // FIFO order (false = LIFO)
    true,     // positive only
    0,        // locator filter (0 = all)
    trxName);
for (MStorageOnHand soh : warehouseStorage) {
    System.out.println("  Locator=" + soh.getM_Locator_ID()
        + " qty=" + soh.getQtyOnHand());
}

更新庫存

修改現有庫存數量的主要方法是 MStorageOnHand.add()。此方法由單據完成流程(MProduction、MMovement、MInOut)在內部呼叫 — 您很少會直接呼叫它,但了解其運作方式很重要:

// Add (or subtract) inventory at a locator
// This is called internally by MProduction.completeIt(), MMovement.completeIt(), etc.
boolean success = MStorageOnHand.add(
    ctx,
    locatorId,                    // where to add/subtract
    productId,                    // which product
    attributeSetInstanceId,       // which lot/serial (0 = no tracking)
    qty,                          // positive to add, negative to subtract
    dateMaterialPolicy,           // FIFO/LIFO date
    trxName);

// Note: If the warehouse has IsDisallowNegativeInv = true and the
// subtraction would result in negative inventory, this throws
// NegativeInventoryDisallowedException

庫存移動:MMovement

當您需要在儲位之間移轉庫存 — 無論是在同一倉庫內還是跨不同倉庫 — 您需要建立一份 MMovement 單據。與 MProduction 一樣,MMovement 實作了 DocAction 介面,並遵循標準的單據工作流程。

MMovement 結構

欄位 說明
M_Movement_ID 主鍵
DocumentNo 自動產生的單據編號
MovementDate 庫存移動日期
DocStatus / DocAction 單據工作流程狀態
IsApproved 移動是否已核准
IsInTransit 貨物是否在運輸途中

MMovementLine — 移動明細行

欄位 說明
M_Movement_ID 上層移動單據
M_Product_ID 被移轉的產品
MovementQty 移動數量
M_Locator_ID 來源儲位(移出)
M_LocatorTo_ID 目標儲位(移入)
M_AttributeSetInstance_ID 來源批號/序號(如適用)
M_AttributeSetInstanceTo_ID 目標批號/序號

建立與完成移動單據

// Step 1: Check source inventory
BigDecimal available = MStorageOnHand.getQtyOnHandForLocator(
    productId, fromLocatorId, 0, trxName);
BigDecimal transferQty = new BigDecimal("50");

if (available.compareTo(transferQty) < 0) {
    throw new AdempiereException("Insufficient stock: "
        + available + " available, " + transferQty + " required");
}

// Step 2: Create movement header
MMovement movement = new MMovement(ctx, 0, trxName);
movement.setAD_Org_ID(orgId);
movement.setMovementDate(new Timestamp(System.currentTimeMillis()));
movement.saveEx();

// Step 3: Create movement line
MMovementLine line = new MMovementLine(movement);
line.setM_Product_ID(productId);
line.setMovementQty(transferQty);
line.setM_Locator_ID(fromLocatorId);       // source
line.setM_LocatorTo_ID(toLocatorId);       // destination
line.saveEx();

// Step 4: Complete the movement
if (!movement.processIt(DocAction.ACTION_Complete)) {
    throw new AdempiereException("Movement failed: "
        + movement.getProcessMsg());
}
movement.saveEx();

System.out.println("Movement " + movement.getDocumentNo() + " completed");

// Step 5: Verify inventory updated
BigDecimal newSourceQty = MStorageOnHand.getQtyOnHandForLocator(
    productId, fromLocatorId, 0, trxName);
BigDecimal newDestQty = MStorageOnHand.getQtyOnHandForLocator(
    productId, toLocatorId, 0, trxName);

System.out.println("Source locator: " + newSourceQty);
System.out.println("Destination locator: " + newDestQty);

移動單據完成時的處理流程

當 MMovement 執行 completeIt() 時,系統會:

  1. 驗證來源和目標儲位是否不同(儲位或屬性集實例至少有一個必須不同)
  2. 檢查是否有必填的屬性集實例
  3. 對於 ASI=0 的明細行,根據物料策略(FIFO/LIFO)自動產生 MMovementLineMA 記錄
  4. 呼叫 MStorageOnHand.add() 減少來源儲位的庫存
  5. 呼叫 MStorageOnHand.add() 增加目標儲位的庫存
  6. 建立 MTransaction 記錄以供稽核追蹤(MovementType:M+ 代表入庫,M- 代表出庫)

物料策略:FIFO 和 LIFO

在消耗庫存時 — 無論是透過生產還是移動 — 系統需要知道要消耗哪些特定庫存。這是由產品的物料策略決定的,該策略在產品類別層級進行設定。

FIFO(先進先出)

最早的庫存(最早的 DateMaterialPolicy)會最先被消耗。這適用於易腐壞的商品,也是最常見的策略。使用 FIFO 查詢庫存時,記錄按 DateMaterialPolicy 升冪排序。

LIFO(後進先出)

最新的庫存(最晚的 DateMaterialPolicy)會最先被消耗。較不常見,但在某些行業中用於稅務最佳化。記錄按 DateMaterialPolicy 降冪排序。

物料策略對庫存移動的影響

// When a movement line has ASI = 0 (no specific lot selected),
// the system auto-allocates from available storage based on material policy.
//
// Example: Product has 3 storage records at the source locator:
//   Lot A: 30 units, received 2026-01-15
//   Lot B: 50 units, received 2026-02-01
//   Lot C: 20 units, received 2026-02-15
//
// Moving 60 units with FIFO policy:
//   - 30 units from Lot A (oldest)
//   - 30 units from Lot B (next oldest)
//
// Moving 60 units with LIFO policy:
//   - 20 units from Lot C (newest)
//   - 40 units from Lot B (next newest)

// Query storage in FIFO order
MStorageOnHand[] fifoStorage = MStorageOnHand.getWarehouse(
    ctx, warehouseId, productId,
    0,        // ASI
    null,     // guarantee date
    true,     // FiFo = true
    true,     // positive only
    0,        // locator filter
    trxName);

// Query storage in LIFO order
MStorageOnHand[] lifoStorage = MStorageOnHand.getWarehouse(
    ctx, warehouseId, productId,
    0, null,
    false,    // FiFo = false (LIFO)
    true, 0, trxName);

重點摘要

  • MWarehouse 代表實體儲存設施,每個倉庫包含多個由 X/Y/Z 座標識別的 MLocator 儲位。使用 MWarehouse.get() 進行快取存取,使用 getDefaultLocator() 取得主要收貨位置。
  • MStorageOnHand 是即時庫存帳冊,以儲位 + 產品 + 屬性集實例為鍵。使用其靜態查詢方法(getQtyOnHandgetAllgetWarehouse)來檢查庫存水準,並了解其 add() 方法是由單據完成流程在內部呼叫的。
  • MMovement 處理儲位之間的庫存移轉,遵循標準的 DocAction 工作流程。每個 MMovementLine 指定產品、數量、來源儲位和目標儲位。完成時,系統會更新兩個位置的 MStorageOnHand。
  • 物料策略(FIFO/LIFO)決定優先消耗哪些庫存批次。DateMaterialPolicy 欄位驅動消耗順序,當未選擇特定批次時,系統會自動產生配置記錄。注意:FIFO 和 LIFO 在會計架構中也作為成本計算方法使用——物料策略與成本方法共同決定消耗順序和成本估值。
  • 在建立移動單據之前,請務必驗證庫存可用量以避免錯誤,尤其是當倉庫啟用了 IsDisallowNegativeInv 時。

下一步

在進階課程中,進階生產與 BOM 管理將深入介紹多產品生產計畫(MProductionPlan)、使用 MProductionLineMA 進行批次追蹤、BOM 驗證,以及虛擬組裝展開等內容。

日本語

概要

  • 学習内容:
    • iDempiere が MWarehouse と MLocator を使用して倉庫とロケーターをどのように構成するか(ロケーター座標、優先度、デフォルト値を含む)
    • MStorageOnHand の静的メソッドを使用して在庫を照会する方法 — 製品、倉庫、ロケーター、属性セットインスタンス別
    • MMovement と MMovementLine を使用して、ロケーター間の在庫移動を作成・処理する方法
    • マテリアルポリシー(FIFO/LIFO)が、移動や製造時にどの在庫ロットから優先的に消費されるかをどのように決定するか
  • 前提条件:レッスン 24 — 製造の概要
  • 推定読了時間:18 分

はじめに

在庫管理はあらゆる ERP システムの中核です。何を保有しているか、それがどこにあるか、そして拠点間で効率的に移動できること — これらは購買、製造、販売、財務報告を支える基本的な機能です。

iDempiere は、4 つの主要クラスを基盤とした包括的な在庫管理システムを提供します:MWarehouse(倉庫マスターデータ)、MLocator(倉庫内のビン/ロケーション)、MStorageOnHand(リアルタイム在庫数量)、および MMovement(在庫移動伝票)。このレッスンでは、これらのコンポーネントがどのように連携して、組織全体の在庫を追跡・管理するかを学びます。

倉庫構造:MWarehouse と MLocator

MWarehouse — 倉庫マスター

倉庫は、組織内の物理的または論理的な保管場所を表します。各倉庫は単一の組織に属し、複数のロケーター(ビン)を含むことができます。

フィールド 説明
M_Warehouse_ID 主キー
AD_Org_ID 所属組織
Value / Name 倉庫コードと説明
C_Location_ID 倉庫の物理的な住所
Separator ロケーター座標を区切る文字(デフォルト:*)
IsInTransit true の場合、この倉庫は拠点間で輸送中の商品を表す
IsDisallowNegativeInv true の場合、在庫がゼロ以下になることを防止する
// Get a warehouse by ID (immutable, from cache)
MWarehouse warehouse = MWarehouse.get(ctx, warehouseId);
System.out.println("Warehouse: " + warehouse.getName());

// Get all warehouses for an organization
MWarehouse[] orgWarehouses = MWarehouse.getForOrg(ctx, orgId);
for (MWarehouse wh : orgWarehouses) {
    System.out.println("  " + wh.getValue() + " - " + wh.getName()
        + (wh.isInTransit() ? " [In Transit]" : ""));
}

// Get the default locator for a warehouse
MLocator defaultLocator = warehouse.getDefaultLocator();
System.out.println("Default locator: " + defaultLocator.getValue());

MLocator — 倉庫のビンとロケーション

ロケーターは、倉庫内の特定の保管位置を表します。ロケーターは 3 次元座標系(X、Y、Z)を使用して物理的な位置を識別します — 通路、棚、段と考えてください。

フィールド 説明
M_Locator_ID 主キー
M_Warehouse_ID 親倉庫
Value ロケーターコード(通常は座標から自動生成)
X / Y / Z 3 次元座標(通路、ビン、レベル)
IsDefault 倉庫のデフォルトロケーター
PriorityNo ロケーター選択の優先度(小さいほど優先度が高い)
// Get a locator by ID (immutable, from cache)
MLocator locator = MLocator.get(ctx, locatorId);
System.out.println("Locator: " + locator.getValue()
    + " in warehouse " + locator.getWarehouseName());

// Get or create a locator by coordinates
// If a locator with these coordinates exists, it's returned;
// otherwise a new one is created
MLocator newLocator = MLocator.get(ctx, warehouseId,
    "LOC-A1-R3", "A1", "R3", "S1");  // value, X, Y, Z
newLocator.saveEx();

// Get default locator for a warehouse (highest priority)
MLocator defaultLoc = MLocator.getDefault(warehouse);

// Get all locators for a warehouse
MLocator[] locators = warehouse.getLocators(false);  // false = use cache
for (MLocator loc : locators) {
    System.out.println("  " + loc.getValue()
        + " [" + loc.getX() + "," + loc.getY() + "," + loc.getZ() + "]"
        + (loc.isDefault() ? " (default)" : ""));
}

在庫追跡:MStorageOnHand

MStorageOnHand はリアルタイムの在庫台帳です。各レコードは、特定のロケーターにおける特定の製品の、特定の属性セットインスタンス(ロット/シリアル番号)ごとの数量を表します。複合キーは:M_Locator_ID + M_Product_ID + M_AttributeSetInstance_ID です。

在庫の照会

MStorageOnHand は、在庫数量を照会するための多数の静的メソッドを提供します:

// Get total on-hand quantity for a product in a warehouse
BigDecimal qtyOnHand = MStorageOnHand.getQtyOnHand(
    productId, warehouseId, 0,  // 0 = any ASI
    trxName);
System.out.println("Total on hand: " + qtyOnHand);

// Get on-hand quantity at a specific locator
BigDecimal qtyAtLocator = MStorageOnHand.getQtyOnHandForLocator(
    productId, locatorId, 0, trxName);
System.out.println("At locator: " + qtyAtLocator);

// Get all storage records for a product at a locator (with qty > 0)
MStorageOnHand[] storage = MStorageOnHand.getAll(
    ctx, productId, locatorId, trxName);
for (MStorageOnHand soh : storage) {
    System.out.println("  ASI=" + soh.getM_AttributeSetInstance_ID()
        + " qty=" + soh.getQtyOnHand()
        + " date=" + soh.getDateMaterialPolicy());
}

// Get warehouse-wide storage sorted by material policy (FIFO or LIFO)
MStorageOnHand[] warehouseStorage = MStorageOnHand.getWarehouse(
    ctx, warehouseId, productId,
    0,        // ASI (0 = any)
    null,     // min guarantee date
    true,     // FIFO order (false = LIFO)
    true,     // positive only
    0,        // locator filter (0 = all)
    trxName);
for (MStorageOnHand soh : warehouseStorage) {
    System.out.println("  Locator=" + soh.getM_Locator_ID()
        + " qty=" + soh.getQtyOnHand());
}

在庫の更新

在庫数量を変更する主要なメソッドは MStorageOnHand.add() です。このメソッドは伝票完了処理(MProduction、MMovement、MInOut)によって内部的に呼び出されます — 直接呼び出すことはほとんどありませんが、その仕組みを理解することは重要です:

// Add (or subtract) inventory at a locator
// This is called internally by MProduction.completeIt(), MMovement.completeIt(), etc.
boolean success = MStorageOnHand.add(
    ctx,
    locatorId,                    // where to add/subtract
    productId,                    // which product
    attributeSetInstanceId,       // which lot/serial (0 = no tracking)
    qty,                          // positive to add, negative to subtract
    dateMaterialPolicy,           // FIFO/LIFO date
    trxName);

// Note: If the warehouse has IsDisallowNegativeInv = true and the
// subtraction would result in negative inventory, this throws
// NegativeInventoryDisallowedException

在庫移動:MMovement

ロケーター間で在庫を移動する必要がある場合 — 同一倉庫内であっても異なる倉庫間であっても — MMovement 伝票を作成します。MProduction と同様に、MMovement は DocAction を実装し、標準的な伝票ワークフローに従います。

MMovement の構造

フィールド 説明
M_Movement_ID 主キー
DocumentNo 自動採番される伝票番号
MovementDate 在庫移動の日付
DocStatus / DocAction 伝票ワークフローの状態
IsApproved 移動が承認済みかどうか
IsInTransit 商品が現在輸送中かどうか

MMovementLine — 移動明細行

フィールド 説明
M_Movement_ID 親の移動伝票
M_Product_ID 移動する製品
MovementQty 移動数量
M_Locator_ID 出庫元ロケーター(移動元)
M_LocatorTo_ID 入庫先ロケーター(移動先)
M_AttributeSetInstance_ID 出庫元のロット/シリアル(該当する場合)
M_AttributeSetInstanceTo_ID 入庫先のロット/シリアル

移動伝票の作成と完了

// Step 1: Check source inventory
BigDecimal available = MStorageOnHand.getQtyOnHandForLocator(
    productId, fromLocatorId, 0, trxName);
BigDecimal transferQty = new BigDecimal("50");

if (available.compareTo(transferQty) < 0) {
    throw new AdempiereException("Insufficient stock: "
        + available + " available, " + transferQty + " required");
}

// Step 2: Create movement header
MMovement movement = new MMovement(ctx, 0, trxName);
movement.setAD_Org_ID(orgId);
movement.setMovementDate(new Timestamp(System.currentTimeMillis()));
movement.saveEx();

// Step 3: Create movement line
MMovementLine line = new MMovementLine(movement);
line.setM_Product_ID(productId);
line.setMovementQty(transferQty);
line.setM_Locator_ID(fromLocatorId);       // source
line.setM_LocatorTo_ID(toLocatorId);       // destination
line.saveEx();

// Step 4: Complete the movement
if (!movement.processIt(DocAction.ACTION_Complete)) {
    throw new AdempiereException("Movement failed: "
        + movement.getProcessMsg());
}
movement.saveEx();

System.out.println("Movement " + movement.getDocumentNo() + " completed");

// Step 5: Verify inventory updated
BigDecimal newSourceQty = MStorageOnHand.getQtyOnHandForLocator(
    productId, fromLocatorId, 0, trxName);
BigDecimal newDestQty = MStorageOnHand.getQtyOnHandForLocator(
    productId, toLocatorId, 0, trxName);

System.out.println("Source locator: " + newSourceQty);
System.out.println("Destination locator: " + newDestQty);

移動伝票の完了時に行われる処理

MMovement で completeIt() が実行されると、システムは以下を行います:

  1. 出庫元と入庫先のロケーターが異なることを検証する(ロケーターまたは属性セットインスタンスのいずれかが異なる必要がある)
  2. 必須の属性セットインスタンスを確認する
  3. ASI=0 の明細行に対して、マテリアルポリシー(FIFO/LIFO)に基づいて MMovementLineMA レコードを自動生成する
  4. MStorageOnHand.add() を呼び出して、出庫元ロケーターの在庫を減少させる
  5. MStorageOnHand.add() を呼び出して、入庫先ロケーターの在庫を増加させる
  6. 監査証跡用の MTransaction レコードを作成する(MovementType:M+ は入庫、M- は出庫)

マテリアルポリシー:FIFO と LIFO

在庫を消費する際 — 製造であれ移動であれ — システムはどの在庫を消費するかを判断する必要があります。これは製品のマテリアルポリシーによって決定され、製品カテゴリレベルで設定されます。

FIFO(先入先出)

最も古い在庫(最も早い DateMaterialPolicy)が最初に消費されます。これは生鮮品に適しており、最も一般的なポリシーです。FIFO で在庫を照会する場合、レコードは DateMaterialPolicy の昇順でソートされます。

LIFO(後入先出)

最も新しい在庫(最も遅い DateMaterialPolicy)が最初に消費されます。あまり一般的ではありませんが、一部の業界では税務最適化のために使用されます。レコードは DateMaterialPolicy の降順でソートされます。

マテリアルポリシーが在庫移動に与える影響

// When a movement line has ASI = 0 (no specific lot selected),
// the system auto-allocates from available storage based on material policy.
//
// Example: Product has 3 storage records at the source locator:
//   Lot A: 30 units, received 2026-01-15
//   Lot B: 50 units, received 2026-02-01
//   Lot C: 20 units, received 2026-02-15
//
// Moving 60 units with FIFO policy:
//   - 30 units from Lot A (oldest)
//   - 30 units from Lot B (next oldest)
//
// Moving 60 units with LIFO policy:
//   - 20 units from Lot C (newest)
//   - 40 units from Lot B (next newest)

// Query storage in FIFO order
MStorageOnHand[] fifoStorage = MStorageOnHand.getWarehouse(
    ctx, warehouseId, productId,
    0,        // ASI
    null,     // guarantee date
    true,     // FiFo = true
    true,     // positive only
    0,        // locator filter
    trxName);

// Query storage in LIFO order
MStorageOnHand[] lifoStorage = MStorageOnHand.getWarehouse(
    ctx, warehouseId, productId,
    0, null,
    false,    // FiFo = false (LIFO)
    true, 0, trxName);

重要なポイント

  • MWarehouse は物理的な保管施設を表し、それぞれ X/Y/Z 座標で識別される複数の MLocator ビンを含みます。MWarehouse.get() でキャッシュからアクセスし、getDefaultLocator() で主要な入庫場所を取得します。
  • MStorageOnHand はリアルタイムの在庫台帳であり、ロケーター + 製品 + 属性セットインスタンスをキーとします。静的照会メソッド(getQtyOnHandgetAllgetWarehouse)を使用して在庫レベルを確認し、add() メソッドが伝票完了処理によって内部的に呼び出されることを理解してください。
  • MMovement はロケーター間の在庫移動を処理し、標準的な DocAction ワークフローに従います。各 MMovementLine は製品、数量、出庫元ロケーター、入庫先ロケーターを指定します。完了時に、システムは両方のロケーションの MStorageOnHand を更新します。
  • マテリアルポリシー(FIFO/LIFO)は、どの在庫ロットが最初に消費されるかを決定します。DateMaterialPolicy フィールドが消費順序を制御し、特定のロットが選択されていない場合、システムは配分レコードを自動生成します。注意:FIFO と LIFO は会計スキーマにおける原価計算方法としても使用されます——マテリアルポリシーと原価計算方法が連携して、消費順序と原価評価の両方を決定します。
  • 移動伝票を作成する前に、エラーを避けるために必ず在庫の可用性を検証してください。特に倉庫で IsDisallowNegativeInv が有効になっている場合は重要です。

次のステップ

上級レベルでは、上級製造と BOM 管理で、複数製品の製造計画(MProductionPlan)、MProductionLineMA を使用したロット追跡、BOM 検証、およびファントム組立の展開について詳しく解説します。

You Missed