External API - Odoo 11.0

Стандартным механизмом для расширения функционала Odoo является использование модулей, но многие его функции и все данные доступны извне для анализа или интеграции с различными инструментами. Часть API Model Reference доступна через XML-RPC и может быть задействована на разных языках программирования.

Соединение

Настройка

Если у вас уже есть установленные сервер Odoo, то вы может использовать следующие параметры

  • Python
  • Ruby
  • PHP
  • Java
url = <insert server URL>
db = <insert database name>
username = 'admin'
password = <insert password for your admin user (default: admin)>
url = <insert server URL>
db = <insert database name>
username = "admin"
password = <insert password for your admin user (default: admin)>
$url = <insert server URL>;
$db = <insert database name>;
$username = "admin";
$password = <insert password for your admin user (default: admin)>;
final String url = <insert server URL>,
              db = <insert database name>,
        username = "admin",
        password = <insert password for your admin user (default: admin)>;

Демо

Чтобы упростить себе жизнь, вы также можете запросить тестовую базу данных по этой ссылке https://demo.odoo.com:

  • Python
  • Ruby
  • PHP
  • Java
import xmlrpclib
info = xmlrpclib.ServerProxy('https://demo.odoo.com/start').start()
url, db, username, password = \
    info['host'], info['database'], info['user'], info['password']
require "xmlrpc/client"
info = XMLRPC::Client.new2('https://demo.odoo.com/start').call('start')
url, db, username, password = \
    info['host'], info['database'], info['user'], info['password']
require_once('ripcord.php');
$info = ripcord::client('https://demo.odoo.com/start')->start();
list($url, $db, $username, $password) =
  array($info['host'], $info['database'], $info['user'], $info['password']);
final XmlRpcClient client = new XmlRpcClient();

final XmlRpcClientConfigImpl start_config = new XmlRpcClientConfigImpl();
start_config.setServerURL(new URL("https://demo.odoo.com/start"));
final Map<String, String> info = (Map<String, String>)client.execute(
    start_config, "start", emptyList());

final String url = info.get("host"),
              db = info.get("database"),
        username = info.get("user"),
        password = info.get("password");

Авторизация

Odoo требует чтобы пользователи API проходили авторизацию прежде чем они смогут делать запросы.

Эндпоинт xmlrpc/2/common предоставляет доступ к вызовам, которые не требуют аутентификации, например сама аутентификация или запрос информации о версии. Самый простой способ проверить правильность информации о соединении перед аутентификацией - запросить версию сервера. Сама аутентификация выполняется через функцию``authenticate`` и возвращает идентификатор пользователя (uid) используемый в дальнейшем вместо логина.

  • Python
  • Ruby
  • PHP
  • Java
common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url))
common.version()
common = XMLRPC::Client.new2("#{url}/xmlrpc/2/common")
common.call('version')
$common = ripcord::client("$url/xmlrpc/2/common");
$common->version();
final XmlRpcClientConfigImpl common_config = new XmlRpcClientConfigImpl();
common_config.setServerURL(
    new URL(String.format("%s/xmlrpc/2/common", url)));
client.execute(common_config, "version", emptyList());
{
    "server_version": "8.0",
    "server_version_info": [8, 0, 0, "final", 0],
    "server_serie": "8.0",
    "protocol_version": 1,
}
  • Python
  • Ruby
  • PHP
  • Java
uid = common.authenticate(db, username, password, {})
uid = common.call('authenticate', db, username, password, {})
$uid = $common->authenticate($db, $username, $password, array());
int uid = (int)client.execute(
    common_config, "authenticate", asList(
        db, username, password, emptyMap()));

Методы

Вторым эндпоинтом является xmlrpc/2/object, он используется для вызова методов моделей с помощью функции удаленного вызова процедур(RPC) execute_kw .

Каждый вызов execute_kw принимает следующие параметры:

  • имя базы данных - string
  • идентификатор пользователя (полученный с помощью функции authenticate) - integer
  • пароль пользователя - строка
  • имя модели - строка
  • имя вызываемого метода внутри модели - строка
  • список позиционных параметров - массив/список
  • ключевые параметры - словарь

