Compare commits
3 Commits
0d1866ace3
...
18.0-surve
| Author | SHA1 | Date | |
|---|---|---|---|
| 67fe375975 | |||
| ab1ba7dbb6 | |||
| aff1a6caae |
1
survey_dropdown_choice/__init__.py
Normal file
1
survey_dropdown_choice/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
29
survey_dropdown_choice/__manifest__.py
Normal file
29
survey_dropdown_choice/__manifest__.py
Normal 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,
|
||||
}
|
||||
1
survey_dropdown_choice/models/__init__.py
Normal file
1
survey_dropdown_choice/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import survey_question
|
||||
13
survey_dropdown_choice/models/survey_question.py
Normal file
13
survey_dropdown_choice/models/survey_question.py
Normal 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.",
|
||||
)
|
||||
185
survey_dropdown_choice/static/src/js/survey_dropdown_choice.js
Normal file
185
survey_dropdown_choice/static/src/js/survey_dropdown_choice.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
15
survey_dropdown_choice/views/survey_question_views.xml
Normal file
15
survey_dropdown_choice/views/survey_question_views.xml
Normal 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>
|
||||
110
survey_dropdown_choice/views/survey_templates.xml
Normal file
110
survey_dropdown_choice/views/survey_templates.xml
Normal 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>
|
||||
@@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user