Browse Source

Merge 8909c65fde into c84ce7d809

pull/345/merge
drpsyko101 9 months ago
committed by GitHub
parent
commit
aec072cc08
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. BIN
      rest_api_odoo/Postman Collections/Odoo REST Api.postman_collection.zip
  2. 2
      rest_api_odoo/__manifest__.py
  3. 384
      rest_api_odoo/controllers/main.py
  4. 1
      rest_api_odoo/models/__init__.py
  5. 45
      rest_api_odoo/models/res_users.py
  6. 32
      rest_api_odoo/security/res_api_odoo_security.xml
  7. BIN
      rest_api_odoo/static/description/assets/screenshots/screenshot_2.png
  8. 120
      rest_api_odoo/static/description/index.html
  9. 18
      rest_api_odoo/views/res_users_views.xml

BIN
rest_api_odoo/Postman Collections/Odoo REST Api.postman_collection.zip

Binary file not shown.

2
rest_api_odoo/__manifest__.py

@ -33,7 +33,7 @@
"depends": ['base', 'web'],
"data": [
'security/ir.model.access.csv',
'views/res_users_views.xml',
'security/res_api_odoo_security.xml',
'views/connection_api_views.xml'
],
'images': ['static/description/banner.png'],

384
rest_api_odoo/controllers/main.py

