1cef7aac6e3c98c74de1ab4433552f5ab5d571ac
[odoo/odoo.git] / addons / email_template / email_template.py
1 import base64
2 import random
3 import time
4 import types
5 import netsvc
6
7 LOGGER = netsvc.Logger()
8
9 TEMPLATE_ENGINES = []
10
11 from osv import osv, fields
12 from tools.translate import _
13 from mako.template import Template  #For backward combatibility
14 try:
15     from mako.template import Template as MakoTemplate
16     from mako import exceptions
17     TEMPLATE_ENGINES.append(('mako', 'Mako Templates'))
18 except:
19     LOGGER.notifyChannel(
20                          _("Email Template"),
21                          netsvc.LOG_ERROR,
22                          _("Mako templates not installed")
23                          )
24 try:
25     from django.template import Context, Template as DjangoTemplate
26     #Workaround for bug:
27     #http://code.google.com/p/django-tagging/issues/detail?id=110
28     from django.conf import settings
29     settings.configure()
30     #Workaround ends
31     TEMPLATE_ENGINES.append(('django', 'Django Template'))
32 except:
33     LOGGER.notifyChannel(
34                          _("Email Template"),
35                          netsvc.LOG_ERROR,
36                          _("Django templates not installed")
37                          )
38
39 import email_template_engines
40 import tools
41 import report
42 import pooler
43
44
45 def get_value(cursor, user, recid, message=None, template=None, context=None):
46     """
47     Evaluates an expression and returns its value
48     @param cursor: Database Cursor
49     @param user: ID of current user
50     @param recid: ID of the target record under evaluation
51     @param message: The expression to be evaluated
52     @param template: BrowseRecord object of the current template
53     @param context: Open ERP Context
54     @return: Computed message (unicode) or u""
55     """
56     pool = pooler.get_pool(cursor.dbname)
57     if message is None:
58         message = {}
59     #Returns the computed expression
60     if message:
61         try:
62             message = tools.ustr(message)
63             object = pool.get(template.model_int_name).browse(cursor, user, recid, context)
64             env = {
65                 'user':pool.get('res.users').browse(cursor, user, user, context),
66                 'db':cursor.dbname
67                    }
68             if template.template_language == 'mako':
69                 templ = MakoTemplate(message, input_encoding='utf-8')
70                 reply = MakoTemplate(message).render_unicode(object=object,
71                                                              peobject=object,
72                                                              env=env,
73                                                              format_exceptions=True)
74             elif template.template_language == 'django':
75                 templ = DjangoTemplate(message)
76                 env['object'] = object
77                 env['peobject'] = object
78                 reply = templ.render(Context(env))
79             return reply or False
80         except Exception:
81             return u""
82     else:
83         return message
84
85 class email_template(osv.osv):
86     "Templates for sending Email"
87     
88     _name = "email.template"
89     _description = 'Email Templates for Models'
90
91     def change_model(self, cursor, user, ids, object_name, context=None):
92         if object_name:
93             mod_name = self.pool.get('ir.model').read(
94                                               cursor,
95                                               user,
96                                               object_name,
97                                               ['model'], context)['model']
98         else:
99             mod_name = False
100         return {
101                 'value':{'model_int_name':mod_name}
102                 }
103
104     _columns = {
105         'name' : fields.char('Name', size=100, required=True),
106         'object_name':fields.many2one('ir.model', 'Model'),
107         'model_int_name':fields.char('Model Internal Name', size=200,),
108         'enforce_from_account':fields.many2one(
109                    'email_template.account',
110                    string="Enforce From Account",
111                    help="Emails will be sent only from this account."),
112         'from_email' : fields.related('enforce_from_account', 'email_id',
113                                                 type='char', string='From',),        
114         'def_to':fields.char(
115                  'Recepient (To)',
116                  size=250,
117                  help="The default recepient of email." 
118                  "Placeholders can be used here."),
119         'def_cc':fields.char(
120                  'Default CC',
121                  size=250,
122                  help="The default CC for the email."
123                  " Placeholders can be used here."),
124         'def_bcc':fields.char(
125                   'Default BCC',
126                   size=250,
127                   help="The default BCC for the email."
128                   " Placeholders can be used here."),
129         'lang':fields.char(
130                    'Language',
131                    size=250,
132                    help="The default language for the email."
133                    " Placeholders can be used here. "
134                    "eg. ${object.partner_id.lang}"),
135         'def_subject':fields.char(
136                   'Default Subject',
137                   size=200,
138                   help="The default subject of email."
139                   " Placeholders can be used here.",
140                   translate=True),
141         'def_body_text':fields.text(
142                     'Standard Body (Text)',
143                     help="The text version of the mail",
144                     translate=True),
145         'def_body_html':fields.text(
146                     'Body (Text-Web Client Only)',
147                     help="The text version of the mail",
148                     translate=True),
149         'use_sign':fields.boolean(
150                   'Signature',
151                   help="the signature from the User details" 
152                   "will be appened to the mail"),
153         'file_name':fields.char(
154                 'File Name Pattern',
155                 size=200,
156                 help="File name pattern can be specified with placeholders." 
157                 "eg. 2009_SO003.pdf",
158                 translate=True),
159         'report_template':fields.many2one(
160                   'ir.actions.report.xml',
161                   'Report to send'),
162         'ref_ir_act_window':fields.many2one(
163                     'ir.actions.act_window',
164                     'Window Action',
165                     readonly=True),
166         'ref_ir_value':fields.many2one(
167                    'ir.values',
168                    'Wizard Button',
169                    readonly=True),
170         'allowed_groups':fields.many2many(
171                   'res.groups',
172                   'template_group_rel',
173                   'templ_id', 'group_id',
174                   string="Allowed User Groups",
175                   help="Only users from these groups will be"
176                   " allowed to send mails from this Template"),
177         'model_object_field':fields.many2one(
178                  'ir.model.fields',
179                  string="Field",
180                  help="Select the field from the model you want to use."
181                  "\nIf it is a relationship field you will be able to "
182                  "choose the nested values in the box below\n(Note:If "
183                  "there are no values make sure you have selected the"
184                  " correct model)",
185                  store=False),
186         'sub_object':fields.many2one(
187                  'ir.model',
188                  'Sub-model',
189                  help='When a relation field is used this field'
190                  ' will show you the type of field you have selected',
191                  store=False),
192         'sub_model_object_field':fields.many2one(
193                  'ir.model.fields',
194                  'Sub Field',
195                  help="When you choose relationship fields "
196                  "this field will specify the sub value you can use.",
197                  store=False),
198         'null_value':fields.char(
199                  'Null Value',
200                  help="This Value is used if the field is empty",
201                  size=50, store=False),
202         'copyvalue':fields.char(
203                 'Expression',
204                 size=100,
205                 help="Copy and paste the value in the "
206                 "location you want to use a system value.",
207                 store=False),
208         'table_html':fields.text(
209              'HTML code',
210              help="Copy this html code to your HTML message"
211              " body for displaying the info in your mail.",
212              store=False),
213         #Template language(engine eg.Mako) specifics
214         'template_language':fields.selection(
215                 TEMPLATE_ENGINES,
216                 'Templating Language',
217                 required=True
218                 )
219     }
220
221     _defaults = {
222         'template_language' : lambda *a:'mako',
223
224     }
225     _sql_constraints = [
226         ('name', 'unique (name)', _('The template name must be unique !'))
227     ]
228
229     def create(self, cr, uid, vals, context=None):
230         id = super(email_template, self).create(cr, uid, vals, context)
231         src_obj = self.pool.get('ir.model').read(cr, uid, vals['object_name'], ['model'], context)['model']
232         vals['ref_ir_act_window'] = self.pool.get('ir.actions.act_window').create(cr, uid, {
233              'name': _("%s Mail Form") % vals['name'],
234              'type': 'ir.actions.act_window',
235              'res_model': 'email_template.send.wizard',
236              'src_model': src_obj,
237              'view_type': 'form',
238              'context': "{'src_model':'%s','template_id':'%d','src_rec_id':active_id,'src_rec_ids':active_ids}" % (src_obj, id),
239              'view_mode':'form,tree',
240              'view_id': self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'email_template.send.wizard.form')], context=context)[0],
241              'target': 'new',
242              'auto_refresh':1
243         }, context)
244         vals['ref_ir_value'] = self.pool.get('ir.values').create(cr, uid, {
245              'name': _('Send Mail (%s)') % vals['name'],
246              'model': src_obj,
247              'key2': 'client_action_multi',
248              'value': "ir.actions.act_window," + str(vals['ref_ir_act_window']),
249              'object': True,
250          }, context)
251         self.write(cr, uid, id, {
252             'ref_ir_act_window': vals['ref_ir_act_window'],
253             'ref_ir_value': vals['ref_ir_value'],
254         }, context)
255         return id
256
257     def unlink(self, cr, uid, ids, context=None):
258         for template in self.browse(cr, uid, ids, context):
259             obj = self.pool.get(template.object_name.model)
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         return super(email_template, self).unlink(cr, uid, ids, context)
268     
269     def copy(self, cr, uid, id, default=None, context=None):
270         if default is None:
271             default = {}
272         default = default.copy()
273         old = self.read(cr, uid, id, ['name'], context=context)
274         new_name = _("Copy of template ") + old.get('name', 'No Name')
275         check = self.search(cr, uid, [('name', '=', new_name)], context=context)
276         if check:
277             new_name = new_name + '_' + random.choice('abcdefghij') + random.choice('lmnopqrs') + random.choice('tuvwzyz')
278         default.update({'name':new_name})
279         return super(email_template, self).copy(cr, uid, id, default, context)
280     
281     def compute_pl(self,
282                    model_object_field,
283                    sub_model_object_field,
284                    null_value, template_language='mako'):
285         """
286         Returns the expression based on data provided
287         @param model_object_field: First level field
288         @param sub_model_object_field: Second level drilled down field (M2O)
289         @param null_value: What has to be returned if the value is empty
290         @param template_language: The language used for templating
291         @return: computed expression
292         """
293         #Configure for MAKO
294         copy_val = ''
295         if template_language == 'mako':
296             if model_object_field:
297                 copy_val = "${object." + model_object_field
298             if sub_model_object_field:
299                 copy_val += "." + sub_model_object_field
300             if null_value:
301                 copy_val += " or '" + null_value + "'"
302             if model_object_field:
303                 copy_val += "}"
304         elif template_language == 'django':
305             if model_object_field:
306                 copy_val = "{{object." + model_object_field
307             if sub_model_object_field:
308                 copy_val += "." + sub_model_object_field
309             if null_value:
310                 copy_val = copy_val + '|default:"' + null_value + '"'  
311             copy_val = copy_val + "}}"        
312         return copy_val 
313             
314     def onchange_model_object_field(self, cr, uid, ids, model_object_field, template_language, context=None):
315         if not model_object_field:
316             return {}
317         result = {}
318         field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
319         #Check if field is relational
320         if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
321             res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
322             if res_ids:
323                 result['sub_object'] = res_ids[0]
324                 result['copyvalue'] = self.compute_pl(False,
325                                                       False,
326                                                       False,
327                                                       template_language)
328                 result['sub_model_object_field'] = False
329                 result['null_value'] = False
330         else:
331             #Its a simple field... just compute placeholder
332             result['sub_object'] = False
333             result['copyvalue'] = self.compute_pl(field_obj.name,
334                                                   False,
335                                                   False,
336                                                   template_language
337                                                   )
338             result['sub_model_object_field'] = False
339             result['null_value'] = False
340         return {'value':result}
341         
342     def onchange_sub_model_object_field(self, cr, uid, ids, model_object_field, sub_model_object_field, template_language, context=None):
343         if not model_object_field or not sub_model_object_field:
344             return {}
345         result = {}
346         field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
347         if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
348             res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
349             sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
350             if res_ids:
351                 result['sub_object'] = res_ids[0]
352                 result['copyvalue'] = self.compute_pl(field_obj.name,
353                                                       sub_field_obj.name,
354                                                       False,
355                                                       template_language
356                                                       )
357                 result['sub_model_object_field'] = sub_model_object_field
358                 result['null_value'] = False
359         else:
360             #Its a simple field... just compute placeholder
361             result['sub_object'] = False
362             result['copyvalue'] = self.compute_pl(field_obj.name,
363                                                   False,
364                                                   False,
365                                                   template_language
366                                                   )
367             result['sub_model_object_field'] = False
368             result['null_value'] = False
369         return {'value':result}
370
371     def onchange_null_value(self, cr, uid, ids, model_object_field, sub_model_object_field, null_value, template_language, context=None):
372         if not model_object_field and not null_value:
373             return {}
374         result = {}
375         field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
376         if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
377             res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
378             sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
379             if res_ids:
380                 result['sub_object'] = res_ids[0]
381                 result['copyvalue'] = self.compute_pl(field_obj.name,
382                                                       sub_field_obj.name,
383                                                       null_value,
384                                                       template_language
385                                                       )
386                 result['sub_model_object_field'] = sub_model_object_field
387                 result['null_value'] = null_value
388         else:
389             #Its a simple field... just compute placeholder
390             result['sub_object'] = False
391             result['copyvalue'] = self.compute_pl(field_obj.name,
392                                                   False,
393                                                   null_value,
394                                                   template_language
395                                                   )
396             result['sub_model_object_field'] = False
397             result['null_value'] = null_value
398         return {'value':result}
399                
400     def generate_attach_reports(self,
401                                  cursor,
402                                  user,
403                                  template,
404                                  record_id,
405                                  mail,
406                                  context=None):
407         """
408         Generate report to be attached and attach it
409         to the email
410         
411         @param cursor: Database Cursor
412         @param user: ID of User
413         @param template: Browse record of
414                          template
415         @param record_id: ID of the target model
416                           for which this mail has
417                           to be generated
418         @param mail: Browse record of email object 
419         @return: True 
420         """
421         reportname = 'report.' + \
422             self.pool.get('ir.actions.report.xml').read(
423                                          cursor,
424                                          user,
425                                          template.report_template.id,
426                                          ['report_name'],
427                                          context)['report_name']
428         service = netsvc.LocalService(reportname)
429         data = {}
430         data['model'] = template.model_int_name
431         (result, format) = service.create(cursor,
432                                           user,
433                                           [record_id],
434                                           data,
435                                           context)
436         attachment_obj = self.pool.get('ir.attachment')
437         new_att_vals = {
438             'name':mail.subject + ' (Email Attachment)',
439             'datas':base64.b64encode(result),
440             'datas_fname':tools.ustr(
441                              get_value(
442                                    cursor,
443                                    user,
444                                    record_id,
445                                    template.file_name,
446                                    template,
447                                    context
448                                    ) or 'Report') + "." + format,
449             'description':mail.subject or "No Description",
450             'res_model':'email_template.mailbox',
451             'res_id':mail.id
452         }
453         attachment_id = attachment_obj.create(cursor,
454                                               user,
455                                               new_att_vals,
456                                               context)
457         if attachment_id:
458             self.pool.get('email_template.mailbox').write(
459                               cursor,
460                               user,
461                               mail.id,
462                               {
463                                'attachments_ids':[
464                                                   [6, 0, [attachment_id]]
465                                                     ],
466                                'mail_type':'multipart/mixed'
467                                },
468                                context)
469         return True
470     
471     def generate_mailbox_item_from_template(self,
472                                       cursor,
473                                       user,
474                                       template,
475                                       record_id,
476                                       context=None):
477         """
478         Generates an email from the template for
479         record record_id of target object
480         
481         @param cursor: Database Cursor
482         @param user: ID of User
483         @param template: Browse record of
484                          template
485         @param record_id: ID of the target model
486                           for which this mail has
487                           to be generated
488         @return: ID of created object 
489         """
490         if context is None:
491             context = {}
492         #If account to send from is in context select it, else use enforced account
493         if 'account_id' in context.keys():
494             from_account = self.pool.get('email_template.account').read(
495                                                     cursor,
496                                                     user,
497                                                     context.get('account_id'),
498                                                     ['name', 'email_id'],
499                                                     context
500                                                     )
501         else:
502             from_account = {
503                             'id':template.enforce_from_account.id,
504                             'name':template.enforce_from_account.name,
505                             'email_id':template.enforce_from_account.email_id
506                             }
507         lang = get_value(cursor,
508                          user,
509                          record_id,
510                          template.lang,
511                          template,
512                          context)
513         if lang:
514             ctx = context.copy()
515             ctx.update({'lang':lang})
516             template = self.browse(cursor, user, template_id, context=ctx)
517         mailbox_values = {
518             'email_from': tools.ustr(from_account['name']) + \
519                         "<" + tools.ustr(from_account['email_id']) + ">",
520             'email_to':get_value(cursor,
521                                user,
522                                record_id,
523                                template.def_to,
524                                template,
525                                context),
526             'email_cc':get_value(cursor,
527                                user,
528                                record_id,
529                                template.def_cc,
530                                template,
531                                context),
532             'email_bcc':get_value(cursor,
533                                 user,
534                                 record_id,
535                                 template.def_bcc,
536                                 template,
537                                 context),
538             'subject':get_value(cursor,
539                                     user,
540                                     record_id,
541                                     template.def_subject,
542                                     template,
543                                     context),
544             'body_text':get_value(cursor,
545                                       user,
546                                       record_id,
547                                       template.def_body_text,
548                                       template,
549                                       context),
550             'body_html':get_value(cursor,
551                                       user,
552                                       record_id,
553                                       template.def_body_html,
554                                       template,
555                                       context),
556             'account_id' :from_account['id'],
557             #This is a mandatory field when automatic emails are sent
558             'state':'na',
559             'folder':'drafts',
560             'mail_type':'multipart/alternative' 
561         }
562         #Use signatures if allowed
563         if template.use_sign:
564             sign = self.pool.get('res.users').read(cursor,
565                                                    user,
566                                                    user,
567                                                    ['signature'],
568                                                    context)['signature']
569             if mailbox_values['body_text']:
570                 mailbox_values['body_text'] += sign
571             if mailbox_values['body_html']:
572                 mailbox_values['body_html'] += sign
573         mailbox_id = self.pool.get('email_template.mailbox').create(
574                                                              cursor,
575                                                              user,
576                                                              mailbox_values,
577                                                              context)
578         return mailbox_id
579         
580     def generate_mail(self,
581                       cursor,
582                       user,
583                       template_id,
584                       record_ids,
585                       context=None):
586         if context is None:
587             context = {}
588         template = self.browse(cursor, user, template_id, context=context)
589         if not template:
590             raise Exception("The requested template could not be loaded")
591         for record_id in record_ids:
592             mailbox_id = self._generate_mailbox_item_from_template(
593                                                                 cursor,
594                                                                 user,
595                                                                 template,
596                                                                 record_id,
597                                                                 context)
598             mail = self.pool.get('email_template.mailbox').browse(
599                                                         cursor,
600                                                         user,
601                                                         mailbox_id,
602                                                         context=context
603                                                               )
604             if template.report_template:
605                 self._generate_attach_reports(
606                                               cursor,
607                                               user,
608                                               template,
609                                               record_id,
610                                               mail,
611                                               context
612                                               )
613             self.pool.get('email_template.mailbox').write(
614                                                 cursor,
615                                                 user,
616                                                 mailbox_id,
617                                                 {'folder':'outbox'},
618                                                 context=context
619                                                       )
620         return True
621
622 email_template()
623
624 class email_template_preview(osv.osv_memory):
625     _name = "email_template.preview"
626     _description = "Email Template Preview"
627     
628     def _default_model(self, cursor, user, context=None):
629         """
630         Returns the default value for model field
631         @param cursor: Database Cursor
632         @param user: ID of current user
633         @param context: Open ERP Context
634         """
635         return self.pool.get('email.template').read(
636                                                    cursor,
637                                                    user,
638                                                    context['active_id'],
639                                                    ['object_name'],
640                                                    context)['object_name']
641         
642     _columns = {
643         'ref_template':fields.many2one(
644                                        'email.template',
645                                        'Template', readonly=True),
646         'rel_model':fields.many2one('ir.model', 'Model', readonly=True),
647         'to':fields.char('To', size=250, readonly=True),
648         'cc':fields.char('CC', size=250, readonly=True),
649         'bcc':fields.char('BCC', size=250, readonly=True),
650         'subject':fields.char('Subject', size=200, readonly=True),
651         'body_text':fields.text('Body', readonly=True),
652         'body_html':fields.text('Body', readonly=True),
653         'report':fields.char('Report Name', size=100, readonly=True),
654     }
655     _defaults = {
656         'ref_template': lambda self, cr, uid, ctx:ctx['active_id'],
657         'rel_model': _default_model
658     }
659     # Need to check 
660     def on_change_ref(self, cr, uid, ids, rel_model_ref, context=None):
661         if context is None:
662             context = {}
663         if not rel_model_ref:
664             return {}
665         vals = {}
666         if context == {}:
667             context = self.context
668         template = self.pool.get('email.template').browse(cr, uid, context['active_id'], context)
669         #Search translated template
670         lang = get_value(cr, uid, rel_model_ref, template.lang, template, context)
671         if lang:
672             ctx = context.copy()
673             ctx.update({'lang':lang})
674             template = self.pool.get('email.template').browse(cr, uid, context['active_id'], ctx)
675         vals['to'] = get_value(cr, uid, rel_model_ref, template.def_to, template, context)
676         vals['cc'] = get_value(cr, uid, rel_model_ref, template.def_cc, template, context)
677         vals['bcc'] = get_value(cr, uid, rel_model_ref, template.def_bcc, template, context)
678         vals['subject'] = get_value(cr, uid, rel_model_ref, template.def_subject, template, context)
679         vals['body_text'] = get_value(cr, uid, rel_model_ref, template.def_body_text, template, context)
680         vals['body_html'] = get_value(cr, uid, rel_model_ref, template.def_body_html, template, context)
681         vals['report'] = get_value(cr, uid, rel_model_ref, template.file_name, template, context)
682         return {'value':vals}
683         
684 email_template_preview()
685
686 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: