3 Commits

Author SHA1 Message Date
67fe375975 [WIP]survey_dropdown_choice 2026-05-05 17:02:02 +02:00
ab1ba7dbb6 [FIX]survey_record_generation
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m34s
2026-04-23 16:52:17 +02:00
aff1a6caae [IMP] survey_record_generation : new option update_existing_fields
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m32s
2026-04-09 15:14:49 +02:00
18 changed files with 575 additions and 89 deletions

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,29 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Survey dropdown choice",
"summary": "Display a 'simple choice' question as a searchable dropdown instead of radio buttons",
"description": """
Adds an option on 'Multiple choice: only one answer' questions to render the
suggested answers as a searchable dropdown. While taking the survey, the
respondent can type in the input to filter the visible options, which is much
more convenient than scrolling through a long radio-button list.
""",
"version": "18.0.1.0.0",
"license": "AGPL-3",
"author": "Elabore",
"website": "https://www.elabore.coop",
"category": "Marketing/Surveys",
"depends": ["survey"],
"data": [
"views/survey_question_views.xml",
"views/survey_templates.xml",
],
"assets": {
"survey.survey_assets": [
"survey_dropdown_choice/static/src/scss/survey_dropdown_choice.scss",
"survey_dropdown_choice/static/src/js/survey_dropdown_choice.js",
],
},
"installable": True,
}

View File

@@ -0,0 +1 @@
from . import survey_question

View File

@@ -0,0 +1,13 @@
from odoo import fields, models
class SurveyQuestion(models.Model):
_inherit = "survey.question"
display_dropdown = fields.Boolean(
string="Display as searchable dropdown",
help="Render the suggested answers as a searchable dropdown instead of "
"a list of radio buttons. Only relevant for 'Multiple choice: only "
"one answer' questions. The respondent can type in the dropdown to "
"filter the available options.",
)

View File

@@ -0,0 +1,185 @@
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
// Strip combining diacritical marks (U+0300..U+036F) so "Élève" matches "eleve".
const STRIP_DIACRITICS = (str) =>
(str || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
publicWidget.registry.SurveyDropdownChoice = publicWidget.Widget.extend({
selector: ".o_survey_dropdown_choice",
events: {
"input .o_survey_dropdown_search": "_onSearchInput",
"focus .o_survey_dropdown_search": "_onSearchFocus",
"click .o_survey_dropdown_search_wrapper": "_onWrapperClick",
"keydown .o_survey_dropdown_search": "_onSearchKeydown",
"mousedown .o_survey_dropdown_option": "_onOptionMousedown",
"click .o_survey_dropdown_option": "_onOptionClick",
"click input[type='radio']": "_onRadioInputClick",
},
/**
* @override
*/
start() {
this._panelEl = this.el.querySelector(".o_survey_dropdown_panel");
this._searchEl = this.el.querySelector(".o_survey_dropdown_search");
this._noMatchEl = this.el.querySelector(".o_survey_dropdown_no_match");
this._options = Array.from(this.el.querySelectorAll(".o_survey_dropdown_option"));
this._onDocumentMousedown = this._onDocumentMousedown.bind(this);
document.addEventListener("mousedown", this._onDocumentMousedown);
return this._super(...arguments);
},
/**
* @override
*/
destroy() {
document.removeEventListener("mousedown", this._onDocumentMousedown);
return this._super(...arguments);
},
// --------------------------------------------------------------------------
// Handlers
// --------------------------------------------------------------------------
_onWrapperClick() {
// Make sure clicking anywhere in the search row (chevron, padding, ...)
// focuses the input, which in turn opens the panel.
if (document.activeElement !== this._searchEl) {
this._searchEl.focus();
} else {
this._openPanel();
this._filter("");
}
},
_onSearchFocus() {
this._openPanel();
this._filter("");
},
_onSearchInput(ev) {
this._openPanel();
this._filter(ev.currentTarget.value);
},
_onSearchKeydown(ev) {
if (ev.key === "Escape") {
this._closePanel();
ev.currentTarget.blur();
}
},
/**
* Prevent the search input's `blur` from firing before `click` resolves
* (otherwise the panel would close and swallow the click).
*/
_onOptionMousedown(ev) {
ev.preventDefault();
},
/**
* The wrapping <label> forwards clicks to the radio input as its
* default action. Stop that bubbled click from reaching
* SurveyFormWidget._onRadioChoiceClick at the .o_survey_form level —
* it would see the o_survey_form_choice_item_selected class we just
* added and toggle the radio back off, leaving the question with no
* selected answer at submission time.
*/
_onRadioInputClick(ev) {
ev.stopPropagation();
},
_onOptionClick(ev) {
const option = ev.currentTarget;
const radio = option.querySelector('input[type="radio"]');
if (!radio) {
return;
}
// Manage the radio state ourselves and skip the survey form's
// "click-radio-to-untick" handler — picking the same item from a
// dropdown should re-affirm the selection, not clear it.
const radioName = radio.getAttribute("name");
this.el
.querySelectorAll(`input[type="radio"][name="${radioName}"]`)
.forEach((other) => {
other.checked = false;
other.classList.remove("o_survey_form_choice_item_selected");
});
radio.checked = true;
radio.classList.add("o_survey_form_choice_item_selected");
// Trigger `change` so survey_form.js handles "Other" textarea
// visibility, conditional questions and auto-submit-on-pick.
radio.dispatchEvent(new Event("change", { bubbles: true }));
this._searchEl.value = option.dataset.labelValue || "";
this._options.forEach((opt) => {
const isSelected = opt === option;
opt.classList.toggle("o_survey_selected", isSelected);
opt.classList.toggle("bg-light", isSelected);
const check = opt.querySelector(".o_survey_dropdown_check");
if (check) {
check.classList.toggle("invisible", !isSelected);
}
});
this._closePanel();
this._searchEl.focus();
},
_onDocumentMousedown(ev) {
if (!this.el.contains(ev.target)) {
this._closePanel();
}
},
// --------------------------------------------------------------------------
// Internal
// --------------------------------------------------------------------------
_openPanel() {
this._panelEl.classList.remove("d-none");
},
_closePanel() {
this._panelEl.classList.add("d-none");
},
_filter(query) {
const needle = STRIP_DIACRITICS(query.trim());
let visibleCount = 0;
this._options.forEach((option) => {
const haystack = STRIP_DIACRITICS(option.dataset.labelValue || option.textContent);
const matches = !needle || haystack.includes(needle);
option.classList.toggle("d-none", !matches);
if (matches) {
visibleCount += 1;
}
});
if (this._noMatchEl) {
this._noMatchEl.classList.toggle("d-none", visibleCount > 0);
}
},
});
// In `page_per_question` layout, survey_form.js loads the next question's HTML
// via AJAX and only re-bootstraps date pickers (see _onNextScreenDone). Extend
// it so our dropdown widget is also instantiated on each navigation.
if (publicWidget.registry.SurveyFormWidget) {
publicWidget.registry.SurveyFormWidget.include({
async _onNextScreenDone() {
const result = await this._super(...arguments);
const $dropdowns = this.$el.find(".o_survey_dropdown_choice");
if ($dropdowns.length) {
this.trigger_up("widgets_start_request", { $target: $dropdowns });
}
return result;
},
});
}
export default publicWidget.registry.SurveyDropdownChoice;

View File

@@ -0,0 +1,42 @@
.o_survey_dropdown_choice {
.o_survey_dropdown_search_wrapper {
cursor: text;
}
.o_survey_dropdown_search {
cursor: text;
}
// The chevron sits above the input — let clicks pass through so focusing
// the input (which opens the panel) works when clicking near the arrow.
.o_survey_dropdown_caret {
pointer-events: none;
}
.o_survey_dropdown_panel {
position: absolute;
left: 0;
right: 0;
z-index: 1050;
max-height: 18rem;
overflow-y: auto;
}
.o_survey_dropdown_option {
cursor: pointer;
transition: background-color 0.1s ease-in-out;
&:hover {
background-color: var(--bs-light, #f8f9fa);
}
&.o_survey_selected {
font-weight: 500;
}
}
.o_survey_dropdown_check {
width: 1rem;
flex-shrink: 0;
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="survey_question_form_dropdown" model="ir.ui.view">
<field name="name">survey.question.form.dropdown</field>
<field name="model">survey.question</field>
<field name="inherit_id" ref="survey.survey_question_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='comments_allowed']" position="before">
<field name="display_dropdown" invisible="question_type != 'simple_choice'"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
Inherit the standard "simple_choice" template:
- when display_dropdown is set on the question AND we are not in
read-only/print mode, render our dropdown variant instead
- otherwise keep the standard radio-button rendering untouched
-->
<template id="question_simple_choice_inherit"
inherit_id="survey.question_simple_choice"
name="Question: simple choice (dropdown variant)">
<xpath expr="//div[@data-question-type='simple_choice_radio']" position="before">
<t t-if="question.display_dropdown and not survey_form_readonly"
t-call="survey_dropdown_choice.question_simple_choice_dropdown"/>
</xpath>
<xpath expr="//div[@data-question-type='simple_choice_radio']" position="attributes">
<attribute name="t-if">not question.display_dropdown or survey_form_readonly</attribute>
</xpath>
</template>
<!--
Searchable dropdown variant.
Keeps `data-question-type="simple_choice_radio"` and a real <input
type="radio"> per option so that the existing survey_form.js
validation/serialization logic (_validateForm, _prepareSubmitChoices)
works without any change.
-->
<template id="question_simple_choice_dropdown" name="Question: simple choice (dropdown)">
<t t-set="answer_line" t-value="answer_lines.filtered(lambda line: line.suggested_answer_id)"/>
<t t-set="comment_line" t-value="answer_lines.filtered(lambda line: line.value_char_box)"/>
<t t-set="selected_label" t-value="answer_line.suggested_answer_id"/>
<div class="o_survey_answer_wrapper o_survey_form_choice o_survey_dropdown_choice position-relative"
t-att-data-name="question.id"
t-att-data-is-skipped-question="is_skipped_question or None"
data-question-type="simple_choice_radio">
<div class="o_survey_dropdown_search_wrapper position-relative">
<input type="text"
class="o_survey_dropdown_search form-control pe-5"
autocomplete="off"
t-att-placeholder="question.question_placeholder or 'Type to filter...'"
t-att-value="selected_label.value if selected_label else ''"/>
<i class="o_survey_dropdown_caret fa fa-chevron-down position-absolute end-0 top-50 translate-middle-y me-3 text-muted"
aria-hidden="true"/>
</div>
<div class="o_survey_dropdown_panel d-none mt-1 border rounded shadow-sm bg-white">
<ul class="o_survey_dropdown_options list-unstyled m-0 p-0">
<t t-foreach="question.suggested_answer_ids" t-as="label">
<t t-set="answer_selected" t-value="answer_line and answer_line.suggested_answer_id.id == label.id"/>
<li t-attf-class="o_survey_dropdown_option px-3 py-2 #{'o_survey_selected bg-light' if answer_selected else ''}"
t-att-data-label-value="label.value"
t-att-data-answer-id="label.id">
<label t-att-for="str(question.id) + '_' + str(label.id)"
class="o_survey_choice_btn w-100 m-0 d-flex align-items-center"
style="cursor: pointer;">
<input t-att-id="str(question.id) + '_' + str(label.id)"
type="radio"
t-att-value="label.id"
t-attf-class="o_survey_form_choice_item invisible position-absolute #{'o_survey_form_choice_item_selected' if answer_selected else ''}"
t-att-name="question.id"
t-att-checked="'checked' if answer_selected else None"/>
<i t-attf-class="o_survey_dropdown_check fa fa-check me-2 #{'' if answer_selected else 'invisible'}"
aria-hidden="true"/>
<span class="text-break" t-field="label.value"/>
</label>
</li>
</t>
<li class="o_survey_dropdown_no_match d-none px-3 py-2 text-muted fst-italic">
No match found.
</li>
<t t-if="question.comments_allowed and question.comment_count_as_answer">
<li t-attf-class="o_survey_dropdown_option o_survey_dropdown_option_other px-3 py-2 #{'o_survey_selected bg-light' if comment_line else ''}"
data-label-value="">
<label class="o_survey_choice_btn w-100 m-0 d-flex align-items-center"
style="cursor: pointer;">
<input type="radio"
class="o_survey_form_choice_item o_survey_js_form_other_comment invisible position-absolute"
value="-1"
t-att-name="question.id"
t-att-checked="comment_line and 'checked' or None"/>
<i t-attf-class="o_survey_dropdown_check fa fa-check me-2 #{'' if comment_line else 'invisible'}"
aria-hidden="true"/>
<span t-out="question.comments_message or default_comments_message"/>
</label>
</li>
</t>
</ul>
</div>
<t t-if="question.comments_allowed and question.comment_count_as_answer">
<div t-attf-class="o_survey_comment_container mt-3 py-0 px-1 h-auto #{'d-none' if not comment_line else ''}">
<textarea type="text" class="form-control o_survey_question_text_box bg-transparent rounded-0 p-0"
t-att-disabled="None if comment_line else 'disabled'"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</t>
<div t-if="question.comments_allowed and not question.comment_count_as_answer"
class="mb-2 o_survey_comment_container mt-3">
<textarea type="text"
class="col form-control o_survey_comment o_survey_question_text_box bg-transparent rounded-0 p-0"
t-att-placeholder="question.comments_message or default_comments_message"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</div>
</template>
</odoo>

View File

@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -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 :
----------------------------------------------------
@@ -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.0",
"version": "18.0.1.0.1",
"license": "AGPL-3",
"author": "Elabore",
"website": "https://www.elabore.coop",

View File

@@ -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 : <b>%s</b>"
msgstr "Le type de champ est : <b>%s</b>"
@@ -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"

View File

@@ -32,8 +32,9 @@ class SurveyQuestion(models.Model):
def fill(self):
for question in self:
if question.model_id:
new_suggested_answer_ids = [Command.clear()]
if question.suggested_answer_ids:
question.suggested_answer_ids = [Command.clear()]
elif question.model_id:
record_model = question.model_id.model
if question.fill_domain:
@@ -43,7 +44,11 @@ class SurveyQuestion(models.Model):
records = self.env[record_model].search(domain)
new_suggested_answer_ids += [Command.create({'value':record.display_name, 'record_id':f"{record_model},{record.id}"
}) for record in records]
question.suggested_answer_ids = new_suggested_answer_ids
question.suggested_answer_ids = [
Command.create({
'value': record.display_name,
'record_id': f"{record_model},{record.id}",
})
for record in records
]

View File

@@ -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",

View File

@@ -5,9 +5,6 @@ from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools.misc import format_date
_logger = logging.getLogger(__name__)
type_mapping = { #field types on the left, question types on the right. TODO : what about booleans ?
"char": ["char_box", "numerical_box", "date", "datetime", "simple_choice", "multiple_choice"],
"text": ["char_box", "date", "simple_choice"],
@@ -78,20 +75,28 @@ class SurveyRecordCreationFieldValues(models.Model):
record.field_help = field_help
@api.depends('field_id')
@api.depends('field_id', 'survey_record_creation_id.survey_id')
def _compute_allowed_question_ids(self):
for record_creation_field_values in self:
if not record_creation_field_values.survey_id or not record_creation_field_values.field_id:
record_creation_field_values.allowed_question_ids = None
for record in self:
survey = record.survey_id or record.survey_record_creation_id.survey_id
if not survey or not record.field_id:
record.allowed_question_ids = False
continue
question_domain = [('survey_id','=',record_creation_field_values.survey_id.id)]
if record_creation_field_values.field_id.ttype in ['many2one','many2many']:
question_domain.extend(['|','&',('answer_values_type','=','record'),('model_id','=',record_creation_field_values.field_id.relation),('answer_values_type','=','value')])
if record_creation_field_values.field_id.ttype in type_mapping:
question_domain.append(('question_type','in',type_mapping[record_creation_field_values.field_id.ttype]))
question_domain = [('survey_id', '=', survey.id)]
record_creation_field_values.allowed_question_ids = self.env['survey.question'].search(question_domain)
if record.field_id.ttype in ['many2one', 'many2many']:
question_domain.extend([
'|', '&',
('answer_values_type', '=', 'record'),
('model_id', '=', record.field_id.relation),
('answer_values_type', '=', 'value'),
])
if record.field_id.ttype in type_mapping:
question_domain.append(('question_type', 'in', type_mapping[record.field_id.ttype]))
questions = self.env['survey.question'].search(question_domain)
record.allowed_question_ids = questions
@api.model

View File

@@ -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:

View File

@@ -722,9 +722,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

View File

@@ -6,15 +6,17 @@
<field name="inherit_id" ref="survey.survey_question_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='suggested_answer_ids']" position="before">
<field name="answer_values_type" />
<field name="model_id" invisible="answer_values_type != 'record'" />
<field name="model_name" invisible="1" />
<field name="fill_domain" widget="domain" options="{'model': 'model_name'}" invisible="answer_values_type != 'record' or not model_id" />
<button name="fill" string="Empty and fill"
type="object"
colspan="2"
help="Empty the list and fill it with all items of selected model matching domain"
invisible="answer_values_type != 'record'" />
<group invisible="question_type not in ['simple_choice', 'multiple_choice', 'matrix']">
<field name="answer_values_type" />
<field name="model_id" invisible="answer_values_type != 'record'" />
<field name="model_name" invisible="1" />
<field name="fill_domain" widget="domain" options="{'model': 'model_name'}" invisible="answer_values_type != 'record' or not model_id" />
<button name="fill" string="Empty and fill"
type="object"
colspan="2"
help="Empty the list and fill it with all items of selected model matching domain"
invisible="answer_values_type != 'record'" />
</group>
</xpath>
<xpath expr="//field[@name='suggested_answer_ids']" position="attributes">
<attribute

View File

@@ -16,12 +16,19 @@
</list>
<form>
<group>
<field name="name" />
<field name="model_id" />
<field name="ignore_if_mandatory_field_is_missing" />
<field name="update_existing_records" />
<field name="allowed_field_ids" invisible="1"/>
<field name="field_to_retrieve_existing_records" invisible="not update_existing_records"/>
<group colspan="4">
<field name="name" />
<field name="model_id" />
<field name="ignore_if_mandatory_field_is_missing" />
<field name="allowed_field_ids" invisible="1"/>
</group>
<group>
<field name="update_existing_records" />
</group>
<group invisible="not update_existing_records">
<field name="field_to_retrieve_existing_records"/>
<field name="update_existing_values"/>
</group>
<div colspan="2" style="width:100%;">
<div class="alert alert-warning"
invisible="not update_existing_records">