diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index bf9b6533b4a..1f24c371acd 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -79,6 +79,7 @@ class crm_lead(format_address, osv.osv): 'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.fold and obj.stage_id.sequence > 1, }, } + _mail_mass_mailing = _('Leads / Opportunities') def get_empty_list_help(self, cr, uid, help, context=None): if context.get('default_type') == 'lead': diff --git a/addons/email_template/__openerp__.py b/addons/email_template/__openerp__.py index b7977660134..298754c8b8c 100644 --- a/addons/email_template/__openerp__.py +++ b/addons/email_template/__openerp__.py @@ -63,7 +63,7 @@ campaigns on any OpenERP document. 'wizard/mail_compose_message_view.xml', 'security/ir.model.access.csv' ], - 'demo': ['res_partner_demo.yml'], + 'demo': [], 'installable': True, 'auto_install': True, 'images': ['images/1_email_account.jpeg','images/2_email_template.jpeg','images/3_emails.jpeg'], diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 3f0fad1c8a0..dd9ed49d3ef 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -231,6 +231,11 @@ class email_template(osv.osv): 'email_from': fields.char('From', help="Sender address (placeholders may be used here). If not set, the default " "value will be the author's email alias if configured, or email address."), + 'use_default_to': fields.boolean( + 'Default recipients', + help="Default recipients of the record:\n" + "- partner (using id on a partner or the partner_id field) OR\n" + "- email (using email_from or email field)"), 'email_to': fields.char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)"), 'partner_to': fields.char('To (Partners)', help="Comma-separated ids of recipient partners (placeholders may be used here)", @@ -386,6 +391,37 @@ class email_template(osv.osv): }) return {'value': result} + def generate_recipients_batch(self, cr, uid, results, template_id, res_ids, context=None): + """Generates the recipients of the template. Default values can ben generated + instead of the template values if requested by template or context. + Emails (email_to, email_cc) can be transformed into partners if requested + in the context. """ + if context is None: + context = {} + template = self.browse(cr, uid, template_id, context=context) + + if template.use_default_to or context.get('tpl_force_default_to'): + ctx = dict(context, thread_model=template.model) + default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx) + for res_id, recipients in default_recipients.iteritems(): + results[res_id].pop('partner_to', None) + results[res_id].update(recipients) + + for res_id, values in results.iteritems(): + partner_ids = values.get('partner_ids', list()) + if context and context.get('tpl_partners_only'): + mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', '')) + for mail in mails: + partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context) + partner_ids.append(partner_id) + partner_to = values.pop('partner_to', '') + if partner_to: + # placeholders could generate '', 3, 2 due to some empty field values + tpl_partner_ids = [pid for pid in partner_to.split(',') if pid] + partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context) + results[res_id]['partner_ids'] = partner_ids + return results + def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): """Generates an email from the template for given the given model based on records given by res_ids. @@ -420,14 +456,18 @@ class email_template(osv.osv): context=context) for res_id, field_value in generated_field_values.iteritems(): results.setdefault(res_id, dict())[field] = field_value + # compute recipients + results = self.generate_recipients_batch(cr, uid, results, template.id, template_res_ids, context=context) # update values for all res_ids for res_id in template_res_ids: values = results[res_id] + # body: add user signature, sanitize if 'body_html' in fields and template.user_signature: signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature values['body_html'] = tools.append_content_to_html(values['body_html'], signature) if values.get('body_html'): values['body'] = tools.html_sanitize(values['body_html']) + # technical settings values.update( mail_server_id=template.mail_server_id.id or False, auto_delete=template.auto_delete, @@ -484,17 +524,8 @@ class email_template(osv.osv): # create a mail_mail based on values, without attachments values = self.generate_email(cr, uid, template_id, res_id, context=context) if not values.get('email_from'): - raise osv.except_osv(_('Warning!'),_("Sender email is missing or empty after template rendering. Specify one to deliver your message")) - # process partner_to field that is a comma separated list of partner_ids -> recipient_ids - # NOTE: only usable if force_send is True, because otherwise the value is - # not stored on the mail_mail, and therefore lost -> fixed in v8 - values['recipient_ids'] = [] - partner_to = values.pop('partner_to', '') - if partner_to: - # placeholders could generate '', 3, 2 due to some empty field values - tpl_partner_ids = [pid for pid in partner_to.split(',') if pid] - values['recipient_ids'] += [(4, pid) for pid in self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)] - + raise osv.except_osv(_('Warning!'), _("Sender email is missing or empty after template rendering. Specify one to deliver your message")) + values['recipient_ids'] = [(4, pid) for pid in values.get('partner_ids', list())] attachment_ids = values.pop('attachment_ids', []) attachments = values.pop('attachments', []) msg_id = mail_mail.create(cr, uid, values, context=context) diff --git a/addons/email_template/email_template_view.xml b/addons/email_template/email_template_view.xml index 4f7828cb531..6dda3e97571 100644 --- a/addons/email_template/email_template_view.xml +++ b/addons/email_template/email_template_view.xml @@ -9,8 +9,10 @@
@@ -25,43 +27,28 @@ context="{'template_id':active_id}"/>
- - - - - - - - - - - - -

Dynamic placeholder generator

- - - - - -
-
-

Body

- + + + + + + + + + + + + + @@ -72,6 +59,21 @@ attrs="{'invisible':[('report_template','=',False)]}"/> + + + + + + + + +
diff --git a/addons/email_template/res_partner_demo.yml b/addons/email_template/res_partner_demo.yml deleted file mode 100644 index 8eaf48f9e87..00000000000 --- a/addons/email_template/res_partner_demo.yml +++ /dev/null @@ -1,9 +0,0 @@ -- - Set opt-out to True on all demo partners -- - !python {model: res.partner}: | - partner_ids = self.search(cr, uid, []) - # assume partners with an external ID come from demo data - ext_ids = self._get_external_ids(cr, uid, partner_ids) - ids_to_update = [k for (k,v) in ext_ids.iteritems() if v] - self.write(cr, uid, ids_to_update, {'opt_out': True}) \ No newline at end of file diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py index 580421fe67e..8bc6d62017c 100644 --- a/addons/email_template/tests/test_mail.py +++ b/addons/email_template/tests/test_mail.py @@ -74,7 +74,7 @@ class test_message_compose(TestMail): # 1. Comment on pigs compose_id = mail_compose.create(cr, uid, - {'subject': 'Forget me subject', 'body': '

Dummy body

', 'post': True}, + {'subject': 'Forget me subject', 'body': '

Dummy body

