1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2009 Sharoon Thomas
6 # Copyright (C) 2010-2010 OpenERP SA (<http://www.openerp.com>)
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>
21 ##############################################################################
24 from osv import fields
30 from tools.translate import _
34 def get_value(cursor, user, recid, message=None, template=None, context=None):
36 Evaluates an expression and returns its value
37 @param cursor: Database Cursor
38 @param user: ID of current user
39 @param recid: ID of the target record under evaluation
40 @param message: The expression to be evaluated
41 @param template: BrowseRecord object of the current template
42 @param context: OpenERP Context
43 @return: Computed message (unicode) or u""
45 pool = pooler.get_pool(cursor.dbname)
48 #Returns the computed expression
51 message = tools.ustr(message)
52 object = pool.get(template.model_int_name).browse(cursor, user, recid, context=context)
54 'user':pool.get('res.users').browse(cursor, user, user, context=context),
57 templ = MakoTemplate(message, input_encoding='utf-8')
58 reply = MakoTemplate(message).render_unicode(object=object, peobject=object, env=env, format_exceptions=True)
61 logging.exception("can't render %r", message)
66 class email_template(osv.osv):
67 "Templates for sending Email"
69 _name = "email.template"
70 _description = 'Email Templates for Models'
72 def change_model(self, cursor, user, ids, object_name, context=None):
75 mod_name = self.pool.get('ir.model').browse(cursor, user, object_name, context).model
76 return {'value':{'model_int_name':mod_name}}
79 'name' : fields.char('Name', size=100, required=True),
80 'object_name':fields.many2one('ir.model', 'Resource'),
81 'model_int_name':fields.char('Model Internal Name', size=200,),
82 'from_account':fields.many2one(
84 string="Email Account",
85 help="Emails will be sent from this approved account."),
89 help="The Recipient of email. "
90 "Placeholders can be used here. "
91 "e.g. ${object.email_to}"),
95 help="Carbon Copy address(es), comma-separated."
96 " Placeholders can be used here. "
97 "e.g. ${object.email_cc}"),
98 'def_bcc':fields.char(
101 help="Blind Carbon Copy address(es), comma-separated."
102 " Placeholders can be used here. "
103 "e.g. ${object.email_bcc}"),
104 'reply_to':fields.char('Reply-To',
106 help="The address recipients should reply to,"
107 " if different from the From address."
108 " Placeholders can be used here. "
109 "e.g. ${object.email_reply_to}"),
110 'message_id':fields.char('Message-ID',
112 help="Specify the Message-ID SMTP header to use in outgoing emails. Please note that this overrides the Resource tracking option! Placeholders can be used here."),
113 'track_campaign_item':fields.boolean('Resource Tracking',
114 help="Enable this is you wish to include a special \
115 tracking marker in outgoing emails so you can identify replies and link \
116 them back to the corresponding resource record. \
117 This is useful for CRM leads for example"),
121 help="The default language for the email."
122 " Placeholders can be used here. "
123 "eg. ${object.partner_id.lang}"),
124 'def_subject':fields.char(
127 help="The subject of email."
128 " Placeholders can be used here.",
130 'def_body_text':fields.text(
131 'Standard Body (Text)',
132 help="The text version of the mail",
134 'def_body_html':fields.text(
135 'Body (Text-Web Client Only)',
136 help="The text version of the mail",
138 'use_sign':fields.boolean(
140 help="the signature from the User details"
141 " will be appended to the mail"),
142 'file_name':fields.char(
145 help="Name of the generated report file. Placeholders can be used in the filename. eg: 2009_SO003.pdf",
147 'report_template':fields.many2one(
148 'ir.actions.report.xml',
150 'attachment_ids': fields.many2many(
152 'email_template_attachment_rel',
156 help="You may attach existing files to this template, "
157 "so they will be added in all emails created from this template"),
158 'ref_ir_act_window':fields.many2one(
159 'ir.actions.act_window',
161 help="Action that will open this email template on Resource records",
163 'ref_ir_value':fields.many2one(
166 help="Button in the side bar of the form view of this Resource that will invoke the Window Action",
168 'allowed_groups':fields.many2many(
170 'template_group_rel',
171 'templ_id', 'group_id',
172 string="Allowed User Groups",
173 help="Only users from these groups will be"
174 " allowed to send mails from this Template"),
175 'model_object_field':fields.many2one(
178 help="Select the field from the model you want to use."
179 "\nIf it is a relationship field you will be able to "
180 "choose the nested values in the box below\n(Note:If "
181 "there are no values make sure you have selected the"
184 'sub_object':fields.many2one(
187 help='When a relation field is used this field'
188 ' will show you the type of field you have selected',
190 'sub_model_object_field':fields.many2one(
193 help="When you choose relationship fields "
194 "this field will specify the sub value you can use.",
196 'null_value':fields.char(
198 help="This Value is used if the field is empty",
199 size=50, store=False),
200 'copyvalue':fields.char(
203 help="Copy and paste the value in the "
204 "location you want to use a system value.",
206 'table_html':fields.text(
208 help="Copy this html code to your HTML message"
209 " body for displaying the info in your mail.",
211 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
215 ('name', 'unique (name)','The template name must be unique !')
218 def create_action(self, cr, uid, ids, context=None):
222 action_obj = self.pool.get('ir.actions.act_window')
223 for template in self.browse(cr, uid, ids, context=context):
224 src_obj = template.object_name.model
225 vals['ref_ir_act_window'] = action_obj.create(cr, uid, {
226 'name': template.name,
227 'type': 'ir.actions.act_window',
228 'res_model': 'email_template.send.wizard',
229 'src_model': src_obj,
231 'context': "{'src_model':'%s','template_id':'%d','src_rec_id':active_id,'src_rec_ids':active_ids}" % (src_obj, template.id),
232 'view_mode':'form,tree',
233 'view_id': self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'email_template.send.wizard.form')], context=context)[0],
237 vals['ref_ir_value'] = self.pool.get('ir.values').create(cr, uid, {
238 'name': _('Send Mail (%s)') % template.name,
240 'key2': 'client_action_multi',
241 'value': "ir.actions.act_window," + str(vals['ref_ir_act_window']),
244 self.write(cr, uid, ids, {
245 'ref_ir_act_window': vals.get('ref_ir_act_window',False),
246 'ref_ir_value': vals.get('ref_ir_value',False),
250 def unlink_action(self, cr, uid, ids, context=None):
251 for template in self.browse(cr, uid, ids, context=context):
253 if template.ref_ir_act_window:
254 self.pool.get('ir.actions.act_window').unlink(cr, uid, template.ref_ir_act_window.id, context)
255 if template.ref_ir_value:
256 self.pool.get('ir.values').unlink(cr, uid, template.ref_ir_value.id, context)
258 raise osv.except_osv(_("Warning"), _("Deletion of Record failed"))
260 def delete_action(self, cr, uid, ids, context=None):
261 self.unlink_action(cr, uid, ids, context=context)
264 def unlink(self, cr, uid, ids, context=None):
265 self.unlink_action(cr, uid, ids, context=context)
266 return super(email_template, self).unlink(cr, uid, ids, context=context)
268 def copy(self, cr, uid, id, default=None, context=None):
271 default = default.copy()
272 old = self.read(cr, uid, id, ['name'], context=context)
273 new_name = _("Copy of template ") + old.get('name', 'No Name')
274 check = self.search(cr, uid, [('name', '=', new_name)], context=context)
276 new_name = new_name + '_' + random.choice('abcdefghij') + random.choice('lmnopqrs') + random.choice('tuvwzyz')
277 default.update({'name':new_name})
278 return super(email_template, self).copy(cr, uid, id, default, context)
280 def build_expression(self, field_name, sub_field_name, null_value):
282 Returns a template expression based on data provided
283 @param field_name: field name
284 @param sub_field_name: sub field name (M2O)
285 @param null_value: default value if the target value is empty
286 @return: computed expression
290 expression = "${object." + field_name
292 expression += "." + sub_field_name
294 expression += " or '''%s'''" % null_value
298 # def onchange_model_object_field(self, cr, uid, ids, model_object_field, context=None):
299 # if not model_object_field:
302 # field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
303 # #Check if field is relational
304 # if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
305 # res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
307 # result['sub_object'] = res_ids[0]
308 # result['copyvalue'] = self.build_expression(False, False, False)
309 # result['sub_model_object_field'] = False
310 # result['null_value'] = False
312 # #Its a simple field... just compute placeholder
313 # result['sub_object'] = False
314 # result['copyvalue'] = self.build_expression(field_obj.name, False, False)
315 # result['sub_model_object_field'] = False
316 # result['null_value'] = False
317 # return {'value':result}
319 # def onchange_sub_model_object_field(self, cr, uid, ids, model_object_field, sub_model_object_field, context=None):
320 # if not model_object_field or not sub_model_object_field:
323 # field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
324 # if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
325 # res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
326 # sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
328 # result['sub_object'] = res_ids[0]
329 # result['copyvalue'] = self.build_expression(field_obj.name, sub_field_obj.name, False)
330 # result['sub_model_object_field'] = sub_model_object_field
331 # result['null_value'] = False
333 # #Its a simple field... just compute placeholder
334 # result['sub_object'] = False
335 # result['copyvalue'] = self.build_expression(field_obj.name, False, False)
336 # result['sub_model_object_field'] = False
337 # result['null_value'] = False
338 # return {'value':result}
341 # def onchange_null_value(self, cr, uid, ids, model_object_field, sub_model_object_field, null_value, template_language, context=None):
342 # if not model_object_field and not null_value:
345 # field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
346 # if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
347 # res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
348 # sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
350 # result['sub_object'] = res_ids[0]
351 # result['copyvalue'] = self.build_expression(field_obj.name,
352 # sub_field_obj.name,
356 # result['sub_model_object_field'] = sub_model_object_field
357 # result['null_value'] = null_value
359 # #Its a simple field... just compute placeholder
360 # result['sub_object'] = False
361 # result['copyvalue'] = self.build_expression(field_obj.name,
366 # result['sub_model_object_field'] = False
367 # result['null_value'] = null_value
368 # return {'value':result}
370 def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, null_value=None, context=None):
374 'sub_model_object_field': False,
377 if model_object_field:
378 fields_obj = self.pool.get('ir.model.fields')
379 field_value = fields_obj.browse(cr, uid, model_object_field, context)
380 if field_value.ttype in ['many2one', 'one2many', 'many2many']:
381 res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
382 sub_field_value = False
383 if sub_model_object_field:
384 sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
387 'sub_object': res_ids[0],
388 'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False, null_value or False),
389 'sub_model_object_field': sub_model_object_field or False,
390 'null_value': null_value or False
394 'copyvalue': self.build_expression(field_value.name, False, null_value or False),
395 'null_value': null_value or False
397 return {'value':result}
399 def _add_attachment(self, cursor, user, mailbox_id, name, data, filename, context=None):
401 Add an attachment to a given mailbox entry.
402 :param data: base64 encoded attachment data to store
404 attachment_obj = self.pool.get('ir.attachment')
406 'name': (name or '') + _(' (Email Attachment)'),
408 'datas_fname': filename,
409 'description': name or _('No Description'),
410 'res_model':'email.message',
411 'res_id': mailbox_id,
413 attachment_id = attachment_obj.create(cursor, user, attachment_data, context)
415 self.pool.get('email.message').write(cursor, user, mailbox_id,
417 'attachments_ids':[(4, attachment_id)],
418 'mail_type':'multipart/mixed'
422 def generate_attach_reports(self, cursor, user, template, record_id, mail, context=None):
424 Generate report to be attached and attach it
425 to the email, and add any directly attached files as well.
427 @param cursor: Database Cursor
428 @param user: ID of User
429 @param template: Browse record of
431 @param record_id: ID of the target model
432 for which this mail has
434 @param mail: Browse record of email object
437 if template.report_template:
438 reportname = 'report.' + self.pool.get('ir.actions.report.xml').browse(cursor,
439 user, template.report_template.id, context).report_name
440 service = netsvc.LocalService(reportname)
442 data['model'] = template.model_int_name
443 (result, format) = service.create(cursor, user, [record_id], data, context)
444 fname = tools.ustr(get_value(cursor, user, record_id,
445 template.file_name, template, context)
448 if not fname.endswith(ext):
450 self._add_attachment(cursor, user, mail.id, mail.subject, base64.b64encode(result), fname, context)
452 if template.attachment_ids:
453 for attachment in template.attachment_ids:
454 self._add_attachment(cursor, user, mail.id, attachment.name, attachment.datas, attachment.datas_fname, context)
458 def _generate_mailbox_item_from_template(self, cursor, user, template, record_id, context=None):
460 Generates an email from the template for
461 record record_id of target object
463 @param cursor: Database Cursor
464 @param user: ID of User
465 @param template: Browse record of
467 @param record_id: ID of the target model
468 for which this mail has
470 @return: ID of created object
474 #If account to send from is in context select it, else use enforced account
475 if 'account_id' in context.keys():
476 from_account = self.pool.get('email.smtp_server').read(cursor, user, context.get('account_id'), ['name', 'email_id'], context)
479 'id':template.from_account.id,
480 'name':template.from_account.name,
481 'email_id':template.from_account.email_id
483 lang = get_value(cursor, user, record_id, template.lang, template, context)
486 ctx.update({'lang':lang})
487 template = self.browse(cursor, user, template.id, context=ctx)
489 # determine name of sender, either it is specified in email_id or we
490 # use the account name
491 email_id = from_account['email_id'].strip()
492 email_from = re.findall(r'([^ ,<@]+@[^> ,]+)', email_id)[0]
493 if email_from != email_id:
494 # we should keep it all, name is probably specified in the address
495 email_from = from_account['email_id']
497 email_from = tools.ustr(from_account['name']) + "<" + tools.ustr(email_id) + ">"
499 # FIXME: should do this in a loop and rename template fields to the corresponding
500 # mailbox fields. (makes no sense to have different names I think.
502 'email_from': email_from,
503 'email_to':get_value(cursor,
509 'email_cc':get_value(cursor,
515 'email_bcc':get_value(cursor,
521 'reply_to':get_value(cursor,
527 'subject':get_value(cursor,
530 template.def_subject,
533 'body_text':get_value(cursor,
536 template.def_body_text,
539 'body_html':get_value(cursor,
542 template.def_body_html,
545 #This is a mandatory field when automatic emails are sent
548 'mail_type':'multipart/alternative',
549 'template_id': template.id
552 if template['message_id']:
553 # use provided message_id with placeholders
554 mailbox_values.update({'message_id': get_value(cursor, user, record_id, template['message_id'], template, context)})
556 elif template['track_campaign_item']:
557 # get appropriate message-id
558 mailbox_values.update({'message_id': tools.misc.generate_tracking_message_id(record_id)})
560 # if not mailbox_values['account_id']:
561 # raise Exception("Unable to send the mail. No account linked to the template.")
562 #Use signatures if allowed
563 if template.use_sign:
564 sign = self.pool.get('res.users').read(cursor, user, user, ['signature'], context)['signature']
565 if mailbox_values['body_text']:
566 mailbox_values['body_text'] += sign
567 if mailbox_values['body_html']:
568 mailbox_values['body_html'] += sign
569 mailbox_id = self.pool.get('email.message').create(cursor, user, mailbox_values, context)
574 def generate_mail(self, cursor, user, template_id, record_ids, context=None):
577 template = self.browse(cursor, user, template_id, context=context)
579 raise Exception("The requested template could not be loaded")
581 mailbox_obj = self.pool.get('email.message')
582 for record_id in record_ids:
583 mailbox_id = self._generate_mailbox_item_from_template(cursor, user, template, record_id, context)
584 mail = mailbox_obj.browse(cursor, user, mailbox_id, context=context)
585 if template.report_template or template.attachment_ids:
586 self.generate_attach_reports(cursor, user, template, record_id, mail, context )
587 mailbox_obj.write(cursor, user, mailbox_id, {'folder':'outbox', 'state': 'waiting'}, context=context)
592 class email_message(osv.osv):
593 _inherit = 'email.message'
595 'template_id': fields.many2one('email.template', 'Email-Template', readonly=True),
598 def process_email_queue(self, cr, uid, ids=None, context=None):
599 result = super(email_message, self).process_email_queue(cr, uid, ids, context)
600 attachment_obj = self.pool.get('ir.attachment')
601 for message in self.browse(cr, uid, result, context):
602 if message.template_id and message.template_id.auto_delete:
603 self.unlink(cr, uid, [id], context=context)
604 attachment_ids = [x.id for x in message.attachments_ids]
605 attachment_obj.unlink(cr, uid, attachment_ids, context=context)
610 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: