1 Commits

Author SHA1 Message Date
a08c3ec18c [IMP]survey_record_generation:show alerte when deleting a crm tag used in lead record creation
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m42s
2026-04-28 19:23:45 +02:00
142 changed files with 3147 additions and 1251 deletions

View File

@@ -3,7 +3,7 @@ name: pre-commit
on:
pull_request:
branches:
- "18.0*"
- "16.0*"
jobs:
pre-commit:

View File

@@ -10,7 +10,7 @@ manifest-required-authors=Elabore
manifest-required-keys=license
manifest-deprecated-keys=description,active
license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
valid-odoo-versions=18.0
valid-odoo-versions=16.0
[MESSAGES CONTROL]
disable=all

View File

@@ -9,7 +9,7 @@ manifest-required-authors=Elabore
manifest-required-keys=license
manifest-deprecated-keys=description,active
license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
valid-odoo-versions=18.0
valid-odoo-versions=16.0
[MESSAGES CONTROL]
disable=all

View File

@@ -13,7 +13,7 @@ Add fields used by several survey addons
* Implementation of theses fields should be in another module
""",
"version": "18.0.1.0.0",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Elabore",
"website": "https://www.elabore.coop",

View File

@@ -0,0 +1,109 @@
==========================
Survey contacts generation
==========================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:0a298d1600a5f93ffe77357631c7e799e78b23b84c362b126720e36655dd5227
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsurvey-lightgray.png?logo=github
:target: https://github.com/OCA/survey/tree/15.0/survey_contact_generation
:alt: OCA/survey
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/survey-15-0/survey-15-0-survey_contact_generation
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/survey&target_branch=15.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module allows to generate new contacts from surveys answers.
**Table of contents**
.. contents::
:local:
Configuration
=============
To configure the contact generation:
#. Go to the configured survey.
#. In the *Contact* section of the *Options* tab, set
*Generate Contact* on, if you want contacts to be
generated from the answers to this survey.
#. In each question associated with a future new contact,
specify the corresponding contact field. To do this,
go to the 'Options' tab, then navigate to the 'Contact' group,
and select the 'Contact field' field.
Usage
=====
if the survey is properly configured, once it is submited
by an anonomous user, a new contact is create or an
existing one is linked.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/survey/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/survey/issues/new?body=module:%20survey_contact_generation%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Tecnativa
Contributors
~~~~~~~~~~~~
* `Tecnativa <https://www.tecnativa.com>`_
* David Vidal
* Ernesto Tejeda
* Stefan Ungureanu
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-chienandalu| image:: https://github.com/chienandalu.png?size=40px
:target: https://github.com/chienandalu
:alt: chienandalu
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-chienandalu|
This module is part of the `OCA/survey <https://github.com/OCA/survey/tree/15.0/survey_contact_generation>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

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

View File

@@ -0,0 +1,25 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Survey contacts generation",
"summary": "Generate new contacts from surveys",
"version": "16.0.1.0.0",
"development_status": "Beta",
"category": "Marketing/Survey",
"website": "https://github.com/OCA/survey",
"author": "Tecnativa, Odoo Community Association (OCA)",
"maintainers": ["clement_thomas"],
"license": "AGPL-3",
"depends": ["survey","partner_firstname"],
"data": [
'security/ir.model.access.csv',
"views/survey_question_views.xml",
"views/survey_survey_views.xml",
],
"demo": ["demo/survey_contact_generation_demo.xml"],
"assets": {
"web.assets_tests": [
"/survey_contact_generation/static/tests/survey_contact_generation_tour.js",
],
},
}

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Survey-->
<record model="survey.survey" id="survey_contact_creation">
<field name="title">Contact Creation Survey</field>
<field name="access_token">80e5f1e2-1a9d-4c51-8e23-09e93f7fa2c5</field>
<field name="access_mode">public</field>
<field name="users_can_go_back" eval="True" />
<field name="generate_contact" eval="True" />
</record>
<record model="survey.question" id="survey_contact_q0">
<field name="survey_id" ref="survey_contact_creation" />
<field name="sequence">0</field>
<field name="title">Name</field>
<field name="question_type">char_box</field>
<field name="constr_mandatory" eval="True" />
<field name="res_partner_field" ref="base.field_res_partner__name" />
</record>
<record model="survey.question" id="survey_contact_q1">
<field name="survey_id" ref="survey_contact_creation" />
<field name="sequence">1</field>
<field name="title">Email</field>
<field name="question_type">char_box</field>
<field name="res_partner_field" ref="base.field_res_partner__email" />
</record>
<record model="survey.question" id="survey_contact_q2">
<field name="survey_id" ref="survey_contact_creation" />
<field name="sequence">2</field>
<field name="title">Notes</field>
<field name="question_type">text_box</field>
<field name="res_partner_field" ref="base.field_res_partner__comment" />
</record>
<record model="survey.question" id="survey_contact_q3">
<field name="survey_id" ref="survey_contact_creation" />
<field name="sequence">3</field>
<field name="title">Color</field>
<field name="question_type">numerical_box</field>
<field name="res_partner_field" ref="base.field_res_partner__color" />
</record>
<record model="survey.question" id="survey_contact_q4">
<field name="survey_id" ref="survey_contact_creation" />
<field name="sequence">4</field>
<field name="title">Date</field>
<field name="question_type">date</field>
<field name="res_partner_field" ref="base.field_res_partner__date" />
</record>
<record model="survey.question" id="survey_contact_q5">
<field name="survey_id" ref="survey_contact_creation" />
<field name="sequence">4</field>
<field name="title">Country</field>
<field name="question_type">simple_choice</field>
<field name="res_partner_field" ref="base.field_res_partner__country_id" />
</record>
<record model="survey.question.answer" id="survey_contact_q5_sug1">
<field name="question_id" ref="survey_contact_q5" />
<field name="sequence">1</field>
<field name="value">Spain</field>
<field name="res_partner_field_resource_ref" ref="base.es" />
</record>
<record model="survey.question.answer" id="survey_contact_q5_sug2">
<field name="question_id" ref="survey_contact_q5" />
<field name="sequence">2</field>
<field name="value">Romania</field>
<field name="res_partner_field_resource_ref" ref="base.ro" />
</record>
<record model="survey.question" id="survey_contact_q6">
<field name="survey_id" ref="survey_contact_creation" />
<field name="sequence">4</field>
<field name="title">Tags</field>
<field name="question_type">multiple_choice</field>
<field name="res_partner_field" ref="base.field_res_partner__category_id" />
</record>
<record model="survey.question.answer" id="survey_contact_q6_sug1">
<field name="question_id" ref="survey_contact_q6" />
<field name="sequence">1</field>
<field name="value">Vendor</field>
<field
name="res_partner_field_resource_ref"
ref="base.res_partner_category_0"
/>
</record>
<record model="survey.question.answer" id="survey_contact_q6_sug2">
<field name="question_id" ref="survey_contact_q6" />
<field name="sequence">2</field>
<field name="value">Prospects</field>
<field
name="res_partner_field_resource_ref"
ref="base.res_partner_category_2"
/>
</record>
<record model="survey.question.answer" id="survey_contact_q6_sug3">
<field name="question_id" ref="survey_contact_q6" />
<field name="sequence">3</field>
<field name="value">Employees</field>
<field
name="res_partner_field_resource_ref"
ref="base.res_partner_category_3"
/>
</record>
</odoo>

View File

@@ -0,0 +1,168 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * survey_contact_generation
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-14 11:55+0000\n"
"PO-Revision-Date: 2023-06-14 13:58+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: \n"
"X-Generator: Poedit 3.0.1\n"
#. module: survey_contact_generation
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_question__allowed_field_ids
msgid "Allowed Field"
msgstr "Campo permitido"
#. module: survey_contact_generation
#: model:survey.question,title:survey_contact_generation.survey_contact_q3
msgid "Color"
msgstr "Color"
#. module: survey_contact_generation
#: model_terms:ir.ui.view,arch_db:survey_contact_generation.survey_form
#: model_terms:ir.ui.view,arch_db:survey_contact_generation.survey_question_form
msgid "Contact"
msgstr "Contacto"
#. module: survey_contact_generation
#: model:survey.survey,title:survey_contact_generation.survey_contact_creation
msgid "Contact Creation Survey"
msgstr "Encuesta de creación de contactos"
#. module: survey_contact_generation
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_question__res_partner_field
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_question_answer__res_partner_field
msgid "Contact field"
msgstr "Campo de contacto"
#. module: survey_contact_generation
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_question_answer__res_partner_field_resource_ref
msgid "Contact field value"
msgstr ""
#. module: survey_contact_generation
#: model:survey.question,title:survey_contact_generation.survey_contact_q5
msgid "Country"
msgstr ""
#. module: survey_contact_generation
#: model:survey.question,title:survey_contact_generation.survey_contact_q4
msgid "Date"
msgstr "Fecha"
#. module: survey_contact_generation
#: model:survey.question,title:survey_contact_generation.survey_contact_q1
msgid "Email"
msgstr "Correo electrónico"
#. module: survey_contact_generation
#: model:survey.question.answer,value:survey_contact_generation.survey_contact_q6_sug3
msgid "Employees"
msgstr ""
#. module: survey_contact_generation
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_survey__generate_contact
msgid "Generate Contact"
msgstr "Generar contacto"
#. module: survey_contact_generation
#: model:ir.model.fields,help:survey_contact_generation.field_survey_survey__generate_contact
msgid "Generate contacts for anonymous survey users"
msgstr "Generar contacto para encuestas anónimas"
#. module: survey_contact_generation
#: model:survey.question,comments_message:survey_contact_generation.survey_contact_q0
#: model:survey.question,comments_message:survey_contact_generation.survey_contact_q1
#: model:survey.question,comments_message:survey_contact_generation.survey_contact_q2
#: model:survey.question,comments_message:survey_contact_generation.survey_contact_q3
#: model:survey.question,comments_message:survey_contact_generation.survey_contact_q4
#: model:survey.question,comments_message:survey_contact_generation.survey_contact_q5
#: model:survey.question,comments_message:survey_contact_generation.survey_contact_q6
msgid "If other, please specify:"
msgstr "Si es otro, especifíquelo:"
#. module: survey_contact_generation
#: model:survey.question,title:survey_contact_generation.survey_contact_q0
msgid "Name"
msgstr "Nombre"
#. module: survey_contact_generation
#: model:survey.question,title:survey_contact_generation.survey_contact_q2
msgid "Notes"
msgstr "Notas"
#. module: survey_contact_generation
#: model:survey.question.answer,value:survey_contact_generation.survey_contact_q6_sug2
msgid "Prospects"
msgstr ""
#. module: survey_contact_generation
#: model:survey.question.answer,value:survey_contact_generation.survey_contact_q5_sug2
msgid "Romania"
msgstr ""
#. module: survey_contact_generation
#: model:survey.question.answer,value:survey_contact_generation.survey_contact_q5_sug1
msgid "Spain"
msgstr ""
#. module: survey_contact_generation
#: model:ir.model,name:survey_contact_generation.model_survey_survey
msgid "Survey"
msgstr "Planificación"
#. module: survey_contact_generation
#: model:ir.model,name:survey_contact_generation.model_survey_question_answer
msgid "Survey Label"
msgstr ""
#. module: survey_contact_generation
#: model:ir.model,name:survey_contact_generation.model_survey_question
msgid "Survey Question"
msgstr "Pregunta de la encuesta"
#. module: survey_contact_generation
#: model:ir.model,name:survey_contact_generation.model_survey_user_input
msgid "Survey User Input"
msgstr "Entrada de usuario de la encuesta"
#. module: survey_contact_generation
#: model:survey.question,title:survey_contact_generation.survey_contact_q6
msgid "Tags"
msgstr ""
#. module: survey_contact_generation
#: model:survey.question,validation_error_msg:survey_contact_generation.survey_contact_q0
#: model:survey.question,validation_error_msg:survey_contact_generation.survey_contact_q1
#: model:survey.question,validation_error_msg:survey_contact_generation.survey_contact_q2
#: model:survey.question,validation_error_msg:survey_contact_generation.survey_contact_q3
#: model:survey.question,validation_error_msg:survey_contact_generation.survey_contact_q4
#: model:survey.question,validation_error_msg:survey_contact_generation.survey_contact_q5
#: model:survey.question,validation_error_msg:survey_contact_generation.survey_contact_q6
msgid "The answer you entered is not valid."
msgstr "La respuesta que has introducido no es válida."
#. module: survey_contact_generation
#: model:survey.question,constr_error_msg:survey_contact_generation.survey_contact_q0
#: model:survey.question,constr_error_msg:survey_contact_generation.survey_contact_q1
#: model:survey.question,constr_error_msg:survey_contact_generation.survey_contact_q2
#: model:survey.question,constr_error_msg:survey_contact_generation.survey_contact_q3
#: model:survey.question,constr_error_msg:survey_contact_generation.survey_contact_q4
#: model:survey.question,constr_error_msg:survey_contact_generation.survey_contact_q5
#: model:survey.question,constr_error_msg:survey_contact_generation.survey_contact_q6
msgid "This question requires an answer."
msgstr "Esta pregunta requiere una respuesta."
#. module: survey_contact_generation
#: model:survey.question.answer,value:survey_contact_generation.survey_contact_q6_sug1
msgid "Vendor"
msgstr ""

View File

@@ -0,0 +1,64 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * survey_contact_generation
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-19 14:33+0000\n"
"PO-Revision-Date: 2023-09-19 14:33+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: survey_contact_generation
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_question__allowed_field_ids
msgid "Allowed Field"
msgstr "Champs autorisés"
#. module: survey_contact_generation
#: model_terms:ir.ui.view,arch_db:survey_contact_generation.survey_form
#: model_terms:ir.ui.view,arch_db:survey_contact_generation.survey_question_form
msgid "Contact"
msgstr ""
#. module: survey_contact_generation
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_question__res_partner_field
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_question_answer__res_partner_field
msgid "Contact field"
msgstr "Champ contact"
#. module: survey_contact_generation
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_question_answer__res_partner_field_resource_ref
msgid "Contact field value"
msgstr "Valeur du champ contact"
#. module: survey_contact_generation
#: model:ir.model.fields,field_description:survey_contact_generation.field_survey_survey__generate_contact
msgid "Generate Contact"
msgstr "Générer un contact"
#. module: survey_contact_generation
#: model:ir.model.fields,help:survey_contact_generation.field_survey_survey__generate_contact
msgid "Generate contacts for anonymous survey users"
msgstr "Générer des contacts pour les questionnaires anonymes"
#. module: survey_contact_generation
#: model:ir.model,name:survey_contact_generation.model_survey_question_answer
msgid "Survey Label"
msgstr "Étiquette du questionnaire"
#. module: survey_contact_generation
#: model:ir.model,name:survey_contact_generation.model_survey_question
msgid "Survey Question"
msgstr "Question du questionnaire"
#. module: survey_contact_generation
#: model:ir.model,name:survey_contact_generation.model_survey_user_input
msgid "Survey User Input"
msgstr "Entrée utilisateur du questionnaire"

View File

@@ -0,0 +1,3 @@
from . import survey_question
from . import survey_survey
from . import survey_user_input

View File

@@ -0,0 +1,81 @@
# Copyright 2022 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
type_mapping = {
"char_box": ["char", "text"],
"text_box": ["html"],
"numerical_box": ["integer", "float", "html", "char"],
"date": ["date", "text", "char"],
"datetime": ["datetime", "html", "char"],
"simple_choice": ["many2one", "html", "char"],
"multiple_choice": ["many2many", "html", "char"],
}
class SurveyQuestion(models.Model):
_inherit = "survey.question"
allowed_field_ids = fields.Many2many(
comodel_name="ir.model.fields",
compute="_compute_allowed_field_ids",
)
res_partner_field = fields.Many2one(
string="Contact field",
comodel_name="ir.model.fields",
domain="[('id', 'in', allowed_field_ids)]",
)
@api.depends("question_type")
def _compute_allowed_field_ids(self):
for record in self:
record.allowed_field_ids = (
self.env["ir.model.fields"]
.search(
[
("model", "=", "res.partner"),
("ttype", "in", type_mapping.get(record.question_type, [])),
]
)
.ids
)
class SurveyQuestionAnswer(models.Model):
_inherit = "survey.question.answer"
@api.model
def default_get(self, fields):
result = super().default_get(fields)
if (
not result.get("res_partner_field")
or "res_partner_field_resource_ref" not in fields
):
return result
partner_field = self.env["ir.model.fields"].browse(result["res_partner_field"])
# Otherwise we'll just use the value (char, text)
if partner_field.ttype not in {"many2one", "many2many"}:
return result
res = self.env[partner_field.relation].search([], limit=1)
if res:
result["res_partner_field_resource_ref"] = "%s,%s" % (
partner_field.relation,
res.id,
)
return result
@api.model
def _selection_res_partner_field_resource_ref(self):
return [(model.model, model.name) for model in self.env["ir.model"].search([])]
res_partner_field = fields.Many2one(related="question_id.res_partner_field")
res_partner_field_resource_ref = fields.Reference(
string="Contact field value",
selection="_selection_res_partner_field_resource_ref",
)
@api.onchange("res_partner_field_resource_ref")
def _onchange_res_partner_field_resource_ref(self):
"""Set the default value as the product name, although we can change it"""
if self.res_partner_field_resource_ref:
self.value = self.res_partner_field_resource_ref.display_name or ""

View File

@@ -0,0 +1,11 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class SurveySurvey(models.Model):
_inherit = "survey.survey"
generate_contact = fields.Boolean(
help="Generate contacts for anonymous survey users",
)

View File

@@ -0,0 +1,94 @@
# Copyright 2022 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
class SurveyUserInput(models.Model):
_inherit = "survey.user_input"
def _prepare_partner(self):
"""Extract partner values from the answers"""
self.ensure_one()
elegible_inputs = self.user_input_line_ids.filtered(
lambda x: x.question_id.res_partner_field and not x.skipped
)
basic_inputs = elegible_inputs.filtered(
lambda x: x.answer_type not in {"suggestion"}
and x.question_id.res_partner_field.name != "comment"
and not (x.question_id.comments_allowed and x.question_id.comment_count_as_answer and x.question_id.res_partner_field.ttype in ("many2one","many2many")) #exclude comments answer in case of many2one or many2many reference
)
vals = {
line.question_id.res_partner_field.name: line[f"value_{line.answer_type}"]
for line in basic_inputs
}
for line in elegible_inputs - basic_inputs:
field_name = line.question_id.res_partner_field.name
if line.question_id.res_partner_field.ttype == "many2one":
vals[
field_name
] = line.suggested_answer_id.res_partner_field_resource_ref.id
elif line.question_id.res_partner_field.ttype == "many2many":
vals.setdefault(field_name, [])
if line.suggested_answer_id: # exclude "comment" answer
vals[field_name] += [
(4, line.suggested_answer_id.res_partner_field_resource_ref.id)
]
# We'll use the comment field to add any other infos
elif field_name == "comment":
vals.setdefault("comment", "")
value = (
line.suggested_answer_id.value
if line.answer_type == "suggestion"
else line[f"value_{line.answer_type}"]
)
vals["comment"] += f"\n{line.question_id.title}: {value}"
else:
if line.question_id.question_type == "multiple_choice":
if not vals.get(field_name):
vals[field_name] = line.suggested_answer_id.value
else:
vals[field_name] += line.suggested_answer_id.value
else:
vals[field_name] = line.suggested_answer_id.value
return vals
def _create_contact_post_process(self, partner):
"""After creating the lead send an internal message with the input link"""
partner.message_post_with_view(
"mail.message_origin_link",
values={"self": partner, "origin": self.survey_id},
subtype_id=self.env.ref("mail.mt_note").id,
)
def _mark_done(self):
"""Generate the contact when the survey is submitted"""
for user_input in self.filtered(
lambda r: r.survey_id.generate_contact# and not self.partner_id #uncomment to avoid contact generation several times
):
vals = user_input._prepare_partner()
partner = False
email = vals.get("email")
firstname = vals.get("firstname")
lastname = vals.get("lastname")
# check doublon only if email send
if not email:
continue
#search if partner exists with same email, firstname and lastname depending on submitted data
doublon_domain = []
if email:
doublon_domain.append(("email", "ilike", email))
if firstname:
doublon_domain.append(("firstname", "ilike", firstname))
if lastname:
doublon_domain.append(("lastname", "ilike", lastname))
if doublon_domain:
partner = self.env["res.partner"].search(doublon_domain, limit=1)
if not partner:
partner = self.env["res.partner"].create(vals)
self._create_contact_post_process(partner)
self.update({"partner_id": partner.id, "email": partner.email})
return super()._mark_done()

View File

@@ -0,0 +1,10 @@
To configure the contact generation:
#. Go to the configured survey.
#. In the *Contact* section of the *Options* tab, set
*Generate Contact* on, if you want contacts to be
generated from the answers to this survey.
#. In each question associated with a future new contact,
specify the corresponding contact field. To do this,
go to the 'Options' tab, then navigate to the 'Contact' group,
and select the 'Contact field' field.

View File

@@ -0,0 +1,5 @@
* `Tecnativa <https://www.tecnativa.com>`_
* David Vidal
* Ernesto Tejeda
* Stefan Ungureanu

View File

@@ -0,0 +1 @@
This module allows to generate new contacts from surveys answers.

View File

@@ -0,0 +1,3 @@
if the survey is properly configured, once it is submited
by an anonomous user, a new contact is create or an
existing one is linked.

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ir_model_fields_survey_user,ir.model.fields.survey.user,base.model_ir_model_fields,survey.group_survey_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ir_model_fields_survey_user ir.model.fields.survey.user base.model_ir_model_fields survey.group_survey_user 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<title>Survey contacts generation</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
@@ -275,7 +275,7 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
@@ -301,7 +301,7 @@ span.option {
span.pre {
white-space: pre }
span.problematic, pre.problematic {
span.problematic {
color: red }
span.section-subtitle {
@@ -360,105 +360,89 @@ ul.auto-toc {
</style>
</head>
<body>
<div class="document">
<div class="document" id="survey-contacts-generation">
<h1 class="title">Survey contacts generation</h1>
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="survey-xlsx-expand-multiple-choice">
<h1>Survey XLSX - Expand Multiple Choice</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:2ec53fabb2863ebd536f204a7cb4fa4833a634a5711a9e325ed64f50a4c3c4b6
!! source digest: sha256:0a298d1600a5f93ffe77357631c7e799e78b23b84c362b126720e36655dd5227
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/elabore-coop/survey-tools/tree/18.0/survey_xlsx_expand_multiple_choice"><img alt="elabore-coop/survey-tools" src="https://img.shields.io/badge/github-elabore--coop%2Fsurvey--tools-lightgray.png?logo=github" /></a></p>
<p>This module improves the <strong>Survey Results XLSX export</strong> provided by
<tt class="docutils literal">survey_xlsx</tt> for questions that can hold several answers.</p>
<p>By default such questions are exported as a single column containing every
selected value joined together, which is hard to analyse in a spreadsheet.
This module splits them into dedicated columns:</p>
<ul class="simple">
<li><strong>Multiple choice</strong> questions (<em>multiple answers allowed</em>): one column per
possible answer, with <tt class="docutils literal">Oui</tt> / <tt class="docutils literal">Non</tt> as value.</li>
<li><strong>Matrix</strong> questions: one column per matrix row, with the selected option
as value.</li>
</ul>
<div class="admonition warning">
<p class="first admonition-title">Warning</p>
<p>This module relies on report extension hooks that are <strong>not part of the
standard</strong> <tt class="docutils literal">survey_xlsx</tt> yet. They are introduced by this pull request:</p>
<p><a class="reference external" href="https://github.com/elabore-coop/survey/pull/1">https://github.com/elabore-coop/survey/pull/1</a></p>
<p class="last">You must run a <tt class="docutils literal">survey_xlsx</tt> that includes these hooks (the PR branch,
until it is merged upstream). Installed against a plain <tt class="docutils literal">survey_xlsx</tt>,
this module installs without error but the export <strong>silently falls back</strong>
to the default one-column-per-question behaviour.</p>
</div>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/survey/tree/15.0/survey_contact_generation"><img alt="OCA/survey" src="https://img.shields.io/badge/github-OCA%2Fsurvey-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/survey-15-0/survey-15-0-survey_contact_generation"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/survey&amp;target_branch=15.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows to generate new contacts from surveys answers.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<p>Export the results of a survey as usual:</p>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>To configure the contact generation:</p>
<ol class="arabic simple">
<li>Go to <em>Surveys</em> and open a survey.</li>
<li>Print the <em>Survey Results XLSX</em> report.</li>
<li>Go to the configured survey.</li>
<li>In the <em>Contact</em> section of the <em>Options</em> tab, set
<em>Generate Contact</em> on, if you want contacts to be
generated from the answers to this survey.</li>
<li>In each question associated with a future new contact,
specify the corresponding contact field. To do this,
go to the Options tab, then navigate to the Contact group,
and select the Contact field field.</li>
</ol>
<p>In the generated spreadsheet:</p>
<ul class="simple">
<li>A multiple choice question <tt class="docutils literal">Favorite colors</tt> with options <em>Red</em>, <em>Green</em>
and <em>Blue</em> produces three columns <tt class="docutils literal">Favorite colors / Red</tt>,
<tt class="docutils literal">Favorite colors / Green</tt> and <tt class="docutils literal">Favorite colors / Blue</tt>, each containing
<tt class="docutils literal">Oui</tt> or <tt class="docutils literal">Non</tt>.</li>
<li>A matrix question <tt class="docutils literal">Satisfaction</tt> with rows <em>Dashboards</em> and <em>Customer
relationship</em> produces two columns <tt class="docutils literal">Satisfaction / Dashboards</tt> and
<tt class="docutils literal">Satisfaction / Customer relationship</tt>, each containing the selected
option (e.g. <em>Not satisfied at all</em>).</li>
</ul>
<p>Other question types keep their standard single-column export.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<p>if the survey is properly configured, once it is submited
by an anonomous user, a new contact is create or an
existing one is linked.</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/elabore-coop/survey-tools/issues">GitHub Issues</a>.
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/survey/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/elabore-coop/survey-tools/issues/new?body=module:%20survey_xlsx_expand_multiple_choice%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<a class="reference external" href="https://github.com/OCA/survey/issues/new?body=module:%20survey_contact_generation%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-3">Credits</a></h2>
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>Elabore</li>
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.elabore.coop">Elabore</a><ul>
<li>Quentin Mondot</li>
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a><ul>
<li>David Vidal</li>
<li>Ernesto Tejeda</li>
<li>Stefan Ungureanu</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h3>
<p>This module is part of the <a class="reference external" href="https://github.com/elabore-coop/survey-tools/tree/18.0/survey_xlsx_expand_multiple_choice">elabore-coop/survey-tools</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/chienandalu"><img alt="chienandalu" src="https://github.com/chienandalu.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/survey/tree/15.0/survey_contact_generation">OCA/survey</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,82 @@
odoo.define("survey.tour_test_survey_contact_generation", function (require) {
"use strict";
const tour = require("web_tour.tour");
tour.register(
"test_survey_contact_generation",
{
test: true,
url: "/survey/start/80e5f1e2-1a9d-4c51-8e23-09e93f7fa2c5",
},
[
{
content: "Click on Start",
trigger: "button.btn:contains('Start Survey')",
},
{
content: "Name",
trigger: "div.js_question-wrapper:contains('Name') input",
run: "text My Name",
},
{
content: "Email",
trigger: "div.js_question-wrapper:contains('Email') input",
run: "text survey_contact_generation@test.com",
},
{
content: "Notes",
trigger: "div.js_question-wrapper:contains('Notes') textarea",
run: "text This is a test note",
},
{
content: "Color",
trigger: "div.js_question-wrapper:contains('Color') input",
run: "text 1",
},
{
content: "Date",
trigger: "div.js_question-wrapper:contains('Date') input",
run: "text 01/01/2023",
},
{
content: "Country",
trigger:
"div.js_question-wrapper:contains('Country') label:contains('Romania') i",
run: function () {
$(
"div.js_question-wrapper:contains('Country') label:contains('Romania') i"
).prop("checked", true);
},
},
{
content: "Tags",
trigger:
"div.js_question-wrapper:contains('Tags') label:contains('Prospects') i",
run: function () {
$(
"div.js_question-wrapper:contains('Tags') label:contains('Prospects') i"
).prop("checked", true);
},
},
{
content: "Tags",
trigger:
"div.js_question-wrapper:contains('Tags') label:contains('Vendor') i",
run: function () {
$(
"div.js_question-wrapper:contains('Tags') label:contains('Vendor') i"
).prop("checked", true);
},
},
{
content: "Click Submit",
trigger: "button[value='finish']",
},
{
content: "Thank you",
trigger: "h1:contains('Thank you!')",
},
]
);
});

View File

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

View File

@@ -0,0 +1,31 @@
# Copyright 2023 Tecnativa - David Vidal
# Copyright 2023 Tecnativa - Stefan Ungureanu
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests import HttpCase, tagged
from odoo.addons.survey.tests.common import SurveyCase
@tagged("-at_install", "post_install")
class SurveyContactGenerationCase(SurveyCase, HttpCase):
def setUp(self):
"""We run the tour in the setup so we can share the tests case with other
modules"""
super().setUp()
self.survey = self.env.ref("survey_contact_generation.survey_contact_creation")
initial_user_inputs = self.survey.user_input_ids
# Run the survey as a portal user and get the generated quotation
self.start_tour(
f"/survey/start/{self.survey.access_token}",
"test_survey_contact_generation",
)
self.user_input = self.survey.user_input_ids - initial_user_inputs
@tagged("-at_install", "post_install")
class SurveyContactGenerationTests(SurveyContactGenerationCase):
def test_contact_generation(self):
partner = self.env["res.partner"].search(
[("email", "=", "survey_contact_generation@test.com")]
)
self.assertEqual(partner, self.user_input.partner_id)

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="survey_question_form" model="ir.ui.view">
<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="after">
<group name="contact" string="Contact">
<field name="res_partner_field" widget="selection" />
<field name="allowed_field_ids" invisible="1" />
</group>
</xpath>
<xpath expr="//field[@name='suggested_answer_ids']" position="attributes">
<attribute
name="context"
>{'default_question_id': active_id, 'default_res_partner_field': res_partner_field}</attribute>
</xpath>
<xpath
expr="//field[@name='suggested_answer_ids']//field[@name='value']"
position="after"
>
<field name="res_partner_field" invisible="1" />
<field
name="res_partner_field_resource_ref"
readonly="False"
options="{'hide_model': True, 'no_create': True, 'no_edit': True, 'no_open': True}"
attrs="{'column_invisible': [('parent.res_partner_field', '=', False)]}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="survey_form" model="ir.ui.view">
<field name="model">survey.survey</field>
<field name="inherit_id" ref="survey.survey_survey_view_form" />
<field name="arch" type="xml">
<group name="options" position="inside">
<group name="contact_options" string="Contact">
<field name="generate_contact" />
</group>
</group>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,105 @@
=======================
Survey leads generation
=======================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:72ceb1068020aaec4baba6df86a6b1b024793db57bdfc2cec8374da9cac8d031
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsurvey-lightgray.png?logo=github
:target: https://github.com/OCA/survey/tree/15.0/survey_crm_generation
:alt: OCA/survey
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/survey-15-0/survey-15-0-survey_crm_generation
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/survey&target_branch=15.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module allows to generate leads/opportunities from surveys.
**Table of contents**
.. contents::
:local:
Configuration
=============
To configure the leads/opportunities generation:
#. Go to the configured survey.
#. In the *CRM* section of the *Options* tab, set *Generate leads* on.
#. Optionally you can set default tags for the generated leads.
#. You can set the crm team to assign the leads to.
The questions marked to be shown in the lead description will be shown there.
Usage
=====
Once the surveys are submited a lead/opportunity (depending on the default options for
the company) will be generated with a link to the answers.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/survey/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/survey/issues/new?body=module:%20survey_crm_generation%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Tecnativa
Contributors
~~~~~~~~~~~~
* `Tecnativa <https://www.tecnativa.com>`_
* David Vidal
* Stefan ungureanu
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-chienandalu| image:: https://github.com/chienandalu.png?size=40px
:target: https://github.com/chienandalu
:alt: chienandalu
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-chienandalu|
This module is part of the `OCA/survey <https://github.com/OCA/survey/tree/15.0/survey_crm_generation>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

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

View File

@@ -0,0 +1,26 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Survey leads generation",
"summary": "Generate CRM leads/opportunities from surveys",
"version": "16.0.1.0.0",
"development_status": "Beta",
"category": "Marketing/Survey",
"website": "https://github.com/OCA/survey",
"author": "Tecnativa, Odoo Community Association (OCA)",
"maintainers": ["chienandalu"],
"license": "AGPL-3",
"depends": ["survey", "crm"],
"data": [
"views/survey_survey_views.xml",
"views/survey_question_views.xml",
"views/survey_user_input_views.xml",
"views/crm_lead_views.xml",
],
"demo": ["demo/survey_crm_demo.xml"],
"assets": {
"web.assets_tests": [
"/survey_crm_generation/static/tests/survey_crm_generation_tour.js",
],
},
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="crm.tag" id="tag_survey_leads">
<field name="name">Survey Leads</field>
</record>
<record model="crm.tag" id="tag_oca_partnership">
<field name="name">OCA Partnership</field>
</record>
<!-- Sales Team -->
<record model="crm.team" id="oca_partnership_leads">
<field name="name">OCA Partnership</field>
</record>
<!-- Survey -->
<record model="survey.survey" id="become_partner">
<field name="title">Become OCA Partner</field>
<field name="description">Be part of the Odoo Community!</field>
<field name="access_token">08b4db21-66cc-4c69-a712-cc364c54902c</field>
<field name="access_mode">public</field>
<field name="generate_leads" eval="True" />
<field name="crm_team_id" ref="oca_partnership_leads" />
<field
name="crm_tag_ids"
eval="[(6,0,[ref('survey_crm_generation.tag_oca_partnership'), ref('survey_crm_generation.tag_survey_leads')])]"
/>
<field name="users_can_go_back" eval="True" />
</record>
<record model="survey.question" id="survey_oca_q0">
<field name="survey_id" ref="become_partner" />
<field name="sequence">0</field>
<field name="title">E-mail address</field>
<field name="question_type">text_box</field>
<field name="show_in_lead_description" eval="True" />
<field name="constr_mandatory" eval="True" />
</record>
<record model="survey.question" id="survey_oca_q1">
<field name="survey_id" ref="become_partner" />
<field name="sequence">1</field>
<field name="title">Your company name?</field>
<field name="question_type">text_box</field>
<field name="show_in_lead_description" eval="True" />
<field name="constr_mandatory" eval="True" />
</record>
<record model="survey.question" id="survey_oca_q3">
<field name="survey_id" ref="become_partner" />
<field name="sequence">2</field>
<field name="title">And your name?</field>
<field name="question_type">text_box</field>
<field name="show_in_lead_description" eval="True" />
<field name="constr_mandatory" eval="True" />
</record>
</odoo>

View File

@@ -0,0 +1,137 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * survey_crm_generation
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-01 09:49+0000\n"
"PO-Revision-Date: 2023-06-01 12:07+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.0.1\n"
#. module: survey_crm_generation
#: model:survey.question,title:survey_crm_generation.survey_oca_q3
msgid "And your name?"
msgstr "¿Y tu nombre? "
#. module: survey_crm_generation
#: model:survey.survey,description:survey_crm_generation.become_partner
msgid "Be part of the Odoo Community!"
msgstr "¡Se parte de la comunidad de Odoo!"
#. module: survey_crm_generation
#: model:survey.survey,title:survey_crm_generation.become_partner
msgid "Become OCA Partner"
msgstr "Conviértete en miembro OCA"
#. module: survey_crm_generation
#: model_terms:ir.ui.view,arch_db:survey_crm_generation.survey_form
#: model_terms:ir.ui.view,arch_db:survey_crm_generation.survey_question_form
msgid "CRM"
msgstr "CRM"
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_survey__crm_tag_ids
msgid "Crm Tag"
msgstr "Etiqueta CRM"
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_survey__crm_team_id
msgid "Crm Team"
msgstr "Equipo CRM"
#. module: survey_crm_generation
#: model:survey.question,title:survey_crm_generation.survey_oca_q0
msgid "E-mail address"
msgstr "Dirección de correo electrónico"
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_survey__generate_leads
msgid "Generate Leads"
msgstr "Generación de iniciativa"
#. module: survey_crm_generation
#: model:ir.model.fields,help:survey_crm_generation.field_survey_survey__generate_leads
msgid "Generate leads/opportunities linked to the generated quotations"
msgstr "Generar iniciativas/oportunidades vinculadas a las ofertas generadas"
#. module: survey_crm_generation
#: model:survey.question,comments_message:survey_crm_generation.survey_oca_q0
#: model:survey.question,comments_message:survey_crm_generation.survey_oca_q1
#: model:survey.question,comments_message:survey_crm_generation.survey_oca_q3
msgid "If other, please specify:"
msgstr "Si es otro, especifíquelo:"
#. module: survey_crm_generation
#: model:ir.model,name:survey_crm_generation.model_crm_lead
msgid "Lead/Opportunity"
msgstr "Iniciativa/Oportunidad"
#. module: survey_crm_generation
#: model:crm.tag,name:survey_crm_generation.tag_oca_partnership
#: model:crm.team,name:survey_crm_generation.oca_partnership_leads
msgid "OCA Partnership"
msgstr "Asociación OCA"
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_user_input__opportunity_id
msgid "Opportunity"
msgstr "Oportunidad"
#. module: survey_crm_generation
#: model:ir.model.fields,help:survey_crm_generation.field_survey_survey__crm_tag_ids
msgid "Set the default crm tags in the generated leads/opportunities"
msgstr "Establecer las etiquetas crm por defecto en las iniciativas/oportunidades generadas"
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_question__show_in_lead_description
msgid "Show In Lead Description"
msgstr "Mostrar en la descripción de la iniciativa"
#. module: survey_crm_generation
#: model:ir.model,name:survey_crm_generation.model_survey_survey
msgid "Survey"
msgstr "Encuesta"
#. module: survey_crm_generation
#: model:crm.tag,name:survey_crm_generation.tag_survey_leads
msgid "Survey Leads"
msgstr "Encuesta de la iniciativa"
#. module: survey_crm_generation
#: model:ir.model,name:survey_crm_generation.model_survey_question
msgid "Survey Question"
msgstr "Pregunta de la encuesta"
#. module: survey_crm_generation
#: model:ir.model,name:survey_crm_generation.model_survey_user_input
#: model:ir.model.fields,field_description:survey_crm_generation.field_crm_lead__survey_user_input_id
msgid "Survey User Input"
msgstr "Respuestas de los usuarios"
#. module: survey_crm_generation
#: model:survey.question,validation_error_msg:survey_crm_generation.survey_oca_q0
#: model:survey.question,validation_error_msg:survey_crm_generation.survey_oca_q1
#: model:survey.question,validation_error_msg:survey_crm_generation.survey_oca_q3
msgid "The answer you entered is not valid."
msgstr "La respuesta que has introducido no es válida."
#. module: survey_crm_generation
#: model:survey.question,constr_error_msg:survey_crm_generation.survey_oca_q0
#: model:survey.question,constr_error_msg:survey_crm_generation.survey_oca_q1
#: model:survey.question,constr_error_msg:survey_crm_generation.survey_oca_q3
msgid "This question requires an answer."
msgstr "Esta pregunta requiere una respuesta."
#. module: survey_crm_generation
#: model:survey.question,title:survey_crm_generation.survey_oca_q1
msgid "Your company name?"
msgstr "¿El nombre de su empresa?"

View File

@@ -0,0 +1,85 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * survey_crm_generation
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-10-24 08:50+0000\n"
"PO-Revision-Date: 2023-10-24 08:50+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: survey_crm_generation
#: model_terms:ir.ui.view,arch_db:survey_crm_generation.survey_form
#: model_terms:ir.ui.view,arch_db:survey_crm_generation.survey_question_form
msgid "CRM"
msgstr ""
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_survey__crm_tag_ids
msgid "Crm Tag"
msgstr "Étiquette CRM"
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_survey__crm_team_id
msgid "Crm Team"
msgstr "Équipe commerciale"
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_survey__generate_leads
msgid "Generate Leads"
msgstr "Générer des pistes"
#. module: survey_crm_generation
#: model:ir.model.fields,help:survey_crm_generation.field_survey_survey__generate_leads
msgid "Generate leads/opportunities linked to the generated quotations"
msgstr "Générer des pistes / opportunités liées aux devis"
#. module: survey_crm_generation
#: model:ir.model,name:survey_crm_generation.model_crm_lead
msgid "Lead/Opportunity"
msgstr "Piste/Opportunité"
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_user_input__opportunity_id
msgid "Opportunity"
msgstr "Opportunité"
#. module: survey_crm_generation
#: model:ir.model.fields,help:survey_crm_generation.field_survey_survey__crm_tag_ids
msgid "Set the default crm tags in the generated leads/opportunities"
msgstr "Attribuer les étiquettes par défault dans les pistes générées"
#. module: survey_crm_generation
#: model:ir.model.fields,field_description:survey_crm_generation.field_survey_question__show_in_lead_description
msgid "Show In Lead Description"
msgstr "Montrer dans la description de la piste CRM"
#. module: survey_crm_generation
#: model:ir.model,name:survey_crm_generation.model_survey_survey
msgid "Survey"
msgstr "Questionnaire"
#. module: survey_crm_generation
#: model:ir.model,name:survey_crm_generation.model_survey_question
msgid "Survey Question"
msgstr "Question du questionnaire"
#. module: survey_crm_generation
#: model:ir.model,name:survey_crm_generation.model_survey_user_input
#: model:ir.model.fields,field_description:survey_crm_generation.field_crm_lead__survey_user_input_id
msgid "Survey User Input"
msgstr "Entrée utilisateur du questionnaire"
#. module: survey_crm_generation
#. odoo-python
#: code:addons/survey_crm_generation/models/survey_user_input.py:0
#, python-format
msgid "Survey answers: "
msgstr "Réponses au questionnaire : "

View File

@@ -0,0 +1,4 @@
from . import crm_lead
from . import survey_question
from . import survey_survey
from . import survey_user_input

View File

@@ -0,0 +1,9 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class CrmLead(models.Model):
_inherit = "crm.lead"
survey_user_input_id = fields.Many2one(comodel_name="survey.user_input")

View File

@@ -0,0 +1,9 @@
# Copyright 2022 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class SurveyQuestion(models.Model):
_inherit = "survey.question"
show_in_lead_description = fields.Boolean()

View File

@@ -0,0 +1,16 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class SurveySurvey(models.Model):
_inherit = "survey.survey"
generate_leads = fields.Boolean(
help="Generate leads/opportunities linked to the generated quotations",
)
crm_tag_ids = fields.Many2many(
comodel_name="crm.tag",
help="Set the default crm tags in the generated leads/opportunities",
)
crm_team_id = fields.Many2one(comodel_name="crm.team")

View File

@@ -0,0 +1,77 @@
# Copyright 2022 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models, _
class SurveyUserInput(models.Model):
_inherit = "survey.user_input"
opportunity_id = fields.Many2one(comodel_name="crm.lead")
def _prepare_opportunity(self):
return {
"name": self.survey_id.title,
"tag_ids": [(6, 0, self.survey_id.crm_tag_ids.ids)],
"partner_id": self.partner_id.id,
"user_id": self.survey_id.crm_team_id.user_id.id,
"team_id": self.survey_id.crm_team_id.id,
"company_id": self.create_uid.company_id.id,
"survey_user_input_id": self.id,
"description": self._prepare_lead_description(),
"type": "lead"
}
def _prepare_lead_description(self):
"""We can have surveys without partner. It's handy to have some relevant info
in the description although the answers are linked themselves.
:return str: description for the lead
"""
relevant_answers = self.user_input_line_ids.filtered(
lambda x: not x.skipped and x.question_id.show_in_lead_description
)
li = ''
for answer in relevant_answers:
li += '<li>'
if answer.answer_type == "suggestion":
answer_value = answer.suggested_answer_id
else:
answer_value = answer[f'value_{answer.answer_type}']
#case of value Models
if isinstance(answer_value,models.Model):
# case of Multi Models
if len(answer_value._ids) > 1:
ul2 = f'{answer.question_id.title}: <ul>'
for answer_value_object in answer_value:
ul2 += '<li>'+f"{answer_value_object.display_name}"+'</li>'
ul2 += '</ul>'
li += ul2
# case of One Models
else:
li += f"{answer.question_id.title}: {answer_value.display_name}"
else:
# case of string value
li += f"{answer.question_id.title}: {answer_value}"
li += '</li>'
description = '<u>'+_('Survey answers: ')+"</u><ul>"+li+"</ul>"
return description
def _create_opportunity_post_process(self):
"""After creating the lead send an internal message with the input link"""
self.opportunity_id.message_post_with_view(
"mail.message_origin_link",
values={"self": self.opportunity_id, "origin": self},
subtype_id=self.env.ref("mail.mt_note").id,
)
def _mark_done(self):
"""Generate the opportunity when the survey is submitted"""
res = super()._mark_done()
if not self.survey_id.generate_leads:
return res
vals = self._prepare_opportunity()
self.opportunity_id = self.env["crm.lead"].sudo().create(vals)
self._create_opportunity_post_process()
return res

View File

@@ -0,0 +1,8 @@
To configure the leads/opportunities generation:
#. Go to the configured survey.
#. In the *CRM* section of the *Options* tab, set *Generate leads* on.
#. Optionally you can set default tags for the generated leads.
#. You can set the crm team to assign the leads to.
The questions marked to be shown in the lead description will be shown there.

View File

@@ -0,0 +1,4 @@
* `Tecnativa <https://www.tecnativa.com>`_
* David Vidal
* Stefan ungureanu

View File

@@ -0,0 +1 @@
This module allows to generate leads/opportunities from surveys.

View File

@@ -0,0 +1,2 @@
Once the surveys are submited a lead/opportunity (depending on the default options for
the company) will be generated with a link to the answers.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<title>Survey leads generation</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
@@ -275,7 +275,7 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
@@ -301,7 +301,7 @@ span.option {
span.pre {
white-space: pre }
span.problematic, pre.problematic {
span.problematic {
color: red }
span.section-subtitle {
@@ -360,81 +360,84 @@ ul.auto-toc {
</style>
</head>
<body>
<div class="document">
<div class="document" id="survey-leads-generation">
<h1 class="title">Survey leads generation</h1>
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="survey-xlsx-extra-fields-bridge">
<h1>Survey XLSX - Extra Fields Bridge</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:8baa47af03e5b4b4e70ca6db224a5ac3e73aa66f287c42053a9a8f631efd10c2
!! source digest: sha256:72ceb1068020aaec4baba6df86a6b1b024793db57bdfc2cec8374da9cac8d031
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/elabore-coop/survey-tools/tree/18.0/survey_xlsx_extra_fields"><img alt="elabore-coop/survey-tools" src="https://img.shields.io/badge/github-elabore--coop%2Fsurvey--tools-lightgray.png?logo=github" /></a></p>
<p>This is a <strong>bridge module</strong> between <tt class="docutils literal">survey_xlsx_expand_multiple_choice</tt>
and <tt class="docutils literal">survey_extra_fields</tt>.</p>
<p><tt class="docutils literal">survey_extra_fields</tt> adds a <em>File</em> question type, whose answers are
uploaded attachments that cannot be represented in a spreadsheet cell. This
module excludes those <em>File</em> questions from the <strong>Survey Results XLSX
export</strong>: they get no column at all, instead of an unusable one.</p>
<p>It installs automatically (<tt class="docutils literal">auto_install</tt>) as soon as both
<tt class="docutils literal">survey_xlsx_expand_multiple_choice</tt> and <tt class="docutils literal">survey_extra_fields</tt> are
installed, and is uninstalled when either of them is removed. There is
nothing to configure.</p>
<div class="admonition warning">
<p class="first admonition-title">Warning</p>
<p>The exclusion is implemented through the report extension hooks provided
by <tt class="docutils literal">survey_xlsx_expand_multiple_choice</tt>, which itself relies on hooks
added to <tt class="docutils literal">survey_xlsx</tt> by this pull request:</p>
<p><a class="reference external" href="https://github.com/elabore-coop/survey/pull/1">https://github.com/elabore-coop/survey/pull/1</a></p>
<p class="last">Without those hooks, <em>File</em> questions are not filtered out of the export.</p>
</div>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/survey/tree/15.0/survey_crm_generation"><img alt="OCA/survey" src="https://img.shields.io/badge/github-OCA%2Fsurvey-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/survey-15-0/survey-15-0-survey_crm_generation"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/survey&amp;target_branch=15.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows to generate leads/opportunities from surveys.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>To configure the leads/opportunities generation:</p>
<ol class="arabic simple">
<li>Go to the configured survey.</li>
<li>In the <em>CRM</em> section of the <em>Options</em> tab, set <em>Generate leads</em> on.</li>
<li>Optionally you can set default tags for the generated leads.</li>
<li>You can set the crm team to assign the leads to.</li>
</ol>
<p>The questions marked to be shown in the lead description will be shown there.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<p>Once the surveys are submited a lead/opportunity (depending on the default options for
the company) will be generated with a link to the answers.</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/elabore-coop/survey-tools/issues">GitHub Issues</a>.
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/survey/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/elabore-coop/survey-tools/issues/new?body=module:%20survey_xlsx_extra_fields%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<a class="reference external" href="https://github.com/OCA/survey/issues/new?body=module:%20survey_crm_generation%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-2">Credits</a></h2>
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-3">Authors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>Elabore</li>
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-4">Contributors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.elabore.coop">Elabore</a><ul>
<li>Quentin Mondot</li>
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a><ul>
<li>David Vidal</li>
<li>Stefan ungureanu</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h3>
<p>This module is part of the <a class="reference external" href="https://github.com/elabore-coop/survey-tools/tree/18.0/survey_xlsx_extra_fields">elabore-coop/survey-tools</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/chienandalu"><img alt="chienandalu" src="https://github.com/chienandalu.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/survey/tree/15.0/survey_crm_generation">OCA/survey</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,48 @@
odoo.define(
"survey_crm_generation.tour_test_survey_crm_generation",
function (require) {
"use strict";
const tour = require("web_tour.tour");
tour.register(
"test_survey_crm_generation",
{
test: true,
url: "/survey/start/08b4db21-66cc-4c69-a712-cc364c54902c",
},
[
{
content: "Start Survey",
trigger: "button.btn:contains('Start Survey')",
},
{
content: "E-mail address",
trigger:
"div.js_question-wrapper:contains('E-mail address') textarea",
run: "text test@test.com",
},
{
content: "Your company name?",
trigger:
"div.js_question-wrapper:contains('Your company name?') textarea",
run: "text Tecnativa",
},
{
content: "And your name?",
trigger:
"div.js_question-wrapper:contains('And your name?') textarea",
run: "text Tecnativa",
},
{
content: "Click Submit",
trigger: "button[value='finish']",
},
{
content: "Thank you",
trigger: "h1:contains('Thank you!')",
},
]
);
}
);

View File

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

View File

@@ -0,0 +1,49 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from markupsafe import Markup
from odoo.tests import HttpCase, tagged
from odoo.addons.survey.tests.common import SurveyCase
@tagged("-at_install", "post_install")
class SurveyCrmGenerationCase(SurveyCase, HttpCase):
def setUp(self):
"""We run the tour in the setup so we can share the tests case with other
modules"""
super().setUp()
self.oca_leads = self.env.ref("survey_crm_generation.oca_partnership_leads")
self.survey = self.env.ref("survey_crm_generation.become_partner")
initial_user_inputs = self.survey.user_input_ids
# Run the survey as a portal user and get the generated quotation
self.start_tour(
f"/survey/start/{self.survey.access_token}",
"test_survey_crm_generation",
login="portal",
)
self.user_input = self.survey.user_input_ids - initial_user_inputs
self.generated_lead = self.user_input.opportunity_id
@tagged("-at_install", "post_install")
class SurveyCrmGenerationTests(SurveyCrmGenerationCase):
def test_lead_generation(self):
self.assertFalse(self.generated_lead.stage_id.is_won)
self.assertEqual(self.generated_lead.team_id, self.oca_leads)
self.assertEqual(
self.generated_lead.tag_ids,
(
self.env.ref("survey_crm_generation.tag_oca_partnership")
+ self.env.ref("survey_crm_generation.tag_survey_leads")
),
)
expected_lead_description = Markup(
"<p>E-mail address: test@test.com\n"
"Your company name?: Tecnativa\n"
"And your name?: Tecnativa</p>"
)
self.assertEqual(
self.generated_lead.description,
expected_lead_description,
)

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="crm_lead_view_form" model="ir.ui.view">
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_lead_view_form" />
<field name="arch" type="xml">
<field name="tag_ids" position="after">
<field
string="Created from survey"
name="survey_user_input_id"
attrs="{'invisible': [('survey_user_input_id', '=', False)]}"
readonly="1"
/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="survey_question_form" model="ir.ui.view">
<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="after">
<group name="crm" string="CRM">
<field
name="show_in_lead_description"
attrs="{'invisible': [('question_type', 'in', ['matrix'])]}"
/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="survey_form" model="ir.ui.view">
<field name="model">survey.survey</field>
<field name="inherit_id" ref="survey.survey_survey_view_form" />
<field name="arch" type="xml">
<group name="options" position="inside">
<group name="crm_options" string="CRM">
<field name="generate_leads" />
<field
name="crm_team_id"
attrs="{'invisible': [('generate_leads', '=', False)]}"
/>
<field
name="crm_tag_ids"
widget="many2many_tags"
attrs="{'invisible': [('generate_leads', '=', False)]}"
/>
</group>
</group>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="survey_user_input_view_form">
<field name="model">survey.user_input</field>
<field name="inherit_id" ref="survey.survey_user_input_view_form" />
<field name="arch" type="xml">
<field name="partner_id" position="before">
<field
name="opportunity_id"
attrs="{'invisible': [('opportunity_id', '=', False)]}"
/>
</field>
</field>
</record>
</odoo>

View File

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

View File

@@ -0,0 +1,25 @@
# Copyright 2016-2020 Akretion France (<https://www.akretion.com>)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Survey lead generation attachment",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Elabore",
'summary': 'Link Attachments from Surveys to generated leads',
'description': """
Link Attachments from Surveys to generated leads
----------------------------------------------------
* Create new attachments on lead creation, based on attached file of survey answer (survey.user_input.line)
""",
"website": "https://www.elabore.coop",
"category": "",
"depends": ["survey_base","survey_crm_generation"],
"data": [
],
"installable": True,
"auto_install":True
}

View File

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

View File

@@ -0,0 +1,32 @@
import logging
import textwrap
import uuid
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import float_is_zero
_logger = logging.getLogger(__name__)
class SurveyUserInput(models.Model):
_inherit = 'survey.user_input'
def _mark_done(self):
"""Copy attachments to crm lead"""
res = super()._mark_done()
for user_input in self:
if user_input.survey_id.generate_leads and user_input.opportunity_id:
for user_input_line in user_input.user_input_line_ids:
if user_input_line.value_file:
self.env['ir.attachment'].create({
'res_model':'crm.lead',
'res_id':user_input.opportunity_id.id,
'name': user_input_line.value_file_fname,
'datas': user_input_line.value_file,
'type': 'binary'
})
return res

View File

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

View File

@@ -0,0 +1,26 @@
# Copyright 2016-2020 Akretion France (<https://www.akretion.com>)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Survey Crm Generation Training",
'summary': 'Customize lead creation from survey according to trainings',
'description': """
Customize lead creation from survey according to trainings
----------------------------------------------------
* add event type in crm.lead
* Copy event type selected in survey answer, to created lead
""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Elabore",
"website": "https://www.elabore.coop",
"category": "",
"depends": ["event","survey_crm_generation","survey_event_registration_generation","survey_event_base"],
"data": [
"views/crm_lead_views.xml",
],
"installable": True,
"auto_install":True
}

