[FIX] survey_record_generation: link to already created record at creation
A NOT required "other_record" field was always set after the loop with a write(), even when the linked record was already created. This late write triggers the recomputation of stored computed fields depending on the relation, overriding the values answered by the user. E.g. with subscription_oca, sale.subscription.line price_unit depends on sale_subscription_id.partner_id/pricelist_id: a free amount typed in a numerical_box question (33) was replaced by the product price (20) when the line was linked to its subscription. Now the relation is given to create() when the linked record already exists, so that the user's values are protected from recomputation. The deferred write is kept for records created later. Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ Allow to create record of any model when sending the form :
|
|||||||
* Associate question with fields
|
* Associate question with fields
|
||||||
* For x2m fields : Associate values to questions
|
* For x2m fields : Associate values to questions
|
||||||
""",
|
""",
|
||||||
"version": "18.0.1.0.1",
|
"version": "18.0.1.0.2",
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"author": "Elabore",
|
"author": "Elabore",
|
||||||
"website": "https://www.elabore.coop",
|
"website": "https://www.elabore.coop",
|
||||||
|
|||||||
@@ -160,8 +160,9 @@ class SurveyUserInput(models.Model):
|
|||||||
elif field_value.value_origin == "question":
|
elif field_value.value_origin == "question":
|
||||||
value = self.get_value_from_user_answer(field_value, user_input)
|
value = self.get_value_from_user_answer(field_value, user_input)
|
||||||
elif field_value.value_origin == "other_record":
|
elif field_value.value_origin == "other_record":
|
||||||
# if the other_record value is a required field, get it or raise
|
# if the other record is already created, get it
|
||||||
value = self.get_required_value_from_other_record(
|
# (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
|
model, created_records, field_value
|
||||||
)
|
)
|
||||||
# otherwise, we update the record later (out of this for loop)
|
# otherwise, we update the record later (out of this for loop)
|
||||||
@@ -198,23 +199,17 @@ class SurveyUserInput(models.Model):
|
|||||||
duplicate = self.env[model].search(uniq_domain, limit=1)
|
duplicate = self.env[model].search(uniq_domain, limit=1)
|
||||||
return duplicate
|
return duplicate
|
||||||
|
|
||||||
def get_required_value_from_other_record(
|
def get_value_from_other_record(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
created_records: dict[Any, Any],
|
created_records: dict[Any, Any],
|
||||||
field_value: "SurveyRecordCreationFieldValues",
|
field_value: "SurveyRecordCreationFieldValues",
|
||||||
) -> Any:
|
) -> Any:
|
||||||
model_class = self.env[model]
|
linked_record = created_records.get(field_value.other_created_record_id.id)
|
||||||
if model_class._fields[field_value.field_id.name].required:
|
if linked_record:
|
||||||
# check if the other record is already created,
|
# The other record is already created: link it right away.
|
||||||
# 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)
|
return self.get_value_for_relational_field(field_value, linked_record)
|
||||||
else:
|
if self.env[model]._fields[field_value.field_id.name].required:
|
||||||
raise UserError(
|
raise UserError(
|
||||||
_(
|
_(
|
||||||
"The field %(field)s is mandatory for model %(model)s. "
|
"The field %(field)s is mandatory for model %(model)s. "
|
||||||
@@ -227,6 +222,7 @@ class SurveyUserInput(models.Model):
|
|||||||
"record": field_value.other_created_record_id.name,
|
"record": field_value.other_created_record_id.name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
def get_value_from_user_answer(
|
def get_value_from_user_answer(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
from . import test_survey_record_creation
|
from . import test_survey_record_creation
|
||||||
from . import test_end_migration_18_0_1_0_1
|
from . import test_end_migration_18_0_1_0_1
|
||||||
|
from . import test_computed_fields
|
||||||
|
|||||||
33
survey_record_generation/tests/fake_models.py
Normal file
33
survey_record_generation/tests/fake_models.py
Normal file
@@ -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
|
||||||
112
survey_record_generation/tests/test_computed_fields.py
Normal file
112
survey_record_generation/tests/test_computed_fields.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user