diff options
| author | vbh-odoo <vbh@odoo.com> | 2016-05-20 11:06:33 +0200 |
|---|---|---|
| committer | Jérome Maes <jem@openerp.com> | 2016-05-31 09:09:22 +0200 |
| commit | 3174d2f56ee9a683f385b5eca94678283434cb4a (patch) | |
| tree | fbc49d7cfbb2d115d7a1d4afa57d000f8544fd7b /addons | |
| parent | b581e2004f0921f95c6ac10a64b76abf7a80f2e9 (diff) | |
[MIG] google_drive : migrate to new api
Diffstat (limited to 'addons')
| -rw-r--r-- | addons/google_drive/__openerp__.py | 2 | ||||
| -rw-r--r-- | addons/google_drive/data/google_drive_data.xml | 8 | ||||
| -rw-r--r-- | addons/google_drive/data/google_drive_demo.xml | 7 | ||||
| -rw-r--r-- | addons/google_drive/models/google_drive.py | 229 | ||||
| -rw-r--r-- | addons/google_drive/models/res_config.py | 53 | ||||
| -rw-r--r-- | addons/google_drive/views/google_drive_templates.xml | 18 | ||||
| -rw-r--r-- | addons/google_drive/views/google_drive_views.xml | 25 |
7 files changed, 170 insertions, 172 deletions
diff --git a/addons/google_drive/__openerp__.py b/addons/google_drive/__openerp__.py index e91cb487d76..b58d4b489f2 100644 --- a/addons/google_drive/__openerp__.py +++ b/addons/google_drive/__openerp__.py @@ -10,9 +10,9 @@ 'data': [ 'security/ir.model.access.csv', 'data/google_drive_data.xml', - 'views/res_config_views.xml', 'views/google_drive_views.xml', 'views/google_drive_templates.xml', + 'views/res_config_views.xml', ], 'demo': [ 'data/google_drive_demo.xml' diff --git a/addons/google_drive/data/google_drive_data.xml b/addons/google_drive/data/google_drive_data.xml index 72e541df56e..3637c50caaa 100644 --- a/addons/google_drive/data/google_drive_data.xml +++ b/addons/google_drive/data/google_drive_data.xml @@ -1,13 +1,13 @@ <?xml version="1.0"?> -<openerp> +<odoo> <data noupdate="1"> - + <record id="config_google_drive_client_id" model="ir.config_parameter"> <field name="key">google_drive_client_id</field> <field name="value">598905559630.apps.googleusercontent.com</field> <field name="group_ids" eval="[(4, ref('base.group_system'))]" /> </record> - + <record id="config_google_drive_client_secret" model="ir.config_parameter"> <field name="key">google_drive_client_secret</field> <field name="value">vTmou73c-njP-1qCxm7qx7QE</field> @@ -15,4 +15,4 @@ </record> </data> -</openerp>
\ No newline at end of file +</odoo>
\ No newline at end of file diff --git a/addons/google_drive/data/google_drive_demo.xml b/addons/google_drive/data/google_drive_demo.xml index d222da057fa..59bbbaed87c 100644 --- a/addons/google_drive/data/google_drive_demo.xml +++ b/addons/google_drive/data/google_drive_demo.xml @@ -1,5 +1,5 @@ <?xml version="1.0"?> -<openerp> +<odoo> <data noupdate="1"> <!-- filter demo --> @@ -18,7 +18,6 @@ <field name="google_drive_template_url">https://docs.google.com/spreadsheet/ccc?key=1eIM-q7PX9wp1vEUUdUvrmAe8V3Do60vXDETulNmI9mA#gid=0</field> <field name="name_template">Partner Review %(name)s</field> </record> - - </data> -</openerp> + </data> +</odoo> diff --git a/addons/google_drive/models/google_drive.py b/addons/google_drive/models/google_drive.py index 97d13ec1a5c..655a28d3e23 100644 --- a/addons/google_drive/models/google_drive.py +++ b/addons/google_drive/models/google_drive.py @@ -1,85 +1,92 @@ +# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging - -from openerp import SUPERUSER_ID -from openerp.addons.google_account import TIMEOUT -from openerp.osv import fields, osv -from openerp.tools.translate import _ -from openerp.tools.safe_eval import safe_eval as eval -from openerp.exceptions import UserError - -import werkzeug.urls -import urllib2 import json import re -import openerp +import urllib2 +import werkzeug.urls + +from odoo import api, fields, models +from odoo.exceptions import RedirectWarning, UserError +from odoo.tools.safe_eval import safe_eval as eval +from odoo.tools.translate import _ + +from odoo.addons.google_account import TIMEOUT _logger = logging.getLogger(__name__) -class config(osv.Model): +class GoogleDrive(models.Model): + _name = 'google.drive.config' _description = "Google Drive templates config" - def get_google_drive_url(self, cr, uid, config_id, res_id, template_id, context=None): - config = self.browse(cr, SUPERUSER_ID, config_id, context=context) - model = config.model_id - filter_name = config.filter_id and config.filter_id.name or False - record = self.pool.get(model.model).read(cr, uid, [res_id], context=context)[0] - record.update({'model': model.name, 'filter': filter_name}) - name_gdocs = config.name_template + @api.multi + def get_google_drive_url(self, res_id, template_id): + self.ensure_one() + self = self.sudo() + + model = self.model_id + filter_name = self.filter_id.name if self.filter_id else False + record = self.env[model.model].browse(res_id).read()[0] + record.update({ + 'model': model.name, + 'filter': filter_name + }) + name_gdocs = self.name_template try: name_gdocs = name_gdocs % record except: raise UserError(_("At least one key cannot be found in your Google Drive name pattern")) - attach_pool = self.pool.get("ir.attachment") - attach_ids = attach_pool.search(cr, uid, [('res_model', '=', model.model), ('name', '=', name_gdocs), ('res_id', '=', res_id)]) + attachments = self.env["ir.attachment"].search([('res_model', '=', model.model), ('name', '=', name_gdocs), ('res_id', '=', res_id)]) url = False - if attach_ids: - attachment = attach_pool.browse(cr, uid, attach_ids[0], context) - url = attachment.url + if attachments: + url = attachments[0].url else: - url = self.copy_doc(cr, uid, res_id, template_id, name_gdocs, model.model, context).get('url') + url = self.copy_doc(res_id, template_id, name_gdocs, model.model).get('url') return url - def get_access_token(self, cr, uid, scope=None, context=None): - ir_config = self.pool['ir.config_parameter'] - google_drive_refresh_token = ir_config.get_param(cr, SUPERUSER_ID, 'google_drive_refresh_token') + @api.model + def get_access_token(self, scope=None): + Config = self.env['ir.config_parameter'] + google_drive_refresh_token = Config.sudo().get_param('google_drive_refresh_token') + user_is_admin = self.env['res.users'].browse(self.env.user.id)._is_admin() if not google_drive_refresh_token: - if self.pool['res.users']._is_admin(cr, uid, [uid]): - model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'base_setup', 'action_general_configuration') + if user_is_admin: + dummy, action_id = self.env['ir.model.data'].get_object_reference('base_setup', 'action_general_configuration') msg = _("You haven't configured 'Authorization Code' generated from google, Please generate and configure it .") - raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel')) + raise RedirectWarning(msg, action_id, _('Go to the configuration panel')) else: raise UserError(_("Google Drive is not yet configured. Please contact your administrator.")) - google_drive_client_id = ir_config.get_param(cr, SUPERUSER_ID, 'google_drive_client_id') - google_drive_client_secret = ir_config.get_param(cr, SUPERUSER_ID, 'google_drive_client_secret') + google_drive_client_id = Config.sudo().get_param('google_drive_client_id') + google_drive_client_secret = Config.sudo().get_param('google_drive_client_secret') #For Getting New Access Token With help of old Refresh Token - - data = werkzeug.url_encode(dict(client_id=google_drive_client_id, - refresh_token=google_drive_refresh_token, - client_secret=google_drive_client_secret, - grant_type="refresh_token", - scope=scope or 'https://www.googleapis.com/auth/drive')) + data = werkzeug.url_encode({ + 'client_id': google_drive_client_id, + 'refresh_token': google_drive_refresh_token, + 'client_secret': google_drive_client_secret, + 'grant_type': "refresh_token", + 'scope': scope or 'https://www.googleapis.com/auth/drive' + }) headers = {"Content-type": "application/x-www-form-urlencoded"} try: req = urllib2.Request('https://accounts.google.com/o/oauth2/token', data, headers) content = urllib2.urlopen(req, timeout=TIMEOUT).read() except urllib2.HTTPError: if user_is_admin: - model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'base_setup', 'action_general_configuration') + dummy, action_id = self.env['ir.model.data'].get_object_reference('base_setup', 'action_general_configuration') msg = _("Something went wrong during the token generation. Please request again an authorization code .") - raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel')) + raise RedirectWarning(msg, action_id, _('Go to the configuration panel')) else: raise UserError(_("Google Drive is not yet configured. Please contact your administrator.")) content = json.loads(content) return content.get('access_token') - def copy_doc(self, cr, uid, res_id, template_id, name_gdocs, res_model, context=None): - ir_config = self.pool['ir.config_parameter'] - google_web_base_url = ir_config.get_param(cr, SUPERUSER_ID, 'web.base.url') - access_token = self.get_access_token(cr, uid, context=context) + @api.model + def copy_doc(self, res_id, template_id, name_gdocs, res_model): + google_web_base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + access_token = self.get_access_token() # Copy template in to drive with help of new access token request_url = "https://www.googleapis.com/drive/v2/files/%s?fields=parents/id&access_token=%s" % (template_id, access_token) headers = {"Content-type": "application/x-www-form-urlencoded"} @@ -90,10 +97,17 @@ class config(osv.Model): raise UserError(_("The Google Template cannot be found. Maybe it has been deleted.")) parents_dict = json.loads(parents) - record_url = "Click on link to open Record in Odoo\n %s/?db=%s#id=%s&model=%s" % (google_web_base_url, cr.dbname, res_id, res_model) - data = {"title": name_gdocs, "description": record_url, "parents": parents_dict['parents']} + record_url = "Click on link to open Record in Odoo\n %s/?db=%s#id=%s&model=%s" % (google_web_base_url, self._cr.dbname, res_id, res_model) + data = { + "title": name_gdocs, + "description": record_url, + "parents": parents_dict['parents'] + } request_url = "https://www.googleapis.com/drive/v2/files/%s/copy?access_token=%s" % (template_id, access_token) - headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + headers = { + 'Content-type': 'application/json', + 'Accept': 'text/plain' + } data_json = json.dumps(data) # resp, content = Http().request(request_url, "POST", data_json, headers) req = urllib2.Request(request_url, data_json, headers) @@ -101,11 +115,15 @@ class config(osv.Model): content = json.loads(content) res = {} if content.get('alternateLink'): - attach_pool = self.pool.get("ir.attachment") - attach_vals = {'res_model': res_model, 'name': name_gdocs, 'res_id': res_id, 'type': 'url', 'url': content['alternateLink']} - res['id'] = attach_pool.create(cr, uid, attach_vals) + res['id'] = self.env["ir.attachment"].create({ + 'res_model': res_model, + 'name': name_gdocs, + 'res_id': res_id, + 'type': 'url', + 'url': content['alternateLink'] + }).id # Commit in order to attach the document to the current object instance, even if the permissions has not been written. - cr.commit() + self._cr.commit() res['url'] = content['alternateLink'] key = self._get_key_from_url(res['url']) request_url = "https://www.googleapis.com/drive/v2/files/%s/permissions?emailMessage=This+is+a+drive+file+created+by+Odoo&sendNotificationEmails=false&access_token=%s" % (key, access_token) @@ -114,10 +132,9 @@ class config(osv.Model): req = urllib2.Request(request_url, json.dumps(data), headers) urllib2.urlopen(req, timeout=TIMEOUT) except urllib2.HTTPError: - raise self.pool.get('res.config.settings').get_config_warning(cr, _("The permission 'reader' for 'anyone with the link' has not been written on the document"), context=context) - user = self.pool['res.users'].browse(cr, uid, uid, context=context) - if user.email: - data = {'role': 'writer', 'type': 'user', 'value': user.email} + raise self.env['res.config.settings'].get_config_warning(_("The permission 'reader' for 'anyone with the link' has not been written on the document")) + if self.env.user.email: + data = {'role': 'writer', 'type': 'user', 'value': self.env.user.email} try: req = urllib2.Request(request_url, json.dumps(data), headers) urllib2.urlopen(req, timeout=TIMEOUT) @@ -125,7 +142,8 @@ class config(osv.Model): pass return res - def get_google_drive_config(self, cr, uid, res_model, res_id, context=None): + @api.model + def get_google_drive_config(self, res_model, res_id): ''' Function called by the js, when no google doc are yet associated with a record, with the aim to create one. It will first seek for a google.docs.config associated with the model `res_model` to find out what's the template @@ -140,81 +158,68 @@ class config(osv.Model): if not res_id: raise UserError(_("Creating google drive may only be done by one at a time.")) # check if a model is configured with a template - config_ids = self.search(cr, uid, [('model_id', '=', res_model)], context=context) - configs = [] - for config in self.browse(cr, uid, config_ids, context=context): + configs = self.search([('model_id', '=', res_model)]) + config_values = [] + for config in configs: if config.filter_id: - if (config.filter_id.user_id and config.filter_id.user_id.id != uid): + if config.filter_id.user_id and config.filter_id.user_id.id != self.env.user.id: #Private continue domain = [('id', 'in', [res_id])] + eval(config.filter_id.domain) - local_context = context and context.copy() or {} - local_context.update(eval(config.filter_id.context)) - google_doc_configs = self.pool.get(config.filter_id.model_id).search(cr, uid, domain, context=local_context) + additionnal_context = eval(config.filter_id.context) + google_doc_configs = self.env[config.filter_id.model_id].with_context(**additionnal_context).search(domain) if google_doc_configs: - configs.append({'id': config.id, 'name': config.name}) + config_values.append({'id': config.id, 'name': config.name}) else: - configs.append({'id': config.id, 'name': config.name}) - return configs + config_values.append({'id': config.id, 'name': config.name}) + return config_values + + name = fields.Char('Template Name', required=True) + model_id = fields.Many2one('ir.model', 'Model', ondelete='set null', required=True) + model = fields.Char('Model', related='model_id.model', readonly=True) + filter_id = fields.Many2one('ir.filters', 'Filter', domain="[('model_id', '=', model)]") + google_drive_template_url = fields.Char('Template URL', required=True) + google_drive_resource_id = fields.Char('Resource Id', compute='_compute_ressource_id') + google_drive_client_id = fields.Char('Google Client', compute='_compute_client_id') + name_template = fields.Char('Google Drive Name Pattern', default='Document %(name)s', help='Choose how the new google drive will be named, on google side. Eg. gdoc_%(field_name)s', required=True) + active = fields.Boolean('Active', default=True) def _get_key_from_url(self, url): - mo = re.search("(key=|/d/)([A-Za-z0-9-_]+)", url) - if mo: - return mo.group(2) + word = re.search("(key=|/d/)([A-Za-z0-9-_]+)", url) + if word: + return word.group(2) return None - def _resource_get(self, cr, uid, ids, name, arg, context=None): + @api.multi + def _compute_ressource_id(self): result = {} - for data in self.browse(cr, uid, ids, context): - mo = self._get_key_from_url(data.google_drive_template_url) - if mo: - result[data.id] = mo + for record in self: + word = self._get_key_from_url(record.google_drive_template_url) + if word: + record.google_drive_resource_id = word else: raise UserError(_("Please enter a valid Google Document URL.")) return result - def _client_id_get(self, cr, uid, ids, name, arg, context=None): - result = {} - client_id = self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'google_drive_client_id') - for config_id in ids: - result[config_id] = client_id - return result + @api.multi + def _compute_client_id(self): + google_drive_client_id = self.env['ir.config_parameter'].sudo().get_param('google_drive_client_id') + for record in self: + record.google_drive_client_id = google_drive_client_id - _columns = { - 'name': fields.char('Template Name', required=True), - 'model_id': fields.many2one('ir.model', 'Model', ondelete='set null', required=True), - 'model': fields.related('model_id', 'model', type='char', string='Model', readonly=True), - 'filter_id': fields.many2one('ir.filters', 'Filter', domain="[('model_id', '=', model)]"), - 'google_drive_template_url': fields.char('Template URL', required=True, size=1024), - 'google_drive_resource_id': fields.function(_resource_get, type="char", string='Resource Id'), - 'google_drive_client_id': fields.function(_client_id_get, type="char", string='Google Client '), - 'name_template': fields.char('Google Drive Name Pattern', help='Choose how the new google drive will be named, on google side. Eg. gdoc_%(field_name)s', required=True), - 'active': fields.boolean('Active'), - } - - def onchange_model_id(self, cr, uid, ids, model_id, context=None): - res = {} - if model_id: - model = self.pool['ir.model'].browse(cr, uid, model_id, context=context) - res['value'] = {'model': model.model} + @api.onchange('model_id') + def _onchange_model_id(self): + if self.model_id: + self.model = self.model_id.model else: - res['value'] = {'filter_id': False, 'model': False} - return res + self.filter_id = False + self.model = False - _defaults = { - 'name_template': 'Document %(name)s', - 'active': True, - } - - def _check_model_id(self, cr, uid, ids, context=None): - config_id = self.browse(cr, uid, ids[0], context=context) - if config_id.filter_id and config_id.model_id.model != config_id.filter_id.model_id: + @api.constrains('model_id', 'filter_id') + def _check_model_id(self): + if self.filter_id and self.model_id.model != self.filter_id.model_id: return False return True - _constraints = [ - (_check_model_id, 'Model of selected filter is not matching with model of current template.', ['model_id', 'filter_id']), - ] - def get_google_scope(self): return 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file' diff --git a/addons/google_drive/models/res_config.py b/addons/google_drive/models/res_config.py index d0112824b48..57ba486e884 100644 --- a/addons/google_drive/models/res_config.py +++ b/addons/google_drive/models/res_config.py @@ -1,43 +1,30 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import SUPERUSER_ID -from openerp.osv import fields, osv +from odoo import api, fields, models -class base_config_settings(osv.TransientModel): +class BaseConfigSettings(models.TransientModel): _inherit = "base.config.settings" - def _get_drive_uri(self, cr, uid, ids, field_name, arg, context=None): - return { - wizard_id: self.default_get(cr, uid, ['google_drive_uri']).get('google_drive_uri') - for wizard_id in ids - } + @api.model + def _default_google_drive_authorization_code(self): + return self.env['ir.config_parameter'].sudo().get_param('google_drive_authorization_code') - def _get_wizard_ids(self, cr, uid, ids, context=None): - result = [] - if any(rec.key in ['google_drive_client_id', 'google_redirect_uri'] for rec in self.browse(cr, uid, ids, context=context)): - result.extend(self.pool['base.config.settings'].search(cr, uid, [], context=context)) - return result + google_drive_authorization_code = fields.Char(string='Authorization Code', default=_default_google_drive_authorization_code) + google_drive_uri = fields.Char(compute='_compute_drive_uri', string='URI', help="The URL to generate the authorization code from Google") - _columns = { - 'google_drive_authorization_code': fields.char('Authorization Code'), - 'google_drive_uri': fields.function(_get_drive_uri, string='URI', help="The URL to generate the authorization code from Google", type="char", store={ - 'ir.config_parameter': (_get_wizard_ids, None, 20), - }), # TODO: 1. in master, remove the store, there is no reason for this field to be stored. It's just a dynamic link. - # TODO: 2. when converted to the new API, the code to get the default value can be moved to the compute method directly, and the default value can be removed - # the only reason the default value is defined is because function fields are not computed in draft mode in the old API. - } - _defaults = { - 'google_drive_uri': lambda s, cr, uid, c: s.pool['google.service']._get_google_token_uri(cr, uid, 'drive', scope=s.pool['google.drive.config'].get_google_scope(), context=c), - 'google_drive_authorization_code': lambda s, cr, uid, c: s.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'google_drive_authorization_code', context=c), - } + @api.depends('google_drive_authorization_code') + def _compute_drive_uri(self): + google_drive_uri = self.env['google.service']._get_google_token_uri('drive', scope=self.env['google.drive.config'].get_google_scope()) + for config in self: + config.google_drive_uri = google_drive_uri - def set_google_authorization_code(self, cr, uid, ids, context=None): - ir_config_param = self.pool['ir.config_parameter'] - config = self.browse(cr, uid, ids[0], context) - auth_code = config.google_drive_authorization_code - if auth_code and auth_code != ir_config_param.get_param(cr, uid, 'google_drive_authorization_code', context=context): - refresh_token = self.pool['google.service'].generate_refresh_token(cr, uid, 'drive', config.google_drive_authorization_code, context=context) - ir_config_param.set_param(cr, uid, 'google_drive_authorization_code', auth_code, groups=['base.group_system']) - ir_config_param.set_param(cr, uid, 'google_drive_refresh_token', refresh_token, groups=['base.group_system']) + @api.multi + def set_google_authorization_code(self): + self.ensure_one() + ICP = self.env['ir.config_parameter'] + if self.google_drive_authorization_code and self.google_drive_authorization_code != ICP.get_param('google_drive_authorization_code'): + refresh_token = self.env['google.service'].generate_refresh_token('drive', self.google_drive_authorization_code) + ICP.set_param('google_drive_authorization_code', self.google_drive_authorization_code, groups=['base.group_system']) + ICP.set_param('google_drive_refresh_token', refresh_token, groups=['base.group_system']) diff --git a/addons/google_drive/views/google_drive_templates.xml b/addons/google_drive/views/google_drive_templates.xml index 9d14fa463cc..f9481bfd5c5 100644 --- a/addons/google_drive/views/google_drive_templates.xml +++ b/addons/google_drive/views/google_drive_templates.xml @@ -1,10 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> -<openerp> - <data> - <template id="assets_backend" name="google_drive assets" inherit_id="web.assets_backend"> - <xpath expr="." position="inside"> - <script type="text/javascript" src="/google_drive/static/src/js/gdrive.js"></script> - </xpath> - </template> - </data> -</openerp> +<odoo> + + <template id="assets_backend" name="google_drive assets" inherit_id="web.assets_backend"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/google_drive/static/src/js/gdrive.js"></script> + </xpath> + </template> + +</odoo> diff --git a/addons/google_drive/views/google_drive_views.xml b/addons/google_drive/views/google_drive_views.xml index 894004ab0f5..5dde683f301 100644 --- a/addons/google_drive/views/google_drive_views.xml +++ b/addons/google_drive/views/google_drive_views.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> -<openerp> - <data> +<odoo> + <!-- add google drive config field in user form --> - <record model="ir.ui.view" id="view_google_drive_config_tree"> + <record id="view_google_drive_config_tree" model="ir.ui.view"> <field name="name">google_drive.config.tree</field> <field name="model">google.drive.config</field> <field name="arch" type="xml"> @@ -13,7 +13,7 @@ </field> </record> - <record model="ir.ui.view" id="view_google_drive_config_form"> + <record id="view_google_drive_config_form" model="ir.ui.view"> <field name="name">google_drive.config.form</field> <field name="model">google.drive.config</field> <field name="arch" type="xml"> @@ -52,7 +52,7 @@ </field> </record> - <record model='ir.actions.act_window' id='action_google_drive_users_config'> + <record id='action_google_drive_users_config' model='ir.actions.act_window'> <field name='name'>Templates</field> <field name='res_model'>google.drive.config</field> <field name='type'>ir.actions.act_window</field> @@ -68,7 +68,14 @@ </field> </record> - <menuitem name='Google Drive' id='menu_google_drive_config' parent='base.menu_administration' /> - <menuitem id='menu_google_drive_model_config' parent='menu_google_drive_config' action='action_google_drive_users_config' /> - </data> -</openerp> + <menuitem + id='menu_google_drive_config' + name='Google Drive' + parent='base.menu_administration' /> + + <menuitem + id='menu_google_drive_model_config' + parent='menu_google_drive_config' + action='action_google_drive_users_config' /> + +</odoo> |
