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>