[REF]: Replace the condition 'if not context' with 'if context is None'.
[odoo/odoo.git] / addons / base_action_rule / base_action_rule.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import fields, osv, orm
23 from tools.translate import _
24 from datetime import datetime
25 from datetime import timedelta
26 from tools.safe_eval import safe_eval
27 import pooler 
28 import re
29 import time
30 import tools
31
32 class base_action_rule(osv.osv):
33     """ Base Action Rules """
34
35     _name = 'base.action.rule'
36     _description = 'Action Rules'
37     
38     def _state_get(self, cr, uid, context=None):
39         """ Get State
40             @param self: The object pointer
41             @param cr: the current row, from the database cursor,
42             @param uid: the current user’s ID for security checks,
43             @param context: A standard dictionary for contextual values """
44         return self.state_get(cr, uid, context=context)
45
46     def state_get(self, cr, uid, context=None):
47         """ Get State
48             @param self: The object pointer
49             @param cr: the current row, from the database cursor,
50             @param uid: the current user’s ID for security checks,
51             @param context: A standard dictionary for contextual values """
52         return [('', '')]
53   
54     def priority_get(self, cr, uid, context=None):
55         """ Get Priority
56             @param self: The object pointer
57             @param cr: the current row, from the database cursor,
58             @param uid: the current user’s ID for security checks,
59             @param context: A standard dictionary for contextual values """
60         return [('', '')]
61
62     _columns = {
63         'name':  fields.char('Rule Name', size=64, required=True), 
64         'model_id': fields.many2one('ir.model', 'Object', required=True), 
65         'create_date': fields.datetime('Create Date', readonly=1), 
66         'active': fields.boolean('Active', help="If the active field is set to False,\
67  it will allow you to hide the rule without removing it."), 
68         'sequence': fields.integer('Sequence', help="Gives the sequence order \
69 when displaying a list of rules."), 
70         'trg_date_type':  fields.selection([
71             ('none', 'None'), 
72             ('create', 'Creation Date'), 
73             ('action_last', 'Last Action Date'), 
74             ('date', 'Date'), 
75             ('deadline', 'Deadline'), 
76             ], 'Trigger Date', size=16), 
77         'trg_date_range': fields.integer('Delay after trigger date', \
78                                          help="Delay After Trigger Date,\
79 specifies you can put a negative number. If you need a delay before the \
80 trigger date, like sending a reminder 15 minutes before a meeting."), 
81         'trg_date_range_type': fields.selection([('minutes', 'Minutes'), ('hour', 'Hours'), \
82                                 ('day', 'Days'), ('month', 'Months')], 'Delay type'), 
83         'trg_user_id':  fields.many2one('res.users', 'Responsible'), 
84         'trg_partner_id': fields.many2one('res.partner', 'Partner'), 
85         'trg_partner_categ_id': fields.many2one('res.partner.category', 'Partner Category'), 
86         'trg_state_from': fields.selection(_state_get, 'State', size=16), 
87         'trg_state_to': fields.selection(_state_get, 'Button Pressed', size=16), 
88
89         'act_method': fields.char('Call Object Method', size=64), 
90         'act_user_id': fields.many2one('res.users', 'Set Responsible to'), 
91         'act_state': fields.selection(_state_get, 'Set State to', size=16), 
92         'act_email_cc': fields.char('Add Watchers (Cc)', size=250, help="\
93 These people will receive a copy of the future communication between partner \
94 and users by email"), 
95         'act_remind_partner': fields.boolean('Remind Partner', help="Check \
96 this if you want the rule to send a reminder by email to the partner."), 
97         'act_remind_user': fields.boolean('Remind Responsible', help="Check \
98 this if you want the rule to send a reminder by email to the user."), 
99         'act_reply_to': fields.char('Reply-To', size=64), 
100         'act_remind_attach': fields.boolean('Remind with Attachment', help="Check this if you want that all documents attached to the object be attached to the reminder email sent."), 
101         'act_mail_to_user': fields.boolean('Mail to Responsible', help="Check\
102  this if you want the rule to send an email to the responsible person."), 
103         'act_mail_to_watchers': fields.boolean('Mail to Watchers (CC)', 
104                                                 help="Check this if you want \
105 the rule to mark CC(mail to any other person defined in actions)."), 
106         'act_mail_to_email': fields.char('Mail to these Emails', size=128, \
107         help="Email-id of the persons whom mail is to be sent"), 
108         'act_mail_body': fields.text('Mail body', help="Content of mail"), 
109         'regex_name': fields.char('Regex on Resource Name', size=128, help="Regular expression for matching name of the resource\
110 \ne.g.: 'urgent.*' will search for records having name starting with the string 'urgent'\
111 \nNote: This is case sensitive search."), 
112         'server_action_id': fields.many2one('ir.actions.server', 'Server Action', help="Describes the action name.\neg:on which object which action to be taken on basis of which condition"), 
113         'filter_id':fields.many2one('ir.filters', 'Filter', required=False), 
114         'act_email_from' : fields.char('Email From', size=64, required=False,
115                 help="Use a python expression to specify the right field on which one than we will use for the 'From' field of the header"),
116         'act_email_to' : fields.char('Email To', size=64, required=False,
117                                      help="Use a python expression to specify the right field on which one than we will use for the 'To' field of the header"),
118     }
119
120     _defaults = {
121         'active': lambda *a: True, 
122         'trg_date_type': lambda *a: 'none', 
123         'trg_date_range_type': lambda *a: 'day', 
124         'act_mail_to_user': lambda *a: 0, 
125         'act_remind_partner': lambda *a: 0, 
126         'act_remind_user': lambda *a: 0, 
127         'act_mail_to_watchers': lambda *a: 0, 
128     }
129     
130     _order = 'sequence'
131     
132     def onchange_model_id(self, cr, uid, ids, name):
133         #This is not a good solution as it will affect the domain only on onchange
134         res = {'domain':{'filter_id':[]}}
135         if name:
136             model_name = self.pool.get('ir.model').read(cr, uid, [name], ['model'])
137             if model_name:
138                 mod_name = model_name[0]['model']
139                 res['domain'] = {'filter_id': [('model_id','=',mod_name)]}
140         else:
141             res['value'] = {'filter_id':False}
142         return res
143
144     def pre_action(self, cr, uid, ids, model, context=None):
145         # Searching for action rules
146         cr.execute("SELECT model.model, rule.id  FROM base_action_rule rule \
147                         LEFT JOIN ir_model model on (model.id = rule.model_id) \
148                         where active")
149         res = cr.fetchall()
150         # Check if any rule matching with current object
151         for obj_name, rule_id in res:
152             if not (model == obj_name):
153                 continue
154             else:
155                 obj = self.pool.get(obj_name)
156                 self._action(cr, uid, [rule_id], obj.browse(cr, uid, ids, context=context), context=context)
157         return True
158
159     def _create(self, old_create, model, context=None):
160         if context is None:
161             context  = {}
162         def make_call_old(cr, uid, vals, context=context):
163             new_id = old_create(cr, uid, vals, context=context)
164             if not context.get('action'):
165                 self.pre_action(cr, uid, [new_id], model, context=context)
166             return new_id
167         return make_call_old
168     
169     def _write(self, old_write, model, context=None):
170         if context is None:
171             context  = {}
172         def make_call_old(cr, uid, ids, vals, context=context):
173             if isinstance(ids, (str, int, long)):
174                 ids = [ids]
175             if not context.get('action'):
176                 self.pre_action(cr, uid, ids, model, context=context)
177             return old_write(cr, uid, ids, vals, context=context)
178         return make_call_old
179
180     def _register_hook(self, cr, uid, ids, context=None):
181         if context is None:
182             context = {}
183         for action_rule in self.browse(cr, uid, ids, context=context):
184             model = action_rule.model_id.model
185             obj_pool = self.pool.get(model)
186             if not hasattr(obj_pool, 'base_action_ruled'):
187                 obj_pool.create = self._create(obj_pool.create, model, context=context)
188                 obj_pool.write = self._write(obj_pool.write, model, context=context)
189                 obj_pool.base_action_ruled = True
190
191         return True
192     def create(self, cr, uid, vals, context=None):
193         res_id = super(base_action_rule, self).create(cr, uid, vals, context=context)
194         self._register_hook(cr, uid, [res_id], context=context)        
195         return res_id
196     
197     def write(self, cr, uid, ids, vals, context=None):
198         res = super(base_action_rule, self).write(cr, uid, ids, vals, context=context)
199         self._register_hook(cr, uid, ids, context=context)
200         return res
201
202     def _check(self, cr, uid, automatic=False, use_new_cursor=False, \
203                        context=None):
204         """
205         This Function is call by scheduler.
206         """
207         rule_pool = self.pool.get('base.action.rule')
208         rule_ids = rule_pool.search(cr, uid, [], context=context)
209         return self._register_hook(cr, uid, rule_ids, context=context)
210         
211
212     def format_body(self, body):
213         """ Foramat Action rule's body
214             @param self: The object pointer """
215         return body and tools.ustr(body) or ''
216
217     def format_mail(self, obj, body):
218         """ Foramat Mail
219             @param self: The object pointer """
220
221         data = {
222             'object_id': obj.id, 
223             'object_subject': hasattr(obj, 'name') and obj.name or False, 
224             'object_date': hasattr(obj, 'date') and obj.date or False, 
225             'object_description': hasattr(obj, 'description') and obj.description or False, 
226             'object_user': hasattr(obj, 'user_id') and (obj.user_id and obj.user_id.name) or '/', 
227             'object_user_email': hasattr(obj, 'user_id') and (obj.user_id and \
228                                     obj.user_id.address_id and obj.user_id.address_id.email) or '/', 
229             'object_user_phone': hasattr(obj, 'user_id') and (obj.user_id and\
230                                      obj.user_id.address_id and obj.user_id.address_id.phone) or '/', 
231             'partner': hasattr(obj, 'partner_id') and (obj.partner_id and obj.partner_id.name) or '/', 
232             'partner_email': hasattr(obj, 'partner_address_id') and (obj.partner_address_id and\
233                                          obj.partner_address_id.email) or '/', 
234         }
235         return self.format_body(body % data)
236
237     def email_send(self, cr, uid, obj, emails, body, emailfrom=None, context=None):
238         """ send email
239             @param self: The object pointer
240             @param cr: the current row, from the database cursor,
241             @param uid: the current user’s ID for security checks,
242             @param email: pass the emails
243             @param emailfrom: Pass name the email From else False
244             @param context: A standard dictionary for contextual values """
245
246         if not emailfrom:
247             emailfrom = tools.config.get('email_from', False)
248
249         if context is None:
250             context = {}
251
252         body = self.format_mail(obj, body)
253         if not emailfrom:
254             if hasattr(obj, 'user_id')  and obj.user_id and obj.user_id.address_id and\
255                         obj.user_id.address_id.email:
256                 emailfrom = obj.user_id.address_id.email
257
258         name = '[%d] %s' % (obj.id, tools.ustr(obj.name))
259         emailfrom = tools.ustr(emailfrom)
260         reply_to = emailfrom
261         if not emailfrom:
262             raise osv.except_osv(_('Error!'), 
263                     _("No E-Mail ID Found for your Company address!"))
264         return tools.email_send(emailfrom, emails, name, body, reply_to=reply_to, openobject_id=str(obj.id))
265
266
267     def do_check(self, cr, uid, action, obj, context=None):
268         """ check Action
269             @param self: The object pointer
270             @param cr: the current row, from the database cursor,
271             @param uid: the current user’s ID for security checks,
272             @param context: A standard dictionary for contextual values """
273         if context is None:
274             context = {}
275         ok = True 
276         if action.filter_id:
277             if action.model_id.model == action.filter_id.model_id:
278                 context.update(eval(action.filter_id.context))
279                 obj_ids = obj._table.search(cr, uid, eval(action.filter_id.domain), context=context)
280                 if not obj.id in obj_ids:
281                     ok = False
282             else:
283                 ok = False
284         if getattr(obj, 'user_id', False):
285             ok = ok and (not action.trg_user_id.id or action.trg_user_id.id==obj.user_id.id)
286         if getattr(obj, 'partner_id', False):
287             ok = ok and (not action.trg_partner_id.id or action.trg_partner_id.id==obj.partner_id.id)
288             ok = ok and (
289                 not action.trg_partner_categ_id.id or
290                 (
291                     obj.partner_id.id and
292                     (action.trg_partner_categ_id.id in map(lambda x: x.id, obj.partner_id.category_id or []))
293                 )
294             )
295         state_to = context.get('state_to', False)
296         state = getattr(obj, 'state', False)
297         if state:
298             ok = ok and (not action.trg_state_from or action.trg_state_from==state)
299         if state_to:
300             ok = ok and (not action.trg_state_to or action.trg_state_to==state_to)
301         elif action.trg_state_to:
302             ok = False
303         reg_name = action.regex_name
304         result_name = True
305         if reg_name:
306             ptrn = re.compile(str(reg_name))
307             _result = ptrn.search(str(obj.name))
308             if not _result:
309                 result_name = False
310         regex_n = not reg_name or result_name
311         ok = ok and regex_n
312         return ok
313
314     def do_action(self, cr, uid, action, model_obj, obj, context=None):
315         """ Do Action
316             @param self: The object pointer
317             @param cr: the current row, from the database cursor,
318             @param uid: the current user’s ID for security checks,
319             @param action: pass action
320             @param model_obj: pass Model object
321             @param context: A standard dictionary for contextual values """
322         if context is None:
323             context = {}
324
325         if action.server_action_id:
326             context.update({'active_id':obj.id, 'active_ids':[obj.id]})
327             self.pool.get('ir.actions.server').run(cr, uid, [action.server_action_id.id], context)
328         write = {}
329
330         if hasattr(obj, 'user_id') and action.act_user_id:
331             obj.user_id = action.act_user_id
332             write['user_id'] = action.act_user_id.id
333         if hasattr(obj, 'date_action_last'):
334             write['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
335         if hasattr(obj, 'state') and action.act_state:
336             obj.state = action.act_state
337             write['state'] = action.act_state
338
339         if hasattr(obj, 'categ_id') and action.act_categ_id:
340             obj.categ_id = action.act_categ_id
341             write['categ_id'] = action.act_categ_id.id
342
343         model_obj.write(cr, uid, [obj.id], write, context)
344
345         if hasattr(model_obj, 'remind_user') and action.act_remind_user:
346             model_obj.remind_user(cr, uid, [obj.id], context, attach=action.act_remind_attach)
347         if hasattr(model_obj, 'remind_partner') and action.act_remind_partner:
348             model_obj.remind_partner(cr, uid, [obj.id], context, attach=action.act_remind_attach)
349         if action.act_method:
350             getattr(model_obj, 'act_method')(cr, uid, [obj.id], action, context)
351
352         emails = []
353         if hasattr(obj, 'user_id') and action.act_mail_to_user:
354             if obj.user_id and obj.user_id.address_id:
355                 emails.append(obj.user_id.address_id.email)
356
357         if action.act_mail_to_watchers:
358             emails += (action.act_email_cc or '').split(',')
359         if action.act_mail_to_email:
360             emails += (action.act_mail_to_email or '').split(',')
361
362         locals_for_emails = {
363             'user' : self.pool.get('res.users').browse(cr, uid, uid, context=context),
364             'obj' : obj,
365         }
366
367         if action.act_email_to:
368             emails.append(safe_eval(action.act_email_to, {}, locals_for_emails))
369
370         emails = filter(None, emails)
371         if len(emails) and action.act_mail_body:
372             emails = list(set(emails))
373             email_from = safe_eval(action.act_email_from, {}, locals_for_emails)
374
375             def to_email(text):
376                 return re.findall(r'([^ ,<@]+@[^> ,]+)', text or '')
377             emails = to_email(','.join(filter(None, emails)))
378             email_froms = to_email(email_from)
379             if email_froms:
380                 self.email_send(cr, uid, obj, emails, action.act_mail_body, emailfrom=email_froms[0])
381         return True
382
383     def _action(self, cr, uid, ids, objects, scrit=None, context=None):
384         """ Do Action
385             @param self: The object pointer
386             @param cr: the current row, from the database cursor,
387             @param uid: the current user’s ID for security checks,
388             @param ids: List of Basic Action Rule’s IDs,
389             @param objects: pass objects
390             @param context: A standard dictionary for contextual values """
391         if context is None:
392             context = {}
393
394
395         context.update({'action': True})
396         if not scrit:
397             scrit = []
398
399         for action in self.browse(cr, uid, ids, context=context):
400             model_obj = self.pool.get(action.model_id.model)
401             for obj in objects:
402                 ok = self.do_check(cr, uid, action, obj, context=context)
403                 if not ok:
404                     continue
405
406                 base = False
407                 if action.trg_date_type=='create' and hasattr(obj, 'create_date'):
408                     base = datetime.strptime(obj.create_date[:19], '%Y-%m-%d %H:%M:%S')
409                 elif action.trg_date_type=='action_last' and hasattr(obj, 'create_date'):
410                     if hasattr(obj, 'date_action_last') and obj.date_action_last:
411                         base = datetime.strptime(obj.date_action_last, '%Y-%m-%d %H:%M:%S')
412                     else:
413                         base = datetime.strptime(obj.create_date[:19], '%Y-%m-%d %H:%M:%S')
414                 elif action.trg_date_type=='deadline' and hasattr(obj, 'date_deadline') \
415                                 and obj.date_deadline:
416                     base = datetime.strptime(obj.date_deadline, '%Y-%m-%d %H:%M:%S')
417                 elif action.trg_date_type=='date' and hasattr(obj, 'date') and obj.date:
418                     base = datetime.strptime(obj.date, '%Y-%m-%d %H:%M:%S')
419                 if base:
420                     fnct = {
421                         'minutes': lambda interval: timedelta(minutes=interval), 
422                         'day': lambda interval: timedelta(days=interval), 
423                         'hour': lambda interval: timedelta(hours=interval), 
424                         'month': lambda interval: timedelta(months=interval), 
425                     }
426                     d = base + fnct[action.trg_date_range_type](action.trg_date_range)
427                     dt = d.strftime('%Y-%m-%d %H:%M:%S')
428                     ok = False
429                     if hasattr(obj, 'date_action_last') and hasattr(obj, 'date_action_next'):
430                         ok = (dt <= time.strftime('%Y-%m-%d %H:%M:%S')) and \
431                                 ((not obj.date_action_next) or \
432                                 (dt >= obj.date_action_next and \
433                                 obj.date_action_last < obj.date_action_next))
434                         if not ok:
435                             if not obj.date_action_next or dt < obj.date_action_next:
436                                 obj.date_action_next = dt
437                                 model_obj.write(cr, uid, [obj.id], {'date_action_next': dt}, context)
438                 else:
439                     ok = action.trg_date_type == 'none'
440
441                 if ok:
442                     self.do_action(cr, uid, action, model_obj, obj, context)
443                     break
444         context.update({'action': False})
445         return True
446
447     def _check_mail(self, cr, uid, ids, context=None):
448         """ Check Mail
449             @param self: The object pointer
450             @param cr: the current row, from the database cursor,
451             @param uid: the current user’s ID for security checks,
452             @param ids: List of Action Rule’s IDs
453             @param context: A standard dictionary for contextual values """
454
455         empty = orm.browse_null()
456         rule_obj = self.pool.get('base.action.rule')
457         for rule in self.browse(cr, uid, ids, context=context):
458             if rule.act_mail_body:
459                 try:
460                     rule_obj.format_mail(empty, rule.act_mail_body)
461                 except (ValueError, KeyError, TypeError):
462                     return False
463         return True
464
465     _constraints = [
466         (_check_mail, 'Error: The mail is not well formated', ['act_mail_body']), 
467     ]
468
469 base_action_rule()
470
471
472 class ir_cron(osv.osv):
473     _inherit = 'ir.cron' 
474     
475     def _poolJobs(self, db_name, check=False):
476         try:
477             db = pooler.get_db(db_name)
478         except:
479             return False
480         cr = db.cursor()
481         try:
482             next = datetime.now().strftime('%Y-%m-%d %H:00:00')
483             # Putting nextcall always less than current time in order to call it every time
484             cr.execute('UPDATE ir_cron set nextcall = \'%s\' where numbercall<>0 and active and model=\'base.action.rule\' ' % (next))
485         finally:
486             cr.commit()
487             cr.close()
488
489         super(ir_cron, self)._poolJobs(db_name, check=check)
490
491 ir_cron()
492
493
494 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: