[MIG] gamification: Migrate to new API

Includes compatibility-change to website_forum: a selection(callable)
converted to a Selection(list) since that can easily be extended in the
new API.
This commit is contained in:
Xavier Morel
2016-08-05 10:18:02 +02:00
committed by Yannick Tivisse
parent c7a818caee
commit 0c649644db
20 changed files with 1596 additions and 1900 deletions
-2
View File
@@ -33,6 +33,4 @@ Both goals and badges are flexibles and can be adapted to a large range of modul
'data/badge.xml',
'views/gamification.xml',
],
'application': False,
'auto_install': False,
}
+2 -3
View File
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<record id="badge_good_job" model="gamification.badge">
<field name="name">Good Job</field>
@@ -58,4 +57,4 @@
]]></field>
</record>
</data>
</openerp>
</odoo>
+2 -3
View File
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<odoo>
<data noupdate="1">
<record forcecreate="True" id="ir_cron_check_challenge"
model="ir.cron">
@@ -13,4 +12,4 @@
<field name="args">()</field>
</record>
</data>
</openerp>
</odoo>
+4 -5
View File
@@ -1,5 +1,4 @@
<?xml version="1.0"?>
<openerp>
<odoo>
<!-- Mail template is done in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
@@ -163,7 +162,7 @@
<field name="period">once</field>
<field name="visibility_mode">personal</field>
<field name="report_message_frequency">never</field>
<field name="user_domain" eval="[('groups_id', '=', ref('base.group_user'))]" />
<field name="user_domain" eval="str([('groups_id', '=', ref('base.group_user'))])" />
<field name="state">inprogress</field>
<field name="category">other</field>
</record>
@@ -173,7 +172,7 @@
<field name="period">once</field>
<field name="visibility_mode">personal</field>
<field name="report_message_frequency">never</field>
<field name="user_domain" eval="[('groups_id', '=', ref('base.group_erp_manager'))]" />
<field name="user_domain" eval="str([('groups_id', '=', ref('base.group_erp_manager'))])" />
<field name="state">inprogress</field>
<field name="category">other</field>
</record>
@@ -202,4 +201,4 @@
</record>
</data>
</openerp>
</odoo>
+154 -183
View File
@@ -1,18 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
from openerp.tools.translate import _
from datetime import date
import logging
from openerp.exceptions import UserError
from datetime import date
from odoo import api, fields, models, _, exceptions
_logger = logging.getLogger(__name__)
class gamification_badge_user(osv.Model):
class BadgeUser(models.Model):
"""User having received a badge"""
_name = 'gamification.badge.user'
@@ -20,19 +16,16 @@ class gamification_badge_user(osv.Model):
_order = "create_date desc"
_rec_name = "badge_name"
_columns = {
'user_id': fields.many2one('res.users', string="User", required=True, ondelete="cascade", select=True),
'sender_id': fields.many2one('res.users', string="Sender", help="The user who has send the badge"),
'badge_id': fields.many2one('gamification.badge', string='Badge', required=True, ondelete="cascade", select=True),
'challenge_id': fields.many2one('gamification.challenge', string='Challenge originating', help="If this badge was rewarded through a challenge"),
'comment': fields.text('Comment'),
'badge_name': fields.related('badge_id', 'name', type="char", string="Badge Name"),
'create_date': fields.datetime('Created', readonly=True),
'create_uid': fields.many2one('res.users', string='Creator', readonly=True),
}
user_id = fields.Many2one('res.users', string="User", required=True, ondelete="cascade", select=True)
sender_id = fields.Many2one('res.users', string="Sender", help="The user who has send the badge")
badge_id = fields.Many2one('gamification.badge', string='Badge', required=True, ondelete="cascade", select=True)
challenge_id = fields.Many2one('gamification.challenge', string='Challenge originating', help="If this badge was rewarded through a challenge")
comment = fields.Text('Comment')
badge_name = fields.Char(related='badge_id.name', string="Badge Name")
create_date = fields.Datetime('Created', readonly=True)
create_uid = fields.Many2one('res.users', string='Creator', readonly=True)
def _send_badge(self, cr, uid, ids, context=None):
def _send_badge(self):
"""Send a notification to a user for receiving a badge
Does not verify constrains on badge granting.
@@ -40,27 +33,29 @@ class gamification_badge_user(osv.Model):
The stats counters are incremented
:param ids: list(int) of badge users that will receive the badge
"""
res = True
temp_obj = self.pool.get('mail.template')
user_obj = self.pool.get('res.users')
template_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'gamification', 'email_template_badge_received')[1]
for badge_user in self.browse(cr, uid, ids, context=context):
template = temp_obj.get_email_template(cr, uid, template_id, badge_user.id, context=context)
body_html = temp_obj.render_template(cr, uid, template.body_html, 'gamification.badge.user', badge_user.id, context=template._context)
res = user_obj.message_post(
cr, uid, badge_user.user_id.id,
Template = self.env['mail.template']
template_id = self.env.ref('gamification.email_template_badge_received')
for badge_user in self:
# .ids would trigger the "multi mode" which returns a mapping of
# res_id to templates
template = template_id.get_email_template(badge_user.id)
body_html = Template.with_context(template._context).render_template(template.body_html, 'gamification.badge.user', badge_user.ids)
badge_user.user_id.message_post(
body=body_html,
subtype='gamification.mt_badge_granted',
partner_ids=[badge_user.user_id.partner_id.id],
context=context)
return res
partner_ids=badge_user.user_id.partner_id.ids
)
def create(self, cr, uid, vals, context=None):
self.pool.get('gamification.badge').check_granting(cr, uid, badge_id=vals.get('badge_id'), context=context)
return super(gamification_badge_user, self).create(cr, uid, vals, context=context)
return True
@api.model
def create(self, vals):
self.env['gamification.badge'].browse(vals['badge_id']).check_granting()
return super(BadgeUser, self).create(vals)
class gamification_badge(osv.Model):
class GamificationBadge(models.Model):
"""Badge object that users can send and receive"""
CAN_GRANT = 1
@@ -73,204 +68,180 @@ class gamification_badge(osv.Model):
_description = 'Gamification badge'
_inherit = ['mail.thread']
def _get_owners_info(self, cr, uid, ids, name, args, context=None):
name = fields.Char('Badge', required=True, translate=True)
active = fields.Boolean('Active', default=True)
description = fields.Text('Description', translate=True)
image = fields.Binary("Image", attachment=True, help="This field holds the image used for the badge, limited to 256x256")
rule_auth = fields.Selection([
('everyone', 'Everyone'),
('users', 'A selected list of users'),
('having', 'People having some badges'),
('nobody', 'No one, assigned through challenges'),
], default='everyone',
string="Allowance to Grant", help="Who can grant this badge", required=True)
rule_auth_user_ids = fields.Many2many(
'res.users', 'rel_badge_auth_users',
string='Authorized Users',
help="Only these people can give this badge")
rule_auth_badge_ids = fields.Many2many(
'gamification.badge', 'gamification_badge_rule_badge_rel', 'badge1_id', 'badge2_id',
string='Required Badges',
help="Only the people having these badges can give this badge")
rule_max = fields.Boolean('Monthly Limited Sending', help="Check to set a monthly limit per person of sending this badge")
rule_max_number = fields.Integer('Limitation Number', help="The maximum number of time this badge can be sent per month per person.")
challenge_ids = fields.One2many('gamification.challenge', 'reward_id', string="Reward of Challenges")
goal_definition_ids = fields.Many2many(
'gamification.goal.definition', 'badge_unlocked_definition_rel',
string='Rewarded by', help="The users that have succeeded theses goals will receive automatically the badge.")
owner_ids = fields.One2many(
'gamification.badge.user', 'badge_id',
string='Owners', help='The list of instances of this badge granted to users')
stat_count = fields.Integer("Total", compute='_get_owners_info', help="The number of time this badge has been received.")
stat_count_distinct = fields.Integer("Number of users", compute='_get_owners_info', help="The number of time this badge has been received by unique users.")
unique_owner_ids = fields.Many2many(
'res.users', string="Unique Owners", compute='_get_owners_info',
help="The list of unique users having received this badge.")
stat_this_month = fields.Integer(
"Monthly total", compute='_get_badge_user_stats',
help="The number of time this badge has been received this month.")
stat_my = fields.Integer(
"My Total", compute='_get_badge_user_stats',
help="The number of time the current user has received this badge.")
stat_my_this_month = fields.Integer(
"My Monthly Total", compute='_get_badge_user_stats',
help="The number of time the current user has received this badge this month.")
stat_my_monthly_sending = fields.Integer(
'My Monthly Sending Total',
compute='_get_badge_user_stats',
help="The number of time the current user has sent this badge this month.")
remaining_sending = fields.Integer(
"Remaining Sending Allowed", compute='_remaining_sending_calc',
help="If a maximum is set")
@api.depends('owner_ids')
def _get_owners_info(self):
"""Return:
the list of unique res.users ids having received this badge
the total number of time this badge was granted
the total number of users this badge was granted to
"""
result = dict((res_id, {'stat_count': 0, 'stat_count_distinct': 0, 'unique_owner_ids': []}) for res_id in ids)
cr.execute("""
self.env.cr.execute("""
SELECT badge_id, count(user_id) as stat_count,
count(distinct(user_id)) as stat_count_distinct,
array_agg(distinct(user_id)) as unique_owner_ids
FROM gamification_badge_user
WHERE badge_id in %s
GROUP BY badge_id
""", (tuple(ids),))
for (badge_id, stat_count, stat_count_distinct, unique_owner_ids) in cr.fetchall():
result[badge_id] = {
'stat_count': stat_count,
'stat_count_distinct': stat_count_distinct,
'unique_owner_ids': unique_owner_ids,
}
return result
""", [tuple(self.ids)])
def _get_badge_user_stats(self, cr, uid, ids, name, args, context=None):
defaults = {
'stat_count': 0,
'stat_count_distinct': 0,
'unique_owner_ids': [],
}
mapping = {
badge_id: {
'stat_count': count,
'stat_count_distinct': distinct_count,
'unique_owner_ids': owner_ids,
}
for (badge_id, count, distinct_count, owner_ids) in self.env.cr._obj
}
for badge in self:
badge.update(mapping.get(badge.id, defaults))
@api.depends('owner_ids.badge_id', 'owner_ids.create_date', 'owner_ids.user_id')
def _get_badge_user_stats(self):
"""Return stats related to badge users"""
result = dict.fromkeys(ids, False)
badge_user_obj = self.pool.get('gamification.badge.user')
first_month_day = date.today().replace(day=1).strftime(DF)
for bid in ids:
result[bid] = {
'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid)], context=context, count=True),
'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_date', '>=', first_month_day)], context=context, count=True),
'stat_my_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True),
'stat_my_monthly_sending': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_uid', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True)
}
return result
first_month_day = fields.Date.to_string(date.today().replace(day=1))
def _remaining_sending_calc(self, cr, uid, ids, name, args, context=None):
for badge in self:
owners = badge.owner_ids
badge.stats_my = sum(o.user_id == self.env.user for o in owners)
badge.stats_this_month = sum(o.create_date >= first_month_day for o in owners)
badge.stats_my_this_month = sum(
o.user_id == self.env.user and o.create_date >= first_month_day
for o in owners
)
badge.stats_my_monthly_sending = sum(
o.create_uid == self.env.user and o.create_date >= first_month_day
for o in owners
)
@api.depends(
'rule_auth',
'rule_auth_user_ids',
'rule_auth_badge_ids',
'rule_max',
'rule_max_number',
'stat_my_monthly_sending',
)
def _remaining_sending_calc(self):
"""Computes the number of badges remaining the user can send
0 if not allowed or no remaining
integer if limited sending
-1 if infinite (should not be displayed)
"""
result = dict.fromkeys(ids, False)
for badge in self.browse(cr, uid, ids, context=context):
if self._can_grant_badge(cr, uid, badge.id, context) != 1:
for badge in self:
if badge._can_grant_badge() != self.CAN_GRANT:
# if the user cannot grant this badge at all, result is 0
result[badge.id] = 0
badge.remaining_sending = 0
elif not badge.rule_max:
# if there is no limitation, -1 is returned which means 'infinite'
result[badge.id] = -1
badge.remaining_sending = -1
else:
result[badge.id] = badge.rule_max_number - badge.stat_my_monthly_sending
return result
badge.remaining_sending = badge.rule_max_number - badge.stat_my_monthly_sending
_columns = {
'name': fields.char('Badge', required=True, translate=True),
'description': fields.text('Description', translate=True),
'image': fields.binary("Image", attachment=True,
help="This field holds the image used for the badge, limited to 256x256"),
'rule_auth': fields.selection([
('everyone', 'Everyone'),
('users', 'A selected list of users'),
('having', 'People having some badges'),
('nobody', 'No one, assigned through challenges'),
],
string="Allowance to Grant",
help="Who can grant this badge",
required=True),
'rule_auth_user_ids': fields.many2many('res.users', 'rel_badge_auth_users',
string='Authorized Users',
help="Only these people can give this badge"),
'rule_auth_badge_ids': fields.many2many('gamification.badge',
'gamification_badge_rule_badge_rel', 'badge1_id', 'badge2_id',
string='Required Badges',
help="Only the people having these badges can give this badge"),
'rule_max': fields.boolean('Monthly Limited Sending',
help="Check to set a monthly limit per person of sending this badge"),
'rule_max_number': fields.integer('Limitation Number',
help="The maximum number of time this badge can be sent per month per person."),
'stat_my_monthly_sending': fields.function(_get_badge_user_stats,
type="integer",
string='My Monthly Sending Total',
multi='badge_users',
help="The number of time the current user has sent this badge this month."),
'remaining_sending': fields.function(_remaining_sending_calc, type='integer',
string='Remaining Sending Allowed', help="If a maxium is set"),
'challenge_ids': fields.one2many('gamification.challenge', 'reward_id',
string="Reward of Challenges"),
'goal_definition_ids': fields.many2many('gamification.goal.definition', 'badge_unlocked_definition_rel',
string='Rewarded by',
help="The users that have succeeded theses goals will receive automatically the badge."),
'owner_ids': fields.one2many('gamification.badge.user', 'badge_id',
string='Owners', help='The list of instances of this badge granted to users'),
'active': fields.boolean('Active'),
'unique_owner_ids': fields.function(_get_owners_info,
string='Unique Owners',
help="The list of unique users having received this badge.",
multi='unique_users',
type="many2many", relation="res.users"),
'stat_count': fields.function(_get_owners_info, string='Total',
type="integer",
multi='stat_users',
help="The number of time this badge has been received."),
'stat_count_distinct': fields.function(_get_owners_info,
type="integer",
string='Number of users',
multi='stat_users',
help="The number of time this badge has been received by unique users."),
'stat_this_month': fields.function(_get_badge_user_stats,
type="integer",
string='Monthly total',
multi='badge_users',
help="The number of time this badge has been received this month."),
'stat_my': fields.function(_get_badge_user_stats, string='My Total',
type="integer",
multi='badge_users',
help="The number of time the current user has received this badge."),
'stat_my_this_month': fields.function(_get_badge_user_stats,
type="integer",
string='My Monthly Total',
multi='badge_users',
help="The number of time the current user has received this badge this month."),
}
_defaults = {
'rule_auth': 'everyone',
'active': True,
}
def check_granting(self, cr, uid, badge_id, context=None):
def check_granting(self):
"""Check the user 'uid' can grant the badge 'badge_id' and raise the appropriate exception
if not
Do not check for SUPERUSER_ID
"""
status_code = self._can_grant_badge(cr, uid, badge_id, context=context)
status_code = self._can_grant_badge()
if status_code == self.CAN_GRANT:
return True
elif status_code == self.NOBODY_CAN_GRANT:
raise UserError(_('This badge can not be sent by users.'))
raise exceptions.UserError(_('This badge can not be sent by users.'))
elif status_code == self.USER_NOT_VIP:
raise UserError(_('You are not in the user allowed list.'))
raise exceptions.UserError(_('You are not in the user allowed list.'))
elif status_code == self.BADGE_REQUIRED:
raise UserError(_('You do not have the required badges.'))
raise exceptions.UserError(_('You do not have the required badges.'))
elif status_code == self.TOO_MANY:
raise UserError(_('You have already sent this badge too many time this month.'))
raise exceptions.UserError(_('You have already sent this badge too many time this month.'))
else:
_logger.exception("Unknown badge status code: %d" % int(status_code))
_logger.error("Unknown badge status code: %s" % status_code)
return False
def _can_grant_badge(self, cr, uid, badge_id, context=None):
def _can_grant_badge(self):
"""Check if a user can grant a badge to another user
:param uid: the id of the res.users trying to send the badge
:param badge_id: the granted badge id
:return: integer representing the permission.
"""
if uid == SUPERUSER_ID:
if self.env.user._is_admin():
return self.CAN_GRANT
badge = self.browse(cr, uid, badge_id, context=context)
if badge.rule_auth == 'nobody':
if self.rule_auth == 'nobody':
return self.NOBODY_CAN_GRANT
elif badge.rule_auth == 'users' and uid not in [user.id for user in badge.rule_auth_user_ids]:
elif self.rule_auth == 'users' and self.env.user not in self.rule_auth_user_ids:
return self.USER_NOT_VIP
elif self.rule_auth == 'having':
all_user_badges = self.env['gamification.badge.user'].search([('user_id', '=', self.env.uid)])
if self.rule_auth_badge_ids - all_user_badges:
return self.BADGE_REQUIRED
elif badge.rule_auth == 'having':
all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', uid)], context=context)
for required_badge in badge.rule_auth_badge_ids:
if required_badge.id not in all_user_badges:
return self.BADGE_REQUIRED
if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number:
if self.rule_max and self.stat_my_monthly_sending >= self.rule_max_number:
return self.TOO_MANY
# badge.rule_auth == 'everyone' -> no check
return self.CAN_GRANT
def check_progress(self, cr, uid, context=None):
try:
model, res_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'badge_hidden')
except ValueError:
return True
badge_user_obj = self.pool.get('gamification.badge.user')
if not badge_user_obj.search(cr, uid, [('user_id', '=', uid), ('badge_id', '=', res_id)], context=context):
values = {
'user_id': uid,
'badge_id': res_id,
}
badge_user_obj.create(cr, SUPERUSER_ID, values, context=context)
return True
File diff suppressed because it is too large Load Diff
+252 -299
View File
@@ -1,21 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv, expression
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
from openerp.tools.safe_eval import safe_eval
from openerp.tools.translate import _
from openerp.exceptions import UserError
import logging
import time
from datetime import date, datetime, timedelta
from odoo import api, fields, models, _, exceptions
from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
class gamification_goal_definition(osv.Model):
class GoalDefinition(models.Model):
"""Goal definition
A goal definition contains the way to evaluate an objective
@@ -25,273 +22,233 @@ class gamification_goal_definition(osv.Model):
_name = 'gamification.goal.definition'
_description = 'Gamification goal definition'
def _get_suffix(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, '')
for goal in self.browse(cr, uid, ids, context=context):
if goal.suffix and not goal.monetary:
res[goal.id] = goal.suffix
elif goal.monetary:
# use the current user's company currency
user = self.pool.get('res.users').browse(cr, uid, uid, context)
if goal.suffix:
res[goal.id] = "%s %s" % (user.company_id.currency_id.symbol, goal.suffix)
else:
res[goal.id] = user.company_id.currency_id.symbol
else:
res[goal.id] = ""
return res
name = fields.Char("Goal Definition", required=True, translate=True)
description = fields.Text("Goal Description")
monetary = fields.Boolean("Monetary Value", default=False, help="The target and current value are defined in the company currency.")
suffix = fields.Char("Suffix", help="The unit of the target and current values", translate=True)
full_suffix = fields.Char("Full Suffix", compute='_compute_full_suffix', help="The currency and suffix field")
computation_mode = fields.Selection([
('manually', "Recorded manually"),
('count', "Automatic: number of records"),
('sum', "Automatic: sum on a field"),
('python', "Automatic: execute a specific Python code"),
], default='manually', string="Computation Mode", required=True,
help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.")
display_mode = fields.Selection([
('progress', "Progressive (using numerical values)"),
('boolean', "Exclusive (done or not-done)"),
], default='progress', string="Displayed as", required=True)
model_id = fields.Many2one('ir.model', string='Model', help='The model object for the field to evaluate')
field_id = fields.Many2one('ir.model.fields', string='Field to Sum', help='The field containing the value to evaluate')
field_date_id = fields.Many2one('ir.model.fields', string='Date Field', help='The date to use for the time period evaluated')
domain = fields.Char(
"Filter Domain", required=True, default="[]",
help="Domain for filtering records. General rule, not user depending,"
" e.g. [('state', '=', 'done')]. The expression can contain"
" reference to 'user' which is a browse record of the current"
" user if not in batch mode.")
_columns = {
'name': fields.char('Goal Definition', required=True, translate=True),
'description': fields.text('Goal Description'),
'monetary': fields.boolean('Monetary Value', help="The target and current value are defined in the company currency."),
'suffix': fields.char('Suffix', help="The unit of the target and current values", translate=True),
'full_suffix': fields.function(_get_suffix, type="char", string="Full Suffix", help="The currency and suffix field"),
'computation_mode': fields.selection([
('manually', 'Recorded manually'),
('count', 'Automatic: number of records'),
('sum', 'Automatic: sum on a field'),
('python', 'Automatic: execute a specific Python code'),
],
string="Computation Mode",
help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.",
required=True),
'display_mode': fields.selection([
('progress', 'Progressive (using numerical values)'),
('boolean', 'Exclusive (done or not-done)'),
],
string="Displayed as", required=True),
'model_id': fields.many2one('ir.model',
string='Model',
help='The model object for the field to evaluate'),
'field_id': fields.many2one('ir.model.fields',
string='Field to Sum',
help='The field containing the value to evaluate'),
'field_date_id': fields.many2one('ir.model.fields',
string='Date Field',
help='The date to use for the time period evaluated'),
'domain': fields.char("Filter Domain",
help="Domain for filtering records. General rule, not user depending, e.g. [('state', '=', 'done')]. The expression can contain reference to 'user' which is a browse record of the current user if not in batch mode.",
required=True),
batch_mode = fields.Boolean("Batch Mode", help="Evaluate the expression in batch instead of once for each user")
batch_distinctive_field = fields.Many2one('ir.model.fields', string="Distinctive field for batch user", help="In batch mode, this indicates which field distinct one user form the other, e.g. user_id, partner_id...")
batch_user_expression = fields.Char("Evaluated expression for batch mode", help="The value to compare with the distinctive field. The expression can contain reference to 'user' which is a browse record of the current user, e.g. user.id, user.partner_id.id...")
compute_code = fields.Text("Python Code", 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.")
condition = fields.Selection([
('higher', "The higher the better"),
('lower', "The lower the better")
], default='higher', required=True, string="Goal Performance",
help="A goal is considered as completed when the current value is compared to the value to reach")
action_id = fields.Many2one('ir.actions.act_window', string="Action", help="The action that will be called to update the goal value.")
res_id_field = fields.Char("ID Field of user", help="The field name on the user profile (res.users) containing the value for res_id for action.")
'batch_mode': fields.boolean('Batch Mode',
help="Evaluate the expression in batch instead of once for each user"),
'batch_distinctive_field': fields.many2one('ir.model.fields',
string="Distinctive field for batch user",
help="In batch mode, this indicates which field distinct one user form the other, e.g. user_id, partner_id..."),
'batch_user_expression': fields.char("Evaluted expression for batch mode",
help="The value to compare with the distinctive field. The expression can contain reference to 'user' which is a browse record of the current user, e.g. user.id, user.partner_id.id..."),
'compute_code': fields.text('Python Code',
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."),
'condition': fields.selection([
('higher', 'The higher the better'),
('lower', 'The lower the better')
],
string='Goal Performance',
help='A goal is considered as completed when the current value is compared to the value to reach',
required=True),
'action_id': fields.many2one('ir.actions.act_window', string="Action",
help="The action that will be called to update the goal value."),
'res_id_field': fields.char("ID Field of user",
help="The field name on the user profile (res.users) containing the value for res_id for action."),
}
@api.depends('suffix', 'monetary') # also depends of user...
def _compute_full_suffix(self):
for goal in self:
items = []
_defaults = {
'condition': 'higher',
'computation_mode': 'manually',
'domain': "[]",
'monetary': False,
'display_mode': 'progress',
}
if goal.monetary:
items.append(self.env.user.company_id.currency_id.symbol or u'¤')
if goal.suffix:
items.append(goal.suffix)
def number_following(self, cr, uid, model_name="mail.thread", context=None):
"""Return the number of 'model_name' objects the user is following
goal.full_suffix = u' '.join(items)
The model specified in 'model_name' must inherit from mail.thread
"""
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
return self.pool.get('mail.followers').search(cr, uid, [('res_model', '=', model_name), ('partner_id', '=', user.partner_id.id)], count=True, context=context)
def _check_domain_validity(self, cr, uid, ids, context=None):
def _check_domain_validity(self):
# take admin as should always be present
superuser = self.pool['res.users'].browse(cr, uid, SUPERUSER_ID, context=context)
for definition in self.browse(cr, uid, ids, context=context):
for definition in self:
if definition.computation_mode not in ('count', 'sum'):
continue
obj = self.pool[definition.model_id.model]
Obj = self.env[definition.model_id.model]
try:
domain = safe_eval(definition.domain, {'user': superuser})
# demmy search to make sure the domain is valid
obj.search(cr, uid, domain, context=context, count=True)
domain = safe_eval(definition.domain, {
'user': self.env.user.sudo(self.env.user)
})
# dummy search to make sure the domain is valid
Obj.search_count(domain)
except (ValueError, SyntaxError), e:
msg = e.message or (e.msg + '\n' + e.text)
raise UserError(_("The domain for the definition %s seems incorrect, please check it.\n\n%s") % (definition.name, msg))
raise exceptions.UserError(_("The domain for the definition %s seems incorrect, please check it.\n\n%s") % (definition.name, msg))
return True
def _check_model_validity(self, cr, uid, ids, context=None):
def _check_model_validity(self):
""" make sure the selected field and model are usable"""
for definition in self.browse(cr, uid, ids, context=context):
for definition in self:
try:
if not definition.model_id or not definition.field_id:
if not (definition.model_id and definition.field_id):
continue
model = self.pool[definition.model_id.model]
field = model._fields[definition.field_id.name]
if not field.store:
raise UserError(
Model = self.env[definition.model_id.model]
field = Model._fields.get(definition.field_id.name)
if not (field and field.store):
raise exceptions.UserError(
_("The model configuration for the definition %s seems incorrect, please check it.\n\n%s not stored") % (definition.name, definition.field_id.name))
except KeyError, e:
raise UserError(
raise exceptions.UserError(
_("The model configuration for the definition %s seems incorrect, please check it.\n\n%s not found") % (definition.name, e.message))
def create(self, cr, uid, vals, context=None):
res_id = super(gamification_goal_definition, self).create(cr, uid, vals, context=context)
if vals.get('computation_mode') in ('count', 'sum'):
self._check_domain_validity(cr, uid, [res_id], context=context)
@api.model
def create(self, vals):
definition = super(GoalDefinition, self).create(vals)
if definition.computation_mode in ('count', 'sum'):
definition._check_domain_validity()
if vals.get('field_id'):
self._check_model_validity(cr, uid, [res_id], context=context)
return res_id
definition._check_model_validity()
return definition
def write(self, cr, uid, ids, vals, context=None):
res = super(gamification_goal_definition, self).write(cr, uid, ids, vals, context=context)
@api.multi
def write(self, vals):
res = super(GoalDefinition, self).write(vals)
if vals.get('computation_mode', 'count') in ('count', 'sum') and (vals.get('domain') or vals.get('model_id')):
self._check_domain_validity(cr, uid, ids, context=context)
self._check_domain_validity()
if vals.get('field_id') or vals.get('model_id') or vals.get('batch_mode'):
self._check_model_validity(cr, uid, ids, context=context)
self._check_model_validity()
return res
def on_change_model_id(self, cr, uid, ids, model_id, context=None):
@api.onchange('model_id')
def _change_model_id(self):
"""Force domain for the `field_id` and `field_date_id` fields"""
if not model_id:
if not self.model_id:
return {'domain': {'field_id': expression.FALSE_DOMAIN, 'field_date_id': expression.FALSE_DOMAIN}}
model = self.pool['ir.model'].browse(cr, uid, model_id, context=context)
model_fields_domain = [('store', '=', True),
'|', ('model_id', '=', model_id), ('model_id', 'in', model.inherited_model_ids.ids)]
model = self.env['ir.model'].browse(self.model_id)
model_fields_domain = [
('store', '=', True),
'|', ('model_id', '=', self.model_id),
('model_id', 'in', model.inherited_model_ids.ids)]
model_date_fields_domain = expression.AND([[('ttype', 'in', ('date', 'datetime'))], model_fields_domain])
return {'domain': {'field_id': model_fields_domain, 'field_date_id': model_date_fields_domain}}
class gamification_goal(osv.Model):
class Goal(models.Model):
"""Goal instance for a user
An individual goal for a user on a specified time period"""
_name = 'gamification.goal'
_description = 'Gamification goal instance'
def _get_completion(self, cr, uid, ids, field_name, arg, context=None):
"""Return the percentage of completeness of the goal, between 0 and 100"""
res = dict.fromkeys(ids, 0.0)
for goal in self.browse(cr, uid, ids, context=context):
if goal.definition_condition == 'higher':
if goal.current >= goal.target_goal:
res[goal.id] = 100.0
else:
res[goal.id] = round(100.0 * goal.current / goal.target_goal, 2)
elif goal.current < goal.target_goal:
# a goal 'lower than' has only two values possible: 0 or 100%
res[goal.id] = 100.0
else:
res[goal.id] = 0.0
return res
def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
goal_definition = self.pool.get('gamification.goal.definition')
if not definition_id:
return {'value': {'definition_id': False}}
goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
return {'value': {'computation_mode': goal_definition.computation_mode, 'definition_condition': goal_definition.condition}}
_columns = {
'definition_id': fields.many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade"),
'user_id': fields.many2one('res.users', string='User', required=True, auto_join=True, ondelete="cascade"),
'line_id': fields.many2one('gamification.challenge.line', string='Challenge Line', ondelete="cascade"),
'challenge_id': fields.related('line_id', 'challenge_id',
string="Challenge",
type='many2one',
relation='gamification.challenge',
store=True, readonly=True,
help="Challenge that generated the goal, assign challenge to users to generate goals with a value in this field."),
'start_date': fields.date('Start Date'),
'end_date': fields.date('End Date'), # no start and end = always active
'target_goal': fields.float('To Reach',
required=True,
track_visibility='always'), # no goal = global index
'current': fields.float('Current Value', required=True, track_visibility='always'),
'completeness': fields.function(_get_completion, type='float', string='Completeness'),
'state': fields.selection([
('draft', 'Draft'),
('inprogress', 'In progress'),
('reached', 'Reached'),
('failed', 'Failed'),
('canceled', 'Canceled'),
],
string='State',
required=True,
track_visibility='always'),
'to_update': fields.boolean('To update'),
'closed': fields.boolean('Closed goal', help="These goals will not be recomputed."),
'computation_mode': fields.related('definition_id', 'computation_mode', type='char', string="Computation mode"),
'remind_update_delay': fields.integer('Remind delay',
help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified."),
'last_update': fields.date('Last Update',
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."),
'definition_description': fields.related('definition_id', 'description', type='char', string='Definition Description', readonly=True),
'definition_condition': fields.related('definition_id', 'condition', type='char', string='Definition Condition', readonly=True),
'definition_suffix': fields.related('definition_id', 'full_suffix', type="char", string="Suffix", readonly=True),
'definition_display': fields.related('definition_id', 'display_mode', type="char", string="Display Mode", readonly=True),
}
_defaults = {
'current': 0,
'state': 'draft',
'start_date': fields.date.today,
}
_order = 'start_date desc, end_date desc, definition_id, id'
def _check_remind_delay(self, cr, uid, goal, context=None):
definition_id = fields.Many2one('gamification.goal.definition', string="Goal Definition", required=True, ondelete="cascade")
user_id = fields.Many2one('res.users', string="User", required=True, auto_join=True, ondelete="cascade")
line_id = fields.Many2one('gamification.challenge.line', string="Challenge Line", ondelete="cascade")
challenge_id = fields.Many2one(
related='line_id.challenge_id', store=True, readonly=True,
help="Challenge that generated the goal, assign challenge to users "
"to generate goals with a value in this field.")
start_date = fields.Date("Start Date", default=fields.Date.today)
end_date = fields.Date("End Date") # no start and end = always active
target_goal = fields.Float('To Reach', required=True, track_visibility='always')
# no goal = global index
current = fields.Float("Current Value", required=True, default=0, track_visibility='always')
completeness = fields.Float("Completeness", compute='_get_completion')
state = fields.Selection([
('draft', "Draft"),
('inprogress', "In progress"),
('reached', "Reached"),
('failed', "Failed"),
('canceled', "Canceled"),
], default='draft', string='State', required=True, track_visibility='always')
to_update = fields.Boolean('To update')
closed = fields.Boolean('Closed goal', help="These goals will not be recomputed.")
computation_mode = fields.Selection(related='definition_id.computation_mode')
remind_update_delay = fields.Integer(
"Remind delay", help="The number of days after which the user "
"assigned to a manual goal will be reminded. "
"Never reminded if no value is specified.")
last_update = fields.Date(
"Last Update",
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.")
definition_description = fields.Text("Definition Description", related='definition_id.description', readonly=True)
definition_condition = fields.Selection("Definition Condition", related='definition_id.condition', readonly=True)
definition_suffix = fields.Char("Suffix", related='definition_id.full_suffix', readonly=True)
definition_display = fields.Selection("Display Mode", related='definition_id.display_mode', readonly=True)
@api.depends('current', 'target_goal', 'definition_id.condition')
def _get_completion(self):
"""Return the percentage of completeness of the goal, between 0 and 100"""
for goal in self:
if goal.definition_condition == 'higher':
if goal.current >= goal.target_goal:
goal.completeness = 100.0
else:
goal.completeness = round(100.0 * goal.current / goal.target_goal, 2)
elif goal.current < goal.target_goal:
# a goal 'lower than' has only two values possible: 0 or 100%
goal.completeness = 100.0
else:
goal.completeness = 0.0
def _check_remind_delay(self):
"""Verify if a goal has not been updated for some time and send a
reminder message of needed.
:return: data to write on the goal object
"""
temp_obj = self.pool['mail.template']
if goal.remind_update_delay and goal.last_update:
delta_max = timedelta(days=goal.remind_update_delay)
last_update = datetime.strptime(goal.last_update, DF).date()
if date.today() - last_update > delta_max:
# generate a remind report
temp_obj = self.pool.get('mail.template')
template_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'gamification', 'email_template_goal_reminder')[0]
template = temp_obj.get_email_template(cr, uid, template_id, goal.id, context=context)
body_html = temp_obj.render_template(cr, uid, template.body_html, 'gamification.goal', goal.id, context=template._context)
self.pool['mail.thread'].message_post(cr, uid, 0, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment')
return {'to_update': True}
return {}
if not (self.remind_update_delay and self.last_update):
return {}
def _get_write_values(self, cr, uid, goal, new_value, context=None):
delta_max = timedelta(days=self.remind_update_delay)
last_update = fields.Date.from_string(self.last_update)
if date.today() - last_update < delta_max:
return {}
# generate a reminder report
template = self.env.ref('gamification.email_template_goal_reminder')\
.get_email_template(self.id)
body_html = self.env['mail.template'].with_context(template._context)\
.render_template(template.body_html, 'gamification.goal', self.id)
self.env['mail.thread'].message_post(
body=body_html,
partner_ids=[ self.user_id.partner_id.id],
subtype='mail.mt_comment'
)
return {'to_update': True}
def _get_write_values(self, new_value):
"""Generate values to write after recomputation of a goal score"""
if new_value == goal.current:
if new_value == self.current:
# avoid useless write if the new value is the same as the old one
return {}
result = {goal.id: {'current': new_value}}
if (goal.definition_id.condition == 'higher' and new_value >= goal.target_goal) \
or (goal.definition_id.condition == 'lower' and new_value <= goal.target_goal):
result = {'current': new_value}
if (self.definition_id.condition == 'higher' and new_value >= self.target_goal) \
or (self.definition_id.condition == 'lower' and new_value <= self.target_goal):
# success, do no set closed as can still change
result[goal.id]['state'] = 'reached'
result['state'] = 'reached'
elif goal.end_date and fields.date.today() > goal.end_date:
elif self.end_date and fields.Date.today() > self.end_date:
# check goal failure
result[goal.id]['state'] = 'failed'
result[goal.id]['closed'] = True
result['state'] = 'failed'
result['closed'] = True
return result
return {self: result}
def update_goal(self, cr, uid, ids, context=None):
@api.multi
def update_goal(self):
"""Update the goals to recomputes values and change of states
If a manual goal is not updated for enough time, the user will be
@@ -299,49 +256,47 @@ class gamification_goal(osv.Model):
If a goal reaches the target value, the status is set to reached
If the end date is passed (at least +1 day, time not considered) without
the target value being reached, the goal is set as failed."""
if context is None:
context = {}
commit = context.get('commit_gamification', False)
goals_by_definition = {}
for goal in self.browse(cr, uid, ids, context=context):
for goal in self:
goals_by_definition.setdefault(goal.definition_id, []).append(goal)
for definition, goals in goals_by_definition.items():
goals_to_write = dict((goal.id, {}) for goal in goals)
goals_to_write = {}
if definition.computation_mode == 'manually':
for goal in goals:
goals_to_write[goal.id].update(self._check_remind_delay(cr, uid, goal, context))
goals_to_write[goal] = goal._check_remind_delay()
elif definition.computation_mode == 'python':
# TODO batch execution
for goal in goals:
# execute the chosen method
cxt = {
'self': self.pool.get('gamification.goal'),
'self': self.pool['gamification.goal'],
'object': goal,
'pool': self.pool,
'cr': cr,
'context': dict(context), # copy context to prevent side-effects of eval
'uid': uid,
'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time
'cr': self.env.cr,
'context': dict(self.env.context), # copy context to prevent side-effects of eval
'uid': self.env.uid,
'date': date,
'datetime': datetime,
'timedelta': timedelta,
'time': time,
}
code = definition.compute_code.strip()
safe_eval(code, cxt, mode="exec", nocopy=True)
# the result of the evaluated codeis put in the 'result' local variable, propagated to the context
result = cxt.get('result')
if result is not None and type(result) in (float, int, long):
goals_to_write.update(
self._get_write_values(cr, uid, goal, result, context=context)
)
if result is not None and isinstance(result, (float, int, long)):
goals_to_write.update(goal._get_write_values(result))
else:
_logger.exception(_('Invalid return content from the evaluation of code for definition %s') % definition.name)
_logger.error(
"Invalid return content '%r' from the evaluation "
"of code for definition %s, expected a number",
result, definition.name)
else: # count or sum
Obj = self.env[definition.model_id.model]
obj = self.pool.get(definition.model_id.model)
field_date_name = definition.field_date_id and definition.field_date_id.name or False
field_date_name = definition.field_date_id.name
if definition.computation_mode == 'count' and definition.batch_mode:
# batch mode, trying to do as much as possible in one request
general_domain = safe_eval(definition.domain)
@@ -363,21 +318,19 @@ class gamification_goal(osv.Model):
if field_name == 'id':
# grouping on id does not work and is similar to search anyway
user_ids = obj.search(cr, uid, subquery_domain, context=context)
user_values = [{'id': user_id, 'id_count': 1} for user_id in user_ids]
users = Obj.search(subquery_domain)
user_values = [{'id': user.id, 'id_count': 1} for user in users]
else:
user_values = obj.read_group(cr, uid, subquery_domain, fields=[field_name], groupby=[field_name], context=context)
user_values = Obj.read_group(subquery_domain, fields=[field_name], groupby=[field_name])
# user_values has format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...]
for goal in [g for g in goals if g.id in query_goals.keys()]:
for goal in [g for g in goals if g.id in query_goals]:
for user_value in user_values:
queried_value = field_name in user_value and user_value[field_name] or False
if isinstance(queried_value, tuple) and len(queried_value) == 2 and isinstance(queried_value[0], (int, long)):
queried_value = queried_value[0]
if queried_value == query_goals[goal.id]:
new_value = user_value.get(field_name+'_count', goal.current)
goals_to_write.update(
self._get_write_values(cr, uid, goal, new_value, context=context)
)
goals_to_write.update(goal._get_write_values(new_value))
else:
for goal in goals:
@@ -393,111 +346,111 @@ class gamification_goal(osv.Model):
if definition.computation_mode == 'sum':
field_name = definition.field_id.name
# TODO for master: group on user field in batch mode
res = obj.read_group(cr, uid, domain, [field_name], [], context=context)
res = Obj.read_group(domain, [field_name], [])
new_value = res and res[0][field_name] or 0.0
else: # computation mode = count
new_value = obj.search(cr, uid, domain, context=context, count=True)
new_value = Obj.search_count(domain)
goals_to_write.update(
self._get_write_values(cr, uid, goal, new_value, context=context)
)
goals_to_write.update(goal._get_write_values(new_value))
for goal_id, value in goals_to_write.items():
if not value:
for goal, values in goals_to_write.iteritems():
if not values:
continue
self.write(cr, uid, [goal_id], value, context=context)
if commit:
cr.commit()
goal.write(values)
if self.env.context.get('commit_gamification'):
self.env.cr.commit()
return True
def action_start(self, cr, uid, ids, context=None):
@api.multi
def action_start(self):
"""Mark a goal as started.
This should only be used when creating goals manually (in draft state)"""
self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
return self.update_goal(cr, uid, ids, context=context)
self.write({'state': 'inprogress'})
return self.update_goal()
def action_reach(self, cr, uid, ids, context=None):
@api.multi
def action_reach(self):
"""Mark a goal as reached.
If the target goal condition is not met, the state will be reset to In
Progress at the next goal update until the end date."""
return self.write(cr, uid, ids, {'state': 'reached'}, context=context)
return self.write({'state': 'reached'})
def action_fail(self, cr, uid, ids, context=None):
@api.multi
def action_fail(self):
"""Set the state of the goal to failed.
A failed goal will be ignored in future checks."""
return self.write(cr, uid, ids, {'state': 'failed'}, context=context)
return self.write({'state': 'failed'})
def action_cancel(self, cr, uid, ids, context=None):
@api.multi
def action_cancel(self):
"""Reset the completion after setting a goal as reached or failed.
This is only the current state, if the date and/or target criterias
This is only the current state, if the date and/or target criteria
match the conditions for a change of state, this will be applied at the
next goal update."""
return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
return self.write({'state': 'inprogress'})
def create(self, cr, uid, vals, context=None):
"""Overwrite the create method to add a 'no_remind_goal' field to True"""
context = dict(context or {})
context['no_remind_goal'] = True
return super(gamification_goal, self).create(cr, uid, vals, context=context)
@api.model
def create(self, vals):
return super(Goal, self.with_context(no_remind_goal=True)).create(vals)
def write(self, cr, uid, ids, vals, context=None):
@api.multi
def write(self, vals):
"""Overwrite the write method to update the last_update field to today
If the current value is changed and the report frequency is set to On
change, a report is generated
"""
if context is None:
context = {}
vals['last_update'] = fields.date.today()
result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context)
for goal in self.browse(cr, uid, ids, context=context):
vals['last_update'] = fields.Date.today()
result = super(Goal, self).write(vals)
for goal in self:
if goal.state != "draft" and ('definition_id' in vals or 'user_id' in vals):
# avoid drag&drop in kanban view
raise UserError(_('Can not modify the configuration of a started goal'))
raise exceptions.UserError(_('Can not modify the configuration of a started goal'))
if vals.get('current'):
if 'no_remind_goal' in context:
# new goals should not be reported
continue
if goal.challenge_id and goal.challenge_id.report_message_frequency == 'onchange':
self.pool.get('gamification.challenge').report_progress(cr, SUPERUSER_ID, goal.challenge_id, users=[goal.user_id], context=context)
if vals.get('current') and 'no_remind_goal' not in self.env.context:
if goal.challenge_id.report_message_frequency == 'onchange':
goal.challenge_id.sudo().report_progress(users=goal.user_id)
return result
def get_action(self, cr, uid, goal_id, context=None):
@api.multi
def get_action(self):
"""Get the ir.action related to update the goal
In case of a manual goal, should return a wizard to update the value
:return: action description in a dictionnary
:return: action description in a dictionary
"""
goal = self.browse(cr, uid, goal_id, context=context)
if goal.definition_id.action_id:
if self.definition_id.action_id:
# open a the action linked to the goal
action = goal.definition_id.action_id.read()[0]
action = self.definition_id.action_id.read()[0]
if goal.definition_id.res_id_field:
current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
action['res_id'] = safe_eval(goal.definition_id.res_id_field, {'user': current_user})
if self.definition_id.res_id_field:
current_user = self.env.user.sudo(self.env.user)
action['res_id'] = safe_eval(self.definition_id.res_id_field, {
'user': current_user
})
# if one element to display, should see it in form mode if possible
action['views'] = [(view_id, mode) for (view_id, mode) in action['views'] if mode == 'form'] or action['views']
action['views'] = [
(view_id, mode)
for (view_id, mode) in action['views']
if mode == 'form'
] or action['views']
return action
if goal.computation_mode == 'manually':
if self.computation_mode == 'manually':
# open a wizard window to update the value manually
action = {
'name': _("Update %s") % goal.definition_id.name,
'id': goal_id,
'name': _("Update %s") % self.definition_id.name,
'id': self.id,
'type': 'ir.actions.act_window',
'views': [[False, 'form']],
'target': 'new',
'context': {'default_goal_id': goal_id, 'default_current': goal.current},
'context': {'default_goal_id': self.id, 'default_current': self.current},
'res_model': 'gamification.goal.wizard'
}
return action
@@ -1,5 +1,4 @@
<?xml version="1.0" ?>
<openerp>
<odoo>
<data noupdate="1">
<record id="base.group_hr_manager" model="res.groups">
<field name="name">Manager</field>
@@ -43,4 +42,4 @@
</record>
</data>
</openerp>
</odoo>
+25 -45
View File
@@ -1,72 +1,52 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.tests import common
from odoo.tests import common
class test_challenge(common.TransactionCase):
def setUp(self):
super(test_challenge, self).setUp()
cr, uid = self.cr, self.uid
self.data_obj = self.registry('ir.model.data')
self.user_obj = self.registry('res.users')
self.challenge_obj = self.registry('gamification.challenge')
self.line_obj = self.registry('gamification.challenge.line')
self.goal_obj = self.registry('gamification.goal')
self.badge_obj = self.registry('gamification.badge')
self.badge_user_obj = self.registry('gamification.badge.user')
self.demo_user_id = self.data_obj.get_object_reference(cr, uid, 'base', 'user_demo')[1]
self.group_user_id = self.data_obj.get_object_reference(cr, uid, 'base', 'group_user')[1]
self.challenge_base_id = self.data_obj.get_object_reference(cr, uid, 'gamification', 'challenge_base_discover')[1]
self.definition_timezone_id = self.data_obj.get_object_reference(cr, uid, 'gamification', 'definition_base_timezone')[1]
self.badge_id = self.data_obj.get_object_reference(cr, uid, 'gamification', 'badge_good_job')[1]
def test_00_join_challenge(self):
cr, uid, context = self.cr, self.uid, {}
user_ids = self.user_obj.search(cr, uid, [('groups_id', '=', self.group_user_id)])
challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
employees_group = self.env.ref('base.group_user')
user_ids = employees_group.users
challenge = self.env.ref('gamification.challenge_base_discover')
self.assertGreaterEqual(len(challenge.user_ids), len(user_ids), "Not enough users in base challenge")
self.user_obj.create(cr, uid, {
self.env['res.users'].with_context(no_reset_password=True).create({
'name': 'R2D2',
'login': 'r2d2@openerp.com',
'email': 'r2d2@openerp.com',
'groups_id': [(6, 0, [self.group_user_id])]
}, {'no_reset_password': True})
'groups_id': [(6, 0, [employees_group.id])]
})
self.challenge_obj._update_all(cr, uid, [self.challenge_base_id], context=context)
challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
challenge._update_all()
self.assertGreaterEqual(len(challenge.user_ids), len(user_ids)+1, "These are not droids you are looking for")
def test_10_reach_challenge(self):
cr, uid, context = self.cr, self.uid, {}
self.challenge_obj.write(cr, uid, [self.challenge_base_id], {'state': 'inprogress'}, context=context)
challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
challenge_user_ids = [user.id for user in challenge.user_ids]
Goals = self.env['gamification.goal']
challenge = self.env.ref('gamification.challenge_base_discover')
challenge.state = 'inprogress'
self.assertEqual(challenge.state, 'inprogress', "Challenge failed the change of state")
line_ids = self.line_obj.search(cr, uid, [('challenge_id', '=', self.challenge_base_id)], context=context)
goal_ids = self.goal_obj.search(cr, uid, [('challenge_id', '=', self.challenge_base_id), ('state', '!=', 'draft')], context=context)
self.assertEqual(len(goal_ids), len(line_ids)*len(challenge_user_ids), "Incorrect number of goals generated, should be 1 goal per user, per challenge line")
goal_ids = Goals.search([('challenge_id', '=', challenge.id), ('state', '!=', 'draft')])
self.assertEqual(len(goal_ids), len(challenge.line_ids) * len(challenge.user_ids.ids), "Incorrect number of goals generated, should be 1 goal per user, per challenge line")
demo = self.env.ref('base.user_demo')
# demo user will set a timezone
self.user_obj.write(cr, uid, self.demo_user_id, {'tz': "Europe/Brussels"}, context=context)
goal_ids = self.goal_obj.search(cr, uid, [('user_id', '=', self.demo_user_id), ('definition_id', '=', self.definition_timezone_id)], context=context)
demo.tz = "Europe/Brussels"
goal_ids = Goals.search([('user_id', '=', demo.id), ('definition_id', '=', self.env.ref('gamification.definition_base_timezone').id)])
self.goal_obj.update_goal(cr, uid, goal_ids, context=context)
reached_goal_ids = self.goal_obj.search(cr, uid, [('id', 'in', goal_ids), ('state', '=', 'reached')], context=context)
self.assertEqual(set(goal_ids), set(reached_goal_ids), "Not every goal was reached after changing timezone")
goal_ids.update_goal()
missed = goal_ids.filtered(lambda g: g.state != 'reached')
self.assertFalse(missed, "Not every goal was reached after changing timezone")
# reward for two firsts as admin may have timezone
self.challenge_obj.write(cr, uid, self.challenge_base_id, {'reward_first_id': self.badge_id, 'reward_second_id': self.badge_id}, context=context)
self.challenge_obj.write(cr, uid, self.challenge_base_id, {'state': 'done'}, context=context)
badge_id = self.env.ref('gamification.badge_good_job').id
challenge.write({'reward_first_id': badge_id, 'reward_second_id': badge_id})
challenge.state = 'done'
badge_ids = self.badge_user_obj.search(cr, uid, [('badge_id', '=', self.badge_id), ('user_id', '=', self.demo_user_id)])
self.assertGreater(len(badge_ids), 0, "Demo user has not received the badge")
badge_ids = self.env['gamification.badge.user'].search([('badge_id', '=', badge_id), ('user_id', '=', demo.id)])
self.assertEqual(len(badge_ids), 1, "Demo user has not received the badge")
+172 -176
View File
@@ -1,188 +1,184 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<odoo>
<!-- Badge views -->
<record id="badge_list_action" model="ir.actions.act_window">
<field name="name">Badges</field>
<field name="res_model">gamification.badge</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a badge.
</p>
<p>
A badge is a symbolic token granted to a user as a sign of reward.
It can be deserved automatically when some conditions are met or manually by users.
Some badges are harder than others to get with specific conditions.
</p>
</field>
</record>
<!-- Badge views -->
<record id="badge_list_action" model="ir.actions.act_window">
<field name="name">Badges</field>
<field name="res_model">gamification.badge</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a badge.
</p>
<p>
A badge is a symbolic token granted to a user as a sign of reward.
It can be deserved automatically when some conditions are met or manually by users.
Some badges are harder than others to get with specific conditions.
</p>
</field>
</record>
<record id="badge_list_view" model="ir.ui.view">
<field name="name">Badge List</field>
<field name="model">gamification.badge</field>
<field name="arch" type="xml">
<tree string="Badge List">
<field name="name"/>
<field name="stat_count"/>
<field name="stat_this_month"/>
<field name="stat_my"/>
<field name="rule_auth"/>
</tree>
</field>
</record>
<record id="badge_list_view" model="ir.ui.view">
<field name="name">Badge List</field>
<field name="model">gamification.badge</field>
<field name="arch" type="xml">
<tree string="Badge List">
<field name="name"/>
<field name="stat_count"/>
<field name="stat_this_month"/>
<field name="stat_my"/>
<field name="rule_auth"/>
</tree>
</field>
</record>
<record id="badge_form_view" model="ir.ui.view">
<field name="name">Badge Form</field>
<field name="model">gamification.badge</field>
<field name="arch" type="xml">
<form string="Badge">
<header>
<button string="Grant this Badge" type="action" name="%(action_grant_wizard)d" class="oe_highlight" attrs="{'invisible': [('remaining_sending','=',0)]}" />
</header>
<sheet>
<div class="oe_button_box" name="button_box"/>
<field name="image" widget='image' class="oe_avatar"/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="Badge Name"/>
</h1>
<record id="badge_form_view" model="ir.ui.view">
<field name="name">Badge Form</field>
<field name="model">gamification.badge</field>
<field name="arch" type="xml">
<form string="Badge">
<header>
<button string="Grant this Badge" type="action" name="%(action_grant_wizard)d" class="oe_highlight" attrs="{'invisible': [('remaining_sending','=',0)]}" />
</header>
<sheet>
<div class="oe_button_box" name="button_box"/>
<field name="image" widget='image' class="oe_avatar"/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="Badge Name"/>
</h1>
</div>
<group>
<field name="description" nolabel="1" placeholder="Badge Description"/>
</group>
<group string="Granting">
<div class="oe_grey" colspan="2">
Security rules to define who is allowed to manually grant badges. Not enforced for administrator.
</div>
<group>
<field name="description" nolabel="1" placeholder="Badge Description"/>
</group>
<group string="Granting">
<div class="oe_grey" colspan="2">
Security rules to define who is allowed to manually grant badges. Not enforced for administrator.
</div>
<group>
<field name="rule_auth" widget="radio"/>
<field name="rule_auth_user_ids" attrs="{'invisible': [('rule_auth','!=','users')]}" widget="many2many_tags" />
<field name="rule_auth_badge_ids" attrs="{'invisible': [('rule_auth','!=','having')]}" widget="many2many_tags" />
<field name="rule_max" attrs="{'invisible': [('rule_auth','=','nobody')]}" />
<field name="rule_max_number" attrs="{'invisible': ['|',('rule_max','=',False),('rule_auth','=','nobody')]}"/>
<label for="stat_my_monthly_sending"/>
<div>
<field name="stat_my_monthly_sending" attrs="{'invisible': [('rule_auth','=','nobody')]}" />
<div attrs="{'invisible': [('remaining_sending','=',-1)]}" class="oe_grey">
You can still grant <field name="remaining_sending" class="oe_inline"/> badges this month
</div>
<div attrs="{'invisible': [('remaining_sending','!=',-1)]}" class="oe_grey">
No monthly sending limit
</div>
<field name="rule_auth" widget="radio"/>
<field name="rule_auth_user_ids" attrs="{'invisible': [('rule_auth','!=','users')]}" widget="many2many_tags" />
<field name="rule_auth_badge_ids" attrs="{'invisible': [('rule_auth','!=','having')]}" widget="many2many_tags" />
<field name="rule_max" attrs="{'invisible': [('rule_auth','=','nobody')]}" />
<field name="rule_max_number" attrs="{'invisible': ['|',('rule_max','=',False),('rule_auth','=','nobody')]}"/>
<label for="stat_my_monthly_sending"/>
<div>
<field name="stat_my_monthly_sending" attrs="{'invisible': [('rule_auth','=','nobody')]}" />
<div attrs="{'invisible': [('remaining_sending','=',-1)]}" class="oe_grey">
You can still grant <field name="remaining_sending" class="oe_inline"/> badges this month
</div>
</group>
</group>
<group string="Rewards for challenges">
<field name="challenge_ids" widget="many2many_kanban" nolabel="1" context="{'default_reward_id': active_id}" />
</group>
<group string="Statistics">
<group>
<field name="stat_count"/>
<field name="stat_this_month"/>
<field name="stat_count_distinct"/>
</group>
<group>
<field name="stat_my"/>
<field name="stat_my_this_month"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="badge_kanban_view" model="ir.ui.view" >
<field name="name">Badge Kanban View</field>
<field name="model">gamification.badge</field>
<field name="arch" type="xml">
<kanban class="o_kanban_badge">
<field name="name"/>
<field name="description"/>
<field name="image"/>
<field name="stat_my"/>
<field name="stat_count"/>
<field name="stat_this_month"/>
<field name="unique_owner_ids"/>
<field name="stat_my_monthly_sending"/>
<field name="remaining_sending" />
<field name="rule_max_number" />
<templates>
<t t-name="kanban-box">
<div t-attf-class="o_kanban_gamification oe_kanban_global_click #{record.stat_my.raw_value ? 'oe_kanban_color_5' : 'oe_kanban_color_white'}">
<div class="o_kanban_image">
<img t-att-src="kanban_image('gamification.badge', 'image', record.image.raw_value)" t-att-title="record.name.value"/>
</div>
<div class="oe_kanban_details">
<strong><field name="name"/></strong>
<span class="oe_grey">
<t t-if="record.remaining_sending.value != -1">
<t t-esc="record.stat_my_monthly_sending.value"/>/<t t-esc="record.rule_max_number.value"/>
</t>
<t t-if="record.remaining_sending.value == -1">
<t t-esc="record.stat_my_monthly_sending.value"/>/∞
</t>
</span>
<div t-if="record.remaining_sending.value == 0" class="oe_grey">Can not grant</div>
<div>
<strong><t t-esc="record.stat_count.raw_value"/></strong> granted,
<strong><t t-esc="record.stat_this_month.raw_value"/></strong> this month
</div>
<div t-if="record.description.value" class="o_kanban_badge_description">
<em><field name="description"/></em>
<div>
<t t-foreach="record.unique_owner_ids.raw_value.slice(0,11)" t-as="owner">
<img width="24" height="24" class="oe_kanban_avatar" t-att-src="kanban_image('res.users', 'image_small', owner)" t-att-data-member_id="owner"/>
</t>
</div>
</div>
<div t-if="record.remaining_sending.value != 0" class="o_kanban_button">
<button type="action" name="%(action_grant_wizard)d" class="oe_highlight btn btn-sm btn-primary">Grant</button>
</div>
<div attrs="{'invisible': [('remaining_sending','!=',-1)]}" class="oe_grey">
No monthly sending limit
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</group>
</group>
<group string="Rewards for challenges">
<field name="challenge_ids" widget="many2many_kanban" nolabel="1" context="{'default_reward_id': active_id}" />
</group>
<group string="Statistics">
<group>
<field name="stat_count"/>
<field name="stat_this_month"/>
<field name="stat_count_distinct"/>
</group>
<group>
<field name="stat_my"/>
<field name="stat_my_this_month"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Badge user viewss -->
<record id="badge_user_kanban_view" model="ir.ui.view" >
<field name="name">Badge User Kanban View</field>
<field name="model">gamification.badge.user</field>
<field name="arch" type="xml">
<kanban class="oe_background_grey">
<field name="badge_name"/>
<field name="badge_id"/>
<field name="user_id"/>
<field name="comment"/>
<field name="create_date"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click oe_kanban_badge oe_kanban_color_white o_kanban_gamification">
<div class="o_kanban_content">
<div class="pull-left">
<a type="open"><img t-att-src="kanban_image('gamification.badge', 'image', record.badge_id.raw_value)" t-att-title="record.badge_name.value" width="24" height="24" /></a>
</div>
<h4>
<a type="open"><t t-esc="record.badge_name.raw_value" /></a>
</h4>
<t t-if="record.comment.raw_value">
<p><em><field name="comment"/></em></p>
<record id="badge_kanban_view" model="ir.ui.view" >
<field name="name">Badge Kanban View</field>
<field name="model">gamification.badge</field>
<field name="arch" type="xml">
<kanban class="o_kanban_badge">
<field name="name"/>
<field name="description"/>
<field name="image"/>
<field name="stat_my"/>
<field name="stat_count"/>
<field name="stat_this_month"/>
<field name="unique_owner_ids"/>
<field name="stat_my_monthly_sending"/>
<field name="remaining_sending" />
<field name="rule_max_number" />
<templates>
<t t-name="kanban-box">
<div t-attf-class="o_kanban_gamification oe_kanban_global_click #{record.stat_my.raw_value ? 'oe_kanban_color_5' : 'oe_kanban_color_white'}">
<div class="o_kanban_image">
<img t-att-src="kanban_image('gamification.badge', 'image', record.image.raw_value)" t-att-title="record.name.value"/>
</div>
<div class="oe_kanban_details">
<strong><field name="name"/></strong>
<span class="oe_grey">
<t t-if="record.remaining_sending.value != -1">
<t t-esc="record.stat_my_monthly_sending.value"/>/<t t-esc="record.rule_max_number.value"/>
</t>
<p>Granted by <a type="open"><field name="create_uid" /></a> the <t t-esc="moment(record.create_date.raw_value).format('L')" /></p>
<t t-if="record.remaining_sending.value == -1">
<t t-esc="record.stat_my_monthly_sending.value"/>/∞
</t>
</span>
<div t-if="record.remaining_sending.value == 0" class="oe_grey">Can not grant</div>
<div>
<strong><t t-esc="record.stat_count.raw_value"/></strong> granted,
<strong><t t-esc="record.stat_this_month.raw_value"/></strong> this month
</div>
<div t-if="record.description.value" class="o_kanban_badge_description">
<em><field name="description"/></em>
<div>
<t t-foreach="record.unique_owner_ids.raw_value.slice(0,11)" t-as="owner">
<img width="24" height="24" class="oe_kanban_avatar" t-att-src="kanban_image('res.users', 'image_small', owner)" t-att-data-member_id="owner"/>
</t>
</div>
</div>
<div t-if="record.remaining_sending.value != 0" class="o_kanban_button">
<button type="action" name="%(action_grant_wizard)d" class="oe_highlight btn btn-sm btn-primary">Grant</button>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</data>
</openerp>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- Badge user viewss -->
<record id="badge_user_kanban_view" model="ir.ui.view" >
<field name="name">Badge User Kanban View</field>
<field name="model">gamification.badge.user</field>
<field name="arch" type="xml">
<kanban class="oe_background_grey">
<field name="badge_name"/>
<field name="badge_id"/>
<field name="user_id"/>
<field name="comment"/>
<field name="create_date"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click oe_kanban_badge oe_kanban_color_white o_kanban_gamification">
<div class="o_kanban_content">
<div class="pull-left">
<a type="open"><img t-att-src="kanban_image('gamification.badge', 'image', record.badge_id.raw_value)" t-att-title="record.badge_name.value" width="24" height="24" /></a>
</div>
<h4>
<a type="open"><t t-esc="record.badge_name.raw_value" /></a>
</h4>
<t t-if="record.comment.raw_value">
<p><em><field name="comment"/></em></p>
</t>
<p>Granted by <a type="open"><field name="create_uid" /></a> the <t t-esc="moment(record.create_date.raw_value).format('L')" /></p>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>
+217 -283
View File
@@ -1,295 +1,229 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<odoo>
<record id="challenge_list_view" model="ir.ui.view">
<field name="name">Challenges List</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<tree string="Goal definitions" decoration-info="state == 'draft'" decoration-muted="state == 'done'">
<field name="name"/>
<field name="period"/>
<field name="manager_id"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="challenge_list_view" model="ir.ui.view">
<field name="name">Challenges List</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<tree string="Goal definitions" decoration-info="state == 'draft'" decoration-muted="state == 'done'">
<field name="name"/>
<field name="period"/>
<field name="manager_id"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="goals_from_challenge_act" model="ir.actions.act_window">
<field name="res_model">gamification.goal</field>
<field name="name">Related Goals</field>
<field name="view_mode">kanban,tree</field>
<field name="context">{'search_default_group_by_definition': True, 'search_default_inprogress': True, 'search_default_challenge_id': active_id, 'default_challenge_id': active_id}</field>
<field name="help" type="html">
<p>
There is no goals associated to this challenge matching your search.
Make sure that your challenge is active and assigned to at least one user.
</p>
</field>
</record>
<record id="goals_from_challenge_act" model="ir.actions.act_window">
<field name="res_model">gamification.goal</field>
<field name="name">Related Goals</field>
<field name="view_mode">kanban,tree</field>
<field name="context">{'search_default_group_by_definition': True, 'search_default_inprogress': True, 'search_default_challenge_id': active_id, 'default_challenge_id': active_id}</field>
<field name="help" type="html">
<p>
There is no goals associated to this challenge matching your search.
Make sure that your challenge is active and assigned to at least one user.
</p>
</field>
</record>
<record id="challenge_form_view" model="ir.ui.view">
<field name="name">Challenge Form</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<form string="Goal definitions">
<header>
<button string="Start Challenge" type="object" name="action_start" states="draft" class="oe_highlight"/>
<button string="Refresh Challenge" type="object" name="action_check" states="inprogress"/>
<button string="Send Report" type="object" name="action_report_progress" states="inprogress,done" groups="base.group_no_one"/>
<field name="state" widget="statusbar" clickable="True"/>
</header>
<sheet>
<!-- action buttons -->
<div class="oe_button_box" name="button_box">
<button type="action"
name="%(goals_from_challenge_act)d"
class="oe_stat_button"
icon="fa-gift"
attrs="{'invisible': [('state','=','draft')]}">
<div class="o_form_field o_stat_info">
<span class="o_stat_text">Related</span>
<span class="o_stat_text">Goals</span>
</div>
</button>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Monthly Sales Objectives"/>
</h1>
<label for="user_domain" class="oe_edit_only" string="Assign Challenge To"/>
<div>
<field name="user_domain" widget="char_domain" options="{'model': 'res.users'}" />
<field name="user_ids" groups="base.group_no_one" widget="many2many_tags" readonly="True" />
<record id="challenge_form_view" model="ir.ui.view">
<field name="name">Challenge Form</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<form string="Goal definitions">
<header>
<button string="Start Challenge" type="object" name="action_start" states="draft" class="oe_highlight"/>
<button string="Refresh Challenge" type="object" name="action_check" states="inprogress"/>
<button string="Send Report" type="object" name="action_report_progress" states="inprogress,done" groups="base.group_no_one"/>
<field name="state" widget="statusbar" clickable="True"/>
</header>
<sheet>
<!-- action buttons -->
<div class="oe_button_box" name="button_box">
<button type="action"
name="%(goals_from_challenge_act)d"
class="oe_stat_button"
icon="fa-gift"
attrs="{'invisible': [('state','=','draft')]}">
<div class="o_form_field o_stat_info">
<span class="o_stat_text">Related</span>
<span class="o_stat_text">Goals</span>
</div>
</div>
<group>
<group>
<field name="period" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="visibility_mode" widget="radio" colspan="1" />
</group>
<group>
<field name="manager_id"/>
<field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="end_date" attrs="{'readonly':[('state','!=','draft')]}"/>
</group>
</group>
<notebook>
<page string="Goals">
<field name="line_ids" nolabel="1" colspan="4">
<tree string="Line List" editable="bottom" >
<field name="sequence" widget="handle"/>
<field name="definition_id" on_change="on_change_definition_id(definition_id)" />
<field name="condition"/>
<field name="target_goal"/>
<field name="definition_full_suffix"/>
</tree>
</field>
<field name="description" placeholder="Describe the challenge: what is does, who it targets, why it matters..."/>
</page>
<page string="Reward">
<group>
<field name="reward_id" attrs="{'required': [('reward_realtime','=', True)]}" />
<field name="reward_first_id" />
<field name="reward_second_id" attrs="{'invisible': [('reward_first_id','=', False)]}" />
<field name="reward_third_id" attrs="{'invisible': ['|',('reward_first_id','=', False),('reward_second_id','=', False)]}" />
<field name="reward_failure" attrs="{'invisible': [('reward_first_id','=', False)]}" />
<field name="reward_realtime" />
</group>
<div class="oe_grey">
<p>Badges are granted when a challenge is finished. This is either at the end of a running period (eg: end of the month for a monthly challenge), at the end date of a challenge (if no periodicity is set) or when the challenge is manually closed.</p>
</div>
</page>
<page string="Advanced Options">
<group string="Subscriptions">
<field name="invited_user_ids" widget="many2many_tags" />
</group>
<group string="Notification Messages">
<div class="oe_grey" colspan="4">
<p>Depending on the Display mode, reports will be individual or shared.</p>
</div>
<group colspan="4">
<field name="report_message_frequency"/>
<field name="report_template_id" attrs="{'invisible': [('report_message_frequency','=','never')]}" />
<field name="report_message_group_id" attrs="{'invisible': [('report_message_frequency','=','never')]}" />
</group>
</group>
<group string="Reminders for Manual Goals">
<label for="remind_update_delay" />
<div>
<field name="remind_update_delay" class="oe_inline"/> days
</div>
</group>
<group string="Category" groups="base.group_no_one">
<field name="category" widget="radio" />
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</button>
</div>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_challenge_kanban">
<field name="name">Challenge Kanban</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<kanban string="Challenges" class="oe_background_grey">
<field name="line_ids"/>
<field name="user_ids"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card o_kanban_gamification oe_kanban_global_click">
<div class="o_kanban_content">
<strong>
<h4 class="text-center"><field name="name"/></h4>
<div class="oe_kanban_project_list text-center">
<a type="action" name="%(goals_from_challenge_act)d" style="margin-right: 10px">
<span t-if="record.line_ids.raw_value.length gt 1"><t t-esc="record.line_ids.raw_value.length"/> Goals</span>
<span t-if="record.line_ids.raw_value.length lt 2"><t t-esc="record.line_ids.raw_value.length"/> Goal</span>
</a>
</div>
</strong>
<t t-foreach="record.user_ids.raw_value.slice(0,11)" t-as="member">
<img class="o_kanban_badge_avatars" t-att-src="kanban_image('res.users', 'image_small', member)" t-att-data-member_id="member"/>
</t>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="challenge_list_action" model="ir.actions.act_window">
<field name="name">Challenges</field>
<field name="res_model">gamification.challenge</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{'search_default_inprogress':True, 'default_inprogress':True}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a challenge.
</p>
<p>
Assign a list of goals to chosen users to evaluate them.
The challenge can use a period (weekly, monthly...) for automatic creation of goals.
The goals are created for the specified users or member of the group.
</p>
</field>
</record>
<!-- Specify form view ID to avoid selecting view_challenge_wizard -->
<record id="challenge_list_action_view1" model="ir.actions.act_window.view">
<field eval="1" name="sequence"/>
<field name="view_mode">kanban</field>
<field name="act_window_id" ref="challenge_list_action"/>
<field name="view_id" ref="view_challenge_kanban"/>
</record>
<record id="challenge_list_action_view2" model="ir.actions.act_window.view">
<field eval="10" name="sequence"/>
<field name="view_mode">form</field>
<field name="act_window_id" ref="challenge_list_action"/>
<field name="view_id" ref="challenge_form_view"/>
</record>
<!-- Line -->
<record id="challenge_line_list_view" model="ir.ui.view">
<field name="name">Challenge line list</field>
<field name="model">gamification.challenge.line</field>
<field name="arch" type="xml">
<tree string="Challenge Lines" >
<field name="definition_id"/>
<field name="target_goal"/>
</tree>
</field>
</record>
<record id="challenge_search_view" model="ir.ui.view">
<field name="name">Challenge Search</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<search string="Search Challenges">
<filter name="inprogress" string="Running Challenges"
domain="[('state', '=', 'inprogress')]"/>
<filter name="hr_challenges" string="HR Challenges"
domain="[('category', '=', 'hr')]"/>
<field name="name"/>
<group expand="0" string="Group By">
<filter string="State" domain="[]" context="{'group_by':'state'}"/>
<filter string="Period" domain="[]" context="{'group_by':'period'}"/>
</group>
</search>
</field>
</record>
<record id="view_challenge_wizard" model="ir.ui.view">
<field name="name">Challenge Wizard</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<form string="Challenge">
<field name="reward_failure" invisible="1"/>
<div class="oe_title">
<h1><field name="name" nolabel="1" readonly="1"/></h1>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Monthly Sales Objectives"/>
</h1>
<label for="user_domain" class="oe_edit_only" string="Assign Challenge To"/>
<div>
<field name="user_domain" widget="char_domain" options="{'model': 'res.users'}" />
<field name="user_ids" groups="base.group_no_one" widget="many2many_tags" readonly="True" />
</div>
</div>
<field name="description" nolabel="1" readonly="1" />
<group>
<field name="start_date" readonly="1" />
<field name="end_date" readonly="1" />
<field name="user_ids" string="Participating" readonly="1" widget="many2many_tags" />
<field name="invited_user_ids" string="Invited" readonly="1" widget="many2many_tags" />
</group>
<group string="Goals">
<field name="line_ids" nolabel="1" readonly="1" colspan="4">
<tree string="Challenge Lines" editable="bottom" >
<field name="sequence" widget="handle"/>
<field name="definition_id"/>
<field name="condition"/>
<field name="target_goal"/>
<field name="definition_full_suffix"/>
</tree>
</field>
</group>
<group string="Reward">
<div class="oe_grey" attrs="{'invisible': ['|',('reward_id','!=',False),('reward_first_id','!=',False)]}">
There is no reward upon completion of this challenge.
</div>
<group attrs="{'invisible': [('reward_id','=',False),('reward_first_id','=',False)]}">
<field name="reward_id" readonly="1" attrs="{'invisible': [('reward_first_id','=', False)]}" />
<field name="reward_first_id" readonly="1" attrs="{'invisible': [('reward_first_id','=', False)]}" />
<field name="reward_second_id" readonly="1" attrs="{'invisible': [('reward_second_id','=', False)]}" />
<field name="reward_third_id" readonly="1" attrs="{'invisible': [('reward_third_id','=', False)]}" />
<group>
<field name="period" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="visibility_mode" widget="radio" colspan="1" />
</group>
<group>
<field name="manager_id"/>
<field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="end_date" attrs="{'readonly':[('state','!=','draft')]}"/>
</group>
<div class="oe_grey" attrs="{'invisible': [('reward_failure','=',False)]}">
Even if the challenge is failed, best challengers will be rewarded
</div>
</group>
<footer>
<center>
<button string="Accept" type="object" name="accept_challenge" class="btn-primary" />
<button string="Reject" type="object" class="btn-default" name="discard_challenge"/>
<button string="reply later" special="cancel" class="btn-default"/>
</center>
</footer>
</form>
</field>
</record>
<notebook>
<page string="Goals">
<field name="line_ids" nolabel="1" colspan="4">
<tree string="Line List" editable="bottom" >
<field name="sequence" widget="handle"/>
<field name="definition_id" />
<field name="condition"/>
<field name="target_goal"/>
<field name="definition_full_suffix"/>
</tree>
</field>
<field name="description" placeholder="Describe the challenge: what is does, who it targets, why it matters..."/>
</page>
<page string="Reward">
<group>
<field name="reward_id" attrs="{'required': [('reward_realtime','=', True)]}" />
<field name="reward_first_id" />
<field name="reward_second_id" attrs="{'invisible': [('reward_first_id','=', False)]}" />
<field name="reward_third_id" attrs="{'invisible': ['|',('reward_first_id','=', False),('reward_second_id','=', False)]}" />
<field name="reward_failure" attrs="{'invisible': [('reward_first_id','=', False)]}" />
<field name="reward_realtime" />
</group>
<div class="oe_grey">
<p>Badges are granted when a challenge is finished. This is either at the end of a running period (eg: end of the month for a monthly challenge), at the end date of a challenge (if no periodicity is set) or when the challenge is manually closed.</p>
</div>
</page>
<page string="Advanced Options">
<group string="Subscriptions">
<field name="invited_user_ids" widget="many2many_tags" />
</group>
<group string="Notification Messages">
<div class="oe_grey" colspan="4">
<p>Depending on the Display mode, reports will be individual or shared.</p>
</div>
<group colspan="4">
<field name="report_message_frequency"/>
<field name="report_template_id" attrs="{'invisible': [('report_message_frequency','=','never')]}" />
<field name="report_message_group_id" attrs="{'invisible': [('report_message_frequency','=','never')]}" />
</group>
</group>
<group string="Reminders for Manual Goals">
<label for="remind_update_delay" />
<div>
<field name="remind_update_delay" class="oe_inline"/> days
</div>
</group>
<group string="Category" groups="base.group_no_one">
<field name="category" widget="radio" />
</group>
</page>
</notebook>
<record id="challenge_wizard" model="ir.actions.act_window">
<field name="name">Challenge Description</field>
<field name="res_model">gamification.challenge</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_challenge_wizard"/>
<field name="target">new</field>
</record>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
</data>
</openerp>
<record model="ir.ui.view" id="view_challenge_kanban">
<field name="name">Challenge Kanban</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<kanban string="Challenges" class="oe_background_grey">
<field name="line_ids"/>
<field name="user_ids"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card o_kanban_gamification oe_kanban_global_click">
<div class="o_kanban_content">
<strong>
<h4 class="text-center"><field name="name"/></h4>
<div class="oe_kanban_project_list text-center">
<a type="action" name="%(goals_from_challenge_act)d" style="margin-right: 10px">
<span t-if="record.line_ids.raw_value.length gt 1"><t t-esc="record.line_ids.raw_value.length"/> Goals</span>
<span t-if="record.line_ids.raw_value.length lt 2"><t t-esc="record.line_ids.raw_value.length"/> Goal</span>
</a>
</div>
</strong>
<t t-foreach="record.user_ids.raw_value.slice(0,11)" t-as="member">
<img class="o_kanban_badge_avatars" t-att-src="kanban_image('res.users', 'image_small', member)" t-att-data-member_id="member"/>
</t>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="challenge_list_action" model="ir.actions.act_window">
<field name="name">Challenges</field>
<field name="res_model">gamification.challenge</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{'search_default_inprogress':True, 'default_inprogress':True}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a challenge.
</p>
<p>
Assign a list of goals to chosen users to evaluate them.
The challenge can use a period (weekly, monthly...) for automatic creation of goals.
The goals are created for the specified users or member of the group.
</p>
</field>
</record>
<!-- Specify form view ID to avoid selecting view_challenge_wizard -->
<record id="challenge_list_action_view1" model="ir.actions.act_window.view">
<field eval="1" name="sequence"/>
<field name="view_mode">kanban</field>
<field name="act_window_id" ref="challenge_list_action"/>
<field name="view_id" ref="view_challenge_kanban"/>
</record>
<record id="challenge_list_action_view2" model="ir.actions.act_window.view">
<field eval="10" name="sequence"/>
<field name="view_mode">form</field>
<field name="act_window_id" ref="challenge_list_action"/>
<field name="view_id" ref="challenge_form_view"/>
</record>
<!-- Line -->
<record id="challenge_line_list_view" model="ir.ui.view">
<field name="name">Challenge line list</field>
<field name="model">gamification.challenge.line</field>
<field name="arch" type="xml">
<tree string="Challenge Lines" >
<field name="definition_id"/>
<field name="target_goal"/>
</tree>
</field>
</record>
<record id="challenge_search_view" model="ir.ui.view">
<field name="name">Challenge Search</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<search string="Search Challenges">
<filter name="inprogress" string="Running Challenges"
domain="[('state', '=', 'inprogress')]"/>
<filter name="hr_challenges" string="HR Challenges"
domain="[('category', '=', 'hr')]"/>
<field name="name"/>
<group expand="0" string="Group By">
<filter string="State" domain="[]" context="{'group_by':'state'}"/>
<filter string="Period" domain="[]" context="{'group_by':'period'}"/>
</group>
</search>
</field>
</record>
</odoo>
+7 -10
View File
@@ -1,10 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="assets_backend" name="gamification assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/gamification/static/src/less/gamification.less"/>
</xpath>
</template>
</data>
</openerp>
<odoo>
<template id="assets_backend" name="gamification assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/gamification/static/src/less/gamification.less"/>
</xpath>
</template>
</odoo>
+288 -293
View File
@@ -1,303 +1,298 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<odoo>
<!-- Goal views -->
<record id="goal_list_action" model="ir.actions.act_window">
<field name="name">Goals</field>
<field name="res_model">gamification.goal</field>
<field name="view_mode">tree,form,kanban</field>
<field name="context">{'search_default_group_by_user': True, 'search_default_group_by_definition': True}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a goal.
</p>
<p>
A goal is defined by a user and a goal definition.
Goals can be created automatically by using challenges.
</p>
</field>
</record>
<!-- Goal views -->
<record id="goal_list_action" model="ir.actions.act_window">
<field name="name">Goals</field>
<field name="res_model">gamification.goal</field>
<field name="view_mode">tree,form,kanban</field>
<field name="context">{'search_default_group_by_user': True, 'search_default_group_by_definition': True}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a goal.
</p>
<p>
A goal is defined by a user and a goal definition.
Goals can be created automatically by using challenges.
</p>
</field>
</record>
<record id="goal_list_view" model="ir.ui.view">
<field name="name">Goal List</field>
<field name="model">gamification.goal</field>
<field name="arch" type="xml">
<tree string="Goal List" decoration-danger="state == 'failed'" decoration-success="state == 'reached'" decoration-muted="state == 'canceled'" create="false">
<field name="definition_id" invisible="1" />
<field name="user_id" invisible="1" />
<field name="start_date"/>
<field name="end_date"/>
<field name="current"/>
<field name="target_goal"/>
<field name="completeness" widget="progressbar"/>
<field name="state" invisible="1"/>
<field name="line_id" invisible="1"/>
</tree>
</field>
</record>
<record id="goal_list_view" model="ir.ui.view">
<field name="name">Goal List</field>
<field name="model">gamification.goal</field>
<field name="arch" type="xml">
<tree string="Goal List" decoration-danger="state == 'failed'" decoration-success="state == 'reached'" decoration-muted="state == 'canceled'" create="false">
<field name="definition_id" invisible="1" />
<field name="user_id" invisible="1" />
<field name="start_date"/>
<field name="end_date"/>
<field name="current"/>
<field name="target_goal"/>
<field name="completeness" widget="progressbar"/>
<field name="state" invisible="1"/>
<field name="line_id" invisible="1"/>
</tree>
</field>
</record>
<record id="goal_form_view" model="ir.ui.view">
<field name="name">Goal Form</field>
<field name="model">gamification.goal</field>
<field name="arch" type="xml">
<form string="Goal" create="false">
<header>
<button string="Start goal" type="object" name="action_start" states="draft" class="oe_highlight"/>
<record id="goal_form_view" model="ir.ui.view">
<field name="name">Goal Form</field>
<field name="model">gamification.goal</field>
<field name="arch" type="xml">
<form string="Goal" create="false">
<header>
<button string="Start goal" type="object" name="action_start" states="draft" class="oe_highlight"/>
<button string="Goal Reached" type="object" name="action_reach" states="inprogress" />
<button string="Goal Failed" type="object" name="action_fail" states="inprogress"/>
<button string="Reset Completion" type="object" name="action_cancel" states="failed,reached" groups="base.group_no_one" />
<field name="state" widget="statusbar" statusbar_visible="draft,inprogress,reached" />
</header>
<sheet>
<group>
<group string="Reference">
<field name="definition_id" on_change="on_change_definition_id(definition_id)" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="user_id" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="challenge_id" />
</group>
<group string="Schedule">
<field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="end_date" />
<field name="computation_mode" invisible="1"/>
<label for="remind_update_delay" attrs="{'invisible':[('computation_mode','!=', 'manually')]}"/>
<div attrs="{'invisible':[('computation_mode','!=', 'manually')]}">
<field name="remind_update_delay" class="oe_inline"/>
days
</div>
<field name="last_update" groups="base.group_no_one"/>
</group>
<group string="Data" colspan="4">
<label for="target_goal" />
<div>
<field name="target_goal" attrs="{'readonly':[('state','!=','draft')]}" class="oe_inline"/>
<field name="definition_suffix" class="oe_inline"/>
</div>
<label for="current" />
<div>
<field name="current" class="oe_inline"/>
<button string="refresh" type="object" name="update_goal" class="oe_link" attrs="{'invisible':['|',('computation_mode', '=', 'manually'),('state', '=', 'draft')]}" />
<div class="oe_grey" attrs="{'invisible':[('definition_id', '=', False)]}">
Reached when current value is <strong><field name="definition_condition" class="oe_inline"/></strong> than the target.
</div>
</div>
</group>
<button string="Goal Reached" type="object" name="action_reach" states="inprogress" />
<button string="Goal Failed" type="object" name="action_fail" states="inprogress"/>
<button string="Reset Completion" type="object" name="action_cancel" states="failed,reached" groups="base.group_no_one" />
<field name="state" widget="statusbar" statusbar_visible="draft,inprogress,reached" />
</header>
<sheet>
<group>
<group string="Reference">
<field name="definition_id" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="user_id" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="challenge_id" />
</group>
</sheet>
</form>
</field>
</record>
<group string="Schedule">
<field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="end_date" />
<field name="computation_mode" invisible="1"/>
<record id="goal_search_view" model="ir.ui.view">
<field name="name">Goal Search</field>
<field name="model">gamification.goal</field>
<field name="arch" type="xml">
<search string="Search Goals">
<filter name="my" string="My Goals" domain="[('user_id', '=', uid)]"/>
<separator/>
<filter name="draft" string="Draft" domain="[('state', '=', 'draft')]"/>
<filter name="inprogress" string="Running"
domain="[
'|',
('state', '=', 'inprogress'),
'&amp;',
('state', 'in', ('done', 'failed')),
('end_date', '>=', context_today().strftime('%%Y-%%m-%%d'))
]"/>
<filter name="closed" string="Done"
domain="[
('state', 'in', ('reached', 'failed')),
'|',
('end_date', '=', False),
('end_date', '&lt;', context_today().strftime('%%Y-%%m-%%d'))
]"/>
<separator/>
<field name="user_id"/>
<field name="definition_id"/>
<field name="challenge_id"/>
<group expand="0" string="Group By">
<filter name="group_by_user" string="User" domain="[]" context="{'group_by':'user_id'}"/>
<filter name="group_by_definition" string="Goal Definition" domain="[]" context="{'group_by':'definition_id'}"/>
<filter string="State" domain="[]" context="{'group_by':'state'}"/>
<filter string="End Date" domain="[]" context="{'group_by':'end_date'}"/>
</group>
</search>
</field>
</record>
<record id="goal_kanban_view" model="ir.ui.view" >
<field name="name">Goal Kanban View</field>
<field name="model">gamification.goal</field>
<field name="arch" type="xml">
<kanban class="oe_background_grey">
<field name="definition_id"/>
<field name="user_id"/>
<field name="current"/>
<field name="completeness"/>
<field name="state"/>
<field name="target_goal"/>
<field name="definition_condition"/>
<field name="definition_suffix"/>
<field name="definition_display"/>
<field name="start_date"/>
<field name="end_date"/>
<field name="last_update"/>
<templates>
<t t-name="kanban-tooltip">
<field name="definition_description"/>
</t>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card o_kanban_gamification #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}">
<div class="o_kanban_content">
<p><strong><h4 class="oe_goal_name text-center" tooltip="kanban-tooltip"><field name="definition_id" /></h4></strong></p>
<div class="pull-left">
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" width="24" height="24" />
</div>
<field name="user_id" />
<div class="o_goal_state_block">
<t t-if="record.definition_display.raw_value == 'boolean'">
<div class="o_goal_state text-center">
<t t-if="record.state.raw_value=='reached'"><i class="o_green fa fa-check fa-3x" title="Goal Reached"/></t>
<t t-if="record.state.raw_value=='inprogress'"><i class="fa fa-clock-o fa-3x" title="Goal in Progress"/></t>
<t t-if="record.state.raw_value=='failed'"><i class="o_red fa fa-times fa-3x" title="Goal Failed"/></t>
</div>
</t>
<t t-if="record.definition_display.raw_value == 'progress'">
<t t-if="record.definition_condition.raw_value =='higher'">
<field name="current" widget="gauge" style="width:160px; height: 120px;" options="{'max_field': 'target_goal', 'label_field': 'definition_suffix'}" />
</t>
<t t-if="record.definition_condition.raw_value != 'higher'">
<div t-attf-class="o_goal_state #{record.current.raw_value == record.target_goal.raw_value+1 ? 'o_orange' : record.current.raw_value &gt; record.target_goal.raw_value ? 'o_red' : 'o_green'}">
<t t-esc="record.current.raw_value" />
</div>
<em>Target: less than <t t-esc="record.target_goal.raw_value" /></em>
</t>
</t>
</div>
<p class="text-center">
<t t-if="record.start_date.value">
From <t t-esc="record.start_date.value" />
</t>
<t t-if="record.end_date.value">
To <t t-esc="record.end_date.value" />
</t>
</p>
<label for="remind_update_delay" attrs="{'invisible':[('computation_mode','!=', 'manually')]}"/>
<div attrs="{'invisible':[('computation_mode','!=', 'manually')]}">
<field name="remind_update_delay" class="oe_inline"/>
days
</div>
<field name="last_update" groups="base.group_no_one"/>
</group>
<group string="Data" colspan="4">
<label for="target_goal" />
<div>
<field name="target_goal" attrs="{'readonly':[('state','!=','draft')]}" class="oe_inline"/>
<field name="definition_suffix" class="oe_inline"/>
</div>
<label for="current" />
<div>
<field name="current" class="oe_inline"/>
<button string="refresh" type="object" name="update_goal" class="oe_link" attrs="{'invisible':['|',('computation_mode', '=', 'manually'),('state', '=', 'draft')]}" />
<div class="oe_grey" attrs="{'invisible':[('definition_id', '=', False)]}">
Reached when current value is <strong><field name="definition_condition" class="oe_inline"/></strong> than the target.
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- Goal definitions view -->
<record id="goal_definition_list_action" model="ir.actions.act_window">
<field name="name">Goal Definitions</field>
<field name="res_model">gamification.goal.definition</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a goal definition.
</p>
<p>
A goal definition is a technical model of goal defining a condition to reach.
The dates, values to reach or users are defined in goal instance.
</p>
</field>
</record>
<record id="goal_definition_list_view" model="ir.ui.view">
<field name="name">Goal Definitions List</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<tree string="Goal Definitions">
<field name="name"/>
<field name="computation_mode"/>
</tree>
</field>
</record>
<record id="goal_definition_form_view" model="ir.ui.view">
<field name="name">Goal Definitions Form</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<form string="Goal definitions">
<sheet>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
<label for="description" class="oe_edit_only"/>
<div>
<field name="description" class="oe_inline"/>
</div>
<group string="How to compute the goal?">
<field widget="radio" name="computation_mode"/>
<!-- Hide the fields below if manually -->
<field name="model_id" class="oe_inline" on_change="on_change_model_id(model_id)"
attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}"/>
<field name="field_id" class="oe_inline"
attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}"/>
<field name="field_date_id" class="oe_inline" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))]}"/>
<field name="domain" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
<field name="compute_code" attrs="{'invisible':[('computation_mode','!=','python')], 'required':[('computation_mode','=','python')]}" placeholder="e.g. result = pool.get('mail.followers').search(cr, uid, [('res_model', '=', 'mail.channel'), ('partner_id', '=', object.user_id.partner_id.id)], count=True, context=context)"/>
<field name="condition" widget="radio"/>
</group>
<group string="Optimisation" attrs="{'invisible': [('computation_mode', '!=', 'count')]}">
<field name="batch_mode" />
<div colspan="4">In batch mode, the domain is evaluated globally. If enabled, do not use keyword 'user' in above filter domain.</div>
<field name="batch_distinctive_field" attrs="{'invisible': [('batch_mode', '=', False)], 'required': [('batch_mode', '=', True)]}"
domain="[('model_id', '=', model_id)]" class="oe_inline" />
<field name="batch_user_expression" attrs="{'invisible': [('batch_mode', '=', False)], 'required': [('batch_mode', '=', True)]}" class="oe_inline"
placeholder="e.g. user.partner_id.id"/>
</group>
<group string="Formating Options">
<field name="display_mode" widget="radio" />
<field name="suffix" placeholder="e.g. days" class="oe_inline"/>
<field name="monetary"/>
</group>
<group string="Clickable Goals" attrs="{'invisible': [('computation_mode', '=', 'manually')]}">
<field name="action_id" class="oe_inline"/>
<field name="res_id_field" attrs="{'invisible': [('action_id', '=', False)]}" class="oe_inline"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="goal_definition_search_view" model="ir.ui.view">
<field name="name">Goal Definition Search</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<search string="Search Goal Definitions">
<field name="name"/>
<field name="model_id"/>
<field name="field_id"/>
<group expand="0" string="Group By">
<filter string="Model" domain="[]" context="{'group_by':'model_id'}"/>
<filter string="Computation Mode" domain="[]" context="{'group_by':'computation_mode'}"/>
</group>
</group>
</search>
</field>
</record>
</sheet>
</form>
</field>
</record>
<!-- menus in settings - technical feature required -->
<menuitem id="gamification_menu" name="Gamification Tools" parent="base.menu_administration" groups="base.group_no_one" />
<menuitem id="gamification_challenge_menu" parent="gamification_menu" action="challenge_list_action" sequence="0"/>
<menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="10"/>
<menuitem id="gamification_definition_menu" parent="gamification_menu" action="goal_definition_list_action" sequence="20"/>
<menuitem id="gamification_badge_menu" parent="gamification_menu" action="badge_list_action" sequence="30"/>
<record id="goal_search_view" model="ir.ui.view">
<field name="name">Goal Search</field>
<field name="model">gamification.goal</field>
<field name="arch" type="xml">
<search string="Search Goals">
<filter name="my" string="My Goals" domain="[('user_id', '=', uid)]"/>
<separator/>
<filter name="draft" string="Draft" domain="[('state', '=', 'draft')]"/>
<filter name="inprogress" string="Running"
domain="[
'|',
('state', '=', 'inprogress'),
'&amp;',
('state', 'in', ('done', 'failed')),
('end_date', '>=', context_today().strftime('%%Y-%%m-%%d'))
]"/>
<filter name="closed" string="Done"
domain="[
('state', 'in', ('reached', 'failed')),
'|',
('end_date', '=', False),
('end_date', '&lt;', context_today().strftime('%%Y-%%m-%%d'))
]"/>
<separator/>
</data>
</openerp>
<field name="user_id"/>
<field name="definition_id"/>
<field name="challenge_id"/>
<group expand="0" string="Group By">
<filter name="group_by_user" string="User" domain="[]" context="{'group_by':'user_id'}"/>
<filter name="group_by_definition" string="Goal Definition" domain="[]" context="{'group_by':'definition_id'}"/>
<filter string="State" domain="[]" context="{'group_by':'state'}"/>
<filter string="End Date" domain="[]" context="{'group_by':'end_date'}"/>
</group>
</search>
</field>
</record>
<record id="goal_kanban_view" model="ir.ui.view" >
<field name="name">Goal Kanban View</field>
<field name="model">gamification.goal</field>
<field name="arch" type="xml">
<kanban class="oe_background_grey">
<field name="definition_id"/>
<field name="user_id"/>
<field name="current"/>
<field name="completeness"/>
<field name="state"/>
<field name="target_goal"/>
<field name="definition_condition"/>
<field name="definition_suffix"/>
<field name="definition_display"/>
<field name="start_date"/>
<field name="end_date"/>
<field name="last_update"/>
<templates>
<t t-name="kanban-tooltip">
<field name="definition_description"/>
</t>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card o_kanban_gamification #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}">
<div class="o_kanban_content">
<p><strong><h4 class="oe_goal_name text-center" tooltip="kanban-tooltip"><field name="definition_id" /></h4></strong></p>
<div class="pull-left">
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" width="24" height="24" />
</div>
<field name="user_id" />
<div class="o_goal_state_block">
<t t-if="record.definition_display.raw_value == 'boolean'">
<div class="o_goal_state text-center">
<t t-if="record.state.raw_value=='reached'"><i class="o_green fa fa-check fa-3x" title="Goal Reached"/></t>
<t t-if="record.state.raw_value=='inprogress'"><i class="fa fa-clock-o fa-3x" title="Goal in Progress"/></t>
<t t-if="record.state.raw_value=='failed'"><i class="o_red fa fa-times fa-3x" title="Goal Failed"/></t>
</div>
</t>
<t t-if="record.definition_display.raw_value == 'progress'">
<t t-if="record.definition_condition.raw_value =='higher'">
<field name="current" widget="gauge" style="width:160px; height: 120px;" options="{'max_field': 'target_goal', 'label_field': 'definition_suffix'}" />
</t>
<t t-if="record.definition_condition.raw_value != 'higher'">
<div t-attf-class="o_goal_state #{record.current.raw_value == record.target_goal.raw_value+1 ? 'o_orange' : record.current.raw_value &gt; record.target_goal.raw_value ? 'o_red' : 'o_green'}">
<t t-esc="record.current.raw_value" />
</div>
<em>Target: less than <t t-esc="record.target_goal.raw_value" /></em>
</t>
</t>
</div>
<p class="text-center">
<t t-if="record.start_date.value">
From <t t-esc="record.start_date.value" />
</t>
<t t-if="record.end_date.value">
To <t t-esc="record.end_date.value" />
</t>
</p>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- Goal definitions view -->
<record id="goal_definition_list_action" model="ir.actions.act_window">
<field name="name">Goal Definitions</field>
<field name="res_model">gamification.goal.definition</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a goal definition.
</p>
<p>
A goal definition is a technical model of goal defining a condition to reach.
The dates, values to reach or users are defined in goal instance.
</p>
</field>
</record>
<record id="goal_definition_list_view" model="ir.ui.view">
<field name="name">Goal Definitions List</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<tree string="Goal Definitions">
<field name="name"/>
<field name="computation_mode"/>
</tree>
</field>
</record>
<record id="goal_definition_form_view" model="ir.ui.view">
<field name="name">Goal Definitions Form</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<form string="Goal definitions">
<sheet>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
<label for="description" class="oe_edit_only"/>
<div>
<field name="description" class="oe_inline"/>
</div>
<group string="How to compute the goal?">
<field widget="radio" name="computation_mode"/>
<!-- Hide the fields below if manually -->
<field name="model_id" class="oe_inline"
attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}"/>
<field name="field_id" class="oe_inline"
attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}"/>
<field name="field_date_id" class="oe_inline" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))]}"/>
<field name="domain" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
<field name="compute_code" attrs="{'invisible':[('computation_mode','!=','python')], 'required':[('computation_mode','=','python')]}" placeholder="e.g. result = pool.get('mail.followers').search(cr, uid, [('res_model', '=', 'mail.channel'), ('partner_id', '=', object.user_id.partner_id.id)], count=True, context=context)"/>
<field name="condition" widget="radio"/>
</group>
<group string="Optimisation" attrs="{'invisible': [('computation_mode', '!=', 'count')]}">
<field name="batch_mode" />
<div colspan="4">In batch mode, the domain is evaluated globally. If enabled, do not use keyword 'user' in above filter domain.</div>
<field name="batch_distinctive_field" attrs="{'invisible': [('batch_mode', '=', False)], 'required': [('batch_mode', '=', True)]}"
domain="[('model_id', '=', model_id)]" class="oe_inline" />
<field name="batch_user_expression" attrs="{'invisible': [('batch_mode', '=', False)], 'required': [('batch_mode', '=', True)]}" class="oe_inline"
placeholder="e.g. user.partner_id.id"/>
</group>
<group string="Formating Options">
<field name="display_mode" widget="radio" />
<field name="suffix" placeholder="e.g. days" class="oe_inline"/>
<field name="monetary"/>
</group>
<group string="Clickable Goals" attrs="{'invisible': [('computation_mode', '=', 'manually')]}">
<field name="action_id" class="oe_inline"/>
<field name="res_id_field" attrs="{'invisible': [('action_id', '=', False)]}" class="oe_inline"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="goal_definition_search_view" model="ir.ui.view">
<field name="name">Goal Definition Search</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<search string="Search Goal Definitions">
<field name="name"/>
<field name="model_id"/>
<field name="field_id"/>
<group expand="0" string="Group By">
<filter string="Model" domain="[]" context="{'group_by':'model_id'}"/>
<filter string="Computation Mode" domain="[]" context="{'group_by':'computation_mode'}"/>
</group>
</search>
</field>
</record>
<!-- menus in settings - technical feature required -->
<menuitem id="gamification_menu" name="Gamification Tools" parent="base.menu_administration" groups="base.group_no_one" />
<menuitem id="gamification_challenge_menu" parent="gamification_menu" action="challenge_list_action" sequence="0"/>
<menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="10"/>
<menuitem id="gamification_definition_menu" parent="gamification_menu" action="goal_definition_list_action" sequence="20"/>
<menuitem id="gamification_badge_menu" parent="gamification_menu" action="badge_list_action" sequence="30"/>
</odoo>
+15 -19
View File
@@ -1,38 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp.exceptions import UserError
from odoo import api, fields, models, _, exceptions
class grant_badge_wizard(osv.TransientModel):
class grant_badge_wizard(models.TransientModel):
""" Wizard allowing to grant a badge to a user"""
_name = 'gamification.badge.user.wizard'
_columns = {
'user_id': fields.many2one("res.users", string='User', required=True),
'badge_id': fields.many2one("gamification.badge", string='Badge', required=True),
'comment': fields.text('Comment'),
}
def action_grant_badge(self, cr, uid, ids, context=None):
user_id = fields.Many2one("res.users", string='User', required=True)
badge_id = fields.Many2one("gamification.badge", string='Badge', required=True)
comment = fields.Text('Comment')
@api.multi
def action_grant_badge(self):
"""Wizard action for sending a badge to a chosen user"""
badge_user_obj = self.pool.get('gamification.badge.user')
BadgeUser = self.env['gamification.badge.user']
for wiz in self.browse(cr, uid, ids, context=context):
uid = self.env.uid
for wiz in self:
if uid == wiz.user_id.id:
raise UserError(_('You can not grant a badge to yourself'))
raise exceptions.UserError(_('You can not grant a badge to yourself'))
#create the badge
values = {
BadgeUser.create({
'user_id': wiz.user_id.id,
'sender_id': uid,
'badge_id': wiz.badge_id.id,
'comment': wiz.comment,
}
badge_user = badge_user_obj.create(cr, uid, values, context=context)
result = badge_user_obj._send_badge(cr, uid, badge_user, context=context)
})._send_badge()
return result
return True
+30 -32
View File
@@ -1,33 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<odoo>
<record id="view_badge_wizard_grant" model="ir.ui.view">
<field name="name">Grant Badge User Form</field>
<field name="model">gamification.badge.user.wizard</field>
<field name="arch" type="xml">
<form string="Grant Badge To">
Who would you like to reward?
<field name="badge_id" invisible="1"/>
<group>
<field name="user_id" nolabel="1" colspan="4"/>
<field name="comment" nolabel="1" placeholder="Describe what they did and why it matters (will be public)" colspan="4"/>
</group>
<footer>
<button string="Grant Badge" type="object" name="action_grant_badge" class="btn-primary" />
<button string="Cancel" special="cancel" class="btn-default"/>
</footer>
</form>
</field>
</record>
<record id="view_badge_wizard_grant" model="ir.ui.view">
<field name="name">Grant Badge User Form</field>
<field name="model">gamification.badge.user.wizard</field>
<field name="arch" type="xml">
<form string="Grant Badge To">
Who would you like to reward?
<field name="badge_id" invisible="1"/>
<group>
<field name="user_id" nolabel="1" colspan="4"/>
<field name="comment" nolabel="1" placeholder="Describe what they did and why it matters (will be public)" colspan="4"/>
</group>
<footer>
<button string="Grant Badge" type="object" name="action_grant_badge" class="btn-primary" />
<button string="Cancel" special="cancel" class="btn-default"/>
</footer>
</form>
</field>
</record>
<act_window domain="[]" id="action_grant_wizard"
name="Grant Badge"
target="new"
res_model="gamification.badge.user.wizard"
context="{'default_badge_id': active_id, 'badge_id': active_id}"
view_type="form" view_mode="form"
view_id="gamification.view_badge_wizard_grant" />
</data>
</openerp>
<record id="action_grant_wizard" model="ir.actions.act_window">
<field name="name">Grant Badge</field>
<field name="res_model">gamification.badge.user.wizard</field>
<field name="view_id" ref="gamification.view_badge_wizard_grant"/>
<field name="target">new</field>
<field name="context">{
'default_badge_id': active_id,
'badge_id': active_id
}</field>
</record>
</odoo>
+13 -16
View File
@@ -1,27 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
from odoo import api, models, fields
class goal_manual_wizard(osv.TransientModel):
class goal_manual_wizard(models.TransientModel):
"""Wizard to update a manual goal"""
_name = 'gamification.goal.wizard'
_columns = {
'goal_id': fields.many2one("gamification.goal", string='Goal', required=True),
'current': fields.float('Current'),
}
def action_update_current(self, cr, uid, ids, context=None):
goal_id = fields.Many2one("gamification.goal", string='Goal', required=True)
current = fields.Float('Current')
@api.multi
def action_update_current(self):
"""Wizard action for updating the current value"""
goal_obj = self.pool.get('gamification.goal')
for wiz in self.browse(cr, uid, ids, context=context):
towrite = {
for wiz in self:
wiz.goal_id.write({
'current': wiz.current,
'goal_id': wiz.goal_id.id,
'to_update': False,
}
goal_obj.write(cr, uid, [wiz.goal_id.id], towrite, context=context)
goal_obj.update_goal(cr, uid, [wiz.goal_id.id], context=context)
return {}
})
wiz.goal_id.update_goal()
return False
+19 -24
View File
@@ -1,24 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="view_goal_wizard_update_current" model="ir.ui.view">
<field name="name">Update the current value of the Goal</field>
<field name="model">gamification.goal.wizard</field>
<field name="arch" type="xml">
<form string="Grant Badge To">
Set the current value you have reached for this goal
<group>
<field name="goal_id" invisible="1"/>
<field name="current" />
</group>
<footer>
<button string="Update" type="object" name="action_update_current" class="btn-primary" />
<button string="Cancel" special="cancel" class="btn-default"/>
</footer>
</form>
</field>
</record>
</data>
</openerp>
<odoo>
<record id="view_goal_wizard_update_current" model="ir.ui.view">
<field name="name">Update the current value of the Goal</field>
<field name="model">gamification.goal.wizard</field>
<field name="arch" type="xml">
<form string="Grant Badge To">
Set the current value you have reached for this goal
<group>
<field name="goal_id" invisible="1"/>
<field name="current" />
</group>
<footer>
<button string="Update" type="object" name="action_update_current" class="btn-primary" />
<button string="Cancel" special="cancel" class="btn-default"/>
</footer>
</form>
</field>
</record>
</odoo>
@@ -1,5 +1,4 @@
<?xml version="1.0"?>
<openerp>
<odoo>
<data noupdate="1">
<!-- goal definitions -->
@@ -120,7 +119,7 @@
<field name="name">Monthly Sales Targets</field>
<field name="period">monthly</field>
<field name="visibility_mode">ranking</field>
<field name="user_domain" eval="[('groups_id', 'in', ref('sales_team.group_sale_salesman'))]" />
<field name="user_domain" eval="str([('groups_id', 'in', ref('sales_team.group_sale_salesman'))])" />
<field name="report_message_frequency">weekly</field>
</record>
@@ -128,7 +127,7 @@
<field name="name">Lead Acquisition</field>
<field name="period">monthly</field>
<field name="visibility_mode">ranking</field>
<field name="user_domain" eval="[('groups_id', 'in', ref('sales_team.group_sale_salesman'))]" />
<field name="user_domain" eval="str([('groups_id', 'in', ref('sales_team.group_sale_salesman'))])" />
<field name="report_message_frequency">weekly</field>
</record>
@@ -160,4 +159,4 @@
</record>
</data>
</openerp>
</odoo>
@@ -1,11 +1,6 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- challenges -->
<record model="gamification.challenge" id="challenge_crm_sale">
<field name="state">inprogress</field>
</record>
</data>
</openerp>
<odoo>
<!-- challenges -->
<record model="gamification.challenge" id="challenge_crm_sale">
<field name="state">inprogress</field>
</record>
</odoo>
+2 -7
View File
@@ -3,15 +3,10 @@
from openerp import models, fields, api
class gamification_challenge(models.Model):
class Challenge(models.Model):
_inherit = 'gamification.challenge'
@api.model
def _get_categories(self):
res = super(gamification_challenge, self)._get_categories()
res.append(('forum', 'Website / Forum'))
return res
category = fields.Selection(selection_add=[('forum', 'Website / Forum')])
class Badge(models.Model):
_inherit = 'gamification.badge'