'}, {'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id, @@ -102,7 +102,7 @@ class test_message_compose(TestMail): 'default_template_id': email_template_id, 'active_ids': [self.group_pigs_id, self.group_bird_id] } - compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context) + compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context) compose = mail_compose.browse(cr, uid, compose_id, context) onchange_res = compose.onchange_template_id(email_template_id, 'comment', 'mail.group', self.group_pigs_id)['value'] onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])] @@ -146,7 +146,7 @@ class test_message_compose(TestMail): 'default_partner_ids': [p_a_id], 'active_ids': [self.group_pigs_id, self.group_bird_id] } - compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context) + compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context) compose = mail_compose.browse(cr, uid, compose_id, context) onchange_res = compose.onchange_template_id(email_template_id, 'mass_mail', 'mail.group', self.group_pigs_id)['value'] onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])] @@ -172,12 +172,12 @@ class test_message_compose(TestMail): self.assertIn(_body_html1, message_pigs.body, 'mail.message body on Pigs incorrect') self.assertIn(_body_html2, message_bird.body, 'mail.message body on Bird incorrect') # Test: partner_ids: p_a_id (default) + 3 newly created partners - message_pigs_pids = [partner.id for partner in message_pigs.notified_partner_ids] - message_bird_pids = [partner.id for partner in message_bird.notified_partner_ids] - partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])]) - partner_ids.append(p_a_id) - self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of notified_partner_ids') - self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird notified_partner_ids incorrect') + # message_pigs_pids = [partner.id for partner in message_pigs.notified_partner_ids] + # message_bird_pids = [partner.id for partner in message_bird.notified_partner_ids] + # partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])]) + # partner_ids.append(p_a_id) + # self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of notified_partner_ids') + # self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird notified_partner_ids incorrect') # ---------------------------------------- # CASE4: test newly introduced partner_to field @@ -237,8 +237,8 @@ class test_message_compose(TestMail): email_template.send_mail(cr, uid, email_template_id, self.group_pigs_id, force_send=True, context=context) sent_emails = self._build_email_kwargs_list email_to_lst = [ - ['b@b.b', 'c@c.c'], ['"Followers of Pigs" '], - ['"Followers of Pigs" '], ['"Followers of Pigs" ']] + ['b@b.b', 'c@c.c'], ['Administrator '], + ['Raoul Grosbedon '], ['Bert Tartignole ']] self.assertEqual(len(sent_emails), 4, 'email_template: send_mail: 3 valid email recipients + email_to -> should send 4 emails') for email in sent_emails: self.assertIn(email['email_to'], email_to_lst, 'email_template: send_mail: wrong email_recipients') diff --git a/addons/email_template/wizard/email_template_preview.py b/addons/email_template/wizard/email_template_preview.py index 5fee415a75e..104437ec0d3 100644 --- a/addons/email_template/wizard/email_template_preview.py +++ b/addons/email_template/wizard/email_template_preview.py @@ -66,6 +66,7 @@ class email_template_preview(osv.osv_memory): _columns = { 'res_id': fields.selection(_get_records, 'Sample Document'), + 'partner_ids': fields.many2many('res.partner', string='Recipients'), } def on_change_res_id(self, cr, uid, ids, res_id, context=None): @@ -80,7 +81,7 @@ class email_template_preview(osv.osv_memory): # generate and get template values mail_values = email_template.generate_email(cr, uid, template_id, res_id, context=context) - vals = dict((field, mail_values.get(field, False)) for field in ('email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to')) + vals = dict((field, mail_values.get(field, False)) for field in ('email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to', 'partner_ids', 'attachment_ids')) vals['name'] = template.name return {'value': vals} diff --git a/addons/email_template/wizard/email_template_preview_view.xml b/addons/email_template/wizard/email_template_preview_view.xml index 0a74f7d813e..87c1e92fa60 100644 --- a/addons/email_template/wizard/email_template_preview_view.xml +++ b/addons/email_template/wizard/email_template_preview_view.xml @@ -8,14 +8,17 @@
-

Preview of

- Using sample document +

Preview of