Например, если мы имеет доступ для чтения к модели res.partner мы можем вызвать метод check_access_rights с позиционным аргументом operation и ключевым аргуметом raise_exception (чтобы получить результат true/false , а не true/error):

  • Python
  • Ruby
  • PHP
  • Java
models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url))
models.execute_kw(db, uid, password,
    'res.partner', 'check_access_rights',
    ['read'], {'raise_exception': False})
models = XMLRPC::Client.new2("#{url}/xmlrpc/2/object").proxy
models.execute_kw(db, uid, password,
    'res.partner', 'check_access_rights',
    ['read'], {raise_exception: false})
$models = ripcord::client("$url/xmlrpc/2/object");
$models->execute_kw($db, $uid, $password,
    'res.partner', 'check_access_rights',
    array('read'), array('raise_exception' => false));
final XmlRpcClient models = new XmlRpcClient() {{
    setConfig(new XmlRpcClientConfigImpl() {{
        setServerURL(new URL(String.format("%s/xmlrpc/2/object", url)));
    }});
}};
models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "check_access_rights",
    asList("read"),
    new HashMap() {{ put("raise_exception", false); }}
));
true

Список записей

Записи могут быть перечислены и отфильтрованы с помощью search().

search() принимает обязательный параметр:ref:[UNKNOWN NODE title_reference] (возможно пустой) и возвращает id базы данных всех записей, соответствующих фильтру. Например перечислить клиентов, который являются компаниями,:

  • Python
  • Ruby
  • PHP
  • Java
models.execute_kw(db, uid, password,
    'res.partner', 'search',
    [[['is_company', '=', True], ['customer', '=', True]]])
models.execute_kw(db, uid, password,
    'res.partner', 'search',
    [[['is_company', '=', true], ['customer', '=', true]]])
$models->execute_kw($db, $uid, $password,
    'res.partner', 'search', array(
        array(array('is_company', '=', true),
              array('customer', '=', true))));
asList((Object[])models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "search",
    asList(asList(
        asList("is_company", "=", true),
        asList("customer", "=", true)))
)));
[7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74]

Разбиение на страницы

По умолчанию поиск вернет идентификаторы всех записей, соответствующих условию, которое может быть очень велико. Параметры offset и limit позволяют возвращать подмножества всех записей, удовлетворяющих запросу.

  • Python
  • Ruby
  • PHP
  • Java
models.execute_kw(db, uid, password,
    'res.partner', 'search',
    [[['is_company', '=', True], ['customer', '=', True]]],
    {'offset': 10, 'limit': 5})
models.execute_kw(db, uid, password,
    'res.partner', 'search',
    [[['is_company', '=', true], ['customer', '=', true]]],
    {offset: 10, limit: 5})
$models->execute_kw($db, $uid, $password,
    'res.partner', 'search',
    array(array(array('is_company', '=', true),
                array('customer', '=', true))),
    array('offset'=>10, 'limit'=>5));
asList((Object[])models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "search",
    asList(asList(
        asList("is_company", "=", true),
        asList("customer", "=", true))),
    new HashMap() {{ put("offset", 10); put("limit", 5); }}
)));
[13, 20, 30, 22, 29]

Количество записей

Вместо того, чтобы запросить гигантский список записей, а затем посчитать их, search_count() запрос может быть использован для получения только количества записей удовлетворяющих условию. Он использует такой же domain фильтр, как и search() и никаких других параметров.

  • Python
  • Ruby
  • PHP
  • Java
models.execute_kw(db, uid, password,
    'res.partner', 'search_count',
    [[['is_company', '=', True], ['customer', '=', True]]])
models.execute_kw(db, uid, password,
    'res.partner', 'search_count',
    [[['is_company', '=', true], ['customer', '=', true]]])
$models->execute_kw($db, $uid, $password,
    'res.partner', 'search_count',
    array(array(array('is_company', '=', true),
                array('customer', '=', true))));
(Integer)models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "search_count",
    asList(asList(
        asList("is_company", "=", true),
        asList("customer", "=", true)))
));
19

Чтение записей