@ -22,222 +22,244 @@
import json
import logging
from odoo import http
from odoo.http import request
from datetime import datetime
from odoo import http, models
from odoo.http import request, Response
from ast import literal_eval
_logger = logging.getLogger(__name__)
class RestApi(http.Controller):
"""This is a controller which is used to generate responses based on the
api requests"""
class IrHttp(models.AbstractModel):
"""This model is used to authenticate the api-key when sending a request"""
_inherit = "ir.http"
def auth_api_key(self, api_key):
@classmethod
def _auth_method_rest_api(cls):
"""This function is used to authenticate the api-key when sending a
request"""
user_id = request.env['res.users'].search([('api_key', '=', api_key)])
if api_key is not None and user_id:
response = True
elif not user_id:
response = ('<html><body><h2>Invalid <i>API Key</i> '
'!</h2></body></html>')
else:
response = ("<html><body><h2>No <i>API Key</i> Provided "
"!</h2></body></html>")
try:
api_key = request.httprequest.headers.get("Authorization").lstrip("Bearer ")
if not api_key:
raise PermissionError("Invalid API key provided.")
user_id = request.env["res.users.apikeys"]._check_credentials(
scope="rpc", key=api_key
)
request.update_env(user_id)
except Exception as e:
_logger.error(e)
return Response(
json.dumps({"message": e.args[0]}),
status=401,
content_type="application/json",
)
class RestApi(http.Controller):
"""This is a controller which is used to generate responses based on the
api requests"""
def simplest_type(self, input):
"""Try cast input into native Python class, otherwise return as string"""
try:
return literal_eval(input)
except Exception:
# Handle lowercase booleans
if input == "true":
return True
if input == "false":
return False
return input
return response
def sanitize_records(self, records):
"""Sanitize records for response"""
for record in records:
for key, value in record.items():
# Manually convert datetime fields to string format
if isinstance(value, datetime):
record[key] = value.isoformat()
return records
def generate_response(self, method, model, rec_id):
def generate_response(self, method, **query):
"""This function is used to generate the response based on the type
of request and the parameters given"""
option = request.env['connection.api'].search(
[('model_id', '=', model)], limit=1)
try:
model = query.pop("model")
option = request.env["connection.api"].search(
[("model_id", "=", model)], limit=1
)
model_name = option.model_id.model
model_display_name = option.model_id.name
if method != 'DELETE':
try:
data = json.loads(request.httprequest.data)
else:
except Exception:
data = {}
fields = []
if data:
for field in data['fields']:
for field in data["fields"]:
fields.append(field)
if not fields and method != 'DELETE':
return ("<html><body><h2>No fields selected for the model"
"</h2></body></html>")
if not option:
return ("<html><body><h2>No Record Created for the model"
"</h2></body></html>")
try:
if method == 'GET':
# Return records' ID by default if not specified
if not fields:
fields.append("id")
# Get all model's fields if wildcard is used
if "*" in fields:
fields = []
for field in data['fields']:
record_fields = request.env[str(model_name)].fields_get(
[], attributes=["type"]
)
for field, value in record_fields.items():
value_type = value.get("type")
if not (value_type == "binary"):
fields.append(field)
if not option:
raise NotImplementedError("No Record Created for the model. ")
if method == "GET":
if not option.is_get:
return ("<html><body><h2>Method Not Allowed"
"</h2></body></html>")
else:
datas = []
if rec_id != 0:
partner_records = request.env[
str(model_name)].search_read(
domain=[('id', '=', rec_id)],
fields=fields
)
data = json.dumps({
'records': partner_records
})
datas.append(data)
return request.make_response(data=datas)
else:
partner_records = request.env[
str(model_name)].search_read(
domain=[],
fields=fields
)
data = json.dumps({
'records': partner_records
})
datas.append(data)
return request.make_response(data=datas)
except:
return ("<html><body><h2>Invalid JSON Data"
"</h2></body></html>")
if method == 'POST':
raise NameError()
limit = 0
if query.get("limit"):
limit = int(str(query.get("limit")))
offset = 0
if query.get("offset"):
offset = int(str(query.get("offset")))
domains = []
for key, value in query.items():
if not (key == "limit" or key == "offset"):
domains.append((key, "=", self.simplest_type(value)))
partner_records = request.env[str(model_name)].search_read(
domains, fields, limit=limit, offset=offset
)
return Response(
json.dumps({"records": self.sanitize_records(partner_records)}),
content_type="application/json",
)
if method == "POST":
if not option.is_post:
return ("<html><body><h2>Method Not Allowed"
"</h2></body></html>")
else:
try:
raise NotImplementedError()
if not data or "values" not in data:
raise ValueError("No Data Provided")
data = json.loads(request.httprequest.data)
datas = []
new_resource = request.env[str(model_name)].create(
data['values'])
partner_records = request.env[
str(model_name)].search_read(
domain=[('id', '=', new_resource.id)],
fields=fields
)
new_data = json.dumps({'New resource': partner_records, })
datas.append(new_data)
return request.make_response(data=datas)
except:
return ("<html><body><h2>Invalid JSON Data"
"</h2></body></html>")
if method == 'PUT':
new_resource = request.env[str(model_name)].create(data["values"])
partner_records = request.env[str(model_name)].search_read(
[("id", "=", new_resource.id)], fields
)
return Response(
json.dumps({"new_record": self.sanitize_records(partner_records)}),
status=201,
content_type="application/json",
)
if method == "PUT":
if not option.is_put:
return ("<html><body><h2>Method Not Allowed"
"</h2></body></html>")
else:
if rec_id == 0:
return ("<html><body><h2>No ID Provided"
"</h2></body></html>")
else:
resource = request.env[str(model_name)].browse(
int(rec_id))
raise NotImplementedError()
if "id" not in query:
raise ValueError("No ID Provided")
if not data or "values" not in data:
raise ValueError("No Data Provided")
resource_id = str(query.get("id"))
resource = request.env[str(model_name)].browse(int(resource_id))
if not resource.exists():
return ("<html><body><h2>Resource not found"
"</h2></body></html>")
else:
try:
datas = []
raise ValueError("Resource not found")
data = json.loads(request.httprequest.data)
resource.write(data['values'])
partner_records = request.env[
str(model_name)].search_read(
domain=[('id', '=', resource.id)],
fields=fields
)
new_data = json.dumps(
{'Updated resource': partner_records,
})
datas.append(new_data)
return request.make_response(data=datas)
except:
return ("<html><body><h2>Invalid JSON Data "
"!</h2></body></html>")
if method == 'DELETE':
resource.write(data["values"])
partner_records = request.env[str(model_name)].search_read(
[("id", "=", resource.id)], fields
)
return Response(
json.dumps(
{"updated_record": self.sanitize_records(partner_records)}
),
content_type="application/json",
)
if method == "DELETE":
if not option.is_delete:
return ("<html><body><h2>Method Not Allowed"
"</h2></body></html>")
else:
if rec_id == 0:
return ("<html><body><h2>No ID Provided"
"</h2></body></html>")
else:
resource = request.env[str(model_name)].browse(
int(rec_id))
raise NotImplementedError()
if "id" not in query:
raise ValueError("No ID Provided")
resource_id = str(query.get("id"))
resource = request.env[str(model_name)].browse(int(resource_id))
if not resource.exists():
return ("<html><body><h2>Resource not found"
"</h2></body></html>")
else:
records = request.env[
str(model_name)].search_read(
domain=[('id', '=', resource.id)],
fields=['id', 'display_name']
)
remove = json.dumps(
{"Resource deleted": records,
})
raise ValueError("Resource not found")
partner_records = request.env[str(model_name)].search_read(
[("id", "=", resource.id)], fields
)
resource.unlink()
return request.make_response(data=remove)
return Response(
json.dumps(
{
"message": "Resource deleted",
"data": self.sanitize_records(partner_records),
}
),
status=202,
content_type="application/json",
)
@http.route(['/send_request'], type='http',
auth='none',
methods=['GET', 'POST', 'PUT', 'DELETE'], csrf=False)
# If not using any method above, simply return an error
raise NotImplementedError()
except ValueError as e:
return Response(
json.dumps({"message": e.args[0]}),
status=403,
content_type="application/json",
)
except NotImplementedError as e:
return Response(
json.dumps(
{
"message": f"Method not allowed. {e.args[0]}Please contact your admininstrator to enable {method} method for {model_display_name or 'this'} record."
}
),
status=405,
content_type="application/json",
)
except Exception as e:
_logger.error(e)
return Response(
json.dumps({"message": f"Internal server error. {e.args[0]}"}),
status=500,
content_type="application/json",
)
@http.route(
["/send_request"],
type="http",
auth="rest_api",
methods=["GET", "POST", "PUT", "DELETE"],
csrf=False,
)
def fetch_data(self, **kw):
"""This controller will be called when sending a request to the
specified url, and it will authenticate the api-key and then will
generate the result"""
http_method = request.httprequest.method
api_key = request.httprequest.headers.get('api-key')
auth_api = self.auth_api_key(api_key)
model = kw.get('model')
username = request.httprequest.headers.get('login')
password = request.httprequest.headers.get('password')
request.session.authenticate(request.session.db, username,
password)
model_id = request.env['ir.model'].search(
[('model', '=', model)])
model = kw.pop("model")
model_id = request.env["ir.model"].search([("model", "=", model)])
if not model_id:
return ("<html><body><h3>Invalid model, check spelling or maybe "
"the related "
"module is not installed"
"</h3></body></html>")
if auth_api == True:
if not kw.get('Id'):
rec_id = 0
else:
rec_id = int(kw.get('Id'))
result = self.generate_response(http_method, model_id.id, rec_id)
return result
else:
return auth_api
@http.route(['/odoo_connect'], type="http", auth="none", csrf=False,
methods=['GET'])
def odoo_connect(self, **kw):
"""This is the controller which initializes the api transaction by
generating the api-key for specific user and database"""
username = request.httprequest.headers.get('login')
password = request.httprequest.headers.get('password')
db = request.httprequest.headers.get('db')
try:
request.session.update(http.get_default_session(), db=db)
auth = request.session.authenticate(request.session.db, username,
password)
user = request.env['res.users'].browse(auth)
api_key = request.env.user.generate_api(username)
datas = json.dumps({"Status": "auth successful",
"User": user.name,
"api-key": api_key})
return request.make_response(data=datas)
except:
return ("<html><body><h2>wrong login credentials"
"</h2></body></html>")
return Response(
json.dumps(
{
"message": "Invalid model, check spelling or maybe the related module is not installed"
}
),
status=403,
content_type="application/json",
)
return self.generate_response(http_method, model=model_id.id, **kw)