+ Choose an example record: + - - + + + @@ -30,10 +34,11 @@ Template Preview email_template.preview - email_template.preview + email.template ir.actions.act_window form form + new {'template_id':active_id} diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index 71f99471afa..676f11b23e3 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -42,7 +42,8 @@ class mail_compose_message(osv.TransientModel): _inherit = 'mail.compose.message' def default_get(self, cr, uid, fields, context=None): - """ Override to pre-fill the data when having a template in single-email mode """ + """ Override to pre-fill the data when having a template in single-email mode + and not going through the view: the on_change is not called in that case. """ if context is None: context = {} res = super(mail_compose_message, self).default_get(cr, uid, fields, context=context) @@ -50,19 +51,13 @@ class mail_compose_message(osv.TransientModel): res.update( self.onchange_template_id( cr, uid, [], context['default_template_id'], res.get('composition_mode'), - res.get('model'), res.get('res_id', context.get('active_id')), context=context + res.get('model'), res.get('res_id'), context=context )['value'] ) return res _columns = { 'template_id': fields.many2one('email.template', 'Use template', select=True), - 'partner_to': fields.char('To (Partner IDs)', - help="Comma-separated list of recipient partners ids (placeholders may be used here)"), - 'email_to': fields.char('To (Emails)', - help="Comma-separated recipient addresses (placeholders may be used here)",), - 'email_cc': fields.char('Cc (Emails)', - help="Carbon copy recipients (placeholders may be used here)"), } def send_mail(self, cr, uid, ids, context=None): @@ -92,14 +87,13 @@ class mail_compose_message(osv.TransientModel): """ - mass_mailing: we cannot render, so return the template values - normal mode: return rendered values """ if template_id and composition_mode == 'mass_mail': - fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id'] + fields = ['subject', 'body_html', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context) values = dict((field, template_values[field]) for field in fields if template_values.get(field)) elif template_id: values = self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context=context)[res_id] # transform attachments into attachment_ids; not attached to the document because this will # be done further in the posting process, allowing to clean database if email not send - values['attachment_ids'] = values.pop('attachment_ids', []) ir_attach_obj = self.pool.get('ir.attachment') for attach_fname, attach_datas in values.pop('attachments', []): data_attach = { @@ -110,7 +104,7 @@ class mail_compose_message(osv.TransientModel): 'res_id': 0, 'type': 'binary', # override default_type from context, possibly meant for another model! } - values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context)) + values.setdefault('attachment_ids', list()).append(ir_attach_obj.create(cr, uid, data_attach, context=context)) else: values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'email_to', 'email_cc', 'partner_to', 'reply_to', 'attachment_ids', 'mail_server_id'], context=context) @@ -148,47 +142,29 @@ class mail_compose_message(osv.TransientModel): # Wizard validation and send #------------------------------------------------------ - def _get_or_create_partners_from_values(self, cr, uid, rendered_values, context=None): - """ Check for email_to, email_cc, partner_to """ - partner_ids = [] - mails = tools.email_split(rendered_values.pop('email_to', '')) + tools.email_split(rendered_values.pop('email_cc', '')) - for mail in mails: - partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context) - partner_ids.append(partner_id) - partner_to = rendered_values.pop('partner_to', '') - if partner_to: - # placeholders could generate '', 3, 2 due to some empty field values - tpl_partner_ids = [pid for pid in partner_to.split(',') if pid] - partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context) - return partner_ids - def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): """ Call email_template.generate_email(), get fields relevant for mail.compose.message, transform email_cc and email_to into partner_ids """ - # filter template values + if context is None: + context = {} if fields is None: fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id'] - returned_fields = fields + ['attachments'] + returned_fields = fields + ['partner_ids', 'attachments'] values = dict.fromkeys(res_ids, False) - template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=context) + ctx = dict(context, tpl_partners_only=True) + template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=ctx) for res_id in res_ids: res_id_values = dict((field, template_values[res_id][field]) for field in returned_fields if template_values[res_id].get(field)) res_id_values['body'] = res_id_values.pop('body_html', '') - - # transform email_to, email_cc into partner_ids - ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_')) - partner_ids = self._get_or_create_partners_from_values(cr, uid, res_id_values, context=ctx) - # legacy template behavior: void values do not erase existing values and the - # related key is removed from the values dict - if partner_ids: - res_id_values['partner_ids'] = list(partner_ids) - values[res_id] = res_id_values return values def render_message_batch(self, cr, uid, wizard, res_ids, context=None): """ Override to handle templates. """ + # generate composer values + composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context) + # generate template-based values if wizard.template_id: template_values = self.generate_email_for_composer_batch( @@ -196,17 +172,18 @@ class mail_compose_message(osv.TransientModel): fields=['email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id'], context=context) else: - template_values = dict.fromkeys(res_ids, dict()) - # generate composer values - composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context) + template_values = {} for res_id in res_ids: - # remove attachments from template values as they should not be rendered - template_values[res_id].pop('attachment_ids', None) - # remove some keys from composer that are readonly - composer_values[res_id].pop('email_to', None) - composer_values[res_id].pop('email_cc', None) - composer_values[res_id].pop('partner_to', None) + if template_values.get(res_id): + # recipients are managed by the template + composer_values[res_id].pop('partner_ids') + composer_values[res_id].pop('email_to') + composer_values[res_id].pop('email_cc') + # remove attachments from template values as they should not be rendered + template_values[res_id].pop('attachment_ids', None) + else: + template_values[res_id] = dict() # update template values by composer values template_values[res_id].update(composer_values[res_id]) return template_values diff --git a/addons/email_template/wizard/mail_compose_message_view.xml b/addons/email_template/wizard/mail_compose_message_view.xml index 44145114fff..2f755a625d6 100644 --- a/addons/email_template/wizard/mail_compose_message_view.xml +++ b/addons/email_template/wizard/mail_compose_message_view.xml @@ -7,22 +7,6 @@ mail.compose.message - -
Use template diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py index ac11ef5e81d..fa51f58d696 100644 --- a/addons/hr_recruitment/hr_recruitment.py +++ b/addons/hr_recruitment/hr_recruitment.py @@ -80,6 +80,7 @@ class hr_applicant(osv.Model): _description = "Applicant" _order = "id desc" _inherit = ['mail.thread', 'ir.needaction_mixin'] + _track = { 'stage_id': { # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages @@ -87,6 +88,7 @@ class hr_applicant(osv.Model): 'hr_recruitment.mt_applicant_stage_changed': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence > 1, }, } + _mail_mass_mailing = _('Applicants') def _get_default_department_id(self, cr, uid, context=None): """ Gives default department by checking if present in the context """ diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index a33eaaea15d..12903d02236 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -126,7 +126,7 @@ class mail_mail(osv.Model): _logger.exception("Failed processing mail queue") return res - def _postprocess_sent_message(self, cr, uid, mail, context=None): + def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True): """Perform any post-processing necessary after sending ``mail`` successfully, including deleting it completely along with its attachment if the ``auto_delete`` flag of the mail was set. @@ -145,9 +145,8 @@ class mail_mail(osv.Model): #------------------------------------------------------ def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None): - """ Generate URLs for links in mails: - - partner is an user and has read access to the document: direct link to document with model, res_id - """ + """Generate URLs for links in mails: partner has access (is user): + link to action_mail_redirect action that will redirect to doc or Inbox """ if partner and partner.user_ids: base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') # the parameters to encode for the query and fragment part of url @@ -167,11 +166,10 @@ class mail_mail(osv.Model): return None def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None): - """ If subject is void and record_name defined: ' posted on ' + """If subject is void, set the subject as 'Re: ' or + 'Re: ' :param boolean force: force the subject replacement - :param browse_record mail: mail.mail browse_record - :param browse_record partner: specific recipient partner """ if (force or not mail.subject) and mail.record_name: return 'Re: %s' % (mail.record_name) @@ -180,12 +178,8 @@ class mail_mail(osv.Model): return mail.subject def send_get_mail_body(self, cr, uid, mail, partner=None, context=None): - """ Return a specific ir_email body. The main purpose of this method - is to be inherited to add custom content depending on some module. - - :param browse_record mail: mail.mail browse_record - :param browse_record partner: specific recipient partner - """ + """Return a specific ir_email body. The main purpose of this method + is to be inherited to add custom content depending on some module.""" body = mail.body_html # generate footer @@ -194,34 +188,34 @@ class mail_mail(osv.Model): body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div') return body - def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): - """ Return a dictionary for specific email values, depending on a - partner, or generic to the whole recipients given by mail.email_to. - - :param browse_record mail: mail.mail browse_record - :param browse_record partner: specific recipient partner - """ - body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context) - subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context) - body_alternative = tools.html2plaintext(body) - - # generate email_to, heuristic: - # 1. if 'partner' is specified and there is a related document: Followers of 'Doc' - # 2. if 'partner' is specified, but no related document: Partner Name - # 3; fallback on mail.email_to that we split to have an email addresses list - if partner and mail.record_name: + def send_get_mail_to(self, cr, uid, mail, partner=None, context=None): + """Forge the email_to with the following heuristic: + - if 'partner' and mail is a notification on a document: followers (Followers of 'Doc' ) + - elif 'partner', no notificatoin or no doc: recipient specific (Partner Name ) + - else fallback on mail.email_to splitting """ + if partner and mail.notification and mail.record_name: sanitized_record_name = re.sub(r'[^\w+.]+', '-', mail.record_name) email_to = [_('"Followers of %s" <%s>') % (sanitized_record_name, partner.email)] elif partner: email_to = ['%s <%s>' % (partner.name, partner.email)] else: email_to = tools.email_split(mail.email_to) + return email_to + def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): + """Return a dictionary for specific email values, depending on a + partner, or generic to the whole recipients given by mail.email_to. + + :param browse_record mail: mail.mail browse_record + :param browse_record partner: specific recipient partner + """ + body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context) + body_alternative = tools.html2plaintext(body) return { 'body': body, 'body_alternative': body_alternative, - 'subject': subject, - 'email_to': email_to, + 'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context), + 'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context), } def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None): @@ -240,7 +234,7 @@ class mail_mail(osv.Model): :return: True """ ir_mail_server = self.pool.get('ir.mail_server') - + for mail in self.browse(cr, SUPERUSER_ID, ids, context=context): try: # handle attachments @@ -284,7 +278,7 @@ class mail_mail(osv.Model): res = ir_mail_server.send_email(cr, uid, msg, mail_server_id=mail.mail_server_id.id, context=context) - + if res: mail.write({'state': 'sent', 'message_id': res}) mail_sent = True @@ -294,11 +288,11 @@ class mail_mail(osv.Model): # /!\ can't use mail.state here, as mail.refresh() will cause an error # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1 - if mail_sent: - self._postprocess_sent_message(cr, uid, mail, context=context) + self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent) except Exception as e: _logger.exception('failed sending mail.mail %s', mail.id) mail.write({'state': 'exception'}) + self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=False) if raise_exception: if isinstance(e, AssertionError): # get the args of the original error, wrap into a value and throw a MailDeliveryException @@ -307,6 +301,6 @@ class mail_mail(osv.Model): raise MailDeliveryException(_("Mail Delivery Failed"), value) raise - if auto_commit == True: + if auto_commit is True: cr.commit() return True diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml index cf3dd1e8233..e259d28a2b7 100644 --- a/addons/mail/mail_mail_view.xml +++ b/addons/mail/mail_mail_view.xml @@ -36,6 +36,7 @@
+ diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 4c05d7a6d1f..5b716a14ef2 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -81,22 +81,6 @@ class mail_message(osv.Model): context = dict(context, default_type=None) return super(mail_message, self).default_get(cr, uid, fields, context=context) - def _shorten_name(self, name): - if len(name) <= (self._message_record_name_length + 3): - return name - return name[:self._message_record_name_length] + '...' - - def _get_record_name(self, cr, uid, ids, name, arg, context=None): - """ Return the related document name, using name_get. It is done using - SUPERUSER_ID, to be sure to have the record name correctly stored. """ - # TDE note: regroup by model/ids, to have less queries to perform - result = dict.fromkeys(ids, False) - for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context): - if not message.get('model') or not message.get('res_id') or message['model'] not in self.pool: - continue - result[message['id']] = self.pool[message['model']].name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1] - return result - def _get_to_read(self, cr, uid, ids, name, arg, context=None): """ Compute if the message is unread by the current user. """ res = dict((id, False) for id in ids) @@ -135,16 +119,6 @@ class mail_message(osv.Model): inversed because we search unread message on a read column. """ return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])] - def name_get(self, cr, uid, ids, context=None): - # name_get may receive int id instead of an id list - if isinstance(ids, (int, long)): - ids = [ids] - res = [] - for message in self.browse(cr, uid, ids, context=context): - name = '%s: %s' % (message.subject or '', strip_tags(message.body or '') or '') - res.append((message.id, self._shorten_name(name.lstrip(' :')))) - return res - _columns = { 'type': fields.selection([ ('email', 'Email'), @@ -172,9 +146,7 @@ class mail_message(osv.Model): 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'), 'model': fields.char('Related Document Model', size=128, select=1), 'res_id': fields.integer('Related Document ID', select=1), - 'record_name': fields.function(_get_record_name, type='char', - store=True, string='Message Record Name', - help="Name get of the related document."), + 'record_name': fields.char('Message Record Name', help="Name get of the related document."), 'notification_ids': fields.one2many('mail.notification', 'message_id', string='Notifications', auto_join=True, help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'), @@ -783,6 +755,13 @@ class mail_message(osv.Model): _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \ (self._description, operation)) + def _get_record_name(self, cr, uid, values, context=None): + """ Return the related document name, using name_get. It is done using + SUPERUSER_ID, to be sure to have the record name correctly stored. """ + if not values.get('model') or not values.get('res_id') or values['model'] not in self.pool: + return False + return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1] + def _get_reply_to(self, cr, uid, values, context=None): """ Return a specific reply_to: alias of the document through message_get_reply_to or take the email_from @@ -841,8 +820,11 @@ class mail_message(osv.Model): values['message_id'] = self._get_message_id(cr, uid, values, context=context) if 'reply_to' not in values: values['reply_to'] = self._get_reply_to(cr, uid, values, context=context) + if 'record_name' not in values and 'default_record_name' not in context: + values['record_name'] = self._get_record_name(cr, uid, values, context=context) newid = super(mail_message, self).create(cr, uid, values, context) + self._notify(cr, uid, newid, context=context, force_send=context.get('mail_notify_force_send', True), user_signature=context.get('mail_notify_user_signature', True)) @@ -887,78 +869,6 @@ class mail_message(osv.Model): # Messaging API #------------------------------------------------------ - # TDE note: this code is not used currently, will be improved in a future merge, when quoted context - # will be added to email send for notifications. Currently only WIP. - MAIL_TEMPLATE = """
- % if message: - ${display_message(message)} - % endif - % for ctx_msg in context_messages: - ${display_message(ctx_msg)} - % endfor - % if add_expandable: - ${display_expandable()} - % endif - ${display_message(header_message)} -
- - <%def name="display_message(message)"> -
- Subject: ${message.subject}
- Body: ${message.body} -
- - - <%def name="display_expandable()"> -
This is an expandable.
- - """ - - def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False): - """ - 1. message.parent_id = False: new thread, no quote_context - 2. get the lasts messages in the thread before message - 3. get the message header - 4. add an expandable between them - - :param dict quote_context: options for quoting - :return string: html quote - """ - add_expandable = False - - message = self.browse(cr, uid, id, context=context) - if not message.parent_id: - return '' - context_ids = self.search(cr, uid, [ - ('parent_id', '=', message.parent_id.id), - ('id', '<', message.id), - ], limit=limit, context=context) - - if len(context_ids) >= limit: - add_expandable = True - context_ids = context_ids[0:-1] - - context_ids.append(message.parent_id.id) - context_messages = self.browse(cr, uid, context_ids, context=context) - header_message = context_messages.pop() - - try: - if not add_original: - message = False - result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message, - context_messages=context_messages, - header_message=header_message, - add_expandable=add_expandable, - # context kw would clash with mako internals - ctx=context, - format_exceptions=True) - result = result.strip() - return result - except Exception: - _logger.exception("failed to render mako template for quoting message") - return '' - return result - def _notify(self, cr, uid, newid, context=None, force_send=False, user_signature=True): """ Add the related record followers to the destination partner_ids if is not a private message. Call mail_notification.notify to manage the email sending @@ -975,9 +885,11 @@ class mail_message(osv.Model): cr, SUPERUSER_ID, [ ('res_model', '=', message.model), ('res_id', '=', message.res_id), - ('subtype_ids', 'in', message.subtype_id.id) ], context=context) - partners_to_notify |= set(fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)) + partners_to_notify |= set( + fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context) + if message.subtype_id.id in [st.id for st in fo.subtype_ids] + ) # remove me from notified partners, unless the message is written on my own wall if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id: partners_to_notify |= set([message.author_id.id]) @@ -1006,25 +918,3 @@ class mail_message(osv.Model): 'partner_id': partner.id, 'read': True, }, context=context) - - #------------------------------------------------------ - # Tools - #------------------------------------------------------ - - def check_partners_email(self, cr, uid, partner_ids, context=None): - """ Verify that selected partner_ids have an email_address defined. - Otherwise throw a warning. """ - partner_wo_email_lst = [] - for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context): - if not partner.email: - partner_wo_email_lst.append(partner) - if not partner_wo_email_lst: - return {} - warning_msg = _('The following partners chosen as recipients for the email have no email address linked :') - for partner in partner_wo_email_lst: - warning_msg += '\n- %s' % (partner.name) - return {'warning': { - 'title': _('Partners email addresses not found'), - 'message': warning_msg, - } - } diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 812ff91be90..1d7e6730427 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -97,6 +97,9 @@ class mail_thread(osv.AbstractModel): # :param function lambda: returns whether the tracking should record using this subtype _track = {} + # Mass mailing feature + _mail_mass_mailing = False + def get_empty_list_help(self, cr, uid, help, context=None): """ Override of BaseModel.get_empty_list_help() to generate an help message that adds alias information. """ @@ -662,15 +665,31 @@ class mail_thread(osv.AbstractModel): # Email specific #------------------------------------------------------ + def message_get_default_recipients(self, cr, uid, ids, context=None): + if context and context.get('thread_model') and context['thread_model'] in self.pool and context['thread_model'] != self._name: + sub_ctx = dict(context) + sub_ctx.pop('thread_model') + return self.pool[context['thread_model']].message_get_default_recipients(cr, uid, ids, context=sub_ctx) + res = {} + for record in self.browse(cr, SUPERUSER_ID, ids, context=context): + recipient_ids, email_to, email_cc = set(), False, False + if 'partner_id' in self._all_columns and record.partner_id: + recipient_ids.add(record.partner_id.id) + elif 'email_from' in self._all_columns and record.email_from: + email_to = record.email_from + elif 'email' in self._all_columns: + email_to = record.email + res[record.id] = {'partner_ids': list(recipient_ids), 'email_to': email_to, 'email_cc': email_cc} + return res + def message_get_reply_to(self, cr, uid, ids, context=None): """ Returns the preferred reply-to email address that is basically the alias of the document, if it exists. """ if not self._inherits.get('mail.alias'): return [False for id in ids] - return ["%s@%s" % (record['alias_name'], record['alias_domain']) - if record.get('alias_domain') and record.get('alias_name') - else False - for record in self.read(cr, SUPERUSER_ID, ids, ['alias_name', 'alias_domain'], context=context)] + return ["%s@%s" % (record.alias_name, record.alias_domain) + if record.alias_domain and record.alias_name else False + for record in self.browse(cr, SUPERUSER_ID, ids, context=context)] #------------------------------------------------------ # Mail gateway diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py index 8da2441f3bc..8b8f0d3c145 100644 --- a/addons/mail/res_partner.py +++ b/addons/mail/res_partner.py @@ -28,6 +28,7 @@ class res_partner_mail(osv.Model): _name = "res.partner" _inherit = ['res.partner', 'mail.thread'] _mail_flat_thread = False + _mail_mass_mailing = _('Customers') _columns = { 'notification_email_send': fields.selection([ @@ -53,4 +54,5 @@ class res_partner_mail(osv.Model): self._message_add_suggested_recipient(cr, uid, recipients, partner, partner=partner, reason=_('Partner Profile')) return recipients -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + def message_get_default_recipients(self, cr, uid, ids, context=None): + return dict((id, {'partner_ids': [id], 'email_to': False, 'email_cc': False}) for id in ids) diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 17dd4b57cd0..99a7751e15e 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -507,18 +507,15 @@ openerp.mail = function (session) { } $.when(recipient_done).done(function (partner_ids) { var context = { - 'default_composition_mode': default_composition_mode, 'default_parent_id': self.id, 'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''), 'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}), 'default_partner_ids': partner_ids, + 'default_is_log': self.is_log, 'mail_post_autofollow': true, 'mail_post_autofollow_partner_ids': partner_ids, 'is_private': self.is_private }; - if (self.is_log) { - _.extend(context, {'mail_compose_log': true}); - } if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) { context.default_model = self.context.default_model; context.default_res_id = self.context.default_res_id; diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py index c7f1056c59a..bea80b729a4 100644 --- a/addons/mail/tests/test_mail_features.py +++ b/addons/mail/tests/test_mail_features.py @@ -210,24 +210,6 @@ class test_mail(TestMail): self.assertTrue(subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs') self.assertTrue(subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs') - def test_10_message_quote_context(self): - """ Tests designed for message_post. """ - cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs - - msg1_id = self.mail_message.create(cr, uid, {'body': 'Thread header about Zap Brannigan', 'subject': 'My subject'}) - msg2_id = self.mail_message.create(cr, uid, {'body': 'First answer, should not be displayed', 'subject': 'Re: My subject', 'parent_id': msg1_id}) - msg3_id = self.mail_message.create(cr, uid, {'body': 'Second answer', 'subject': 'Re: My subject', 'parent_id': msg1_id}) - msg4_id = self.mail_message.create(cr, uid, {'body': 'Third answer', 'subject': 'Re: My subject', 'parent_id': msg1_id}) - msg_new_id = self.mail_message.create(cr, uid, {'body': 'My answer I am propagating', 'subject': 'Re: My subject', 'parent_id': msg1_id}) - - result = self.mail_message.message_quote_context(cr, uid, msg_new_id, limit=3) - self.assertIn('Thread header about Zap Brannigan', result, 'Thread header content should be in quote.') - self.assertIn('Second answer', result, 'Answer should be in quote.') - self.assertIn('Third answer', result, 'Answer should be in quote.') - self.assertIn('expandable', result, 'Expandable should be present.') - self.assertNotIn('First answer, should not be displayed', result, 'Old answer should not be in quote.') - self.assertNotIn('My answer I am propagating', result, 'Thread header content should be in quote.') - def test_11_notification_url(self): """ Tests designed to test the URL added in notification emails. """ cr, uid, group_pigs = self.cr, self.uid, self.group_pigs @@ -674,7 +656,6 @@ class test_mail(TestMail): 'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])] }, context={ 'default_composition_mode': 'reply', - 'default_model': 'mail.thread', 'default_res_id': self.group_pigs_id, 'default_parent_id': message.id }) @@ -699,11 +680,10 @@ class test_mail(TestMail): # -------------------------------------------------- # Do: Compose in mass_mail_mode on pigs and bird - compose_id = mail_compose.create(cr, user_raoul.id, - { + compose_id = mail_compose.create( + cr, user_raoul.id, { 'subject': _subject, 'body': '${object.description}', - 'post': True, 'partner_ids': [(4, p_c_id), (4, p_d_id)], }, context={ 'default_composition_mode': 'mass_mail', @@ -718,6 +698,13 @@ class test_mail(TestMail): 'default_res_id': -1, 'active_ids': [self.group_pigs_id, group_bird_id] }) + # check mail_mail + mail_mail_ids = self.mail_mail.search(cr, uid, [('subject', '=', _subject)]) + for mail_mail in self.mail_mail.browse(cr, uid, mail_mail_ids): + self.assertEqual(set([p.id for p in mail_mail.recipient_ids]), set([p_c_id, p_d_id]), + 'compose wizard: mail_mail mass mailing: mail.mail in mass mail incorrect recipients') + + # check logged messages group_pigs.refresh() group_bird.refresh() message1 = group_pigs.message_ids[0] @@ -733,14 +720,14 @@ class test_mail(TestMail): 'compose wizard: message_post: mail.message in mass mail subject incorrect') self.assertEqual(message1.body, '

