1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from datetime import datetime, timedelta
27 from openerp import SUPERUSER_ID
28 from openerp.osv import fields, osv
29 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
31 _logger = logging.getLogger(__name__)
33 DATE_RANGE_FUNCTION = {
34 'minutes': lambda interval: timedelta(minutes=interval),
35 'hour': lambda interval: timedelta(hours=interval),
36 'day': lambda interval: timedelta(days=interval),
37 'month': lambda interval: timedelta(months=interval),
38 False: lambda interval: timedelta(0),
41 def get_datetime(date_str):
42 '''Return a datetime from a date string or a datetime string'''
43 # complete date time if date_str contains only a date
44 if ' ' not in date_str:
45 date_str = date_str + " 00:00:00"
46 return datetime.strptime(date_str, DEFAULT_SERVER_DATETIME_FORMAT)
49 class base_action_rule(osv.osv):
50 """ Base Action Rules """
52 _name = 'base.action.rule'
53 _description = 'Action Rules'
57 'name': fields.char('Rule Name', size=64, required=True),
58 'model_id': fields.many2one('ir.model', 'Related Document Model',
59 required=True, domain=[('osv_memory', '=', False)]),
60 'model': fields.related('model_id', 'model', type="char", size=256, string='Model'),
61 'create_date': fields.datetime('Create Date', readonly=1),
62 'active': fields.boolean('Active',
63 help="When unchecked, the rule is hidden and will not be executed."),
64 'sequence': fields.integer('Sequence',
65 help="Gives the sequence order when displaying a list of rules."),
66 'kind': fields.selection(
67 [('on_create', 'On Creation'),
68 ('on_write', 'On Update'),
69 ('on_create_or_write', 'On Creation & Update'),
70 ('on_time', 'Based on Timed Condition')],
71 string='When to Run'),
72 'trg_date_id': fields.many2one('ir.model.fields', string='Trigger Date',
73 help="When should the condition be triggered. If present, will be checked by the scheduler. If empty, will be checked at creation and update.",
74 domain="[('model_id', '=', model_id), ('ttype', 'in', ('date', 'datetime'))]"),
75 'trg_date_range': fields.integer('Delay after trigger date',
76 help="Delay after the trigger date." \
77 "You can put a negative number if you need a delay before the" \
78 "trigger date, like sending a reminder 15 minutes before a meeting."),
79 'trg_date_range_type': fields.selection([('minutes', 'Minutes'), ('hour', 'Hours'),
80 ('day', 'Days'), ('month', 'Months')], 'Delay type'),
81 'trg_date_calendar_id': fields.many2one(
82 'resource.calendar', 'Use Calendar',
83 help='When calculating a day-based timed condition, it is possible to use a calendar to compute the date based on working days.',
86 'act_user_id': fields.many2one('res.users', 'Set Responsible'),
87 'act_followers': fields.many2many("res.partner", string="Add Followers"),
88 'server_action_ids': fields.many2many('ir.actions.server', string='Server Actions',
89 domain="[('model_id', '=', model_id)]",
90 help="Examples: email reminders, call object service, etc."),
91 'filter_pre_id': fields.many2one('ir.filters', string='Before Update Filter',
93 domain="[('model_id', '=', model_id.model)]",
94 help="If present, this condition must be satisfied before the update of the record."),
95 'filter_id': fields.many2one('ir.filters', string='Filter',
97 domain="[('model_id', '=', model_id.model)]",
98 help="If present, this condition must be satisfied before executing the action rule."),
99 'last_run': fields.datetime('Last Run', readonly=1),
104 'trg_date_range_type': 'day',
107 def onchange_kind(self, cr, uid, ids, kind, context=None):
109 if kind in ['on_create', 'on_create_or_write']:
110 clear_fields = ['filter_pre_id', 'trg_date_id', 'trg_date_range', 'trg_date_range_type']
111 elif kind in ['on_write', 'on_create_or_write']:
112 clear_fields = ['trg_date_id', 'trg_date_range', 'trg_date_range_type']
113 elif kind == 'on_time':
114 clear_fields = ['filter_pre_id']
115 return {'value': dict.fromkeys(clear_fields, False)}
117 def _filter(self, cr, uid, action, action_filter, record_ids, context=None):
118 """ filter the list record_ids that satisfy the action filter """
119 if record_ids and action_filter:
120 assert action.model == action_filter.model_id, "Filter model different from action rule model"
121 model = self.pool[action_filter.model_id]
122 domain = [('id', 'in', record_ids)] + eval(action_filter.domain)
123 ctx = dict(context or {})
124 ctx.update(eval(action_filter.context))
125 record_ids = model.search(cr, uid, domain, context=ctx)
128 def _process(self, cr, uid, action, record_ids, context=None):
129 """ process the given action on the records """
130 model = self.pool[action.model_id.model]
134 if 'date_action_last' in model._all_columns:
135 values['date_action_last'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
136 if action.act_user_id and 'user_id' in model._all_columns:
137 values['user_id'] = action.act_user_id.id
139 model.write(cr, uid, record_ids, values, context=context)
141 if action.act_followers and hasattr(model, 'message_subscribe'):
142 follower_ids = map(int, action.act_followers)
143 model.message_subscribe(cr, uid, record_ids, follower_ids, context=context)
145 # execute server actions
146 if action.server_action_ids:
147 server_action_ids = map(int, action.server_action_ids)
148 for record in model.browse(cr, uid, record_ids, context):
149 action_server_obj = self.pool.get('ir.actions.server')
150 ctx = dict(context, active_model=model._name, active_ids=[record.id], active_id=record.id)
151 action_server_obj.run(cr, uid, server_action_ids, context=ctx)
155 def _wrap_create(self, old_create, model):
156 """ Return a wrapper around `old_create` calling both `old_create` and
157 `_process`, in that order.
159 def create(cr, uid, vals, context=None, **kwargs):
160 # avoid loops or cascading actions
161 if context and context.get('action'):
162 return old_create(cr, uid, vals, context=context)
164 context = dict(context or {}, action=True)
165 new_id = old_create(cr, uid, vals, context=context, **kwargs)
167 # retrieve the action rules to run on creation
168 action_dom = [('model', '=', model), ('kind', 'in', ['on_create', 'on_create_or_write'])]
169 action_ids = self.search(cr, uid, action_dom, context=context)
171 # check postconditions, and execute actions on the records that satisfy them
172 for action in self.browse(cr, uid, action_ids, context=context):
173 if self._filter(cr, uid, action, action.filter_id, [new_id], context=context):
174 self._process(cr, uid, action, [new_id], context=context)
179 def _wrap_write(self, old_write, model):
180 """ Return a wrapper around `old_write` calling both `old_write` and
181 `_process`, in that order.
183 def write(cr, uid, ids, vals, context=None, **kwargs):
184 # avoid loops or cascading actions
185 if context and context.get('action'):
186 return old_write(cr, uid, ids, vals, context=context, **kwargs)
188 context = dict(context or {}, action=True)
189 ids = [ids] if isinstance(ids, (int, long, str)) else ids
191 # retrieve the action rules to run on update
192 action_dom = [('model', '=', model), ('kind', 'in', ['on_write', 'on_create_or_write'])]
193 action_ids = self.search(cr, uid, action_dom, context=context)
194 actions = self.browse(cr, uid, action_ids, context=context)
196 # check preconditions
198 for action in actions:
199 pre_ids[action] = self._filter(cr, uid, action, action.filter_pre_id, ids, context=context)
202 old_write(cr, uid, ids, vals, context=context, **kwargs)
204 # check postconditions, and execute actions on the records that satisfy them
205 for action in actions:
206 post_ids = self._filter(cr, uid, action, action.filter_id, pre_ids[action], context=context)
208 self._process(cr, uid, action, post_ids, context=context)
213 def _register_hook(self, cr, ids=None):
214 """ Wrap the methods `create` and `write` of the models specified by
215 the rules given by `ids` (or all existing rules if `ids` is `None`.)
219 ids = self.search(cr, SUPERUSER_ID, [])
220 for action_rule in self.browse(cr, SUPERUSER_ID, ids):
221 model = action_rule.model_id.model
222 model_obj = self.pool.get(model)
223 if model_obj and not hasattr(model_obj, 'base_action_ruled'):
224 model_obj.create = self._wrap_create(model_obj.create, model)
225 model_obj.write = self._wrap_write(model_obj.write, model)
226 model_obj.base_action_ruled = True
230 def _update_cron(self, cr, uid, context=None):
232 cron = self.pool['ir.model.data'].get_object(
233 cr, uid, 'base_action_rule', 'ir_cron_crm_action', context=context)
237 return cron.toggle(model=self._name, domain=[('kind', '=', 'on_time')])
239 def create(self, cr, uid, vals, context=None):
240 res_id = super(base_action_rule, self).create(cr, uid, vals, context=context)
241 if self._register_hook(cr, [res_id]):
242 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
243 self._update_cron(cr, uid, context=context)
246 def write(self, cr, uid, ids, vals, context=None):
247 if isinstance(ids, (int, long)):
249 super(base_action_rule, self).write(cr, uid, ids, vals, context=context)
250 if self._register_hook(cr, ids):
251 openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
252 self._update_cron(cr, uid, context=context)
255 def unlink(self, cr, uid, ids, context=None):
256 res = super(base_action_rule, self).unlink(cr, uid, ids, context=context)
257 self._update_cron(cr, uid, context=context)
260 def onchange_model_id(self, cr, uid, ids, model_id, context=None):
261 data = {'model': False, 'filter_pre_id': False, 'filter_id': False}
263 model = self.pool.get('ir.model').browse(cr, uid, model_id, context=context)
264 data.update({'model': model.model})
265 return {'value': data}
267 def _check_delay(self, cr, uid, action, record, record_dt, context=None):
268 if action.trg_date_calendar_id and action.trg_date_range_type == 'day':
269 start_dt = get_datetime(record_dt)
270 action_dt = self.pool['resource.calendar'].schedule_days_get_date(
271 cr, uid, action.trg_date_calendar_id.id, action.trg_date_range,
272 day_date=start_dt, compute_leaves=True, context=context
275 delay = DATE_RANGE_FUNCTION[action.trg_date_range_type](action.trg_date_range)
276 action_dt = get_datetime(record_dt) + delay
279 def _check(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
280 """ This Function is called by scheduler. """
281 context = context or {}
282 # retrieve all the action rules to run based on a timed condition
283 action_dom = [('kind', '=', 'on_time')]
284 action_ids = self.search(cr, uid, action_dom, context=context)
285 for action in self.browse(cr, uid, action_ids, context=context):
288 last_run = get_datetime(action.last_run)
290 last_run = datetime.utcfromtimestamp(0)
292 # retrieve all the records that satisfy the action's condition
293 model = self.pool[action.model_id.model]
297 domain = eval(action.filter_id.domain)
298 ctx.update(eval(action.filter_id.context))
299 if 'lang' not in ctx:
300 # Filters might be language-sensitive, attempt to reuse creator lang
301 # as we are usually running this as super-user in background
302 [filter_meta] = action.filter_id.perm_read()
303 user_id = filter_meta['write_uid'] and filter_meta['write_uid'][0] or \
304 filter_meta['create_uid'][0]
305 ctx['lang'] = self.pool['res.users'].browse(cr, uid, user_id).lang
306 record_ids = model.search(cr, uid, domain, context=ctx)
308 # determine when action should occur for the records
309 date_field = action.trg_date_id.name
310 if date_field == 'date_action_last' and 'create_date' in model._all_columns:
311 get_record_dt = lambda record: record[date_field] or record.create_date
313 get_record_dt = lambda record: record[date_field]
315 # process action on the records that should be executed
316 for record in model.browse(cr, uid, record_ids, context=context):
317 record_dt = get_record_dt(record)
320 action_dt = self._check_delay(cr, uid, action, record, record_dt, context=context)
321 if last_run <= action_dt < now:
323 context = dict(context or {}, action=True)
324 self._process(cr, uid, action, [record.id], context=context)
327 _logger.error(traceback.format_exc())
329 action.write({'last_run': now.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
332 # auto-commit for batch processing