[IMP] base: modify some displayed texts for res_partner
* Add a new error message upon deleting the partner linked
to an active user.
* Improve the error message wording upon archiving the partner
linked to an active user. Add the linked users names at the end of
the message.
* If the user has 'write' access on res.users, raise a RedirectWarning
instead of a ValidationError, to offer an easy access to the related users :
- If there are multiple Users, redirect to a list view.
- If there is only one User, redirect to the form view.
* Adapt the test_access_deleted_records from test_orm.py to use a more
basic model : `res.partner.category` (with less business logic), because the
new unlink error message is dependent on a condition that will throw an error
in case of multiple unlinks of the same record (MissingError).
* Force close the frontend archive confirmation Dialog since _toggleArchiveState
may be interrupted by another Warning/Error Dialog. If the new Dialog was the
redirectWarning, and the user chose to be redirected, the first Dialog would
stay open even thought the context already changed.
* Modify the placeholders for a partner Name/Company
-> placeholders will change according to an Individual or a Company.
* Adapt the main_flow tour to correctly select the partner name field on
mobile (tested in enterprise).
Task ID : 2410217
PR : https://github.com/odoo/odoo/pull/63370
This commit is contained in:
committed by
Damien (abd)
parent
08d4707fe3
commit
162f3059d5
@@ -438,8 +438,11 @@ var ListController = BasicController.extend({
|
||||
otherActionItems.push({
|
||||
description: _t("Archive"),
|
||||
callback: () => {
|
||||
Dialog.confirm(this, _t("Are you sure that you want to archive all the selected records?"), {
|
||||
confirm_callback: () => this._toggleArchiveState(true),
|
||||
const dialog = Dialog.confirm(this, _t("Are you sure that you want to archive all the selected records?"), {
|
||||
confirm_callback: () => {
|
||||
this._toggleArchiveState(true);
|
||||
dialog.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
}, {
|
||||
|
||||
@@ -17,7 +17,7 @@ from werkzeug import urls
|
||||
from odoo import api, fields, models, tools, SUPERUSER_ID, _, Command
|
||||
from odoo.modules import get_module_resource
|
||||
from odoo.osv.expression import get_unaccent_wrapper
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.exceptions import RedirectWarning, UserError, ValidationError
|
||||
|
||||
# Global variables used for the warning fields declared on the res.partner
|
||||
# in the following modules : sale, purchase, account, stock
|
||||
@@ -515,9 +515,18 @@ class Partner(models.Model):
|
||||
# This is wrong if the user is not active, as partner.user_ids only returns active users.
|
||||
# Hence this temporary hack until the ORM updates inverse fields correctly.
|
||||
self.invalidate_cache(['user_ids'], self._ids)
|
||||
for partner in self:
|
||||
if partner.active and partner.user_ids:
|
||||
raise ValidationError(_('You cannot archive a contact linked to an internal user.'))
|
||||
users = self.env['res.users'].sudo().search([('partner_id', 'in', self.ids)])
|
||||
if users:
|
||||
if self.env['res.users'].sudo(False).check_access_rights('write', raise_exception=False):
|
||||
error_msg = _('You cannot archive contacts linked to an active user.\n'
|
||||
'You first need to archive their associated user.\n\n'
|
||||
'Linked active users : %(names)s', names=", ".join([u.display_name for u in users]))
|
||||
action_error = users._action_show()
|
||||
raise RedirectWarning(error_msg, action_error, _('Go to users'))
|
||||
else:
|
||||
raise ValidationError(_('You cannot archive contacts linked to an active user.\n'
|
||||
'Ask an administrator to archive their associated user first.\n\n'
|
||||
'Linked active users :\n%(names)s', names=", ".join([u.display_name for u in users])))
|
||||
# res.partner must only allow to set the company_id of a partner if it
|
||||
# is the same as the company of all users that inherit from this partner
|
||||
# (this is to allow the code from res_users to write to the partner!) or
|
||||
@@ -569,6 +578,22 @@ class Partner(models.Model):
|
||||
partner._handle_first_contact_creation()
|
||||
return partners
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_user(self):
|
||||
users = self.env['res.users'].sudo().search([('partner_id', 'in', self.ids)])
|
||||
if not users:
|
||||
return # no linked user, operation is allowed
|
||||
if self.env['res.users'].sudo(False).check_access_rights('write', raise_exception=False):
|
||||
error_msg = _('You cannot delete contacts linked to an active user.\n'
|
||||
'You should rather archive them after archiving their associated user.\n\n'
|
||||
'Linked active users : %(names)s', names=", ".join([u.display_name for u in users]))
|
||||
action_error = users._action_show()
|
||||
raise RedirectWarning(error_msg, action_error, _('Go to users'))
|
||||
else:
|
||||
raise ValidationError(_('You cannot delete contacts linked to an active user.\n'
|
||||
'Ask an administrator to archive their associated user first.\n\n'
|
||||
'Linked active users :\n%(names)s', names=", ".join([u.display_name for u in users])))
|
||||
|
||||
def _load_records_create(self, vals_list):
|
||||
partners = super(Partner, self.with_context(_partners_skip_fields_sync=True))._load_records_create(vals_list)
|
||||
|
||||
|
||||
@@ -835,6 +835,29 @@ class Users(models.Model):
|
||||
# for a few places explicitly clearing the has_group cache
|
||||
has_group.clear_cache = _has_group.clear_cache
|
||||
|
||||
def _action_show(self):
|
||||
"""If self is a singleton, directly access the form view. If it is a recordset, open a tree view"""
|
||||
view_id = self.env.ref('base.view_users_form').id
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'res.users',
|
||||
'context': {'create': False},
|
||||
}
|
||||
if len(self) > 1:
|
||||
action.update({
|
||||
'name': _('Users'),
|
||||
'view_mode': 'list,form',
|
||||
'views': [[None, 'list'], [view_id, 'form']],
|
||||
'domain': [('id', 'in', self.ids)],
|
||||
})
|
||||
else:
|
||||
action.update({
|
||||
'view_mode': 'form',
|
||||
'views': [[view_id, 'form']],
|
||||
'res_id': self.id,
|
||||
})
|
||||
return action
|
||||
|
||||
def action_show_groups(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import ast
|
||||
|
||||
from odoo import SUPERUSER_ID, Command
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.exceptions import RedirectWarning, UserError, ValidationError
|
||||
from odoo.tests.common import TransactionCase, BaseCase
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tools.safe_eval import safe_eval, const_eval, expr_eval
|
||||
@@ -508,8 +508,10 @@ class TestBase(TransactionCase):
|
||||
'partner_id': test_partner.id,
|
||||
})
|
||||
# Cannot archive the partner
|
||||
with self.assertRaises(RedirectWarning):
|
||||
test_partner.with_user(self.env.ref('base.user_admin')).toggle_active()
|
||||
with self.assertRaises(ValidationError):
|
||||
test_partner.toggle_active()
|
||||
test_partner.with_user(self.env.ref('base.user_demo')).toggle_active()
|
||||
|
||||
# Can archive the user but the partner stays active
|
||||
test_user.toggle_active()
|
||||
|
||||
@@ -17,9 +17,9 @@ class TestORM(TransactionCase):
|
||||
@mute_logger('odoo.models')
|
||||
def test_access_deleted_records(self):
|
||||
""" Verify that accessing deleted records works as expected """
|
||||
p1 = self.env['res.partner'].create({'name': 'W'})
|
||||
p2 = self.env['res.partner'].create({'name': 'Y'})
|
||||
p1.unlink()
|
||||
c1 = self.env['res.partner.category'].create({'name': 'W'})
|
||||
c2 = self.env['res.partner.category'].create({'name': 'Y'})
|
||||
c1.unlink()
|
||||
|
||||
# read() is expected to skip deleted records because our API is not
|
||||
# transactional for a sequence of search()->read() performed from the
|
||||
@@ -31,12 +31,12 @@ class TestORM(TransactionCase):
|
||||
'login': 'test2',
|
||||
'groups_id': [Command.set([self.ref('base.group_user')])],
|
||||
})
|
||||
ps = (p1 + p2).with_user(user)
|
||||
self.assertEqual([{'id': p2.id, 'name': 'Y'}], ps.read(['name']), "read() should skip deleted records")
|
||||
self.assertEqual([], ps[0].read(['name']), "read() should skip deleted records")
|
||||
cs = (c1 + c2).with_user(user)
|
||||
self.assertEqual([{'id': c2.id, 'name': 'Y'}], cs.read(['name']), "read() should skip deleted records")
|
||||
self.assertEqual([], cs[0].read(['name']), "read() should skip deleted records")
|
||||
|
||||
# Deleting an already deleted record should be simply ignored
|
||||
self.assertTrue(p1.unlink(), "Re-deleting should be a no-op")
|
||||
self.assertTrue(c1.unlink(), "Re-deleting should be a no-op")
|
||||
|
||||
@mute_logger('odoo.models')
|
||||
def test_access_partial_deletion(self):
|
||||
|
||||
@@ -81,11 +81,12 @@
|
||||
<div class="oe_title">
|
||||
<field name="company_type" options="{'horizontal': true}" widget="radio" groups="base.group_no_one"/>
|
||||
<h1>
|
||||
<field name="name" default_focus="1" placeholder="e.g. John Doe" attrs="{'required' : [('type', '=', 'contact')]}"/>
|
||||
<field name="name" default_focus="1" placeholder="e.g. Lumber Inc" attrs="{'required' : [('type', '=', 'contact')], 'invisible': [('is_company','=', False)]}"/>
|
||||
<field name="name" default_focus="1" placeholder="e.g. Brandom Freeman" attrs="{'required' : [('type', '=', 'contact')], 'invisible': [('is_company','=', True)]}"/>
|
||||
</h1>
|
||||
<field name="parent_id"
|
||||
widget="res_partner_many2one"
|
||||
placeholder="Company"
|
||||
placeholder="e.g. Lumber Inc"
|
||||
domain="[('is_company', '=', True)]" context="{'default_is_company': True, 'show_vat': True}"
|
||||
attrs="{'invisible': [('is_company','=', True)]}"/>
|
||||
</div>
|
||||
@@ -157,12 +158,13 @@
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="company_type" widget="radio" options="{'horizontal': true}"/>
|
||||
<h1>
|
||||
<field class="o_text_overflow" name="name" default_focus="1" placeholder="Name" attrs="{'required' : [('type', '=', 'contact')]}"/>
|
||||
<field class="o_text_overflow" name="name" default_focus="1" placeholder="e.g. Lumber Inc" attrs="{'required' : [('type', '=', 'contact')], 'invisible': [('is_company','=', False)]}"/>
|
||||
<field class="o_text_overflow" name="name" default_focus="1" placeholder="e.g. Brandom Freeman" attrs="{'required' : [('type', '=', 'contact')], 'invisible': [('is_company','=', True)]}"/>
|
||||
</h1>
|
||||
<div class="o_row">
|
||||
<field name="parent_id"
|
||||
widget="res_partner_many2one"
|
||||
placeholder="Company"
|
||||
placeholder="e.g. Lumber Inc"
|
||||
domain="[('is_company', '=', True)]" context="{'default_is_company': True, 'show_vat': True}"
|
||||
attrs="{'invisible': ['|', '&', ('is_company','=', True),('parent_id', '=', False),('company_name', '!=', False),('company_name', '!=', '')]}"/>
|
||||
<field name="company_name" attrs="{'invisible': ['|', '|', ('company_name', '=', False), ('company_name', '=', ''), ('is_company', '=', True)]}"/>
|
||||
|
||||
@@ -171,7 +171,7 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
|
||||
position: "right",
|
||||
}, {
|
||||
mobile: true,
|
||||
trigger: ".modal-dialog .o_field_widget[name=name] input",
|
||||
trigger: "input[name=name]:not(.o_invisible_modifier)",
|
||||
extra_trigger: ".modal:not(.o_inactive_modal) .modal-dialog .o_field_radio.o_field_widget[name=company_type]",
|
||||
content: _t('Select a seller'),
|
||||
position: 'top',
|
||||
@@ -462,7 +462,7 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
|
||||
position: "left",
|
||||
}, {
|
||||
mobile: true,
|
||||
trigger: '.o_field_widget[name=name] input',
|
||||
trigger: "input[name=name]:not(.o_invisible_modifier)",
|
||||
extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Organization / Contact')",
|
||||
content: _t('Let\'s enter the name.'),
|
||||
position: 'left',
|
||||
|
||||
Reference in New Issue
Block a user