1
rest_api_odoo/models/__init__.py

@ -21,4 +21,3 @@
#############################################################################
from . import connection_api
from . import res_users

45
rest_api_odoo/models/res_users.py

@ -1,45 +0,0 @@
# -*- coding:utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
import uuid
from odoo import fields, models
class UserLogin(models.Model):
"""This class is used to inherit users and add api key generation"""
_inherit = 'res.users'
api_key = fields.Char(string="API Key", readonly=True,
help="Api key for connecting with the "
"Database.The key will be "
"generated when authenticating "
"rest api.")
def generate_api(self, username):
"""This function is used to generate api-key for each user"""
users = self.env['res.users'].sudo().search([('login', '=', username)])
if not users.api_key:
users.api_key = str(uuid.uuid4())
key = users.api_key
else:
key = users.api_key
return key

32
rest_api_odoo/security/res_api_odoo_security.xml

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record model="ir.module.category" id="module_category_rest_api_odoo">
<field name="name">REST API</field>
<field name="description">Helps you manage your REST API records.</field>
<field name="sequence">17</field>
</record>
<record id="group_rest_api_odoo_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id"
ref="rest_api_odoo.module_category_rest_api_odoo" />
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]" />
</record>
<record id="base.default_user" model="res.users">
<field name="groups_id"
eval="[(4,ref('rest_api_odoo.group_rest_api_odoo_manager'))]" />
</record>
<record id="rest_api_odoo_rule_manager" model="ir.rule">
<field name="name">All REST APIs</field>
<field name="model_id" ref="model_connection_api" />
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups"
eval="[(4, ref('rest_api_odoo.group_rest_api_odoo_manager'))]" />
</record>
</data>
</odoo>

