537 lines
24 KiB
Python
537 lines
24 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from odoo import SUPERUSER_ID
|
|
from odoo.addons.base.models.res_users import is_selection_groups, get_selection_groups, name_selection_groups
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests.common import Form, TransactionCase, new_test_user, tagged
|
|
from odoo.tools import mute_logger
|
|
|
|
|
|
class TestUsers(TransactionCase):
|
|
|
|
def test_name_search(self):
|
|
""" Check name_search on user. """
|
|
User = self.env['res.users']
|
|
|
|
test_user = User.create({'name': 'Flad the Impaler', 'login': 'vlad'})
|
|
like_user = User.create({'name': 'Wlad the Impaler', 'login': 'vladi'})
|
|
other_user = User.create({'name': 'Nothing similar', 'login': 'nothing similar'})
|
|
all_users = test_user | like_user | other_user
|
|
|
|
res = User.name_search('vlad', operator='ilike')
|
|
self.assertEqual(User.browse(i[0] for i in res) & all_users, test_user)
|
|
|
|
res = User.name_search('vlad', operator='not ilike')
|
|
self.assertEqual(User.browse(i[0] for i in res) & all_users, all_users)
|
|
|
|
res = User.name_search('', operator='ilike')
|
|
self.assertEqual(User.browse(i[0] for i in res) & all_users, all_users)
|
|
|
|
res = User.name_search('', operator='not ilike')
|
|
self.assertEqual(User.browse(i[0] for i in res) & all_users, User)
|
|
|
|
res = User.name_search('lad', operator='ilike')
|
|
self.assertEqual(User.browse(i[0] for i in res) & all_users, test_user | like_user)
|
|
|
|
res = User.name_search('lad', operator='not ilike')
|
|
self.assertEqual(User.browse(i[0] for i in res) & all_users, other_user)
|
|
|
|
def test_user_partner(self):
|
|
""" Check that the user partner is well created """
|
|
|
|
User = self.env['res.users']
|
|
Partner = self.env['res.partner']
|
|
Company = self.env['res.company']
|
|
|
|
company_1 = Company.create({'name': 'company_1'})
|
|
company_2 = Company.create({'name': 'company_2'})
|
|
|
|
partner = Partner.create({
|
|
'name': 'Bob Partner',
|
|
'company_id': company_2.id
|
|
})
|
|
|
|
# case 1 : the user has no partner
|
|
test_user = User.create({
|
|
'name': 'John Smith',
|
|
'login': 'jsmith',
|
|
'company_ids': [company_1.id],
|
|
'company_id': company_1.id
|
|
})
|
|
|
|
self.assertFalse(
|
|
test_user.partner_id.company_id,
|
|
"The partner_id linked to a user should be created without any company_id")
|
|
|
|
# case 2 : the user has a partner
|
|
test_user = User.create({
|
|
'name': 'Bob Smith',
|
|
'login': 'bsmith',
|
|
'company_ids': [company_1.id],
|
|
'company_id': company_1.id,
|
|
'partner_id': partner.id
|
|
})
|
|
|
|
self.assertEqual(
|
|
test_user.partner_id.company_id,
|
|
company_1,
|
|
"If the partner_id of a user has already a company, it is replaced by the user company"
|
|
)
|
|
|
|
|
|
def test_change_user_company(self):
|
|
""" Check the partner company update when the user company is changed """
|
|
|
|
User = self.env['res.users']
|
|
Company = self.env['res.company']
|
|
|
|
test_user = User.create({'name': 'John Smith', 'login': 'jsmith'})
|
|
company_1 = Company.create({'name': 'company_1'})
|
|
company_2 = Company.create({'name': 'company_2'})
|
|
|
|
test_user.company_ids += company_1
|
|
test_user.company_ids += company_2
|
|
|
|
# 1: the partner has no company_id, no modification
|
|
test_user.write({
|
|
'company_id': company_1.id
|
|
})
|
|
|
|
self.assertFalse(
|
|
test_user.partner_id.company_id,
|
|
"On user company change, if its partner_id has no company_id,"
|
|
"the company_id of the partner_id shall NOT be updated")
|
|
|
|
# 2: the partner has a company_id different from the new one, update it
|
|
test_user.partner_id.write({
|
|
'company_id': company_1.id
|
|
})
|
|
|
|
test_user.write({
|
|
'company_id': company_2.id
|
|
})
|
|
|
|
self.assertEqual(
|
|
test_user.partner_id.company_id,
|
|
company_2,
|
|
"On user company change, if its partner_id has already a company_id,"
|
|
"the company_id of the partner_id shall be updated"
|
|
)
|
|
|
|
@mute_logger('odoo.sql_db')
|
|
def test_deactivate_portal_users_access(self):
|
|
"""Test that only a portal users can deactivate his account."""
|
|
user_internal = self.env['res.users'].create({
|
|
'name': 'Internal',
|
|
'login': 'user_internal',
|
|
'password': 'password',
|
|
'groups_id': [self.env.ref('base.group_user').id],
|
|
})
|
|
|
|
with self.assertRaises(UserError, msg='Internal users should not be able to deactivate their account'):
|
|
user_internal._deactivate_portal_user()
|
|
|
|
@mute_logger('odoo.sql_db', 'odoo.addons.base.models.res_users_deletion')
|
|
def test_deactivate_portal_users_archive_and_remove(self):
|
|
"""Test that if the account can not be removed, it's archived instead
|
|
and sensitive information are removed.
|
|
|
|
In this test, the deletion of "portal_user" will succeed,
|
|
but the deletion of "portal_user_2" will fail.
|
|
"""
|
|
User = self.env['res.users']
|
|
portal_user = User.create({
|
|
'name': 'Portal',
|
|
'login': 'portal_user',
|
|
'password': 'password',
|
|
'groups_id': [self.env.ref('base.group_portal').id],
|
|
})
|
|
portal_partner = portal_user.partner_id
|
|
|
|
portal_user_2 = User.create({
|
|
'name': 'Portal',
|
|
'login': 'portal_user_2',
|
|
'password': 'password',
|
|
'groups_id': [self.env.ref('base.group_portal').id],
|
|
})
|
|
portal_partner_2 = portal_user_2.partner_id
|
|
|
|
(portal_user | portal_user_2)._deactivate_portal_user()
|
|
|
|
self.assertTrue(portal_user.exists() and not portal_user.active, 'Should have archived the user 1')
|
|
|
|
self.assertEqual(portal_user.name, 'Portal', 'Should have kept the user name')
|
|
self.assertEqual(portal_user.partner_id.name, 'Portal', 'Should have kept the partner name')
|
|
self.assertNotEqual(portal_user.login, 'portal_user', 'Should have removed the user login')
|
|
|
|
asked_deletion_1 = self.env['res.users.deletion'].search([('user_id', '=', portal_user.id)])
|
|
asked_deletion_2 = self.env['res.users.deletion'].search([('user_id', '=', portal_user_2.id)])
|
|
|
|
self.assertTrue(asked_deletion_1, 'Should have added the user 1 in the deletion queue')
|
|
self.assertTrue(asked_deletion_2, 'Should have added the user 2 in the deletion queue')
|
|
|
|
# The deletion will fail for "portal_user_2",
|
|
# because of the absence of "ondelete=cascade"
|
|
self.cron = self.env['ir.cron'].create({
|
|
'name': 'Test Cron',
|
|
'user_id': portal_user_2.id,
|
|
'model_id': self.env.ref('base.model_res_partner').id,
|
|
})
|
|
|
|
self.env['res.users.deletion']._gc_portal_users()
|
|
|
|
self.assertFalse(portal_user.exists(), 'Should have removed the user')
|
|
self.assertFalse(portal_partner.exists(), 'Should have removed the partner')
|
|
self.assertEqual(asked_deletion_1.state, 'done', 'Should have marked the deletion as done')
|
|
|
|
self.assertTrue(portal_user_2.exists(), 'Should have kept the user')
|
|
self.assertTrue(portal_partner_2.exists(), 'Should have kept the partner')
|
|
self.assertEqual(asked_deletion_2.state, 'fail', 'Should have marked the deletion as failed')
|
|
|
|
def test_context_get_lang(self):
|
|
self.env['res.lang'].with_context(active_test=False).search([
|
|
('code', 'in', ['fr_FR', 'es_ES', 'de_DE', 'en_US'])
|
|
]).write({'active': True})
|
|
|
|
user = new_test_user(self.env, 'jackoneill')
|
|
user = user.with_user(user)
|
|
user.lang = 'fr_FR'
|
|
|
|
company = user.company_id.partner_id.sudo()
|
|
company.lang = 'de_DE'
|
|
|
|
request = SimpleNamespace()
|
|
request.best_lang = 'es_ES'
|
|
request_patch = patch('odoo.addons.base.models.res_users.request', request)
|
|
self.addCleanup(request_patch.stop)
|
|
request_patch.start()
|
|
|
|
self.assertEqual(user.context_get()['lang'], 'fr_FR')
|
|
self.env.registry.clear_cache()
|
|
user.lang = False
|
|
|
|
self.assertEqual(user.context_get()['lang'], 'es_ES')
|
|
self.env.registry.clear_cache()
|
|
request_patch.stop()
|
|
|
|
self.assertEqual(user.context_get()['lang'], 'de_DE')
|
|
self.env.registry.clear_cache()
|
|
company.lang = False
|
|
|
|
self.assertEqual(user.context_get()['lang'], 'en_US')
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestUsers2(TransactionCase):
|
|
|
|
def test_reified_groups(self):
|
|
""" The groups handler doesn't use the "real" view with pseudo-fields
|
|
during installation, so it always works (because it uses the normal
|
|
groups_id field).
|
|
"""
|
|
# use the specific views which has the pseudo-fields
|
|
f = Form(self.env['res.users'], view='base.view_users_form')
|
|
f.name = "bob"
|
|
f.login = "bob"
|
|
user = f.save()
|
|
|
|
self.assertIn(self.env.ref('base.group_user'), user.groups_id)
|
|
|
|
# all template user groups are copied
|
|
default_user = self.env.ref('base.default_user')
|
|
self.assertEqual(default_user.groups_id, user.groups_id)
|
|
|
|
def test_selection_groups(self):
|
|
# create 3 groups that should be in a selection
|
|
app = self.env['ir.module.category'].create({'name': 'Foo'})
|
|
group1, group2, group0 = self.env['res.groups'].create([
|
|
{'name': name, 'category_id': app.id}
|
|
for name in ('User', 'Manager', 'Visitor')
|
|
])
|
|
# THIS PART IS NECESSARY TO REPRODUCE AN ISSUE: group1.id < group2.id < group0.id
|
|
self.assertLess(group1.id, group2.id)
|
|
self.assertLess(group2.id, group0.id)
|
|
# implication order is group0 < group1 < group2
|
|
group2.implied_ids = group1
|
|
group1.implied_ids = group0
|
|
groups = group0 + group1 + group2
|
|
|
|
# determine the name of the field corresponding to groups
|
|
fname = next(
|
|
name
|
|
for name in self.env['res.users'].fields_get()
|
|
if is_selection_groups(name) and group0.id in get_selection_groups(name)
|
|
)
|
|
self.assertCountEqual(get_selection_groups(fname), groups.ids)
|
|
|
|
# create a user
|
|
user = self.env['res.users'].create({'name': 'foo', 'login': 'foo'})
|
|
|
|
# put user in group0, and check field value
|
|
user.write({fname: group0.id})
|
|
self.assertEqual(user.groups_id & groups, group0)
|
|
self.assertEqual(user.read([fname])[0][fname], group0.id)
|
|
|
|
# put user in group1, and check field value
|
|
user.write({fname: group1.id})
|
|
self.assertEqual(user.groups_id & groups, group0 + group1)
|
|
self.assertEqual(user.read([fname])[0][fname], group1.id)
|
|
|
|
# put user in group2, and check field value
|
|
user.write({fname: group2.id})
|
|
self.assertEqual(user.groups_id & groups, groups)
|
|
self.assertEqual(user.read([fname])[0][fname], group2.id)
|
|
|
|
normalized_values = user._remove_reified_groups({fname: group0.id})
|
|
self.assertEqual(sorted(normalized_values['groups_id']), [(3, group1.id), (3, group2.id), (4, group0.id)])
|
|
|
|
normalized_values = user._remove_reified_groups({fname: group1.id})
|
|
self.assertEqual(sorted(normalized_values['groups_id']), [(3, group2.id), (4, group1.id)])
|
|
|
|
normalized_values = user._remove_reified_groups({fname: group2.id})
|
|
self.assertEqual(normalized_values['groups_id'], [(4, group2.id)])
|
|
|
|
def test_read_list_with_reified_field(self):
|
|
""" Check that read_group and search_read get rid of reified fields"""
|
|
User = self.env['res.users']
|
|
fnames = ['name', 'email', 'login']
|
|
|
|
# find some reified field name
|
|
reified_fname = next(
|
|
fname
|
|
for fname in User.fields_get()
|
|
if fname.startswith(('in_group_', 'sel_groups_'))
|
|
)
|
|
|
|
# check that the reified field name has no effect in fields
|
|
res_with_reified = User.read_group([], fnames + [reified_fname], ['company_id'])
|
|
res_without_reified = User.read_group([], fnames, ['company_id'])
|
|
self.assertEqual(res_with_reified, res_without_reified, "Reified fields should be ignored in read_group")
|
|
|
|
# check that the reified fields are not considered invalid in search_read
|
|
# and are ignored
|
|
res_with_reified = User.search_read([], fnames + [reified_fname])
|
|
res_without_reified = User.search_read([], fnames)
|
|
self.assertEqual(res_with_reified, res_without_reified, "Reified fields should be ignored in search_read")
|
|
|
|
# Verify that the read_group is raising an error if reified field is used as groupby
|
|
with self.assertRaises(ValueError):
|
|
User.read_group([], fnames + [reified_fname], [reified_fname])
|
|
|
|
def test_reified_groups_on_change(self):
|
|
"""Test that a change on a reified fields trigger the onchange of groups_id."""
|
|
group_public = self.env.ref('base.group_public')
|
|
group_portal = self.env.ref('base.group_portal')
|
|
group_user = self.env.ref('base.group_user')
|
|
|
|
# Build the reified group field name
|
|
user_groups = group_public | group_portal | group_user
|
|
user_groups_ids = [str(group_id) for group_id in sorted(user_groups.ids)]
|
|
group_field_name = f"sel_groups_{'_'.join(user_groups_ids)}"
|
|
|
|
# <group col="4" invisible="sel_groups_1_9_10 != 1" groups="base.group_no_one" class="o_label_nowrap">
|
|
with self.debug_mode():
|
|
user_form = Form(self.env['res.users'], view='base.view_users_form')
|
|
user_form.name = "Test"
|
|
user_form.login = "Test"
|
|
self.assertFalse(user_form.share)
|
|
|
|
user_form[group_field_name] = group_portal.id
|
|
self.assertTrue(user_form.share, 'The groups_id onchange should have been triggered')
|
|
|
|
user_form[group_field_name] = group_user.id
|
|
self.assertFalse(user_form.share, 'The groups_id onchange should have been triggered')
|
|
|
|
user_form[group_field_name] = group_public.id
|
|
self.assertTrue(user_form.share, 'The groups_id onchange should have been triggered')
|
|
|
|
|
|
@tagged('post_install', '-at_install', 'res_groups')
|
|
class TestUsersGroupWarning(TransactionCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
"""
|
|
These are the Groups and their Hierarchy we have Used to test Group warnings.
|
|
|
|
Category groups hierarchy:
|
|
Sales
|
|
├── User: All Documents
|
|
└── Administrator
|
|
Timesheets
|
|
├── User: own timesheets only
|
|
├── User: all timesheets
|
|
└── Administrator
|
|
Project
|
|
├── User
|
|
└── Administrator
|
|
Field Service
|
|
├── User
|
|
└── Administrator
|
|
|
|
Implied groups hierarchy:
|
|
Sales / Administrator
|
|
└── Sales / User: All Documents
|
|
|
|
Timesheets / Administrator
|
|
└── Timesheets / User: all timesheets
|
|
└── Timehseets / User: own timesheets only
|
|
|
|
Project / Administrator
|
|
├── Project / User
|
|
└── Timesheets / User: all timesheets
|
|
|
|
Field Service / Administrator
|
|
├── Sales / Administrator
|
|
├── Project / Administrator
|
|
└── Field Service / User
|
|
"""
|
|
super().setUpClass()
|
|
ResGroups = cls.env['res.groups']
|
|
IrModuleCategory = cls.env['ir.module.category']
|
|
categ_sales = IrModuleCategory.create({'name': 'Sales'})
|
|
categ_project = IrModuleCategory.create({'name': 'Project'})
|
|
categ_field_service = IrModuleCategory.create({'name': 'Field Service'})
|
|
categ_timesheets = IrModuleCategory.create({'name': 'Timesheets'})
|
|
|
|
# Sales
|
|
cls.group_sales_user, cls.group_sales_administrator = ResGroups.create([
|
|
{'name': 'User: All Documents', 'category_id': categ_sales.id},
|
|
{'name': 'Administrator', 'category_id': categ_sales.id},
|
|
])
|
|
cls.sales_categ_field = name_selection_groups((cls.group_sales_user | cls.group_sales_administrator).ids)
|
|
cls.group_sales_administrator.implied_ids = cls.group_sales_user
|
|
|
|
# Timesheets
|
|
cls.group_timesheets_user_own_timesheet = ResGroups.create([
|
|
{'name': 'User: own timesheets only', 'category_id': categ_timesheets.id}
|
|
])
|
|
cls.group_timesheets_user_all_timesheet = ResGroups.create([
|
|
{'name': 'User: all timesheets', 'category_id': categ_timesheets.id}
|
|
])
|
|
cls.group_timesheets_administrator = ResGroups.create([
|
|
{'name': 'Administrator', 'category_id': categ_timesheets.id}
|
|
])
|
|
cls.timesheets_categ_field = name_selection_groups((cls.group_timesheets_user_own_timesheet |
|
|
cls.group_timesheets_user_all_timesheet |
|
|
cls.group_timesheets_administrator).ids
|
|
)
|
|
cls.group_timesheets_administrator.implied_ids += cls.group_timesheets_user_all_timesheet
|
|
cls.group_timesheets_user_all_timesheet.implied_ids += cls.group_timesheets_user_own_timesheet
|
|
|
|
# Project
|
|
cls.group_project_user, cls.group_project_admnistrator = ResGroups.create([
|
|
{'name': 'User', 'category_id': categ_project.id},
|
|
{'name': 'Administrator', 'category_id': categ_project.id},
|
|
])
|
|
cls.project_categ_field = name_selection_groups((cls.group_project_user | cls.group_project_admnistrator).ids)
|
|
cls.group_project_admnistrator.implied_ids = (cls.group_project_user | cls.group_timesheets_user_all_timesheet)
|
|
|
|
# Field Service
|
|
cls.group_field_service_user, cls.group_field_service_administrator = ResGroups.create([
|
|
{'name': 'User', 'category_id': categ_field_service.id},
|
|
{'name': 'Administrator', 'category_id': categ_field_service.id},
|
|
])
|
|
cls.field_service_categ_field = name_selection_groups((cls.group_field_service_user | cls.group_field_service_administrator).ids)
|
|
cls.group_field_service_administrator.implied_ids = (cls.group_sales_administrator |
|
|
cls.group_project_admnistrator |
|
|
cls.group_field_service_user).ids
|
|
|
|
# User
|
|
cls.test_group_user = cls.env['res.users'].create({
|
|
'name': 'Test Group User',
|
|
'login': 'TestGroupUser',
|
|
'groups_id': (
|
|
cls.env.ref('base.group_user') |
|
|
cls.group_timesheets_administrator |
|
|
cls.group_field_service_administrator).ids,
|
|
})
|
|
|
|
|
|
def test_user_group_empty_group_warning(self):
|
|
""" User changes Empty Sales access from 'Sales: Administrator'. The
|
|
warning should be there since 'Sales: Administrator' is required when
|
|
user is having 'Field Service: Administrator'. When user reverts the
|
|
changes, warning should disappear. """
|
|
with Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
|
|
UserForm[self.sales_categ_field] = False
|
|
self.assertEqual(
|
|
UserForm.user_group_warning,
|
|
'Since Test Group User is a/an "Field Service: Administrator", they will at least obtain the right "Sales: Administrator"'
|
|
)
|
|
|
|
UserForm[self.sales_categ_field] = self.group_sales_administrator.id
|
|
self.assertFalse(UserForm.user_group_warning)
|
|
|
|
def test_user_group_inheritance_warning(self):
|
|
""" User changes 'Sales: User' from 'Sales: Administrator'. The warning
|
|
should be there since 'Sales: Administrator' is required when user is
|
|
having 'Field Service: Administrator'. When user reverts the changes,
|
|
warning should disappear. """
|
|
with Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
|
|
UserForm[self.sales_categ_field] = self.group_sales_user.id
|
|
self.assertEqual(
|
|
UserForm.user_group_warning,
|
|
'Since Test Group User is a/an "Field Service: Administrator", they will at least obtain the right "Sales: Administrator"'
|
|
)
|
|
|
|
UserForm[self.sales_categ_field] = self.group_sales_administrator.id
|
|
self.assertFalse(UserForm.user_group_warning)
|
|
|
|
def test_user_group_inheritance_warning_multi(self):
|
|
""" User changes 'Sales: User' from 'Sales: Administrator' and
|
|
'Project: User' from 'Project: Administrator'. The warning should
|
|
be there since 'Sales: Administrator' and 'Project: Administrator'
|
|
are required when user is havning 'Field Service: Administrator'.
|
|
When user reverts the changes For 'Sales: Administrator', warning
|
|
should disappear for Sales Access."""
|
|
with Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
|
|
UserForm[self.sales_categ_field] = self.group_sales_user.id
|
|
UserForm[self.project_categ_field] = self.group_project_user.id
|
|
self.assertTrue(
|
|
UserForm.user_group_warning,
|
|
'Since Test Group User is a/an "Field Service: Administrator", they will at least obtain the right "Sales: Administrator", Project: Administrator"',
|
|
)
|
|
|
|
UserForm[self.sales_categ_field] = self.group_sales_administrator.id
|
|
self.assertEqual(
|
|
UserForm.user_group_warning,
|
|
'Since Test Group User is a/an "Field Service: Administrator", they will at least obtain the right "Project: Administrator"'
|
|
)
|
|
|
|
def test_user_group_least_possible_inheritance_warning(self):
|
|
""" User changes 'Timesheets: User: own timesheets only ' from
|
|
'Timesheets: Administrator'. The warning should be there since
|
|
'Timesheets: User: all timesheets' is at least required when user is
|
|
having 'Project: Administrator'. When user reverts the changes For
|
|
'Timesheets: User: all timesheets', warning should disappear."""
|
|
with Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
|
|
UserForm[self.timesheets_categ_field] = self.group_timesheets_user_own_timesheet.id
|
|
self.assertEqual(
|
|
UserForm.user_group_warning,
|
|
'Since Test Group User is a/an "Project: Administrator", they will at least obtain the right "Timesheets: User: all timesheets"'
|
|
)
|
|
|
|
UserForm[self.timesheets_categ_field] = self.group_timesheets_user_all_timesheet.id
|
|
self.assertFalse(UserForm.user_group_warning)
|
|
|
|
def test_user_group_parent_inheritance_no_warning(self):
|
|
""" User changes 'Field Service: User' from 'Field Service: Administrator'.
|
|
The warning should not be there since 'Field Service: User' is not affected
|
|
by any other groups."""
|
|
with Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
|
|
UserForm[self.field_service_categ_field] = self.group_field_service_user.id
|
|
self.assertFalse(UserForm.user_group_warning)
|
|
|
|
|
|
class TestUsersTweaks(TransactionCase):
|
|
def test_superuser(self):
|
|
""" The superuser is inactive and must remain as such. """
|
|
user = self.env['res.users'].browse(SUPERUSER_ID)
|
|
self.assertFalse(user.active)
|
|
with self.assertRaises(UserError):
|
|
user.write({'active': True})
|