summaryrefslogtreecommitdiff
path: root/addons/survey/wizard
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/survey/wizard
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/survey/wizard')
-rw-r--r--addons/survey/wizard/__init__.py4
-rw-r--r--addons/survey/wizard/survey_invite.py277
-rw-r--r--addons/survey/wizard/survey_invite_views.xml65
3 files changed, 346 insertions, 0 deletions
diff --git a/addons/survey/wizard/__init__.py b/addons/survey/wizard/__init__.py
new file mode 100644
index 00000000..df860dda
--- /dev/null
+++ b/addons/survey/wizard/__init__.py
@@ -0,0 +1,4 @@
+# -*- encoding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import survey_invite
diff --git a/addons/survey/wizard/survey_invite.py b/addons/survey/wizard/survey_invite.py
new file mode 100644
index 00000000..4ad35dbe
--- /dev/null
+++ b/addons/survey/wizard/survey_invite.py
@@ -0,0 +1,277 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import re
+import werkzeug
+
+from odoo import api, fields, models, tools, _
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+emails_split = re.compile(r"[;,\n\r]+")
+
+
+class SurveyInvite(models.TransientModel):
+ _name = 'survey.invite'
+ _description = 'Survey Invitation Wizard'
+
+ @api.model
+ def _get_default_from(self):
+ if self.env.user.email:
+ return tools.formataddr((self.env.user.name, self.env.user.email))
+ raise UserError(_("Unable to post message, please configure the sender's email address."))
+
+ @api.model
+ def _get_default_author(self):
+ return self.env.user.partner_id
+
+ # composer content
+ subject = fields.Char('Subject', compute='_compute_subject', readonly=False, store=True)
+ body = fields.Html('Contents', sanitize_style=True, compute='_compute_body', readonly=False, store=True)
+ attachment_ids = fields.Many2many(
+ 'ir.attachment', 'survey_mail_compose_message_ir_attachments_rel', 'wizard_id', 'attachment_id',
+ string='Attachments')
+ template_id = fields.Many2one(
+ 'mail.template', 'Use template', index=True,
+ domain="[('model', '=', 'survey.user_input')]")
+ # origin
+ email_from = fields.Char('From', default=_get_default_from, help="Email address of the sender.")
+ author_id = fields.Many2one(
+ 'res.partner', 'Author', index=True,
+ ondelete='set null', default=_get_default_author,
+ help="Author of the message.")
+ # recipients
+ partner_ids = fields.Many2many(
+ 'res.partner', 'survey_invite_partner_ids', 'invite_id', 'partner_id', string='Recipients',
+ domain="""[
+ '|', (survey_users_can_signup, '=', 1),
+ '|', (not survey_users_login_required, '=', 1),
+ ('user_ids', '!=', False),
+ ]"""
+ )
+ existing_partner_ids = fields.Many2many(
+ 'res.partner', compute='_compute_existing_partner_ids', readonly=True, store=False)
+ emails = fields.Text(string='Additional emails', help="This list of emails of recipients will not be converted in contacts.\
+ Emails must be separated by commas, semicolons or newline.")
+ existing_emails = fields.Text(
+ 'Existing emails', compute='_compute_existing_emails',
+ readonly=True, store=False)
+ existing_mode = fields.Selection([
+ ('new', 'New invite'), ('resend', 'Resend invite')],
+ string='Handle existing', default='resend', required=True)
+ existing_text = fields.Text('Resend Comment', compute='_compute_existing_text', readonly=True, store=False)
+ # technical info
+ mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
+ # survey
+ survey_id = fields.Many2one('survey.survey', string='Survey', required=True)
+ survey_start_url = fields.Char('Survey URL', compute='_compute_survey_start_url')
+ survey_access_mode = fields.Selection(related="survey_id.access_mode", readonly=True)
+ survey_users_login_required = fields.Boolean(related="survey_id.users_login_required", readonly=True)
+ survey_users_can_signup = fields.Boolean(related='survey_id.users_can_signup')
+ deadline = fields.Datetime(string="Answer deadline")
+
+ @api.depends('partner_ids', 'survey_id')
+ def _compute_existing_partner_ids(self):
+ existing_answers = self.survey_id.user_input_ids
+ self.existing_partner_ids = existing_answers.mapped('partner_id') & self.partner_ids
+
+ @api.depends('emails', 'survey_id')
+ def _compute_existing_emails(self):
+ emails = list(set(emails_split.split(self.emails or "")))
+ existing_emails = self.survey_id.mapped('user_input_ids.email')
+ self.existing_emails = '\n'.join(email for email in emails if email in existing_emails)
+
+ @api.depends('existing_partner_ids', 'existing_emails')
+ def _compute_existing_text(self):
+ existing_text = False
+ if self.existing_partner_ids:
+ existing_text = '%s: %s.' % (
+ _('The following customers have already received an invite'),
+ ', '.join(self.mapped('existing_partner_ids.name'))
+ )
+ if self.existing_emails:
+ existing_text = '%s\n' % existing_text if existing_text else ''
+ existing_text += '%s: %s.' % (
+ _('The following emails have already received an invite'),
+ self.existing_emails
+ )
+
+ self.existing_text = existing_text
+
+ @api.depends('survey_id.access_token')
+ def _compute_survey_start_url(self):
+ base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+ for invite in self:
+ invite.survey_start_url = werkzeug.urls.url_join(base_url, invite.survey_id.get_start_url()) if invite.survey_id else False
+
+ @api.onchange('emails')
+ def _onchange_emails(self):
+ if self.emails and (self.survey_users_login_required and not self.survey_id.users_can_signup):
+ raise UserError(_('This survey does not allow external people to participate. You should create user accounts or update survey access mode accordingly.'))
+ if not self.emails:
+ return
+ valid, error = [], []
+ emails = list(set(emails_split.split(self.emails or "")))
+ for email in emails:
+ email_check = tools.email_split_and_format(email)
+ if not email_check:
+ error.append(email)
+ else:
+ valid.extend(email_check)
+ if error:
+ raise UserError(_("Some emails you just entered are incorrect: %s") % (', '.join(error)))
+ self.emails = '\n'.join(valid)
+
+ @api.onchange('partner_ids')
+ def _onchange_partner_ids(self):
+ if self.survey_users_login_required and self.partner_ids:
+ if not self.survey_id.users_can_signup:
+ invalid_partners = self.env['res.partner'].search([
+ ('user_ids', '=', False),
+ ('id', 'in', self.partner_ids.ids)
+ ])
+ if invalid_partners:
+ raise UserError(_(
+ 'The following recipients have no user account: %s. You should create user accounts for them or allow external signup in configuration.',
+ ', '.join(invalid_partners.mapped('name'))
+ ))
+
+ @api.depends('template_id')
+ def _compute_subject(self):
+ for invite in self:
+ if invite.template_id:
+ invite.subject = invite.template_id.subject
+ elif not invite.subject:
+ invite.subject = False
+
+ @api.depends('template_id')
+ def _compute_body(self):
+ for invite in self:
+ if invite.template_id:
+ invite.body = invite.template_id.body_html
+ elif not invite.body:
+ invite.body = False
+
+ @api.model
+ def create(self, values):
+ if values.get('template_id') and not (values.get('body') or values.get('subject')):
+ template = self.env['mail.template'].browse(values['template_id'])
+ if not values.get('subject'):
+ values['subject'] = template.subject
+ if not values.get('body'):
+ values['body'] = template.body_html
+ return super(SurveyInvite, self).create(values)
+
+ # ------------------------------------------------------
+ # Wizard validation and send
+ # ------------------------------------------------------
+
+ def _prepare_answers(self, partners, emails):
+ answers = self.env['survey.user_input']
+ existing_answers = self.env['survey.user_input'].search([
+ '&', ('survey_id', '=', self.survey_id.id),
+ '|',
+ ('partner_id', 'in', partners.ids),
+ ('email', 'in', emails)
+ ])
+ partners_done = self.env['res.partner']
+ emails_done = []
+ if existing_answers:
+ if self.existing_mode == 'resend':
+ partners_done = existing_answers.mapped('partner_id')
+ emails_done = existing_answers.mapped('email')
+
+ # only add the last answer for each user of each type (partner_id & email)
+ # to have only one mail sent per user
+ for partner_done in partners_done:
+ answers |= next(existing_answer for existing_answer in
+ existing_answers.sorted(lambda answer: answer.create_date, reverse=True)
+ if existing_answer.partner_id == partner_done)
+
+ for email_done in emails_done:
+ answers |= next(existing_answer for existing_answer in
+ existing_answers.sorted(lambda answer: answer.create_date, reverse=True)
+ if existing_answer.email == email_done)
+
+ for new_partner in partners - partners_done:
+ answers |= self.survey_id._create_answer(partner=new_partner, check_attempts=False, **self._get_answers_values())
+ for new_email in [email for email in emails if email not in emails_done]:
+ answers |= self.survey_id._create_answer(email=new_email, check_attempts=False, **self._get_answers_values())
+
+ return answers
+
+ def _get_answers_values(self):
+ return {
+ 'deadline': self.deadline,
+ }
+
+ def _send_mail(self, answer):
+ """ Create mail specific for recipient containing notably its access token """
+ subject = self.env['mail.render.mixin'].with_context(safe=True)._render_template(self.subject, 'survey.user_input', answer.ids, post_process=True)[answer.id]
+ body = self.env['mail.render.mixin']._render_template(self.body, 'survey.user_input', answer.ids, post_process=True)[answer.id]
+ # post the message
+ mail_values = {
+ 'email_from': self.email_from,
+ 'author_id': self.author_id.id,
+ 'model': None,
+ 'res_id': None,
+ 'subject': subject,
+ 'body_html': body,
+ 'attachment_ids': [(4, att.id) for att in self.attachment_ids],
+ 'auto_delete': True,
+ }
+ if answer.partner_id:
+ mail_values['recipient_ids'] = [(4, answer.partner_id.id)]
+ else:
+ mail_values['email_to'] = answer.email
+
+ # optional support of notif_layout in context
+ notif_layout = self.env.context.get('notif_layout', self.env.context.get('custom_layout'))
+ if notif_layout:
+ try:
+ template = self.env.ref(notif_layout, raise_if_not_found=True)
+ except ValueError:
+ _logger.warning('QWeb template %s not found when sending survey mails. Sending without layouting.' % (notif_layout))
+ else:
+ template_ctx = {
+ 'message': self.env['mail.message'].sudo().new(dict(body=mail_values['body_html'], record_name=self.survey_id.title)),
+ 'model_description': self.env['ir.model']._get('survey.survey').display_name,
+ 'company': self.env.company,
+ }
+ body = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
+ mail_values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
+
+ return self.env['mail.mail'].sudo().create(mail_values)
+
+ def action_invite(self):
+ """ Process the wizard content and proceed with sending the related
+ email(s), rendering any template patterns on the fly if needed """
+ self.ensure_one()
+ Partner = self.env['res.partner']
+
+ # compute partners and emails, try to find partners for given emails
+ valid_partners = self.partner_ids
+ valid_emails = []
+ for email in emails_split.split(self.emails or ''):
+ partner = False
+ email_normalized = tools.email_normalize(email)
+ if email_normalized:
+ limit = None if self.survey_users_login_required else 1
+ partner = Partner.search([('email_normalized', '=', email_normalized)], limit=limit)
+ if partner:
+ valid_partners |= partner
+ else:
+ email_formatted = tools.email_split_and_format(email)
+ if email_formatted:
+ valid_emails.extend(email_formatted)
+
+ if not valid_partners and not valid_emails:
+ raise UserError(_("Please enter at least one valid recipient."))
+
+ answers = self._prepare_answers(valid_partners, valid_emails)
+ for answer in answers:
+ self._send_mail(answer)
+
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/survey/wizard/survey_invite_views.xml b/addons/survey/wizard/survey_invite_views.xml
new file mode 100644
index 00000000..1c2d66cc
--- /dev/null
+++ b/addons/survey/wizard/survey_invite_views.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <record model="ir.ui.view" id="survey_invite_view_form">
+ <field name="name">survey.invite.view.form</field>
+ <field name="model">survey.invite</field>
+ <field name="arch" type="xml">
+ <form string="Compose Email">
+ <group col="1">
+ <group col="2">
+ <field name="survey_access_mode" invisible="1"/>
+ <field name="survey_users_login_required" invisible="1"/>
+ <field name="survey_users_can_signup" invisible="1"/>
+ <field name="survey_id" readonly="context.get('default_survey_id')"/>
+ <field name="existing_mode" widget="radio" invisible="1" />
+ <field name="survey_start_url" label="Public share URL" readonly="1" widget="CopyClipboardChar"
+ attrs="{'invisible':[('survey_access_mode', '!=', 'public')]}"
+ class="mb16"/>
+ <field name="partner_ids"
+ widget="many2many_tags_email"
+ placeholder="Add existing contacts..."
+ context="{'force_email':True, 'show_email':True, 'no_create_edit': True}"/>
+ <field name="emails"
+ attrs="{
+ 'invisible': [('survey_users_login_required', '=', True)],
+ }"
+ placeholder="Add a list of email of recipients (will not be converted into contacts). Separated by commas, semicolons or newline..."/>
+ </group>
+ <div col="2" class="alert alert-warning" role="alert"
+ attrs="{'invisible': ['|', ('survey_access_mode', '=', 'public'), ('existing_text', '=', False)]}">
+ <field name="existing_text"/>
+ <group col="2">
+ <label for="existing_mode" string="Handle existing"/>
+ <div>
+ <field name="existing_mode" nolabel="1"/>
+ <p attrs="{'invisible': [('existing_mode', '!=', 'resend')]}">Customers will receive the same token.</p>
+ <p attrs="{'invisible': [('existing_mode', '!=', 'new')]}">Customers will receive a new token and be able to completely retake the survey.</p>
+ </div>
+ </group>
+ <field name="existing_partner_ids" invisible="1"/>
+ <field name="existing_emails" invisible="1"/>
+ </div>
+ <group col="2">
+ <field name="subject" placeholder="Subject..."/>
+ </group>
+ <field name="body" options="{'style-inline': true}"/>
+ <group>
+ <group>
+ <field name="attachment_ids" widget="many2many_binary"/>
+ </group>
+ <group>
+ <field name="deadline"/>
+ <field name="template_id" label="Use template"/>
+ </group>
+ </group>
+ </group>
+ <footer>
+ <button string="Send" name="action_invite" type="object" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+ </data>
+</odoo>