diff --git a/survey_record_generation/__manifest__.py b/survey_record_generation/__manifest__.py index 1fa0e2a..2cdcfcb 100644 --- a/survey_record_generation/__manifest__.py +++ b/survey_record_generation/__manifest__.py @@ -11,7 +11,7 @@ Allow to create record of any model when sending the form : * Associate question with fields * For x2m fields : Associate values to questions """, - "version": "18.0.1.0.1", + "version": "18.0.1.0.2", "license": "AGPL-3", "author": "Elabore", "website": "https://www.elabore.coop", diff --git a/survey_record_generation/models/survey_user_input.py b/survey_record_generation/models/survey_user_input.py index 9dff3f4..8cccae0 100644 --- a/survey_record_generation/models/survey_user_input.py +++ b/survey_record_generation/models/survey_user_input.py @@ -160,8 +160,9 @@ class SurveyUserInput(models.Model): elif field_value.value_origin == "question": value = self.get_value_from_user_answer(field_value, user_input) elif field_value.value_origin == "other_record": - # if the other_record value is a required field, get it or raise - value = self.get_required_value_from_other_record( + # if the other record is already created, get it + # (or raise if it is not created yet and the field is required) + value = self.get_value_from_other_record( model, created_records, field_value ) # otherwise, we update the record later (out of this for loop) @@ -198,35 +199,30 @@ class SurveyUserInput(models.Model): duplicate = self.env[model].search(uniq_domain, limit=1) return duplicate - def get_required_value_from_other_record( + def get_value_from_other_record( self, model: str, created_records: dict[Any, Any], field_value: "SurveyRecordCreationFieldValues", ) -> Any: - model_class = self.env[model] - if model_class._fields[field_value.field_id.name].required: - # check if the other record is already created, - # if yes add it to vals, else raise - if ( - len(created_records) > 0 - and created_records[field_value.other_created_record_id.id] - ): - linked_record = created_records[field_value.other_created_record_id.id] - return self.get_value_for_relational_field(field_value, linked_record) - else: - raise UserError( - _( - "The field %(field)s is mandatory for model %(model)s. " - "In Record Creation tab, drag %(record)s " - "on top of the model %(model)s." - ) - % { - "field": field_value.field_id.display_name, - "model": model, - "record": field_value.other_created_record_id.name, - } + linked_record = created_records.get(field_value.other_created_record_id.id) + if linked_record: + # The other record is already created: link it right away. + return self.get_value_for_relational_field(field_value, linked_record) + if self.env[model]._fields[field_value.field_id.name].required: + raise UserError( + _( + "The field %(field)s is mandatory for model %(model)s. " + "In Record Creation tab, drag %(record)s " + "on top of the model %(model)s." ) + % { + "field": field_value.field_id.display_name, + "model": model, + "record": field_value.other_created_record_id.name, + } + ) + return None def get_value_from_user_answer( self, diff --git a/survey_record_generation/tests/__init__.py b/survey_record_generation/tests/__init__.py index 4d95f2e..68ba358 100644 --- a/survey_record_generation/tests/__init__.py +++ b/survey_record_generation/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_survey_record_creation from . import test_end_migration_18_0_1_0_1 +from . import test_computed_fields diff --git a/survey_record_generation/tests/fake_models.py b/survey_record_generation/tests/fake_models.py new file mode 100644 index 0000000..df6cb99 --- /dev/null +++ b/survey_record_generation/tests/fake_models.py @@ -0,0 +1,33 @@ +from odoo import api, fields, models + + +class SurveyRecordGenerationFakeParent(models.Model): + _name = "survey.record.generation.fake.parent" + _description = "Fake parent record (tests only)" + + name = fields.Char() + price = fields.Float() + + +class SurveyRecordGenerationFakeLine(models.Model): + """Line whose price is computed from its parent, like the price of a + subscription line (subscription_oca). + + Pattern reproduced: the survey creates a parent, then a line linked to + it, with a price answered by the user. The user's price must be kept, + the parent's price only applies when the user gave none. + """ + + _name = "survey.record.generation.fake.line" + _description = "Fake line record (tests only)" + + parent_id = fields.Many2one("survey.record.generation.fake.parent") + price_unit = fields.Float( + compute="_compute_price_unit", store=True, readonly=False + ) + + @api.depends("parent_id.price") + def _compute_price_unit(self): + for line in self: + if line.parent_id: + line.price_unit = line.parent_id.price diff --git a/survey_record_generation/tests/test_computed_fields.py b/survey_record_generation/tests/test_computed_fields.py new file mode 100644 index 0000000..79f14ce --- /dev/null +++ b/survey_record_generation/tests/test_computed_fields.py @@ -0,0 +1,112 @@ +from odoo_test_helper import FakeModelLoader + +from odoo.addons.survey.tests.common import SurveyCase + + +class TestComputedFields(SurveyCase): + """User's answers must not be overridden by computed fields depending on + a link to another generated record. + + Use case: a membership form creating a subscription and a subscription + line whose price is typed by the participant (free amount), while the + line price is computed from the subscription. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .fake_models import ( + SurveyRecordGenerationFakeLine, + SurveyRecordGenerationFakeParent, + ) + + cls.loader.update_registry( + (SurveyRecordGenerationFakeParent, SurveyRecordGenerationFakeLine) + ) + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + super().tearDownClass() + + def setUp(self): + super().setUp() + self.survey = self.env["survey.survey"].create({"title": "Join us"}) + self.question_amount = self._add_question( + page=None, + name="Free amount", + qtype="numerical_box", + survey_id=self.survey.id, + sequence=1, + ) + + # Parent, with a default price of 20 + self.parent_creation = self._add_record_creation( + "Parent", "survey.record.generation.fake.parent", 0 + ) + self._add_field_value( + self.parent_creation, "name", value_origin="fixed", + fixed_value_char="Membership", + ) + self._add_field_value( + self.parent_creation, "price", value_origin="fixed", + fixed_value_float=20.0, + ) + + # Line, linked to the parent, with the price answered by the user + self.line_creation = self._add_record_creation( + "Line", "survey.record.generation.fake.line", 1 + ) + self._add_field_value( + self.line_creation, "parent_id", value_origin="other_record", + other_created_record_id=self.parent_creation.id, + ) + self._add_field_value( + self.line_creation, "price_unit", value_origin="question", + question_id=self.question_amount.id, + ) + + def _add_record_creation(self, name, model, sequence): + return self.env["survey.record.creation"].create( + { + "name": name, + "survey_id": self.survey.id, + "model_id": self.env["ir.model"]._get(model).id, + "sequence": sequence, + } + ) + + def _add_field_value(self, record_creation, field_name, **vals): + field = self.env["ir.model.fields"]._get( + record_creation.model_id.model, field_name + ) + return self.env["survey.record.creation.field.values"].create( + { + "survey_record_creation_id": record_creation.id, + "survey_id": self.survey.id, + "model_id": record_creation.model_id.id, + "field_id": field.id, + **vals, + } + ) + + def _submit(self, amount=None): + answer = self._add_answer( + survey=self.survey, partner=False, email="jean@test.fr" + ) + if amount is not None: + self._add_answer_line( + question=self.question_amount, answer=answer, answer_value=amount + ) + answer._mark_done() + return self.env["survey.record.generation.fake.line"].search( + [("parent_id.name", "=", "Membership")] + ) + + def test_answer_is_not_overridden_by_computed_field(self): + line = self._submit(amount=33) + + self.assertEqual(len(line), 1) + self.assertEqual(line.price_unit, 33.0)