Home

Awesome

aio-odoorpc: an async Odoo RPC client

This package builds upon the lower-level aio-odoorpc-base adding a AsyncOdooRPC/OdooRPC class as a thin layer that makes for a friendlier interface.

AsyncOdooRPC is asynchronous code, OdooRPC is synchronous.

This package does not intend to implement all the functionality of other odoo rpc modules like ERPpeek and odoorpc. This is meant to be simpler but equally easy to work with without trying to be too smart. One of the motivations of this package was to have an alternative to odoorpc which started getting in my way. For instance, just instantiating a new object in odoorpc may result in a roundtrip to the remote Odoo server. These unnecessary RPC calls quickly add up and it becomes too difficult to develop fast software. Also, odoorpc offers no asynchronous interface which is a huge lost opportunity for code that spends a lot of time waiting on blocking network calls.

Why use this instead of aio-odoorpc-base

With this interface you can instantiate an object once and then make simpler invocations to remote methods like login, read, search, search_read and search_count. With aio-odoorpc-base, you get only the lower level execute_kw call and must pass a long list of parameters on every invocation.

Also, aio-odoorpc let's you simulate behavior of odoorpc by instantiating the AsyncOdooRPC class with a default_model_name so then all method calls do not need to pass a model_name. In this way, you can easily replace the usage of odoorpc with this object (I know because I migrated a lot of code away from odoorpc). Of course, if you are migrating from odoorpc you should take the opportunity to migrate to async code as well.

Limitations

Right now there are built-in helper functions only for getting info out (read, search, search_read, search_count), nothing to help creating new records or updating field values. Those are coming soon.

Things to know about this module:

Things to know about Odoo's API:

def build_odoo_base_url(*, host: str, port: Optional[int] = None, ssl: bool = True, base_url: str = '') -> str:
    ...

def build_odoo_jsonrpc_endpoint_url(*, host: str, port: Optional[int] = None, ssl: bool = True,
                                    base_url: str = '', custom_odoo_jsonrpc_suffix: Optional[str] = None) -> str:
    ...

def odoo_base_url2jsonrpc_endpoint(odoo_base_url: str = '', custom_odoo_jsonrpc_suffix: Optional[str] = None) -> str:
    ...

To import them use

from aio_odoorpc_base.helpers import build_odoo_base_url, build_odoo_jsonrpc_endpoint_url, odoo_base_url2jsonrpc_endpoint

Examples:

build_odoo_base_url(host='acme.odoo.com', base_url='testing')
>>>> https://acme.odoo.com/testing
build_odoo_jsonrpc_endpoint_url(host='acme.odoo.com', base_url='testing')
>>>> https://acme.odoo.com/testing/jsonrpc
odoo_base_url2jsonrpc_endpoint('https://acme.odoo.com/testing')
>>>> https://acme.odoo.com/testing/jsonrpc

Usage

Note: check the tests folder for more examples

I will omit the event_loop logic I assume that if you want an async module you already have that sorted out yourself or through a framework like FastAPI. All examples below could also be called using the synchronous OdooRPC object, but without the 'await' syntax.

from aio_odoorpc import AsyncOdooRPC
import httpx

# If the http_client you are using does not support a 'base_url' parameter like
# httpx does, you will need to pass the 'url_jsonrpc_endpoint' parameter when
# instantiating the AsyncOdooRPC object.
async with httpx.AsyncClient(base_url='https://acme.odoo.com/jsonrpc') as session:
    odoo = AsyncOdooRPC(database='acme_db', username_or_uid='demo',
                        password='demo', http_client=session)
    await odoo.login()

    try:
        # A default model name has not been set a instantiation time so we should
        # pass the model_name on every invocation. Here it should throw an exception.
        await odoo.read()
    except RuntimeError:
        pass
    else:
        assert False, 'Expected an exception to be raised'
    
    # Now with model_name set it should work. Not passing a list of ids
    # turns the read call into a search_read call with an empty domain (so it matches all)
    # A missing 'fields' parameter means 'fetch all fields'.
    # Hence this call is very expensive, it fetches all fields for all
    # 'sale.order' records
    all_orders_all_fields = await odoo.read(model_name='sale.order')
    
    # creates a copy of odoo obj setting default_model_name to 'sale.order'
    sale_order = odoo.new_for_model('sale.order')
    
    # now we do not need to pass model_name and it works!
    all_orders_all_fields2 = await sale_order.read()

    large_orders = sale_order.search_read(domain=[['amount_total', '>', 10000]],
                                          fields=['partner_id', 'amount_total', 'date_order'])

Object instantiation

The AsyncOdooRPC/OdooRPC object takes these parameters:

odoo = AsyncOdooRPC(...)
sale_order = odoo.new_for_model('sale.order')
sale_order_line = odoo.new_for_model('sale.order.line')
partner = sale_order.new_for_model('partner')

Just remember that new_for_method is nothing special, it only sets a default model name on a copy of an instance. Making copies of copies is perfectly ok.

Dependencies

This package depends on aio-odoorpc-base which has no dependency itself.