%s

' % group_pigs.description, 'compose wizard: message_post: mail.message in mass mail body incorrect') - self.assertEqual(set([p.id for p in message1.notified_partner_ids]), set([p_c_id, p_d_id]), - 'compose wizard: message_post: mail.message in mass mail incorrect notified partners') + # self.assertEqual(set([p.id for p in message1.notified_partner_ids]), set([p_c_id, p_d_id]), + # 'compose wizard: message_post: mail.message in mass mail incorrect notified partners') self.assertEqual(message2.subject, _subject, 'compose wizard: message_post: mail.message in mass mail subject incorrect') self.assertEqual(message2.body, '

%s

' % group_bird.description, 'compose wizard: message_post: mail.message in mass mail body incorrect') - self.assertEqual(set([p.id for p in message2.notified_partner_ids]), set([p_c_id, p_d_id]), - 'compose wizard: message_post: mail.message in mass mail incorrect notified partners') + # self.assertEqual(set([p.id for p in message2.notified_partner_ids]), set([p_c_id, p_d_id]), + # 'compose wizard: message_post: mail.message in mass mail incorrect notified partners') # Test: mail.group followers: author not added as follower in mass mail mode pigs_pids = [p.id for p in group_pigs.message_follower_ids] @@ -757,7 +744,6 @@ class test_mail(TestMail): { 'subject': _subject, 'body': '${object.description}', - 'post': True, 'partner_ids': [(4, p_c_id), (4, p_d_id)], }, context={ 'default_composition_mode': 'mass_mail', diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index e5ff2b51fcd..e14a8f3dfe5 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -38,10 +38,7 @@ class mail_compose_message(osv.TransientModel): at model and view levels to provide specific features. The behavior of the wizard depends on the composition_mode field: - - 'reply': reply to a previous message. The wizard is pre-populated - via ``get_message_data``. - - 'comment': new post on a record. The wizard is pre-populated via - ``get_record_data`` + - 'comment': post on a record. The wizard is pre-populated via ``get_record_data`` - 'mass_mail': wizard in mass mailing mode where the mail details can contain template placeholders that will be merged with actual data before being sent to each recipient. @@ -50,6 +47,7 @@ class mail_compose_message(osv.TransientModel): _inherit = 'mail.message' _description = 'Email composition wizard' _log_access = True + _batch_size = 500 def default_get(self, cr, uid, fields, context=None): """ Handle composition mode. Some details about context keys: @@ -68,28 +66,22 @@ class mail_compose_message(osv.TransientModel): if context is None: context = {} result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context) - # get some important values from context - composition_mode = context.get('default_composition_mode', context.get('mail.compose.message.mode')) - model = context.get('default_model', context.get('active_model')) - res_id = context.get('default_res_id', context.get('active_id')) - message_id = context.get('default_parent_id', context.get('message_id', context.get('active_id'))) - active_ids = context.get('active_ids') + + # v6.1 compatibility mode + result['composition_mode'] = result.get('composition_mode', context.get('mail.compose.message.mode')) + result['model'] = result.get('model', context.get('active_model')) + result['res_id'] = result.get('res_id', context.get('active_id')) + result['parent_id'] = result.get('parent_id', context.get('message_id')) + + # default values according to composition mode - NOTE: reply is deprecated, fall back on comment + if result['composition_mode'] == 'reply': + result['composition_mode'] = 'comment' + vals = {} if 'active_domain' in context: # not context.get() because we want to keep global [] domains - result['use_active_domain'] = True - result['active_domain'] = '%s' % context.get('active_domain') - elif not result.get('active_domain'): - result['active_domain'] = '' - # get default values according to the composition mode - if composition_mode == 'reply': - vals = self.get_message_data(cr, uid, message_id, context=context) - elif composition_mode == 'comment' and model and res_id: - vals = self.get_record_data(cr, uid, model, res_id, context=context) - elif composition_mode == 'mass_mail' and model and active_ids: - vals = {'model': model, 'res_id': res_id} - else: - vals = {'model': model, 'res_id': res_id} - if composition_mode: - vals['composition_mode'] = composition_mode + vals['use_active_domain'] = True + vals['active_domain'] = '%s' % context.get('active_domain') + if result['composition_mode'] == 'comment': + vals.update(self.get_record_data(cr, uid, result, context=context)) for field in vals: if field in fields: @@ -102,13 +94,15 @@ class mail_compose_message(osv.TransientModel): # but when creating the mail.message to create the mail.compose.message # access rights issues may rise # We therefore directly change the model and res_id - if result.get('model') == 'res.users' and result.get('res_id') == uid: + if result['model'] == 'res.users' and result['res_id'] == uid: result['model'] = 'res.partner' result['res_id'] = self.pool.get('res.users').browse(cr, uid, uid).partner_id.id return result def _get_composition_mode_selection(self, cr, uid, context=None): - return [('comment', 'Comment a document'), ('reply', 'Reply to a message'), ('mass_mail', 'Mass mailing')] + return [('comment', 'Post on a document'), + ('mass_mail', 'Email Mass Mailing'), + ('mass_post', 'Post on Multiple Documents')] _columns = { 'composition_mode': fields.selection( @@ -116,19 +110,19 @@ class mail_compose_message(osv.TransientModel): string='Composition mode'), 'partner_ids': fields.many2many('res.partner', 'mail_compose_message_res_partner_rel', - 'wizard_id', 'partner_id', 'Additional contacts'), + 'wizard_id', 'partner_id', 'Additional Contacts'), 'use_active_domain': fields.boolean('Use active domain'), 'active_domain': fields.char('Active domain', readonly=True), - 'post': fields.boolean('Post a copy in the document', - help='Post a copy of the message on the document communication history.'), - 'notify': fields.boolean('Notify followers', - help='Notify followers of the document'), - 'same_thread': fields.boolean('Replies in the document', - help='Replies to the messages will go into the selected document.'), 'attachment_ids': fields.many2many('ir.attachment', 'mail_compose_message_ir_attachments_rel', 'wizard_id', 'attachment_id', 'Attachments'), - 'filter_id': fields.many2one('ir.filters', 'Filters'), + 'is_log': fields.boolean('Log an Internal Note', + help='Whether the message is an internal note (comment mode only)'), + # mass mode options + 'notify': fields.boolean('Notify followers', + help='Notify followers of the document (mass post only)'), + 'same_thread': fields.boolean('Replies in the document', + help='Replies to the messages will go into the selected document (mass mail only)'), } #TODO change same_thread to False in trunk (Require view update) _defaults = { @@ -136,8 +130,6 @@ class mail_compose_message(osv.TransientModel): 'body': lambda self, cr, uid, ctx={}: '', 'subject': lambda self, cr, uid, ctx={}: False, 'partner_ids': lambda self, cr, uid, ctx={}: [], - 'post': False, - 'notify': False, 'same_thread': True, } @@ -169,61 +161,36 @@ class mail_compose_message(osv.TransientModel): not want that feature in the wizard. """ return - def get_record_data(self, cr, uid, model, res_id, context=None): + def get_record_data(self, cr, uid, values, context=None): """ Returns a defaults-like dict with initial values for the composition - wizard when sending an email related to the document record - identified by ``model`` and ``res_id``. - - :param str model: model name of the document record this mail is - related to. - :param int res_id: id of the document record this mail is related to - """ - doc_name_get = self.pool[model].name_get(cr, uid, [res_id], context=context) - record_name = False - if doc_name_get: - record_name = doc_name_get[0][1] - values = { - 'model': model, - 'res_id': res_id, - 'record_name': record_name, - } - if record_name: - values['subject'] = 'Re: %s' % record_name - return values - - def get_message_data(self, cr, uid, message_id, context=None): - """ Returns a defaults-like dict with initial values for the composition - wizard when replying to the given message (e.g. including the quote - of the initial message, and the correct recipients). - - :param int message_id: id of the mail.message to which the user - is replying. - """ - if not message_id: - return {} + wizard when sending an email related a previous email (parent_id) or + a document (model, res_id). This is based on previously computed default + values. """ if context is None: context = {} - message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context=context) + result, subject = {}, False + if values.get('parent_id'): + parent = self.pool.get('mail.message').browse(cr, uid, values.get('parent_id'), context=context) + result['record_name'] = parent.record_name, + subject = tools.ustr(parent.subject or parent.record_name or '') + if not values.get('model'): + result['model'] = parent.model + if not values.get('res_id'): + result['res_id'] = parent.res_id + partner_ids = values.get('partner_ids', list()) + [partner.id for partner in parent.partner_ids] + if context.get('is_private') and parent.author_id: # check message is private then add author also in partner list. + partner_ids += [parent.author_id.id] + result['partner_ids'] = partner_ids + elif values.get('model') and values.get('res_id'): + doc_name_get = self.pool[values.get('model')].name_get(cr, uid, [values.get('res_id')], context=context) + result['record_name'] = doc_name_get and doc_name_get[0][1] or '' + subject = tools.ustr(result['record_name']) - # create subject re_prefix = _('Re:') - reply_subject = tools.ustr(message_data.subject or message_data.record_name or '') - if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)) and message_data.subject: - reply_subject = "%s %s" % (re_prefix, reply_subject) - # get partner_ids from original message - partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else [] - partner_ids += context.get('default_partner_ids', []) - if context.get('is_private',False) and message_data.author_id : #check message is private then add author also in partner list. - partner_ids += [message_data.author_id.id] - # update the result - result = { - 'record_name': message_data.record_name, - 'model': message_data.model, - 'res_id': message_data.res_id, - 'parent_id': message_data.id, - 'subject': reply_subject, - 'partner_ids': partner_ids, - } + if subject and not (subject.startswith('Re:') or subject.startswith(re_prefix)): + subject = "%s %s" % (re_prefix, subject) + result['subject'] = subject + return result #------------------------------------------------------ @@ -235,53 +202,42 @@ class mail_compose_message(osv.TransientModel): email(s), rendering any template patterns on the fly if needed. """ if context is None: context = {} + # clean the context (hint: mass mailing sets some default values that # could be wrongly interpreted by mail_mail) context.pop('default_email_to', None) context.pop('default_partner_ids', None) - active_ids = context.get('active_ids') - is_log = context.get('mail_compose_log', False) - for wizard in self.browse(cr, uid, ids, context=context): - mass_mail_mode = wizard.composition_mode == 'mass_mail' + mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post') active_model_pool = self.pool[wizard.model if wizard.model else 'mail.thread'] if not hasattr(active_model_pool, 'message_post'): context['thread_model'] = wizard.model active_model_pool = self.pool['mail.thread'] # wizard works in batch mode: [res_id] or active_ids or active_domain - if mass_mail_mode and wizard.use_active_domain and wizard.model: + if mass_mode and wizard.use_active_domain and wizard.model: res_ids = self.pool[wizard.model].search(cr, uid, eval(wizard.active_domain), context=context) - elif mass_mail_mode and wizard.model and active_ids: - res_ids = active_ids + elif mass_mode and wizard.model and context.get('active_ids'): + res_ids = context['active_ids'] else: res_ids = [wizard.res_id] - all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context) - - for res_id, mail_values in all_mail_values.iteritems(): - if mass_mail_mode and not wizard.post: - m2m_attachment_ids = self.pool['mail.thread']._message_preprocess_attachments( - cr, uid, mail_values.pop('attachments', []), - mail_values.pop('attachment_ids', []), - 'mail.message', 0, - context=context) - mail_values['attachment_ids'] = m2m_attachment_ids - if not mail_values.get('reply_to'): - mail_values['reply_to'] = mail_values['email_from'] - self.pool.get('mail.mail').create(cr, uid, mail_values, context=context) - else: - subtype = 'mail.mt_comment' - if is_log: # log a note: subtype is False - subtype = False - elif mass_mail_mode: # mass mail: is a log pushed to recipients unless specified, author not added - if not wizard.notify: + sliced_res_ids = [res_ids[i:i + self._batch_size] for i in range(0, len(res_ids), self._batch_size)] + for res_ids in sliced_res_ids: + all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context) + for res_id, mail_values in all_mail_values.iteritems(): + if wizard.composition_mode == 'mass_mail': + self.pool['mail.mail'].create(cr, uid, mail_values, context=context) + else: + subtype = 'mail.mt_comment' + if context.get('mail_compose_log') or (wizard.composition_mode == 'mass_post' and not wizard.notify): # log a note: subtype is False subtype = False - context = dict(context, - mail_notify_force_send=False, # do not send emails directly but use the queue instead - mail_create_nosubscribe=True) # add context key to avoid subscribing the author - active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values) + if wizard.composition_mode == 'mass_post': + context = dict(context, + mail_notify_force_send=False, # do not send emails directly but use the queue instead + mail_create_nosubscribe=True) # add context key to avoid subscribing the author + active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values) return {'type': 'ir.actions.act_window_close'} @@ -289,6 +245,7 @@ class mail_compose_message(osv.TransientModel): """Generate the values that will be used by send_mail to create mail_messages or mail_mails. """ results = dict.fromkeys(res_ids, False) + rendered_values, default_recipients = {}, {} mass_mail_mode = wizard.composition_mode == 'mass_mail' # render all template-based value at once @@ -303,40 +260,46 @@ class mail_compose_message(osv.TransientModel): 'parent_id': wizard.parent_id and wizard.parent_id.id, 'partner_ids': [partner.id for partner in wizard.partner_ids], 'attachment_ids': [attach.id for attach in wizard.attachment_ids], + 'author_id': wizard.author_id.id, + 'email_from': wizard.email_from, + 'record_name': wizard.record_name, } # mass mailing: rendering override wizard static values if mass_mail_mode and wizard.model: + # always keep a copy, reset record name (avoid browsing records) + mail_values.update(notification=True, model=wizard.model, res_id=res_id, record_name=False) + # auto deletion of mail_mail + if 'mail_auto_delete' in context: + mail_values['auto_delete'] = context.get('mail_auto_delete') + # rendered values using template email_dict = rendered_values[res_id] mail_values['partner_ids'] += email_dict.pop('partner_ids', []) + mail_values.update(email_dict) + if wizard.same_thread: + mail_values.pop('reply_to') + elif not mail_values.get('reply_to'): + mail_values['reply_to'] = mail_values['email_from'] + # mail_mail values: body -> body_html, partner_ids -> recipient_ids + mail_values['body_html'] = mail_values.get('body', '') + mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])] + # process attachments: should not be encoded before being processed by message_post / mail_mail create - attachments = [] - if email_dict.get('attachments'): - for name, enc_cont in email_dict.pop('attachments'): - attachments.append((name, base64.b64decode(enc_cont))) - mail_values['attachments'] = attachments + mail_values['attachments'] = [(name, base64.b64decode(enc_cont)) for name, enc_cont in email_dict.pop('attachments', list())] attachment_ids = [] for attach_id in mail_values.pop('attachment_ids'): new_attach_id = self.pool.get('ir.attachment').copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context) attachment_ids.append(new_attach_id) - mail_values['attachment_ids'] = attachment_ids - # email_from: mass mailing only can specify another email_from - if email_dict.get('email_from'): - mail_values['email_from'] = email_dict.pop('email_from') - # replies redirection: mass mailing only - if wizard.same_thread and wizard.post: - email_dict.pop('reply_to', None) - else: - mail_values['reply_to'] = email_dict.pop('reply_to', None) - mail_values.update(email_dict) - # mass mailing without post: mail_mail values - if mass_mail_mode and not wizard.post: - if 'mail_auto_delete' in context: - mail_values['auto_delete'] = context.get('mail_auto_delete') - mail_values['body_html'] = mail_values.get('body', '') - mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])] + mail_values['attachment_ids'] = self.pool['mail.thread']._message_preprocess_attachments( + cr, uid, mail_values.pop('attachments', []), + attachment_ids, 'mail.message', 0, context=context) + results[res_id] = mail_values return results + #------------------------------------------------------ + # Template rendering + #------------------------------------------------------ + def render_message_batch(self, cr, uid, wizard, res_ids, context=None): """Generate template-based values of wizard, for the document records given by res_ids. This method is meant to be inherited by email_template that @@ -346,6 +309,10 @@ class mail_compose_message(osv.TransientModel): once, and render it multiple times. This is useful for mass mailing where template rendering represent a significant part of the process. + Default recipients are also computed, based on mail_thread method + message_get_default_recipients. This allows to ensure a mass mailing has + always some recipients specified. + :param browse wizard: current mail.compose.message browse record :param list res_ids: list of record ids @@ -357,6 +324,9 @@ class mail_compose_message(osv.TransientModel): emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context=context) replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context=context) + ctx = dict(context, thread_model=wizard.model) + default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx) + results = dict.fromkeys(res_ids, False) for res_id in res_ids: results[res_id] = { @@ -365,6 +335,7 @@ class mail_compose_message(osv.TransientModel): 'email_from': emails_from[res_id], 'reply_to': replies_to[res_id], } + results[res_id].update(default_recipients.get(res_id, dict())) return results def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False): diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml index 23931649402..5b6bfc50439 100644 --- a/addons/mail/wizard/mail_compose_message_view.xml +++ b/addons/mail/wizard/mail_compose_message_view.xml @@ -11,6 +11,7 @@ + @@ -28,29 +29,27 @@ - -
+ + + + + + + + + + + + + + email.template.kanban + email.template + + + + + +
+
+ i + +
+
+

