Inventory & Warehouse Management
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:
- Validates that source and destination locators are different (either locator or ASI must differ)
- Checks for mandatory attribute set instances
- For lines with ASI=0, auto-generates
MMovementLineMArecords based on the material policy (FIFO/LIFO) - Calls
MStorageOnHand.add()to decrease inventory at the source locator - Calls
MStorageOnHand.add()to increase inventory at the destination locator - Creates
MTransactionrecords 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 andgetDefaultLocator()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 itsadd()method is called internally by document completion processes. - MMovement handles inventory transfers between locators, following the standard DocAction workflow. Each
MMovementLinespecifies 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
DateMaterialPolicyfield drives the consumption order, and the system auto-generates allocation records when no specific lot is selected. - Always validate stock availability before creating movements to avoid errors, especially when
IsDisallowNegativeInvis 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() 時,系統會:
- 驗證來源和目標儲位是否不同(儲位或屬性集實例至少有一個必須不同)
- 檢查是否有必填的屬性集實例
- 對於 ASI=0 的明細行,根據物料策略(FIFO/LIFO)自動產生
MMovementLineMA記錄 - 呼叫
MStorageOnHand.add()減少來源儲位的庫存 - 呼叫
MStorageOnHand.add()增加目標儲位的庫存 - 建立
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 是即時庫存帳冊,以儲位 + 產品 + 屬性集實例為鍵。使用其靜態查詢方法(
getQtyOnHand、getAll、getWarehouse)來檢查庫存水準,並了解其add()方法是由單據完成流程在內部呼叫的。 - MMovement 處理儲位之間的庫存移轉,遵循標準的 DocAction 工作流程。每個
MMovementLine指定產品、數量、來源儲位和目標儲位。完成時,系統會更新兩個位置的 MStorageOnHand。 - 物料策略(FIFO/LIFO)決定優先消耗哪些庫存批次。
DateMaterialPolicy欄位驅動消耗順序,當未選擇特定批次時,系統會自動產生配置記錄。 - 在建立移動單據之前,請務必驗證庫存可用量以避免錯誤,尤其是當倉庫啟用了
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() が実行されると、システムは以下を行います:
- 出庫元と入庫先のロケーターが異なることを検証する(ロケーターまたは属性セットインスタンスのいずれかが異なる必要がある)
- 必須の属性セットインスタンスを確認する
- ASI=0 の明細行に対して、マテリアルポリシー(FIFO/LIFO)に基づいて
MMovementLineMAレコードを自動生成する MStorageOnHand.add()を呼び出して、出庫元ロケーターの在庫を減少させるMStorageOnHand.add()を呼び出して、入庫先ロケーターの在庫を増加させる- 監査証跡用の
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 はリアルタイムの在庫台帳であり、ロケーター + 製品 + 属性セットインスタンスをキーとします。静的照会メソッド(
getQtyOnHand、getAll、getWarehouse)を使用して在庫レベルを確認し、add()メソッドが伝票完了処理によって内部的に呼び出されることを理解してください。 - MMovement はロケーター間の在庫移動を処理し、標準的な DocAction ワークフローに従います。各
MMovementLineは製品、数量、出庫元ロケーター、入庫先ロケーターを指定します。完了時に、システムは両方のロケーションの MStorageOnHand を更新します。 - マテリアルポリシー(FIFO/LIFO)は、どの在庫ロットが最初に消費されるかを決定します。
DateMaterialPolicyフィールドが消費順序を制御し、特定のロットが選択されていない場合、システムは配分レコードを自動生成します。 - 移動伝票を作成する前に、エラーを避けるために必ず在庫の可用性を検証してください。特に倉庫で
IsDisallowNegativeInvが有効になっている場合は重要です。
次のステップ
上級レベルでは、上級製造と BOM 管理で、複数製品の製造計画(MProductionPlan)、MProductionLineMA を使用したロット追跡、BOM 検証、およびファントム組立の展開について詳しく解説します。