1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (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 General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>
20 ##############################################################################
22 from openerp import SUPERUSER_ID
23 from openerp.osv import fields, osv
24 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
25 from openerp.tools.safe_eval import safe_eval
26 from openerp.tools.translate import _
30 from datetime import date, datetime, timedelta
32 _logger = logging.getLogger(__name__)
35 class gamification_goal_definition(osv.Model):
38 A goal definition contains the way to evaluate an objective
39 Each module wanting to be able to set goals to the users needs to create
40 a new gamification_goal_definition
42 _name = 'gamification.goal.definition'
43 _description = 'Gamification goal definition'
45 def _get_suffix(self, cr, uid, ids, field_name, arg, context=None):
46 res = dict.fromkeys(ids, '')
47 for goal in self.browse(cr, uid, ids, context=context):
48 if goal.suffix and not goal.monetary:
49 res[goal.id] = goal.suffix
51 # use the current user's company currency
52 user = self.pool.get('res.users').browse(cr, uid, uid, context)
54 res[goal.id] = "%s %s" % (user.company_id.currency_id.symbol, goal.suffix)
56 res[goal.id] = user.company_id.currency_id.symbol
62 'name': fields.char('Goal Definition', required=True, translate=True),
63 'description': fields.text('Goal Description'),
64 'monetary': fields.boolean('Monetary Value', help="The target and current value are defined in the company currency."),
65 'suffix': fields.char('Suffix', help="The unit of the target and current values", translate=True),
66 'full_suffix': fields.function(_get_suffix, type="char", string="Full Suffix", help="The currency and suffix field"),
67 'computation_mode': fields.selection([
68 ('manually', 'Recorded manually'),
69 ('count', 'Automatic: number of records'),
70 ('sum', 'Automatic: sum on a field'),
71 ('python', 'Automatic: execute a specific Python code'),
73 string="Computation Mode",
74 help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.",
76 'display_mode': fields.selection([
77 ('progress', 'Progressive (using numerical values)'),
78 ('boolean', 'Exclusive (done or not-done)'),
80 string="Displayed as", required=True),
81 'model_id': fields.many2one('ir.model',
83 help='The model object for the field to evaluate'),
84 'field_id': fields.many2one('ir.model.fields',
85 string='Field to Sum',
86 help='The field containing the value to evaluate'),
87 'field_date_id': fields.many2one('ir.model.fields',
89 help='The date to use for the time period evaluated'),
90 'domain': fields.char("Filter Domain",
91 help="Domain for filtering records. The rule can contain reference to 'user' that is a browse record of the current user, e.g. [('user_id', '=', user.id)].",
93 'compute_code': fields.text('Python Code',
94 help="Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id."),
95 'condition': fields.selection([
96 ('higher', 'The higher the better'),
97 ('lower', 'The lower the better')
99 string='Goal Performance',
100 help='A goal is considered as completed when the current value is compared to the value to reach',
102 'action_id': fields.many2one('ir.actions.act_window', string="Action",
103 help="The action that will be called to update the goal value."),
104 'res_id_field': fields.char("ID Field of user",
105 help="The field name on the user profile (res.users) containing the value for res_id for action.")
109 'condition': 'higher',
110 'computation_mode': 'manually',
113 'display_mode': 'progress',
116 def number_following(self, cr, uid, model_name="mail.thread", context=None):
117 """Return the number of 'model_name' objects the user is following
119 The model specified in 'model_name' must inherit from mail.thread
121 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
122 return self.pool.get('mail.followers').search(cr, uid, [('res_model', '=', model_name), ('partner_id', '=', user.partner_id.id)], count=True, context=context)
126 class gamification_goal(osv.Model):
127 """Goal instance for a user
129 An individual goal for a user on a specified time period"""
131 _name = 'gamification.goal'
132 _description = 'Gamification goal instance'
133 _inherit = 'mail.thread'
135 def _get_completion(self, cr, uid, ids, field_name, arg, context=None):
136 """Return the percentage of completeness of the goal, between 0 and 100"""
137 res = dict.fromkeys(ids, 0.0)
138 for goal in self.browse(cr, uid, ids, context=context):
139 if goal.definition_condition == 'higher':
140 if goal.current >= goal.target_goal:
143 res[goal.id] = round(100.0 * goal.current / goal.target_goal, 2)
144 elif goal.current < goal.target_goal:
145 # a goal 'lower than' has only two values possible: 0 or 100%
151 def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
152 goal_definition = self.pool.get('gamification.goal.definition')
153 if not definition_id:
154 return {'value': {'definition_id': False}}
155 goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
156 return {'value': {'computation_mode': goal_definition.computation_mode, 'definition_condition': goal_definition.condition}}
159 'definition_id': fields.many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade"),
160 'user_id': fields.many2one('res.users', string='User', required=True),
161 'line_id': fields.many2one('gamification.challenge.line', string='Goal Line', ondelete="cascade"),
162 'challenge_id': fields.related('line_id', 'challenge_id',
165 relation='gamification.challenge',
166 store=True, readonly=True,
167 help="Challenge that generated the goal, assign challenge to users to generate goals with a value in this field."),
168 'start_date': fields.date('Start Date'),
169 'end_date': fields.date('End Date'), # no start and end = always active
170 'target_goal': fields.float('To Reach',
172 track_visibility='always'), # no goal = global index
173 'current': fields.float('Current Value', required=True, track_visibility='always'),
174 'completeness': fields.function(_get_completion, type='float', string='Completeness'),
175 'state': fields.selection([
177 ('inprogress', 'In progress'),
178 ('inprogress_update', 'In progress (to update)'),
179 ('reached', 'Reached'),
180 ('failed', 'Failed'),
181 ('canceled', 'Canceled'),
185 track_visibility='always'),
187 'computation_mode': fields.related('definition_id', 'computation_mode', type='char', string="Computation mode"),
188 'remind_update_delay': fields.integer('Remind delay',
189 help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified."),
190 'last_update': fields.date('Last Update',
191 help="In case of manual goal, reminders are sent if the goal as not been updated for a while (defined in challenge). Ignored in case of non-manual goal or goal not linked to a challenge."),
193 'definition_description': fields.related('definition_id', 'description', type='char', string='Definition Description', readonly=True),
194 'definition_condition': fields.related('definition_id', 'condition', type='char', string='Definition Condition', readonly=True),
195 'definition_suffix': fields.related('definition_id', 'full_suffix', type="char", string="Suffix", readonly=True),
196 'definition_display': fields.related('definition_id', 'display_mode', type="char", string="Display Mode", readonly=True),
202 'start_date': fields.date.today,
204 _order = 'create_date desc, end_date desc, definition_id, id'
206 def _check_remind_delay(self, cr, uid, goal, context=None):
207 """Verify if a goal has not been updated for some time and send a
208 reminder message of needed.
210 :return: data to write on the goal object
212 if goal.remind_update_delay and goal.last_update:
213 delta_max = timedelta(days=goal.remind_update_delay)
214 last_update = datetime.strptime(goal.last_update, DF).date()
215 if date.today() - last_update > delta_max and goal.state == 'inprogress':
216 # generate a remind report
217 temp_obj = self.pool.get('email.template')
218 template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context)
219 body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context)
221 self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment')
222 return {'state': 'inprogress_update'}
225 def update(self, cr, uid, ids, context=None):
226 """Update the goals to recomputes values and change of states
228 If a manual goal is not updated for enough time, the user will be
229 reminded to do so (done only once, in 'inprogress' state).
230 If a goal reaches the target value, the status is set to reached
231 If the end date is passed (at least +1 day, time not considered) without
232 the target value being reached, the goal is set as failed."""
236 for goal in self.browse(cr, uid, ids, context=context):
238 if goal.state in ('draft', 'canceled'):
239 # skip if goal draft or canceled
242 if goal.definition_id.computation_mode == 'manually':
243 towrite.update(self._check_remind_delay(cr, uid, goal, context))
245 elif goal.definition_id.computation_mode == 'python':
246 # execute the chosen method
248 'self': self.pool.get('gamification.goal'),
252 'context': dict(context), # copy context to prevent side-effects of eval
255 'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time
257 code = goal.definition_id.compute_code.strip()
258 safe_eval(code, cxt, mode="exec", nocopy=True)
259 # the result of the evaluated codeis put in the 'result' local variable, propagated to the context
260 result = cxt.get('result', False)
261 if result and type(result) in (float, int, long):
262 if result != goal.current:
263 towrite['current'] = result
265 _logger.exception(_('Invalid return content from the evaluation of %s' % code))
268 obj = self.pool.get(goal.definition_id.model_id.model)
269 field_date_name = goal.definition_id.field_date_id.name
271 # eval the domain with user replaced by goal user object
272 domain = safe_eval(goal.definition_id.domain, {'user': goal.user_id})
274 # add temporal clause(s) to the domain if fields are filled on the goal
275 if goal.start_date and field_date_name:
276 domain.append((field_date_name, '>=', goal.start_date))
277 if goal.end_date and field_date_name:
278 domain.append((field_date_name, '<=', goal.end_date))
280 if goal.definition_id.computation_mode == 'sum':
281 field_name = goal.definition_id.field_id.name
282 res = obj.read_group(cr, uid, domain, [field_name], [''], context=context)
283 new_value = res and res[0][field_name] or 0.0
285 else: # computation mode = count
286 new_value = obj.search(cr, uid, domain, context=context, count=True)
288 # avoid useless write if the new value is the same as the old one
289 if new_value != goal.current:
290 towrite['current'] = new_value
292 # check goal target reached
293 if (goal.definition_condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.definition_condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal):
294 towrite['state'] = 'reached'
297 elif goal.end_date and fields.date.today() > goal.end_date:
298 towrite['state'] = 'failed'
300 self.write(cr, uid, [goal.id], towrite, context=context)
303 def action_start(self, cr, uid, ids, context=None):
304 """Mark a goal as started.
306 This should only be used when creating goals manually (in draft state)"""
307 self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
308 return self.update(cr, uid, ids, context=context)
310 def action_reach(self, cr, uid, ids, context=None):
311 """Mark a goal as reached.
313 If the target goal condition is not met, the state will be reset to In
314 Progress at the next goal update until the end date."""
315 return self.write(cr, uid, ids, {'state': 'reached'}, context=context)
317 def action_fail(self, cr, uid, ids, context=None):
318 """Set the state of the goal to failed.
320 A failed goal will be ignored in future checks."""
321 return self.write(cr, uid, ids, {'state': 'failed'}, context=context)
323 def action_cancel(self, cr, uid, ids, context=None):
324 """Reset the completion after setting a goal as reached or failed.
326 This is only the current state, if the date and/or target criterias
327 match the conditions for a change of state, this will be applied at the
329 return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
331 def create(self, cr, uid, vals, context=None):
332 """Overwrite the create method to add a 'no_remind_goal' field to True"""
335 context['no_remind_goal'] = True
336 return super(gamification_goal, self).create(cr, uid, vals, context=context)
338 def write(self, cr, uid, ids, vals, context=None):
339 """Overwrite the write method to update the last_update field to today
341 If the current value is changed and the report frequency is set to On
342 change, a report is generated
346 vals['last_update'] = fields.date.today()
347 result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context)
348 for goal in self.browse(cr, uid, ids, context=context):
349 if goal.state != "draft" and ('definition_id' in vals or 'user_id' in vals):
350 # avoid drag&drop in kanban view
351 raise osv.except_osv(_('Error!'), _('Can not modify the configuration of a started goal'))
353 if vals.get('current'):
354 if 'no_remind_goal' in context:
355 # new goals should not be reported
358 if goal.challenge_id and goal.challenge_id.report_message_frequency == 'onchange':
359 self.pool.get('gamification.challenge').report_progress(cr, SUPERUSER_ID, goal.challenge_id, users=[goal.user_id], context=context)
362 def get_action(self, cr, uid, goal_id, context=None):
363 """Get the ir.action related to update the goal
365 In case of a manual goal, should return a wizard to update the value
366 :return: action description in a dictionnary
368 goal = self.browse(cr, uid, goal_id, context=context)
370 if goal.definition_id.action_id:
371 # open a the action linked to the goal
372 action = goal.definition_id.action_id.read()[0]
374 if goal.definition_id.res_id_field:
375 current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
376 action['res_id'] = safe_eval(goal.definition_id.res_id_field, {'user': current_user})
378 # if one element to display, should see it in form mode if possible
379 action['views'] = [(view_id, mode) for (view_id, mode) in action['views'] if mode == 'form'] or action['views']
382 if goal.computation_mode == 'manually':
383 # open a wizard window to update the value manually
385 'name': _("Update %s") % goal.definition_id.name,
387 'type': 'ir.actions.act_window',
388 'views': [[False, 'form']],
390 'context': {'default_goal_id': goal_id, 'default_current': goal.current},
391 'res_model': 'gamification.goal.wizard'