From 4d03cc6cb1a7fef35c2bade96f67fe3d2a29f1b9 Mon Sep 17 00:00:00 2001 From: Quentin Mondot Date: Tue, 22 Sep 2026 15:52:15 +0200 Subject: [PATCH] [FIX] survey_record_generation: partner_id/email on survey.user_input could be wrong New version : 16.0.1.0.3 _mark_done() only set partner_id/email when it created a *new* res.partner. If the answer already had a partner_id/email (e.g. inherited from the Odoo user logged in when the /survey/start link was opened), that value was kept even though the record creation matched or created a different, correct contact from the participant's own answers. Now we always sync partner_id/email to the res.partner actually matched or created from the participant's answers. Add a migration to relink partner_id/email on existing done submissions. Co-Authored-By: Claude Sonnet 5 --- survey_record_generation/__manifest__.py | 2 +- .../migrations/16.0.1.0.3/end-migration.py | 153 ++++++++++++++++++ .../models/survey_user_input.py | 14 +- 3 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 survey_record_generation/migrations/16.0.1.0.3/end-migration.py diff --git a/survey_record_generation/__manifest__.py b/survey_record_generation/__manifest__.py index 04c6cd0..32bee37 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": "16.0.1.0.2", + "version": "16.0.1.0.3", "license": "AGPL-3", "author": "Elabore", "website": "https://www.elabore.coop", diff --git a/survey_record_generation/migrations/16.0.1.0.3/end-migration.py b/survey_record_generation/migrations/16.0.1.0.3/end-migration.py new file mode 100644 index 0000000..efe02ec --- /dev/null +++ b/survey_record_generation/migrations/16.0.1.0.3/end-migration.py @@ -0,0 +1,153 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +"""Relink survey.user_input.partner_id/email to the contact that actually +matches each participant's own answers. + +Before this version, survey.user_input.partner_id/email were only updated +when _mark_done() created a *new* res.partner. If the answer already had a +partner_id/email (e.g. inherited from the Odoo user who was logged in when +the /survey/start link was opened, or from some other unidentified cause), +_mark_done() silently kept that value even when the survey_record_creation +config found or created a different, correct contact from the participant's +own answers. This backfill re-runs that resolution for every already-done +answer and fixes partner_id/email accordingly. + +It never re-triggers the other survey.user_input._mark_done() overrides +(crm lead / event registration / notification modules, ...): it only calls +the res.partner-matching helpers directly, so it can't create duplicate +leads, registrations, etc. for historical submissions. + +A res.partner is expected to already exist for every done submission (it +was necessarily found or created the first time _mark_done() ran), so this +backfill never creates one: if none is found, the record is left untouched +and logged for manual review instead. +""" +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + env = api.Environment(cr, SUPERUSER_ID, {}) + user_input_model = env["survey.user_input"] + + surveys_with_partner_creation = env["survey.record.creation"].search( + [("model_id.model", "=", "res.partner")] + ).survey_id + + user_inputs = user_input_model.search( + [ + ("survey_id", "in", surveys_with_partner_creation.ids), + ("state", "=", "done"), + ] + ) + + _logger.info( + "survey_record_generation: relinking partner_id/email on %d done " + "survey.user_input records", + len(user_inputs), + ) + + fixed_count = 0 + not_found_count = 0 + + for user_input in user_inputs: + record_creations = user_input.survey_id.survey_record_creation_ids.filtered( + lambda rc: rc.model_id.model == "res.partner" + ).sorted("sequence") + + record_creation = record_creations[:1] + if not record_creation: + continue + + # 1) Prefer the res.partner this very submission created, if any + # (the first one, by id, in the rare case there is more than one): + # it's the exact record _mark_done() produced for this answer, no + # guessing involved. This is also the only option for surveys whose + # record creation has neither update_existing_records nor any + # unicity_check field configured, since find_existing_record()/ + # find_duplicate...() can then never find anything (nothing to + # search on). + record = False + for generated in user_input.generated_record_ids.sorted("id"): + if ( + generated.survey_record_creation_id == record_creation + and generated.created_record_id + and generated.created_record_id._name == "res.partner" + and generated.created_record_id.exists() + ): + # The referenced partner may have since been deleted (e.g. + # merged into another contact): in that case it's not usable + # and we fall through to the search-based lookup below. + record = generated.created_record_id + break + + # 2) Otherwise, this submission matched an already-existing partner + # instead of creating one (find_existing_record()/find_duplicate...() + # branch of _mark_done()): re-derive it the same way. + if not record: + # Only compute the fields find_existing_record()/find_duplicate...() + # actually read (the search field, and any unicity_check field), + # not every field of the record creation: other fields (e.g. a + # "record" reference to a model defined in a module that depends + # on this one) may not be loadable yet at this point of the + # upgrade, and are useless here anyway since this backfill never + # writes to res.partner. + needed_field_names = set() + if ( + record_creation.update_existing_records + and record_creation.field_to_retrieve_existing_records + ): + needed_field_names.add( + record_creation.field_to_retrieve_existing_records.name + ) + unicity_field_values = record_creation.field_values_ids.filtered( + lambda field_value: field_value.unicity_check + ) + needed_field_names.update(unicity_field_values.mapped("field_id.name")) + + vals = {} + for field_value in record_creation.field_values_ids: + if field_value.field_id.name not in needed_field_names: + continue + value, __ = user_input_model.get_value_based_on_value_origin( + field_value=field_value, + user_input=user_input, + created_records={}, + model="res.partner", + other_record_fields_to_update=[], + ) + vals[field_value.field_id.name] = value + + existing_record = user_input_model.find_existing_record( + record_creation, vals + ) + duplicate = ( + user_input_model.find_duplicate_if_there_are_fields_with_unicity_check( + "res.partner", record_creation, vals + ) + ) + record = duplicate or existing_record + + if not record: + _logger.warning( + "survey_record_generation: no existing res.partner found for " + "survey.user_input %s while backfilling partner_id/email, " + "leaving it untouched", + user_input.id, + ) + not_found_count += 1 + continue + + if user_input.partner_id != record or user_input.email != record.email: + user_input.partner_id = record.id + user_input.email = record.email + fixed_count += 1 + + _logger.info( + "survey_record_generation: fixed %d survey.user_input records " + "(%d without a matching res.partner)", + fixed_count, + not_found_count, + ) diff --git a/survey_record_generation/models/survey_user_input.py b/survey_record_generation/models/survey_user_input.py index f8e3930..9dff3f4 100644 --- a/survey_record_generation/models/survey_user_input.py +++ b/survey_record_generation/models/survey_user_input.py @@ -39,6 +39,7 @@ class SurveyUserInput(models.Model): for user_input in self: created_records = {} other_record_fields_to_update: list[SurveyRecordCreationFieldValues] = [] + partner_linked_in_this_run = False record_creation: SurveyRecordCreation for ( @@ -85,8 +86,6 @@ class SurveyUserInput(models.Model): try: with self.env.cr.savepoint(): record = self.env[model].create(vals) - if model == "res.partner" and not self.partner_id: - self.partner_id = record.id except Exception: # This a broad exception because it could be IntegrityError, # EmptyNamesError in case partner_firstname is installed etc... @@ -103,6 +102,17 @@ class SurveyUserInput(models.Model): } ) + if model == "res.partner" and not partner_linked_in_this_run: + # Always reflect the partner actually matched/created from + # this participant's own answers, even if partner_id/email + # were already set on the answer (e.g. inherited from the + # logged-in user when the survey link was opened). Only the + # first res.partner record creation of this run wins, in + # case several are configured on the same survey. + user_input.partner_id = record.id + user_input.email = record.email + partner_linked_in_this_run = True + created_records[record_creation.id] = record # update linked record