Использование API Odoo для удаленного вызова процедур (RPC)
Odoo API
Для того, чтобы взаимодейстовать удаленно с системой Odoo, с помощью RPC методов, нам необходимо в первую очередь получить session_id
, которым мы будем подписывать каждый запрос к системе.
1. Получение информации о сессии
Ниже приведен пример на языке Python
, я прокоментирую кажду строку, думаю все будет понятно.
# Переменная, которая хранит в себе путь к файлу с информацией о сессии
SESSION_FILE = "session.json"
# Адрес сервера, к которму мы планируем подключаться
HOST = 'www.адрес.сервера.by'
# Протокол, http или https
PROTOCOL = "http"
# Порт на котором слушает наш сервер, для https по умолчанию 443
PORT = "80"
# Объединяем все в одну строку
PROTOCOL_HOST_PORT = f"{PROTOCOL}://{HOST}:{PORT}"
# Указываем авторизационные данные, обратите внимание что тут три параметра, логи, пароль и имя базы данных
EMAIL = "admin"
PASSWORD = "admin"
DATABASE = "demo"
# Обращаю ваше внимание, это не JSON данные, а тип данных Dictionary(Словарь) в Python,
# но да, они очень похожи. При отправке вам нужно будет еще преобразовать их в
# JSON строку(байтовый массив) и закрепитьв теле POST запроса
json_request_data = {"jsonrpc": "2.0","params":{
"db":DATABASE,
"login":EMAIL,
"password":PASSWORD
}
}
Обращаю ваше внимание, что запрос к Odoo должен быть оформлен по стандарту jsonrpc 2.0 Теперь подготовим запрос на получение авторазационного токена
#Объявлеям заколовки, которые сделают наш запрос json запросом
headers = {'Content-Type': 'application/json','Accept': 'text/plain'}
# тут мы создаем полный URL запроса
URL = PROTOCOL_HOST_PORT + "/web/session/authenticate"
# Делаем сам запрос,обратите внимание, что наш словарь с авторизационными данными,
# мы преобразуем в строку с помощью метода json.dumps, т.е. на его менсто можно поставить обычную JSON строку
send_data = requests.post(URL,headers=headers,data=json.dumps(json_request_data))
# В этот момент нам может придти ответ двух видов
# Во первых там может быть информация об уже существующей сессии
# от этого пользователя и в заголовке будет содержаться сообщение
# вида session_id=1234567890000
# Поэтому если он там есть, мы его сразу извлекаем и подкидываем в качестве
# информации о сессии
set_cookies = send_data.headers["Set-Cookie"]
if "session_id" in set_cookies:
session_id = set_cookies.split(";")[0].split("=")[1]
return {"result":{"session_id":session_id}}
# Если же в заголовке нет информации о сессии, то из заголовка ответа "Set-Cookie" мы извлекаем authorization_token
authorization_token = set_cookies
# И подготавливаем заголовки для второго запроса, где в куку подсовываем наш authorization_token
headers = {'Cookie':authorization_token,'Content-Type': 'application/json','Accept': 'text/plain'}
# Обратите внимание на оформление запроса, в качестве тела запроса надо указать строку с пустыми {}
get_session_info = requests.post(PROTOCOL_HOST_PORT +'/web/session/get_session_info',headers=headers,data="{}")
# Полученный текст ответа и являет собой JSON строку с нужной нам информацией, и мы можем ее развернуть в
# объект для удобного взаимодействия с помощью метода json.loads. Но ничего не мешает работать
# с ответом как с обычной строкой
session_info = json.loads(get_session_info.text)
Теперь давайте еще раз на словах
- Делаем JSONRPC 2.0 запрос на URI
/web/session/authenticate
- Из ответа извлекаем содержимое заголовка
Set-Cookie
и это будетАвторизационный Токен
или там будет сразуsession_id=12345...
и тогда мы сразу сохраняем его себе в кеш и пользуемся - Подготавливаем второй JSONRPC 2.0 запрос на URI
/web/session/get_session_info
- Добавляем к запросу заколовок
Cookie
иАвторизационный Токен
в качестве его параметра - Полученное тело ответа и будет
JSON строка
с информацией о сессии
2. Использование и проверка session_id
В ифнормации о сессии нас больше всего интересует ключ session_id
. Собстенно это и будет нашим идентификатором, для подписи запросов. Наша сессия не бесконечна и ограничена 90 днями, плюс у нас могут возникнуть различные коллизии и session_id
может стать не действительным. Чтобы убедиться в том, что наше идентификатор еще валиден, мы должны сделать следующее:
# Обратите внимание на оформление параметра 'Cookie', теперь его значение будет иметь примерно такоей вид
# 'Cookie': "session_id=250769263b42fb907b741608cc99ccdbeeac5940"
headers = {'Cookie': "session_id="+session_id,'Content-Type': 'application/json','Accept': 'text/plain'}
# Делаем запрос на URI проверки валидности сессии
check_session = requests.post(PROTOCOL_HOST_PORT +'/web/session/check',headers=headers,data="{}")
# Полученную JSON строку парсим в объект
response = json.loads(check_session.text)
# Если в ответе есть ключ error, то кука не валидна, что мы и выводим, если такого ключа нет, то мы
# наша сессия валида
error = response.get('error',False)
if error:
print(error.get("message"))
return False
return True
Теперь помимо проверки session_id
мы знаем как подписывать запрос к Odoo. Достаточно добавить соотвествующую куку к запросу
Я понимаю что с проверкой можйно не заморачиваться и ее наличие немного замедляет. Но если скрипт будет
каждый раз создавать сессию для своей работы, это тоже не корректно расходует его ресурсы.
3. Выполнение метода с помощью RPC
Для того, чтобы удаленно выполнить метод и получить результаты его работы в виде JSON строки, необходимо сделать следующее:
# Генерируем уникальный идентификатор запроса
req_uuid = uuid.uuid4()
json_rpc_data = {
"jsonrpc": "2.0",
"id": str(req_uuid),
"method": "call",
"params": {
"model": "ir.default", # Имя модели, к которой хотим обратиться
"method": "search_read", # Метод модели, который мы хотим вызвать для исполнения
"args": [ # позиционный аргрументы для метода
# ['id','parent_id','name']
],
"kwargs":{ # ключевые аргументы для метода
"domain":[
('id','=',1),
],
"fields": ['id','field_id','json_value'],
}
}
}
# Оформляем заголовки
headers = {'Cookie': "session_id="+session_id,'Content-Type': 'application/json','Accept': 'text/plain'}
# Делаем запрос на URI удаленного вызова процедур
method_result = requests.post(PROTOCOL_HOST_PORT +'/web/dataset/call_kw',headers=headers,data=json.dumps(json_rpc_data))
# Полученную JSON строку парсим в объект
response = json.loads(method_result.text)
4. Обработка ошибок
Что делать если возникают ошибки на самом сервера. Или как уведомить пользователя с клиентской стороны что он совершил явно не предусматриваемое действие? Для этого реализован механизм передачи возникающих исключений на сторону клиента. Вот как это реализовано. Ниже вы можете видеть пример ответа с ошибкой
{
"jsonrpc": "2.0",
"id": "73d5b198-066f-40fa-a52a-6c40e7e21551",
"error": {
"code": 200,
"message": "Odoo Server Error",
"data": {
"name": "builtins.KeyError",
"debug": "Traceback (most recent call last):\n File \"/home/www-data/odoo/odoo/http.py\", line 653, in _handle_exception\n return super(JsonRequest, self)._handle_exception(exception)\n File \"/home/www-data/odoo/odoo/http.py\", line 312
, in _handle_exception\n raise pycompat.reraise(type(exception), exception, sys.exc_info()[2])\n File \"/home/www-data/odoo/odoo/tools/pycompat.py\", line 87, in reraise\n raise value\n File \"/home/www-data/odoo/odoo/http.py\", line 695, in disp
atch\n result = self._call_function(**self.params)\n File \"/home/www-data/odoo/odoo/http.py\", line 344, in _call_function\n return checked_call(self.db, *args, **kwargs)\n File \"/home/www-data/odoo/odoo/service/model.py\", line 97, in wrapper\
n return f(dbname, *args, **kwargs)\n File \"/home/www-data/odoo/odoo/http.py\", line 337, in checked_call\n result = self.endpoint(*a, **kw)\n File \"/home/www-data/odoo/odoo/http.py\", line 939, in __call__\n return self.method(*args, **kw)\
n File \"/home/www-data/odoo/odoo/http.py\", line 517, in response_wrap\n response = f(*args, **kw)\n File \"/home/www-data/odoo/addons/web/controllers/main.py\", line 934, in call_kw\n return self._call_kw(model, method, args, kwargs)\n File \"
/home/www-data/odoo/addons/web/controllers/main.py\", line 926, in _call_kw\n return call_kw(request.env[model], method, args, kwargs)\n File \"/home/www-data/odoo/odoo/api.py\", line 771, in __getitem__\n return self.registry[model_name]._browse(
(), self)\n File \"/home/www-data/odoo/odoo/modules/registry.py\", line 179, in __getitem__\n return self.models[model_name]\nKeyError: 'stock.inventory2'\n",
"message": "stock.inventory2",
"arguments": [
"stock.inventory2"
],
"exception_type": "internal_error"
}
}
}
В случае возникновения ошибки, в ответе появляется ключ error
, внутри которого вы сможете увидеть все необходимое для обработки ошибки:
code
- код ответа сервера, соотвествуют кодам ответов HTTP сервераmessage
- краткое описание ошибки со стороны платформыdata
- собственно данные по самой ошибкеname
- имя ошибкиdebug
- полный трейлог ошибкиmessage
- сообщение с описанием ошибкиarguments
- список аргументов переданных в вознкшее исключениеexception_type
- тип возникшего исключения
Чаще всего для пользователя самым информативным является поле message
. Я рекомендую его показывать пользователям в случае возникновения ошибок. Данным механизмом разработчик серверной части может пользоваться для уведомления пользователей о неправильных действиях
Здесь вы можете ознакомться с полным скриптом
import requests
import json
import os
import io
import uuid
script_dir = os.path.dirname(os.path.abspath(__file__))
# Переменная, которая хранит в себе путь к файлу с информацией о сессии
SESSION_FILE = os.path.join(script_dir,"session.json")
# Адрес сервера, к которму мы планируем подключаться
HOST = 'www.адрес.сервера.by'
# Протокол, http или https
PROTOCOL = "http"
# Порт на котором слушает наш сервер, для https по умолчанию 443
PORT = "80"
# Объединяем все в одну строку
PROTOCOL_HOST_PORT = f"{PROTOCOL}://{HOST}:{PORT}"
# Указываем авторизационные данные, обратите внимание что тут три параметра, логи, пароль и имя базы данных
EMAIL = "admin"
PASSWORD = "admin"
DATABASE = "demo"
# Обращаю ваше внимание, это не JSON данные, а тип данных Dictionary(Словарь) в Python,
# но да, они очень похожи. При отправке вам нужно будет еще преобразовать их в
# JSON строку(байтовый массив) и закрепитьв теле POST запроса
json_request_data = {"jsonrpc": "2.0","params":{
"db":DATABASE,
"login":EMAIL,
"password":PASSWORD
}
}
def get_session_info():
#Объявлеям заколовки, которые сделают наш запрос json запросом
headers = {'Content-Type': 'application/json','Accept': 'text/plain'}
# тут мы создаем полный URL запроса
URL = PROTOCOL_HOST_PORT + "/web/session/authenticate"
# Делаем сам запрос,обратите внимание, что наш словарь с авторизационными данными,
# мы преобразуем в строку с помощью метода json.dumps, т.е. на его менсто можно поставить обычную JSON строку
send_data = requests.post(URL,headers=headers,data=json.dumps(json_request_data))
# В этот момент нам может придти ответ двух видов
# Во первых там может быть информация об уже существующей сессии
# от этого пользователя и в заголовке будет содержаться сообщение
# вида session_id=1234567890000
# Поэтому если он там есть, мы его сразу извлекаем и подкидываем в качестве
# информации о сессии
set_cookies = send_data.headers["Set-Cookie"]
if "session_id" in set_cookies:
session_id = set_cookies.split(";")[0].split("=")[1]
return {"result":{"session_id":session_id}}
# Если же в заголовке нет информации о сессии, то из заголовка ответа "Set-Cookie" мы извлекаем authorization_token
authorization_token = set_cookies
headers = {'Cookie':authorization_token,'Content-Type': 'application/json','Accept': 'text/plain'}
# Обратите внимание на оформление запроса, в качестве тела запроса надо указать строку с пустыми {}
get_session_info = requests.post(PROTOCOL_HOST_PORT +'/web/session/get_session_info',headers=headers,data="{}")
# Полученный текст ответа и являет собой JSON строку с нужной нам информацией, и мы можем ее развернуть в
# объект для удобного взаимодействия с помощью метода json.loads. Но ничего не мешает работать
# с ответом как с обычной строкой
session_info = json.loads(get_session_info.text)
return session_info
def check_if_session_is_valid(session_id):
if session_id == 'fail':
return False
# Обратите внимание на оформление параметра 'Cookie', теперь его значение будет иметь примерно такоей вид
# 'Cookie': "session_id=250769263b42fb907b741608cc99ccdbeeac5940"
headers = {'Cookie': "session_id="+session_id,'Content-Type': 'application/json','Accept': 'text/plain'}
# Делаем запрос на URI проверки валидности сессии
check_session = requests.post(PROTOCOL_HOST_PORT +'/web/session/check',headers=headers,data="{}")
# Полученную JSON строку парсим в объект
response = json.loads(check_session.text)
# Если в ответе есть ключ error, то кука не валидна, что мы и выводим, если такого ключа нет, то мы
# наша сессия валида
error = response.get('error',False)
if error:
print(json.dumps(error.get('message'), indent=4,ensure_ascii=False))
return False
return True
def write_session_info_to_file(session_info,file_name):
with io.open(file_name, 'w', encoding='utf8') as config_file:
json.dump(session_info, config_file, indent=4)
validate_session_id = False
if os.path.isfile(SESSION_FILE):
session_info = json.loads(open(SESSION_FILE).read())
session_id = session_info.get("result",{}).get("session_id","fail")
if check_if_session_is_valid(session_id):
validate_session_id = session_id
if not validate_session_id or not os.path.isfile(SESSION_FILE):
session_info = get_session_info()
session_id = session_info.get("result",{}).get("session_id","fail")
if check_if_session_is_valid(session_id):
validate_session_id = session_id
write_session_info_to_file(session_info,SESSION_FILE)
if validate_session_id:
# Генерируем уникальный идентификатор запроса
req_uuid = uuid.uuid4()
json_rpc_data = {
"jsonrpc": "2.0",
"id": str(req_uuid),
"method": "call",
"params": {
"model": "ir.default", # Имя модели, к которой хотим обратиться
"method": "search_read", # Метод модели, который мы хотим вызвать для исполнения
"args": [ # позиционный аргрументы для метода
# ['id','parent_id','name']
],
"kwargs":{ # ключевые аргументы для метода
"domain":[
('id','=',1),
],
"fields": ['id','field_id','json_value'],
}
}
}
# Оформляем заголовки
headers = {'Cookie': "session_id="+validate_session_id,'Content-Type': 'application/json','Accept': 'text/plain'}
# Делаем запрос на URI удаленного вызова процедур
method_result = requests.post(PROTOCOL_HOST_PORT +'/web/dataset/call_kw',headers=headers,data=json.dumps(json_rpc_data))
# Полученную JSON строку парсим в объект
response = json.loads(method_result.text)
print(json.dumps(response, indent=4,ensure_ascii=False))
С помощью данного механизма мы можем выполнить любой метод в любой модели данных, к которым есть доступ у пользователя от имени которого мы запускаем выполнение