Improve code.
[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 = a
232         action_obj = self.pool.get('ir.actions.act_window')
233         for template in self.browse(cr, uid, ids, context=context):
234             src_obj = template.object_name.model
235             vals['ref_ir_act_window'] = action_obj.create(cr, uid, {
236                  'name': template.name,
237                  'type': 'ir.actions.act_window',
238                  'res_model': 'email_template.send.wizard',
239                  'src_model': src_obj,
240                  'view_type': 'form',
241                  'context': "{'src_model':'%s','template_id':'%d','src_rec_id':active_id,'src_rec_ids':active_ids}" % (src_obj, template.id),
242                  'view_mode':'form,tree',
243                  'view_id': self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'email_template.send.wizard.form')], context=context)[0],
244                  'target': 'new',
245                  'auto_refresh':1
246             }, context)
247             vals['ref_ir_value'] = self.pool.get('ir.values').create(cr, uid, {
248                  'name': _('Send Mail (%s)') % template.name,
249                  'model': src_obj,
250                  'key2': 'client_action_multi',
251                  'value': "ir.actions.act_window," + str(vals['ref_ir_act_window']),
252                  'object': True,
253              }, context)
254         self.write(cr, uid, ids, {
255                     'ref_ir_act_window': vals['ref_ir_act_window'],
256                     'ref_ir_value': vals['ref_ir_value'],
257                 }, context)
258         return True
259
260     def unlink_action(self, cr, uid, ids, context=None):
261         for template in self.browse(cr, uid, ids, context=context):
262             try:
263                 if template.ref_ir_act_window:
264                     self.pool.get('ir.actions.act_window').unlink(cr, uid, template.ref_ir_act_window.id, context)
265                 if template.ref_ir_value:
266                     self.pool.get('ir.values').unlink(cr, uid, template.ref_ir_value.id, context)
267             except:
268                 raise osv.except_osv(_("Warning"), _("Deletion of Record failed"))
269
270     def delete_action(self, cr, uid, ids, context=None):
271         self.unlink_action(cr, uid, ids, context=context)
272         return True
273
274     def unlink(self, cr, uid, ids, context=None):
275         self.unlink_action(cr, uid, ids, context=context)
276         return super(email_template, self).unlink(cr, uid, ids, context=context)
277
278     def copy(self, cr, uid, id, default=None, context=None):
279         if default is None:
280             default = {}
281         default = default.copy()
282         old = self.read(cr, uid, id, ['name'], context=context)
283         new_name = _("Copy of template ") + old.get('name', 'No Name')
284         check = self.search(cr, uid, [('name', '=', new_name)], context=context)
285         if check:
286             new_name = new_name + '_' + random.choice('abcdefghij') + random.choice('lmnopqrs') + random.choice('tuvwzyz')
287         default.update({'name':new_name})
288         return super(email_template, self).copy(cr, uid, id, default, context)
289
290     def build_expression(self, field_name, sub_field_name, null_value):
291         """
292         Returns a template expression based on data provided
293         @param field_name: field name
294         @param sub_field_name: sub field name (M2O)
295         @param null_value: default value if the target value is empty
296         @return: computed expression
297         """
298         expression = ''
299         if field_name:
300             expression = "${object." + field_name
301             if sub_field_name:
302                 expression += "." + sub_field_name
303             if null_value:
304                 expression += " or '''%s'''" % null_value
305             expression += "}"
306         return expression
307 #
308 #    def onchange_model_object_field(self, cr, uid, ids, model_object_field, context=None):
309 #        if not model_object_field:
310 #            return {}
311 #        result = {}
312 #        field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
313 #        #Check if field is relational
314 #        if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
315 #            res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
316 #            if res_ids:
317 #                result['sub_object'] = res_ids[0]
318 #                result['copyvalue'] = self.build_expression(False, False, False)
319 #                result['sub_model_object_field'] = False
320 #                result['null_value'] = False
321 #        else:
322 #            #Its a simple field... just compute placeholder
323 #            result['sub_object'] = False
324 #            result['copyvalue'] = self.build_expression(field_obj.name, False, False)
325 #            result['sub_model_object_field'] = False
326 #            result['null_value'] = False
327 #        return {'value':result}
328 #
329 #    def onchange_sub_model_object_field(self, cr, uid, ids, model_object_field, sub_model_object_field, context=None):
330 #        if not model_object_field or not sub_model_object_field:
331 #            return {}
332 #        result = {}
333 #        field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
334 #        if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
335 #            res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
336 #            sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
337 #            if res_ids:
338 #                result['sub_object'] = res_ids[0]
339 #                result['copyvalue'] = self.build_expression(field_obj.name, sub_field_obj.name, False)
340 #                result['sub_model_object_field'] = sub_model_object_field
341 #                result['null_value'] = False
342 #        else:
343 #            #Its a simple field... just compute placeholder
344 #            result['sub_object'] = False
345 #            result['copyvalue'] = self.build_expression(field_obj.name, False, False)
346 #            result['sub_model_object_field'] = False
347 #            result['null_value'] = False
348 #        return {'value':result}
349 #
350 #
351 #    def onchange_null_value(self, cr, uid, ids, model_object_field, sub_model_object_field, null_value, template_language, context=None):
352 #        if not model_object_field and not null_value:
353 #            return {}
354 #        result = {}
355 #        field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
356 #        if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
357 #            res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
358 #            sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
359 #            if res_ids:
360 #                result['sub_object'] = res_ids[0]
361 #                result['copyvalue'] = self.build_expression(field_obj.name,
362 #                                                      sub_field_obj.name,
363 #                                                      null_value,
364 #                                                      template_language
365 #                                                      )
366 #                result['sub_model_object_field'] = sub_model_object_field
367 #                result['null_value'] = null_value
368 #        else:
369 #            #Its a simple field... just compute placeholder
370 #            result['sub_object'] = False
371 #            result['copyvalue'] = self.build_expression(field_obj.name,
372 #                                                  False,
373 #                                                  null_value,
374 #                                                  template_language
375 #                                                  )
376 #            result['sub_model_object_field'] = False
377 #            result['null_value'] = null_value
378 #        return {'value':result}
379
380     def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, null_value=None, context=None):
381         result = {
382             'sub_object': False,
383             'copyvalue': False,
384             'sub_model_object_field': False,
385             'null_value': False
386             }
387         if model_object_field:
388             fields_obj = self.pool.get('ir.model.fields')
389             field_value = fields_obj.browse(cr, uid, model_object_field, context)
390             if field_value.ttype in ['many2one', 'one2many', 'many2many']:
391                 res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
392                 sub_field_value = False
393                 if sub_model_object_field:
394                     sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
395                 if res_ids:
396                     result.update({
397                         'sub_object': res_ids[0],
398                         'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False, null_value or False),
399                         'sub_model_object_field': sub_model_object_field or False,
400                         'null_value': null_value or False
401                         })
402             else:
403                 result.update({
404                         'copyvalue': self.build_expression(field_value.name, False, null_value or False),
405                         'null_value': null_value or False
406                         })
407         return {'value':result}
408
409     def _add_attachment(self, cursor, user, mailbox_id, name, data, filename, context=None):
410         """
411         Add an attachment to a given mailbox entry.
412         :param data: base64 encoded attachment data to store
413         """
414         attachment_obj = self.pool.get('ir.attachment')
415         attachment_data = {
416             'name':  (name or '') + _(' (Email Attachment)'),
417             'datas': data,
418             'datas_fname': filename,
419             'description': name or _('No Description'),
420             'res_model':'email.message',
421             'res_id': mailbox_id,
422         }
423         attachment_id = attachment_obj.create(cursor,
424                                               user,
425                                               attachment_data,
426                                               context)
427         if attachment_id:
428             self.pool.get('email.message').write(
429                               cursor,
430                               user,
431                               mailbox_id,
432                               {
433                                'attachments_ids':[(4, attachment_id)],
434                                'mail_type':'multipart/mixed'
435                               },
436                               context)
437
438     def generate_attach_reports(self, cursor, user, template, record_id, mail, context=None):
439         """
440         Generate report to be attached and attach it
441         to the email, and add any directly attached files as well.
442
443         @param cursor: Database Cursor
444         @param user: ID of User
445         @param template: Browse record of
446                          template
447         @param record_id: ID of the target model
448                           for which this mail has
449                           to be generated
450         @param mail: Browse record of email object
451         @return: True
452         """
453         if template.report_template:
454             reportname = 'report.' + \
455                 self.pool.get('ir.actions.report.xml').read(
456                                              cursor,
457                                              user,
458                                              template.report_template.id,
459                                              ['report_name'],
460                                              context)['report_name']
461             service = netsvc.LocalService(reportname)
462             data = {}
463             data['model'] = template.model_int_name
464             (result, format) = service.create(cursor,
465                                               user,
466                                               [record_id],
467                                               data,
468                                               context)
469             fname = tools.ustr(get_value(cursor, user, record_id,
470                                          template.file_name, template, context)
471                                or 'Report')
472             ext = '.' + format
473             if not fname.endswith(ext):
474                 fname += ext
475             self._add_attachment(cursor, user, mail.id, mail.subject, base64.b64encode(result), fname, context)
476
477         if template.attachment_ids:
478             for attachment in template.attachment_ids:
479                 self._add_attachment(cursor, user, mail.id, attachment.name, attachment.datas, attachment.datas_fname, context)
480
481         return True
482
483     def _generate_mailbox_item_from_template(self, cursor, user, template, record_id, context=None):
484         """
485         Generates an email from the template for
486         record record_id of target object
487
488         @param cursor: Database Cursor
489         @param user: ID of User
490         @param template: Browse record of
491                          template
492         @param record_id: ID of the target model
493                           for which this mail has
494                           to be generated
495         @return: ID of created object
496         """
497         if context is None:
498             context = {}
499         #If account to send from is in context select it, else use enforced account
500         if 'account_id' in context.keys():
501             from_account = self.pool.get('email.smtp_server').read(
502                                                     cursor,
503                                                     user,
504                                                     context.get('account_id'),
505                                                     ['name', 'email_id'],
506                                                     context
507                                                     )
508         else:
509             from_account = {
510                             'id':template.from_account.id,
511                             'name':template.from_account.name,
512                             'email_id':template.from_account.email_id
513                             }
514         lang = get_value(cursor,
515                          user,
516                          record_id,
517                          template.lang,
518                          template,
519                          context)
520         if lang:
521             ctx = context.copy()
522             ctx.update({'lang':lang})
523             template = self.browse(cursor, user, template.id, context=ctx)
524
525         # determine name of sender, either it is specified in email_id or we
526         # use the account name
527         email_id = from_account['email_id'].strip()
528         email_from = re.findall(r'([^ ,<@]+@[^> ,]+)', email_id)[0]
529         if email_from != email_id:
530             # we should keep it all, name is probably specified in the address
531             email_from = from_account['email_id']
532         else:
533             email_from = tools.ustr(from_account['name']) + "<" + tools.ustr(email_id) + ">"
534
535         # FIXME: should do this in a loop and rename template fields to the corresponding
536         # mailbox fields. (makes no sense to have different names I think.
537         mailbox_values = {
538             'email_from': email_from,
539             'email_to':get_value(cursor,
540                                user,
541                                record_id,
542                                template.def_to,
543                                template,
544                                context),
545             'email_cc':get_value(cursor,
546                                user,
547                                record_id,
548                                template.def_cc,
549                                template,
550                                context),
551             'email_bcc':get_value(cursor,
552                                 user,
553                                 record_id,
554                                 template.def_bcc,
555                                 template,
556                                 context),
557             'reply_to':get_value(cursor,
558                                 user,
559                                 record_id,
560                                 template.reply_to,
561                                 template,
562                                 context),
563             'subject':get_value(cursor,
564                                     user,
565                                     record_id,
566                                     template.def_subject,
567                                     template,
568                                     context),
569             'body_text':get_value(cursor,
570                                       user,
571                                       record_id,
572                                       template.def_body_text,
573                                       template,
574                                       context),
575             'body_html':get_value(cursor,
576                                       user,
577                                       record_id,
578                                       template.def_body_html,
579                                       template,
580                                       context),
581             #This is a mandatory field when automatic emails are sent
582             'state':'na',
583             'folder':'drafts',
584             'mail_type':'multipart/alternative',
585             'template_id': template.id
586         }
587
588         if template['message_id']:
589             # use provided message_id with placeholders
590             mailbox_values.update({'message_id': get_value(cursor, user, record_id, template['message_id'], template, context)})
591
592         elif template['track_campaign_item']:
593             # get appropriate message-id
594             mailbox_values.update({'message_id': tools.misc.generate_tracking_message_id(record_id)})
595 #
596 #        if not mailbox_values['account_id']:
597 #            raise Exception("Unable to send the mail. No account linked to the template.")
598         #Use signatures if allowed
599         if template.use_sign:
600             sign = self.pool.get('res.users').read(cursor, user, user, ['signature'], context)['signature']
601             if mailbox_values['body_text']:
602                 mailbox_values['body_text'] += sign
603             if mailbox_values['body_html']:
604                 mailbox_values['body_html'] += sign
605         mailbox_id = self.pool.get('email.message').create(cursor, user, mailbox_values, context)
606
607         return mailbox_id
608
609
610     def generate_mail(self, cursor, user, template_id, record_ids,  context=None):
611         if context is None:
612             context = {}
613         template = self.browse(cursor, user, template_id, context=context)
614         if not template:
615             raise Exception("The requested template could not be loaded")
616         result = True
617         mailbox_obj = self.pool.get('email.message')
618         for record_id in record_ids:
619             mailbox_id = self._generate_mailbox_item_from_template(
620                                                                 cursor,
621                                                                 user,
622                                                                 template,
623                                                                 record_id,
624                                                                 context)
625             mail = mailbox_obj.browse(cursor, user, mailbox_id, context=context
626                                               )
627             if template.report_template or template.attachment_ids:
628                 self.generate_attach_reports(cursor, user, template, record_id, mail, context )
629             mailbox_obj.write(cursor, user, mailbox_id, {'folder':'outbox', 'state': 'waiting'}, context=context)
630         return result
631
632 email_template()
633
634 class email_message(osv.osv):
635     _inherit = 'email.message'
636     _columns = {
637         'template_id': fields.many2one('email.template', 'Email-Template', readonly=True),
638         }
639
640     def process_email_queue(self, cr, uid, ids=None, context=None):
641         result = super(email_message, self).process_email_queue(cr, uid, ids, context)
642         attachment_obj = self.pool.get('ir.attachment')
643         for message in self.browse(cr, uid, result, context):
644             if message.template_id and message.template_id.auto_delete:
645                 self.unlink(cr, uid, [id], context=context)
646                 attachment_ids = [x.id for x in message.attachments_ids]
647                 attachment_obj.unlink(cr, uid, attachment_ids, context=context)
648         return result
649
650 email_message()
651
652 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: