diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 15d87b08f7f2d8ae34925be3914373291f14e09b..57981c9055da8be352bf7545d645128898817c92 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -58,7 +58,7 @@ repos:
rev: v2.7.1
hooks:
- id: prettier
- exclude: ^templates/,^data/
+ exclude: ^(data/.*|templates/.*)$
name: prettier (with plugin-xml)
additional_dependencies:
- "prettier@2.7.1"
diff --git a/__init__.py b/__init__.py
index f7209b17100218a42c80c8e984c08597d630b188..72d3ea60a8cf7cd1d26a4066c05e6817315bce8b 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,2 +1 @@
-from . import models
-from . import controllers
+from . import controllers, models
diff --git a/__manifest__.py b/__manifest__.py
index e0f9f7749a76340d7541e26961883e4ee31f948a..c7078e87d7b1e6319cf230dec5c1e03a52912c86 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -16,9 +16,13 @@
# datas
"data/mail_aeci.xml",
"data/mail_end_training.xml",
+ "data/mail_subscription.xml",
# templates
+ "templates/survey_duplicated_answer.xml",
+ "templates/survey_template_management.xml",
# views
"views/survey.xml",
+ "views/survey_question.xml",
"views/survey_user_input.xml",
"views/training.xml",
"views/training_program.xml",
diff --git a/controllers/__init__.py b/controllers/__init__.py
index e425c18c14ba72b12d8086c870cdcd5f615f23f4..7e49d3771cdd28eba66f8c6d6c254a79b499d389 100644
--- a/controllers/__init__.py
+++ b/controllers/__init__.py
@@ -1,4 +1 @@
-# -*- encoding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from . import main
+from . import survey
diff --git a/controllers/main.py b/controllers/survey.py
similarity index 52%
rename from controllers/main.py
rename to controllers/survey.py
index ffbbae975814cc7e93b394f1c127b3f7d6bb1ffc..eff5fadf3977a048e33d0f43ab01dcf62ddc0794 100644
--- a/controllers/main.py
+++ b/controllers/survey.py
@@ -1,32 +1,29 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
+# Copyright 2024 Le Filament (<https://le-filament.com>)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
-from odoo.addons.survey.controllers.main import Survey
from odoo import http
-from odoo.exceptions import UserError
from odoo.http import request
+from odoo.addons.survey.controllers.main import Survey
_logger = logging.getLogger(__name__)
class TrainingSurvey(Survey):
- #
+
# ------------------------------------------------------------
- # TAKING SURVEY ROUTES
+ # Inherit parent
# ------------------------------------------------------------
-
@http.route(
"/survey/start/<string:survey_token>", type="http", auth="public", website=True
)
def survey_start(self, survey_token, answer_token=None, email=False, **post):
- """Start a survey by providing
- * a token linked to a survey;
- * a token linked to an answer or generate a new token if access is allowed;
"""
-
+ Hérite la fonction parente pour relier la réponse à la formation si elle est
+ contenue dans l'URL
+ """
page = super().survey_start(
survey_token=survey_token, answer_token=None, email=False, **post
)
@@ -49,3 +46,24 @@ class TrainingSurvey(Survey):
answer_sudo.training_id = training
return page
+
+ # ------------------------------------------------------
+ # Inherit Business methods
+ # ------------------------------------------------------
+ def _prepare_question_html(self, survey_sudo, answer_sudo, **post):
+ """
+ Annule la réponse en cours et renvoie vers la page de duplication de réponse si
+ l'option est activée
+ """
+ response = super()._prepare_question_html(survey_sudo, answer_sudo, **post)
+
+ if survey_sudo.is_one_answer and answer_sudo.is_duplicate_answer:
+ request.env.cr.rollback()
+ answer_sudo.unlink()
+ response["survey_content"] = request.env["ir.qweb"]._render(
+ "cgscop_survey.survey_duplicated_answer",
+ {"survey": survey_sudo, "title": survey_sudo.title},
+ )
+ _logger.error("Survey answer duplication not allowed")
+
+ return response
diff --git a/data/mail_aeci.xml b/data/mail_aeci.xml
index 821b70a630060792c5b5f7b92e323f2c07ed4063..075eb9f062c9d1bf88e0669f4e577a3cc9b5e79f 100644
--- a/data/mail_aeci.xml
+++ b/data/mail_aeci.xml
@@ -1,58 +1,52 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="mail_template_training_aeci" model="mail.template">
- <field name="name">CG Scop - Formation - AECI</field>
+ <field name="name">Formation - AECI</field>
<field name="model_id" ref="training.model_training_student" />
<field name="subject">Formation {{ object.training_id.program_id.name }} - Questionnaire AECI</field>
- <field name="email_from">lbrien@scop.coop <Laurence BRIEN></field>
+ <field name="email_from">{{ object.training_id.company_id.training_user_contact.login }} <{{ object.training_id.company_id.training_user_contact.name }}></field>
<field name="email_to">{{ (object.partner_id.email or object.email) }}</field>
<field name="description">Mail envoyé au stagiaire pour le questionnaire AECI</field>
<field name="body_html" type="html">
-<div style="margin: 0px; padding: 0px; font-size: 13px;">
- <p style="margin: 0px; padding: 0px; font-size: 13px;">
- Bonjour,
- <br /><br />
- En prévision de ta participation à la formation <t t-out="object.training_id.program_id.name">Nom Formation</t>
- qui se tiendra
- <t t-if="len(object.training_id.slot_ids) == 1">
- le <t
- t-out="object.training_id.slot_ids[0].date_start"
- t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}"
- >05/05/2024</t> de
- <t
- t-out="object.training_id.slot_ids[0].date_start"
- t-options="{'widget': 'datetime', 'format': 'HH:mm'}"
- >09:00</t> à
- <t
- t-out="object.training_id.slot_ids[0].date_end"
- t-options="{'widget': 'datetime', 'format': 'HH:mm'}"
- >12:00</t>
- </t>
- <t t-else="">
- aux dates suivantes :
- <ul>
- <li t-foreach="object.training_id.slot_ids" t-as="slot">
- <t t-out="slot.date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}">05/05/2024</t> de
- <t t-out="slot.date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">09:00</t> à
- <t t-out="slot.date_end" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">12:00</t>
- </li>
- </ul>
- </t>
- je te serai reconnaissante de bien vouloir renseigner le formulaire d'autoévaluation des compétences initiales ci-après :
- <br /><br />
- <a style="display: inline-block; padding: 10px; text-decoration: none; background-color: #17a2b8; color: #fff; border-radius: 5px;" t-att-href="env.context.get('survey_user_input').get_start_url()">Questionnaire AECI</a>
- <br /><br />
- Je te remercie et te souhaite bonne réception de ce courriel.
- <br /><br />
- Bonne journée.
- <br />
- Laurence Brien<br />
- Déléguée à la formation professionnelle
- </p>
-</div>
- </field>
+ <div style="margin: 0px; padding: 0px; font-size: 13px;">
+ <p style="margin: 0px; padding: 0px; font-size: 13px;">
+ Bonjour,
+ <br /><br />
+ En prévision de votre participation à la formation <t t-out="object.training_id.program_id.name">Nom Formation</t>
+ qui se tiendra
+ <t t-if="len(object.training_id.slot_ids) == 1">
+ le <t t-out="object.training_id.slot_ids[0].date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}">05/05/2024</t> de
+ <t t-out="object.training_id.slot_ids[0].date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">09:00</t> à
+ <t t-out="object.training_id.slot_ids[0].date_end" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">12:00</t>
+ </t>
+ <t t-else="">
+ aux dates suivantes :
+ <ul>
+ <li t-foreach="object.training_id.slot_ids" t-as="slot">
+ <t t-out="slot.date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}">05/05/2024</t> de
+ <t t-out="slot.date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">09:00</t> à
+ <t t-out="slot.date_end" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">12:00</t>
+ </li>
+ </ul>
+ </t>
+ je vous remercie de bien vouloir renseigner le formulaire d'autoévaluation des compétences initiales ci-après :
+ <br /><br />
+ <a
+ style="display: inline-block; padding: 10px; text-decoration: none; background-color: #17a2b8; color: #fff; border-radius: 5px;"
+ t-att-href="env.context.get('survey_user_input').get_start_url()"
+ >Questionnaire AECI</a>
+ <br /><br />
+ Je vous remercie et vous souhaite bonne réception de ce courriel.
+ <br /><br />
+ Bonne journée.
+ <br />
+ <t t-if="object.training_id.company_id.training_user_contact.signature">
+ <t t-out="object.training_id.company_id.training_user_contact.signature or ''">--<br/>Mitchell Admin</t>
+ </t>
+ </p>
+ </div>
+ </field>
<field name="lang">fr_FR</field>
- <field name="auto_delete" eval="False" />
+ <field name="auto_delete" eval="False" />
</record>
-
</odoo>
diff --git a/data/mail_end_training.xml b/data/mail_end_training.xml
index 23c2f5dc13e02d2a26cd0591d6b7c8e66db35df0..19223a8d988499a4e5b079c07007794937d6d293 100644
--- a/data/mail_end_training.xml
+++ b/data/mail_end_training.xml
@@ -1,60 +1,59 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="mail_template_training_end" model="mail.template">
- <field name="name">CG Scop - Formation - AECT</field>
- <field name="model_id" ref="training.model_training_student" />
- <field name="subject">Formation {{ object.training_id.program_id.name }} - Questionnaire AECT</field>
- <field name="email_from">lbrien@scop.coop <Laurence BRIEN></field>
- <field name="email_to">{{ (object.partner_id.email or object.email) }}</field>
- <field name="description">Mail envoyé au stagiaire pour le questionnaire AECT</field>
- <field name="body_html" type="html">
-<div style="margin: 0px; padding: 0px; font-size: 13px;">
- <p style="margin: 0px; padding: 0px; font-size: 13px;">
- Bonjour,
- <br /><br />
- Pour donner suite à ta participation à la formation <t t-out="object.training_id.program_id.name">Nom Formation</t>
- qui s’est tenue
- <t t-if="len(object.training_id.slot_ids) == 1">
- le <t t-out="object.training_id.slot_ids[0].date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}">05/05/2024</t> de
- <t t-out="object.training_id.slot_ids[0].date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">09:00</t> à
- <t t-out="object.training_id.slot_ids[0].date_end" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">12:00</t>
- </t>
- <t t-else="">
- aux dates suivantes :
- <ul>
- <li t-foreach="object.training_id.slot_ids" t-as="slot">
- <t t-out="slot.date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}">05/05/2024</t> de
- <t t-out="slot.date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">09:00</t> à
- <t t-out="slot.date_end" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">12:00</t>
- </li>
- </ul>
- </t>
- <br />
- je t’adresse les liens vers deux formulaires en ligne que tu voudras bien renseigner :
- <ul>
- <li>
- <a t-att-href="env.context.get('aect_answer').get_start_url()">Autoévaluation des compétences terminales</a>
- </li>
- <li>
- <a t-att-href="env.context.get('satisfaction_answer').get_start_url()">Evaluation de ta satisfaction</a>
- </li>
- </ul>
- <br /><br />
- Je te prie de bien vouloir trouver en pièce jointe ton <t t-out="object.get_certification_name()"/>
- <br /><br />
- Je te remercie et te souhaite bonne réception de ce courriel.
- <br /><br />
- Bonne journée.
- <br />
- Laurence Brien<br />
- Déléguée à la formation professionnelle
- </p>
-</div>
+ <field name="name">Formation - AECT</field>
+ <field name="model_id" ref="training.model_training_student" />
+ <field name="subject">Formation {{ object.training_id.program_id.name }} - Questionnaire AECT</field>
+ <field name="email_from">{{ object.training_id.company_id.training_user_contact.login }} <{{ object.training_id.company_id.training_user_contact.name }}></field>
+ <field name="email_to">{{ (object.partner_id.email or object.email) }}</field>
+ <field name="description">Mail envoyé au stagiaire pour le questionnaire AECT</field>
+ <field name="body_html" type="html">
+ <div style="margin: 0px; padding: 0px; font-size: 13px;">
+ <p style="margin: 0px; padding: 0px; font-size: 13px;">
+ Bonjour,
+ <br /><br />
+ Pour donner suite à votre participation à la formation <t t-out="object.training_id.program_id.name">Nom Formation</t> qui s’est tenue
+ <t t-if="len(object.training_id.slot_ids) == 1">
+ le <t t-out="object.training_id.slot_ids[0].date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}">05/05/2024</t> de
+ <t t-out="object.training_id.slot_ids[0].date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">09:00</t> à
+ <t t-out="object.training_id.slot_ids[0].date_end" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">12:00</t>
+ </t>
+ <t t-else="">
+ aux dates suivantes :
+ <ul>
+ <li t-foreach="object.training_id.slot_ids" t-as="slot">
+ <t t-out="slot.date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}">05/05/2024</t> de
+ <t t-out="slot.date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">09:00</t> à
+ <t t-out="slot.date_end" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">12:00</t>
+ </li>
+ </ul>
+ </t>
+ <br />
+ je vous adresse les liens vers deux formulaires en ligne que vous voudrez bien renseigner :
+ <ul>
+ <li>
+ <a t-att-href="env.context.get('aect_answer').get_start_url()">Autoévaluation des compétences terminales</a>
+ </li>
+ <li>
+ <a t-att-href="env.context.get('satisfaction_answer').get_start_url()">Evaluation de ta satisfaction</a>
+ </li>
+ </ul>
+ <br /><br />
+ Je vous prie de bien vouloir trouver en pièce jointe ton <t t-out="object.get_certification_name()" />
+ <br /><br />
+ Je vous remercie et vous souhaite bonne réception de ce courriel.
+ <br /><br />
+ Bonne journée.
+ <br />
+ <t t-if="object.training_id.company_id.training_user_contact.signature">
+ <t t-out="object.training_id.company_id.training_user_contact.signature or ''">--<br/>Mitchell Admin</t>
+ </t>
+ </p>
+ </div>
</field>
<field name="report_template" ref="training.report_attestation_pdf" />
<field name="report_name">{{ object.get_certification_name() }} - {{ object.partner_id.name }}</field>
<field name="lang">fr_FR</field>
<field name="auto_delete" eval="False" />
</record>
-
</odoo>
diff --git a/data/mail_subscription.xml b/data/mail_subscription.xml
new file mode 100644
index 0000000000000000000000000000000000000000..51e24063744d6ba805e89a563610ed9153769de3
--- /dev/null
+++ b/data/mail_subscription.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+ <record id="mail_template_training_subscription" model="mail.template">
+ <field name="name">Formation - Pré-inscription</field>
+ <field name="model_id" ref="training.model_training_student" />
+ <field name="subject">Formation {{ object.training_id.program_id.name }} - Pré-inscription</field>
+ <field name="email_from">{{ object.training_id.company_id.training_user_contact.login }} <{{ object.training_id.company_id.training_user_contact.name }}></field>
+ <field name="email_to">{{ (object.partner_id.email or object.email) }}</field>
+ <field name="description">Mail envoyé au stagiaire pour valider sa pré-inscription</field>
+ <field name="body_html" type="html">
+ <div style="margin: 0px; padding: 0px; font-size: 13px;">
+ <p style="margin: 0px; padding: 0px; font-size: 13px;">
+ Bonjour,
+ <br /><br />
+ Votre pré-inscription à la formation <t t-out="object.training_id.program_id.name">Nom Formation</t>
+ qui se tiendra
+ <t t-if="len(object.training_id.slot_ids) == 1">
+ le <t t-out="object.training_id.slot_ids[0].date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}">05/05/2024</t> de
+ <t t-out="object.training_id.slot_ids[0].date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">09:00</t> à
+ <t t-out="object.training_id.slot_ids[0].date_end" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">12:00</t>
+ </t>
+ <t t-else="">
+ aux dates suivantes :
+ <ul>
+ <li t-foreach="object.training_id.slot_ids" t-as="slot">
+ <t t-out="slot.date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}">05/05/2024</t> de
+ <t t-out="slot.date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">09:00</t> à
+ <t t-out="slot.date_end" t-options="{'widget': 'datetime', 'format': 'HH:mm'}">12:00</t>
+ </li>
+ </ul>
+ </t>
+ a bien été prise en compte.
+ <br /><br />
+ Nous reviendrons vers vous très prochainement pour confirmer votre inscription.
+ <br /><br />
+ Je te remercie et te souhaite bonne réception de ce courriel.
+ <br /><br />
+ Bonne journée.
+ <br />
+ <t t-if="object.training_id.company_id.training_user_contact.signature">
+ <t t-out="object.training_id.company_id.training_user_contact.signature or ''">--<br/>Mitchell Admin</t>
+ </t>
+ </p>
+ </div>
+ </field>
+ <field name="lang">fr_FR</field>
+ <field name="auto_delete" eval="False" />
+ </record>
+</odoo>
diff --git a/models/__init__.py b/models/__init__.py
index f4c21ac1e850190a616b0db239b6098d3034f837..09d1366bdcd661a76dd30323c3a9681359f64538 100644
--- a/models/__init__.py
+++ b/models/__init__.py
@@ -1,4 +1,6 @@
from . import survey
+from . import survey_question
+from . import survey_question_answer
from . import survey_user_input
from . import training
from . import training_program
diff --git a/models/survey.py b/models/survey.py
index 388c46ebc465dcff6ff05e7d0c43d0bd0a7dde56..8265319eb7c36cd6e812bdad66cfe2cce61d46f4 100644
--- a/models/survey.py
+++ b/models/survey.py
@@ -1,7 +1,8 @@
# Copyright 2019-2022 Le Filament (<https://le-filament.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-from odoo import fields, models
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
class Survey(models.Model):
@@ -15,7 +16,7 @@ class Survey(models.Model):
)
training_survey_type = fields.Selection(
selection=[
- ("subscribe", "Inscription / Positionnement"),
+ ("subscribe", "Pré-inscription / Positionnement"),
("aeci", "AECI"),
("aect", "AECT"),
("satisfaction", "Satisfaction"),
@@ -29,6 +30,32 @@ class Survey(models.Model):
program_ids = fields.One2many(
comodel_name="training.program", compute="_compute_program_ids"
)
+ is_partner_check = fields.Boolean(
+ string="Vérifier si le contact existe",
+ default=False,
+ help="""
+ Renvoie une erreur à l'utilisateur si son email n'est pas présent dans la base
+ de contacts.
+ Nécessite de configurer dans le questionnaire un champ de type email et
+ l'enregistrer comme contact.
+ """,
+ )
+ is_one_answer = fields.Boolean(
+ string="Une seule réponse par contact",
+ default=False,
+ )
+ is_partner_create = fields.Boolean(
+ string="Créer le contact si il n'existe pas",
+ default=False,
+ )
+ authorized_domain = fields.Char(
+ string="Domaines autorisés",
+ )
+ is_take_again = fields.Boolean(
+ "Refaire le sondage",
+ default=False,
+ help="Afficher un bouton pour pouvoir refaire le sondage une fois terminé",
+ )
# ------------------------------------------------------
# Override ORM
@@ -62,5 +89,64 @@ class Survey(models.Model):
survey.program_ids = False
# ------------------------------------------------------
- # Buttons
+ # Onchange
+ # ------------------------------------------------------
+ @api.onchange("is_partner_check")
+ def _onchange_is_partner_check(self):
+ if not self.is_partner_check:
+ self.is_partner_create = False
+
+ # ------------------------------------------------------
+ # Actions
+ # ------------------------------------------------------
+
+ # ------------------------------------------------------
+ # Override ORM
+ # ------------------------------------------------------
+ def unlink(self):
+ for survey in self:
+ if survey.program_ids.filtered(lambda p: p.state == "confirmed"):
+ raise UserError(_(
+ f"Le questionnaire {survey.title} est lié à un programme de "
+ f"formation validé. "
+ f"Il ne peut être supprimé, vous pouvez l'archiver le si nécessaire."
+ ))
+ if survey.training_ids.filtered(lambda p: p.state != "0_to_plan"):
+ raise UserError(_(
+ f"Le questionnaire {survey.title} est lié à uns session de "
+ f"formation validé. "
+ f"Il ne peut être supprimé, vous pouvez l'archiver le si nécessaire."
+ ))
+ return super().unlink()
+
+ # ------------------------------------------------------
+ # Business function
# ------------------------------------------------------
+ def _create_training_survey(self, training_survey_type):
+ survey_id = self.create({
+ "title": "Questionnaire",
+ "survey_type": "training",
+ "training_survey_type": training_survey_type,
+ "is_one_answer": True,
+ })
+ survey_id._create_email_question()
+ survey_id._create_firstname_question()
+ survey_id._create_lastname_question()
+ survey_id._create_company_question()
+ return survey_id
+
+ def _create_email_question(self):
+ self.ensure_one()
+ self.question_ids._create_email_question(self.id)
+
+ def _create_firstname_question(self):
+ self.ensure_one()
+ self.question_ids._create_firstname_question(self.id)
+
+ def _create_lastname_question(self):
+ self.ensure_one()
+ self.question_ids._create_lastname_question(self.id)
+
+ def _create_company_question(self):
+ self.ensure_one()
+ self.question_ids._create_company_question(self.id)
diff --git a/models/survey_question.py b/models/survey_question.py
new file mode 100644
index 0000000000000000000000000000000000000000..24ad87941af1dbcc33b0b6fa21c05b2123a190c6
--- /dev/null
+++ b/models/survey_question.py
@@ -0,0 +1,107 @@
+# Copyright 2024 Le Filament (<https://le-filament.com>)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo import fields, models
+
+
+class SurveyQuestion(models.Model):
+ _inherit = "survey.question"
+
+ save_as_firstname = fields.Boolean("Enregistrer comme prénom", default=False)
+ save_as_company = fields.Boolean("Enregistrer comme entreprise", default=False)
+
+ # ------------------------------------------------------
+ # Inherit parent
+ # ------------------------------------------------------
+ def _validate_char_box(self, answer):
+ """
+ Hérite la fonction parente pour retourner une erreur :
+ - si le contact ne figure pas dans la liste
+ - si le domaine n'est pas autorisé
+ """
+ # Check existing partner
+ if (
+ self.save_as_email
+ and self.survey_id.is_partner_check
+ and not self.survey_id.is_partner_create
+ ):
+ partner_id = (
+ self.env["res.partner"].sudo().search([("email", "=", answer)], limit=1)
+ )
+ if not partner_id:
+ error_msg = (
+ f"Votre email ne fait pas partie de la liste des emails autorisés pour "
+ f"répondre à ce questionnaire."
+ )
+ return {self.id: error_msg}
+ # Check authorized domains
+ if self.save_as_email and self.survey_id.authorized_domain:
+ domain_list = self.survey_id.authorized_domain.split(",")
+ answer_domain = answer[answer.index("@") + 1 :]
+ if answer_domain not in domain_list:
+ error_msg = (
+ f"Votre email ne fait pas partie de la liste des domaines autorisés "
+ f"pour répondre à ce questionnaire."
+ )
+ return {self.id: error_msg}
+ return super()._validate_char_box(answer)
+
+ # ------------------------------------------------------
+ # Onchange methods
+ # ------------------------------------------------------
+
+ # ------------------------------------------------------
+ # Business Methods
+ # ------------------------------------------------------
+
+ # ------------------------------------------------------
+ # Business function
+ # ------------------------------------------------------
+ def _create_email_question(self, survey_id):
+ self.create(
+ {
+ "title": "Email",
+ "question_type": "char_box",
+ "sequence": 1,
+ "validation_email": True,
+ "save_as_email": True,
+ "constr_mandatory": True,
+ "survey_id": survey_id,
+ }
+ )
+
+ def _create_firstname_question(self, survey_id):
+ self.create(
+ {
+ "title": "Prénom",
+ "question_type": "char_box",
+ "sequence": 2,
+ "save_as_firstname": True,
+ "constr_mandatory": True,
+ "survey_id": survey_id,
+ }
+ )
+
+ def _create_lastname_question(self, survey_id):
+ self.create(
+ {
+ "title": "Nom",
+ "question_type": "char_box",
+ "sequence": 3,
+ "save_as_nickname": True,
+ "constr_mandatory": True,
+ "survey_id": survey_id,
+ }
+ )
+
+ def _create_company_question(self, survey_id):
+ self.create(
+ {
+ "title": "Structure",
+ "question_type": "char_box",
+ "sequence": 4,
+ "save_as_company": True,
+ "constr_mandatory": True,
+ "survey_id": survey_id,
+ }
+ )
diff --git a/models/survey_question_answer.py b/models/survey_question_answer.py
new file mode 100644
index 0000000000000000000000000000000000000000..6719546a10081e948494f1e0e5c41bfe3a4948c4
--- /dev/null
+++ b/models/survey_question_answer.py
@@ -0,0 +1,22 @@
+# Copyright 2024 Le Filament (<https://le-filament.com>)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo import fields, models
+
+
+class SurveyQuestionAnswer(models.Model):
+ _inherit = "survey.question.answer"
+
+ answer_weight = fields.Integer("Pondération")
+
+ # ------------------------------------------------------
+ # Inherit parent
+ # ------------------------------------------------------
+
+ # ------------------------------------------------------
+ # Onchange methods
+ # ------------------------------------------------------
+
+ # ------------------------------------------------------
+ # Business function
+ # ------------------------------------------------------
diff --git a/models/survey_user_input.py b/models/survey_user_input.py
index bbf109ea971d0ad4336cbf7707390929765f7eff..5d91eb1b71efd28a26fe01f13f9e250e97f65c34 100644
--- a/models/survey_user_input.py
+++ b/models/survey_user_input.py
@@ -1,12 +1,16 @@
# Copyright 2019-2022 Le Filament (<https://le-filament.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-from odoo import fields, models, api
+from odoo import Command, fields, models
class SurveyUserInput(models.Model):
_inherit = "survey.user_input"
+ firstname = fields.Char("Prénom")
+ company = fields.Char("Structure")
+ is_duplicate_answer = fields.Boolean(default=False)
+ is_resent = fields.Boolean(default=False)
student_id = fields.Many2one(
comodel_name="training.student",
string="Stagiaire",
@@ -26,45 +30,149 @@ class SurveyUserInput(models.Model):
# ------------------------------------------------------
# Inherit parent
# ------------------------------------------------------
+ def save_lines(self, question, answer, comment=None):
+ """
+ Hérite la fonction parente pour vérifier :
+ - enregistrer le prénom si il est spécifié
+ - enregistrer l'entreprise si elle est spécifiée
+ """
+ super().save_lines(question, answer, comment)
+
+ if question.save_as_firstname and answer:
+ self.write({"firstname": answer})
+ if question.save_as_company and answer:
+ self.write({"company": answer})
- # ------------------------------------------------------
- # Override ORM
- # ------------------------------------------------------
def _mark_done(self):
"""
- Hérite la fonction parente pour gérer les inscriptions aux formations au moment
- de la validation du questionnaire
+ Hérite la fonction parente, à la validation du questionnaire, et en fonction des
+ options définies sur le sondage pour :
+ - supprimer les réponses en cours si l'utilisateur a déjà répondu
+ - renvoyer une erreur si il a déjà répondu et qu'une seule réponse est possible
+ - rattacher le contact à la réponse si il existe
+ - créer le contact et le rattacher à la réponse
+ - gérer les inscriptions aux formations au moment de la validation
+ du questionnaire d'inscription
"""
- super()._mark_done()
+ # Gestion des doublons de réponse
+ if self.survey_id.is_one_answer and self.email:
+ input_ids = self.survey_id.sudo().user_input_ids.filtered(
+ lambda user_input: user_input.email == self.email
+ )
+ old_input_ids = input_ids - self
+ if old_input_ids:
+ if old_input_ids.filtered(lambda i: i.state == "done"):
+ self.write({"is_duplicate_answer": True})
+ else:
+ old_input_ids.unlink()
- student_model = self.env["training.student"].sudo()
+ # Création du contact
+ if self.survey_id.is_partner_create and self.email and not self.partner_id:
+ partner_id = self._check_partner()
+ if partner_id:
+ self.write({"partner_id": partner_id.id})
+ elif self.survey_id.is_partner_create:
+ partner_id = self._create_partner()
+ self.write({"partner_id": partner_id.id})
- student_id = student_model.create(
- {
- "partner_id": self.partner_id.id,
- "training_id": self.training_id.id,
- "student_company": self.company,
- }
- )
- # Création de la réponse au sondage et inscription
- self.create(
- {
- "partner_id": self.partner_id.id,
- "email": self.partner_id.email,
- "nickname": self.partner_id.lastname,
- "firstname": self.partner_id.firstname,
- "company": self.company,
- "survey_id": self.training_id.registration_survey_id.id,
- "student_id": student_id.id,
- "training_id": self.training_id.id,
- }
- )
+ super()._mark_done()
+ # Pré-inscription du stagiaire
+ if (
+ self.survey_id.survey_type == "training"
+ and self.survey_id.training_survey_type == "subscribe"
+ and self.training_id
+ ):
+ student_model = self.env["training.student"].sudo()
+ student_id = student_model.create(
+ {
+ "partner_id": self.partner_id.id if self.partner_id else None,
+ "training_id": self.training_id.id,
+ "student_company": self.company,
+ "student_firstname": self.firstname,
+ "student_lastname": self.nickname,
+ "email": self.email,
+ }
+ )
+ # Création de la réponse au sondage et inscription
+ self.create(
+ {
+ "partner_id": self.partner_id.id,
+ "email": self.partner_id.email,
+ "nickname": self.nickname,
+ "firstname": self.firstname,
+ "company": self.company,
+ "survey_id": self.training_id.registration_survey_id.id,
+ "student_id": student_id.id,
+ "training_id": self.training_id.id,
+ }
+ )
# ------------------------------------------------------
# Compute
# ------------------------------------------------------
# ------------------------------------------------------
- # Buttons
+ # Actions
+ # ------------------------------------------------------
+ def resend_survey(self):
+ self.ensure_one()
+ self.update({"state": "in_progress", "is_resent": True})
+
+ # ------------------------------------------------------
+ # Business method
# ------------------------------------------------------
+ def _check_partner(self):
+ """
+ Recherche ou crée le contact associé à la réponse
+ """
+ partner_id = (
+ self.env["res.partner"].sudo().search([("email", "=", self.email)], limit=1)
+ )
+ return partner_id
+
+ def _create_partner(self):
+ new_partner_id = (
+ self.env["res.partner"]
+ .sudo()
+ .create(
+ {
+ "firstname": self.firstname or "",
+ "lastname": self.nickname or "",
+ "email": self.email,
+ "is_company": False,
+ }
+ )
+ )
+ return new_partner_id
+
+ def _create_input_line(self, question):
+ """
+ Crée automatiquement la ligne de réponse à la question si elle est contenue dans
+ la réponse :
+ :params survey : survey.survey
+ :params answer : survey.user_input
+ :params str question: nom du champ de user_input
+ """
+ self.ensure_one()
+ question_field = (
+ "validation_email" if question == "email" else f"save_as_{question}"
+ )
+ question_id = self.survey_id.question_ids.filtered(
+ lambda q: getattr(q, question_field)
+ )
+ if question_id:
+ self.update(
+ {
+ "user_input_line_ids": [
+ Command.create(
+ {
+ "user_input_id": self.id,
+ "question_id": question_id.id,
+ "answer_type": "char_box",
+ "value_char_box": getattr(self, question),
+ }
+ )
+ ]
+ }
+ )
diff --git a/models/training_program.py b/models/training_program.py
index a61772b7c0d7562d30398d7b912a2cd63199f37b..9de4c5e899e1e5af04e667e801a84b4f962c87ed 100644
--- a/models/training_program.py
+++ b/models/training_program.py
@@ -15,7 +15,7 @@ class TrainingProgram(models.Model):
("survey_type", "=", "training"),
("training_survey_type", "=", "subscribe"),
],
- ondelete="restrict",
+ ondelete="set null",
)
aeci_survey_id = fields.Many2one(
comodel_name="survey.survey",
@@ -24,7 +24,7 @@ class TrainingProgram(models.Model):
("survey_type", "=", "training"),
("training_survey_type", "=", "aeci"),
],
- ondelete="restrict",
+ ondelete="set null",
)
aect_survey_id = fields.Many2one(
comodel_name="survey.survey",
@@ -33,13 +33,46 @@ class TrainingProgram(models.Model):
("survey_type", "=", "training"),
("training_survey_type", "=", "aect"),
],
- ondelete="restrict",
+ ondelete="set null",
)
# ------------------------------------------------------
# Compute
# ------------------------------------------------------
+ # ------------------------------------------------------
+ # Actions
+ # ------------------------------------------------------
+ def action_create_registration_survey(self):
+ self.ensure_one()
+ self.registration_survey_id = self.env["survey.survey"]._create_training_survey("subscribe")
+ return {
+ "type": "ir.actions.act_window",
+ "view_mode": "form",
+ "res_model": "survey.survey",
+ "res_id": self.registration_survey_id.id,
+ }
+
+ def action_create_aeci_survey(self):
+ self.ensure_one()
+ self.aeci_survey_id = self.env["survey.survey"]._create_training_survey("aeci")
+ return {
+ "type": "ir.actions.act_window",
+ "view_mode": "form",
+ "res_model": "survey.survey",
+ "res_id": self.aeci_survey_id.id,
+ }
+
+ def action_create_aect_survey(self):
+ self.ensure_one()
+ self.aect_survey_id = self.env["survey.survey"]._create_training_survey("aect")
+ return {
+ "type": "ir.actions.act_window",
+ "view_mode": "form",
+ "res_model": "survey.survey",
+ "res_id": self.aect_survey_id.id,
+ }
+
# ------------------------------------------------------
# Inherit parent
# ------------------------------------------------------
@@ -48,7 +81,11 @@ class TrainingProgram(models.Model):
raise UserError(
_(
"Le questionnaire de pré-inscription/positionnement "
- "doivent être renseignés."
+ "doit être renseigné."
)
)
super(TrainingProgram, self).action_valid()
+
+ # ------------------------------------------------------
+ # Business Methods
+ # ------------------------------------------------------
diff --git a/models/training_student.py b/models/training_student.py
index dabc563c4d855fe4d8f9eff350e1ccb3d492a0a5..737659c9063dc7c94144716f534467e4192d9fff 100644
--- a/models/training_student.py
+++ b/models/training_student.py
@@ -15,7 +15,7 @@ class TrainingStudent(models.Model):
)
registration_survey_id = fields.Many2one(
comodel_name="survey.user_input",
- string="Inscription/Positionnement",
+ string="Pré-inscription/Positionnement",
compute="_compute_registration_survey",
)
registration_state = fields.Selection(
@@ -67,7 +67,7 @@ class TrainingStudent(models.Model):
def action_view_registration(self):
self.ensure_one()
return {
- "name": f"Questionnaire Inscription {self.partner_id.name}",
+ "name": f"Questionnaire de pré-inscription {self.partner_id.name}",
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "survey.user_input",
@@ -76,10 +76,26 @@ class TrainingStudent(models.Model):
}
def action_create_aeci(self):
+ if not self.training_id.company_id.training_user_contact:
+ raise UserError(_(
+ "Le contact de référence de la structure de formation n'est pas configuré."
+ ))
aeci_template_id = self.env.ref("training_survey.mail_template_training_aeci")
+ training_internal_regulation = self.env["ir.attachment"].sudo().search([
+ ("res_model", "=", "res.company"),
+ ("res_field", "=", "training_internal_regulation"),
+ ("res_id", "=", self.training_id.company_id.id)
+ ])
+ # Ajoute le règlement intérieur au template si il existe
+ if training_internal_regulation:
+ regulation_copy = training_internal_regulation.copy({"name": "Règlement intérieur"})
+ aeci_template_id.attachment_ids = [(4, regulation_copy.id)]
self._create_and_send_survey(
self.training_id.program_id.aeci_survey_id, aeci_template_id
)
+ # Supprime le règlement intérieur au template
+ if training_internal_regulation:
+ aeci_template_id.attachment_ids = [(5, 0, 0)]
# ------------------------------------------------------
# Business methods
@@ -131,10 +147,10 @@ class TrainingStudent(models.Model):
"""
answer_id = self.student_survey_ids.create(
{
- "partner_id": self.partner_id.id,
- "email": self.partner_id.email,
- "nickname": self.partner_id.lastname,
- "firstname": self.partner_id.firstname,
+ "partner_id": self.partner_id.id or False,
+ "email": self.partner_id.email or self.email,
+ "nickname": self.partner_id.lastname or self.student_lastname,
+ "firstname": self.partner_id.firstname or self.student_firstname,
"company": self.student_company,
"survey_id": survey_id.id,
"student_id": self.id,
@@ -146,5 +162,4 @@ class TrainingStudent(models.Model):
answer_id._create_input_line("firstname")
answer_id._create_input_line("nickname")
answer_id._create_input_line("company")
-
return answer_id
diff --git a/templates/survey_duplicated_answer.xml b/templates/survey_duplicated_answer.xml
new file mode 100644
index 0000000000000000000000000000000000000000..94defd23756b7ed8745c1963b8b6d740799112d2
--- /dev/null
+++ b/templates/survey_duplicated_answer.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+ <template id="survey_duplicated_answer" name="Survey: Duplicated Answer">
+ <div class="wrap">
+ <div class="container">
+ <div class="fs-3 fw-light">
+ Vous avez déjà répondu à ce questionnaire, cette nouvelle réponse ne sera pas prise en compte.
+ </div>
+ </div>
+ </div>
+ </template>
+</odoo>
diff --git a/templates/survey_template_management.xml b/templates/survey_template_management.xml
new file mode 100644
index 0000000000000000000000000000000000000000..97acd79f31792a04e2f5c2cfaf50fbf0ce69dec0
--- /dev/null
+++ b/templates/survey_template_management.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+ <template
+ id="survey_button_retake_conditional"
+ inherit_id="survey.survey_button_retake"
+ name="Survey: conditional retake button"
+ >
+ <xpath expr="//t[@t-else]" position="before">
+ <t t-elif="not answer.survey_id.is_take_again">
+ </t>
+ </xpath>
+ </template>
+</odoo>
diff --git a/views/survey.xml b/views/survey.xml
index 1a6cd7253e009575e585a35c403b2d520cf88303..198e3e25579b9c472d36b36fac8d826a8b7fe6fb 100644
--- a/views/survey.xml
+++ b/views/survey.xml
@@ -8,6 +8,7 @@
<field name="model">survey.survey</field>
<field name="inherit_id" ref="survey.survey_survey_view_form" />
<field name="arch" type="xml">
+ <!-- Survey Type -->
<xpath expr="//field[@name='user_id']" position="before">
<field name="survey_type" invisible="1" />
<field
@@ -15,6 +16,20 @@
attrs="{'invisible': [('survey_type', '!=', 'training')], 'required': [('survey_type', '=', 'training')]}"
/>
</xpath>
+ <!-- Survey options -->
+ <xpath expr="//group/group" position="after">
+ <group name="survey_options">
+ <field name="is_take_again" widget="boolean_toggle" />
+ <field name="is_one_answer" widget="boolean_toggle" />
+ <field name="authorized_domain" placeholder="scop.coop,scop.fr" />
+ <field name="is_partner_check" />
+ <field
+ name="is_partner_create"
+ attrs="{'invisible': [('is_partner_check', '!=', True)]}"
+ />
+ </group>
+ </xpath>
+ <!-- Training and programs -->
<xpath expr="//notebook" position="inside">
<page
name="trainings"
@@ -25,7 +40,7 @@
</page>
<page
name="programs"
- strong="Programmes"
+ string="Programmes"
attrs="{'invisible': ['|', ('survey_type', '!=', 'training'), ('training_survey_type', 'not in', ['aeci', 'aect', 'subscribe']), ]}"
>
<field name="program_ids" />
diff --git a/views/survey_question.xml b/views/survey_question.xml
new file mode 100644
index 0000000000000000000000000000000000000000..92a235c0f490f21628bdf884d5bbf71df104646e
--- /dev/null
+++ b/views/survey_question.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+ <!-- Form View -->
+ <record id="survey_question_scop_form" model="ir.ui.view">
+ <field name="name">survey.question.scop.form</field>
+ <field name="model">survey.question</field>
+ <field name="inherit_id" ref="survey.survey_question_form" />
+ <field name="priority">20</field>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='save_as_nickname']" position="after">
+ <field
+ name="save_as_firstname"
+ attrs="{'invisible': [('question_type', '!=', 'char_box')]}"
+ />
+ <field
+ name="save_as_company"
+ attrs="{'invisible': [('question_type', '!=', 'char_box')]}"
+ />
+ </xpath>
+
+ <xpath
+ expr="//field[@name='suggested_answer_ids']/tree/field[@name='value']"
+ position="after"
+ >
+ <field name="answer_weight" />
+ </xpath>
+ </field>
+ </record>
+</odoo>
diff --git a/views/survey_user_input.xml b/views/survey_user_input.xml
index eaf4e3e3452fafd1b0ca4ef5597e8eba7d1122b1..79b5df77b87a6972f56e207d475cd59e28f5586c 100644
--- a/views/survey_user_input.xml
+++ b/views/survey_user_input.xml
@@ -7,6 +7,23 @@
<field name="inherit_id" ref="survey.survey_user_input_view_form" />
<field name="priority">60</field>
<field name="arch" type="xml">
+ <xpath expr="//sheet/group/group" position="inside">
+ <field name="is_resent" invisible="1" />
+ <div colspan="2">
+ <button
+ name="resend_survey"
+ type="object"
+ string="Renvoyer le questionnaire"
+ attrs="{'invisible': [('state', '!=', 'done')]}"
+ confirm="Cette action va modifier le statut de la réponse à En cours et générer un lien à transmettre à l'utilisateur pour qu'il puisse modifier ses réponses."
+ />
+ </div>
+ </xpath>
+ <xpath expr="//field[@name='partner_id']" position="after">
+ <field name="company" />
+ <field name="nickname" groups="base.group_no_one" />
+ <field name="firstname" groups="base.group_no_one" />
+ </xpath>
<xpath
expr="//sheet/group/group/field[@name='test_entry']"
position="after"
@@ -24,7 +41,7 @@
</field>
</record>
- <!-- Form View -->
+ <!-- Kanban View -->
<record id="survey_user_input_training_kanban" model="ir.ui.view">
<field name="name">survey.user_input.training.kanban</field>
<field name="model">survey.user_input</field>
diff --git a/views/training.xml b/views/training.xml
index 5517bf3f121767eb75e1331985cb92ff10819ba4..c423cdda8ed41b2068de9d4a4d7780316479ec25 100644
--- a/views/training.xml
+++ b/views/training.xml
@@ -31,7 +31,7 @@
<xpath expr="//notebook" position="inside">
<page string="Questionnaires" name="survey">
<p class="o_horizontal_separator">
- Inscription/Positionnement
+ Pré-inscription/Positionnement
</p>
<p>
<field name="registration_survey_id" class="me-4" />
diff --git a/views/training_program.xml b/views/training_program.xml
index 25cad67bad013bd0e9253bea2021e1ed5dbe5051..2144cd2417c406a6dcbad989887d00518cf1ee88 100644
--- a/views/training_program.xml
+++ b/views/training_program.xml
@@ -10,20 +10,59 @@
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Questionnaires" name="survey">
- <group>
+ <p class="o_horizontal_separator">
+ Pré-inscription/Positionnement
+ </p>
+ <p class="mb-4">
<field
name="registration_survey_id"
- options="{'no_create': 1, 'no_edit': 1}"
+ options="{'no_create': 1}"
+ class="me-4 col-12 col-md-6"
/>
+ <button
+ name="action_create_registration_survey"
+ type="object"
+ class="btn-outline-primary"
+ string="Créer un nouveau questionnaire de Pré-inscription"
+ attrs="{'invisible': [('registration_survey_id', '!=', False)]}"
+ />
+
+ </p>
+ <p class="o_horizontal_separator">
+ AECI
+ </p>
+ <p class="mb-4">
<field
name="aeci_survey_id"
- options="{'no_create': 1, 'no_edit': 1}"
+ class="me-4 col-12 col-md-6"
+ options="{'no_create': 1}"
+ />
+ <button
+ name="action_create_aeci_survey"
+ type="object"
+ class="btn-outline-primary"
+ string="Créer un nouveau questionnaire AECI"
+ attrs="{'invisible': [('registration_survey_id', '!=', False)]}"
/>
+ </p>
+
+ <p class="o_horizontal_separator">
+ AECT
+ </p>
+ <p class="mb-4">
<field
name="aect_survey_id"
- options="{'no_create': 1, 'no_edit': 1}"
+ class="me-4 col-12 col-md-6"
+ options="{'no_create': 1}"
+ />
+ <button
+ name="action_create_aect_survey"
+ type="object"
+ class="btn-outline-primary"
+ string="Créer un nouveau questionnaire AECT"
+ attrs="{'invisible': [('registration_survey_id', '!=', False)]}"
/>
- </group>
+ </p>
</page>
</xpath>
</field>