From 3b6dd12bcd4ad6c890a1846ad40877f3f2a8e11f Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 14 Feb 2019 17:03:58 +0100 Subject: [PATCH] [IMP] onchange_helper: Improve performance when called on an existing record Before this change, the inmemory record was populated from values copied from the exisitng record. It was no predictable to know in advance which values were required to correctly play onchange methods. Therefore all the values defined on the model were copied from the exising record to populate the inmemory record. The side effect of this approach was that in a lot of cases, a lot of useless values was copied leading to performance issue with computed fields. With this change, we use the current record to call the onchange methods in an onchange context to avoid direct write to the database each time a new value is assigned by an onchange. At the end of the process, we restore the current record to its original state and return a dictionary with only the fields modified by the onchange. --- .travis.yml | 6 +- onchange_helper/README.rst | 1 + onchange_helper/__manifest__.py | 7 +- onchange_helper/models/models.py | 246 ++++++++++-- onchange_helper/tests/test_onchange.py | 64 ++- setup/test_onchange_helper/odoo/__init__.py | 1 + .../odoo/addons/__init__.py | 1 + .../odoo/addons/test_onchange_helper | 1 + setup/test_onchange_helper/setup.py | 6 + test_onchange_helper/README.rst | 9 + test_onchange_helper/__init__.py | 1 + test_onchange_helper/__manifest__.py | 26 ++ .../demo/test_onchange_helper_discussion.xml | 12 + .../demo/test_onchange_helper_message.xml | 23 ++ test_onchange_helper/models/__init__.py | 6 + .../models/test_onchange_helper_category.py | 44 ++ .../models/test_onchange_helper_discussion.py | 72 ++++ .../test_onchange_helper_emailmessage.py | 20 + .../models/test_onchange_helper_message.py | 57 +++ .../models/test_onchange_helper_multi.py | 25 ++ .../models/test_onchange_helper_multi_line.py | 15 + test_onchange_helper/readme/CONTRIBUTORS.rst | 1 + test_onchange_helper/readme/DESCRIPTION.rst | 1 + .../test_onchange_helper_category.xml | 17 + .../test_onchange_helper_discussion.xml | 17 + .../test_onchange_helper_emailmessage.xml | 17 + .../security/test_onchange_helper_message.xml | 17 + .../security/test_onchange_helper_multi.xml | 17 + .../test_onchange_helper_multi_line.xml | 17 + .../static/description/icon.png | Bin 0 -> 9455 bytes test_onchange_helper/tests/__init__.py | 1 + .../tests/test_onchange_helper.py | 378 ++++++++++++++++++ 32 files changed, 1069 insertions(+), 57 deletions(-) create mode 100644 setup/test_onchange_helper/odoo/__init__.py create mode 100644 setup/test_onchange_helper/odoo/addons/__init__.py create mode 120000 setup/test_onchange_helper/odoo/addons/test_onchange_helper create mode 100644 setup/test_onchange_helper/setup.py create mode 100644 test_onchange_helper/README.rst create mode 100644 test_onchange_helper/__init__.py create mode 100644 test_onchange_helper/__manifest__.py create mode 100644 test_onchange_helper/demo/test_onchange_helper_discussion.xml create mode 100644 test_onchange_helper/demo/test_onchange_helper_message.xml create mode 100644 test_onchange_helper/models/__init__.py create mode 100644 test_onchange_helper/models/test_onchange_helper_category.py create mode 100644 test_onchange_helper/models/test_onchange_helper_discussion.py create mode 100644 test_onchange_helper/models/test_onchange_helper_emailmessage.py create mode 100644 test_onchange_helper/models/test_onchange_helper_message.py create mode 100644 test_onchange_helper/models/test_onchange_helper_multi.py create mode 100644 test_onchange_helper/models/test_onchange_helper_multi_line.py create mode 100644 test_onchange_helper/readme/CONTRIBUTORS.rst create mode 100644 test_onchange_helper/readme/DESCRIPTION.rst create mode 100644 test_onchange_helper/security/test_onchange_helper_category.xml create mode 100644 test_onchange_helper/security/test_onchange_helper_discussion.xml create mode 100644 test_onchange_helper/security/test_onchange_helper_emailmessage.xml create mode 100644 test_onchange_helper/security/test_onchange_helper_message.xml create mode 100644 test_onchange_helper/security/test_onchange_helper_multi.xml create mode 100644 test_onchange_helper/security/test_onchange_helper_multi_line.xml create mode 100644 test_onchange_helper/static/description/icon.png create mode 100644 test_onchange_helper/tests/__init__.py create mode 100644 test_onchange_helper/tests/test_onchange_helper.py diff --git a/.travis.yml b/.travis.yml index f8f7def57..6fd40d591 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,8 +25,10 @@ env: matrix: - LINT_CHECK="1" - TRANSIFEX="1" - - TESTS="1" ODOO_REPO="odoo/odoo" EXCLUDE="database_cleanup,auth_session_timeout,auth_brute_force,base_import_odoo" - - TESTS="1" ODOO_REPO="OCA/OCB" EXCLUDE="database_cleanup,auth_session_timeout,auth_brute_force,base_import_odoo" + - TESTS="1" ODOO_REPO="odoo/odoo" EXCLUDE="database_cleanup,auth_session_timeout,auth_brute_force,base_import_odoo,onchange_helper,test_onchange_helper" + - TESTS="1" ODOO_REPO="OCA/OCB" EXCLUDE="database_cleanup,auth_session_timeout,auth_brute_force,base_import_odoo,onchange_helper,test_onchange_helper" + - TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="onchange_helper,test_onchange_helper" + - TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="onchange_helper,test_onchange_helper" - TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="database_cleanup" - TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="database_cleanup" - TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="auth_session_timeout" diff --git a/onchange_helper/README.rst b/onchange_helper/README.rst index c30d33249..794346959 100644 --- a/onchange_helper/README.rst +++ b/onchange_helper/README.rst @@ -56,6 +56,7 @@ Contributors * Guewen Baconnier * Florian da Costa +* Laurent Mignon Maintainer ---------- diff --git a/onchange_helper/__manifest__.py b/onchange_helper/__manifest__.py index 0d1b9b6a1..eb8f6207d 100644 --- a/onchange_helper/__manifest__.py +++ b/onchange_helper/__manifest__.py @@ -3,10 +3,11 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). {'name': 'Onchange Helper', - 'version': '10.0.1.0.0', + 'version': '10.0.2.0.0', 'summary': 'Technical module that ease execution of onchange in Python code', - 'author': 'Akretion,Camptocamp,Odoo Community Association (OCA)', - 'website': 'http://www.akretion.com', + 'author': 'Akretion,Camptocamp, ACSONE SA/NV, ' + 'Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/server-tools', 'license': 'AGPL-3', 'category': 'Generic Modules', 'depends': ['base'], diff --git a/onchange_helper/models/models.py b/onchange_helper/models/models.py index 3c40ec23e..d532fbde3 100644 --- a/onchange_helper/models/models.py +++ b/onchange_helper/models/models.py @@ -1,51 +1,219 @@ # -*- coding: utf-8 -*- # © 2016-2017 Akretion (http://www.akretion.com) # © 2016-2017 Camptocamp (http://www.camptocamp.com/) +# © 2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, models +from odoo import api, models, fields class Base(models.AbstractModel): - _inherit = 'base' + _inherit = "base" @api.model - def _get_new_values(self, record, on_change_result): - vals = on_change_result.get('value', {}) - new_values = {} - for fieldname, value in vals.iteritems(): - if fieldname not in record: - column = self._fields[fieldname] - if value and column.type == 'many2one': - value = value[0] # many2one are tuple (id, name) - new_values[fieldname] = value - return new_values - - def play_onchanges(self, values, onchange_fields): - onchange_specs = self._onchange_spec() - # we need all fields in the dict even the empty ones - # otherwise 'onchange()' will not apply changes to them - all_values = values.copy() - - # If self is a record (play onchange on existing record) - # we take the value of the field - # If self is an empty record we will have an empty value + def _compute_onchange_dirty( + self, original_record, modified_record, fieldname_onchange=None + ): + """ + Return the list of dirty fields. (designed to be called by + play_onchanges) + The list of dirty fields is computed from the list marked as dirty + on the record. Form this list, we remove the fields for which the value + into the original record is the same as the one into the current record + :param original_record: + :return: changed values + """ + dirties = [] + if fieldname_onchange: + for field_name, field in modified_record._fields.items(): + # special case. We consider that a related field is modified + # if a modified field is in the first position of the path + # to traverse to get the value. + if field.related and field.related[0].startswith( + fieldname_onchange + ): + dirties.append(field_name) + for field_name in modified_record._get_dirty(): + original_value = original_record[field_name] + new_value = modified_record[field_name] + if original_value == new_value: + continue + dirties.append(field_name) + for field_name, field in modified_record._fields.items(): + new_value = modified_record[field_name] + if field.type not in ("one2many", "many2many"): + continue + # if the field is a one2many or many2many, check that any + # item is a new Id + if models.NewId in [type(i.id) for i in new_value]: + dirties.append(field_name) + continue + # if the field is a one2many or many2many, check if any item + # is dirty + for r in new_value: + if r._get_dirty(): + ori = [ + r1 + for r1 in original_record[field_name] + if r1.id == r.id + ][0] + # if fieldname_onchange is None avoid recurssion... + if fieldname_onchange and self._compute_onchange_dirty( + ori, r + ): + dirties.append(field_name) + break + return dirties + + def _convert_to_onchange(self, record, field, value): + if field.type == "many2one": + # for many2one, we keep the id and don't call the + # convert_on_change to avoid the call to name_get by the + # convert_to_onchange + if value.id: + return value.id + return False + elif field.type in ("one2many", "many2many"): + result = [(5,)] + for record in value: + vals = {} + for name in record._cache: + if name in models.LOG_ACCESS_COLUMNS: + continue + v = record[name] + f = record._fields[name] + if f.type == "many2one" and isinstance(v.id, models.NewId): + continue + vals[name] = self._convert_to_onchange(record, f, v) + if not record.id: + result.append((0, 0, vals)) + elif vals: + result.append((1, record.id, vals)) + else: + result.append((4, record.id)) + return result + else: + return field.convert_to_onchange(value, record, [field.name]) + + def play_onchanges(self, values, onchange_fields=None): + """ + Play the onchange methods defined on the current record and return the + changed values. + The record is not modified by the onchange. + + The intend of this method is to provide a way to get on the server side + the values returned by the onchange methods when called by the UI. + This method is useful in B2B contexts where records are created or + modified from server side. + + The returned values are those changed by the execution of onchange + methods registered for the onchange_fields according to the provided + values. As consequence, the result will not contain the modifications + that could occurs by the execution of compute methods registered for + the same onchange_fields. + + It's on purpose that we avoid to trigger the compute methods for the + onchange_fields. These compute methods will be triggered when calling + the create or write method. In this way we avoid to compute useless + information. + + + :param values: dict of input value that + :param onchange_fields: fields for which onchange methods will be + played. If not provided, the list of field is based on the values keys. + Order in onchange_fields is very important as onchanges methods will + be played in that order. + :return: changed values + + This method reimplement the onchange method to be able to work on the + current recordset if provided. + """ + env = self.env if self: self.ensure_one() - record_values = self._convert_to_write(self.read()[0]) - else: - record_values = {} - for field in self._fields: - if field not in all_values: - all_values[field] = record_values.get(field, False) - - new_values = {} - for field in onchange_fields: - onchange_values = self.onchange(all_values, field, onchange_specs) - new_values.update(self._get_new_values(values, onchange_values)) - all_values.update(new_values) - - return { - f: v for f, v in all_values.iteritems() - if not (self._fields[f].compute and not self._fields[f].inverse) - and (f in values or f in new_values)} + + if not isinstance(onchange_fields, list): + onchange_fields = [onchange_fields] + + if not onchange_fields: + onchange_fields = values.keys() + + # filter out keys in field_onchange that do not refer to actual fields + names = [n for n in onchange_fields if n in self._fields] + + # create a new record with values, and attach ``self`` to it + with env.do_in_onchange(): + # keep a copy of the original record. + # attach ``self`` with a different context (for cache consistency) + origin = self.with_context(__onchange=True) + origin_dirty = set(self._get_dirty()) + fields.copy_cache(self, origin.env) + if self: + record = self + record.update(values) + else: + # initialize with default values, they may be used in onchange + new_values = self.default_get(self._fields.keys()) + new_values.update(values) + record = self.new(new_values) + values = {name: record[name] for name in record._cache} + record._origin = origin + + # determine which field(s) should be triggered an onchange + todo = list(names) or list(values) + done = set() + + # dummy assignment: trigger invalidations on the record + with env.do_in_onchange(): + for name in todo: + if name == "id": + continue + value = record[name] + field = self._fields[name] + if field.type == "many2one" and field.delegate and not value: + # do not nullify all fields of parent record for new + # records + continue + record[name] = value + + dirty = set() + + # process names in order (or the keys of values if no name given) + while todo: + name = todo.pop(0) + if name in done: + continue + done.add(name) + + with env.do_in_onchange(): + # apply field-specific onchange methods + record._onchange_eval(name, "1", {}) + + # determine which fields have been modified + dirties = self._compute_onchange_dirty(origin, record, name) + dirty |= set(dirties) + todo.extend(dirties) + + # prepare the result to return a dictionary with the new values for + # the dirty fields + result = {} + for name in dirty: + field = self._fields[name] + value = record[name] + if field.type == "many2one" and isinstance(value.id, models.NewId): + continue + result[name] = self._convert_to_onchange(record, field, value) + + # reset dirty values into the current record + if self: + to_reset = dirty | set(values.keys()) + with env.do_in_onchange(): + for name in to_reset: + original = origin[name] + new = self[name] + if original == new: + continue + self[name] = origin[name] + env.dirty[record].discard(name) + env.dirty[record].update(origin_dirty) + return result diff --git a/onchange_helper/tests/test_onchange.py b/onchange_helper/tests/test_onchange.py index 22e155294..101a997de 100644 --- a/onchange_helper/tests/test_onchange.py +++ b/onchange_helper/tests/test_onchange.py @@ -1,26 +1,64 @@ # -*- coding: utf-8 -*- # Copyright 2018 Akretion (http://www.akretion.com). +# Copyright 2019 ACSONE SA/NV # @author Sébastien BEAU # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import openerp.tests.common as common +import mock +import odoo.tests.common as common class TestOnchange(common.TransactionCase): def test_playing_onchange_on_model(self): - result = self.env['res.partner'].play_onchanges({ - 'company_type': 'company', - }, ['company_type']) - self.assertEqual(result['is_company'], True) + res_partner = self.env["res.partner"] + with mock.patch.object( + res_partner.__class__, "write" + ) as patched_write: + result = self.env["res.partner"].play_onchanges( + {"company_type": "company"}, ["company_type"] + ) + patched_write.assert_not_called() + self.assertEqual(result["is_company"], True) def test_playing_onchange_on_record(self): - company = self.env.ref('base.main_company') - result = company.play_onchanges({ - 'email': 'contact@akretion.com'}, - ['email']) + company = self.env.ref("base.main_company") + with mock.patch.object(company.__class__, "write") as patched_write: + result = company.play_onchanges( + {"email": "contact@akretion.com"}, ["email"] + ) + patched_write.assert_not_called() + modified_fields = set(result.keys()) + self.assertSetEqual( + modified_fields, {"rml_footer", "rml_footer_readonly"} + ) self.assertEqual( - result['rml_footer'], - u'Phone: +1 555 123 8069 | Email: contact@akretion.com | ' - u'Website: http://www.example.com') - self.assertEqual(company.email, u'info@yourcompany.example.com') + result["rml_footer"], + u"Phone: +1 555 123 8069 | Email: contact@akretion.com | " + u"Website: http://www.example.com", + ) + self.assertEqual(result["rml_footer_readonly"], result["rml_footer"]) + + # check that the original record is not modified + self.assertFalse(company._get_dirty()) + self.assertEqual(company.email, u"info@yourcompany.example.com") + + def test_onchange_record_with_dirty_field(self): + company = self.env.ref("base.main_company") + company._set_dirty("name") + self.assertListEqual(company._get_dirty(), ["name"]) + company.play_onchanges({"email": "contact@akretion.com"}, ["email"]) + self.assertListEqual(company._get_dirty(), ["name"]) + + def test_onchange_wrong_key(self): + res_partner = self.env["res.partner"] + with mock.patch.object( + res_partner.__class__, "write" + ) as patched_write: + # we specify a wrong field name... This field should be + # ignored + result = self.env["res.partner"].play_onchanges( + {"company_type": "company"}, ["company_type", "wrong_key"] + ) + patched_write.assert_not_called() + self.assertEqual(result["is_company"], True) diff --git a/setup/test_onchange_helper/odoo/__init__.py b/setup/test_onchange_helper/odoo/__init__.py new file mode 100644 index 000000000..de40ea7ca --- /dev/null +++ b/setup/test_onchange_helper/odoo/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/test_onchange_helper/odoo/addons/__init__.py b/setup/test_onchange_helper/odoo/addons/__init__.py new file mode 100644 index 000000000..de40ea7ca --- /dev/null +++ b/setup/test_onchange_helper/odoo/addons/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/test_onchange_helper/odoo/addons/test_onchange_helper b/setup/test_onchange_helper/odoo/addons/test_onchange_helper new file mode 120000 index 000000000..d3646f7a5 --- /dev/null +++ b/setup/test_onchange_helper/odoo/addons/test_onchange_helper @@ -0,0 +1 @@ +../../../../test_onchange_helper \ No newline at end of file diff --git a/setup/test_onchange_helper/setup.py b/setup/test_onchange_helper/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/test_onchange_helper/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/test_onchange_helper/README.rst b/test_onchange_helper/README.rst new file mode 100644 index 000000000..29b3e6caa --- /dev/null +++ b/test_onchange_helper/README.rst @@ -0,0 +1,9 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +==================== +Onchange Helper TEST +==================== + +*This module is only intended to test the onchange_helper addon.* diff --git a/test_onchange_helper/__init__.py b/test_onchange_helper/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/test_onchange_helper/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/test_onchange_helper/__manifest__.py b/test_onchange_helper/__manifest__.py new file mode 100644 index 000000000..93855c767 --- /dev/null +++ b/test_onchange_helper/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Onchange Helper TEST", + "summary": """ + Test addon for the onchange_helper addon""", + "version": "10.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://acsone.eu/", + "depends": ["onchange_helper"], + "data": [ + 'security/test_onchange_helper_multi_line.xml', + 'security/test_onchange_helper_multi.xml', + "security/test_onchange_helper_emailmessage.xml", + "security/test_onchange_helper_message.xml", + "security/test_onchange_helper_discussion.xml", + "security/test_onchange_helper_category.xml", + ], + "demo": [ + "demo/test_onchange_helper_discussion.xml", + "demo/test_onchange_helper_message.xml", + ] +} diff --git a/test_onchange_helper/demo/test_onchange_helper_discussion.xml b/test_onchange_helper/demo/test_onchange_helper_discussion.xml new file mode 100644 index 000000000..06e119d08 --- /dev/null +++ b/test_onchange_helper/demo/test_onchange_helper_discussion.xml @@ -0,0 +1,12 @@ + + + + + + + Stuff + + + + diff --git a/test_onchange_helper/demo/test_onchange_helper_message.xml b/test_onchange_helper/demo/test_onchange_helper_message.xml new file mode 100644 index 000000000..5860fd0b5 --- /dev/null +++ b/test_onchange_helper/demo/test_onchange_helper_message.xml @@ -0,0 +1,23 @@ + + + + + + + + Hey dude! + + + + + + What's up? + + + + + This is a much longer message + + + diff --git a/test_onchange_helper/models/__init__.py b/test_onchange_helper/models/__init__.py new file mode 100644 index 000000000..0002916fc --- /dev/null +++ b/test_onchange_helper/models/__init__.py @@ -0,0 +1,6 @@ +from . import test_onchange_helper_category +from . import test_onchange_helper_discussion +from . import test_onchange_helper_message +from . import test_onchange_helper_emailmessage +from . import test_onchange_helper_multi +from . import test_onchange_helper_multi_line diff --git a/test_onchange_helper/models/test_onchange_helper_category.py b/test_onchange_helper/models/test_onchange_helper_category.py new file mode 100644 index 000000000..7533f74b6 --- /dev/null +++ b/test_onchange_helper/models/test_onchange_helper_category.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class TestOnchangeHelperCategory(models.Model): + + _name = "test_onchange_helper.category" + _description = "Test Onchange Helper Category" + + name = fields.Char(required=True) + parent = fields.Many2one(_name) + root_categ = fields.Many2one(_name) + display_name = fields.Char() + computed_display_name = fields.Char( + compute="_compute_computed_display_name" + ) + + @api.onchange("name", "parent") + def _onchange_name_or_parent(self): + if self.parent: + self.display_name = self.parent.display_name + " / " + self.name + else: + self.display_name = self.name + + @api.onchange("parent") + def _onchange_parent(self): + current = self + while current.parent: + current = current.parent + if current == self: + self.root_categ = False + else: + self.root_categ = current + + @api.depends("name", "parent.display_name") + def _compute_computed_display_name(self): + for cat in self: + if cat.parent: + self.display_name = cat.parent.display_name + " / " + cat.name + else: + cat.display_name = cat.name diff --git a/test_onchange_helper/models/test_onchange_helper_discussion.py b/test_onchange_helper/models/test_onchange_helper_discussion.py new file mode 100644 index 000000000..9e3f2aa5b --- /dev/null +++ b/test_onchange_helper/models/test_onchange_helper_discussion.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class TestOnchangeHelperDiscussion(models.Model): + + _name = "test_onchange_helper.discussion" + _description = "Test Onchange Helper Discussion" + + name = fields.Char( + string="Title", + required=True, + help="General description of what this discussion is about.", + ) + moderator = fields.Many2one("res.users") + categories = fields.Many2many( + "test_onchange_helper.category", + "test_onchange_helper_discussion_category", + "discussion", + "category", + ) + participants = fields.Many2many("res.users") + messages = fields.One2many("test_onchange_helper.message", "discussion") + message_concat = fields.Text(string="Message concatenate") + important_messages = fields.One2many( + "test_onchange_helper.message", + "discussion", + domain=[("important", "=", True)], + ) + very_important_messages = fields.One2many( + "test_onchange_helper.message", + "discussion", + domain=lambda self: self._domain_very_important(), + ) + emails = fields.One2many("test_onchange_helper.emailmessage", "discussion") + important_emails = fields.One2many( + "test_onchange_helper.emailmessage", + "discussion", + domain=[("important", "=", True)], + ) + + def _domain_very_important(self): + """Ensure computed O2M domains work as expected.""" + return [("important", "=", True)] + + @api.onchange("name") + def _onchange_name(self): + # test onchange modifying one2many field values + # update body of existings messages and emails + for message in self.messages: + message.body = "not last dummy message" + for message in self.important_messages: + message.body = "not last dummy message" + # add new dummy message + message_vals = self.messages._add_missing_default_values( + {"body": "dummy message", "important": True} + ) + self.messages |= self.messages.new(message_vals) + self.important_messages |= self.messages.new(message_vals) + + @api.onchange("moderator") + def _onchange_moderator(self): + self.participants |= self.moderator + + @api.onchange("messages") + def _onchange_messages(self): + self.message_concat = "\n".join( + ["%s:%s" % (m.name, m.body) for m in self.messages] + ) diff --git a/test_onchange_helper/models/test_onchange_helper_emailmessage.py b/test_onchange_helper/models/test_onchange_helper_emailmessage.py new file mode 100644 index 000000000..252222c5f --- /dev/null +++ b/test_onchange_helper/models/test_onchange_helper_emailmessage.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class TestOnchangeHelperEmailmessage(models.Model): + + _name = "test_onchange_helper.emailmessage" + _description = "Test Onchange Helper Emailmessage" + _inherits = {"test_onchange_helper.message": "message"} + + message = fields.Many2one( + "test_onchange_helper.message", + "Message", + required=True, + ondelete="cascade", + ) + email_to = fields.Char("To") diff --git a/test_onchange_helper/models/test_onchange_helper_message.py b/test_onchange_helper/models/test_onchange_helper_message.py new file mode 100644 index 000000000..64af72ce8 --- /dev/null +++ b/test_onchange_helper/models/test_onchange_helper_message.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class TestOnchangeHelperMessage(models.Model): + + _name = "test_onchange_helper.message" + _description = "Test Onchange Helper Message" + + discussion = fields.Many2one( + "test_onchange_helper.discussion", ondelete="cascade" + ) + body = fields.Text() + author = fields.Many2one("res.users", default=lambda self: self.env.user) + name = fields.Char(string="Title", compute="_compute_name", store=True) + display_name = fields.Char( + string="Abstract", compute="_compute_display_name" + ) + discussion_name = fields.Char( + related="discussion.name", string="Discussion Name" + ) + author_partner = fields.Many2one( + "res.partner", + compute="_compute_author_partner", + search="_search_author_partner", + ) + important = fields.Boolean() + + @api.one + @api.depends("author.name", "discussion.name") + def _compute_name(self): + self.name = "[%s] %s" % ( + self.discussion.name or "", + self.author.name or "", + ) + + @api.one + @api.depends("author.name", "discussion.name", "body") + def _compute_display_name(self): + stuff = "[%s] %s: %s" % ( + self.author.name, + self.discussion.name or "", + self.body or "", + ) + self.display_name = stuff[:80] + + @api.one + @api.depends("author", "author.partner_id") + def _compute_author_partner(self): + self.author_partner = self.author.partner_id + + @api.model + def _search_author_partner(self, operator, value): + return [("author.partner_id", operator, value)] diff --git a/test_onchange_helper/models/test_onchange_helper_multi.py b/test_onchange_helper/models/test_onchange_helper_multi.py new file mode 100644 index 000000000..ae55e295a --- /dev/null +++ b/test_onchange_helper/models/test_onchange_helper_multi.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class TestOnchangeHelperMulti(models.Model): + + _name = "test_onchange_helper.multi" + _description = "Test Onchange Helper Multi" + + name = fields.Char(related="partner.name", readonly=True) + partner = fields.Many2one("res.partner") + lines = fields.One2many("test_onchange_helper.multi.line", "multi") + + @api.onchange("name") + def _onchange_name(self): + for line in self.lines: + line.name = self.name + + @api.onchange("partner") + def _onchange_partner(self): + for line in self.lines: + line.partner = self.partner diff --git a/test_onchange_helper/models/test_onchange_helper_multi_line.py b/test_onchange_helper/models/test_onchange_helper_multi_line.py new file mode 100644 index 000000000..841873be9 --- /dev/null +++ b/test_onchange_helper/models/test_onchange_helper_multi_line.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class TestOnchangeHelperMultiLine(models.Model): + + _name = "test_onchange_helper.multi.line" + _description = "Test Onchange Helper Multi Line" + + multi = fields.Many2one("test_onchange_helper.multi", ondelete="cascade") + name = fields.Char() + partner = fields.Many2one("res.partner") diff --git a/test_onchange_helper/readme/CONTRIBUTORS.rst b/test_onchange_helper/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..586a941fd --- /dev/null +++ b/test_onchange_helper/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Laurent Mignon (https://acsone.eu) \ No newline at end of file diff --git a/test_onchange_helper/readme/DESCRIPTION.rst b/test_onchange_helper/readme/DESCRIPTION.rst new file mode 100644 index 000000000..bacdee0ee --- /dev/null +++ b/test_onchange_helper/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +*This module is only intended to test the onchange_helper addon.* diff --git a/test_onchange_helper/security/test_onchange_helper_category.xml b/test_onchange_helper/security/test_onchange_helper_category.xml new file mode 100644 index 000000000..2ff570df3 --- /dev/null +++ b/test_onchange_helper/security/test_onchange_helper_category.xml @@ -0,0 +1,17 @@ + + + + + + + test_onchange_helper.category access name + + + + + + + + + diff --git a/test_onchange_helper/security/test_onchange_helper_discussion.xml b/test_onchange_helper/security/test_onchange_helper_discussion.xml new file mode 100644 index 000000000..9efab1419 --- /dev/null +++ b/test_onchange_helper/security/test_onchange_helper_discussion.xml @@ -0,0 +1,17 @@ + + + + + + + test_onchange_helperdiscussion access name + + + + + + + + + diff --git a/test_onchange_helper/security/test_onchange_helper_emailmessage.xml b/test_onchange_helper/security/test_onchange_helper_emailmessage.xml new file mode 100644 index 000000000..8150dc30c --- /dev/null +++ b/test_onchange_helper/security/test_onchange_helper_emailmessage.xml @@ -0,0 +1,17 @@ + + + + + + + test_onchange_helperemailmessage access name + + + + + + + + + diff --git a/test_onchange_helper/security/test_onchange_helper_message.xml b/test_onchange_helper/security/test_onchange_helper_message.xml new file mode 100644 index 000000000..75ae9cd81 --- /dev/null +++ b/test_onchange_helper/security/test_onchange_helper_message.xml @@ -0,0 +1,17 @@ + + + + + + + test_onchange_helper.message access name + + + + + + + + + diff --git a/test_onchange_helper/security/test_onchange_helper_multi.xml b/test_onchange_helper/security/test_onchange_helper_multi.xml new file mode 100644 index 000000000..1edcc14a5 --- /dev/null +++ b/test_onchange_helper/security/test_onchange_helper_multi.xml @@ -0,0 +1,17 @@ + + + + + + + test.onchange.helper.multi access name + + + + + + + + + diff --git a/test_onchange_helper/security/test_onchange_helper_multi_line.xml b/test_onchange_helper/security/test_onchange_helper_multi_line.xml new file mode 100644 index 000000000..6a20c302f --- /dev/null +++ b/test_onchange_helper/security/test_onchange_helper_multi_line.xml @@ -0,0 +1,17 @@ + + + + + + + test.onchange.helper.multi.line access name + + + + + + + + + diff --git a/test_onchange_helper/static/description/icon.png b/test_onchange_helper/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/test_onchange_helper/tests/__init__.py b/test_onchange_helper/tests/__init__.py new file mode 100644 index 000000000..73370c825 --- /dev/null +++ b/test_onchange_helper/tests/__init__.py @@ -0,0 +1 @@ +from . import test_onchange_helper diff --git a/test_onchange_helper/tests/test_onchange_helper.py b/test_onchange_helper/tests/test_onchange_helper.py new file mode 100644 index 000000000..ee774e669 --- /dev/null +++ b/test_onchange_helper/tests/test_onchange_helper.py @@ -0,0 +1,378 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import mock +from contextlib import contextmanager +import odoo.tests.common as common + + +class TestOnchangeHelper(common.TransactionCase): + def setUp(self): + super(TestOnchangeHelper, self).setUp() + self.Category = self.env["test_onchange_helper.category"] + self.Message = self.env["test_onchange_helper.message"] + self.Discussion = self.env["test_onchange_helper.discussion"] + + @contextmanager + def assertNoOrmWrite(self, model): + with mock.patch.object( + model.__class__, "create" + ) as mocked_create, mock.patch.object( + model.__class__, "write" + ) as mocked_write: + yield + mocked_create.assert_not_called() + mocked_write.assert_not_called() + + def test_play_onhanges_no_recompute(self): + # play_onchanges must not trigger recomputes except if an onchange + # method access a computed field. + # changing 'discussion' should recompute 'name' + values = {"name": "Cat Name"} + self.env.invalidate_all() + with self.assertNoOrmWrite(self.Category): + result = self.Category.play_onchanges(values, ["name"]) + self.assertNotIn("computed_display_name", result) + + def test_play_onchanges_many2one_new_record(self): + root = self.Category.create({"name": "root"}) + + values = {"name": "test", "parent": root.id, "root_categ": False} + + self.env.invalidate_all() + with self.assertNoOrmWrite(self.Category): + result = self.Category.play_onchanges(values, "parent") + self.assertIn("root_categ", result) + self.assertEqual(result["root_categ"], root.id) + + values.update(result) + values["parent"] = False + + self.env.invalidate_all() + with self.assertNoOrmWrite(self.Category): + result = self.Category.play_onchanges(values, "parent") + # since the root_categ is already False into values the field is not + # changed by the onchange + self.assertNotIn("root_categ", result) + + def test_play_onchanges_many2one_existing_record(self): + root = self.Category.create({"name": "root"}) + + values = {"name": "test", "parent": root.id, "root_categ": False} + + self.env.invalidate_all() + with self.assertNoOrmWrite(self.Category): + result = self.Category.play_onchanges(values, "parent") + self.assertIn("root_categ", result) + self.assertEqual(result["root_categ"], root.id) + + # create child catefory + values.update(result) + child = self.Category.create(values) + self.assertEqual(root.id, child.root_categ.id) + + # since the parent is set to False and the root_categ + values = {"parent": False} + self.env.invalidate_all() + with self.assertNoOrmWrite(child): + result = child.play_onchanges(values, "parent") + + self.assertIn("root_categ", result) + self.assertEqual(result["root_categ"], False) + + def test_play_onchange_one2many_new_record(self): + """ test the effect of play_onchanges() on one2many fields on new + record""" + BODY = "What a beautiful day!" + USER = self.env.user + + # create an independent message + message = self.Message.create({"body": BODY}) + + # modify discussion name + values = { + "name": "Foo", + "categories": [], + "moderator": False, + "participants": [], + "messages": [ + (4, message.id), + ( + 0, + 0, + { + "name": "[%s] %s" % ("", USER.name), + "body": BODY, + "author": USER.id, + "important": False, + }, + ), + ], + } + self.env.invalidate_all() + with self.assertNoOrmWrite(self.Discussion): + result = self.Discussion.play_onchanges(values, "name") + self.assertIn("messages", result) + self.assertItemsEqual( + result["messages"], + [ + (5,), + ( + 1, + message.id, + { + "name": "[%s] %s" % ("Foo", USER.name), + "body": "not last dummy message", + "author": message.author.id, + "important": message.important, + }, + ), + ( + 0, + 0, + { + "name": "[%s] %s" % ("Foo", USER.name), + "body": "not last dummy message", + "author": USER.id, + "important": False, + }, + ), + ( + 0, + 0, + { + "name": "[%s] %s" % ("Foo", USER.name), + "body": "dummy message", + "author": USER.id, + "important": True, + }, + ), + ], + ) + + self.assertIn("important_messages", result) + self.assertItemsEqual( + result["important_messages"], + [ + (5,), + ( + 0, + 0, + { + "author": USER.id, + "body": "dummy message", + "important": True, + }, + ), + ], + ) + + def test_play_onchange_one2many_existing_record(self): + """ test the effect of play_onchanges() on one2many fields on existing + record""" + BODY = "What a beautiful day!" + USER = self.env.user + + # create an independent message + message = self.Message.create({"body": BODY}) + + # modify discussion name + values = { + "name": "Foo", + "categories": [], + "moderator": False, + "participants": [], + "messages": [ + (4, message.id), + ( + 0, + 0, + { + "name": "[%s] %s" % ("", USER.name), + "body": BODY, + "author": USER.id, + "important": False, + }, + ), + ], + } + discussion = self.Discussion.create(values) + + values = {"name": "New foo"} + with self.assertNoOrmWrite(discussion): + result = discussion.play_onchanges(values, "name") + self.assertIn("messages", result) + self.assertItemsEqual( + result["messages"], + [ + (5,), + ( + 1, + discussion.messages[0].id, + { + "name": "[%s] %s" % ("New foo", USER.name), + "body": "not last dummy message", + "author": message.author.id, + "important": message.important, + "discussion": discussion.id, + }, + ), + ( + 1, + discussion.messages[1].id, + { + "name": "[%s] %s" % ("New foo", USER.name), + "body": "not last dummy message", + "author": USER.id, + "important": False, + "discussion": discussion.id, + }, + ), + ( + 0, + 0, + { + "name": "[%s] %s" % ("New foo", USER.name), + "body": "dummy message", + "author": USER.id, + "important": True, + "discussion": discussion.id, + }, + ), + ], + ) + + self.assertIn("important_messages", result) + self.assertItemsEqual( + result["important_messages"], + [ + (5,), + ( + 0, + 0, + { + "author": USER.id, + "body": "dummy message", + "important": True, + "discussion": discussion.id, + }, + ), + ], + ) + + def test_onchange_specific(self): + """test that only the id is added if a new item is added to an + existing relation""" + discussion = self.env.ref("test_onchange_helper.discussion_demo_0") + demo = self.env.ref("base.user_demo") + + # first remove demo user from participants + discussion.participants -= demo + self.assertNotIn(demo, discussion.participants) + + # check that demo_user is added to participants when set as moderator + values = { + "name": discussion.name, + "moderator": demo.id, + "categories": [(4, cat.id) for cat in discussion.categories], + "messages": [(4, msg.id) for msg in discussion.messages], + "participants": [(4, usr.id) for usr in discussion.participants], + } + self.env.invalidate_all() + with self.assertNoOrmWrite(discussion): + result = discussion.play_onchanges(values, "moderator") + + self.assertIn("participants", result) + self.assertItemsEqual( + result["participants"], + [(5,)] + [(4, user.id) for user in discussion.participants + demo], + ) + + def test_onchange_one2many_value(self): + """ test that the values provided for a one2many field inside are used + by the play_onchanges """ + discussion = self.env.ref("test_onchange_helper.discussion_demo_0") + demo = self.env.ref("base.user_demo") + + self.assertEqual(len(discussion.messages), 3) + messages = [(4, msg.id) for msg in discussion.messages] + messages[0] = (1, messages[0][1], {"body": "test onchange"}) + values = { + "name": discussion.name, + "moderator": demo.id, + "categories": [(4, cat.id) for cat in discussion.categories], + "messages": messages, + "participants": [(4, usr.id) for usr in discussion.participants], + "message_concat": False, + } + with self.assertNoOrmWrite(discussion): + result = discussion.play_onchanges(values, "messages") + self.assertIn("message_concat", result) + self.assertEqual( + result["message_concat"], + "\n".join( + ["%s:%s" % (m.name, m.body) for m in discussion.messages] + ), + ) + + def test_onchange_one2many_line(self): + """ test that changes on a field used as first position into the + related path of a related field will trigger the onchange also on the + related field """ + partner = self.env.ref("base.res_partner_1") + multi = self.env["test_onchange_helper.multi"].create( + {"partner": partner.id} + ) + line = multi.lines.create({"multi": multi.id}) + + values = multi._convert_to_write( + {key: multi[key] for key in ("partner", "lines")} + ) + self.assertEqual( + values, {"partner": partner.id, "lines": [(6, 0, [line.id])]} + ) + + # modify 'partner' + # -> set 'partner' on all lines + # -> recompute 'name' (related on partner) + # -> set 'name' on all lines + partner = self.env.ref("base.res_partner_2") + values = { + "partner": partner.id, + "lines": [ + (6, 0, [line.id]), + (0, 0, {"name": False, "partner": False}), + ], + } + + self.env.invalidate_all() + with self.assertNoOrmWrite(multi): + result = multi.play_onchanges(values, "partner") + self.assertEqual( + result, + { + "name": partner.name, + "lines": [ + (5,), + ( + 1, + line.id, + { + "name": partner.name, + "partner": partner.id, + "multi": multi.id, + }, + ), + ( + 0, + 0, + { + "name": partner.name, + "partner": partner.id, + "multi": multi.id, + }, + ), + ], + }, + )