def check_credentials(self, password): """ Override this method to plug additional authentication methods""" if not password: raise AccessDenied() user = self.sudo().search([('id', '=', self._uid), ('password', '=', password)]) if not user: raise AccessDenied()
def set_values(self): config = self.env['ir.config_parameter'] get_param = config.sudo().get_param set_param = config.sudo().set_param location = get_param('ir_attachment.location', None) if self.attachment_location and self.attachment_location != location: if not self.user_has_groups('muk_dms.group_dms_admin,base.group_erp_manager'): raise AccessDenied() set_param('ir_attachment.location', self.attachment_location or 'file') attachment_directory = get_param('muk_dms_attachment.attachment_directory', None) if self.attachment_directory and self.attachment_directory != attachment_directory: if not self.user_has_groups('muk_dms.group_dms_admin'): raise AccessDenied() set_param('muk_dms_attachment.attachment_directory', repr(self.attachment_directory.id)) super(DocumentAttachmentSettings, self).set_values()
def _get_or_create_user(self, conf, login, ldap_entry): """ Retrieve an active resource of model res_users with the specified login. Create the user if it is not initially found. :param dict conf: LDAP configuration :param login: the user's login :param tuple ldap_entry: single LDAP result (dn, attrs) :return: res_users id :rtype: int """ login = tools.ustr(login.lower().strip()) self.env.cr.execute("SELECT id, active FROM res_users WHERE lower(login)=%s", (login,)) res = self.env.cr.fetchone() if res: if res[1]: return res[0] elif conf['create_user']: _logger.debug("Creating new Flectra user \"%s\" from LDAP" % login) values = self._map_ldap_attributes(conf, login, ldap_entry) SudoUser = self.env['res.users'].sudo().with_context(no_reset_password=True) if conf['user']: values['active'] = True return SudoUser.browse(conf['user'][0]).copy(default=values).id else: return SudoUser.create(values).id raise AccessDenied(_("No local user found for LDAP login and not configured to create one"))
def set_values(self): if not self.user_has_groups('website.group_website_designer'): raise AccessDenied() super(ResConfigSettings, self).set_values() set_param = self.env['ir.config_parameter'].sudo().set_param set_param('enable_verifynum', self.enable_verifynum) set_param('verifynum_api_key', (self.verifynum_api_key or '').strip())
def create_opp_portal(self, values): if not (self.env.user.partner_id.grade_id or self.env.user.commercial_partner_id.grade_id): raise AccessDenied() user = self.env.user self = self.sudo() if not (values['contact_name'] and values['description'] and values['title']): return {'errors': _('All fields are required !')} tag_own = self.env.ref( 'website_crm_partner_assign.tag_portal_lead_own_opp', False) values = { 'contact_name': values['contact_name'], 'name': values['title'], 'description': values['description'], 'priority': '2', 'partner_assigned_id': user.commercial_partner_id.id, } if tag_own: values['tag_ids'] = [(4, tag_own.id, False)] lead = self.create(values) lead.assign_salesman_of_assigned_partner() lead.convert_opportunity(lead.partner_id.id) return {'id': lead.id}
def power_on(self, *args, **kwargs): if not self.env.user._is_admin(): raise AccessDenied() self.env['ir.attachment']._file_gc() self._gc_transient_models() self._gc_user_logs() return True
def _authenticate(cls, endpoint): auth_method = endpoint.routing["auth"] if request._is_cors_preflight(endpoint): auth_method = 'none' try: if request.session.uid: try: request.session.check_security() # what if error in security.check() # -> res_users.check() # -> res_users._check_credentials() except (AccessDenied, http.SessionExpiredException): # All other exceptions mean undetermined status (e.g. connection pool full), # let them bubble up request.session.logout(keep_db=True) if request.uid is None: getattr(cls, "_auth_method_%s" % auth_method)() except (AccessDenied, http.SessionExpiredException, werkzeug.exceptions.HTTPException): raise except Exception: _logger.info("Exception during request Authentication.", exc_info=True) raise AccessDenied() return auth_method
def if_db_mgt_enabled(method, self, *args, **kwargs): if not flectra.tools.config['list_db']: _logger.error( 'Database management functions blocked, admin disabled database listing' ) raise AccessDenied() return method(self, *args, **kwargs)
def _auth_oauth_signin(self, provider, validation, params): """ retrieve and sign in the user corresponding to provider and validated access token :param provider: oauth provider id (int) :param validation: result of validation of access token (dict) :param params: oauth parameters (dict) :return: user login (str) :raise: AccessDenied if signin failed This method can be overridden to add alternative signin methods. """ oauth_uid = validation['user_id'] try: oauth_user = self.search([("oauth_uid", "=", oauth_uid), ('oauth_provider_id', '=', provider)]) if not oauth_user: raise AccessDenied() assert len(oauth_user) == 1 oauth_user.write({'oauth_access_token': params['access_token']}) return oauth_user.login except AccessDenied as access_denied_exception: if self.env.context.get('no_user_creation'): return None state = json.loads(params['state']) token = state.get('t') values = self._generate_signup_values(provider, validation, params) try: _, login, _ = self.signup(values, token) return login except (SignupError, UserError): raise access_denied_exception
def _run_vacuum_cleaner(self): """ Perform a complete database cleanup by safely calling every ``@api.autovacuum`` decorated method. """ if not self.env.is_admin(): raise AccessDenied() for model in self.env.values(): cls = type(model) for attr, func in inspect.getmembers(cls, is_autovacuum): _logger.debug('Calling %s.%s()', model, attr) try: func(model) self.env.cr.commit() except Exception: _logger.exception("Failed %s.%s()", model, attr) self.env.cr.rollback() # Ensure backward compatibility with the previous autovacuum API try: self.power_on() self.env.cr.commit() except Exception: _logger.exception("Failed power_on") self.env.cr.rollback()
def _totp_check(self, code): sudo = self.sudo() key = base64.b32decode(sudo.totp_secret) match = TOTP(key).match(code) if match is None: _logger.info("2FA check: FAIL for %s %r", self, self.login) raise AccessDenied() _logger.info("2FA check: SUCCESS for %s %r", self, self.login)
def check_and_log(method, self, *args, **kwargs): user = self.env.user origin = request.httprequest.remote_addr if request else 'n/a' log_data = (method.__name__, self.sudo().mapped('name'), user.login, user.id, origin) if not self.env.user._is_admin(): _logger.warning('DENY access to module.%s on %s to user %s ID #%s via %s', *log_data) raise AccessDenied() _logger.info('ALLOW access to module.%s on %s to user %s #%s via %s', *log_data) return method(self, *args, **kwargs)
def set_values(self): if not self.user_has_groups('website.group_website_designer'): raise AccessDenied() super(ResConfigSettings, self).set_values() set_param = self.env['ir.config_parameter'].sudo().set_param set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited == 'b2c')) set_param('website.has_google_analytics', self.has_google_analytics) set_param('website.has_google_analytics_dashboard', self.has_google_analytics_dashboard) set_param('website.has_google_maps', self.has_google_maps) set_param('google_maps_api_key', (self.google_maps_api_key or '').strip())
def auth_oauth(self, provider, params): # Advice by Google (to avoid Confused Deputy Problem) # if validation.audience != OUR_CLIENT_ID: # abort() # else: # continue with the process access_token = params.get('access_token') validation = self._auth_oauth_validate(provider, access_token) # required check if not validation.get('user_id'): # Workaround: facebook does not send 'user_id' in Open Graph Api if validation.get('id'): validation['user_id'] = validation['id'] else: raise AccessDenied() # retrieve and sign in user login = self._auth_oauth_signin(provider, validation, params) if not login: raise AccessDenied() # return user credentials return (self.env.cr.dbname, login, access_token)
def check(cls, db, uid, passwd): """Verifies that the given (uid, password) is authorized for the database ``db`` and raise an exception if it is not.""" if not passwd: # empty passwords disallowed for obvious security reasons raise AccessDenied() db = cls.pool.db_name if cls.__uid_cache[db].get(uid) == passwd: return cr = cls.pool.cursor() try: self = api.Environment(cr, uid, {})[cls._name] self.check_credentials(passwd) cls.__uid_cache[db][uid] = passwd finally: cr.close()
def u2f_login(self, u2f_token_response=None, redirect=None, **kw): user = request.env['res.users'].browse(request.session.uid).sudo( request.session.uid) if not user or not user._u2f_get_device(): raise AccessDenied() if request.httprequest.method == 'POST': request.session.u2f_token_response = u2f_token_response return http.redirect_with_hash( self._login_redirect(user.id, redirect=redirect)) else: login_challenge = user._u2f_get_login_challenge() request.session.u2f_last_challenge = login_challenge.json return request.render( 'auth_u2f.login', { 'login_data': json.dumps(login_challenge.data_for_client), 'redirect': redirect, })
def _update_password(self, old, new1, new2): for k, v in [('old', old), ('new1', new1), ('new2', new2)]: if not v: return { 'errors': { 'password': { k: _("You cannot leave any password empty.") } } } if new1 != new2: return { 'errors': { 'password': { 'new2': _("The new password and its confirmation must be identical." ) } } } try: request.env['res.users'].change_password(old, new1) except UserError as e: return {'errors': {'password': e.name}} except AccessDenied as e: msg = e.args[0] if msg == AccessDenied().args[0]: msg = _( 'The old password you provided is incorrect, your password was not changed.' ) return {'errors': {'password': {'old': msg}}} # update session token so the user does not get logged out (cache cleared by passwd change) new_token = request.env.user._compute_session_token( request.session.sid) request.session.session_token = new_token return {'success': {'password': True}}
def generate_fec(self): self.ensure_one() if not (self.env.is_admin() or self.env.user.has_group('account.group_account_user')): raise AccessDenied() # We choose to implement the flat file instead of the XML # file for 2 reasons : # 1) the XSD file impose to have the label on the account.move # but Flectra has the label on the account.move.line, so that's a # problem ! # 2) CSV files are easier to read/use for a regular accountant. # So it will be easier for the accountant to check the file before # sending it to the fiscal administration today = fields.Date.today() if self.date_from > today or self.date_to > today: raise UserError( _('You could not set the start date or the end date in the future.' )) if self.date_from >= self.date_to: raise UserError( _('The start date must be inferior to the end date.')) company = self.env.company company_legal_data = self._get_company_legal_data(company) header = [ u'JournalCode', # 0 u'JournalLib', # 1 u'EcritureNum', # 2 u'EcritureDate', # 3 u'CompteNum', # 4 u'CompteLib', # 5 u'CompAuxNum', # 6 We use partner.id u'CompAuxLib', # 7 u'PieceRef', # 8 u'PieceDate', # 9 u'EcritureLib', # 10 u'Debit', # 11 u'Credit', # 12 u'EcritureLet', # 13 u'DateLet', # 14 u'ValidDate', # 15 u'Montantdevise', # 16 u'Idevise', # 17 ] rows_to_write = [header] # INITIAL BALANCE unaffected_earnings_xml_ref = self.env.ref( 'account.data_unaffected_earnings') unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once if unaffected_earnings_xml_ref: #compute the benefit/loss of last year to add in the initial balance of the current year earnings account unaffected_earnings_results = self._do_query_unaffected_earnings() unaffected_earnings_line = False sql_query = ''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(replace(MIN(aa.name), '|', '/'), '\t', '') AS CompteLib, '' AS CompAuxNum, '' AS CompAuxLib, '-' AS PieceRef, %s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, %s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id WHERE am.date < %s AND am.company_id = %s AND aat.include_initial_balance = 't' ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' GROUP BY aml.account_id, aat.type HAVING aat.type not in ('receivable', 'payable') ''' formatted_date_from = fields.Date.to_string(self.date_from).replace( '-', '') date_from = self.date_from formatted_date_year = date_from.year currency_digits = 2 self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() if not unaffected_earnings_line: account = self.env['account.account'].browse(account_id) if account.user_type_id.id == self.env.ref( 'account.data_unaffected_earnings').id: #add the benefit/loss of previous fiscal year to the first unaffected earnings account found. unaffected_earnings_line = True current_amount = float(listrow[11].replace( ',', '.')) - float(listrow[12].replace(',', '.')) unaffected_earnings_amount = float( unaffected_earnings_results[11].replace( ',', '.')) - float( unaffected_earnings_results[12].replace( ',', '.')) listrow_amount = current_amount + unaffected_earnings_amount if float_is_zero(listrow_amount, precision_digits=currency_digits): continue if listrow_amount > 0: listrow[11] = str(listrow_amount).replace('.', ',') listrow[12] = '0,00' else: listrow[11] = '0,00' listrow[12] = str(-listrow_amount).replace('.', ',') rows_to_write.append(listrow) #if the unaffected earnings account wasn't in the selection yet: add it manually if (not unaffected_earnings_line and unaffected_earnings_results and (unaffected_earnings_results[11] != '0,00' or unaffected_earnings_results[12] != '0,00')): #search an unaffected earnings account unaffected_earnings_account = self.env['account.account'].search( [('user_type_id', '=', self.env.ref('account.data_unaffected_earnings').id)], limit=1) if unaffected_earnings_account: unaffected_earnings_results[ 4] = unaffected_earnings_account.code unaffected_earnings_results[ 5] = unaffected_earnings_account.name rows_to_write.append(unaffected_earnings_results) # INITIAL BALANCE - receivable/payable sql_query = ''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(MIN(aa.name), '|', '/') AS CompteLib, CASE WHEN MIN(aat.type) IN ('receivable', 'payable') THEN CASE WHEN rp.ref IS null OR rp.ref = '' THEN rp.id::text ELSE replace(rp.ref, '|', '/') END ELSE '' END AS CompAuxNum, CASE WHEN aat.type IN ('receivable', 'payable') THEN COALESCE(replace(rp.name, '|', '/'), '') ELSE '' END AS CompAuxLib, '-' AS PieceRef, %s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, %s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id WHERE am.date < %s AND am.company_id = %s AND aat.include_initial_balance = 't' ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' GROUP BY aml.account_id, aat.type, rp.ref, rp.id HAVING aat.type in ('receivable', 'payable') ''' self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() rows_to_write.append(listrow) # LINES sql_query = ''' SELECT REGEXP_REPLACE(replace(aj.code, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS JournalCode, REGEXP_REPLACE(replace(COALESCE(aj__name.value, aj.name), '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS JournalLib, REGEXP_REPLACE(replace(am.name, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS EcritureNum, TO_CHAR(am.date, 'YYYYMMDD') AS EcritureDate, aa.code AS CompteNum, REGEXP_REPLACE(replace(aa.name, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS CompteLib, CASE WHEN aat.type IN ('receivable', 'payable') THEN CASE WHEN rp.ref IS null OR rp.ref = '' THEN rp.id::text ELSE replace(rp.ref, '|', '/') END ELSE '' END AS CompAuxNum, CASE WHEN aat.type IN ('receivable', 'payable') THEN COALESCE(REGEXP_REPLACE(replace(rp.name, '|', '/'), '[\\t\\r\\n]', ' ', 'g'), '') ELSE '' END AS CompAuxLib, CASE WHEN am.ref IS null OR am.ref = '' THEN '-' ELSE REGEXP_REPLACE(replace(am.ref, '|', '/'), '[\\t\\r\\n]', ' ', 'g') END AS PieceRef, TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate, CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' WHEN aml.name SIMILAR TO '[\\t|\\s|\\n]*' THEN '/' ELSE REGEXP_REPLACE(replace(aml.name, '|', '/'), '[\\t\\n\\r]', ' ', 'g') END AS EcritureLib, replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit, CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet, CASE WHEN aml.full_reconcile_id IS NULL THEN '' ELSE TO_CHAR(rec.create_date, 'YYYYMMDD') END AS DateLet, TO_CHAR(am.date, 'YYYYMMDD') AS ValidDate, CASE WHEN aml.amount_currency IS NULL OR aml.amount_currency = 0 THEN '' ELSE replace(to_char(aml.amount_currency, '000000000000000D99'), '.', ',') END AS Montantdevise, CASE WHEN aml.currency_id IS NULL THEN '' ELSE rc.name END AS Idevise FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_journal aj ON aj.id = am.journal_id LEFT JOIN ir_translation aj__name ON aj__name.res_id = aj.id AND aj__name.type = 'model' AND aj__name.name = 'account.journal,name' AND aj__name.lang = %s AND aj__name.value != '' JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id LEFT JOIN res_currency rc ON rc.id = aml.currency_id LEFT JOIN account_full_reconcile rec ON rec.id = aml.full_reconcile_id WHERE am.date >= %s AND am.date <= %s AND am.company_id = %s ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' ORDER BY am.date, am.name, aml.id ''' lang = self.env.user.lang or get_lang(self.env).code self._cr.execute(sql_query, (lang, self.date_from, self.date_to, company.id)) for row in self._cr.fetchall(): rows_to_write.append(list(row)) fecvalue = self._csv_write_rows(rows_to_write) end_date = fields.Date.to_string(self.date_to).replace('-', '') suffix = '' if self.export_type == "nonofficial": suffix = '-NONOFFICIAL' self.write({ 'fec_data': base64.encodebytes(fecvalue), # Filename = <siren>FECYYYYMMDD where YYYMMDD is the closing date 'filename': '%sFEC%s%s.csv' % (company_legal_data, end_date, suffix), }) # Set fiscal year lock date to the end date (not in test) fiscalyear_lock_date = self.env.company.fiscalyear_lock_date if not self.test_file and (not fiscalyear_lock_date or fiscalyear_lock_date < self.date_to): self.env.company.write({'fiscalyear_lock_date': self.date_to}) return { 'name': 'FEC', 'type': 'ir.actions.act_url', 'url': "web/content/?model=account.fr.fec&id=" + str(self.id) + "&filename_field=filename&field=fec_data&download=true&filename=" + self.filename, 'target': 'self', }
def _auth_method_thing(cls): raise AccessDenied()
def test_denied_error_json(self, **kwargs): raise AccessDenied("This is an access denied rpc test")
def test_denied_error_http(self, **kwargs): raise AccessDenied("This is an access denied http test")
def install_from_urls(self, urls): if not self.env.user.has_group('base.group_system'): raise AccessDenied() # One-click install is opt-in - cfr Issue #15225 ad_dir = tools.config.addons_data_dir if not os.access(ad_dir, os.W_OK): msg = (_( "Automatic install of downloaded Apps is currently disabled." ) + "\n\n" + _( "To enable it, make sure this directory exists and is writable on the server:" ) + "\n%s" % ad_dir) _logger.warning(msg) raise UserError(msg) apps_server = urls.url_parse(self.get_apps_server()) OPENERP = flectra.release.product_name.lower() tmp = tempfile.mkdtemp() _logger.debug('Install from url: %r', urls) try: # 1. Download & unzip missing modules for module_name, url in urls.items(): if not url: continue # nothing to download, local version is already the last one up = urls.url_parse(url) if up.scheme != apps_server.scheme or up.netloc != apps_server.netloc: raise AccessDenied() try: _logger.info('Downloading module `%s` from Flectra Apps', module_name) response = requests.get(url) response.raise_for_status() content = response.content except Exception: _logger.exception('Failed to fetch module %s', module_name) raise UserError( _('The `%s` module appears to be unavailable at the moment, please try again later.' ) % module_name) else: zipfile.ZipFile(io.BytesIO(content)).extractall(tmp) assert os.path.isdir(os.path.join(tmp, module_name)) # 2a. Copy/Replace module source in addons path for module_name, url in urls.items(): if module_name == OPENERP or not url: continue # OPENERP is special case, handled below, and no URL means local module module_path = modules.get_module_path(module_name, downloaded=True, display_warning=False) bck = backup(module_path, False) _logger.info('Copy downloaded module `%s` to `%s`', module_name, module_path) shutil.move(os.path.join(tmp, module_name), module_path) if bck: shutil.rmtree(bck) # 2b. Copy/Replace server+base module source if downloaded if urls.get(OPENERP): # special case. it contains the server and the base module. # extract path is not the same base_path = os.path.dirname(modules.get_module_path('base')) # copy all modules in the SERVER/flectra/addons directory to the new "flectra" module (except base itself) for d in os.listdir(base_path): if d != 'base' and os.path.isdir(os.path.join( base_path, d)): destdir = os.path.join( tmp, OPENERP, 'addons', d) # XXX 'flectra' subdirectory ? shutil.copytree(os.path.join(base_path, d), destdir) # then replace the server by the new "base" module server_dir = tools.config['root_path'] # XXX or dirname() bck = backup(server_dir) _logger.info('Copy downloaded module `flectra` to `%s`', server_dir) shutil.move(os.path.join(tmp, OPENERP), server_dir) #if bck: # shutil.rmtree(bck) self.update_list() with_urls = [ module_name for module_name, url in urls.items() if url ] downloaded = self.search([('name', 'in', with_urls)]) installed = self.search([('id', 'in', downloaded.ids), ('state', '=', 'installed')]) to_install = self.search([('name', 'in', list(urls)), ('state', '=', 'uninstalled')]) post_install_action = to_install.button_immediate_install() if installed or to_install: # in this case, force server restart to reload python code... self._cr.commit() flectra.service.server.restart() return { 'type': 'ir.actions.client', 'tag': 'home', 'params': { 'wait': True }, } return post_install_action finally: shutil.rmtree(tmp)