Inventory API¶
Manage stock pickings (WH-IN/WH-OUT) and inventory operations via the REST API.
Overview¶
Stock Pickings (Transfers)¶
| Operation | Method | Endpoint |
|---|---|---|
| Create | POST | /restapi/1.0/object/stock.picking |
| List/Search | GET | /restapi/1.0/object/stock.picking |
| Read | GET | /restapi/1.0/object/stock.picking/{id} |
| Update | PUT | /restapi/1.0/object/stock.picking/{id} |
Stock Moves¶
| Operation | Method | Endpoint |
|---|---|---|
| List/Search | GET | /restapi/1.0/object/stock.move |
| Read | GET | /restapi/1.0/object/stock.move/{id} |
| Update | PUT | /restapi/1.0/object/stock.move/{id} |
Authentication¶
Stock Picking Fields¶
Core Fields¶
| Field | Type | Description |
|---|---|---|
name |
String | Transfer reference (WH/IN/00001) |
partner_id |
Integer | Partner (supplier/customer) |
picking_type_id |
Integer | Operation type (receipts, deliveries) |
location_id |
Integer | Source location |
location_dest_id |
Integer | Destination location |
scheduled_date |
Datetime | Scheduled date |
date_done |
Datetime | Completion date |
origin |
String | Source document |
state |
String | Picking state |
sale_id |
Integer | Linked sale order |
purchase_id |
Integer | Linked purchase order |
Picking States¶
| State | Label | Description |
|---|---|---|
draft |
Draft | Not confirmed |
waiting |
Waiting Another Operation | Waiting for other |
confirmed |
Waiting | Waiting for stock |
assigned |
Ready | Stock reserved, ready to process |
done |
Done | Completed |
cancel |
Cancelled | Cancelled |
Picking Types (JDX)¶
| Type | Code | Description |
|---|---|---|
| Receipts | WH/IN | Incoming goods (WH-IN) |
| Delivery Orders | WH/OUT | Outgoing goods (WH-OUT) |
| Internal Transfers | WH/INT | Internal movements |
Stock Move Fields¶
| Field | Type | Description |
|---|---|---|
product_id |
Integer | Product ID |
product_uom_qty |
Float | Demand quantity |
quantity_done |
Float | Done quantity |
reserved_availability |
Float | Reserved quantity |
state |
String | Move state |
picking_id |
Integer | Parent picking |
API Examples¶
Get Ready Receipts (WH-IN)¶
# Get pickings ready to receive
curl -X GET "https://your-odoo.com/restapi/1.0/object/stock.picking" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "domain=[('picking_type_id.code','=','incoming'),('state','=','assigned')]" \
--data-urlencode "fields=['name','partner_id','scheduled_date','origin','sale_id']" \
--data-urlencode "order=scheduled_date"
Get Ready Deliveries (WH-OUT)¶
# Get pickings ready to deliver
curl -X GET "https://your-odoo.com/restapi/1.0/object/stock.picking" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "domain=[('picking_type_id.code','=','outgoing'),('state','=','assigned')]" \
--data-urlencode "fields=['name','partner_id','scheduled_date','origin','sale_id']" \
--data-urlencode "order=scheduled_date"
Get Picking Detail¶
curl -X GET "https://your-odoo.com/restapi/1.0/object/stock.picking/123" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "fields=['name','partner_id','picking_type_id','scheduled_date','date_done','state','origin','sale_id','move_ids_without_package']"
Get Picking Moves¶
curl -X GET "https://your-odoo.com/restapi/1.0/object/stock.move" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "domain=[('picking_id','=',123)]" \
--data-urlencode "fields=['product_id','product_uom_qty','quantity_done','reserved_availability','state']"
Update Move Quantity Done¶
# Set quantity done on a move
curl -X PUT "https://your-odoo.com/restapi/1.0/object/stock.move/456" \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"quantity_done": 5.0
}'
Get Pickings by Sale Order¶
curl -X GET "https://your-odoo.com/restapi/1.0/object/stock.picking" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "domain=[('sale_id','=',789)]" \
--data-urlencode "fields=['name','picking_type_id','state','scheduled_date']"
Get Pickings by Date Range¶
curl -X GET "https://your-odoo.com/restapi/1.0/object/stock.picking" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "domain=[('scheduled_date','>=','2025-12-01'),('scheduled_date','<=','2025-12-31'),('state','=','assigned')]" \
--data-urlencode "fields=['name','partner_id','picking_type_id','scheduled_date']"
Related Models¶
Picking Types¶
curl -X GET "https://your-odoo.com/restapi/1.0/object/stock.picking.type" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "fields=['name','code','sequence_code']"
Stock Locations¶
curl -X GET "https://your-odoo.com/restapi/1.0/object/stock.location" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "domain=[('usage','=','internal')]" \
--data-urlencode "fields=['name','complete_name','usage']"
Products with Stock¶
curl -X GET "https://your-odoo.com/restapi/1.0/object/product.product" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "fields=['name','default_code','qty_available','virtual_available']"
Domain Filter Examples¶
# Receipts (incoming)
[('picking_type_id.code', '=', 'incoming')]
# Deliveries (outgoing)
[('picking_type_id.code', '=', 'outgoing')]
# Ready to process
[('state', '=', 'assigned')]
# Waiting for stock
[('state', '=', 'confirmed')]
# Completed transfers
[('state', '=', 'done')]
# By partner
[('partner_id', '=', 123)]
# By sale order
[('sale_id', '=', 789)]
# Today's scheduled
[('scheduled_date', '>=', '2025-12-13 00:00:00'),
('scheduled_date', '<=', '2025-12-13 23:59:59')]
# Overdue (not done, past scheduled date)
[('state', 'not in', ['done', 'cancel']),
('scheduled_date', '<', '2025-12-13')]
Python Client Example¶
import requests
from datetime import datetime
class InventoryClient:
def __init__(self, base_url: str, api_key: str):
self.base_url = base_url.rstrip('/')
self.headers = {
'X-API-Key': api_key,
'Content-Type': 'application/json'
}
def get_ready_receipts(self, limit: int = 50) -> list:
"""Get pickings ready to receive (WH-IN)."""
domain = [
('picking_type_id.code', '=', 'incoming'),
('state', '=', 'assigned')
]
params = {
'domain': str(domain),
'fields': "['name','partner_id','scheduled_date','origin','sale_id']",
'limit': limit,
'order': 'scheduled_date'
}
response = requests.get(
f"{self.base_url}/restapi/1.0/object/stock.picking",
headers=self.headers,
params=params
)
response.raise_for_status()
return response.json().get('stock.picking', [])
def get_ready_deliveries(self, limit: int = 50) -> list:
"""Get pickings ready to deliver (WH-OUT)."""
domain = [
('picking_type_id.code', '=', 'outgoing'),
('state', '=', 'assigned')
]
params = {
'domain': str(domain),
'fields': "['name','partner_id','scheduled_date','origin','sale_id']",
'limit': limit,
'order': 'scheduled_date'
}
response = requests.get(
f"{self.base_url}/restapi/1.0/object/stock.picking",
headers=self.headers,
params=params
)
response.raise_for_status()
return response.json().get('stock.picking', [])
def get_picking_detail(self, picking_id: int) -> dict:
"""Get full picking details."""
params = {
'fields': "['name','partner_id','picking_type_id','scheduled_date','date_done','state','origin','sale_id']"
}
response = requests.get(
f"{self.base_url}/restapi/1.0/object/stock.picking/{picking_id}",
headers=self.headers,
params=params
)
response.raise_for_status()
pickings = response.json().get('stock.picking', [])
return pickings[0] if pickings else None
def get_picking_moves(self, picking_id: int) -> list:
"""Get moves for a picking."""
params = {
'domain': str([('picking_id', '=', picking_id)]),
'fields': "['product_id','product_uom_qty','quantity_done','reserved_availability','state']"
}
response = requests.get(
f"{self.base_url}/restapi/1.0/object/stock.move",
headers=self.headers,
params=params
)
response.raise_for_status()
return response.json().get('stock.move', [])
def set_move_done_qty(self, move_id: int, qty: float) -> dict:
"""Set quantity done on a move."""
response = requests.put(
f"{self.base_url}/restapi/1.0/object/stock.move/{move_id}",
headers=self.headers,
json={'quantity_done': qty}
)
response.raise_for_status()
return response.json().get('stock.move', {})
def get_product_stock(self, product_id: int) -> dict:
"""Get stock levels for a product."""
params = {
'fields': "['name','qty_available','virtual_available','incoming_qty','outgoing_qty']"
}
response = requests.get(
f"{self.base_url}/restapi/1.0/object/product.product/{product_id}",
headers=self.headers,
params=params
)
response.raise_for_status()
products = response.json().get('product.product', [])
return products[0] if products else None
# Usage
client = InventoryClient('https://your-odoo.com', 'your_api_key')
# Get ready receipts
receipts = client.get_ready_receipts()
print(f"Ready to receive: {len(receipts)} pickings")
# Get ready deliveries
deliveries = client.get_ready_deliveries()
print(f"Ready to deliver: {len(deliveries)} pickings")
# Process a receipt
for receipt in receipts:
picking_id = receipt['id']
moves = client.get_picking_moves(picking_id)
for move in moves:
# Set done qty = demand qty
client.set_move_done_qty(move['id'], move['product_uom_qty'])
print(f"Processed {receipt['name']}")
Important Notes¶
stock.move vs stock.move.line¶
When validating WH-IN/WH-OUT, you may encounter:
"Cannot set the done quantity from this stock move, work directly with the move lines"
Cause: Move has multiple stock.move.line records (from different quants/lots).
Solution: Update each stock.move.line.qty_done instead of stock.move.quantity_done:
# Get move lines
curl -X GET "https://your-odoo.com/restapi/1.0/object/stock.move.line" \
-H "X-API-Key: your_api_key" \
-G \
--data-urlencode "domain=[('move_id','=',456)]" \
--data-urlencode "fields=['product_id','product_uom_qty','qty_done','lot_id']"
# Update move line
curl -X PUT "https://your-odoo.com/restapi/1.0/object/stock.move.line/789" \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"qty_done": 5.0
}'
Validation¶
For complex operations like validating pickings (button_validate), use JSON-RPC to call Odoo methods directly, as REST API may not support workflow actions.
Reservation Status¶
Check reserved_availability vs product_uom_qty to determine reservation status:
def check_reservation(move):
demand = move['product_uom_qty']
reserved = move['reserved_availability']
if reserved >= demand:
return 'fully_reserved' # Ready
elif reserved > 0:
return 'partially_reserved' # Partial
else:
return 'not_reserved' # Waiting
Dashboard Stats Example¶
def get_inventory_dashboard(client: InventoryClient) -> dict:
"""Get inventory dashboard statistics."""
receipts = client.get_ready_receipts(limit=500)
deliveries = client.get_ready_deliveries(limit=500)
return {
'ready_receipts': len(receipts),
'ready_deliveries': len(deliveries),
'recent_receipts': receipts[:5],
'recent_deliveries': deliveries[:5]
}