View File

@@ -0,0 +1,53 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * survey_crm_generation_training
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-04 10:52+0000\n"
"PO-Revision-Date: 2024-12-04 10:52+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: survey_crm_generation_training
#: model_terms:ir.ui.view,arch_db:survey_crm_generation_training.crm_lead_view_for_crm_generation_training
msgid ""
"<span class=\"o_stat_value\">\n"
" Responses\n"
" </span>"
msgstr ""
"<span class=\"o_stat_value\">\n"
" Réponses\n"
" </span>"
#. module: survey_crm_generation_training
#: model_terms:ir.ui.view,arch_db:survey_crm_generation_training.view_crm_case_leads_filter_survey_crm_generation_training
#: model_terms:ir.ui.view,arch_db:survey_crm_generation_training.view_crm_case_opportunities_filter_survey_crm_generation_training
msgid "Event type"
msgstr "Formation(s)"
#. module: survey_crm_generation_training
#: model:ir.model.fields,field_description:survey_crm_generation_training.field_crm_lead__event_type_ids
msgid "Formation"
msgstr ""
#. module: survey_crm_generation_training
#: model:ir.model,name:survey_crm_generation_training.model_crm_lead
msgid "Lead/Opportunity"
msgstr "Piste/Opportunité"
#. module: survey_crm_generation_training
#: model:ir.model,name:survey_crm_generation_training.model_survey_user_input
msgid "Survey User Input for custom matrix"
msgstr "Entrée des utilisateurs de l'enquête pour la matrice personnalisée"
#. module: survey_crm_generation_training
#: model_terms:ir.ui.view,arch_db:survey_crm_generation_training.crm_lead_view_for_crm_generation_training
msgid "View survey answer that created this lead"
msgstr "Voir les réponses aux questionnaire qui ont créé cette piste"

