[FIX] Schedule jobs even if their next time has passed.
[odoo/odoo.git] / bin / addons / base / ir / ir_cron.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
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 from mx import DateTime
24 import time
25 import netsvc
26 import tools
27 import pooler
28 from osv import fields,osv
29 from tools.safe_eval import safe_eval as eval
30
31 def str2tuple(s):
32     return eval('tuple(%s)' % (s or ''))
33
34 _intervalTypes = {
35     'work_days': lambda interval: DateTime.RelativeDateTime(days=interval),
36     'days': lambda interval: DateTime.RelativeDateTime(days=interval),
37     'hours': lambda interval: DateTime.RelativeDateTime(hours=interval),
38     'weeks': lambda interval: DateTime.RelativeDateTime(days=7*interval),
39     'months': lambda interval: DateTime.RelativeDateTime(months=interval),
40     'minutes': lambda interval: DateTime.RelativeDateTime(minutes=interval),
41 }
42
43 class ir_cron(osv.osv, netsvc.Agent):
44     _name = "ir.cron"
45     _columns = {
46         'name': fields.char('Name', size=60, required=True),
47         'user_id': fields.many2one('res.users', 'User', required=True),
48         'active': fields.boolean('Active'),
49         'interval_number': fields.integer('Interval Number'),
50         'interval_type': fields.selection( [('minutes', 'Minutes'),
51             ('hours', 'Hours'), ('work_days','Work Days'), ('days', 'Days'),('weeks', 'Weeks'), ('months', 'Months')], 'Interval Unit'),
52         'numbercall': fields.integer('Number of Calls', help='Number of time the function is called,\na negative number indicates that the function will always be called'),
53         'doall' : fields.boolean('Repeat Missed'),
54         'nextcall' : fields.datetime('Next Call Date', required=True),
55         'model': fields.char('Object', size=64),
56         'function': fields.char('Function', size=64),
57         'args': fields.text('Arguments'),
58         'priority': fields.integer('Priority', help='0=Very Urgent\n10=Not urgent')
59     }
60
61     _defaults = {
62         'nextcall' : lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
63         'priority' : lambda *a: 5,
64         'user_id' : lambda obj,cr,uid,context: uid,
65         'interval_number' : lambda *a: 1,
66         'interval_type' : lambda *a: 'months',
67         'numbercall' : lambda *a: 1,
68         'active' : lambda *a: 1,
69         'doall' : lambda *a: 1
70     }
71
72     def _check_args(self, cr, uid, ids, context=None):
73         try:
74             for this in self.browse(cr, uid, ids, context):
75                 str2tuple(this.args)
76         except:
77             return False
78         return True
79
80     _constraints= [
81         (_check_args, 'Invalid arguments', ['args']),
82     ]
83
84     def _callback(self, cr, uid, model, func, args):
85         args = str2tuple(args)
86         m = self.pool.get(model)
87         if m and hasattr(m, func):
88             f = getattr(m, func)
89             try:
90                 f(cr, uid, *args)
91             except Exception, e:
92                 self._logger.notifyChannel('timers', netsvc.LOG_ERROR, "Job call of self.pool.get('%s').%s(cr, uid, *%r) failed" % (model, func, args))
93                 self._logger.notifyChannel('timers', netsvc.LOG_ERROR, tools.exception_to_unicode(e))
94
95
96     def _poolJobs(self, db_name, check=False):
97         try:
98             db, pool = pooler.get_db_and_pool(db_name)
99         except:
100             return False
101         cr = db.cursor()
102         try:
103             if not pool._init:
104                 now = DateTime.now()
105                 cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority')
106                 for job in cr.dictfetchall():
107                     nextcall = DateTime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S')
108                     numbercall = job['numbercall']
109
110                     ok = False
111                     while nextcall < now and numbercall:
112                         if numbercall > 0:
113                             numbercall -= 1
114                         if not ok or job['doall']:
115                             self._callback(cr, job['user_id'], job['model'], job['function'], job['args'])
116                         if numbercall:
117                             nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
118                         ok = True
119                     addsql=''
120                     if not numbercall:
121                         addsql = ', active=False'
122                     cr.execute("update ir_cron set nextcall=%s, numbercall=%s"+addsql+" where id=%s", (nextcall.strftime('%Y-%m-%d %H:%M:%S'), numbercall, job['id']))
123                     cr.commit()
124
125
126             cr.execute('select min(nextcall) as min_next_call from ir_cron where numbercall<>0 and active')
127             next_call = cr.dictfetchone()['min_next_call']
128             if next_call:
129                 next_call = time.mktime(time.strptime(next_call, '%Y-%m-%d %H:%M:%S'))
130             else:
131                 next_call = int(time.time()) + 3600   # if do not find active cron job from database, it will run again after 1 day
132
133             if not check:
134                 self.setAlarm(self._poolJobs, next_call, db_name, db_name)
135
136         finally:
137             cr.commit()
138             cr.close()
139
140     def restart(self, dbname):
141         self.cancel(dbname)
142         self._poolJobs(dbname)
143
144     def create(self, cr, uid, vals, context=None):
145         res = super(ir_cron, self).create(cr, uid, vals, context=context)
146         cr.commit()
147         self.restart(cr.dbname)
148         return res
149
150     def write(self, cr, user, ids, vals, context=None):
151         res = super(ir_cron, self).write(cr, user, ids, vals, context=context)
152         cr.commit()
153         self.restart(cr.dbname)
154         return res
155
156     def unlink(self, cr, uid, ids, context=None):
157         res = super(ir_cron, self).unlink(cr, uid, ids, context=context)
158         cr.commit()
159         self.restart(cr.dbname)
160         return res
161 ir_cron()
162
163 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
164