diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/survey/wizard | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/survey/wizard')
| -rw-r--r-- | addons/survey/wizard/__init__.py | 4 | ||||
| -rw-r--r-- | addons/survey/wizard/survey_invite.py | 277 | ||||
| -rw-r--r-- | addons/survey/wizard/survey_invite_views.xml | 65 |
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> |
