diff --git a/survey_extra_fields/i18n/fr.po b/survey_extra_fields/i18n/fr.po
index 8a6d53f..b75bbb0 100644
--- a/survey_extra_fields/i18n/fr.po
+++ b/survey_extra_fields/i18n/fr.po
@@ -6,8 +6,13 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
+<<<<<<< HEAD
"POT-Creation-Date: 2026-02-18 16:38+0000\n"
"PO-Revision-Date: 2026-02-18 16:38+0000\n"
+=======
+"POT-Creation-Date: 2026-06-10 15:03+0000\n"
+"PO-Revision-Date: 2026-06-10 15:03+0000\n"
+>>>>>>> d0afa23 ([IMP] survey_extra_fields : handle file question on page navigation)
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -20,6 +25,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..14c7174 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 @@
++
+