View File

@@ -0,0 +1,2 @@
from . import crm_lead
from . import survey_user_input

View File

@@ -0,0 +1,23 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class CrmLead(models.Model):
_inherit = "crm.lead"
event_type_ids = fields.Many2many('event.type', string='Formation')
def action_view_survey_user_input_id(self):
form_view_id = self.env.ref('survey.survey_user_input_view_form').id
if self.survey_user_input_id:
action = self.env["ir.actions.actions"]._for_xml_id("survey.action_survey_user_input")
action['res_id'] = self.survey_user_input_id.id
action['domain'] = [('id', '=', self.survey_user_input_id.id)]
action['view_type'] = 'form'
action['view_mode'] = 'form'
action['binding_view_types'] = 'form'
action['view_id'] = form_view_id
action['views'] = [(form_view_id, 'form')]
return action

View File

@@ -0,0 +1,22 @@
# Copyright 2022 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models, _, Command
class SurveyUserInput(models.Model):
_inherit = "survey.user_input"
def _prepare_opportunity(self):
res = super(SurveyUserInput, self)._prepare_opportunity()
#if answer (survey_user_input) contains event_type_ids, copy it in lead.
res["event_type_ids"] = [Command.set(self.event_type_ids.ids)]
# use survey responsible as seller instead of manager of sale department
if self.survey_id.user_id:
res["user_id"] = self.survey_id.user_id.id
return res

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_crm_case_opportunities_filter_survey_crm_generation_training" model="ir.ui.view">
<field name="name">crm.lead.search.opportunity.crm.generation.training"</field>
<field name="inherit_id" ref="crm.view_crm_case_opportunities_filter" />
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<filter name="salesperson" position="before">
<filter string="Event type" name="event_type" context="{'group_by':'event_type_ids'}" />
</filter>
<field name="tag_ids" position="before">
<field name="event_type_ids" />
</field>
</field>
</record>
<record id="view_crm_case_leads_filter_survey_crm_generation_training" model="ir.ui.view">
<field name="name">crm.lead.search.leads.crm.generation.training"</field>
<field name="inherit_id" ref="crm.view_crm_case_leads_filter" />
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<filter name="salesperson" position="before">
<filter string="Event type" name="event_type" context="{'group_by':'event_type_ids'}" />
</filter>
<field name="tag_ids" position="before">
<field name="event_type_ids" />
</field>
</field>
</record>
<record id="crm_lead_view_for_crm_generation_training" model="ir.ui.view">
<field name="model">crm.lead</field>
<field name="name">crm.lead.crm.generation.training</field>
<field name="inherit_id" ref="crm.crm_lead_view_form" />
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button class="oe_stat_button"
name="action_view_survey_user_input_id"
type="object"
icon="fa-wpforms"
help="View survey answer that created this lead"
attrs="{'invisible': [('survey_user_input_id', '=', False)]}"
>
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
Responses
</span>
</div>
</button>
</div>
<group name="lead_priority" position="after">
<group>
<field
name="event_type_ids"
widget="many2many_tags"
/>
</group>
</group>
</field>
</record>
<!-- CRM OPPORTUNITIES TREE VIEW -->
<record id="crm_case_tree_view_oppor_survey_crm_generation_training" model="ir.ui.view">
<field name="name">crm.lead.tree.opportunity.survey.crm.generation.training</field>
<field name="model">crm.lead</field>
<field name="priority">1</field>
<field name="inherit_id" ref="crm.crm_case_tree_view_oppor" />
<field name="arch" type="xml">
<field name="name" position="after">
<field name="event_type_ids" widget="many2many_tags" />
</field>
</field>
</record>
<!-- CRM OPPORTUNITIES TREE VIEW -->
<record id="crm_case_tree_view_leads_survey_crm_generation_training" model="ir.ui.view">
<field name="name">crm.case.tree.view.leads.survey.crm.generation.training</field>
<field name="model">crm.lead</field>
<field name="priority">1</field>
<field name="inherit_id" ref="crm.crm_case_tree_view_leads" />
<field name="arch" type="xml">
<field name="name" position="after">
<field name="event_type_ids" widget="many2many_tags" />
</field>
</field>
</record>
</odoo>

View File

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

View File

@@ -0,0 +1,23 @@
# Copyright 2016-2020 Akretion France (<https://www.akretion.com>)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Survey event base",
'summary': 'Add field to reference events and product events in servey answers (survey.user_input)',
'description': """
Add field to reference events and product events in servey answers (survey.user_input)
----------------------------------------------------
*
""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Elabore",
"website": "https://www.elabore.coop",
"category": "",
"depends": ["event","survey_base"],
"data": [
],
"installable": True,
}

View File

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

View File

@@ -0,0 +1,61 @@
import logging
import textwrap
import uuid
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import float_is_zero
_logger = logging.getLogger(__name__)
class SurveyUserInput(models.Model):
_inherit = 'survey.user_input'
events_ids = fields.Many2many('event.event', string='Events', compute="compute_events_ids", search="search_events_ids") #related events
event_type_ids = fields.Many2many('event.type', string='Formations', compute="compute_event_type_ids", search="search_event_type_ids") #related event products
def search_events_ids(self, operator, value):
user_input_ids = set()
user_input_lines = self.env['survey.user_input.line'].search([('record_reference_model','=','event.event'),('record_reference','=', value)])
for user_input_line in user_input_lines:
user_input_ids.add(user_input_line.user_input_id.id)
return [('id',operator,list(user_input_ids))]
@api.depends('user_input_line_ids')
def compute_events_ids(self):
"""get all answers of type "event.event"
"""
for user_input in self:
event_ids = []
for user_input in self:
for user_input_line in user_input.user_input_line_ids:
if user_input_line.record_reference_model == 'event.event':
event_ids.append(user_input_line.record_reference)
user_input.events_ids = event_ids
def search_event_type_ids(self, operator, value):
user_input_ids = set()
user_input_lines = self.env['survey.user_input.line'].search([('record_reference_model','=','event.type'),('record_reference','=', value)])
for user_input_line in user_input_lines:
user_input_ids.add(user_input_line.user_input_id.id)
return [('id',operator,list(user_input_ids))]
@api.depends('user_input_line_ids')
def compute_event_type_ids(self):
"""get all answers of type "event.type"
"""
for user_input in self:
event_type_ids = []
for user_input in self:
for user_input_line in user_input.user_input_line_ids:
if user_input_line.record_reference_model == 'event.type':
event_type_ids.append(user_input_line.record_reference)
user_input.event_type_ids = event_type_ids

View File

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

View File

@@ -0,0 +1,19 @@
# Copyright 2016-2020 Akretion France (<https://www.akretion.com>)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Survey event registration generation",
"version": "16.0.0.0.0",
"license": "AGPL-3",
"author": "Elabore",
"website": "https://www.elabore.coop",
"category": "",
"depends": ["survey", "survey_base"],
"data": [
'security/ir.model.access.csv',
'views/survey_question_views.xml',
'views/survey_survey_views.xml',
],
"installable": True,
}

View File

@@ -0,0 +1,3 @@
from . import survey_user_input
from . import survey_survey
from . import survey_question

View File

@@ -0,0 +1,38 @@
from email.policy import default
from odoo import models, fields, api
class SurveyQuestion(models.Model):
_inherit = 'survey.question'
event_registration_allowed_field_ids = fields.Many2many(
comodel_name="ir.model.fields",
compute="_compute_event_registration_allowed_field_ids",
) #fields of event registration, proposed in question, to associate answer to good event registration field, during event registration creation
event_registration_field = fields.Many2one(
string="Event registration field",
comodel_name="ir.model.fields",
domain="[('id', 'in', event_registration_allowed_field_ids)]",
) #field of event registration selected, used in event registration creation
@api.depends("question_type")
def _compute_event_registration_allowed_field_ids(self):
"""propose all event registration fields corresponding to selected question type
"""
type_mapping = {
"char_box": ["char", "text"],
"text_box": ["html"],
"numerical_box": ["integer", "float", "html", "char"],
"date": ["date", "text", "char"],
"datetime": ["datetime", "html", "char"],
"simple_choice": ["many2one", "html", "char"],
"multiple_choice": ["many2many", "html", "char"],
}
for record in self:
search_domain = [
("model", "=", "event.registration"),
("ttype", "in", type_mapping.get(record.question_type, [])),
]
record.event_registration_allowed_field_ids = self.env["ir.model.fields"].search(search_domain).ids

View File

@@ -0,0 +1,12 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class SurveySurvey(models.Model):
_inherit = "survey.survey"
generate_registration = fields.Boolean(
help="Generate event registration for selected event",
)

View File

@@ -0,0 +1,82 @@
import logging
import textwrap
import uuid
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import float_is_zero
_logger = logging.getLogger(__name__)
class SurveyUserInput(models.Model):
_inherit = 'survey.user_input'
registration_id = fields.Many2one('event.registration', 'Event registration') #registration created automaticaly on survey post
def _prepare_registration(self):
"""Extract registration values from the answers"""
elegible_inputs = self.user_input_line_ids.filtered(
lambda x: x.question_id.event_registration_field and not x.skipped
)
vals = {}
for line in elegible_inputs:
if line.question_id.event_registration_field.ttype == 'many2one':
vals[line.question_id.event_registration_field.name] = line.suggested_answer_id.record_reference
else:
vals[line.question_id.event_registration_field.name] = line[f"value_{line.answer_type}"]
return vals
def _create_registration_post_process(self, registration):
"""After creating the event registration send an internal message with the input link"""
registration.message_post_with_view(
"mail.message_origin_link",
values={"self": registration, "origin": self},
subtype_id=self.env.ref("mail.mt_note").id,
)
def _mark_done(self):
"""Generate registration when the survey is submitted"""
for user_input in self.filtered(
lambda r: r.survey_id.generate_registration and not self.registration_id
):
vals = user_input._prepare_registration()
# check doublon : if old draft registration already exists : update it
email = vals.get('email')
event_id = vals.get('event_id')
old_registration = False
if email and event_id:
old_registration = self.env["event.registration"].search([('email','=',email),('event_id','=',event_id)])
if old_registration:
old_registration = old_registration[0]
if old_registration.state == 'draft':
registration = old_registration
registration.write(vals)
registration.message_post_with_view(
"mail.message_origin_link",
values={"edit":True, "self": registration, "origin": user_input},
subtype_id=self.env.ref("mail.mt_note").id,
)
if not old_registration:
registration = self.env["event.registration"].create(vals)
self._create_registration_post_process(registration)
self.update({"registration_id": registration.id})
res = super()._mark_done()
# after all, set partner id of registration as the partner of user input
for user_input in self:
if user_input.registration_id and user_input.partner_id:
user_input.registration_id.partner_id = user_input.partner_id
return res

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ir_model_fields_survey_user,ir.model.fields.survey.user,base.model_ir_model_fields,survey.group_survey_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ir_model_fields_survey_user ir.model.fields.survey.user base.model_ir_model_fields survey.group_survey_user 1 0 0 0

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Survey question form -->
<record id="survey_question_form" model="ir.ui.view">
<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="after">
<group name="event_registration" string="Event registration">
<!-- event registration field, filtered by event_registration_allowed_field_ids (invisible) -->
<field name="event_registration_field" widget="selection" />
<field name="event_registration_allowed_field_ids" invisible="1" />
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Survey form -->
<record id="survey_form" model="ir.ui.view">
<field name="model">survey.survey</field>
<field name="inherit_id" ref="survey.survey_survey_view_form" />
<field name="arch" type="xml">
<group name="options" position="inside">
<group name="event_registration_options" string="Events registrations">
<!-- Possibility to generate event registration -->
<field name="generate_registration" />
</group>
</group>
</field>
</record>
</odoo>

View File

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

View File

@@ -0,0 +1,25 @@
# Copyright 2016-2020 Akretion France (<https://www.akretion.com>)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Survey event generation attachment",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Elabore",
"website": "https://www.elabore.coop",
"category": "",
'summary': 'Link Attachments from Surveys to generated event registration',
'description': """
Link Attachments from Surveys to generated event registration
----------------------------------------------------
* Create new attachments on event registration creation, based on attached file of survey answer (survey.user_input.line)
""",
"depends": ["survey_base","survey_event_registration_generation"],
"data": [
],
"installable": True,
"auto_install":True
}

View File

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

View File

@@ -0,0 +1,32 @@
import logging
import textwrap
import uuid
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import float_is_zero
_logger = logging.getLogger(__name__)
class SurveyUserInput(models.Model):
_inherit = 'survey.user_input'
def _mark_done(self):
"""Copy attachments to event registration"""
res = super()._mark_done()
for user_input in self:
if user_input.survey_id.generate_registration and user_input.registration_id:
for user_input_line in user_input.user_input_line_ids:
if user_input_line.value_file:
self.env['ir.attachment'].create({
'res_model':'event.registration',
'res_id':user_input.registration_id.id,
'name': user_input_line.value_file_fname,
'datas': user_input_line.value_file,
'type': 'binary'
})
return res

View File

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

View File

@@ -0,0 +1,18 @@
# Copyright 2016-2020 Akretion France (<https://www.akretion.com>)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Survey event speaker generation",
"version": "16.0.0.0.0",
"license": "AGPL-3",
"author": "Elabore",
"website": "https://www.elabore.coop",
"category": "",
"depends": ["survey", "survey_event_base", "event_speaker"],
"data": [
'views/mail_templates_chatter.xml',
'views/survey_survey_views.xml',
],
"installable": True,
}

View File

@@ -0,0 +1,56 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * survey_event_speaker_generation
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-19 14:42+0000\n"
"PO-Revision-Date: 2023-09-19 14:42+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: survey_event_speaker_generation
#: model_terms:ir.ui.view,arch_db:survey_event_speaker_generation.survey_form
msgid "Events speakers"
msgstr "Intervenants d'événements"
#. module: survey_event_speaker_generation
#: model:ir.model.fields,field_description:survey_event_speaker_generation.field_survey_user_input__speaker_id
msgid "Event speaker"
msgstr "Intervenants"
#. module: survey_event_speaker_generation
#: model:ir.model.fields,field_description:survey_event_speaker_generation.field_survey_survey__generate_speaker
msgid "Generate Speaker"
msgstr "Générer un intervenant"
#. module: survey_event_speaker_generation
#: model:ir.model.fields,help:survey_event_speaker_generation.field_survey_survey__generate_speaker
msgid "Generate speaker for selected event"
msgstr "Générer un intervenant pour l'événement selectionné"
#. module: survey_event_speaker_generation
#: model:ir.model,name:survey_event_speaker_generation.model_survey_survey
msgid "Survey"
msgstr "Questionnaire"
#. module: survey_event_speaker_generation
#: model:ir.model,name:survey_event_speaker_generation.model_survey_user_input
msgid "Survey User Input"
msgstr "Entrée utilisateur du questionnaire"
#. module: survey_event_speaker_generation
#: model_terms:ir.ui.view,arch_db:survey_event_speaker_generation.message_event_speaker_assigned
msgid "from survey answers"
msgstr "depuis la réponse au questionnaire"
#. module: survey_event_speaker_generation
#: model_terms:ir.ui.view,arch_db:survey_event_speaker_generation.message_event_speaker_assigned
msgid "has been assigned to"
msgstr "a été assigné à"

View File

@@ -0,0 +1,2 @@
from . import survey_user_input
from . import survey_survey

View File

@@ -0,0 +1,11 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class SurveySurvey(models.Model):
_inherit = "survey.survey"
generate_speaker = fields.Boolean(
help="Generate speaker for selected event",
) #Field to check if user wants to generate a speaker on survey submit

View File

@@ -0,0 +1,41 @@
import logging
import textwrap
import uuid
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _, Command
from odoo.exceptions import ValidationError
from odoo.tools import float_is_zero
_logger = logging.getLogger(__name__)
class SurveyUserInput(models.Model):
_inherit = 'survey.user_input'
speaker_id = fields.Many2one('res.partner', 'Event speaker') #created partner when submit survey
def _create_speaker_post_process(self, speaker):
for event in self.events_ids:
"""Add message to chatter to note speaker creation and association with event"""
speaker.message_post_with_view(
"survey_event_speaker_generation.message_event_speaker_assigned",
values={"speaker": speaker, "user_input": self, "event":event},
subtype_id=self.env.ref("mail.mt_note").id,
)
def _mark_done(self):
"""Attach partner as speaker of event"""
res = super()._mark_done()
for user_input in self.filtered(lambda r: r.survey_id.generate_speaker and not r.speaker_id and r.partner_id):
user_input.update({"speaker_id": user_input.partner_id.id})
for event in user_input.events_ids:
for track in event.track_ids:
track.speaker_ids = [Command.link(user_input.speaker_id.id)]
user_input._create_speaker_post_process(user_input.speaker_id)
return res

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Message notification in res.partner chatter -->
<template id="message_event_speaker_assigned">
<div style="margin: 0px; padding: 0px; font-size: 13px;">
<a href="#" t-att-data-oe-model="speaker._name" t-att-data-oe-id="speaker.id">
<t t-esc="speaker.name" />
</a>
has been assigned as speaker to
<a href="#" t-att-data-oe-model="event._name" t-att-data-oe-id="event.id">
<t t-esc="event.display_name" />
</a>
from survey answers
<a href="#" t-att-data-oe-model="user_input._name" t-att-data-oe-id="user_input.id">
<t t-esc="user_input.display_name" />
</a>
</div>
</template>
</data>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Survey form -->
<record id="survey_form" model="ir.ui.view">
<field name="model">survey.survey</field>
<field name="inherit_id" ref="survey.survey_survey_view_form" />
<field name="arch" type="xml">
<group name="options" position="inside">
<group name="event_speaker_options" string="Events speakers">
<field name="generate_speaker" />
</group>
</group>
</field>
</record>
</odoo>

View File

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

View File

@@ -0,0 +1,24 @@
# Copyright 2016-2020 Akretion France (<https://www.akretion.com>)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Survey event visibility",
"version": "16.0.0.0.0",
"license": "AGPL-3",
"author": "Elabore",
'summary': 'Add visibility field in event stage',
'description': """
Add visibility information in product and events
----------------------------------------------------
The product and event visibility is computed from event stage visibility.
""",
"website": "https://www.elabore.coop",
"category": "",
"depends": ["survey"],
"data": [
'views/event_stage_views.xml',
],
"installable": True,
}

View File

@@ -0,0 +1,3 @@
from . import event_stage
from . import event_event
from . import product_product

View File

@@ -0,0 +1,28 @@
from odoo import models, fields, api
class EventEvent(models.Model):
_inherit = 'event.event'
visible_in_survey = fields.Boolean('Visible in survey', related='stage_id.visible_in_survey', readonly=True,
help="""Events in step configured as 'visible in survey'.""")
def get_events_from_event_products(self, product_ids=None, only_visible_in_survey=False):
"""Search events in stage filtered by product present in ticket..
Args:
product_ids (list[product.product ids], optional): to filter only events with tickets using product in this list. Defaults to None.
Returns:
event.event: Events
"""
event_search = []
if product_ids:
event_tickets = self.env['event.event.ticket'].search([('product_id','in',product_ids)])
event_search.append(('event_ticket_ids','in',event_tickets.ids))
if only_visible_in_survey:
event_search.append(('visible_in_survey','=',True))
return self.env['event.event'].search(event_search)

View File

@@ -0,0 +1,7 @@
from odoo import models, fields, api
class EventStage(models.Model):
_inherit = 'event.stage'
visible_in_survey = fields.Boolean('Visible in surveys') #if checked, only events on this stage are visible in surveys

View File

@@ -0,0 +1,23 @@
from odoo import models, fields, api
class ProductProduct(models.Model):
_inherit = 'product.product'
visible_in_survey = fields.Boolean('Visible in survey', compute='_compute_visible_in_survey', readonly=True,
help="""Events in step configured as 'visible in survey'.""")
def _compute_visible_in_survey(self):
#get all events in step 'visible in survey'
product_ids = set()
events = self.env['event.event'].search([('visible_in_survey','=',True)])
for event in events:
for ticket in event.event_ticket_ids:
product_ids.add(ticket.product_id.id)
for event_product in self:
if event_product.id in product_ids:
event_product.visible_in_survey = True
else:
event_product.visible_in_survey = False

View File

@@ -0,0 +1,31 @@
<?xml version="1.0"?>
<odoo>
<data>
<!-- Event stage view form -->
<!-- * Add field visible_in_survey -->
<record id="event_stage_view_form_survey_event_registration_generation" model="ir.ui.view">
<field name="name">event.stage.view.form.survey.event.registration.generation</field>
<field name="model">event.stage</field>
<field name="inherit_id" ref="event.event_stage_view_form" />
<field name="arch" type="xml">
<field name="pipe_end" position="after">
<field name="visible_in_survey" />
</field>
</field>
</record>
<!-- Event stage view tree -->
<!-- * Add field visible_in_survey -->
<record id="event_stage_view_tree_survey_event_registration_generation" model="ir.ui.view">
<field name="name">event.stage.view.tree.survey.event.registration.generation</field>
<field name="model">event.stage</field>
<field name="inherit_id" ref="event.event_stage_view_tree" />
<field name="arch" type="xml">
<field name="name" position="after">
<field name="visible_in_survey" />
</field>
</field>
</record>
</data>
</odoo>

View File

@@ -13,7 +13,7 @@ Add extra question types to surveys:
- Client-side validation (size and extension) before form submission
- Server-side validation on save to enforce constraints
""",
"version": "18.0.1.0.0",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Elabore",
"website": "https://www.elabore.coop",

