[MERGE] [ADD] gamification

New modules gamification, hr_gamification, gamification_sale_crm, portal_gamification.

Merry Christmas OpenERP

bzr revid: mat@openerp.com-20131224155138-3bbp8m3gyrfsb3ni
This commit is contained in:
Martin Trigaux
2013-12-24 16:51:38 +01:00
53 changed files with 5757 additions and 2 deletions
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import models
import wizard
+59
View File
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Gamification',
'version': '1.0',
'author': 'OpenERP SA',
'category': 'Human Ressources',
'depends': ['mail', 'email_template', 'web_kanban_gauge'],
'description': """
Gamification process
====================
The Gamification module provides ways to evaluate and motivate the users of OpenERP.
The users can be evaluated using goals and numerical objectives to reach.
**Goals** are assigned through **challenges** to evaluate and compare members of a team with each others and through time.
For non-numerical achievements, **badges** can be granted to users. From a simple "thank you" to an exceptional achievement, a badge is an easy way to exprimate gratitude to a user for their good work.
Both goals and badges are flexibles and can be adapted to a large range of modules and actions. When installed, this module creates easy goals to help new users to discover OpenERP and configure their user profile.
""",
'data': [
'wizard/update_goal.xml',
'wizard/grant_badge.xml',
'views/badge.xml',
'views/challenge.xml',
'views/goal.xml',
'data/cron.xml',
'security/gamification_security.xml',
'security/ir.model.access.csv',
'data/goal_base.xml',
'data/badge.xml',
],
'installable': True,
'application': True,
'auto_install': False,
'css': ['static/src/css/gamification.css'],
'js': ['static/src/js/gamification.js',],
'qweb': ['static/src/xml/gamification.xml'],
}
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record forcecreate="True" id="ir_cron_check_challenge"
model="ir.cron">
<field name="name">Run Goal Challenge Checker</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
<field name="model">gamification.challenge</field>
<field name="function">_cron_update</field>
<field name="args">()</field>
</record>
</data>
</openerp>
+208
View File
@@ -0,0 +1,208 @@
<?xml version="1.0"?>
<openerp>
<!-- Mail template is done in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="0">
<!--Email template -->
<record id="email_template_goal_reminder" model="email.template">
<field name="name">Reminder for Goal Update</field>
<field name="body_html"><![CDATA[
<header>
<strong>Reminder ${object.name}</strong>
</header>
<p>You have not updated your progress for the goal ${object.definition_id.name} (currently reached at ${object.completeness}%) for at least ${object.remind_update_delay} days. Do not forget to do it.</p>
<p>If you have not changed your score yet, you can use the button "The current value is up to date" to indicate so.</p>
]]></field>
</record>
<record id="simple_report_template" model="email.template">
<field name="name">Simple Challenge Report Progress</field>
<field name="body_html"><![CDATA[
<header>
<strong>${object.name}</strong>
</header>
<p class="oe_grey">The following message contains the current progress for the challenge ${object.name}</p>
% if object.visibility_mode == 'personal':
<table width="100%" border="1">
<tr>
<th>Goal</th>
<th>Target</th>
<th>Current</th>
<th>Completeness</th>
</tr>
% for line in ctx["challenge_lines"]:
<tr
% if line['completeness'] >= 100:
style="font-weight:bold;"
% endif
>
<td>${line['name']}</td>
<td>${line['target']}
% if line['suffix']:
${line['suffix']}
% endif
</td>
<td>${line['current']}
% if line['suffix']:
${line['suffix']}
% endif
</td>
<td>${line['completeness']} %</td>
</tr>
% endfor
</table>
% else:
% for line in ctx["challenge_lines"]:
<table width="100%" border="1">
<tr>
<th colspan="4">${line['name']}</th>
</tr>
<tr>
<th>#</th>
<th>Person</th>
<th>Completeness</th>
<th>Current</th>
</tr>
% for goal in line['goals']:
<tr
% if goal.completeness >= 100:
style="font-weight:bold;"
% endif
>
<td>${goal['rank']}</td>
<td>${goal['name']}</td>
<td>${goal['completeness']}%</td>
<td>${goal['current']}/${line['target']}
% if line['suffix']:
${line['suffix']}
% endif
</td>
</tr>
% endfor
</table>
<br/><br/>
% endfor
% endif
]]></field>
</record>
</data>
<data>
<!-- goal definitions -->
<record model="gamification.goal.definition" id="definition_base_timezone">
<field name="name">Set your Timezone</field>
<field name="description">Configure your profile and specify your timezone</field>
<field name="computation_mode">count</field>
<field name="display_mode">boolean</field>
<field name="model_id" eval="ref('base.model_res_users')" />
<field name="domain">[('id','=',user.id),('partner_id.tz', '!=', False)]</field>
<field name="action_id" eval="ref('base.action_res_users_my')" />
<field name="res_id_field">user.id</field>
</record>
<record model="gamification.goal.definition" id="definition_base_company_data">
<field name="name">Set your Company Data</field>
<field name="description">Write some information about your company (specify at least a name)</field>
<field name="computation_mode">count</field>
<field name="display_mode">boolean</field>
<field name="model_id" eval="ref('base.model_res_company')" />
<field name="domain">[('user_ids', 'in', [user.id]), ('name', '=', 'Your Company')]</field>
<field name="condition">lower</field>
<field name="action_id" eval="ref('base.action_res_company_form')" />
<field name="res_id_field">user.company_id.id</field>
</record>
<record model="gamification.goal.definition" id="definition_base_company_logo">
<field name="name">Set your Company Logo</field>
<field name="computation_mode">count</field>
<field name="display_mode">boolean</field>
<field name="model_id" eval="ref('base.model_res_company')" />
<field name="domain">[('user_ids', 'in', user.id),('logo', '!=', False)]</field>
<field name="action_id" eval="ref('base.action_res_company_form')" />
<field name="res_id_field">user.company_id.id</field>
</record>
<record id="action_new_simplified_res_users" model="ir.actions.act_window">
<field name="name">Create User</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.users</field>
<field name="view_type">form</field>
<field name="target">current</field>
<field name="view_id" ref="base.view_users_simple_form"/>
<field name="context">{'default_groups_ref': ['base.group_user']}</field>
<field name="help">Create and manage users that will connect to the system. Users can be deactivated should there be a period of time during which they will/should not connect to the system. You can assign them groups in order to give them specific access to the applications they need to use in the system.</field>
</record>
<record model="gamification.goal.definition" id="definition_base_invite">
<field name="name">Invite new Users</field>
<field name="description">Create at least another user</field>
<field name="display_mode">boolean</field>
<field name="computation_mode">count</field>
<field name="model_id" eval="ref('base.model_res_users')" />
<field name="domain">[('id', '!=', user.id)]</field>
<field name="action_id" eval="ref('action_new_simplified_res_users')" />
</record>
<record model="gamification.goal.definition" id="definition_nbr_following">
<field name="name">Mail Group Following</field>
<field name="description">Follow mail groups to receive news</field>
<field name="computation_mode">python</field>
<field name="compute_code">result = pool.get('mail.followers').search(cr, uid, [('res_model', '=', 'mail.group'), ('partner_id', '=', object.user_id.partner_id.id)], count=True, context=context)</field>
<field name="action_id" eval="ref('mail.action_view_groups')" />
</record>
<!-- challenges -->
<record model="gamification.challenge" id="challenge_base_discover">
<field name="name">Complete your Profile</field>
<field name="period">once</field>
<field name="visibility_mode">personal</field>
<field name="report_message_frequency">never</field>
<field name="autojoin_group_id" eval="ref('base.group_user')" />
<field name="state">inprogress</field>
<field name="category">other</field>
</record>
<record model="gamification.challenge" id="challenge_base_configure">
<field name="name">Setup your Company</field>
<field name="period">once</field>
<field name="visibility_mode">personal</field>
<field name="report_message_frequency">never</field>
<field name="user_ids" eval="[(4, ref('base.user_root'))]" />
<field name="state">inprogress</field>
<field name="category">other</field>
</record>
<!-- lines -->
<record model="gamification.challenge.line" id="line_base_discover1">
<field name="definition_id" eval="ref('definition_base_timezone')" />
<field name="target_goal">1</field>
<field name="challenge_id" eval="ref('challenge_base_discover')" />
</record>
<record model="gamification.challenge.line" id="line_base_admin2">
<field name="definition_id" eval="ref('definition_base_company_logo')" />
<field name="target_goal">1</field>
<field name="challenge_id" eval="ref('challenge_base_configure')" />
</record>
<record model="gamification.challenge.line" id="line_base_admin1">
<field name="definition_id" eval="ref('definition_base_company_data')" />
<field name="target_goal">0</field>
<field name="challenge_id" eval="ref('challenge_base_configure')" />
</record>
<record model="gamification.challenge.line" id="line_base_admin3">
<field name="definition_id" eval="ref('definition_base_invite')" />
<field name="target_goal">1</field>
<field name="challenge_id" eval="ref('challenge_base_configure')" />
</record>
</data>
</openerp>
+86
View File
@@ -0,0 +1,86 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Drive Engagement with Gamification</h2>
<h3 class="oe_slogan">Leverage natural desire for competition</h3>
<p class="oe_mt32">
Reinforce good habits and improve win rates with real-time recognition and rewards inspired by <a href="http://en.wikipedia.org/wiki/Gamification">game mechanics</a>. Align teams around clear business objectives with challenges, personal objectives and team leader boards.
</p>
<div class="oe_span4 oe_centered">
<h3>Leaderboards</h3>
<div class="oe_row_img oe_centered">
<img class="oe_picture" src="crm_game_01.png">
</div>
<p>
Promote leaders and competition amongst sales team with performance ratios.
</p>
</div>
<div class="oe_span4 oe_centered">
<h3>Personnal Objectives</h3>
<div class="oe_row_img">
<img class="oe_picture" src="crm_game_02.png">
</div>
<p>
Assign clear goals to users to align them with the company objectives.
</p>
</div>
<div class="oe_span4 oe_centered">
<h3>Visual Information</h3>
<div class="oe_row_img oe_centered">
<img class="oe_picture" src="crm_game_03.png">
</div>
<p>
See in an glance the progress of each user.
</p>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Create custom Challenges</h2>
<div class="oe_span6">
<p class="oe_mt32">
Use predefined goals to generate easily your own challenges. Assign it to a team or individual users. Receive feedback as often as needed: daily, weekly... Repeat it automatically to compare progresses through time.
</p>
</div>
<div class="oe_span6">
<div class="oe_row_img oe_centered">
<img class="oe_picture oe_screenshot" src="crm_sc_05.png">
</div>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Motivate with Badges</h2>
<div class="oe_span6">
<div class="oe_row_img oe_centered">
<img class="oe_picture" src="crm_linkedin.png">
</div>
</div>
<div class="oe_span6">
<p class="oe_mt32">
Inspire achievement with recognition of coworker's good work by rewarding badges. These can be deserved manually or upon completion of challenges. Add fun to the competition with rare badges.
</p>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Adapt to any module</h2>
<div class="oe_span6">
<p class="oe_mt32">
Create goals linked to any module. The evaluation system is very flexible and can be used for many different tasks : sales evaluation, creation of events, project completion or even helping new users to complete their profile.
</p>
</div>
<div class="oe_span6">
<div class="oe_row_img oe_centered">
<img class="oe_picture oe_screenshot" src="crm_sc_02.png">
</div>
</div>
</div>
</section>
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import goal
import challenge
import res_users
import badge
+276
View File
@@ -0,0 +1,276 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
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
_logger = logging.getLogger(__name__)
class gamification_badge_user(osv.Model):
"""User having received a badge"""
_name = 'gamification.badge.user'
_description = 'Gamification user badge'
_order = "create_date desc"
_columns = {
'user_id': fields.many2one('res.users', string="User", required=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),
'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),
}
def _send_badge(self, cr, uid, ids, context=None):
"""Send a notification to a user for receiving a badge
Does not verify constrains on badge granting.
The users are added to the owner_ids (create badge_user if needed)
The stats counters are incremented
:param ids: list(int) of badge users that will receive the badge
"""
res = True
temp_obj = self.pool.get('email.template')
user_obj = self.pool.get('res.users')
template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context)
for badge_user in self.browse(cr, uid, ids, context=context):
body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=context)
res = user_obj.message_post(cr, uid, badge_user.user_id.id, body=body_html, context=context)
return res
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)
class gamification_badge(osv.Model):
"""Badge object that users can send and receive"""
CAN_GRANT = 1
NOBODY_CAN_GRANT = 2
USER_NOT_VIP = 3
BADGE_REQUIRED = 4
TOO_MANY = 5
_name = 'gamification.badge'
_description = 'Gamification badge'
_inherit = ['mail.thread']
def _get_owners_info(self, cr, uid, ids, name, args, context=None):
"""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.fromkeys(ids, False)
for obj in self.browse(cr, uid, ids, context=context):
res = list(set(owner.user_id.id for owner in obj.owner_ids))
result[obj.id] = {
'unique_owner_ids': res,
'stat_count': len(obj.owner_ids),
'stat_count_distinct': len(res)
}
return result
def _get_badge_user_stats(self, cr, uid, ids, name, args, context=None):
"""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
def _remaining_sending_calc(self, cr, uid, ids, name, args, context=None):
"""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:
# if the user cannot grant this badge at all, result is 0
result[badge.id] = 0
elif not badge.rule_max:
# if there is no limitation, -1 is returned which means 'infinite'
result[badge.id] = -1
else:
result[badge.id] = badge.rule_max_number - badge.stat_my_monthly_sending
return result
_columns = {
'name': fields.char('Badge', required=True, translate=True),
'description': fields.text('Description'),
'image': fields.binary("Image", 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',
'rel_badge_badge', '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='unique_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='unique_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):
"""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)
if status_code == self.CAN_GRANT:
return True
elif status_code == self.NOBODY_CAN_GRANT:
raise osv.except_osv(_('Warning!'), _('This badge can not be sent by users.'))
elif status_code == self.USER_NOT_VIP:
raise osv.except_osv(_('Warning!'), _('You are not in the user allowed list.'))
elif status_code == self.BADGE_REQUIRED:
raise osv.except_osv(_('Warning!'), _('You do not have the required badges.'))
elif status_code == self.TOO_MANY:
raise osv.except_osv(_('Warning!'), _('You have already sent this badge too many time this month.'))
else:
_logger.exception("Unknown badge status code: %d" % int(status_code))
return False
def _can_grant_badge(self, cr, uid, badge_id, context=None):
"""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:
return self.CAN_GRANT
badge = self.browse(cr, uid, badge_id, context=context)
if badge.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]:
return self.USER_NOT_VIP
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:
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 = template_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
+821
View File
@@ -0,0 +1,821 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
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, datetime, timedelta
import calendar
import logging
_logger = logging.getLogger(__name__)
# display top 3 in ranking, could be db variable
MAX_VISIBILITY_RANKING = 3
def start_end_date_for_period(period, default_start_date=False, default_end_date=False):
"""Return the start and end date for a goal period based on today
:return: (start_date, end_date), datetime.date objects, False if the period is
not defined or unknown"""
today = date.today()
if period == 'daily':
start_date = today
end_date = start_date
elif period == 'weekly':
delta = timedelta(days=today.weekday())
start_date = today - delta
end_date = start_date + timedelta(days=7)
elif period == 'monthly':
month_range = calendar.monthrange(today.year, today.month)
start_date = today.replace(day=1)
end_date = today.replace(day=month_range[1])
elif period == 'yearly':
start_date = today.replace(month=1, day=1)
end_date = today.replace(month=12, day=31)
else: # period == 'once':
start_date = default_start_date # for manual goal, start each time
end_date = default_end_date
if start_date and end_date:
return (start_date.strftime(DF), end_date.strftime(DF))
else:
return (start_date, end_date)
class gamification_challenge(osv.Model):
"""Gamification challenge
Set of predifined objectives assigned to people with rules for recurrence and
rewards
If 'user_ids' is defined and 'period' is different than 'one', the set will
be assigned to the users for each period (eg: every 1st of each month if
'monthly' is selected)
"""
_name = 'gamification.challenge'
_description = 'Gamification challenge'
_inherit = 'mail.thread'
def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
"""Return the next report date based on the last report date and report
period.
:return: a string in DEFAULT_SERVER_DATE_FORMAT representing the date"""
res = {}
for challenge in self.browse(cr, uid, ids, context):
last = datetime.strptime(challenge.last_report_date, DF).date()
if challenge.report_message_frequency == 'daily':
next = last + timedelta(days=1)
res[challenge.id] = next.strftime(DF)
elif challenge.report_message_frequency == 'weekly':
next = last + timedelta(days=7)
res[challenge.id] = next.strftime(DF)
elif challenge.report_message_frequency == 'monthly':
month_range = calendar.monthrange(last.year, last.month)
next = last.replace(day=month_range[1]) + timedelta(days=1)
res[challenge.id] = next.strftime(DF)
elif challenge.report_message_frequency == 'yearly':
res[challenge.id] = last.replace(year=last.year + 1).strftime(DF)
# frequency == 'once', reported when closed only
else:
res[challenge.id] = False
return res
def _get_categories(self, cr, uid, context=None):
return [
('hr', 'Human Ressources / Engagement'),
('other', 'Settings / Gamification Tools'),
]
def _get_report_template(self, cr, uid, context=None):
try:
return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'simple_report_template')[1]
except ValueError:
return False
_order = 'end_date, start_date, name, id'
_columns = {
'name': fields.char('Challenge Name', required=True, translate=True),
'description': fields.text('Description', translate=True),
'state': fields.selection([
('draft', 'Draft'),
('inprogress', 'In Progress'),
('done', 'Done'),
],
string='State', required=True, track_visibility='onchange'),
'manager_id': fields.many2one('res.users',
string='Responsible', help="The user responsible for the challenge."),
'user_ids': fields.many2many('res.users', 'user_ids',
string='Users',
help="List of users participating to the challenge"),
'autojoin_group_id': fields.many2one('res.groups',
string='Auto-subscription Group',
help='Group of users whose members will be automatically added to user_ids once the challenge is started'),
'period': fields.selection([
('once', 'Non recurring'),
('daily', 'Daily'),
('weekly', 'Weekly'),
('monthly', 'Monthly'),
('yearly', 'Yearly')
],
string='Periodicity',
help='Period of automatic goal assigment. If none is selected, should be launched manually.',
required=True),
'start_date': fields.date('Start Date',
help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
'end_date': fields.date('End Date',
help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
'invited_user_ids': fields.many2many('res.users', 'invited_user_ids',
string="Suggest to users"),
'line_ids': fields.one2many('gamification.challenge.line', 'challenge_id',
string='Lines',
help="List of goals that will be set",
required=True),
'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"),
'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"),
'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"),
'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"),
'reward_failure': fields.boolean('Reward Bests if not Succeeded?'),
'visibility_mode': fields.selection([
('personal', 'Individual Goals'),
('ranking', 'Leader Board (Group Ranking)'),
],
string="Display Mode", required=True),
'report_message_frequency': fields.selection([
('never', 'Never'),
('onchange', 'On change'),
('daily', 'Daily'),
('weekly', 'Weekly'),
('monthly', 'Monthly'),
('yearly', 'Yearly')
],
string="Report Frequency", required=True),
'report_message_group_id': fields.many2one('mail.group',
string='Send a copy to',
help='Group that will receive a copy of the report in addition to the user'),
'report_template_id': fields.many2one('email.template', string="Report Template", required=True),
'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after',
help="Never reminded if no value or zero is specified."),
'last_report_date': fields.date('Last Report Date'),
'next_report_date': fields.function(_get_next_report_date,
type='date', string='Next Report Date', store=True),
'category': fields.selection(lambda s, *a, **k: s._get_categories(*a, **k),
string="Appears in", help="Define the visibility of the challenge through menus", required=True),
}
_defaults = {
'period': 'once',
'state': 'draft',
'visibility_mode': 'personal',
'report_message_frequency': 'never',
'last_report_date': fields.date.today,
'start_date': fields.date.today,
'manager_id': lambda s, cr, uid, c: uid,
'category': 'hr',
'reward_failure': False,
'report_template_id': lambda s, *a, **k: s._get_report_template(*a, **k),
}
def create(self, cr, uid, vals, context=None):
"""Overwrite the create method to add the user of groups"""
# add users when change the group auto-subscription
if vals.get('autojoin_group_id'):
new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
if not vals.get('user_ids'):
vals['user_ids'] = []
vals['user_ids'] += [(4, user.id) for user in new_group.users]
create_res = super(gamification_challenge, self).create(cr, uid, vals, context=context)
# subscribe new users to the challenge
if vals.get('user_ids'):
# done with browse after super to be sure catch all after orm process
challenge = self.browse(cr, uid, create_res, context=context)
self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
return create_res
def write(self, cr, uid, ids, vals, context=None):
if isinstance(ids, (int,long)):
ids = [ids]
# add users when change the group auto-subscription
if vals.get('autojoin_group_id'):
new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
if not vals.get('user_ids'):
vals['user_ids'] = []
vals['user_ids'] += [(4, user.id) for user in new_group.users]
if vals.get('state') == 'inprogress':
# starting a challenge
if not vals.get('autojoin_group_id'):
# starting challenge, add users in autojoin group
if not vals.get('user_ids'):
vals['user_ids'] = []
for challenge in self.browse(cr, uid, ids, context=context):
if challenge.autojoin_group_id:
vals['user_ids'] += [(4, user.id) for user in challenge.autojoin_group_id.users]
self.generate_goals_from_challenge(cr, uid, ids, context=context)
elif vals.get('state') == 'done':
self.check_challenge_reward(cr, uid, ids, force=True, context=context)
elif vals.get('state') == 'draft':
# resetting progress
if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', 'in', ['inprogress', 'inprogress_update'])], context=context):
raise osv.except_osv("Error", "You can not reset a challenge with unfinished goals.")
write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
# subscribe new users to the challenge
if vals.get('user_ids'):
# done with browse after super if changes in groups
for challenge in self.browse(cr, uid, ids, context=context):
self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
return write_res
##### Update #####
def _cron_update(self, cr, uid, context=None, ids=False):
"""Daily cron check.
- Start planned challenges (in draft and with start_date = today)
- Create the missing goals (eg: modified the challenge to add lines)
- Update every running challenge
"""
# start planned challenges
planned_challenge_ids = self.search(cr, uid, [
('state', '=', 'draft'),
('start_date', '<=', fields.date.today())])
self.write(cr, uid, planned_challenge_ids, {'state': 'inprogress'}, context=context)
# close planned challenges
planned_challenge_ids = self.search(cr, uid, [
('state', '=', 'inprogress'),
('end_date', '>=', fields.date.today())])
self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context)
if not ids:
ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
return self._update_all(cr, uid, ids, context=context)
def _update_all(self, cr, uid, ids, context=None):
"""Update the challenges and related goals
:param list(int) ids: the ids of the challenges to update, if False will
update only challenges in progress."""
if isinstance(ids, (int,long)):
ids = [ids]
goal_obj = self.pool.get('gamification.goal')
# we use yesterday to update the goals that just ended
yesterday = date.today() - timedelta(days=1)
goal_ids = goal_obj.search(cr, uid, [
('challenge_id', 'in', ids),
'|',
('state', 'in', ('inprogress', 'inprogress_update')),
'&',
('state', 'in', ('reached', 'failed')),
'|',
('end_date', '>=', yesterday.strftime(DF)),
('end_date', '=', False)
], context=context)
# update every running goal already generated linked to selected challenges
goal_obj.update(cr, uid, goal_ids, context=context)
for challenge in self.browse(cr, uid, ids, context=context):
if challenge.autojoin_group_id:
# check in case of new users in challenge, this happens if manager removed users in challenge manually
self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context)
self.generate_goals_from_challenge(cr, uid, [challenge.id], context=context)
# goals closed but still opened at the last report date
closed_goals_to_report = goal_obj.search(cr, uid, [
('challenge_id', '=', challenge.id),
('start_date', '>=', challenge.last_report_date),
('end_date', '<=', challenge.last_report_date)
])
if len(closed_goals_to_report) > 0:
# some goals need a final report
self.report_progress(cr, uid, challenge, subset_goal_ids=closed_goals_to_report, context=context)
if fields.date.today() == challenge.next_report_date:
self.report_progress(cr, uid, challenge, context=context)
self.check_challenge_reward(cr, uid, ids, context=context)
return True
def quick_update(self, cr, uid, challenge_id, context=None):
"""Update all the goals of a challenge, no generation of new goals"""
goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', '=', challenge_id)], context=context)
self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context)
return True
def action_check(self, cr, uid, ids, context=None):
"""Check a challenge
Create goals that haven't been created yet (eg: if added users)
Recompute the current value for each goal related"""
return self._update_all(cr, uid, ids=ids, context=context)
def action_report_progress(self, cr, uid, ids, context=None):
"""Manual report of a goal, does not influence automatic report frequency"""
if isinstance(ids, (int,long)):
ids = [ids]
for challenge in self.browse(cr, uid, ids, context):
self.report_progress(cr, uid, challenge, context=context)
return True
##### Automatic actions #####
def generate_goals_from_challenge(self, cr, uid, ids, context=None):
"""Generate the goals for each line and user.
If goals already exist for this line and user, the line is skipped. This
can be called after each change in the list of users or lines.
:param list(int) ids: the list of challenge concerned"""
for challenge in self.browse(cr, uid, ids, context):
(start_date, end_date) = start_end_date_for_period(challenge.period)
# if no periodicity, use challenge dates
if not start_date and challenge.start_date:
start_date = challenge.start_date
if not end_date and challenge.end_date:
end_date = challenge.end_date
for line in challenge.line_ids:
for user in challenge.user_ids:
goal_obj = self.pool.get('gamification.goal')
domain = [('line_id', '=', line.id), ('user_id', '=', user.id)]
if start_date:
domain.append(('start_date', '=', start_date))
# goal already existing for this line ?
if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
# resume canceled goals
domain.append(('state', '=', 'canceled'))
canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
goal_obj.update(cr, uid, canceled_goal_ids, context=context)
# skip to next user
continue
values = {
'definition_id': line.definition_id.id,
'line_id': line.id,
'user_id': user.id,
'target_goal': line.target_goal,
'state': 'inprogress',
}
if start_date:
values['start_date'] = start_date
if end_date:
values['end_date'] = end_date
if challenge.remind_update_delay:
values['remind_update_delay'] = challenge.remind_update_delay
new_goal_id = goal_obj.create(cr, uid, values, context)
goal_obj.update(cr, uid, [new_goal_id], context=context)
return True
##### JS utilities #####
def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
"""Return a serialised version of the goals information
:challenge: browse record of challenge to compute
:user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
:restrict_goal_ids: <list(int)> compute only the results for this subset if gamification.goal ids, if False retrieve every goal of current running challenge
:restrict_top: <int> for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
restrict_goal_ids has priority over restrict_top
format list
# if visibility_mode == 'ranking'
{
'name': <gamification.goal.description name>,
'description': <gamification.goal.description description>,
'condition': <reach condition {lower,higher}>,
'computation_mode': <target computation {manually,count,sum,python}>,
'monetary': <{True,False}>,
'suffix': <value suffix>,
'action': <{True,False}>,
'display_mode': <{progress,boolean}>,
'target': <challenge line target>,
'own_goal_id': <gamification.goal id where user_id == uid>,
'goals': [
{
'id': <gamification.goal id>,
'rank': <user ranking>,
'user_id': <res.users id>,
'name': <res.users name>,
'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
'completeness': <percentage>,
'current': <current value>,
}
]
},
# if visibility_mode == 'personal'
{
'id': <gamification.goal id>,
'name': <gamification.goal.description name>,
'description': <gamification.goal.description description>,
'condition': <reach condition {lower,higher}>,
'computation_mode': <target computation {manually,count,sum,python}>,
'monetary': <{True,False}>,
'suffix': <value suffix>,
'action': <{True,False}>,
'display_mode': <{progress,boolean}>,
'target': <challenge line target>,
'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
'completeness': <percentage>,
'current': <current value>,
}
"""
goal_obj = self.pool.get('gamification.goal')
(start_date, end_date) = start_end_date_for_period(challenge.period)
res_lines = []
for line in challenge.line_ids:
line_data = {
'name': line.definition_id.name,
'description': line.definition_id.description,
'condition': line.definition_id.condition,
'computation_mode': line.definition_id.computation_mode,
'monetary': line.definition_id.monetary,
'suffix': line.definition_id.suffix,
'action': True if line.definition_id.action_id else False,
'display_mode': line.definition_id.display_mode,
'target': line.target_goal,
}
domain = [
('line_id', '=', line.id),
('state', '!=', 'draft'),
]
if restrict_goal_ids:
domain.append(('ids', 'in', restrict_goal_ids))
else:
# if no subset goals, use the dates for restriction
if start_date:
domain.append(('start_date', '=', start_date))
if end_date:
domain.append(('end_date', '=', end_date))
if challenge.visibility_mode == 'personal':
if not user_id:
raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information"))
domain.append(('user_id', '=', user_id))
sorting = goal_obj._order
limit = 1
# initialise in case search returns no results
line_data.update({
'id': 0,
'current': 0,
'completeness': 0,
'state': 'draft',
})
else:
line_data.update({
'own_goal_id': False,
'goals': [],
})
sorting = "completeness desc, current desc"
limit = False
goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
ranking = 0
for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
if challenge.visibility_mode == 'personal':
# limit=1 so only one result
line_data.update({
'id': goal.id,
'current': goal.current,
'completeness': goal.completeness,
'state': goal.state,
})
else:
ranking += 1
if user_id and goal.user_id.id == user_id:
line_data['own_goal_id'] = goal.id
elif restrict_top and ranking > restrict_top:
# not own goal, over top, skipping
continue
line_data['goals'].append({
'id': goal.id,
'user_id': goal.user_id.id,
'name': goal.user_id.name,
'rank': ranking,
'current': goal.current,
'completeness': goal.completeness,
'state': goal.state,
})
res_lines.append(line_data)
return res_lines
##### Reporting #####
def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
"""Post report about the progress of the goals
:param challenge: the challenge object that need to be reported
:param users: the list(res.users) of users that are concerned by
the report. If False, will send the report to every user concerned
(goal users and group that receive a copy). Only used for challenge with
a visibility mode set to 'personal'.
:param goal_ids: the list(int) of goal ids linked to the challenge for
the report. If not specified, use the goals for the current challenge
period. This parameter can be used to produce report for previous challenge
periods.
:param subset_goal_ids: a list(int) of goal ids to restrict the report
"""
if context is None:
context = {}
temp_obj = self.pool.get('email.template')
ctx = context.copy()
if challenge.visibility_mode == 'ranking':
lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
ctx.update({'challenge_lines': lines_boards})
body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
# send to every follower of the challenge
self.message_post(cr, uid, challenge.id,
body=body_html,
context=context,
subtype='mail.mt_comment')
if challenge.report_message_group_id:
self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
body=body_html,
context=context,
subtype='mail.mt_comment')
else:
# generate individual reports
for user in users or challenge.user_ids:
goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
if not goals:
continue
ctx.update({'challenge_lines': goals})
body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
# send message only to users, not on the challenge
self.message_post(cr, uid, 0,
body=body_html,
partner_ids=[(4, user.partner_id.id)],
context=context,
subtype='mail.mt_comment')
if challenge.report_message_group_id:
self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
body=body_html,
context=context,
subtype='mail.mt_comment')
return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
##### Challenges #####
def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
"""The user accept the suggested challenge"""
user_id = user_id or uid
user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
message = "%s has joined the challenge" % user.name
self.message_post(cr, uid, challenge_ids, body=message, context=context)
self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
return self.generate_goals_from_challenge(cr, uid, challenge_ids, context=context)
def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
"""The user discard the suggested challenge"""
user_id = user_id or uid
user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
message = "%s has refused the challenge" % user.name
self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
return self.write(cr, uid, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
id = result and result[1] or False
result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
result['res_id'] = challenge_id
return result
def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
"""Actions for the end of a challenge
If a reward was selected, grant it to the correct users.
Rewards granted at:
- the end date for a challenge with no periodicity
- the end of a period for challenge with periodicity
- when a challenge is manually closed
(if no end date, a running challenge is never rewarded)
"""
if isinstance(ids, (int,long)):
ids = [ids]
context = context or {}
for challenge in self.browse(cr, uid, ids, context=context):
(start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
yesterday = date.today() - timedelta(days=1)
if end_date == yesterday.strftime(DF) or force:
# open chatter message
message_body = _("The challenge %s is finished." % challenge.name)
# reward for everybody succeeding
rewarded_users = []
if challenge.reward_id:
for user in challenge.user_ids:
reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [
('challenge_id', '=', challenge.id),
('user_id', '=', user.id),
('start_date', '=', start_date),
('end_date', '=', end_date),
('state', '=', 'reached')
], context=context)
if len(reached_goal_ids) == len(challenge.line_ids):
self.reward_user(cr, uid, user.id, challenge.reward_id.id, context)
rewarded_users.append(user)
if rewarded_users:
message_body += _("<br/>Reward (badge %s) for every succeeding user was sent to %s." % (challenge.reward_id.name, ", ".join([user.name for user in rewarded_users])))
else:
message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
# reward bests
if challenge.reward_first_id:
(first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context)
if first_user:
self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, context)
message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
message_body += "<br/> 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
else:
message_body += _("Nobody reached the required conditions to receive special badges.")
if second_user and challenge.reward_second_id:
self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, context)
message_body += "<br/> 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
if third_user and challenge.reward_third_id:
self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, context)
message_body += "<br/> 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
self.message_post(cr, uid, challenge.id, body=message_body, context=context)
return True
def get_top3_users(self, cr, uid, challenge, context=None):
"""Get the top 3 users for a defined challenge
Ranking criterias:
1. succeed every goal of the challenge
2. total completeness of each goal (can be over 100)
Top 3 is computed only for users succeeding every goal of the challenge,
except if reward_failure is True, in which case every user is
considered.
:return: ('first', 'second', 'third'), tuple containing the res.users
objects of the top 3 users. If no user meets the criterias for a rank,
it is set to False. Nobody can receive a rank is noone receives the
higher one (eg: if 'second' == False, 'third' will be False)
"""
goal_obj = self.pool.get('gamification.goal')
(start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
challengers = []
for user in challenge.user_ids:
all_reached = True
total_completness = 0
# every goal of the user for the running period
goal_ids = goal_obj.search(cr, uid, [
('challenge_id', '=', challenge.id),
('user_id', '=', user.id),
('start_date', '=', start_date),
('end_date', '=', end_date)
], context=context)
for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
if goal.state != 'reached':
all_reached = False
if goal.definition_condition == 'higher':
# can be over 100
total_completness += 100.0 * goal.current / goal.target_goal
elif goal.state == 'reached':
# for lower goals, can not get percentage so 0 or 100
total_completness += 100
challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
# nobody succeeded
return (False, False, False)
if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
# only one user succeeded
return (sorted_challengers[0]['user'], False, False)
if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
# only one user succeeded
return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
def reward_user(self, cr, uid, user_id, badge_id, context=None):
"""Create a badge user and send the badge to him
:param user_id: the user to reward
:param badge_id: the concerned badge
"""
badge_user_obj = self.pool.get('gamification.badge.user')
user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context)
return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
class gamification_challenge_line(osv.Model):
"""Gamification challenge line
Predifined goal for 'gamification_challenge'
These are generic list of goals with only the target goal defined
Should only be created for the gamification_challenge object
"""
_name = 'gamification.challenge.line'
_description = 'Gamification generic goal for challenge'
_order = "sequence, id"
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)
ret = {
'value': {
'condition': goal_definition.condition,
'definition_full_suffix': goal_definition.full_suffix
}
}
return ret
_columns = {
'name': fields.related('definition_id', 'name', string="Name"),
'challenge_id': fields.many2one('gamification.challenge',
string='Challenge',
required=True,
ondelete="cascade"),
'definition_id': fields.many2one('gamification.goal.definition',
string='Goal Definition',
required=True,
ondelete="cascade"),
'target_goal': fields.float('Target Value to Reach',
required=True),
'sequence': fields.integer('Sequence',
help='Sequence number for ordering'),
'condition': fields.related('definition_id', 'condition', type="selection",
readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),
}
_default = {
'sequence': 1,
}
+394
View File
@@ -0,0 +1,394 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
from openerp.tools.safe_eval import safe_eval
from openerp.tools.translate import _
import logging
import time
from datetime import date, datetime, timedelta
_logger = logging.getLogger(__name__)
class gamification_goal_definition(osv.Model):
"""Goal definition
A goal definition contains the way to evaluate an objective
Each module wanting to be able to set goals to the users needs to create
a new gamification_goal_definition
"""
_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
_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. The rule can contain reference to 'user' that is a browse record of the current user, e.g. [('user_id', '=', user.id)].",
required=True),
'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.")
}
_defaults = {
'condition': 'higher',
'computation_mode': 'manually',
'domain': "[]",
'monetary': False,
'display_mode': 'progress',
}
def number_following(self, cr, uid, model_name="mail.thread", context=None):
"""Return the number of 'model_name' objects the user is following
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)
class gamification_goal(osv.Model):
"""Goal instance for a user
An individual goal for a user on a specified time period"""
_name = 'gamification.goal'
_description = 'Gamification goal instance'
_inherit = 'mail.thread'
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),
'line_id': fields.many2one('gamification.challenge.line', string='Goal Line', ondelete="cascade"),
'challenge_id': fields.related('line_id', 'challenge_id',
string="Challenge",
type='many2one',
relation='gamification.challenge',
store=True),
'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'),
('inprogress_update', 'In progress (to update)'),
('reached', 'Reached'),
('failed', 'Failed'),
('canceled', 'Canceled'),
],
string='State',
required=True,
track_visibility='always'),
'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 = 'create_date desc, end_date desc, definition_id, id'
def _check_remind_delay(self, cr, uid, goal, context=None):
"""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
"""
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 and goal.state == 'inprogress':
# generate a remind report
temp_obj = self.pool.get('email.template')
template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context)
body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context)
self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment')
return {'state': 'inprogress_update'}
return {}
def update(self, cr, uid, ids, context=None):
"""Update the goals to recomputes values and change of states
If a manual goal is not updated for enough time, the user will be
reminded to do so (done only once, in 'inprogress' state).
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 = {}
for goal in self.browse(cr, uid, ids, context=context):
towrite = {}
if goal.state in ('draft', 'canceled'):
# skip if goal draft or canceled
continue
if goal.definition_id.computation_mode == 'manually':
towrite.update(self._check_remind_delay(cr, uid, goal, context))
elif goal.definition_id.computation_mode == 'python':
# execute the chosen method
cxt = {
'self': self.pool.get('gamification.goal'),
'object': goal,
'pool': self.pool,
'cr': cr,
'context': dict(context), # copy context to prevent side-effects of eval
'uid': uid,
'result': False,
'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time
}
code = goal.definition_id.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', False)
if result and type(result) in (float, int, long):
if result != goal.current:
towrite['current'] = result
else:
_logger.exception(_('Invalid return content from the evaluation of %s' % code))
else: # count or sum
obj = self.pool.get(goal.definition_id.model_id.model)
field_date_name = goal.definition_id.field_date_id.name
# eval the domain with user replaced by goal user object
domain = safe_eval(goal.definition_id.domain, {'user': goal.user_id})
# add temporal clause(s) to the domain if fields are filled on the goal
if goal.start_date and field_date_name:
domain.append((field_date_name, '>=', goal.start_date))
if goal.end_date and field_date_name:
domain.append((field_date_name, '<=', goal.end_date))
if goal.definition_id.computation_mode == 'sum':
field_name = goal.definition_id.field_id.name
res = obj.read_group(cr, uid, domain, [field_name], [''], context=context)
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)
# avoid useless write if the new value is the same as the old one
if new_value != goal.current:
towrite['current'] = new_value
# check goal target reached
if (goal.definition_condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.definition_condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal):
towrite['state'] = 'reached'
# check goal failure
elif goal.end_date and fields.date.today() > goal.end_date:
towrite['state'] = 'failed'
if towrite:
self.write(cr, uid, [goal.id], towrite, context=context)
return True
def action_start(self, cr, uid, ids, context=None):
"""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(cr, uid, ids, context=context)
def action_reach(self, cr, uid, ids, context=None):
"""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)
def action_fail(self, cr, uid, ids, context=None):
"""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)
def action_cancel(self, cr, uid, ids, context=None):
"""Reset the completion after setting a goal as reached or failed.
This is only the current state, if the date and/or target criterias
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)
def create(self, cr, uid, vals, context=None):
"""Overwrite the create method to add a 'no_remind_goal' field to True"""
if context is None:
context = {}
context['no_remind_goal'] = True
return super(gamification_goal, self).create(cr, uid, vals, context=context)
def write(self, cr, uid, ids, vals, context=None):
"""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):
if goal.state != "draft" and ('definition_id' in vals or 'user_id' in vals):
# avoid drag&drop in kanban view
raise osv.except_osv(_('Error!'), _('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)
return result
def get_action(self, cr, uid, goal_id, context=None):
"""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
"""
goal = self.browse(cr, uid, goal_id, context=context)
if goal.definition_id.action_id:
# open a the action linked to the goal
action = goal.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 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']
return action
if goal.computation_mode == 'manually':
# open a wizard window to update the value manually
action = {
'name': _("Update %s") % goal.definition_id.name,
'id': goal_id,
'type': 'ir.actions.act_window',
'views': [[False, 'form']],
'target': 'new',
'context': {'default_goal_id': goal_id, 'default_current': goal.current},
'res_model': 'gamification.goal.wizard'
}
return action
return False
+132
View File
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.osv import osv
from challenge import MAX_VISIBILITY_RANKING
class res_users_gamification_group(osv.Model):
""" Update of res.users class
- if adding groups to an user, check gamification.challenge linked to
this group, and the user. This is done by overriding the write method.
"""
_name = 'res.users'
_inherit = ['res.users']
def write(self, cr, uid, ids, vals, context=None):
"""Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge"""
write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context)
if vals.get('groups_id'):
# form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
challenge_obj = self.pool.get('gamification.challenge')
challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context)
if challenge_ids:
challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context)
return write_res
def create(self, cr, uid, vals, context=None):
"""Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge"""
write_res = super(res_users_gamification_group, self).create(cr, uid, vals, context=context)
if vals.get('groups_id'):
# form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
challenge_obj = self.pool.get('gamification.challenge')
challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context)
if challenge_ids:
challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, write_res)]}, context=context)
return write_res
# def get_goals_todo_info(self, cr, uid, context=None):
def get_serialised_gamification_summary(self, cr, uid, context=None):
return self._serialised_goals_summary(cr, uid, user_id=uid, context=context)
def _serialised_goals_summary(self, cr, uid, user_id, context=None):
"""Return a serialised list of goals assigned to the user, grouped by challenge
[
{
'id': <gamification.challenge id>,
'name': <gamification.challenge name>,
'visibility_mode': <visibility {ranking,personal}>,
'currency': <res.currency id>,
'lines': [(see gamification_challenge._get_serialized_challenge_lines() format)]
},
]
"""
all_goals_info = []
challenge_obj = self.pool.get('gamification.challenge')
user = self.browse(cr, uid, uid, context=context)
challenge_ids = challenge_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context):
# serialize goals info to be able to use it in javascript
all_goals_info.append({
'id': challenge.id,
'name': challenge.name,
'visibility_mode': challenge.visibility_mode,
'currency': user.company_id.currency_id.id,
'lines': challenge_obj._get_serialized_challenge_lines(cr, uid, challenge, user_id, restrict_top=MAX_VISIBILITY_RANKING, context=context),
})
return all_goals_info
def get_challenge_suggestions(self, cr, uid, context=None):
"""Return the list of challenges suggested to the user"""
challenge_info = []
challenge_obj = self.pool.get('gamification.challenge')
challenge_ids = challenge_obj.search(cr, uid, [('invited_user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context):
values = {
'id': challenge.id,
'name': challenge.name,
'description': challenge.description,
}
challenge_info.append(values)
return challenge_info
class res_groups_gamification_group(osv.Model):
""" Update of res.groups class
- if adding users from a group, check gamification.challenge linked to
this group, and the user. This is done by overriding the write method.
"""
_name = 'res.groups'
_inherit = 'res.groups'
# No need to overwrite create as very unlikely to be the value in the autojoin_group_id field
def write(self, cr, uid, ids, vals, context=None):
"""Overwrite to autosubscribe users if add users to a group marked as autojoin, these will be added to the challenge"""
write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context)
if vals.get('users'):
# form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
user_ids = [command[1] for command in vals['users'] if command[0] == 4]
user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]]
challenge_obj = self.pool.get('gamification.challenge')
challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', ids)], context=context)
if challenge_ids:
challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context)
return write_res
@@ -0,0 +1,43 @@
<?xml version="1.0" ?>
<openerp>
<data noupdate="1">
<record model="ir.module.category" id="module_goal_category">
<field name="name">Gamification</field>
<field name="description"></field>
<field name="sequence">17</field>
</record>
<record id="group_goal_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="module_goal_category"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
<record id="goal_user_visibility" model="ir.rule">
<field name="name">User can only see his/her goals or goal from the same challenge in board visibility</field>
<field name="model_id" ref="model_gamification_goal"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="domain_force">[
'|',
('user_id','=',user.id),
'&amp;',
('challenge_id.user_ids','in',user.id),
('challenge_id.visibility_mode','=','ranking')]</field>
</record>
<record id="goal_gamification_manager_visibility" model="ir.rule">
<field name="name">Gamification Manager can see any goal</field>
<field name="model_id" ref="model_gamification_goal"/>
<field name="groups" eval="[(4, ref('group_goal_manager'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
</data>
</openerp>
@@ -0,0 +1,19 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
goal_employee,"Goal Employee",model_gamification_goal,base.group_user,1,1,0,0
goal_manager,"Goal Manager",model_gamification_goal,group_goal_manager,1,1,1,1
goal_definition_employee,"Goal Definition Employee",model_gamification_goal_definition,base.group_user,1,0,0,0
goal_definition_manager,"Goal Definition Manager",model_gamification_goal_definition,group_goal_manager,1,1,1,1
challenge_employee,"Goal Challenge Employee",model_gamification_challenge,base.group_user,1,0,0,0
challenge_manager,"Goal Challenge Manager",model_gamification_challenge,group_goal_manager,1,1,1,1
challenge_line_employee,"Challenge Line Employee",model_gamification_challenge_line,base.group_user,1,0,0,0
challenge_line_manager,"Challenge Line Manager",model_gamification_challenge_line,group_goal_manager,1,1,1,1
badge_employee,"Badge Employee",model_gamification_badge,base.group_user,1,0,0,0
badge_manager,"Badge Manager",model_gamification_badge,group_goal_manager,1,1,1,1
badge_user_employee,"Badge-user Employee",model_gamification_badge_user,base.group_user,1,1,1,0
badge_user_manager,"Badge-user Manager",model_gamification_badge_user,group_goal_manager,1,1,1,1
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 goal_employee Goal Employee model_gamification_goal base.group_user 1 1 0 0
3 goal_manager Goal Manager model_gamification_goal group_goal_manager 1 1 1 1
4 goal_definition_employee Goal Definition Employee model_gamification_goal_definition base.group_user 1 0 0 0
5 goal_definition_manager Goal Definition Manager model_gamification_goal_definition group_goal_manager 1 1 1 1
6 challenge_employee Goal Challenge Employee model_gamification_challenge base.group_user 1 0 0 0
7 challenge_manager Goal Challenge Manager model_gamification_challenge group_goal_manager 1 1 1 1
8 challenge_line_employee Challenge Line Employee model_gamification_challenge_line base.group_user 1 0 0 0
9 challenge_line_manager Challenge Line Manager model_gamification_challenge_line group_goal_manager 1 1 1 1
10 badge_employee Badge Employee model_gamification_badge base.group_user 1 0 0 0
11 badge_manager Badge Manager model_gamification_badge group_goal_manager 1 1 1 1
12 badge_user_employee Badge-user Employee model_gamification_badge_user base.group_user 1 1 1 0
13 badge_user_manager Badge-user Manager model_gamification_badge_user group_goal_manager 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,3 @@
gamification.css: gamification.sass
sass --trace -t expanded gamification.sass gamification.css
@@ -0,0 +1,190 @@
@charset "UTF-8";
.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_goal {
width: 230px;
min-height: 200px;
}
.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_badge {
width: 250px;
min-height: 150px;
}
.openerp .oe_kanban_badge_avatars {
margin-top: 8px;
}
.openerp .oe_kanban_badge_avatars img {
width: 30px;
height: 30px;
padding-left: 0;
margin-top: 3px;
border-radius: 2px;
-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.openerp .oe_kanban_goal .oe_goal_state_block {
width: 200px;
height: 130px;
margin: auto;
margin-bottom: -20px;
}
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state {
font-size: 2.5em;
font-weight: bold;
padding-top: 30px;
}
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state.oe_e {
font-size: 7em;
}
.openerp .oe_kanban_goal .oe_goal_state_block, .openerp .oe_kanban_goal p {
text-align: center;
}
.openerp .oe_kanban_content .oe_goal_gauge:first-child {
margin: auto;
}
.openerp .oe_kanban_content .oe_goal_gauge svg {
margin-top: -20px;
}
.openerp .oe_no_overflow {
overflow: hidden;
}
.openerp .oe_red {
color: red;
}
.openerp .oe_green {
color: green;
}
.openerp .oe_orange {
color: orange;
}
.openerp .oe_form td .oe_no_padding {
margin-left: -6px;
}
.openerp .oe_mail_wall .oe_mail_wall_aside {
margin-top: 15px;
position: relative;
display: inline-block;
vertical-align: top;
width: 280px;
border-radius: 2px;
}
.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_challenge_list {
background-color: #ededf6;
}
.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_suggestion {
background-color: #d3def1;
}
.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_suggestion ul {
padding-left: 15px;
}
.openerp .oe_mail_wall .oe_mail_wall_aside h4, .openerp .oe_mail_wall .oe_mail_wall_aside .oe_goals_list .oe_thead {
text-align: center;
padding-bottom: 15px;
}
.openerp .oe_mail_wall .oe_mail_wall_aside > div {
border-bottom: solid 5px white;
border-radius: 2px;
}
.openerp .oe_mail_wall .oe_goal {
border-bottom: solid 3px white;
padding: 5px 10px;
}
.openerp .oe_mail_wall .oe_goal .oe_update_challenge.oe_e, .openerp .oe_mail_wall .oe_goal .oe_goal_action.oe_e {
visibility: hidden;
font-size: 25px;
float: right;
position: relative;
}
.openerp .oe_mail_wall .oe_goal div:hover > .oe_update_challenge, .openerp .oe_mail_wall .oe_goal div:hover > .oe_goal_action, .openerp .oe_mail_wall .oe_goal th:hover > .oe_goal_action {
visibility: visible;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list {
padding-left: 0;
margin-top: 5px;
margin-bottom: 10px;
width: 100%;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress div {
display: inline-block;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress::before {
content: "•";
padding-right: 4px;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached::before {
content: "✓";
padding-right: 0;
margin-left: -2px;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached .oe_cell {
text-decoration: line-through;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_cell.oe_goal_current {
font-size: 150%;
font-weight: bold;
min-width: 50px;
padding: 0 5px;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_outer_box {
display: inline-block;
position: relative;
z-index: 0;
vertical-align: middle;
width: 100%;
border: solid 1px rgba(0, 0, 0, 0.03);
border-radius: 2px;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_outer_box.oe_no_progress {
border: none;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_progress_background {
background-color: white;
position: absolute;
height: 100%;
width: 100%;
z-index: -2;
top: 0;
left: 0;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_progress {
background-color: #d4e9de;
position: absolute;
height: 100%;
width: 0;
z-index: -1;
top: 0;
left: 0;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_thead {
font-weight: normal;
padding: 5px;
text-align: center;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_cell {
padding: 3px 0;
}
.openerp .oe_mail_wall .oe_table.oe_goals_list .col0 {
font-size: 200%;
font-weight: bold;
width: 25px;
text-align: center;
}
.openerp .oe_mail_wall .oe_table.oe_goals_list .col1 {
padding: 0 5px;
}
.openerp .oe_mail_wall .oe_table.oe_goals_list .col2 {
width: auto;
}
.openerp .oe_mail_wall .oe_user_avatar {
width: 24px;
padding-right: 5px;
}
.openerp .oe_mail_wall .oe_mail {
display: inline-block;
}
.openerp .oe_mail_wall .oe_table {
display: table;
}
.openerp .oe_mail_wall .oe_row {
display: table-row;
}
.openerp .oe_mail_wall .oe_cell {
display: table-cell;
vertical-align: middle;
}
@@ -0,0 +1,181 @@
@charset "utf-8"
.openerp
// Kanban views
.oe_kanban_view
.oe_kanban_card.oe_kanban_goal
width: 230px
min-height: 200px
.oe_kanban_card.oe_kanban_badge
width: 250px
min-height: 150px
.oe_kanban_badge_avatars
margin-top: 8px
img
width: 30px
height: 30px
padding-left: 0
margin-top: 3px
border-radius: 2px
-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2)
.oe_kanban_goal
.oe_goal_state_block
width: 200px
height: 130px
margin: auto
margin-bottom: -20px
.oe_goal_state
font-size: 2.5em
font-weight: bold
padding-top: 30px
.oe_goal_state.oe_e
font-size: 7em
.oe_goal_state_block,p
text-align: center
.oe_kanban_content
.oe_goal_gauge:first-child
margin: auto /* avoid margin-right: 16px */
.oe_goal_gauge
svg
margin-top: -20px
.oe_no_overflow
overflow: hidden
.oe_red
color: red
.oe_green
color: green
.oe_orange
color: orange
// compensate padding from .openerp .oe_form td.oe_form_group_cell + .oe_form_group_cell
.oe_form td .oe_no_padding
margin-left: -6px
.oe_mail_wall
.oe_mail_wall_aside
margin-top: 15px
position: relative
display: inline-block
vertical-align: top
width: 280px
border-radius: 2px
.oe_gamification_challenge_list
background-color: #EDEDF6
.oe_gamification_suggestion
background-color: rgb(211, 222, 241)
ul
padding-left: 15px
h4, .oe_goals_list .oe_thead
text-align: center
padding-bottom: 15px
.oe_mail_wall_aside > div
border-bottom: solid 5px white
border-radius: 2px
.oe_goal
border-bottom: solid 3px white
padding: 5px 10px
.oe_update_challenge.oe_e, .oe_goal_action.oe_e
visibility: hidden
font-size: 25px
float: right
position: relative
div:hover > .oe_update_challenge, div:hover > .oe_goal_action, th:hover > .oe_goal_action
visibility: visible
.oe_goals_list
padding-left: 0
margin-top: 5px
margin-bottom: 10px
width: 100%
.oe_no_progress
div
display: inline-block
.oe_no_progress::before
content: "•"
padding-right: 4px
.oe_no_progress.oe_goal_reached::before
content: "✓"
padding-right: 0
margin-left: -2px
.oe_no_progress.oe_goal_reached
.oe_cell
text-decoration: line-through
.oe_cell.oe_goal_current
font-size: 150%
font-weight: bold
min-width: 50px
padding: 0 5px
.oe_goal_outer_box
display: inline-block
position: relative
z-index: 0
vertical-align: middle
width: 100%
border: solid 1px rgba(0,0,0,0.03)
border-radius: 2px
.oe_goal_outer_box.oe_no_progress
border: none
.oe_goal_progress_background
background-color: white
position: absolute
height: 100%
width: 100%
z-index: -2
top: 0
left: 0
.oe_goal_progress
background-color: rgb(212, 233, 222)
position: absolute
height: 100%
width: 0
z-index: -1
top: 0
left: 0
.oe_thead
font-weight: normal
padding: 5px
text-align: center
.oe_cell
padding: 3px 0
.oe_table.oe_goals_list
.col0
font-size: 200%
font-weight: bold
width: 25px
text-align: center
.col1
padding: 0 5px
.col2
width: auto
.oe_user_avatar
width: 24px
padding-right: 5px
.oe_mail
display: inline-block
.oe_table
display: table
.oe_row
display: table-row
.oe_cell
display: table-cell
vertical-align: middle
@@ -0,0 +1,148 @@
openerp.gamification = function(instance) {
var QWeb = instance.web.qweb;
instance.gamification.Sidebar = instance.web.Widget.extend({
template: 'gamification.UserWallSidebar',
init: function (parent, action) {
var self = this;
this._super(parent, action);
this.deferred = $.Deferred();
this.goals_info = {};
this.challenge_suggestions = {};
$(document).off('keydown.klistener');
},
events: {
// update a challenge and related goals
'click a.oe_update_challenge': function(event) {
var self = this;
var challenge_id = parseInt(event.currentTarget.id, 10);
var goals_updated = new instance.web.Model('gamification.challenge').call('quick_update', [challenge_id]);
$.when(goals_updated).done(function() {
self.get_goal_todo_info();
});
},
// action to modify a goal
'click a.oe_goal_action': function(event) {
var self = this;
var goal_id = parseInt(event.currentTarget.id, 10);
var goal_action = new instance.web.Model('gamification.goal').call('get_action', [goal_id]).then(function(res) {
goal_action['action'] = res;
});
$.when(goal_action).done(function() {
var action = self.do_action(goal_action.action);
$.when(action).done(function () {
new instance.web.Model('gamification.goal').call('update', [[goal_id]]).then(function(res) {
self.get_goal_todo_info();
});
});
});
},
// get more info about a challenge request
'click a.oe_challenge_reply': function(event) {
var self = this;
var challenge_id = parseInt(event.currentTarget.id, 10);
var challenge_action = new instance.web.Model('gamification.challenge').call('reply_challenge_wizard', [challenge_id]).then(function(res) {
challenge_action['action'] = res;
});
$.when(challenge_action).done(function() {
self.do_action(challenge_action.action).done(function () {
self.get_goal_todo_info();
});
});
},
'click .oe_goal h4': function(event) {
var self = this;
this.kkeys = [];
$(document).on('keydown.klistener', function(event) {
if ("37,38,39,40,65,66".indexOf(event.keyCode) < 0) {
$(document).off('keydown.klistener');
} else {
self.kkeys.push(event.keyCode);
if (self.kkeys.toString().indexOf("38,38,40,40,37,39,37,39,66,65") >= 0) {
new instance.web.Model('gamification.badge').call('check_progress', []);
$(document).off('keydown.klistener');
}
}
});
}
},
start: function() {
var self = this;
this._super.apply(this, arguments);
self.get_goal_todo_info();
self.get_challenge_suggestions();
},
get_goal_todo_info: function() {
var self = this;
var challenges = new instance.web.Model('res.users').call('get_serialised_gamification_summary', []).then(function(result) {
if (result.length === 0) {
self.$el.find(".oe_gamification_challenge_list").hide();
} else {
self.$el.find(".oe_gamification_challenge_list").empty();
_.each(result, function(item){
var $item = $(QWeb.render("gamification.ChallengeSummary", {challenge: item}));
self.render_money_fields($item);
self.render_user_avatars($item);
self.$el.find('.oe_gamification_challenge_list').append($item);
});
}
});
},
get_challenge_suggestions: function() {
var self = this;
var challenge_suggestions = new instance.web.Model('res.users').call('get_challenge_suggestions', []).then(function(result) {
if (result.length === 0) {
self.$el.find(".oe_gamification_suggestion").hide();
} else {
var $item = $(QWeb.render("gamification.ChallengeSuggestion", {challenges: result}));
self.$el.find('.oe_gamification_suggestion').append($item);
}
});
},
render_money_fields: function(item) {
var self = this;
self.dfm = new instance.web.form.DefaultFieldManager(self);
// Generate a FieldMonetary for each .oe_goal_field_monetary
item.find(".oe_goal_field_monetary").each(function() {
var currency_id = parseInt( $(this).attr('data-id'), 10);
money_field = new instance.web.form.FieldMonetary(self.dfm, {
attrs: {
modifiers: '{"readonly": true}'
}
});
money_field.set('currency', currency_id);
money_field.get_currency_info();
money_field.set('value', parseInt($(this).text(), 10));
money_field.replace($(this));
});
},
render_user_avatars: function(item) {
var self = this;
item.find(".oe_user_avatar").each(function() {
var user_id = parseInt( $(this).attr('data-id'), 10);
var url = instance.session.url('/web/binary/image', {model: 'res.users', field: 'image_small', id: user_id});
$(this).attr("src", url);
});
}
});
instance.mail.Widget.include({
start: function() {
this._super();
var sidebar = new instance.gamification.Sidebar(this);
sidebar.appendTo($('.oe_mail_wall_aside'));
},
});
instance.web_kanban.KanbanRecord.include({
// open related goals when clicking on challenge kanban view
on_card_clicked: function() {
if (this.view.dataset.model === 'gamification.challenge') {
this.$('.oe_kanban_project_list a').first().click();
} else {
this._super.apply(this, arguments);
}
},
});
};
@@ -0,0 +1,114 @@
<templates>
<t t-name="gamification.UserWallSidebar" class="oe_gamification_user_wall_sidebar">
<div>
<div class="oe_gamification_suggestion"></div>
<div class="oe_gamification_challenge_list"></div>
</div>
</t>
<t t-name="gamification.ChallengeSummary">
<div class="oe_goal">
<div>
<a class="oe_update_challenge oe_e" rol="button" t-attf-id="{challenge.id}">e</a>
<h4><t t-esc="challenge.name" /></h4>
</div>
<t t-if="challenge.visibility_mode == 'personal'">
<div class="oe_table oe_goals_list">
<div t-foreach="challenge.lines" t-as="line" t-attf-class="oe_row oe_goal_outer_box #{line.state == 'reached' ? 'oe_goal_reached' : ''} #{line.display_mode != 'progress' ? 'oe_no_progress' : ''}">
<t t-if="line.display_mode == 'progress'">
<div class="oe_goal_progress_background"></div>
<div class="oe_goal_progress" t-attf-style="#{line.display_mode == 'progress' ? 'width: '+line.completeness+'%;' : 'width:0;'}"></div>
<div class="oe_cell oe_goal_current"><t t-esc="line.current" /></div>
</t>
<div class="oe_cell">
<t t-if="line.computation_mode != 'manually' and !line.action">
<span t-att-title="line.description"><t t-esc="line.name" /></span>
</t>
<t t-if="line.action or line.computation_mode == 'manually'">
<span t-att-title="line.description"><a class="oe_goal_action" t-att-id="line.id"><t t-esc="line.name" /></a></span>
</t>
<t t-if="line.display_mode == 'progress'"><br/>
<div class="oe_grey">
<t t-if="line.definition_condition == 'higher'">
Target:
</t>
<t t-if="line.definition_condition == 'lower'">
Target: &lt;=
</t>
<span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="line.target"/></span>
<t t-if="line.suffix"><t t-esc="line.suffix"/></t>
</div>
</t>
</div>
</div>
</div>
</t>
<t t-if="challenge.visibility_mode == 'ranking'">
<div t-foreach="challenge.lines" t-as="line" class="oe_goals_list oe_table">
<div class="oe_row">
<div class="oe_cell oe_thead" colspan="3" t-attf-title="#{line.description ? line.description : ''}">
<strong><t t-esc="line.name"/></strong>
<br/>
<div class="oe_grey">
<t t-if="line.definition_condition == 'higher'">
Target:
</t>
<t t-if="line.definition_condition == 'lower'">
Target: &lt;=
</t>
<span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="line.target" /></span><t t-if="line.suffix"> <t t-esc="line.suffix"/></t>
</div>
</div>
</div>
<div t-foreach="line.goals" t-as="goal" t-attf-class="#{goal.id == line.own_goal_id ? 'oe_bold' : ''}">
<div t-attf-class="oe_row oe_goal_outer_box #{goal.state == 'reached' ? 'oe_goal_reached' : ''} #{line.display_mode != 'progress' ? 'oe_no_progress' : ''}">
<t t-if="line.display_mode == 'progress'">
<div class="oe_goal_progress_background"></div>
<div class="oe_goal_progress" t-attf-style="#{line.display_mode == 'progress' ? 'width: '+goal.completeness+'%;' : 'width:0;'}"></div>
</t>
<div class="oe_cell col0"><t t-esc="goal.rank" /></div>
<div class="oe_cell col1"><img class="oe_user_avatar" t-attf-alt="#{goal.name}" t-attf-data-id="#{goal.user_id}"/></div>
<div class="oe_cell col2">
<t t-if="line.display_mode == 'progress'">
<!-- progress, action on current value -->
<t t-esc="goal.name"/><br/>
<t t-if="goal.id != line.own_goal_id or (line.computation_mode != 'manually' and !line.action)">
<span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="goal.current" /></span><t t-if="line.suffix"> <t t-esc="line.suffix"/></t>
</t>
<t t-if="goal.id == line.own_goal_id and (line.action or line.computation_mode == 'manually')">
<a class="oe_goal_action" t-att-id="line.own_goal_id">
<span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="goal.current" /></span><t t-if="line.suffix"> <t t-esc="line.suffix"/></t>
</a>
</t>
</t>
<t t-if="line.display_mode != 'progress'">
<!-- not progress, action on user name -->
<t t-if="goal.id != line.own_goal_id or (line.computation_mode != 'manually' and !line.action)">
<t t-esc="goal.name"/>
</t>
<t t-if="goal.id == line.own_goal_id and (line.action or line.computation_mode == 'manually')">
<a class="oe_goal_action" t-att-id="line.own_goal_id"><t t-esc="goal.name"/></a>
</t>
</t>
</div>
</div>
</div>
</div>
</t>
</div>
</t>
<t t-name="gamification.ChallengeSuggestion">
<div class="oe_goal">
<h4>Invited Challenges</h4>
<ul t-foreach="challenges" t-as="challenge" class="oe_goals_list">
<li>
<strong><a class="oe_challenge_reply" t-attf-title="#{challenge.description.value ? challenge.description.value : ''}" t-attf-id="{challenge.id}" title="more details..."><t t-esc="challenge.name"/></a></strong>
</li>
</ul>
</div>
</t>
</templates>
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2013 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import test_challenge
checks = [
test_challenge,
]
@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2013 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.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)
self.assertGreaterEqual(len(challenge.user_ids), len(user_ids), "Not enough users in base challenge")
self.user_obj.create(cr, uid, {
'name': 'R2D2',
'login': 'r2d2@openerp.com',
'email': 'r2d2@openerp.com',
'groups_id': [(6, 0, [self.group_user_id])]
}, {'no_reset_password': True})
challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
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]
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")
# 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)
self.goal_obj.update(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")
# 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_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")
+194
View File
@@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- 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_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" version="7.0">
<header>
<button string="Grant this Badge" type="action" name="%(action_grant_wizard)d" class="oe_highlight" attrs="{'invisible': [('remaining_sending','=',0)]}" />
<button string="Check Badge" type="object" name="check_automatic" groups="base.group_no_one" />
</header>
<sheet>
<div class="oe_right oe_button_box">
</div>
<field name="image" widget='image' class="oe_left oe_avatar"/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
</div>
<group>
<field name="description" nolabel="1" placeholder="Badge Description" />
</group>
<group string="Granting">
<div class="oe_grey" colspan="4">
Security rules to define who is allowed to manually grant badges. Not enforced for administrator.
</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>
<div attrs="{'invisible': [('remaining_sending','!=',-1)]}" class="oe_grey">
No monthly sending limit
</div>
</div>
</group>
<group string="Rewards for challenges">
<field name="challenge_ids" widget="many2many_kanban" nolabel="1" />
</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 version="7.0" class="oe_background_grey">
<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="#{record.stat_my.raw_value ? 'oe_kanban_color_5' : 'oe_kanban_color_white'} oe_kanban_card oe_kanban_global_click oe_kanban_badge">
<div class="oe_kanban_content">
<div class="oe_kanban_left">
<a type="open"><img t-att-src="kanban_image('gamification.badge', 'image', record.image.raw_value)" t-att-title="record.name.value" width="90" height="90" /></a>
</div>
<div class="oe_no_overflow">
<h4><field name="name"/></h4>
<t t-if="record.remaining_sending.value != 0">
<button type="action" name="%(action_grant_wizard)d" class="oe_highlight">Grant</button>
<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>
</t>
<t t-if="record.remaining_sending.value == 0">
<div class="oe_grey">Can not grant</div>
</t>
<p>
<strong><t t-esc="record.stat_count.raw_value"/></strong> granted,<br/>
<strong><t t-esc="record.stat_this_month.raw_value"/></strong> this month
</p>
</div>
<div class="oe_kanban_badge_avatars">
<t t-if="record.description.value">
<p><em><field name="description"/></em></p>
</t>
<a type="object" name="get_granted_employees">
<t t-foreach="record.unique_owner_ids.raw_value.slice(0,11)" t-as="owner">
<img t-att-src="kanban_image('res.users', 'image_small', owner)" t-att-data-member_id="owner"/>
</t>
</a>
</div>
</div>
</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 version="7.0" 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">
<div class="oe_kanban_content">
<div class="oe_kanban_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="record.create_date.raw_value.toString(Date.CultureInfo.formatPatterns.shortDate)" /></p>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</data>
</openerp>
+289
View File
@@ -0,0 +1,289 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<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" colors="blue:state == 'draft';grey: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="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" version="7.0">
<header>
<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>
<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_ids" class="oe_edit_only" string="Assign Challenge To"/>
<div>
<field name="user_ids" widget="many2many_tags" />
</div>
</div>
<!-- action buttons -->
<div class="oe_right oe_button_box">
<button type="action" name="%(goals_from_challenge_act)d" string="Related Goals" attrs="{'invisible': [('state','=','draft')]}" />
</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" version="7.0" 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"/>
<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)]}" />
</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="autojoin_group_id" />
<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>
<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 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"/>
</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" version="7.0">
<field name="line_ids"/>
<field name="user_ids"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_goal oe_kanban_global_click">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">í</span>
<ul class="oe_dropdown_menu">
<li><a type="edit">Configure Challenge</a></li>
</ul>
</div>
<div class="oe_kanban_content">
<h4><field name="name"/></h4>
<div class="oe_kanban_project_list">
<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>
<div class="oe_kanban_badge_avatars">
<t t-foreach="record.user_ids.raw_value.slice(0,11)" t-as="member">
<img t-att-src="kanban_image('res.users', 'image_small', member)" t-att-data-member_id="member"/>
</t>
</div>
</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" version="7.0">
<field name="reward_failure" invisible="1"/>
<div class="oe_title">
<h1><field name="name" nolabel="1" readonly="1"/></h1>
</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" version="7.0" 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>
<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="oe_highlight" />
<button string="Reject" type="object" name="discard_challenge"/> or
<button string="reply later" special="cancel" class="oe_link"/>
</center>
</footer>
</form>
</field>
</record>
<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>
</data>
</openerp>
+289
View File
@@ -0,0 +1,289 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- 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" colors="red:state == 'failed';green:state == 'reached';grey:state == 'canceled'">
<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" version="7.0">
<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,inprogress_update" />
<button string="Goal Failed" type="object" name="action_fail" states="inprogress,inprogress_update"/>
<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" attrs="{'readonly':[('state','!=','draft')]}"/>
</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" 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>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<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="Current"
domain="[
'|',
('state', 'in', ('inprogress', 'inprogress_update')),
('end_date', '>=', context_today().strftime('%%Y-%%m-%%d'))
]"/>
<filter name="closed" string="Passed" domain="[('state', 'in', ('reached', 'failed'))]"/>
<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 version="7.0" 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 oe_gamification_goal oe_kanban_goal #{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="oe_kanban_content">
<p><h4 class="oe_goal_name" tooltip="kanban-tooltip"><field name="definition_id" /></h4></p>
<div class="oe_kanban_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="oe_goal_state_block">
<t t-if="record.definition_display.raw_value == 'boolean'">
<div class="oe_goal_state oe_e">
<t t-if="record.state.raw_value=='reached'"><span class="oe_green" title="Goal Reached">W</span></t>
<t t-if="record.state.raw_value=='inprogress' || record.state.raw_value=='inprogress_update'"><span title="Goal in Progress">N</span></t>
<t t-if="record.state.raw_value=='failed'"><span class="oe_red" title="Goal Failed">X</span></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="oe_goal_state #{record.current.raw_value == record.target_goal.raw_value+1 ? 'oe_orange' : record.current.raw_value &gt; record.target_goal.raw_value ? 'oe_red' : 'oe_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>
<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" version="7.0">
<sheet>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" class="oe_inline"/>
</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" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
<field name="field_id" attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}" domain="[('model_id','=',model_id)]" class="oe_inline"/>
<field name="field_date_id" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))]}" domain="[('ttype', 'in', ('date', 'datetime')), ('model_id','=',model_id)]" class="oe_inline"/>
<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.group'), ('partner_id', '=', object.user_id.partner_id.id)], count=True, context=context)"/>
<field name="condition" widget="radio"/>
</group>
<group string="Formating Options">
<field name="display_mode" widget="radio" />
<field name="suffix" placeholder="e.g. days"/>
<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_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="0"/>
<menuitem id="gamification_challenge_menu" parent="gamification_menu" action="challenge_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"/>
</data>
</openerp>
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import update_goal
import grant_badge
+56
View File
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.osv import fields, osv
from openerp.tools.translate import _
class grant_badge_wizard(osv.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):
"""Wizard action for sending a badge to a chosen user"""
badge_obj = self.pool.get('gamification.badge')
badge_user_obj = self.pool.get('gamification.badge.user')
for wiz in self.browse(cr, uid, ids, context=context):
if uid == wiz.user_id.id:
raise osv.except_osv(_('Warning!'), _('You can not grant a badge to yourself'))
#create the badge
values = {
'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_obj._send_badge(cr, uid, badge_user, context=context)
return result
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<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" version="7.0">
Who would you like to reward?
<group>
<field name="user_id" nolabel="1" />
<field name="badge_id" invisible="1"/>
<field name="comment" nolabel="1" placeholder="Describe what they did and why it matters (will be public)" class="oe_no_padding" />
</group>
<footer>
<button string="Grant Badge" type="object" name="action_grant_badge" class="oe_highlight" /> or
<button string="Cancel" special="cancel" class="oe_link"/>
</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>
+44
View File
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.osv import fields, osv
class goal_manual_wizard(osv.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):
"""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 = {
'current': wiz.current,
'goal_id': wiz.goal_id.id,
}
goal_obj.write(cr, uid, [wiz.goal_id.id], towrite, context=context)
goal_obj.update(cr, uid, [wiz.goal_id.id], context=context)
return {}
@@ -0,0 +1,24 @@
<?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" version="7.0">
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="oe_highlight" /> or
<button string="Cancel" special="cancel" class="oe_link"/>
</footer>
</form>
</field>
</record>
</data>
</openerp>
+20
View File
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'CRM Gamification',
'version': '1.0',
'author': 'OpenERP SA',
'category': 'hidden',
'depends': ['gamification','sale_crm'],
'description': """Example of goal definitions and challenges that can be used related to the usage of the CRM Sale module.""",
'data': ['sale_crm_goals.xml'],
'demo': ['sale_crm_goals_demo.xml'],
'auto_install': True,
}
@@ -0,0 +1,173 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- goal definitions -->
<record model="gamification.goal.definition" id="definition_crm_tot_invoices">
<field name="name">Total Invoiced</field>
<field name="description"></field>
<field name="computation_mode">sum</field>
<field name="monetary">True</field>
<field name="model_id" eval="ref('account.model_account_invoice_report')" />
<field name="field_id" eval="ref('account.field_account_invoice_report_price_total')" />
<field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
<field name="domain">[('state','!=','cancel'),('user_id','=',user.id),('type','=','out_invoice')]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_nbr_new_leads">
<field name="name">New Leads</field>
<field name="description">Based on the creation date</field>
<field name="computation_mode">count</field>
<field name="suffix">leads</field>
<field name="model_id" eval="ref('crm.model_crm_lead')" />
<field name="field_date_id" eval="ref('crm.field_crm_lead_create_date')" />
<!-- lead AND opportunity as don't want to be penalised for lead converted to opportunity -->
<field name="domain">[('user_id','=',user.id), '|', ('type', '=', 'lead'), ('type', '=', 'opportunity')]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_lead_delay_open">
<field name="name">Time to Qualify a Lead</field>
<field name="description">The average number of days to open the case (lower than)</field>
<field name="computation_mode">sum</field>
<field name="condition">lower</field>
<field name="suffix">days</field>
<field name="model_id" eval="ref('crm.model_crm_lead_report')" />
<field name="field_id" eval="ref('crm.field_crm_lead_report_delay_close')" />
<field name="field_date_id" eval="ref('crm.field_crm_lead_report_date_closed')" />
<field name="domain">[('user_id','=',user.id),('type', '=', 'lead')]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_lead_delay_close">
<field name="name">Days to Close a Deal</field>
<field name="description">The average number of days to close the case (lower than)</field>
<field name="computation_mode">sum</field>
<field name="condition">lower</field>
<field name="suffix">days</field>
<field name="model_id" eval="ref('crm.model_crm_lead_report')" />
<field name="field_id" eval="ref('crm.field_crm_lead_report_delay_open')" />
<field name="field_date_id" eval="ref('crm.field_crm_lead_report_opening_date')" />
<field name="domain">[('user_id','=',user.id)]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_nbr_call">
<field name="name">Logged Calls</field>
<field name="description">Log a certain number of calls to reach this goal</field>
<field name="computation_mode">count</field>
<field name="suffix">calls</field>
<field name="model_id" eval="ref('crm.model_crm_phonecall')" />
<field name="field_date_id" eval="ref('crm.field_crm_phonecall_date_closed')" />
<field name="domain">[('user_id','=',user.id),('state','=','done')]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_nbr_new_opportunities">
<field name="name">New Opportunities</field>
<field name="description">Based on the opening date</field>
<field name="computation_mode">count</field>
<field name="suffix">opportunities</field>
<field name="model_id" eval="ref('crm.model_crm_lead')" />
<field name="field_date_id" eval="ref('crm.field_crm_lead_date_open')" />
<field name="domain">[('user_id','=',user.id),('type','=','opportunity')]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_nbr_sale_order_created">
<field name="name">New Sales Orders</field>
<field name="description">Based on the creation date</field>
<field name="computation_mode">count</field>
<field name="suffix">orders</field>
<field name="model_id" eval="ref('sale.model_sale_order')" />
<field name="field_date_id" eval="ref('sale.field_sale_order_date_order')" />
<field name="domain">[('user_id','=',user.id),('state','not in',('draft', 'sent', 'cancel'))]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_nbr_paid_sale_order">
<field name="name">Paid Sales Orders</field>
<field name="description">Based on the invoice date</field>
<field name="computation_mode">count</field>
<field name="suffix">orders</field>
<field name="model_id" eval="ref('account.model_account_invoice_report')" />
<field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
<field name="domain">[('state','=','paid'),('user_id','=',user.id),('type','=','out_invoice')]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_tot_paid_sale_order">
<field name="name">Total Paid Sales Orders</field>
<field name="description">Based on the invoice date</field>
<field name="computation_mode">count</field>
<field name="monetary">True</field>
<field name="model_id" eval="ref('account.model_account_invoice_report')" />
<field name="field_id" eval="ref('account.field_account_invoice_report_price_total')" />
<field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
<field name="domain">[('state','=','paid'),('user_id','=',user.id),('type','=','out_invoice')]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_nbr_customer_refunds">
<field name="name">Customer Refunds</field>
<field name="description">Refund the least customers (lower than)</field>
<field name="computation_mode">count</field>
<field name="condition">lower</field>
<field name="suffix">invoices</field>
<field name="model_id" eval="ref('account.model_account_invoice_report')" />
<field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
<field name="domain">[('state','!=','cancel'),('user_id','=',user.id),('type','=','out_refund')]</field>
</record>
<record model="gamification.goal.definition" id="definition_crm_tot_customer_refunds">
<field name="name">Total Customer Refunds</field>
<field name="description">The total refunded value is a negative value. Validated when higher (min refunded).</field>
<field name="computation_mode">sum</field>
<field name="condition">higher</field>
<field name="monetary">True</field>
<field name="model_id" eval="ref('account.model_account_invoice_report')" />
<field name="field_id" eval="ref('account.field_account_invoice_report_price_total')" />
<field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
<field name="domain">[('state','!=','cancel'),('user_id','=',user.id),('type','=','out_refund')]</field>
</record>
<!-- challenges -->
<record model="gamification.challenge" id="challenge_crm_sale">
<field name="name">Monthly Sales Targets</field>
<field name="period">monthly</field>
<field name="visibility_mode">ranking</field>
<field name="autojoin_group_id" eval="ref('base.group_sale_salesman')" />
<field name="report_message_frequency">weekly</field>
</record>
<record model="gamification.challenge" id="challenge_crm_marketing">
<field name="name">Lead Acquisition</field>
<field name="period">monthly</field>
<field name="visibility_mode">ranking</field>
<field name="autojoin_group_id" eval="ref('base.group_sale_salesman')" />
<field name="report_message_frequency">weekly</field>
</record>
<!-- lines -->
<record model="gamification.challenge.line" id="line_crm_sale1">
<field name="definition_id" eval="ref('definition_crm_tot_invoices')" />
<field name="target_goal">20000</field>
<field name="challenge_id" eval="ref('challenge_crm_sale')" />
</record>
<record model="gamification.challenge.line" id="line_crm_marketing1">
<field name="definition_id" eval="ref('definition_crm_nbr_new_leads')" />
<field name="target_goal">7</field>
<field name="challenge_id" eval="ref('challenge_crm_marketing')" />
<field name="sequence">1</field>
</record>
<record model="gamification.challenge.line" id="line_crm_marketing2">
<field name="definition_id" eval="ref('definition_crm_lead_delay_open')" />
<field name="target_goal">15</field>
<field name="challenge_id" eval="ref('challenge_crm_marketing')" />
<field name="sequence">2</field>
</record>
<record model="gamification.challenge.line" id="line_crm_marketing3">
<field name="definition_id" eval="ref('definition_crm_nbr_new_opportunities')" />
<field name="target_goal">5</field>
<field name="challenge_id" eval="ref('challenge_crm_marketing')" />
<field name="sequence">3</field>
</record>
</data>
</openerp>
@@ -0,0 +1,23 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- challenges -->
<record model="gamification.challenge" id="challenge_crm_sale">
<field name="user_ids" eval="[(4,ref('base.user_demo'))]" />
<field name="state">inprogress</field>
</record>
<!-- goals -->
<record model="gamification.goal" id="goal_crm_sale1">
<field name="definition_id" eval="ref('definition_crm_tot_invoices')" />
<field name="user_id" eval="ref('base.user_demo')" />
<field name="line_id" eval="ref('line_crm_sale1')" />
<field name="start_date" eval="time.strftime('%Y-%m-01')" />
<field name="end_date" eval="time.strftime('%Y-%m-31')" />
<field name="target_goal">2000</field>
<field name="state">inprogress</field>
</record>
</data>
</openerp>
+2
View File
@@ -42,6 +42,8 @@ class hr_config_settings(osv.osv_memory):
help ="""This installs the module hr_contract."""),
'module_hr_evaluation': fields.boolean('Organize employees periodic evaluation',
help ="""This installs the module hr_evaluation."""),
'module_hr_gamification': fields.boolean('Drive engagement with challenges and badges',
help ="""This installs the module hr_gamification."""),
'module_account_analytic_analysis': fields.boolean('Allow invoicing based on timesheets (the sale application will be installed)',
help ="""This installs the module account_analytic_analysis, which will install sales management too."""),
'module_hr_payroll': fields.boolean('Manage payroll',
+4
View File
@@ -56,6 +56,10 @@
<field name="module_hr_expense" class="oe_inline"/>
<label for="module_hr_expense"/>
</div>
<div>
<field name="module_hr_gamification" class="oe_inline"/>
<label for="module_hr_gamification"/>
</div>
</div>
</group>
<group name="timesheet_grp">
+1 -1
View File
@@ -72,4 +72,4 @@
.openerp .oe_employee_vignette .oe_followers {
width: auto;
float: none;
}
}
+4
View File
@@ -101,6 +101,8 @@ Thanks,
class hr_employee(osv.osv):
_name = "hr.employee"
_inherit="hr.employee"
_columns = {
'evaluation_plan_id': fields.many2one('hr_evaluation.plan', 'Appraisal Plan'),
'evaluation_date': fields.date('Next Appraisal Date', help="The date of the next appraisal is computed by the appraisal plan's dates (first appraisal + periodicity)."),
@@ -123,6 +125,8 @@ class hr_employee(osv.osv):
obj_evaluation.button_plan_in_progress(cr, uid, [plan_id], context=context)
return True
class hr_evaluation(osv.osv):
_name = "hr_evaluation.evaluation"
_inherit = "mail.thread"
@@ -39,4 +39,4 @@ access_survey_response_hr_employee,survey.response.employee,survey.model_survey_
access_survey_question_column_heading_hr_employee,survey.question.column.heading.employee,survey.model_survey_question_column_heading,base.group_user,1,0,0,0
access_survey_response_line_hr_employee,survey.response.line.employee,survey.model_survey_response_line,base.group_user,1,1,1,0
access_survey_response_answer_hr_employee,survey.response.answer.hr.employee,survey.model_survey_response_answer,base.group_user,1,1,1,0
access_survey_tbl_column_heading_hr_employee,survey.tbl.column.heading,survey.model_survey_tbl_column_heading,base.group_user,1,1,1,0
access_survey_tbl_column_heading_hr_employee,survey.tbl.column.heading,survey.model_survey_tbl_column_heading,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
39 access_survey_question_column_heading_hr_employee survey.question.column.heading.employee survey.model_survey_question_column_heading base.group_user 1 0 0 0
40 access_survey_response_line_hr_employee survey.response.line.employee survey.model_survey_response_line base.group_user 1 1 1 0
41 access_survey_response_answer_hr_employee survey.response.answer.hr.employee survey.model_survey_response_answer base.group_user 1 1 1 0
42 access_survey_tbl_column_heading_hr_employee survey.tbl.column.heading survey.model_survey_tbl_column_heading base.group_user 1 1 1 0
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import models
import wizard
+40
View File
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'HR Gamification',
'version': '1.0',
'author': 'OpenERP SA',
'category': 'hidden',
'depends': ['gamification', 'hr'],
'description': """Use the HR ressources for the gamification process.
The HR officer can now manage challenges and badges.
This allow the user to send badges to employees instead of simple users.
Badge received are displayed on the user profile.
""",
'data': [
'security/ir.model.access.csv',
'security/gamification_security.xml',
'wizard/grant_badge.xml',
'views/gamification.xml',
],
'js': ['static/src/js/gamification.js'],
}
+22
View File
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import gamification
@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.osv import fields, osv
class hr_gamification_badge_user(osv.Model):
"""User having received a badge"""
_name = 'gamification.badge.user'
_inherit = ['gamification.badge.user']
_columns = {
'employee_id': fields.many2one("hr.employee", string='Employee'),
}
def _check_employee_related_user(self, cr, uid, ids, context=None):
for badge_user in self.browse(cr, uid, ids, context=context):
if badge_user.user_id and badge_user.employee_id:
if badge_user.employee_id not in badge_user.user_id.employee_ids:
return False
return True
_constraints = [
(_check_employee_related_user, "The selected employee does not correspond to the selected user.", ['employee_id']),
]
class gamification_badge(osv.Model):
_name = 'gamification.badge'
_inherit = ['gamification.badge']
def get_granted_employees(self, cr, uid, badge_ids, context=None):
if context is None:
context = {}
employee_ids = []
badge_user_ids = self.pool.get('gamification.badge.user').search(cr, uid, [('badge_id', 'in', badge_ids), ('employee_id', '!=', False)], context=context)
for badge_user in self.pool.get('gamification.badge.user').browse(cr, uid, badge_user_ids, context):
employee_ids.append(badge_user.employee_id.id)
# remove duplicates
employee_ids = list(set(employee_ids))
return {
'type': 'ir.actions.act_window',
'name': 'Granted Employees',
'view_mode': 'kanban,tree,form',
'view_type': 'form',
'res_model': 'hr.employee',
'domain': [('id', 'in', employee_ids)]
}
class hr_employee(osv.osv):
_name = "hr.employee"
_inherit = "hr.employee"
def _get_employee_goals(self, cr, uid, ids, field_name, arg, context=None):
"""Return the list of goals assigned to the employee"""
res = {}
for employee in self.browse(cr, uid, ids, context=context):
res[employee.id] = self.pool.get('gamification.goal').search(cr,uid,[('user_id', '=', employee.user_id.id), ('challenge_id.category', '=', 'hr')], context=context)
return res
def _get_employee_badges(self, cr, uid, ids, field_name, arg, context=None):
"""Return the list of badge_users assigned to the employee"""
res = {}
for employee in self.browse(cr, uid, ids, context=context):
res[employee.id] = self.pool.get('gamification.badge.user').search(cr, uid, [
'|',
('employee_id', '=', employee.id),
'&',
('employee_id', '=', False),
('user_id', '=', employee.user_id.id)
], context=context)
return res
def _has_badges(self, cr, uid, ids, field_name, arg, context=None):
"""Return the list of badge_users assigned to the employee"""
res = {}
for employee in self.browse(cr, uid, ids, context=context):
employee_badge_ids = self.pool.get('gamification.badge.user').search(cr, uid, [
'|',
('employee_id', '=', employee.id),
'&',
('employee_id', '=', False),
('user_id', '=', employee.user_id.id)
], context=context)
res[employee.id] = len(employee_badge_ids) > 0
return res
_columns = {
'goal_ids': fields.function(_get_employee_goals, type="one2many", obj='gamification.goal', string="Employee HR Goals"),
'badge_ids': fields.function(_get_employee_badges, type="one2many", obj='gamification.badge.user', string="Employee Badges"),
'has_badges': fields.function(_has_badges, type="boolean", string="Has Badges"),
}
@@ -0,0 +1,17 @@
<?xml version="1.0" ?>
<openerp>
<data noupdate="1">
<record id="goal_officer_visibility" model="ir.rule">
<field name="name">HR Officer can see any goal</field>
<field name="model_id" ref="gamification.model_gamification_goal"/>
<field name="groups" eval="[(4, ref('base.group_hr_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
</data>
</openerp>
@@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
challenge_officer,"Challenge Officer",gamification.model_gamification_challenge,base.group_hr_user,1,1,1,1
challenge_line_officer,"Challenge Line Officer",gamification.model_gamification_challenge_line,base.group_hr_user,1,1,1,1
badge_officer,"Badge Officer",gamification.model_gamification_badge,base.group_hr_user,1,1,1,1
badge_user_officer,"Badge-user Officer",gamification.model_gamification_badge_user,base.group_hr_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 challenge_officer Challenge Officer gamification.model_gamification_challenge base.group_hr_user 1 1 1 1
3 challenge_line_officer Challenge Line Officer gamification.model_gamification_challenge_line base.group_hr_user 1 1 1 1
4 badge_officer Badge Officer gamification.model_gamification_badge base.group_hr_user 1 1 1 1
5 badge_user_officer Badge-user Officer gamification.model_gamification_badge_user base.group_hr_user 1 1 1 1
@@ -0,0 +1,19 @@
openerp.hr_gamification = function(instance) {
instance.web_kanban.KanbanRecord.include({
on_card_clicked: function() {
if (this.view.dataset.model === 'gamification.badge.user') {
var action = {
type: 'ir.actions.act_window',
res_model: 'gamification.badge',
view_mode: 'form',
view_type: 'form,kanban,tree',
views: [[false, 'form']],
res_id: this.record.badge_id.raw_value[0]
};
this.do_action(action);
} else {
this._super.apply(this, arguments);
}
}
});
};
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="hr_badge_form_view" model="ir.ui.view">
<field name="name">Badge Form</field>
<field name="model">gamification.badge</field>
<field name="inherit_id" ref="gamification.badge_form_view"/>
<field name="arch" type="xml">
<xpath expr="//div[@class='oe_right oe_button_box']" position="inside">
<button string="Granted Employees" type="object" name="get_granted_employees" attrs="{'invisible': [('stat_count','=',0)]}" />
</xpath>
</field>
</record>
<!-- HR Employee -->
<record id="hr_hr_employee_view_form" model="ir.ui.view">
<field name="name">hr.hr.employee.view.form</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@string='Public Information']" position="before">
<page string="Received Badges" attrs="{'invisible': [('user_id', '=', False)]}">
<field name="has_badges" invisible="1"/>
<button string="Grant a Badge" type="action" name="%(action_reward_wizard)d"/> to reward this employee for a good action
<div class="oe_view_nocontent" attrs="{'invisible': [('has_badges', '=', True)]}">
<p class="oe_view_nocontent_create">
Click to grant this employee his first badge
</p><p class="oe_grey">
Badges are rewards of good work. Give them to people you believe deserve it.
</p>
</div>
<field name="badge_ids" widget="many2many_kanban" />
</page>
</xpath>
<xpath expr="//page[@string='Public Information']" position="after">
<page string="Goals">
<field name="goal_ids" widget="many2many_kanban" />
</page>
</xpath>
</field>
</record>
<record id="goals_menu_groupby_action2" model="ir.actions.act_window">
<field name="res_model">gamification.goal</field>
<field name="view_type">form</field>
<field name="name">Goals History</field>
<field name="view_mode">tree,kanban</field>
<field name="context">{'search_default_group_by_user': True, 'search_default_group_by_type': True}</field>
<field name="domain">[('challenge_id.category', '=', 'hr')]</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 type.
Goals can be created automatically by using challenges.
</p>
</field>
</record>
<record id="challenge_list_action2" 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="domain">[('category', '=', 'hr')]</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>
<record id="challenge_list_action2_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_action2"/>
<field name="view_id" ref="gamification.view_challenge_kanban"/>
</record>
<record id="challenge_list_action2_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_action2"/>
<field name="view_id" ref="gamification.challenge_form_view"/>
</record>
<menuitem id="menu_hr_gamification" parent="hr.menu_hr_root" name="Engagement" sequence="40"/>
<menuitem id="gamification_badge_menu_hr" parent="menu_hr_gamification" action="gamification.badge_list_action" />
<menuitem id="gamification_challenge_menu_hr" parent="menu_hr_gamification" action="challenge_list_action2" groups="base.group_hr_user"/>
<menuitem id="gamification_goal_menu_hr" parent="menu_hr_gamification" action="goals_menu_groupby_action2" groups="base.group_hr_user"/>
</data>
</openerp>
+22
View File
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import grant_badge
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.osv import fields, osv
from openerp.tools.translate import _
class hr_grant_badge_wizard(osv.TransientModel):
_name = 'gamification.badge.user.wizard'
_inherit = ['gamification.badge.user.wizard']
_columns = {
'employee_id': fields.many2one("hr.employee", string='Employee', required=True),
'user_id': fields.related("employee_id", "user_id",
type="many2one", relation="res.users",
store=True, string='User')
}
def action_grant_badge(self, cr, uid, ids, context=None):
"""Wizard action for sending a badge to a chosen employee"""
if context is None:
context = {}
badge_user_obj = self.pool.get('gamification.badge.user')
for wiz in self.browse(cr, uid, ids, context=context):
if not wiz.user_id:
raise osv.except_osv(_('Warning!'), _('You can send badges only to employees linked to a user.'))
if uid == wiz.user_id.id:
raise osv.except_osv(_('Warning!'), _('You can not send a badge to yourself'))
values = {
'user_id': wiz.user_id.id,
'sender_id': uid,
'badge_id': wiz.badge_id.id,
'employee_id': wiz.employee_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)
return result
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="view_badge_wizard_grant_employee" model="ir.ui.view">
<field name="name">Grant Badge Employee Form</field>
<field name="model">gamification.badge.user.wizard</field>
<field name="inherit_id" ref="gamification.view_badge_wizard_grant" />
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='user_id']" position="replace">
<field name="employee_id" nolabel="1" domain="[('user_id', '!=', False),('user_id', '!=', uid)]" />
</xpath>
</data>
</field>
</record>
<record id="view_badge_wizard_reward" model="ir.ui.view">
<field name="name">Reward Employee Badge Form</field>
<field name="model">gamification.badge.user.wizard</field>
<field name="arch" type="xml">
<form string="Reward Employee with" version="7.0">
What are you thank for?
<group>
<field name="employee_id" invisible="1" />
<field name="badge_id" nolabel="1" colspan="4" />
<field name="comment" nolabel="1" placeholder="Describe what they did and why it matters (will be public)" />
</group>
<footer>
<button string="Reward Employee" type="object" name="action_grant_badge" class="oe_highlight" /> or
<button string="Cancel" special="cancel" class="oe_link"/>
</footer>
</form>
</field>
</record>
<act_window domain="[]" id="action_reward_wizard"
name="Reward Employee"
target="new"
res_model="gamification.badge.user.wizard"
context="{'default_employee_id': active_id, 'employee_id': active_id}"
view_type="form" view_mode="form"
view_id="view_badge_wizard_reward"/>
</data>
</openerp>
+21
View File
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
+42
View File
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Portal Gamification',
'version': '1',
'category': 'Tools',
'complexity': 'easy',
'description': """
This module adds security rules for gamification to allow portal users to participate to challenges
===================================================================================================
""",
'author': 'OpenERP SA',
'depends': ['gamification','portal'],
'data': [
'security/ir.model.access.csv',
'security/portal_security.xml',
],
'installable': True,
'auto_install': True,
'category': 'Hidden',
}
@@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
goal_portal,"Goal Portal",gamification.model_gamification_goal,portal.group_portal,1,1,0,0
goal_definition_portal,"Goal Definition Portal",gamification.model_gamification_goal_definition,portal.group_portal,1,0,0,0
challenge_portal,"Goal Challenge Portal",gamification.model_gamification_challenge,portal.group_portal,1,0,0,0
challenge_line_portal,"Challenge Line Portal",gamification.model_gamification_challenge_line,portal.group_portal,1,0,0,0
badge_portal,"Badge Portal",gamification.model_gamification_badge,portal.group_portal,1,0,0,0
badge_user_portal,"Badge-user Portal",gamification.model_gamification_badge_user,portal.group_portal,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 goal_portal Goal Portal gamification.model_gamification_goal portal.group_portal 1 1 0 0
3 goal_definition_portal Goal Definition Portal gamification.model_gamification_goal_definition portal.group_portal 1 0 0 0
4 challenge_portal Goal Challenge Portal gamification.model_gamification_challenge portal.group_portal 1 0 0 0
5 challenge_line_portal Challenge Line Portal gamification.model_gamification_challenge_line portal.group_portal 1 0 0 0
6 badge_portal Badge Portal gamification.model_gamification_badge portal.group_portal 1 0 0 0
7 badge_user_portal Badge-user Portal gamification.model_gamification_badge_user portal.group_portal 1 1 1 0
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="gamification.goal_user_visibility" model="ir.rule">
<field name="groups" eval="[(4, ref('portal.group_portal'))]"/>
</record>
</data>
</openerp>