+ +

+
+ +
+
+
+
+
+
+
+
+
+ + + Templates + email.template + form + kanban,tree,form + { + 'form_view_ref': 'mass_mailing.email_template_form_minimal', + 'default_use_default_to': True, +} + + + + + + + diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml new file mode 100644 index 00000000000..7892feacacc --- /dev/null +++ b/addons/mass_mailing/views/mass_mailing.xml @@ -0,0 +1,626 @@ + + + + + + + + + + + + + + mail.mass_mailing.contact.search + mail.mass_mailing.contact + + + + + + + + + + + + + + + + + mail.mass_mailing.contact.tree + mail.mass_mailing.contact + 10 + + + + + + + + + + + + Mailing List Subscribers + mail.mass_mailing.contact + form + tree + {'search_default_not_opt_out': 1} + + + + Recipients + mail.mass_mailing.contact + form + tree + {'search_default_list_id': active_id, 'search_default_not_opt_out': 1} + +

+ Click to create a recipient. +

+
+
+ + + + + + mail.mass_mailing.list.search + mail.mass_mailing.list + + + + + + + + + mail.mass_mailing.list.tree + mail.mass_mailing.list + 10 + + + + + + + + + + mail.mass_mailing.list.form + mail.mass_mailing.list + +
+ +
+ +
+
+
+
+
+
+
+ + + Contact Lists + mail.mass_mailing.list + form + tree,form + +

+ Click here to create a new mailing list. +

+ Mailing lists allows you to to manage customers and + contacts easily and to send to mailings in a single click. +

+
+ + + + + + mail.mass_mailing.search + mail.mass_mailing + + + + + + + + + + + + + + mail.mass_mailing.tree + mail.mass_mailing + 10 + + + + + + + + + + + + + + mail.mass_mailing.form + mail.mass_mailing + +
+
+
+
+

+ + emails are in queue and will be sent soon. +

+
+ +
+ + + + + +
+
+ +
+ + + + + + + + + + +
+ + + Related Mailing(s) + + + + + + + + + +