View File

@@ -31,7 +31,7 @@ class SurveyExtraFieldsController(Survey):
[("access_token", "=", survey_token)], limit=1
)
if not survey:
raise request.not_found()
return request.not_found()
answer = request.env["survey.user_input"].sudo().search(
[
@@ -41,14 +41,14 @@ class SurveyExtraFieldsController(Survey):
limit=1,
)
if not answer:
raise request.not_found()
return request.not_found()
line = request.env["survey.user_input.line"].sudo().browse(line_id)
if not line.exists() or line.user_input_id != answer:
raise request.not_found()
return request.not_found()
if not line.value_file:
raise request.not_found()
return request.not_found()
file_content = base64.b64decode(line.value_file)
filename = line.value_file_fname or "file"

View File

@@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-10 15:03+0000\n"
"PO-Revision-Date: 2026-06-10 15:03+0000\n"
"POT-Creation-Date: 2026-02-18 16:38+0000\n"
"PO-Revision-Date: 2026-02-18 16:38+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -20,11 +20,6 @@ msgstr ""
msgid ".pdf,.docx,.xlsx"
msgstr ""
#. module: survey_extra_fields
#: model_terms:ir.ui.view,arch_db:survey_extra_fields.question_file
msgid "<i class=\"fa fa-times me-1\"/>Remove file"
msgstr "<i class=\"fa fa-times me-1\"/>Supprimer le fichier"
#. module: survey_extra_fields
#: model:ir.model.fields,field_description:survey_extra_fields.field_survey_question__allowed_extensions
msgid "Allowed Extensions"
@@ -41,8 +36,8 @@ msgid ""
"Comma-separated list of allowed extensions (e.g. .pdf,.docx). Leave empty to"
" allow all types."
msgstr ""
"Liste d'extensions autorisées séparées par une virgule (e.g. .pdf,.docx). "
"Laisser vide pour autoriser tous les types de fichier."
"Liste d'extensions autorisées séparées par une virgule (e.g. .pdf,.docx). Laisser vide pour"
" autoriser tous les types de fichier."
#. module: survey_extra_fields
#: model:ir.model.fields.selection,name:survey_extra_fields.selection__survey_question__question_type__file
@@ -59,9 +54,7 @@ msgstr "Taille maximale du fichier"
#. module: survey_extra_fields
#: model:ir.model.fields,help:survey_extra_fields.field_survey_question__max_file_size
msgid "Maximum file size in MB. Leave 0 for no limit."
msgstr ""
"Taille maximale du fichier en MB. Laisser à 0 pour ne pas restreindre la "
"taille. La valeur par défaut est 10 MB."
msgstr "Taille maximale du fichier en MB. Laisser à 0 pour ne pas restreindre la taille. La valeur par défaut est 10 MB."
#. module: survey_extra_fields
#: model:ir.model.fields,field_description:survey_extra_fields.field_survey_question__question_type
@@ -73,6 +66,13 @@ msgstr "Type de question"
msgid "Skipped"
msgstr "Ignoré"
#. module: survey_extra_fields
#: model:ir.model.fields,field_description:survey_extra_fields.field_survey_question__smart_search
#: model:ir.model.fields,field_description:survey_extra_fields.field_survey_user_input__smart_search
#: model:ir.model.fields,field_description:survey_extra_fields.field_survey_user_input_line__smart_search
msgid "Smart Search"
msgstr "Recherche intelligente"
#. module: survey_extra_fields
#: model:ir.model,name:survey_extra_fields.model_survey_question
msgid "Survey Question"
@@ -91,39 +91,34 @@ msgstr "Ligne d'entrée pour l'utilisateur du sondage"
#. module: survey_extra_fields
#. odoo-python
#: code:addons/survey_extra_fields/models/survey_user_input.py:0
#, python-format
msgid "The file '%(name)s' exceeds the maximum allowed size of %(size)s MB."
msgstr ""
"Le fichier '%(name)s' dépasse la taille maximale autorisée de %(size)s MB."
msgstr "Le fichier '%(name)s' dépasse la taille maximale autorisée de %(size)s MB."
#. module: survey_extra_fields
#. odoo-python
#: code:addons/survey_extra_fields/models/survey_user_input.py:0
#, python-format
msgid "The file '%(name)s' is not allowed. Accepted formats: %(exts)s."
msgstr ""
"Le fichier '%(name)s' n'est pas autorisé. Les formats de fichier autorisés "
"sont : %(exts)s."
msgstr "Le fichier '%(name)s' n'est pas autorisé. Les formats de fichier autorisés sont : %(exts)s."
#. module: survey_extra_fields
#. odoo-javascript
#: code:addons/survey_extra_fields/static/src/js/survey_form.js:0
#, python-format
msgid "The file exceeds the maximum allowed size of %s MB."
msgstr "Le fichier dépasse la taille maximale autorisée de %s MB."
#. module: survey_extra_fields
#. odoo-python
#: code:addons/survey_extra_fields/models/survey_user_input.py:0
msgid "This answer cannot be overwritten."
msgstr "Cette réponse ne peut pas être remplacée."
#. module: survey_extra_fields
#. odoo-javascript
#: code:addons/survey_extra_fields/static/src/js/survey_form.js:0
#, python-format
msgid "This file type is not allowed. Accepted formats: %s."
msgstr ""
"Le fichier n'est pas autorisé. Les formats de fichier autorisés sont : %s."
msgstr "Le fichier n'est pas autorisé. Les formats de fichier autorisés sont : %s."
#. module: survey_extra_fields
#. odoo-javascript
#: code:addons/survey_extra_fields/static/src/js/survey_form.js:0
#, python-format
msgid "This question requires an answer."
msgstr "Cette question requiert une réponse."

View File

@@ -8,7 +8,7 @@ import os
from typing import TYPE_CHECKING
from odoo import _, models
from odoo.exceptions import UserError, ValidationError
from odoo.exceptions import ValidationError
if TYPE_CHECKING:
from odoo.addons.survey_extra_fields.models.survey_question import SurveyQuestion
@@ -17,39 +17,20 @@ if TYPE_CHECKING:
class SurveyUserInput(models.Model):
_inherit = "survey.user_input"
def _save_lines(
self,
question: SurveyQuestion,
answer: str | None,
comment: str | None = None,
overwrite_existing: bool = True,
) -> None:
def save_lines(self, question: SurveyQuestion, answer: str | None, comment: str | None = None) -> None:
if question.question_type == "file":
old_answers = self.env["survey.user_input.line"].search([
("user_input_id", "=", self.id),
("question_id", "=", question.id),
])
if old_answers and not overwrite_existing:
raise UserError(_("This answer cannot be overwritten."))
if not answer and any(line.value_file for line in old_answers):
# No new file was submitted: a file input cannot be pre-filled
# by the browser when navigating back to a previous page, so an
# empty answer here does not mean the user removed their file.
# Keep the previously uploaded file instead of overwriting it
# with a skipped answer.
return
vals = {
"user_input_id": self.id,
"question_id": question.id,
"skipped": False,
"answer_type": "file",
}
file_data = json.loads(answer) if answer else {}
if file_data.get("cleared"):
# The user explicitly removed the file: drop the stored data and
# mark the line as skipped.
vals.update(answer_type=None, skipped=True, value_file=False, value_file_fname=False)
elif file_data:
if answer:
file_data = json.loads(answer)
file_b64 = file_data.get("data", "")
file_name = file_data.get("name", "")
self._check_file_constraints(question, file_b64, file_name)
@@ -62,9 +43,7 @@ class SurveyUserInput(models.Model):
else:
self.env["survey.user_input.line"].create(vals)
else:
return super()._save_lines(
question, answer, comment=comment, overwrite_existing=overwrite_existing
)
return super().save_lines(question, answer, comment=comment)
def _check_file_constraints(
self,

View File

@@ -1,232 +1,146 @@
/** @odoo-module **/
odoo.define("survey_extra_fields.survey_form", function (require) {
"use strict";
import publicWidget from "@web/legacy/js/public/public_widget";
import { _t } from "@web/core/l10n/translation";
import { rpc } from "@web/core/network/rpc";
var core = require("web.core");
var _t = core._t;
var survey_form = require("survey.form");
publicWidget.registry.SurveyFormWidget.include({
/**
* @override
* Bind delegated listeners on the form root so they keep working after each
* page is re-rendered (the inner content is replaced on navigation, but the
* root element persists). They let the user clear a selected file before
* submitting the form.
*/
start: function () {
return this._super.apply(this, arguments).then(() => {
this.$el.on(
"change.surveyExtraFile",
'input[data-question-type="file"]',
this._onFileInputChange.bind(this)
);
this.$el.on(
"click.surveyExtraFile",
".o_survey_file_clear",
this._onFileClearClick.bind(this)
);
});
},
survey_form.include({
_readFileAsDataURL: function (file) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function (e) {
resolve(e.target.result);
};
reader.onerror = function () {
reject(reader.error);
};
reader.readAsDataURL(file);
});
},
/**
* On selection, show the file "chip" (filename + remove button) and hide the
* raw input, so a freshly selected file looks exactly like an already stored
* one (rendered server-side when navigating back).
*/
_onFileInputChange: function (ev) {
const input = ev.currentTarget;
const container = input.closest(".o_survey_comment_container");
if (!container || !(input.files && input.files.length > 0)) {
return;
}
const chip = container.querySelector(".o_survey_file_selected");
const nameEl = container.querySelector(".o_survey_file_name");
if (nameEl) {
nameEl.textContent = input.files[0].name;
}
if (chip) {
chip.classList.remove("d-none");
}
delete input.dataset.fileCleared;
input.classList.add("d-none");
},
/**
* Discard the current file: hide the chip and bring back the input so the
* user can pick a new one. A file already stored server-side is only really
* replaced once a new file is submitted (see _save_lines).
*/
_onFileClearClick: function (ev) {
ev.preventDefault();
const container = ev.currentTarget.closest(".o_survey_comment_container");
if (!container) {
return;
}
const input = container.querySelector('input[data-question-type="file"]');
const chip = container.querySelector(".o_survey_file_selected");
if (input) {
input.value = "";
// Flag the explicit removal so the submit tells the server to drop
// any previously stored file (instead of preserving it).
input.dataset.fileCleared = "1";
input.classList.remove("d-none");
}
if (chip) {
chip.classList.add("d-none");
}
},
_readFileAsDataURL: function (file) {
return new Promise(function (resolve, reject) {
const reader = new FileReader();
reader.onload = function (e) {
resolve(e.target.result);
};
reader.onerror = function () {
reject(reader.error);
};
reader.readAsDataURL(file);
});
},
/**
* @override
* The base implementation builds the submit params synchronously and fires
* the RPC immediately. File inputs need to be read asynchronously (FileReader),
* so when the current page contains file answers we replicate the submit flow
* here, injecting the base64 file payload before submitting.
*/
_submitForm: async function (options) {
const fileInputs = this.el.querySelectorAll('input[data-question-type="file"]');
// A file action is either a new selection or an explicit removal of a
// previously stored file (which must be communicated to the server).
const hasFileAction = Array.from(fileInputs).some(
(input) => (input.files && input.files.length > 0) || input.dataset.fileCleared
);
if (!hasFileAction || this.options.isStartScreen) {
return this._super(options);
}
const params = {};
if (options.previousPageId) {
params.previous_page_id = options.previousPageId;
}
if (options.nextSkipped) {
params.next_skipped_page_or_question = true;
}
const $form = this.$("form");
const formData = new FormData($form[0]);
if (!options.skipValidation && !this._validateForm($form, formData)) {
return;
}
this._prepareSubmitValues(formData, params);
// Read all selected files as base64 and add them to the submit params.
// Explicitly cleared inputs (no new file) send a "cleared" sentinel so
// the server removes the previously stored file.
const filePromises = [];
for (const input of fileInputs) {
if (input.files && input.files.length > 0) {
const file = input.files[0];
const name = input.name;
filePromises.push(
this._readFileAsDataURL(file).then((dataURL) => {
params[name] = JSON.stringify({
data: dataURL.split(",")[1],
name: file.name,
});
})
);
} else if (input.dataset.fileCleared) {
params[input.name] = JSON.stringify({ cleared: true });
}
}
// Prevent user from submitting more times using enter key.
this.preventEnterSubmit = true;
if (this.options.sessionInProgress) {
this.fadeInOutDelay = 400;
this.readonly = true;
}
await Promise.all(filePromises);
const submitPromise = rpc(
`/survey/submit/${this.options.surveyToken}/${this.options.answerToken}`,
params
);
this._nextScreen(submitPromise, options);
},
/**
* @override
* Add client-side validation (required, max size, allowed extensions) for
* file questions, which the base implementation does not know about.
*/
_validateForm: function ($form, formData) {
const result = this._super.apply(this, arguments);
const errors = {};
const inactiveQuestionIds = this.options.sessionInProgress
? []
: this._getInactiveConditionalQuestionIds();
$form.find('input[data-question-type="file"]').each(function () {
const $questionWrapper = $(this).closest(".js_question-wrapper");
const questionId = $questionWrapper.attr("id");
if (inactiveQuestionIds.includes(parseInt(questionId))) {
return;
}
const questionRequired = $questionWrapper.data("required");
const constrErrorMsg =
$questionWrapper.data("constrErrorMsg") ||
_t("This question requires an answer.");
if (questionRequired && !(this.files && this.files.length > 0)) {
// A file may already be stored server-side (e.g. uploaded then
// navigating back): the chip is visible even though the input is
// empty. Treat that as a valid answer.
const container = this.closest(".o_survey_comment_container");
const chip = container && container.querySelector(".o_survey_file_selected");
const hasExistingFile = chip && !chip.classList.contains("d-none");
if (!hasExistingFile) {
errors[questionId] = constrErrorMsg;
_submitForm: function (options) {
var self = this;
var $fileInputs = this.$('input[data-question-type="file"]');
var hasFiles = false;
$fileInputs.each(function () {
if (this.files && this.files.length > 0) {
hasFiles = true;
return false;
}
return;
});
if (!hasFiles || this.options.isStartScreen) {
return this._super(options);
}
if (this.files && this.files.length > 0) {
const file = this.files[0];
const maxSizeMB = parseInt($(this).data("maxFileSize"));
if (maxSizeMB && file.size > maxSizeMB * 1024 * 1024) {
errors[questionId] = _t(
"The file exceeds the maximum allowed size of %s MB.",
maxSizeMB
);
// Async flow: read files then submit
var params = {};
if (options.previousPageId) {
params.previous_page_id = options.previousPageId;
}
var $form = this.$("form");
var formData = new FormData($form[0]);
if (!options.skipValidation) {
if (!this._validateForm($form, formData)) {
return;
}
const allowedExtensions = $(this).data("allowedExtensions");
if (allowedExtensions) {
const allowed = allowedExtensions
.split(",")
.map((e) => e.trim().toLowerCase());
const ext = "." + file.name.split(".").pop().toLowerCase();
if (!allowed.includes(ext)) {
errors[questionId] = _t(
"This file type is not allowed. Accepted formats: %s.",
allowedExtensions
}
this._prepareSubmitValues(formData, params);
// Read all selected files as base64
var filePromises = [];
$fileInputs.each(function () {
if (this.files && this.files.length > 0) {
var file = this.files[0];
var name = this.name;
filePromises.push(
self._readFileAsDataURL(file).then(function (dataURL) {
params[name] = JSON.stringify({
data: dataURL.split(",")[1],
name: file.name,
});
})
);
}
});
this.preventEnterSubmit = true;
if (this.options.sessionInProgress) {
this.fadeInOutDelay = 400;
this.readonly = true;
}
Promise.all(filePromises).then(function () {
var submitPromise = self._rpc({
route: _.str.sprintf(
"%s/%s/%s",
"/survey/submit",
self.options.surveyToken,
self.options.answerToken
),
params: params,
});
self._nextScreen(submitPromise, options);
});
},
_validateForm: function ($form, formData) {
var result = this._super.apply(this, arguments);
var errors = {};
var inactiveQuestionIds = this.options.sessionInProgress
? []
: this._getInactiveConditionalQuestionIds();
$form.find('input[data-question-type="file"]').each(function () {
var $questionWrapper = $(this).closest(".js_question-wrapper");
var questionId = $questionWrapper.attr("id");
if (inactiveQuestionIds.includes(parseInt(questionId))) {
return;
}
var questionRequired = $questionWrapper.data("required");
var constrErrorMsg =
$questionWrapper.data("constrErrorMsg") ||
_t("This question requires an answer.");
if (questionRequired && !(this.files && this.files.length > 0)) {
errors[questionId] = constrErrorMsg;
return;
}
if (this.files && this.files.length > 0) {
var file = this.files[0];
var maxSizeMB = parseInt($(this).data("maxFileSize"));
if (maxSizeMB && file.size > maxSizeMB * 1024 * 1024) {
errors[questionId] = _.str.sprintf(
_t("The file exceeds the maximum allowed size of %s MB."),
maxSizeMB
);
return;
}
var allowedExtensions = $(this).data("allowedExtensions");
if (allowedExtensions) {
var allowed = allowedExtensions.split(",").map(function (e) {
return e.trim().toLowerCase();
});
var ext = "." + file.name.split(".").pop().toLowerCase();
if (!allowed.includes(ext)) {
errors[questionId] = _.str.sprintf(
_t("This file type is not allowed. Accepted formats: %s."),
allowedExtensions
);
}
}
}
});
if (_.keys(errors).length > 0) {
this._showErrors(errors);
return false;
}
});
if (Object.keys(errors).length > 0) {
this._showErrors(errors);
return false;
}
return result;
},
return result;
},
});
});
export default publicWidget.registry.SurveyFormWidget;

View File

@@ -46,14 +46,14 @@ class TestSurveyFileCommon(common.TestSurveyCommon):
class TestSurveyFileSaveLines(TestSurveyFileCommon):
"""Test the _save_lines method for file question type."""
"""Test the save_lines method for file question type."""
def test_save_file_answer(self):
"""Submitting a file stores base64 data and filename."""
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json = json.dumps({"data": self.file_b64, "name": self.file_name})
answer._save_lines(self.question_file, file_json)
answer.save_lines(self.question_file, file_json)
line = answer.user_input_line_ids.filtered(
lambda l: l.question_id == self.question_file
@@ -68,7 +68,7 @@ class TestSurveyFileSaveLines(TestSurveyFileCommon):
"""Submitting empty answer marks the line as skipped."""
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
answer._save_lines(self.question_file, "")
answer.save_lines(self.question_file, "")
line = answer.user_input_line_ids.filtered(
lambda l: l.question_id == self.question_file
@@ -81,11 +81,11 @@ class TestSurveyFileSaveLines(TestSurveyFileCommon):
"""Submitting a new file updates the existing answer line."""
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json_1 = json.dumps({"data": self.file_b64, "name": "first.pdf"})
answer._save_lines(self.question_file, file_json_1)
answer.save_lines(self.question_file, file_json_1)
new_b64 = base64.b64encode(b"Updated content").decode()
file_json_2 = json.dumps({"data": new_b64, "name": "second.pdf"})
answer._save_lines(self.question_file, file_json_2)
answer.save_lines(self.question_file, file_json_2)
lines = answer.user_input_line_ids.filtered(
lambda l: l.question_id == self.question_file
@@ -94,38 +94,19 @@ class TestSurveyFileSaveLines(TestSurveyFileCommon):
self.assertEqual(lines.value_file, new_b64.encode())
self.assertEqual(lines.value_file_fname, "second.pdf")
def test_save_file_then_empty_keeps_file(self):
"""Submitting empty after a file keeps it (file inputs cannot be
pre-filled when navigating back, so an empty answer must not erase it)."""
def test_save_file_then_skip(self):
"""Uploading a file then submitting empty marks line as skipped."""
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json = json.dumps({"data": self.file_b64, "name": self.file_name})
answer._save_lines(self.question_file, file_json)
answer.save_lines(self.question_file, file_json)
answer._save_lines(self.question_file, "")
line = answer.user_input_line_ids.filtered(
lambda l: l.question_id == self.question_file
)
self.assertEqual(len(line), 1)
self.assertFalse(line.skipped)
self.assertEqual(line.value_file, self.file_b64.encode())
self.assertEqual(line.value_file_fname, self.file_name)
def test_save_file_explicitly_cleared(self):
"""Submitting the 'cleared' sentinel after a file removes it."""
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json = json.dumps({"data": self.file_b64, "name": self.file_name})
answer._save_lines(self.question_file, file_json)
answer._save_lines(self.question_file, json.dumps({"cleared": True}))
answer.save_lines(self.question_file, "")
line = answer.user_input_line_ids.filtered(
lambda l: l.question_id == self.question_file
)
self.assertEqual(len(line), 1)
self.assertTrue(line.skipped)
self.assertFalse(line.value_file)
self.assertFalse(line.value_file_fname)
class TestSurveyFileConstraints(TestSurveyFileCommon):
@@ -136,7 +117,7 @@ class TestSurveyFileConstraints(TestSurveyFileCommon):
self.question_file.write({"max_file_size": 0, "allowed_extensions": False})
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json = json.dumps({"data": self.file_b64, "name": "anything.exe"})
answer._save_lines(self.question_file, file_json)
answer.save_lines(self.question_file, file_json)
line = answer.user_input_line_ids.filtered(
lambda l: l.question_id == self.question_file
)
@@ -147,7 +128,7 @@ class TestSurveyFileConstraints(TestSurveyFileCommon):
self.question_file.write({"max_file_size": 10})
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json = json.dumps({"data": self.file_b64, "name": self.file_name})
answer._save_lines(self.question_file, file_json)
answer.save_lines(self.question_file, file_json)
line = answer.user_input_line_ids.filtered(
lambda l: l.question_id == self.question_file
)
@@ -161,14 +142,14 @@ class TestSurveyFileConstraints(TestSurveyFileCommon):
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json = json.dumps({"data": large_b64, "name": self.file_name})
with self.assertRaises(Exception):
answer._save_lines(self.question_file, file_json)
answer.save_lines(self.question_file, file_json)
def test_allowed_extension_passes(self):
"""File with an allowed extension passes."""
self.question_file.write({"allowed_extensions": ".pdf,.docx"})
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json = json.dumps({"data": self.file_b64, "name": "report.docx"})
answer._save_lines(self.question_file, file_json)
answer.save_lines(self.question_file, file_json)
line = answer.user_input_line_ids.filtered(
lambda l: l.question_id == self.question_file
)
@@ -180,14 +161,14 @@ class TestSurveyFileConstraints(TestSurveyFileCommon):
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json = json.dumps({"data": self.file_b64, "name": "script.exe"})
with self.assertRaises(Exception):
answer._save_lines(self.question_file, file_json)
answer.save_lines(self.question_file, file_json)
def test_both_constraints_valid(self):
"""File respecting both size and extension constraints passes."""
self.question_file.write({"max_file_size": 10, "allowed_extensions": ".pdf"})
answer = self._add_answer(self.survey, self.survey_manager.partner_id)
file_json = json.dumps({"data": self.file_b64, "name": self.file_name})
answer._save_lines(self.question_file, file_json)
answer.save_lines(self.question_file, file_json)
line = answer.user_input_line_ids.filtered(
lambda l: l.question_id == self.question_file
)

View File

@@ -8,10 +8,10 @@
<field name="arch" type="xml">
<xpath expr="//page[@name='options']//field[@name='matrix_subtype']" position="before">
<field name="max_file_size"
invisible="question_type != 'file'"
attrs="{'invisible': [('question_type', '!=', 'file')]}"
string="Max File Size (MB)"/>
<field name="allowed_extensions"
invisible="question_type != 'file'"
attrs="{'invisible': [('question_type', '!=', 'file')]}"
placeholder=".pdf,.docx,.xlsx"/>
</xpath>
</field>

Some files were not shown because too many files have changed in this diff Show More