From c1e8a32e7d1d4269fc2b66bd0c1057ca9320275e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Voet=20=28ryv=29?= Date: Thu, 26 Oct 2023 17:06:54 +0200 Subject: [PATCH] [FIX] core,*: Fix display_name computations for new records. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As of https://github.com/odoo/odoo/pull/114024, `display_name` is implicit in every form view (see the use of addFieldDependencies in the Form controller). Therefore, when you create a new record for this model, it calls the first `onchange`, which will compute display_name for a new record (id without origin). Some `_compute_display_name` don't handle new records correctly and raise a traceback. These models are sometimes directly accessible: - Accounting > Account Group > New => Traceback - Contact > Contact Tags > New => Traceback Other models are inaccessible by default (no view to access or create a new record), but if someone creates a view for them with studio (or modifies an existing one to allow creation): - `crm.iap.lead.role` - `crm.iap.lead.seniority` - `chatbot.script.answer` - `payment.token` Change the code of `_compute_display_name` on these models to be more defensive and avoid (potential) tracebacks. Similarly, change the `convert_to_display_name` of `fields.Datetime` to take into account `None` value. closes odoo/odoo#139592 Related: odoo/enterprise#49721 Signed-off-by: Rémy Voet (ryv) --- addons/account/models/account_account.py | 3 +- .../crm_iap_mine/models/crm_iap_lead_role.py | 2 +- .../models/crm_iap_lead_seniority.py | 2 +- .../models/chatbot_script_answer.py | 10 +++--- addons/payment/models/payment_token.py | 3 ++ odoo/addons/base/models/res_partner.py | 2 +- odoo/addons/base/tests/__init__.py | 1 + odoo/addons/base/tests/test_display_name.py | 34 +++++++++++++++++++ odoo/fields.py | 5 +-- 9 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 odoo/addons/base/tests/test_display_name.py diff --git a/addons/account/models/account_account.py b/addons/account/models/account_account.py index a3210cccfec..46f93b09a69 100644 --- a/addons/account/models/account_account.py +++ b/addons/account/models/account_account.py @@ -843,8 +843,7 @@ class AccountGroup(models.Model): prefix = group.code_prefix_start and str(group.code_prefix_start) if prefix and group.code_prefix_end != group.code_prefix_start: prefix += '-' + str(group.code_prefix_end) - name = (prefix and (prefix + ' ') or '') + group.name - group.display_name = name + group.display_name = ' '.join(filter(None, [prefix, group.name])) @api.model diff --git a/addons/crm_iap_mine/models/crm_iap_lead_role.py b/addons/crm_iap_mine/models/crm_iap_lead_role.py index faae0abd882..034cc5e33a3 100644 --- a/addons/crm_iap_mine/models/crm_iap_lead_role.py +++ b/addons/crm_iap_mine/models/crm_iap_lead_role.py @@ -19,4 +19,4 @@ class PeopleRole(models.Model): def _compute_display_name(self): for role in self: - role.display_name = role.name.replace('_', ' ').title() + role.display_name = (role.name or '').replace('_', ' ').title() diff --git a/addons/crm_iap_mine/models/crm_iap_lead_seniority.py b/addons/crm_iap_mine/models/crm_iap_lead_seniority.py index 50211413bf8..63e24b13f7c 100644 --- a/addons/crm_iap_mine/models/crm_iap_lead_seniority.py +++ b/addons/crm_iap_mine/models/crm_iap_lead_seniority.py @@ -19,4 +19,4 @@ class PeopleSeniority(models.Model): @api.depends('name') def _compute_display_name(self): for seniority in self: - seniority.display_name = seniority.name.replace('_', ' ').title() + seniority.display_name = (seniority.name or '').replace('_', ' ').title() diff --git a/addons/im_livechat/models/chatbot_script_answer.py b/addons/im_livechat/models/chatbot_script_answer.py index b78b528c4c1..03ce917e43d 100644 --- a/addons/im_livechat/models/chatbot_script_answer.py +++ b/addons/im_livechat/models/chatbot_script_answer.py @@ -28,10 +28,12 @@ class ChatbotScriptAnswer(models.Model): return super()._compute_display_name() for answer in self: - answer_message = answer.script_step_id.message.replace('\n', ' ') - shortened_message = textwrap.shorten(answer_message, width=26, placeholder=" [...]") - - answer.display_name = f"{shortened_message}: {answer.name}" + if answer.script_step_id: + answer_message = answer.script_step_id.message.replace('\n', ' ') + shortened_message = textwrap.shorten(answer_message, width=26, placeholder=" [...]") + answer.display_name = f"{shortened_message}: {answer.name}" + else: + answer.display_name = answer.name @api.model def _name_search(self, name, domain=None, operator='ilike', limit=None, order=None): diff --git a/addons/payment/models/payment_token.py b/addons/payment/models/payment_token.py index 6afd227d9cb..557e85526f4 100644 --- a/addons/payment/models/payment_token.py +++ b/addons/payment/models/payment_token.py @@ -158,6 +158,9 @@ class PaymentToken(models.Model): """ self.ensure_one() + if not self.create_date: + return '' + padding_length = max_length - len(self.payment_details or '') if not self.payment_details: create_date_str = self.create_date.strftime('%Y/%m/%d') diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index 2d30920cdd2..2d09b7807f6 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -137,7 +137,7 @@ class PartnerCategory(models.Model): names = [] current = category while current: - names.append(current.name) + names.append(current.name or "") current = current.parent_id category.display_name = ' / '.join(reversed(names)) diff --git a/odoo/addons/base/tests/__init__.py b/odoo/addons/base/tests/__init__.py index 8229428d78f..e57be3c2c5e 100644 --- a/odoo/addons/base/tests/__init__.py +++ b/odoo/addons/base/tests/__init__.py @@ -9,6 +9,7 @@ from . import test_basecase from . import test_cache from . import test_deprecation from . import test_db_cursor +from . import test_display_name from . import test_expression from . import test_float from . import test_format_address_mixin diff --git a/odoo/addons/base/tests/test_display_name.py b/odoo/addons/base/tests/test_display_name.py new file mode 100644 index 00000000000..bafb49bec85 --- /dev/null +++ b/odoo/addons/base/tests/test_display_name.py @@ -0,0 +1,34 @@ +import contextlib + +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase, tagged + + +IGNORE_MODEL_NAMES = { + 'ir.attachment', + 'test_new_api.attachment', + 'payment.link.wizard', + 'account.multicurrency.revaluation.wizard', + 'account_followup.manual_reminder', +} + +@tagged('-at_install', 'post_install') +class TestEveryModel(TransactionCase): + + def test_display_name_new_record(self): + for model_name in self.registry: + model = self.env[model_name] + if model._abstract or not model._auto or model_name in IGNORE_MODEL_NAMES: + continue + + with self.subTest( + msg="`_compute_display_name` doesn't work with new record (first onchange call).", + model=model_name, + ): + # Check that the first onchange with display_name works on every models + # OR it will fail anyway when people will use click on New + fields_used = model._fields['display_name'].get_depends(model)[0] + fields_used = [f.split('.', 1)[0] for f in fields_used] + fields_spec = dict.fromkeys(fields_used + ['display_name'], {}) + with contextlib.suppress(UserError): + model.onchange({}, [], fields_spec) diff --git a/odoo/fields.py b/odoo/fields.py index 361a5ec292a..49077d05161 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -2297,8 +2297,9 @@ class Datetime(Field): return self.from_string(value) def convert_to_display_name(self, value, record): - assert record, 'Record expected' - return Datetime.to_string(Datetime.context_timestamp(record, Datetime.from_string(value))) + if not value: + return False + return Datetime.to_string(Datetime.context_timestamp(record, value)) # http://initd.org/psycopg/docs/usage.html#binary-adaptation # Received data is returned as buffer (in Python 2) or memoryview (in Python 3).