BIN
rest_api_odoo/static/description/assets/screenshots/screenshot_2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

120
rest_api_odoo/static/description/index.html

@ -23,7 +23,7 @@
<div class="col-sm-12 col-md-12 col-lg-12">
<!-- APP HERO -->
<h1 style="color: #FFFFFF; font-weight: bolder; font-size: 50px; text-align: center; margin-top: 50px;">
Odoo rest API</h1>
Odoo Rest API</h1>
<img src="./assets/screenshots/hero.gif" class="img-responsive"
width="100%" height="auto"/>
@ -138,7 +138,7 @@
<div class="d-flex align-items-center"
style="margin-top: 30px; margin-bottom: 30px">
<img src="assets/misc/check-box.png" class="mr-2"/>
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Authentication using the generated api key.</span>
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Authentication using the generated API key.</span>
</div>
</div>
<div class="col-sm-12 col-md-6">
@ -217,29 +217,21 @@
<li>First of all, we have to add a new parameter in odoo conf.
file.
</li>
<li><b>server_wide_modules = web, base, rest_api_odoo</b><br/>
<li><code>server_wide_modules = web, base, rest_api_odoo</code><br/>
- This will allow us to send request to server without
selecting database first.<br/>- Incase if you have to
uninstall the module , you have to remove this parameter.
<br/>- Next we can install the module.
</li>
<li>After installing the Rest api app we can see a new api key
<li>After installing the Rest API app we can see a new API key
field in users.
</li>
- Next we have to generate the api-key for the current
- Next we have to generate the API-key for the current
user.<br/>
<li>You can import the postman collections provided in the app
folder for authentication and interacting with database in
various methods.
</li>
</ul>
</p>
<img src="assets/screenshots/screenshot_1.png"
class="img-thumbnail">
<hr>
<center>
<img src="assets/screenshots/screenshot_9.png"
class="img-thumbnail"></center>
</div>
<div style="display: block; margin: 30px auto;">
@ -248,27 +240,8 @@
</h3>
<p style="font-weight: 400; font-family: 'Montserrat', sans-serif; font-size: 14px;">
<ul>
<li>Next you have to select the database and login.</li>
<li>We have attached <b>Postman collections</b> through which
you can
authenticate rest api.
</li>
<li>First, extract the <b>zip</b> file. Then, you will obtain the JSON-format file, which you can directly import into <b>POSTMAN.</b></li>
<li>The url format will be like this - <b>http://cybrosys:8016/odoo_connect</b>
Replace 'cybrosys:8016' with your localhost port number.
</li>
<li>You have to provide database name, username and password
through the headers while sending request.
</li>
<li>If the authentication is successful , an api key will be
generated for the current user.
</li>
<li>This key will be used when sending api requests to
database.
</li>
<li>The response will be like this - <b> {"Status": "auth
successful", "User": "Mitchell Admin", "api-key":
"66c2ebab-d4dc-42f0-87d0-d1646e887569"}.</b></li>
<li>Next you have to generate the API key under <b>My Profile</b> > <b>Account Security</b>.</li>
<li>Set the name of the API key according to your needs.</li>
</ul>
</p>
<img src="assets/screenshots/screenshot_2.png"
@ -276,17 +249,17 @@
</div>
<div style="display: block; margin: 30px auto;">
<h3 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">
Create records in Rest api app
Create records in Rest API app
</h3>
<p style="font-weight: 400; font-family: 'Montserrat', sans-serif; font-size: 14px;">
<ul>
<li>After rest api authentication, we can create records in the
rest api app.
<li>After creating the API key, we can create records in the
Rest API app.
</li>
<li>Here we can choose the model, and also we can
choose the http methods.
</li>
<li>The api response will be based on these records.</li>
<li>The API response will be based on these records.</li>
</ul>
</p>
<img src="assets/screenshots/screenshot_3.png"
@ -307,26 +280,28 @@
</h3>
<p style="font-weight: 400; font-family: 'Montserrat', sans-serif; font-size: 14px;">
<ul>
<li>You can send GET request to retrieve data from the
<li>You can send <code>GET</code> request to retrieve data from the
database.
</li>
<li>The postman collection has been provided with app files for
sending request from postman.
</li>
<li>You have to provide username, password and api key through
the header.
<li>You have to provide the API key in the header: <code>Authorization: Bearer {API_KEY}</code>
where <code>{API_KEY}</code> should be replaced with the actual API key.</li>
</li>
<li>Model can be passed as argument as the technical name , and
also if you want
specific record you can provide the id as well.
<li>Model can be passed as argument as the technical name, and
also if you want to get a specific record you can provide any
of the related fields to the module as well.
</li>
<li>The format for GET method will be like this - <b>http://cybrosys:8016/send_request?model=res.partner&Id=10.</b>
<li>The format for GET method will be like this - <code>http://cybrosys:8016/send_request?model=res.partner&id=10.</code>
</li>
<li>We can specify the fields inside the JSON data, and it will
be like this - <b>{"fields": ["name", "email"]}.</b></li>
<li>This is the format of api response - <b>{"records": [{"id":
be like this - <code>{"fields": ["name", "email"]}</code>.
If no fields are passed , it will returns just the record's ID.
To get all of the fields, set the fields to wildcard - <code>{"fields": ["*"]}</code>.
Pagination can also be used by adding <code>{"limit": 10, "offset": 0}</code>
to the query parameter above.
</li>
<li>This is the format of API response - <code>{"records": [{"id":
10, "email": "deco.addict82@example.com", "name": "Deco
Addict"}]}.</b>
Addict"}]}</code>.
</li>
</ul>
@ -338,31 +313,28 @@
</h3>
<p style="font-weight: 400; font-family: 'Montserrat', sans-serif; font-size: 14px;">
<ul>
<li>Using POST method , you can create new records in the
<li>Using <code>POST</code> method , you can create new records in the
database.
</li>
<li>Just make sure you enabled POST method for the model record
in rest api app , otherwise you will get <b>'method not
<li>Just make sure you enabled <code>POST</code> method for the model record
in Rest API app , otherwise you will get <b>'method not
allowed'</b> message.
</li>
<li>For creating record you have to provide the JSON data along
with the model.
</li>
<li>You can make use of the postman collection that we have
added with app files.
</li>
<li>The format for sending POST request will be like this - <b>http://cybrosys:8016/send_request?model=res.partner.</b>
<li>The format for sending POST request will be like this - <code>http://cybrosys:8016/send_request?model=res.partner.</code>
</li>
<li>This is the format for JSON data - <b>{
<li>This is the format for JSON data - <code>{
"fields" :["name", "phone"] ,
"values": {"name": "abc",
"phone":"55962441552"
} }.</b>
} }.</code>
</li>
<li>Make sure the data entered in correct format otherwise you
will get <b>'Invalid JSON data' message.</b></li>
<li>Response will be in this format - <b>{"New resource":
[{"id": 51, "name": "abc", "phone": "55962441552"}]}.</b>
<li>Response will be in this format - <code>{"New resource":
[{"id": 51, "name": "abc", "phone": "55962441552"}]}</code>.
</li>
</ul>
</p>
@ -373,24 +345,20 @@
Update Records
</h3>
<ul>
<li>Updation of records in the database can be done with PUT
<li>Updation of records in the database can be done with <code>PUT</code>
method.
</li>
<li>You have to provide the model and also the id or the record
that you want to update.
</li>
<li>You can use the Postman collection that we have provided and , you
will be always have to send request with your login
credentials. Otherwise, it will be showing access denied.
</li>
<li>The format for sending PUT request will be like this - <b>http://cybrosys:8016/send_request?model=res.partner&Id=46.</b>
<li>The format for sending <code>PUT</code> request will be like this - <code>http://cybrosys:8016/send_request?model=res.partner&Id=46.</code>
</li>
<li>Here too you have to provide the JSON data through which the
updates will be done.
</li>
<li>The response format will be like this - <b>{"Updated
<li>The response format will be like this - <code>{"Updated
resource": [{"id": 46, "email": "abc@example.com", "name":
"Toni"}]}.</b></li>
"Toni"}]}</code>.</li>
</ul>
</p>
@ -401,20 +369,20 @@
</h3>
<p style="font-weight: 400; font-family: 'Montserrat', sans-serif; font-size: 14px;">
<ul>
<li>Database records can be deleted by sending DELETE method
<li>Database records can be deleted by sending <code>DELETE</code> method
request.
</li>
<li>For the deletion we have to provide the Model and the record
id that we want to delete.
</li>
<li>Make sure you have permission to delete files for the
selected model in the rest api record.
selected model in the Rest API record.
</li>
<li>The delete request format will be like this - <b>http://cybrosys:8016/send_request?model=res.partner&Id=46.</b>
<li>The delete request format will be like this - <code>http://cybrosys:8016/send_request?model=res.partner&Id=46.</code>
</li>
<li> The response after successful deletion will be -<b>
<li> The response after successful deletion will be -<code>
{"Resource deleted": [{"id": 46, "email": "abc@example.com",
"name": "Toni"}]}.</b></li>
"name": "Toni"}]}</code>.</li>
</ul>
</p>
@ -843,7 +811,7 @@
<div>
<h4>WhatsApp</h4>
<p style="line-height: 100%;">Say hi to us on WhatsApp!</p>
<a href="https://api.whatsapp.com/send?phone=918606827707">
<a href="https://API.whatsapp.com/send?phone=918606827707">
<p style="font-weight: 400; font-size: 28px; line-height: 80%; color: #714B67;">
+91 86068
27707</p>

18
rest_api_odoo/views/res_users_views.xml

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Inherited user view for Adding API key. -->
<record id="view_users_form" model="ir.ui.view">
<field name="name">view.users.form.inherit.rest.api.odoo</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="model">res.users</field>
<field name="arch" type="xml">
<xpath expr="//page[@name='access_rights']" position="after">
<page string="API" name="rest-api">
<group>
<field name="api_key" groups="base.group_user"/>
</group>
</page>
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save