Skip to content

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

X-API-Key: your_api_key_here

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']"

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]
    }