Доступ к данным записи осуществляется с помощью метода read(), который содержит список идентификаторов (которые были возвращены по запросу: search()) и опционально список полей, данные из которых мы хотим получить. По умолчанию вы получите значение всех полей, к которым имеет доступ текущий пользователь, количество которых может быть очень большим.

  • Python
  • Ruby
  • PHP
  • Java
ids = models.execute_kw(db, uid, password,
    'res.partner', 'search',
    [[['is_company', '=', True], ['customer', '=', True]]],
    {'limit': 1})
[record] = models.execute_kw(db, uid, password,
    'res.partner', 'read', [ids])
# count the number of fields fetched by default
len(record)
ids = models.execute_kw(db, uid, password,
    'res.partner', 'search',
    [[['is_company', '=', true], ['customer', '=', true]]],
    {limit: 1})
record = models.execute_kw(db, uid, password,
    'res.partner', 'read', [ids]).first
# count the number of fields fetched by default
record.length
$ids = $models->execute_kw($db, $uid, $password,
    'res.partner', 'search',
    array(array(array('is_company', '=', true),
                array('customer', '=', true))),
    array('limit'=>1));
$records = $models->execute_kw($db, $uid, $password,
    'res.partner', 'read', array($ids));
// count the number of fields fetched by default
count($records[0]);
final List ids = asList((Object[])models.execute(
    "execute_kw", asList(
        db, uid, password,
        "res.partner", "search",
        asList(asList(
            asList("is_company", "=", true),
            asList("customer", "=", true))),
        new HashMap() {{ put("limit", 1); }})));
final Map record = (Map)((Object[])models.execute(
    "execute_kw", asList(
        db, uid, password,
        "res.partner", "read",
        asList(ids)
    )
))[0];
// count the number of fields fetched by default
record.size();
121

В случае, если нам интересны только три поля.

  • Python
  • Ruby
  • PHP
  • Java
models.execute_kw(db, uid, password,
    'res.partner', 'read',
    [ids], {'fields': ['name', 'country_id', 'comment']})
models.execute_kw(db, uid, password,
    'res.partner', 'read',
    [ids], {fields: %w(name country_id comment)})
$models->execute_kw($db, $uid, $password,
    'res.partner', 'read',
    array($ids),
    array('fields'=>array('name', 'country_id', 'comment')));
asList((Object[])models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "read",
    asList(ids),
    new HashMap() {{
        put("fields", asList("name", "country_id", "comment"));
    }}
)));
[{"comment": false, "country_id": [21, "Belgium"], "id": 7, "name": "Agrolait"}]

Список полей записи

fields_get() может быть использован для для проверки полей модели на наличие интересующей вас информации.

Поскольку данный запрос возвращает большое количество мета-информации (он так же используется программами-клиентами), она должна быть отфильтрована перед печатью, наиболее интересные элементы для человеческого восприятия это string (описание поля), help (справочный текст, если имеется) и type (чтобы знать какое значение ожидать или посылать при обновлении записи):

  • Python
  • Ruby
  • PHP
  • Java
models.execute_kw(
    db, uid, password, 'res.partner', 'fields_get',
    [], {'attributes': ['string', 'help', 'type']})
models.execute_kw(
    db, uid, password, 'res.partner', 'fields_get',
    [], {attributes: %w(string help type)})
$models->execute_kw($db, $uid, $password,
    'res.partner', 'fields_get',
    array(), array('attributes' => array('string', 'help', 'type')));
