[IMP] email_template :- generate template to message then donot send the mail bug...
[odoo/odoo.git] / addons / email_template / email_template.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2009 Sharoon Thomas
6 #    Copyright (C) 2010-2010 OpenERP SA (<http://www.openerp.com>)
7 #
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.
12 #
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.
17 #
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/>
20 #
21 ##############################################################################
22
23 import base64
24 import random
25 import netsvc
26 import logging
27 import re
28
29 TEMPLATE_ENGINES = []
30
31 from osv import osv, fields
32 from tools.translate import _
33
34 import tools
35 import pooler
36 import logging
37
38 def get_value(cursor, user, recid, message=None, template=None, context=None):
39     """
40     Evaluates an expression and returns its value
41     @param cursor: Database Cursor
42     @param user: ID of current user
43     @param recid: ID of the target record under evaluation
44     @param message: The expression to be evaluated
45     @param template: BrowseRecord object of the current template
46     @param context: OpenERP Context
47     @return: Computed message (unicode) or u""
48     """
49     pool = pooler.get_pool(cursor.dbname)
50     if message is None:
51         message = {}
52     #Returns the computed expression
53     if message:
54         try:
55             message = tools.ustr(message)
56             object = pool.get(template.model_int_name).browse(cursor, user, recid, context=context)
57             env = {
58                 'user':pool.get('res.users').browse(cursor, user, user, context=context),
59                 'db':cursor.dbname
60                }
61             templ = MakoTemplate(message, input_encoding='utf-8')
62             reply = MakoTemplate(message).render_unicode(object=object, peobject=object, env=env, format_exceptions=True)
63             return reply or False
64         except Exception:
65             logging.exception("can't render %r", message)
66             return u""
67     else:
68         return message
69
70 class email_template(osv.osv):
71     "Templates for sending Email"
72
73     _name = "email.template"
74     _description = 'Email Templates for Models'
75
76     def change_model(self, cursor, user, ids, object_name, context=None):
77         if object_name:
78             mod_name = self.pool.get('ir.model').read(
79                                               cursor,
80                                               user,
81                                               object_name,
82                                               ['model'], context)['model']
83         else:
84             mod_name = False
85         return {'value':{'model_int_name':mod_name}}
86
87     _columns = {
88         'name' : fields.char('Name', size=100, required=True),
89         'object_name':fields.many2one('ir.model', 'Resource'),
90         'model_int_name':fields.char('Model Internal Name', size=200,),
91         'from_account':fields.many2one(
92                    'email.smtp_server',
93                    string="Email Account",
94                    help="Emails will be sent from this approved account."),
95         'def_to':fields.char(
96                  'Recipient (To)',
97                  size=250,
98                  help="The Recipient of email. "
99                  "Placeholders can be used here. "
100                  "e.g. ${object.email_to}"),
101         'def_cc':fields.char(
102                  'CC',
103                  size=250,
104                  help="Carbon Copy address(es), comma-separated."
105                     " Placeholders can be used here. "
106                     "e.g. ${object.email_cc}"),
107         'def_bcc':fields.char(
108                   'BCC',
109                   size=250,
110                   help="Blind Carbon Copy address(es), comma-separated."
111                     " Placeholders can be used here. "
112                     "e.g. ${object.email_bcc}"),
113         'reply_to':fields.char('Reply-To',
114                     size=250,
115                     help="The address recipients should reply to,"
116                     " if different from the From address."
117                     " Placeholders can be used here. "
118                     "e.g. ${object.email_reply_to}"),
119         'message_id':fields.char('Message-ID',
120                     size=250,
121                     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."),
122         'track_campaign_item':fields.boolean('Resource Tracking',
123                                 help="Enable this is you wish to include a special \
124 tracking marker in outgoing emails so you can identify replies and link \
125 them back to the corresponding resource record. \
126 This is useful for CRM leads for example"),
127         'lang':fields.char(
128                    'Language',
129                    size=250,
130                    help="The default language for the email."
131                    " Placeholders can be used here. "
132                    "eg. ${object.partner_id.lang}"),
133         'def_subject':fields.char(
134                   'Subject',
135                   size=200,
136                   help="The subject of email."
137                   " Placeholders can be used here.",
138                   translate=True),
139         'def_body_text':fields.text(
140                     'Standard Body (Text)',
141                     help="The text version of the mail",
142                     translate=True),
143         'def_body_html':fields.text(
144                     'Body (Text-Web Client Only)',
145                     help="The text version of the mail",
146                     translate=True),
147         'use_sign':fields.boolean(
148                   'Signature',
149                   help="the signature from the User details"
150                   " will be appended to the mail"),
151         'file_name':fields.char(
152                 'Report Filename',
153                 size=200,
154                 help="Name of the generated report file. Placeholders can be used in the filename. eg: 2009_SO003.pdf",
155                 translate=True),
156         'report_template':fields.many2one(
157                   'ir.actions.report.xml',
158                   'Report to send'),
159         'attachment_ids': fields.many2many(
160                     'ir.attachment',
161                     'email_template_attachment_rel',
162                     'email_template_id',
163                     'attachment_id',
164                     'Attached Files',
165                     help="You may attach existing files to this template, "
166                          "so they will be added in all emails created from this template"),
167         'ref_ir_act_window':fields.many2one(
168                     'ir.actions.act_window',
169                     'Window Action',
170                     help="Action that will open this email template on Resource records",
171                     readonly=True),
172         'ref_ir_value':fields.many2one(
173                    'ir.values',
174                    'Wizard Button',
175                    help="Button in the side bar of the form view of this Resource that will invoke the Window Action",
176                    readonly=True),
177         'allowed_groups':fields.many2many(
178                   'res.groups',
179                   'template_group_rel',
180                   'templ_id', 'group_id',
181                   string="Allowed User Groups",
182                   help="Only users from these groups will be"
183                   " allowed to send mails from this Template"),
184         'model_object_field':fields.many2one(
185                  'ir.model.fields',
186                  string="Field",
187                  help="Select the field from the model you want to use."
188                  "\nIf it is a relationship field you will be able to "
189                  "choose the nested values in the box below\n(Note:If "
190                  "there are no values make sure you have selected the"
191                  " correct model)",
192                  store=False),
193         'sub_object':fields.many2one(
194                  'ir.model',
195                  'Sub-model',
196                  help='When a relation field is used this field'
197                  ' will show you the type of field you have selected',
198                  store=False),
199         'sub_model_object_field':fields.many2one(
200                  'ir.model.fields',
201                  'Sub Field',
202                  help="When you choose relationship fields "
203                  "this field will specify the sub value you can use.",
204                  store=False),
205         'null_value':fields.char(
206                  'Null Value',
207                  help="This Value is used if the field is empty",
208                  size=50, store=False),
209         'copyvalue':fields.char(
210                 'Expression',
211                 size=100,
212                 help="Copy and paste the value in the "
213                 "location you want to use a system value.",
214                 store=False),
215         'table_html':fields.text(
216              'HTML code',
217              help="Copy this html code to your HTML message"
218              " body for displaying the info in your mail.",
219              store=False),
220         'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
221     }
222
223     _sql_constraints = [
224         ('name', 'unique (name)','The template name must be unique !')
225     ]
226
227     def create_action(self, cr, uid, ids, context=None):
228         vals = {}
229         if context is None:
230             context = {}
231         template_obj = self.browse(cr, uid, ids, context=context)[0]
232         src_obj = template_obj.object_name.model
233         vals['ref_ir_act_window'] = self.pool.get('ir.actions.act_window').create(cr, uid, {
234              'name': template_obj.name,
235              'type': 'ir.actions.act_window',
236              'res_model': 'email_template.send.wizard',
237              'src_model': src_obj,
238              'view_type': 'form',
239              'context': "{'src_model':'%s','template_id':'%d','src_rec_id':active_id,'src_rec_ids':active_ids}" % (src_obj, template_obj.id),
240              'view_mode':'form,tree',
241              'view_id': self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'email_template.send.wizard.form')], context=context)[0],
242              'target': 'new',
243              'auto_refresh':1
244         }, context)
245         vals['ref_ir_value'] = self.pool.get('ir.values').create(cr, uid, {
246              'name': _('Send Mail (%s)') % template_obj.name,
247              'model': src_obj,
248              'key2': 'client_action_multi',
249              'value': "ir.actions.act_window," + str(vals['ref_ir_act_window']),
250              'object': True,
251          }, context)
252         self.write(cr, uid, ids, {
253             'ref_ir_act_window': vals['ref_ir_act_window'],
254             'ref_ir_value': vals['ref_ir_value'],
255         }, context)
256         return True
257
258     def unlink_action(self, cr, uid, ids, context=None):
259         for template in self.browse(cr, uid, ids, context=context):
260             try:
261                 if template.ref_ir_act_window:
262                     self.pool.get('ir.actions.act_window').unlink(cr, uid, template.ref_ir_act_window.id, context)
263                 if template.ref_ir_value:
264                     self.pool.get('ir.values').unlink(cr, uid, template.ref_ir_value.id, context)
265             except:
266                 raise osv.except_osv(_("Warning"), _("Deletion of Record failed"))
267
268     def delete_action(self, cr, uid, ids, context=None):
269         self.unlink_action(cr, uid, ids, context=context)
270         return True
271
272     def unlink(self, cr, uid, ids, context=None):
273         self.unlink_action(cr, uid, ids, context=context)
274         return super(email_template, self).unlink(cr, uid, ids, context=context)
275
276     def copy(self, cr, uid, id, default=None, context=None):
277         if default is None:
278             default = {}
279         default = default.copy()
280         old = self.read(cr, uid, id, ['name'], context=context)
281         new_name = _("Copy of template ") + old.get('name', 'No Name')
282         check = self.search(cr, uid, [('name', '=', new_name)], context=context)
283         if check:
284             new_name = new_name + '_' + random.choice('abcdefghij') + random.choice('lmnopqrs') + random.choice('tuvwzyz')
285         default.update({'name':new_name})
286         return super(email_template, self).copy(cr, uid, id, default, context)
287
288     def build_expression(self, field_name, sub_field_name, null_value):
289         """
290         Returns a template expression based on data provided
291         @param field_name: field name
292         @param sub_field_name: sub field name (M2O)
293         @param null_value: default value if the target value is empty
294         @return: computed expression
295         """
296         expression = ''
297         if field_name:
298             expression = "${object." + field_name
299             if sub_field_name:
300                 expression += "." + sub_field_name
301             if null_value:
302                 expression += " or '''%s'''" % null_value
303             expression += "}"
304         return expression
305
306     def onchange_model_object_field(self, cr, uid, ids, model_object_field, context=None):
307         if not model_object_field:
308             return {}
309         result = {}
310         field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
311         #Check if field is relational
312         if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
313             res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
314             if res_ids:
315                 result['sub_object'] = res_ids[0]
316                 result['copyvalue'] = self.build_expression(False, False, False)
317                 result['sub_model_object_field'] = False
318                 result['null_value'] = False
319         else:
320             #Its a simple field... just compute placeholder
321             result['sub_object'] = False
322             result['copyvalue'] = self.build_expression(field_obj.name, False, False)
323             result['sub_model_object_field'] = False
324             result['null_value'] = False
325         return {'value':result}
326
327     def onchange_sub_model_object_field(self, cr, uid, ids, model_object_field, sub_model_object_field, context=None):
328         if not model_object_field or not sub_model_object_field:
329             return {}
330         result = {}
331         field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
332         if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
333             res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
334             sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
335             if res_ids:
336                 result['sub_object'] = res_ids[0]
337                 result['copyvalue'] = self.build_expression(field_obj.name, sub_field_obj.name, False)
338                 result['sub_model_object_field'] = sub_model_object_field
339                 result['null_value'] = False
340         else:
341             #Its a simple field... just compute placeholder
342             result['sub_object'] = False
343             result['copyvalue'] = self.build_expression(field_obj.name, False, False)
344             result['sub_model_object_field'] = False
345             result['null_value'] = False
346         return {'value':result}
347
348     def onchange_null_value(self, cr, uid, ids, model_object_field, sub_model_object_field, null_value, context=None):
349         if not model_object_field and not null_value:
350             return {}
351         result = {}
352         field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
353         if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
354             res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
355             sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
356             if res_ids:
357                 result['sub_object'] = res_ids[0]
358                 result['copyvalue'] = self.build_expression(field_obj.name, sub_field_obj.name, null_value)
359                 result['sub_model_object_field'] = sub_model_object_field
360                 result['null_value'] = null_value
361         else:
362             #Its a simple field... just compute placeholder
363             result['sub_object'] = False
364             result['copyvalue'] = self.build_expression(field_obj.name, False, null_value)
365             result['sub_model_object_field'] = False
366             result['null_value'] = null_value
367         return {'value':result}
368
369     def _add_attachment(self, cursor, user, mailbox_id, name, data, filename, context=None):
370         """
371         Add an attachment to a given mailbox entry.
372         :param data: base64 encoded attachment data to store
373         """
374         attachment_obj = self.pool.get('ir.attachment')
375         attachment_data = {
376             'name':  (name or '') + _(' (Email Attachment)'),
377             'datas': data,
378             'datas_fname': filename,
379             'description': name or _('No Description'),
380             'res_model':'email.message',
381             'res_id': mailbox_id,
382         }
383         attachment_id = attachment_obj.create(cursor,
384                                               user,
385                                               attachment_data,
386                                               context)
387         if attachment_id:
388             self.pool.get('email.message').write(
389                               cursor,
390                               user,
391                               mailbox_id,
392                               {
393                                'attachments_ids':[(4, attachment_id)],
394                                'mail_type':'multipart/mixed'
395                               },
396                               context)
397
398     def generate_attach_reports(self, cursor, user, template, record_id, mail, context=None):
399         """
400         Generate report to be attached and attach it
401         to the email, and add any directly attached files as well.
402
403         @param cursor: Database Cursor
404         @param user: ID of User
405         @param template: Browse record of
406                          template
407         @param record_id: ID of the target model
408                           for which this mail has
409                           to be generated
410         @param mail: Browse record of email object
411         @return: True
412         """
413         if template.report_template:
414             reportname = 'report.' + \
415                 self.pool.get('ir.actions.report.xml').read(
416                                              cursor,
417                                              user,
418                                              template.report_template.id,
419                                              ['report_name'],
420                                              context)['report_name']
421             service = netsvc.LocalService(reportname)
422             data = {}
423             data['model'] = template.model_int_name
424             (result, format) = service.create(cursor,
425                                               user,
426                                               [record_id],
427                                               data,
428                                               context)
429             fname = tools.ustr(get_value(cursor, user, record_id,
430                                          template.file_name, template, context)
431                                or 'Report')
432             ext = '.' + format
433             if not fname.endswith(ext):
434                 fname += ext
435             self._add_attachment(cursor, user, mail.id, mail.subject, base64.b64encode(result), fname, context)
436
437         if template.attachment_ids:
438             for attachment in template.attachment_ids:
439                 self._add_attachment(cursor, user, mail.id, attachment.name, attachment.datas, attachment.datas_fname, context)
440
441         return True
442
443     def _generate_mailbox_item_from_template(self, cursor, user, template, record_id, context=None):
444         """
445         Generates an email from the template for
446         record record_id of target object
447
448         @param cursor: Database Cursor
449         @param user: ID of User
450         @param template: Browse record of
451                          template
452         @param record_id: ID of the target model
453                           for which this mail has
454                           to be generated
455         @return: ID of created object
456         """
457         if context is None:
458             context = {}
459         #If account to send from is in context select it, else use enforced account
460         if 'account_id' in context.keys():
461             from_account = self.pool.get('email.smtp_server').read(
462                                                     cursor,
463                                                     user,
464                                                     context.get('account_id'),
465                                                     ['name', 'email_id'],
466                                                     context
467                                                     )
468         else:
469             from_account = {
470                             'id':template.from_account.id,
471                             'name':template.from_account.name,
472                             'email_id':template.from_account.email_id
473                             }
474         lang = get_value(cursor,
475                          user,
476                          record_id,
477                          template.lang,
478                          template,
479                          context)
480         if lang:
481             ctx = context.copy()
482             ctx.update({'lang':lang})
483             template = self.browse(cursor, user, template.id, context=ctx)
484
485         # determine name of sender, either it is specified in email_id or we
486         # use the account name
487         email_id = from_account['email_id'].strip()
488         email_from = re.findall(r'([^ ,<@]+@[^> ,]+)', email_id)[0]
489         if email_from != email_id:
490             # we should keep it all, name is probably specified in the address
491             email_from = from_account['email_id']
492         else:
493             email_from = tools.ustr(from_account['name']) + "<" + tools.ustr(email_id) + ">"
494
495         # FIXME: should do this in a loop and rename template fields to the corresponding
496         # mailbox fields. (makes no sense to have different names I think.
497         mailbox_values = {
498             'email_from': email_from,
499             'email_to':get_value(cursor,
500                                user,
501                                record_id,
502                                template.def_to,
503                                template,
504                                context),
505             'email_cc':get_value(cursor,
506                                user,
507                                record_id,
508                                template.def_cc,
509                                template,
510                                context),
511             'email_bcc':get_value(cursor,
512                                 user,
513                                 record_id,
514                                 template.def_bcc,
515                                 template,
516                                 context),
517             'reply_to':get_value(cursor,
518                                 user,
519                                 record_id,
520                                 template.reply_to,
521                                 template,
522                                 context),
523             'subject':get_value(cursor,
524                                     user,
525                                     record_id,
526                                     template.def_subject,
527                                     template,
528                                     context),
529             'body_text':get_value(cursor,
530                                       user,
531                                       record_id,
532                                       template.def_body_text,
533                                       template,
534                                       context),
535             'body_html':get_value(cursor,
536                                       user,
537                                       record_id,
538                                       template.def_body_html,
539                                       template,
540                                       context),
541             #This is a mandatory field when automatic emails are sent
542             'state':'na',
543             'folder':'drafts',
544             'mail_type':'multipart/alternative',
545             'template_id': template.id
546         }
547
548         if template['message_id']:
549             # use provided message_id with placeholders
550             mailbox_values.update({'message_id': get_value(cursor, user, record_id, template['message_id'], template, context)})
551
552         elif template['track_campaign_item']:
553             # get appropriate message-id
554             mailbox_values.update({'message_id': tools.misc.generate_tracking_message_id(record_id)})
555 #
556 #        if not mailbox_values['account_id']:
557 #            raise Exception("Unable to send the mail. No account linked to the template.")
558         #Use signatures if allowed
559         if template.use_sign:
560             sign = self.pool.get('res.users').read(cursor, user, user, ['signature'], context)['signature']
561             if mailbox_values['body_text']:
562                 mailbox_values['body_text'] += sign
563             if mailbox_values['body_html']:
564                 mailbox_values['body_html'] += sign
565         mailbox_id = self.pool.get('email.message').create(cursor, user, mailbox_values, context)
566
567         return mailbox_id
568
569
570     def generate_mail(self, cursor, user, template_id, record_ids,  context=None):
571         if context is None:
572             context = {}
573         template = self.browse(cursor, user, template_id, context=context)
574         if not template:
575             raise Exception("The requested template could not be loaded")
576         result = True
577         mailbox_obj = self.pool.get('email.message')
578         for record_id in record_ids:
579             mailbox_id = self._generate_mailbox_item_from_template(
580                                                                 cursor,
581                                                                 user,
582                                                                 template,
583                                                                 record_id,
584                                                                 context)
585             mail = mailbox_obj.browse(cursor, user, mailbox_id, context=context
586                                               )
587             if template.report_template or template.attachment_ids:
588                 self.generate_attach_reports(cursor, user, template, record_id, mail, context )
589             mailbox_obj.write(cursor, user, mailbox_id, {'folder':'outbox', 'state': 'waiting'}, context=context)
590         return result
591
592 email_template()
593
594 class email_message(osv.osv):
595     _inherit = 'email.message'
596     _columns = {
597         'template_id': fields.many2one('email.template', 'Email-Template', readonly=True),
598         }
599
600     def process_email_queue(self, cr, uid, ids=None, context=None):
601         result = super(email_template, self).copy(cr, uid, id, default, context)
602         attachment_obj = self.pool.get('ir.attachment')
603         for message in self.browse(cr, uid, result, context):
604             if message.template_id and message.template_id.auto_delete:
605                 self.unlink(cr, uid, [id], context=context)
606                 attachment_ids = [x.id for x in message.attachments_ids]
607                 attachment_obj.unlink(cr, uid, attachment_ids, context=context)
608         return result
609
610 email_message()
611
612 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: