From ecbeb913d294c07cfbd42c2041311da753c1fb21 Mon Sep 17 00:00:00 2001 From: Quentin Mondot Date: Wed, 25 Mar 2026 18:04:37 +0100 Subject: [PATCH 1/4] [IMP] survey_record_generation: added _rec_name for some models without name fields and corrected use of self in _compute_warning_message (cherry picked from commit 4b66618686739d1f6414692f7db58dbb683ab164) --- survey_record_generation/models/survey_generated_record.py | 1 + survey_record_generation/models/survey_record_creation.py | 4 ++-- .../models/survey_record_creation_field_values.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/survey_record_generation/models/survey_generated_record.py b/survey_record_generation/models/survey_generated_record.py index d199657..576a982 100644 --- a/survey_record_generation/models/survey_generated_record.py +++ b/survey_record_generation/models/survey_generated_record.py @@ -4,6 +4,7 @@ from odoo import models, fields, api class SurveyGeneratedRecord(models.Model): _name = "survey.generated.record" + _rec_name = "survey_record_creation_name" survey_record_creation_name = fields.Char('Name', readonly=True) survey_record_creation_id = fields.Many2one('survey.record.creation', 'Survey record creation', readonly=True) diff --git a/survey_record_generation/models/survey_record_creation.py b/survey_record_generation/models/survey_record_creation.py index c72aeb0..fb2eeb1 100644 --- a/survey_record_generation/models/survey_record_creation.py +++ b/survey_record_generation/models/survey_record_creation.py @@ -54,8 +54,8 @@ class SurveyRecordCreation(models.Model): for record_creation in self: # check if all mandatory fields set if record_creation.model_id: - required_field_ids = self.model_id.field_id.filtered(lambda f:f.required and "property_" not in f.name) - set_field_ids = self.field_values_ids.field_id + required_field_ids = record_creation.model_id.field_id.filtered(lambda f:f.required and "property_" not in f.name) + set_field_ids = record_creation.field_values_ids.field_id missing_fields = required_field_ids - set_field_ids if missing_fields: diff --git a/survey_record_generation/models/survey_record_creation_field_values.py b/survey_record_generation/models/survey_record_creation_field_values.py index 2bc6ec9..81bedd1 100644 --- a/survey_record_generation/models/survey_record_creation_field_values.py +++ b/survey_record_generation/models/survey_record_creation_field_values.py @@ -26,6 +26,7 @@ class SurveyRecordCreationFieldValues(models.Model): """Configure default values of records created on survey submission """ _name = 'survey.record.creation.field.values' + _rec_name = 'displayed_value' survey_record_creation_id = fields.Many2one('survey.record.creation') survey_id = fields.Many2one('survey.survey', related="survey_record_creation_id.survey_id") @@ -175,6 +176,7 @@ class SurveyRecordCreationFieldValuesX2m(models.Model): """O2m an M2m default values """ _name = 'survey.record.creation.field.values.x2m' + _rec_name = 'value_reference' survey_record_creation_field_values_id = fields.Many2one('survey.record.creation.field.values') value_reference = fields.Reference(string='Record', selection='_selection_target_model') -- 2.49.1 From 843e747a5ffc8ac57bc3ad895c566a50d23338c5 Mon Sep 17 00:00:00 2001 From: Quentin Mondot Date: Thu, 26 Mar 2026 11:16:19 +0100 Subject: [PATCH 2/4] [IMP] survey_record_generation: add default filter on survey id and active user_input for generated records (cherry picked from commit 0d1866ace37304d21828845b636704899c77aea1) --- survey_record_generation/models/survey_user_input.py | 2 +- .../views/survey_generated_record_views.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/survey_record_generation/models/survey_user_input.py b/survey_record_generation/models/survey_user_input.py index 15dc299..8e3bf7d 100644 --- a/survey_record_generation/models/survey_user_input.py +++ b/survey_record_generation/models/survey_user_input.py @@ -31,7 +31,7 @@ class SurveyUserInput(models.Model): action = self.env["ir.actions.act_window"]._for_xml_id( "survey_record_generation.survey_generated_record_action" ) - + action['domain'] = [('user_input_id.survey_id', '=', self.survey_id.id)] return action def _mark_done(self): diff --git a/survey_record_generation/views/survey_generated_record_views.xml b/survey_record_generation/views/survey_generated_record_views.xml index 4196f8d..171d0a3 100644 --- a/survey_record_generation/views/survey_generated_record_views.xml +++ b/survey_record_generation/views/survey_generated_record_views.xml @@ -19,8 +19,8 @@ + domain="[('user_input_id.id', '=', context.get('active_id'))]" + /> -- 2.49.1 From 5c0962c1fd4199ac36150b9710b8ce7ce2a1a982 Mon Sep 17 00:00:00 2001 From: Quentin Mondot Date: Thu, 9 Apr 2026 09:42:36 +0200 Subject: [PATCH 3/4] [IMP] survey_record_generation : new option update_existing_fields (cherry picked from commit aff1a6caae4105412c4f2ae029bd00f49aff8c66) --- survey_record_generation/README.rst | 5 + survey_record_generation/__manifest__.py | 2 +- survey_record_generation/i18n/fr.po | 66 ++++--------- .../models/survey_record_creation.py | 5 + .../models/survey_user_input.py | 15 +-- .../tests/test_survey_record_creation.py | 95 ++++++++++++++++++- .../views/survey_survey_views.xml | 19 ++-- 7 files changed, 144 insertions(+), 63 deletions(-) diff --git a/survey_record_generation/README.rst b/survey_record_generation/README.rst index e03d218..acfd1ef 100644 --- a/survey_record_generation/README.rst +++ b/survey_record_generation/README.rst @@ -72,7 +72,12 @@ Record generation configuration For m2o or m2m links, question should be configured before. See Question answers configuration section below. * **other created record**: If value come from other created record (m2o case only) +#. Several options exist for the *record creation* : + #. You can check "Ignore creation if a mandatory field is missing" to prevent the form to crash if some record creations fail. + #. You can check "Update existing records" to update existing records instead of creating it. For this, you need to + precise the "Field to retrieve existing records". Only the first matched record will be updated. By default + the existing values are not replaced, except if you check the option "Update existing values". Question answers configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/survey_record_generation/__manifest__.py b/survey_record_generation/__manifest__.py index 05a64e0..04c6cd0 100644 --- a/survey_record_generation/__manifest__.py +++ b/survey_record_generation/__manifest__.py @@ -2,7 +2,7 @@ { "name": "Survey record generation", - 'summary': 'Allow to create record of any model when sending the form', + 'summary': 'Allow to create or update record of any model when sending the form', 'description': """ Allow to create record of any model when sending the form : ---------------------------------------------------- diff --git a/survey_record_generation/i18n/fr.po b/survey_record_generation/i18n/fr.po index 574b4e5..85473d0 100644 --- a/survey_record_generation/i18n/fr.po +++ b/survey_record_generation/i18n/fr.po @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0\n" +"Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-11-13 16:41+0000\n" -"PO-Revision-Date: 2025-11-13 16:41+0000\n" +"POT-Creation-Date: 2026-04-09 07:55+0000\n" +"PO-Revision-Date: 2026-04-09 07:55+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -44,8 +44,6 @@ msgstr "Question autorisée" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#, python-format msgid "Answer to question: %s" msgstr "Réponse à la question : %s" @@ -130,8 +128,6 @@ msgstr "Type de champ" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#, python-format msgid "Field type is : %s" msgstr "Le type de champ est : %s" @@ -198,22 +194,14 @@ msgid "" "error is ignored." msgstr "" "Si un champs requis est manquant lors de la création de l'enregistrement, " -"une erreur est levée lors de la soumission du formulaire. " -"En activant cette option, l'erreur sera ignorée." +"une erreur est levée lors de la soumission du formulaire. En activant cette " +"option, l'erreur sera ignorée." #. module: survey_record_generation #: model:ir.model.fields,field_description:survey_record_generation.field_survey_record_creation__ignore_if_mandatory_field_is_missing msgid "Ignore creation if a mandatory field is missing" msgstr "Ignorer la création si un champs requis est manquant" -#. module: survey_record_generation -#: model:ir.model.fields,field_description:survey_record_generation.field_survey_generated_record____last_update -#: model:ir.model.fields,field_description:survey_record_generation.field_survey_record_creation____last_update -#: model:ir.model.fields,field_description:survey_record_generation.field_survey_record_creation_field_values____last_update -#: model:ir.model.fields,field_description:survey_record_generation.field_survey_record_creation_field_values_x2m____last_update -msgid "Last Modified on" -msgstr "Dernière modification le" - #. module: survey_record_generation #: model:ir.model.fields,field_description:survey_record_generation.field_survey_generated_record__write_uid #: model:ir.model.fields,field_description:survey_record_generation.field_survey_record_creation__write_uid @@ -269,8 +257,6 @@ msgstr "Pas d'enregistrements générés trouvés" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_question.py:0 -#: code:addons/survey_record_generation/models/survey_question.py:0 -#, python-format msgid "No record found in %s" msgstr "Pas d'enregistrements trouvés parmis %s" @@ -292,7 +278,7 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:survey_record_generation.survey_survey_view_form msgid "" "Only the first matched record will be updated.\n" -" Also to be noticed, the unicity check feature has priority over updating the existing record." +" Also to be noticed, the unicity check feature has priority over updating the existing record." msgstr "" "Attention, seul le premier enregistrement trouvé sera mis à jour. Aussi, si " "vous avez des champs avec une contrainte d'unicité, cette contrainte aura la" @@ -301,8 +287,6 @@ msgstr "" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#, python-format msgid "Other created record: " msgstr "Autre enregistrement créé : " @@ -353,8 +337,6 @@ msgstr "Modèle relatif" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_record_creation.py:0 -#: code:addons/survey_record_generation/models/survey_record_creation.py:0 -#, python-format msgid "Some required fields are not set : %s" msgstr "Certains champs requis ne sont pas remplis : %s" @@ -388,17 +370,25 @@ msgstr "Sondage Création d'enregistrement Valeur des champs" #. module: survey_record_generation #: model:ir.model,name:survey_record_generation.model_survey_user_input msgid "Survey User Input" -msgstr "Saisie utilisateur du sondage" +msgstr "Entrée utilisateur du sondage" #. module: survey_record_generation #: model:ir.model.fields,field_description:survey_record_generation.field_survey_generated_record__survey_record_creation_id msgid "Survey record creation" msgstr "Génération d'enregistrement depuis la participation" +#. module: survey_record_generation +#: model:ir.model.fields,help:survey_record_generation.field_survey_record_creation__update_existing_values +msgid "" +"The default behavior is to not update the existing fields. If checked, the " +"existing fields will be updated. " +msgstr "" +"Le comportement par défaut est de ne pas mettre à jour les valeurs existantes. Si cette option est cochée, " +"les valeurs existantes seront écrasées." + #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_user_input.py:0 -#, python-format msgid "" "The field %(field)s is mandatory for model %(model)s. In Record Creation " "tab, drag %(record)s on top of the model %(model)s." @@ -407,20 +397,16 @@ msgstr "" "Création d'un enregistrement, placez la ligne %(record)s au dessus de la " "ligne du modèle %(model)s." -#. module: survey_record_generation -#. odoo-python -#: code:addons/survey_record_generation/models/survey_user_input.py:0 -#, python-format -msgid "" -"The field %s is mandatory. In Record Creation tab, drag %s at the top of the" -" table" -msgstr "" - #. module: survey_record_generation #: model:ir.model.fields,field_description:survey_record_generation.field_survey_record_creation_field_values__unicity_check msgid "Unicity constraint" msgstr "Contrainte d'unicité" +#. module: survey_record_generation +#: model:ir.model.fields,field_description:survey_record_generation.field_survey_record_creation__update_existing_values +msgid "Update existing values" +msgstr "Écraser les valeurs existantes" + #. module: survey_record_generation #: model:ir.model.fields,field_description:survey_record_generation.field_survey_record_creation__update_existing_records msgid "Update existing records" @@ -455,17 +441,12 @@ msgstr "Message d'erreur" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#, python-format msgid "You should append at least one record in %s" msgstr "Vous devez au moins ajouter un enregistrement dans %s" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_user_input.py:0 -#, python-format msgid "" "[Survey record generation] The answer values type '%(type)s' is not " "supported (for question %(question)s). Use 'record' or 'value' instead." @@ -477,7 +458,6 @@ msgstr "" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_user_input.py:0 -#, python-format msgid "" "[Survey record generation] The boolean value %s(value)s is not supported " "(for question %(question)s)." @@ -488,7 +468,6 @@ msgstr "" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_user_input.py:0 -#, python-format msgid "" "[Survey record generation] The question type %(type)s is not recognized (for" " question %(question)s)." @@ -499,7 +478,6 @@ msgstr "" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_user_input.py:0 -#, python-format msgid "" "[Survey record generation] The question type %(type)s is not supported yet." msgstr "" @@ -509,8 +487,6 @@ msgstr "" #. module: survey_record_generation #. odoo-python #: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#: code:addons/survey_record_generation/models/survey_record_creation_field_values.py:0 -#, python-format msgid "possible values are %s" msgstr "les valeurs possibles sont %s" diff --git a/survey_record_generation/models/survey_record_creation.py b/survey_record_generation/models/survey_record_creation.py index fb2eeb1..c4f7118 100644 --- a/survey_record_generation/models/survey_record_creation.py +++ b/survey_record_generation/models/survey_record_creation.py @@ -27,6 +27,11 @@ class SurveyRecordCreation(models.Model): help="Choose the field you want to use to retrieve the existing record. " "WARNING: We update only the first record found.", ) + update_existing_values = fields.Boolean( + string="Update existing values", + help="The default behavior is to not update the existing fields. " + "If checked, the existing fields will be updated. ", + ) allowed_field_ids = fields.Many2many( "ir.model.fields", compute="_compute_allowed_field_ids", diff --git a/survey_record_generation/models/survey_user_input.py b/survey_record_generation/models/survey_user_input.py index 8e3bf7d..a490741 100644 --- a/survey_record_generation/models/survey_user_input.py +++ b/survey_record_generation/models/survey_user_input.py @@ -71,12 +71,15 @@ class SurveyUserInput(models.Model): if duplicate: record = duplicate elif existing_record: - vals_with_keys_not_in_record = { - k: v - for k, v in vals.items() - if not getattr(existing_record, k, False) - } - existing_record.write(vals_with_keys_not_in_record) + if record_creation.update_existing_values: + existing_record.write(vals) + else: + vals_with_keys_not_in_record = { + k: v + for k, v in vals.items() + if not getattr(existing_record, k, False) + } + existing_record.write(vals_with_keys_not_in_record) record = existing_record else: try: diff --git a/survey_record_generation/tests/test_survey_record_creation.py b/survey_record_generation/tests/test_survey_record_creation.py index 5b84ce2..37a4150 100644 --- a/survey_record_generation/tests/test_survey_record_creation.py +++ b/survey_record_generation/tests/test_survey_record_creation.py @@ -3,6 +3,7 @@ from datetime import date from psycopg2 import IntegrityError from odoo.addons.survey.tests.common import SurveyCase +from odoo.tools import mute_logger class TestSurveyRecordCreation(SurveyCase): @@ -745,9 +746,91 @@ class TestSurveyRecordCreation(SurveyCase): self.answer._mark_done() partner = self.env["res.partner"].search([("name", "=", "Jean")]) - self.assertTrue(len(partner) == 1) - self.assertTrue(partner.email == "jean@test.fr") - self.assertTrue(partner.function == "happiness office manager") + self.assertEqual(len(partner), 1) + self.assertEqual(partner.email, "jean@test.fr") + self.assertEqual(partner.function, "happiness office manager") + + def test_update_all_fields_when_updating_records(self): + # A contact with name 'Jean' and email 'jean@test.fr' already exists. + # We'll update the fields 'function' AND 'email' of this partner + # because the option 'update_existing_values' is True + self.env["res.partner"].create( + { + "name": "Jean", + "email": "jean@test.fr", + } + ) + self.question_email = self._add_question( + page=None, + name="Email", + qtype="char_box", + survey_id=self.survey.id, + sequence=1, + ) + self.question_function = self._add_question( + page=None, + name="Function", + qtype="char_box", + survey_id=self.survey.id, + sequence=1, + ) + + self.survey_record_creation.write( + { + "update_existing_records": True, + "field_to_retrieve_existing_records": self.name_field.id, + "update_existing_values": True, + } + ) + email_field = self.env["ir.model.fields"].search( + [("model", "=", "res.partner"), ("name", "=", "email")] + ) + self.env["survey.record.creation.field.values"].create( + { + "survey_record_creation_id": self.survey_record_creation.id, + "survey_id": self.survey.id, + "model_id": self.res_partner_model.id, + "field_id": email_field.id, + "value_origin": "question", + "question_id": self.question_email.id, + } + ) + function_field = self.env["ir.model.fields"].search( + [("model", "=", "res.partner"), ("name", "=", "function")] + ) + self.env["survey.record.creation.field.values"].create( + { + "survey_record_creation_id": self.survey_record_creation.id, + "survey_id": self.survey.id, + "model_id": self.res_partner_model.id, + "field_id": function_field.id, + "value_origin": "question", + "question_id": self.question_function.id, + } + ) + + self.answer = self._add_answer( + survey=self.survey, partner=False, email="jean@test.fr" + ) + self._add_answer_line( + question=self.question_name, answer=self.answer, answer_value="Jean" + ) + self._add_answer_line( + question=self.question_email, + answer=self.answer, + answer_value="IAmTheNewEmailReplacingTheOldOne@test.fr", + ) + self._add_answer_line( + question=self.question_function, + answer=self.answer, + answer_value="happiness office manager", + ) + self.answer._mark_done() + + partner = self.env["res.partner"].search([("name", "=", "Jean")]) + self.assertEqual(len(partner), 1) + self.assertEqual(partner.email, "IAmTheNewEmailReplacingTheOldOne@test.fr") + self.assertEqual(partner.function, "happiness office manager") def test_unicity_check_has_priority_over_update(self): # In this test, we verify that if a field is set up with unicity_check @@ -810,7 +893,8 @@ class TestSurveyRecordCreation(SurveyCase): with self.assertRaises(IntegrityError): # TODO : propose a better user experience than IntegrityError when # a mandatory field is missing - self.answer._mark_done() + with mute_logger("odoo.sql_db"): + self.answer._mark_done() def test_ignore_if_mandatory_field_is_missing(self): # In this test, we check the behavior of ignore_if_mandatory_field_is_missing @@ -823,7 +907,8 @@ class TestSurveyRecordCreation(SurveyCase): survey=self.survey, partner=False, email="jean@test.fr" ) - self.answer._mark_done() + with mute_logger("odoo.sql_db"): + self.answer._mark_done() # No partner has been created, and no IntegrityError has been raised partner = self.env["res.partner"].search([("name", "=", "Jean")]) diff --git a/survey_record_generation/views/survey_survey_views.xml b/survey_record_generation/views/survey_survey_views.xml index 8507d76..e69a9e6 100644 --- a/survey_record_generation/views/survey_survey_views.xml +++ b/survey_record_generation/views/survey_survey_views.xml @@ -16,12 +16,19 @@
- - - - - - + + + + + + + + + + + + +
-- 2.49.1 From 1cece140849a585e756d1235f124bcc6204b0539 Mon Sep 17 00:00:00 2001 From: Quentin Mondot Date: Wed, 10 Jun 2026 17:23:31 +0200 Subject: [PATCH 4/4] [IMP] survey_extra_fields : handle file question on page navigation (cherry picked from commit d0afa2310d78011ef90873618ca20fe58d888940) --- survey_extra_fields/i18n/fr.po | 5 + .../models/survey_user_input.py | 15 ++- .../static/src/js/survey_form.js | 99 +++++++++++++++++-- survey_extra_fields/tests/test_survey_file.py | 23 ++++- .../views/survey_templates.xml | 21 +++- survey_record_generation/i18n/fr.po | 2 +- 6 files changed, 149 insertions(+), 16 deletions(-) diff --git a/survey_extra_fields/i18n/fr.po b/survey_extra_fields/i18n/fr.po index 8a6d53f..137c1ec 100644 --- a/survey_extra_fields/i18n/fr.po +++ b/survey_extra_fields/i18n/fr.po @@ -20,6 +20,11 @@ msgstr "" msgid ".pdf,.docx,.xlsx" msgstr "" +#. module: survey_extra_fields +#: model_terms:ir.ui.view,arch_db:survey_extra_fields.question_file +msgid "Remove file" +msgstr "Supprimer le fichier" + #. module: survey_extra_fields #: model:ir.model.fields,field_description:survey_extra_fields.field_survey_question__allowed_extensions msgid "Allowed Extensions" diff --git a/survey_extra_fields/models/survey_user_input.py b/survey_extra_fields/models/survey_user_input.py index 5ea52d2..cbe7821 100644 --- a/survey_extra_fields/models/survey_user_input.py +++ b/survey_extra_fields/models/survey_user_input.py @@ -23,14 +23,25 @@ class SurveyUserInput(models.Model): ("user_input_id", "=", self.id), ("question_id", "=", question.id), ]) + if not answer and any(line.value_file for line in old_answers): + # No new file was submitted: a file input cannot be pre-filled + # by the browser when navigating back to a previous page, so an + # empty answer here does not mean the user removed their file. + # Keep the previously uploaded file instead of overwriting it + # with a skipped answer. + return vals = { "user_input_id": self.id, "question_id": question.id, "skipped": False, "answer_type": "file", } - if answer: - file_data = json.loads(answer) + file_data = json.loads(answer) if answer else {} + if file_data.get("cleared"): + # The user explicitly removed the file: drop the stored data and + # mark the line as skipped. + vals.update(answer_type=None, skipped=True, value_file=False, value_file_fname=False) + elif file_data: file_b64 = file_data.get("data", "") file_name = file_data.get("name", "") self._check_file_constraints(question, file_b64, file_name) diff --git a/survey_extra_fields/static/src/js/survey_form.js b/survey_extra_fields/static/src/js/survey_form.js index 20844b8..9219b95 100644 --- a/survey_extra_fields/static/src/js/survey_form.js +++ b/survey_extra_fields/static/src/js/survey_form.js @@ -6,6 +6,68 @@ odoo.define("survey_extra_fields.survey_form", function (require) { var survey_form = require("survey.form"); survey_form.include({ + /** + * @override + * Bind delegated listeners on the form root so they keep working after + * each page is re-rendered (the inner content is replaced on navigation, + * but the root element persists). They let the user clear a selected + * file before submitting the form. + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self.$el.on( + "change.surveyExtraFile", + 'input[data-question-type="file"]', + self._onFileInputChange.bind(self) + ); + self.$el.on( + "click.surveyExtraFile", + ".o_survey_file_clear", + self._onFileClearClick.bind(self) + ); + }); + }, + + /** + * On selection, show the file "chip" (filename + remove button) and hide + * the raw input, so a freshly selected file looks exactly like an already + * stored one (rendered server-side when navigating back). + */ + _onFileInputChange: function (ev) { + var input = ev.currentTarget; + var $container = $(input).closest(".o_survey_comment_container"); + if (!$container.length || !(input.files && input.files.length > 0)) { + return; + } + $container.find(".o_survey_file_name").text(input.files[0].name); + $container.find(".o_survey_file_selected").removeClass("d-none"); + delete input.dataset.fileCleared; + $(input).addClass("d-none"); + }, + + /** + * Discard the current file: hide the chip and bring back the input so the + * user can pick a new one. A file already stored server-side is only + * really replaced once a new file is submitted (see save_lines). + */ + _onFileClearClick: function (ev) { + ev.preventDefault(); + var $container = $(ev.currentTarget).closest(".o_survey_comment_container"); + if (!$container.length) { + return; + } + var $input = $container.find('input[data-question-type="file"]'); + if ($input.length) { + $input.val(""); + // Flag the explicit removal so the submit tells the server to + // drop any previously stored file (instead of preserving it). + $input[0].dataset.fileCleared = "1"; + $input.removeClass("d-none"); + } + $container.find(".o_survey_file_selected").addClass("d-none"); + }, + _readFileAsDataURL: function (file) { return new Promise(function (resolve, reject) { var reader = new FileReader(); @@ -19,18 +81,28 @@ odoo.define("survey_extra_fields.survey_form", function (require) { }); }, + /** + * @override + * The base implementation builds the submit params synchronously and + * fires the RPC immediately. File inputs need to be read asynchronously + * (FileReader), so when the current page contains file answers we + * replicate the submit flow here, injecting the base64 file payload + * before submitting. + */ _submitForm: function (options) { var self = this; var $fileInputs = this.$('input[data-question-type="file"]'); - var hasFiles = false; + // A file action is either a new selection or an explicit removal of + // a previously stored file (which must be communicated to the server). + var hasFileAction = false; $fileInputs.each(function () { - if (this.files && this.files.length > 0) { - hasFiles = true; + if ((this.files && this.files.length > 0) || this.dataset.fileCleared) { + hasFileAction = true; return false; } }); - if (!hasFiles || this.options.isStartScreen) { + if (!hasFileAction || this.options.isStartScreen) { return this._super(options); } @@ -51,7 +123,9 @@ odoo.define("survey_extra_fields.survey_form", function (require) { this._prepareSubmitValues(formData, params); - // Read all selected files as base64 + // Read all selected files as base64 and add them to the submit + // params. Explicitly cleared inputs (no new file) send a "cleared" + // sentinel so the server removes the previously stored file. var filePromises = []; $fileInputs.each(function () { if (this.files && this.files.length > 0) { @@ -65,6 +139,8 @@ odoo.define("survey_extra_fields.survey_form", function (require) { }); }) ); + } else if (this.dataset.fileCleared) { + params[this.name] = JSON.stringify({ cleared: true }); } }); @@ -107,7 +183,16 @@ odoo.define("survey_extra_fields.survey_form", function (require) { $questionWrapper.data("constrErrorMsg") || _t("This question requires an answer."); if (questionRequired && !(this.files && this.files.length > 0)) { - errors[questionId] = constrErrorMsg; + // A file may already be stored server-side (e.g. uploaded + // then navigating back): the chip is visible even though the + // input is empty. Treat that as a valid answer. + var $chip = $(this) + .closest(".o_survey_comment_container") + .find(".o_survey_file_selected"); + var hasExistingFile = $chip.length && !$chip.hasClass("d-none"); + if (!hasExistingFile) { + errors[questionId] = constrErrorMsg; + } return; } if (this.files && this.files.length > 0) { @@ -143,4 +228,4 @@ odoo.define("survey_extra_fields.survey_form", function (require) { return result; }, }); -}); +}); \ No newline at end of file diff --git a/survey_extra_fields/tests/test_survey_file.py b/survey_extra_fields/tests/test_survey_file.py index 3529ac9..150cae8 100644 --- a/survey_extra_fields/tests/test_survey_file.py +++ b/survey_extra_fields/tests/test_survey_file.py @@ -94,19 +94,38 @@ class TestSurveyFileSaveLines(TestSurveyFileCommon): self.assertEqual(lines.value_file, new_b64.encode()) self.assertEqual(lines.value_file_fname, "second.pdf") - def test_save_file_then_skip(self): - """Uploading a file then submitting empty marks line as skipped.""" + def test_save_file_then_empty_keeps_file(self): + """Submitting empty after a file keeps it (file inputs cannot be + pre-filled when navigating back, so an empty answer must not erase it).""" answer = self._add_answer(self.survey, self.survey_manager.partner_id) file_json = json.dumps({"data": self.file_b64, "name": self.file_name}) answer.save_lines(self.question_file, file_json) answer.save_lines(self.question_file, "") + line = answer.user_input_line_ids.filtered( + lambda l: l.question_id == self.question_file + ) + self.assertEqual(len(line), 1) + self.assertFalse(line.skipped) + self.assertEqual(line.value_file, self.file_b64.encode()) + self.assertEqual(line.value_file_fname, self.file_name) + + def test_save_file_explicitly_cleared(self): + """Submitting the 'cleared' sentinel after a file removes it.""" + answer = self._add_answer(self.survey, self.survey_manager.partner_id) + file_json = json.dumps({"data": self.file_b64, "name": self.file_name}) + answer.save_lines(self.question_file, file_json) + + answer.save_lines(self.question_file, json.dumps({"cleared": True})) + line = answer.user_input_line_ids.filtered( lambda l: l.question_id == self.question_file ) self.assertEqual(len(line), 1) self.assertTrue(line.skipped) + self.assertFalse(line.value_file) + self.assertFalse(line.value_file_fname) class TestSurveyFileConstraints(TestSurveyFileCommon): diff --git a/survey_extra_fields/views/survey_templates.xml b/survey_extra_fields/views/survey_templates.xml index 26ac874..003cd1f 100644 --- a/survey_extra_fields/views/survey_templates.xml +++ b/survey_extra_fields/views/survey_templates.xml @@ -15,13 +15,26 @@