(Map<String, Map<String, Object>>)models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "fields_get",
    emptyList(),
    new HashMap() {{
        put("attributes", asList("string", "help", "type"));
    }}
));
{
    "ean13": {
        "type": "char",
        "help": "BarCode",
        "string": "EAN13"
    },
    "property_account_position_id": {
        "type": "many2one",
        "help": "The fiscal position will determine taxes and accounts used for the partner.",
        "string": "Fiscal Position"
    },
    "signup_valid": {
        "type": "boolean",
        "help": "",
        "string": "Signup Token is Valid"
    },
    "date_localization": {
        "type": "date",
        "help": "",
        "string": "Geo Localization Date"
    },
    "ref_company_ids": {
        "type": "one2many",
        "help": "",
        "string": "Companies that refers to partner"
    },
    "sale_order_count": {
        "type": "integer",
        "help": "",
        "string": "# of Sales Order"
    },
    "purchase_order_count": {
        "type": "integer",
        "help": "",
        "string": "# of Purchase Order"
    },

Поиск и чтение

В связи с тем, что это частая задача, Odoo предоставляет search_read() запрос, который согласно его наименованию search() следует за read(), но вместо двух запросов позволяет сделать один и сохранить идентификаторы.

Его аргументы такие же как и у search()“s, но он так же понимает список fields (как read(), если в этом списке нет критериев выбора, он вернет все поля совпадающих с условиями поиска записей):

  • Python
  • Ruby
  • PHP
  • Java
models.execute_kw(db, uid, password,
    'res.partner', 'search_read',
    [[['is_company', '=', True], ['customer', '=', True]]],
    {'fields': ['name', 'country_id', 'comment'], 'limit': 5})
models.execute_kw(db, uid, password,
    'res.partner', 'search_read',
    [[['is_company', '=', true], ['customer', '=', true]]],
    {fields: %w(name country_id comment), limit: 5})
$models->execute_kw($db, $uid, $password,
    'res.partner', 'search_read',
    array(array(array('is_company', '=', true),
                array('customer', '=', true))),
    array('fields'=>array('name', 'country_id', 'comment'), 'limit'=>5));
asList((Object[])models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "search_read",
    asList(asList(
        asList("is_company", "=", true),
        asList("customer", "=", true))),
    new HashMap() {{
        put("fields", asList("name", "country_id", "comment"));
        put("limit", 5);
    }}
)));
[
    {
        "comment": false,
        "country_id": [ 21, "Belgium" ],
        "id": 7,
        "name": "Agrolait"
    },
    {
        "comment": false,
        "country_id": [ 76, "France" ],
        "id": 18,
        "name": "Axelor"
    },
    {
        "comment": false,
        "country_id": [ 233, "United Kingdom" ],
        "id": 12,
        "name": "Bank Wealthy and sons"
    },
    {
        "comment": false,
        "country_id": [ 105, "India" ],
        "id": 14,
        "name": "Best Designers"
    },
    {
        "comment": false,
        "country_id": [ 76, "France" ],
        "id": 17,
        "name": "Camptocamp"
    }
]

Создание записей

Записи модели создаются с использованием create(). Метод создаст одну запись и вернет ее id.

create() принимает словарь где ключ это имя поля, а значение - сохраняемые туда данные использует их для инициализации записи. Для каждого поля, для которого не указаны пара имя-значение, будет присвоено значение по умолчанию.

  • Python
  • Ruby
  • PHP
  • Java
id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
    'name': "New Partner",
}])
id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
    name: "New Partner",
}])
$id = $models->execute_kw($db, $uid, $password,
    'res.partner', 'create',
    array(array('name'=>"New Partner")));
final Integer id = (Integer)models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "create",
    asList(new HashMap() {{ put("name", "New Partner"); }})
));
78

Обновление записей

Значение записей могут быть обновлены используя метод write(), он принимает список записей и словарь обновлямых полей и их значений точно так же как и метод create().

Несколько записей могут обновляться одновременно, но все они получат одинаковые значения для всех указанных полей. В настоящее время невозможно выполнить «вычисляемые» обновления (когда устанавливаемое значение зависит от существующего значения записи).

  • Python
  • Ruby
  • PHP
  • Java
models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {
    'name': "Newer partner"
}])
# get record name after having changed it
models.execute_kw(db, uid, password, 'res.partner', 'name_get', [[id]])
models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {
    name: "Newer partner"
}])
# get record name after having changed it
models.execute_kw(db, uid, password, 'res.partner', 'name_get', [[id]])
$models->execute_kw($db, $uid, $password, 'res.partner', 'write',
    array(array($id), array('name'=>"Newer partner")));
// get record name after having changed it
$models->execute_kw($db, $uid, $password,
    'res.partner', 'name_get', array(array($id)));
models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "write",
    asList(
        asList(id),
        new HashMap() {{ put("name", "Newer Partner"); }}
    )
));
// get record name after having changed it
asList((Object[])models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "name_get",
    asList(asList(id))
)));
[[78, "Newer partner"]]

Удаление записей

Записи могут быть удалены массово, путем указания их id unlink().

  • Python
  • Ruby
  • PHP
  • Java
models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]])
# check if the deleted record is still in the database
models.execute_kw(db, uid, password,
    'res.partner', 'search', [[['id', '=', id]]])
models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]])
# check if the deleted record is still in the database
models.execute_kw(db, uid, password,
    'res.partner', 'search', [[['id', '=', id]]])
$models->execute_kw($db, $uid, $password,
    'res.partner', 'unlink',
    array(array($id)));
// check if the deleted record is still in the database
$models->execute_kw($db, $uid, $password,
    'res.partner', 'search',
    array(array(array('id', '=', $id))));
models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "unlink",
    asList(asList(id))));
// check if the deleted record is still in the database
asList((Object[])models.execute("execute_kw", asList(
    db, uid, password,
    "res.partner", "search",
    asList(asList(asList("id", "=", 78)))
)));
[]

Контроль и самоанализ

Ранее мы использовали fields_get() для запроса модели и с самого начала использовали произвольную модель данных, Odoo хранит большинство метаданных модели внутри нескольких мета моделей, что позволяет, как делать запросы к системе так и изменять модели данных и поля (с некоторыми ограничениями) на лету через XML-RPC.

ir.model

Предоставляет информацию о моделях Odoo через различные поля

name
понятное человеку описание модели
model
имя каждой модели в системе
state
Была ли модель сгенерирована в коде Python (base) или же создана запись в ir.model (manual)
field_id
Список полей модели связанных через One2many с ir.model.fields
view_ids
One2many в Представления, определенные для модели
access_ids
One2many ссылается на Контроль доступа, установленного для модели

ir.model можно использовать для

  • опрашивать систему об установленных моделях (в качестве предварительного условия для работы с моделью или для изучения содержимого системы)
  • получить информацию о конкретной модели (как правило, путем перечисления полей, связанных с ней)
  • динамически создавать новые модели с помощью удаленного вызова процедур (RPC)

Пользовательская модель данных первоначально будет содержать только «встроенные» поля, доступные для всех моделей:

  • Python
  • PHP
  • Ruby
  • Java
models.execute_kw(db, uid, password, 'ir.model', 'create', [{
    'name': "Custom Model",
    'model': "x_custom_model",
    'state': 'manual',
}])
models.execute_kw(
    db, uid, password, 'x_custom_model', 'fields_get',
    [], {'attributes': ['string', 'help', 'type']})
$models->execute_kw(
    $db, $uid, $password,
    'ir.model', 'create', array(array(
        'name' => "Custom Model",
        'model' => 'x_custom_model',
        'state' => 'manual'
    ))
);
$models->execute_kw(
    $db, $uid, $password,
    'x_custom_model', 'fields_get',
    array(),
    array('attributes' => array('string', 'help', 'type'))
);
models.execute_kw(
    db, uid, password,
    'ir.model', 'create', [{
        name: "Custom Model",
        model: 'x_custom_model',
        state: 'manual'
    }])
fields = models.execute_kw(
    db, uid, password, 'x_custom_model', 'fields_get',
    [], {attributes: %w(string help type)})
models.execute(
    "execute_kw", asList(
        db, uid, password,
        "ir.model", "create",
        asList(new HashMap<String, Object>() {{
            put("name", "Custom Model");
            put("model", "x_custom_model");
            put("state", "manual");
        }})
));
final Object fields = models.execute(
    "execute_kw", asList(
        db, uid, password,
        "x_custom_model", "fields_get",
        emptyList(),
        new HashMap<String, Object> () {{
            put("attributes", asList(
                    "string",
                    "help",
                    "type"));
        }}
));
{
    "create_uid": {
        "type": "many2one",
        "string": "Created by"
    },
    "create_date": {
        "type": "datetime",
        "string": "Created on"
    },
    "__last_update": {
        "type": "datetime",
        "string": "Last Modified on"
    },
    "write_uid": {
        "type": "many2one",
        "string": "Last Updated by"
    },
    "write_date": {
        "type": "datetime",
        "string": "Last Updated on"
    },
    "display_name": {
        "type": "char",
        "string": "Display Name"
    },
    "id": {
        "type": "integer",
        "string": "Id"
    }
}

ir.model.fields

Предоставляет информацию о полях моделей Odoo и позволяет добавлять настраиваемые поля без использования кода Python

model_id
Many2one ссылается на ir.model, к которому принадлежит поле
name
техническое имя поля (используется в работе методов``read`` или write)
field_description
понятное для человека описание поля (например string в fields_get)
ttype
type (тип) создаваемого поля
state
было ли поле создано с помощью кода Python (base) или через ir.model.fields (manual)
required, readonly, translate
Включает соответствующий флаг на поле
groups
field-level access control, a Many2many к res.groups
selection, size, on_delete, relation, relation_field, domain
типозависимые свойства и настройки, смотрите детальное описание полей для более подробного понимания

Как и в пользовательских моделях , в качестве фактических полей модели активируются только новые поля, созданные с помощью state="manual".

  • Python
  • PHP
  • Ruby
  • Java
id = models.execute_kw(db, uid, password, 'ir.model', 'create', [{
    'name': "Custom Model",
    'model': "x_custom",
    'state': 'manual',
}])
models.execute_kw(
    db, uid, password,
    'ir.model.fields', 'create', [{
        'model_id': id,
        'name': 'x_name',
        'ttype': 'char',
        'state': 'manual',
        'required': True,
    }])
record_id = models.execute_kw(
    db, uid, password,
    'x_custom', 'create', [{
        'x_name': "test record",
    }])
models.execute_kw(db, uid, password, 'x_custom', 'read', [[record_id]])
$id = $models->execute_kw(
    $db, $uid, $password,
    'ir.model', 'create', array(array(
        'name' => "Custom Model",
        'model' => 'x_custom',
        'state' => 'manual'
    ))
);
$models->execute_kw(
    $db, $uid, $password,
    'ir.model.fields', 'create', array(array(
        'model_id' => $id,
        'name' => 'x_name',
        'ttype' => 'char',
        'state' => 'manual',
        'required' => true
    ))
);
$record_id = $models->execute_kw(
    $db, $uid, $password,
    'x_custom', 'create', array(array(
        'x_name' => "test record"
    ))
);
$models->execute_kw(
    $db, $uid, $password,
    'x_custom', 'read',
    array(array($record_id)));
id = models.execute_kw(
    db, uid, password,
    'ir.model', 'create', [{
        name: "Custom Model",
        model: "x_custom",
        state: 'manual'
    }])
models.execute_kw(
    db, uid, password,
    'ir.model.fields', 'create', [{
        model_id: id,
        name: "x_name",
        ttype: "char",
        state: "manual",
        required: true
    }])
record_id = models.execute_kw(
    db, uid, password,
    'x_custom', 'create', [{
        x_name: "test record"
    }])
models.execute_kw(
    db, uid, password,
    'x_custom', 'read', [[record_id]])
final Integer id = (Integer)models.execute(
    "execute_kw", asList(
        db, uid, password,
        "ir.model", "create",
        asList(new HashMap<String, Object>() {{
            put("name", "Custom Model");
            put("model", "x_custom");
            put("state", "manual");
        }})
));
models.execute(
    "execute_kw", asList(
        db, uid, password,
        "ir.model.fields", "create",
        asList(new HashMap<String, Object>() {{
            put("model_id", id);
            put("name", "x_name");
            put("ttype", "char");
            put("state", "manual");
            put("required", true);
        }})
));
final Integer record_id = (Integer)models.execute(
    "execute_kw", asList(
        db, uid, password,
        "x_custom", "create",
        asList(new HashMap<String, Object>() {{
            put("x_name", "test record");
        }})
));

client.execute(
    "execute_kw", asList(
        db, uid, password,
        "x_custom", "read",
        asList(asList(record_id))
));
[
    {
        "create_uid": [1, "Administrator"],
        "x_name": "test record",
        "__last_update": "2014-11-12 16:32:13",
        "write_uid": [1, "Administrator"],
        "write_date": "2014-11-12 16:32:13",
        "create_date": "2014-11-12 16:32:13",
        "id": 1,
        "display_name": "test record"
    }
]