diff options
| author | Atul Patel (OpenERP) <atp@tinyerp.com> | 2014-02-18 11:26:18 +0530 |
|---|---|---|
| committer | Atul Patel (OpenERP) <atp@tinyerp.com> | 2014-02-18 11:26:18 +0530 |
| commit | 33b99c8a0737e08e6a243e41bb455d3ab7e6c851 (patch) | |
| tree | a1db52bfbeecf0c6fd9096604b7ec4540bb04197 /addons/gamification | |
| parent | caa3510ea434e10c32dcbc134f9c74b2b9f18b2a (diff) | |
| parent | aa0eac1257b97c1be693e8a804fec6ecbf2bb22f (diff) | |
[MERGE]: Merged with addons
bzr revid: atp@tinyerp.com-20140218055618-r36w29vat7zig9ar
Diffstat (limited to 'addons/gamification')
34 files changed, 5595 insertions, 0 deletions
diff --git a/addons/gamification/__init__.py b/addons/gamification/__init__.py new file mode 100644 index 00000000000..21f9c9491ac --- /dev/null +++ b/addons/gamification/__init__.py @@ -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 diff --git a/addons/gamification/__openerp__.py b/addons/gamification/__openerp__.py new file mode 100644 index 00000000000..c522e287725 --- /dev/null +++ b/addons/gamification/__openerp__.py @@ -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'], +} diff --git a/addons/gamification/data/badge.xml b/addons/gamification/data/badge.xml new file mode 100644 index 00000000000..5a390be9deb --- /dev/null +++ b/addons/gamification/data/badge.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data noupdate="1"> + <record id="badge_good_job" model="gamification.badge"> + <field name="name">Good Job</field> + <field name="description">You did great at your job.</field> + <field name="rule_auth">everyone</field> + <field name="image" type="base64" file="gamification/static/img/badge_good_job-image.png"/> + </record> + + <record id="badge_problem_solver" model="gamification.badge"> + <field name="name">Problem Solver</field> + <field name="description">No one can solve challenges like you do.</field> + <field name="rule_auth">everyone</field> + <field name="image" type="base64" file="gamification/static/img/badge_problem_solver-image.png"/> + </record> + + <record id="badge_hidden" model="gamification.badge"> + <field name="name">Hidden</field> + <field name="description">You have found the hidden badge</field> + <field name="rule_auth">nobody</field> + <field name="image" type="base64" file="gamification/static/img/badge_hidden-image.png"/> + <field name="active" eval="False" /> + </record> + + + <record id="badge_idea" model="gamification.badge"> + <field name="name">Brilliant</field> + <field name="description">With your brilliant ideas, you are an inspiration to others.</field> + <field name="rule_auth">everyone</field> + <field name="rule_max">True</field> + <field name="rule_max_number">2</field> + <field name="image" type="base64" file="gamification/static/img/badge_idea-image.png"/> + </record> + </data> + + + <data noupdate="0"> + + <record id="email_template_badge_received" model="email.template"> + <field name="name">Received Badge</field> + <field name="body_html"><![CDATA[ + <p>Congratulation, you have received the badge <strong>${object.badge_id.name}</strong> ! + % if object.sender_id + This badge was granted by <strong>${object.sender_id.name}</strong>. + % endif + </p> + + % if object.comment + <p><em>${object.comment}</em></p> + % endif + ]]></field> + </record> + </data> +</openerp> diff --git a/addons/gamification/data/cron.xml b/addons/gamification/data/cron.xml new file mode 100644 index 00000000000..8585771c97d --- /dev/null +++ b/addons/gamification/data/cron.xml @@ -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>
\ No newline at end of file diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml new file mode 100644 index 00000000000..66dd7be7431 --- /dev/null +++ b/addons/gamification/data/goal_base.xml @@ -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> diff --git a/addons/gamification/html/index.html b/addons/gamification/html/index.html new file mode 100644 index 00000000000..890efa0d1ec --- /dev/null +++ b/addons/gamification/html/index.html @@ -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> diff --git a/addons/gamification/i18n/gamification.pot b/addons/gamification/i18n/gamification.pot new file mode 100644 index 00000000000..0ef4cdbea64 --- /dev/null +++ b/addons/gamification/i18n/gamification.pot @@ -0,0 +1,1733 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * gamification +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 8.0alpha1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-02-13 15:08+0000\n" +"PO-Revision-Date: 2014-02-13 15:08+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Search Goals" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_base_company_data +msgid "Set your Company Data" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Never" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,computation_mode:0 +msgid "Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.goal:0 +#: field:gamification.goal.wizard,goal_id:0 +msgid "Goal" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +msgid "Non recurring" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,next_report_date:0 +msgid "Next Report Date" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,monetary:0 +msgid "Monetary Value" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "My Goals" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,condition:0 +msgid "The lower the better" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +#: view:gamification.goal.wizard:0 +msgid "Grant Badge To" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_count_distinct:0 +msgid "The number of time this badge has been received by unique users." +msgstr "" + +#. module: gamification +#: help:gamification.badge,unique_owner_ids:0 +msgid "The list of unique users having received this badge." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.goal:0 +#: view:gamification.goal.definition:0 +msgid "Group By..." +msgstr "" + +#. module: gamification +#: model:gamification.challenge,name:gamification.challenge_base_configure +msgid "Setup your Company" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "/∞" +msgstr "" + +#. module: gamification +#: code:addons/gamification/wizard/grant_badge.py:44 +#, python-format +msgid "You can not grant a badge to yourself" +msgstr "" + +#. module: gamification +#: field:gamification.goal,completeness:0 +msgid "Completeness" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,field_date_id:0 +msgid "Date Field" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_my_this_month:0 +msgid "The number of time the current user has received this badge this month." +msgstr "" + +#. module: gamification +#: selection:gamification.challenge.line,condition:0 +msgid ">=" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,name:gamification.challenge_wizard +msgid "Challenge Description" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_badge_user_wizard +msgid "gamification.badge.user.wizard" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Reward" +msgstr "" + +#. module: gamification +#: help:gamification.badge,message_ids:0 +#: help:gamification.challenge,message_ids:0 +#: help:gamification.goal,message_ids:0 +msgid "Messages and communication history" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,category:0 +msgid "Define the visibility of the challenge through menus" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Refresh Challenge" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_challenge_line +msgid "Gamification generic goal for challenge" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "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)" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,definition_monetary:0 +msgid "Monetary" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "In progress (to update)" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_goal_definition +msgid "Gamification goal definition" +msgstr "" + +#. module: gamification +#: help:gamification.badge,remaining_sending:0 +msgid "If a maxium is set" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,invited_user_ids:0 +msgid "Suggest to users" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: model:ir.actions.act_window,name:gamification.goal_list_action +#: model:ir.ui.menu,name:gamification.gamification_goal_menu +msgid "Goals" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Badge Description" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "than the target." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "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." +msgstr "" + +#. module: gamification +#: help:gamification.challenge,manager_id:0 +msgid "The user responsible for the challenge." +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/goal.py:385 +#, python-format +msgid "Update %s" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,description:0 +msgid "Goal Description" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "this month" +msgstr "" + +#. module: gamification +#: field:gamification.badge,owner_ids:0 +msgid "Owners" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,sender_id:0 +msgid "Sender" +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_my_this_month:0 +msgid "My Monthly Total" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,remind_update_delay:0 +msgid "Never reminded if no value or zero is specified." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: field:gamification.challenge.line,challenge_id:0 +#: field:gamification.goal,challenge_id:0 +msgid "Challenge" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,condition:0 +msgid "Goal Performance" +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_ids:0 +#: field:gamification.challenge,message_ids:0 +#: field:gamification.goal,message_ids:0 +msgid "Messages" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "badges this month" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:517 +#: code:addons/gamification/models/goal.py:351 +#, python-format +msgid "Error!" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,line_ids:0 +msgid "Lines" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user:0 +msgid "Granted by" +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_count_distinct:0 +msgid "Number of users" +msgstr "" + +#. module: gamification +#: help:gamification.badge,message_unread:0 +#: help:gamification.challenge,message_unread:0 +#: help:gamification.goal,message_unread:0 +msgid "If checked new messages require your attention." +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,name:gamification.action_new_simplified_res_users +msgid "Create User" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Yearly" +msgstr "" + +#. module: gamification +#: selection:gamification.badge,rule_auth:0 +msgid "People having some badges" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "granted," +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,definition_full_suffix:0 +#: field:gamification.goal,definition_suffix:0 +#: field:gamification.goal.definition,suffix:0 +msgid "Suffix" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Reference" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,autojoin_group_id:0 +msgid "Auto-subscription Group" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "refresh" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Even if the challenge is failed, best challengers will be rewarded" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_id:0 +msgid "For Every Succeding User" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,target_goal:0 +msgid "Target Value to Reach" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,start_date:0 +msgid "The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Notification Messages" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,manager_id:0 +msgid "Responsible" +msgstr "" + +#. module: gamification +#. openerp-web +#: code:addons/gamification/static/src/xml/gamification.xml:108 +#, python-format +msgid "more details..." +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,name:gamification.badge_list_action +#: model:ir.ui.menu,name:gamification.gamification_badge_menu +msgid "Badges" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_auth_badge_ids:0 +msgid "Only the people having these badges can give this badge" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,computation_mode:0 +msgid "Recorded manually" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Statistics" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "From" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Granting" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Grant" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "e.g. days" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,definition_id:0 +#: view:gamification.goal:0 +#: field:gamification.goal,definition_id:0 +#: field:gamification.goal.definition,name:0 +msgid "Goal Definition" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,sequence:0 +msgid "Sequence" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "Failed" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: field:gamification.challenge,state:0 +#: view:gamification.goal:0 +#: field:gamification.goal,state:0 +msgid "State" +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_follower_ids:0 +#: field:gamification.challenge,message_follower_ids:0 +#: field:gamification.goal,message_follower_ids:0 +msgid "Followers" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_first_id:0 +msgid "For 1st user" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Can not grant" +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_unread:0 +#: field:gamification.challenge,message_unread:0 +#: field:gamification.goal,message_unread:0 +msgid "Unread Messages" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,display_mode:0 +msgid "Displayed as" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +#: field:gamification.badge,name:0 +#: field:gamification.badge.user,badge_id:0 +#: field:gamification.badge.user.wizard,badge_id:0 +msgid "Badge" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,suffix:0 +msgid "The unit of the target and current values" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: model:ir.actions.act_window,name:gamification.goals_from_challenge_act +msgid "Related Goals" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Subscriptions" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +#: model:ir.actions.act_window,name:gamification.action_grant_wizard +msgid "Grant Badge" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:683 +#, python-format +msgid "<br/>Reward (badge %s) for every succeeding user was sent to %s." +msgstr "" + +#. module: gamification +#: help:gamification.badge,message_summary:0 +#: help:gamification.challenge,message_summary:0 +#: help:gamification.goal,message_summary:0 +msgid "Holds the Chatter summary (number of messages, ...). This summary is directly in html format in order to be inserted in kanban views." +msgstr "" + +#. module: gamification +#: field:gamification.challenge,report_message_group_id:0 +msgid "Send a copy to" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.challenge_list_action +msgid "<p class=\"oe_view_nocontent_create\">\n" +" Click to create a challenge. \n" +" </p>\n" +" <p>\n" +" Assign a list of goals to chosen users to evaluate them.\n" +" The challenge can use a period (weekly, monthly...) for automatic creation of goals.\n" +" The goals are created for the specified users or member of the group.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:695 +#, python-format +msgid "Nobody reached the required conditions to receive special badges." +msgstr "" + +#. module: gamification +#. openerp-web +#: code:addons/gamification/static/src/xml/gamification.xml:35 +#: code:addons/gamification/static/src/xml/gamification.xml:57 +#, python-format +msgid "Target: <=" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Reminders for Manual Goals" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/goal.py:265 +#, python-format +msgid "Invalid return content from the evaluation of %s" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +#: field:gamification.goal.definition,computation_mode:0 +msgid "Computation Mode" +msgstr "" + +#. module: gamification +#: model:email.template,body_html:gamification.simple_report_template +msgid "<header><strong>${object.name}</strong>\n" +"</header><p class=\"oe_grey\">The following message contains the current progress for the challenge ${object.name}</p>\n" +"\n" +"% if object.visibility_mode == 'personal':\n" +" <table width=\"100%\" border=\"1\"><tr><th>Goal</th>\n" +" <th>Target</th>\n" +" <th>Current</th>\n" +" <th>Completeness</th>\n" +" </tr>\n" +" % for line in ctx[\"challenge_lines\"]:\n" +" <tr if line>= 100:\n" +" style=\"font-weight:bold;\"\n" +" % endif\n" +" >\n" +" <td>${line['name']}</td>\n" +" <td>${line['target']}\n" +" % if line['suffix']:\n" +" ${line['suffix']}\n" +" % endif\n" +" </td>\n" +" <td>${line['current']}\n" +" % if line['suffix']:\n" +" ${line['suffix']}\n" +" % endif\n" +" </td>\n" +" <td>${line['completeness']} %</td>\n" +" </tr>\n" +" % endfor\n" +" </table>\n" +"% else:\n" +" % for line in ctx[\"challenge_lines\"]:\n" +" <table width=\"100%\" border=\"1\"><tr><th colspan=\"4\">${line['name']}</th>\n" +" </tr><tr><th>#</th>\n" +" <th>Person</th>\n" +" <th>Completeness</th>\n" +" <th>Current</th>\n" +" </tr>\n" +" % for goal in line['goals']:\n" +" <tr if goal.completeness>= 100:\n" +" style=\"font-weight:bold;\"\n" +" % endif\n" +" >\n" +" <td>${goal['rank']}</td>\n" +" <td>${goal['name']}</td>\n" +" <td>${goal['completeness']}%</td>\n" +" <td>${goal['current']}/${line['target']}\n" +" % if line['suffix']:\n" +" ${line['suffix']}\n" +" % endif\n" +" </td>\n" +" </tr>\n" +" % endfor\n" +" </table><br><br>\n" +"\n" +" % endfor\n" +"% endif\n" +" " +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,condition:0 +msgid "A goal is considered as completed when the current value is compared to the value to reach" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,remind_update_delay:0 +msgid "Non-updated manual goals will be reminded after" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,full_suffix:0 +msgid "The currency and suffix field" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,display_mode:0 +msgid "Exclusive (done or not-done)" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:223 +#: code:addons/gamification/models/badge.py:225 +#: code:addons/gamification/models/badge.py:227 +#: code:addons/gamification/models/badge.py:229 +#: code:addons/gamification/wizard/grant_badge.py:44 +#, python-format +msgid "Warning!" +msgstr "" + +#. module: gamification +#: field:gamification.goal,last_update:0 +msgid "Last Update" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,report_message_frequency:0 +msgid "Report Frequency" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,field_id:0 +msgid "The field containing the value to evaluate" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,end_date:0 +msgid "The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date." +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,create_date:0 +msgid "Created" +msgstr "" + +#. module: gamification +#: model:gamification.badge,name:gamification.badge_idea +msgid "Brilliant" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,visibility_mode:0 +msgid "Individual Goals" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Invited" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,report_message_frequency:0 +msgid "On change" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Daily" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.goal.definition:0 +msgid "Goal definitions" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Participating" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_auth:0 +msgid "Who can grant this badge" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,last_report_date:0 +msgid "Last Report Date" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,computation_mode:0 +msgid "Automatic: execute a specific Python code" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_goal_wizard +msgid "gamification.goal.wizard" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_badge_user +msgid "Gamification user badge" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Describe the challenge: what is does, who it targets, why it matters..." +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Goal List" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,comment:0 +#: field:gamification.badge.user.wizard,comment:0 +msgid "Comment" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "In progress" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Search Challenges" +msgstr "" + +#. module: gamification +#: selection:gamification.badge,rule_auth:0 +msgid "No one, assigned through challenges" +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_my:0 +msgid "My Total" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "reply later" +msgstr "" + +#. module: gamification +#: field:gamification.badge,image:0 +msgid "Image" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Running Challenges" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,category:0 +msgid "Appears in" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Passed" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "Clickable Goals" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "Canceled" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,badge_name:0 +msgid "Badge Name" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,definition_suffix:0 +msgid "Unit" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:229 +#, python-format +msgid "You have already sent this badge too many time this month." +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_my_monthly_sending:0 +msgid "My Monthly Sending Total" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Check Badge" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Weekly" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Assign Challenge To" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +#: view:gamification.challenge:0 +#: view:gamification.goal.wizard:0 +msgid "or" +msgstr "" + +#. module: gamification +#: help:gamification.challenge.line,sequence:0 +msgid "Sequence number for ordering" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Category" +msgstr "" + +#. module: gamification +#: field:gamification.goal,current:0 +msgid "Current Value" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,field_date_id:0 +msgid "The date to use for the time period evaluated" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.goals_from_challenge_act +msgid "<p>\n" +" There is no goals associated to this challenge matching your search.\n" +" Make sure that your challenge is active and assigned to at least one user.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.badge_list_action +msgid "<p class=\"oe_view_nocontent_create\">\n" +" Click to create a badge. \n" +" </p>\n" +" <p>\n" +" A badge is a symbolic token granted to a user as a sign of reward.\n" +" It can be deserved automatically when some conditions are met or manually by users.\n" +" Some badges are harder than others to get with specific conditions.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_auth_badge_ids:0 +msgid "Required Badges" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Line List" +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_max:0 +msgid "Monthly Limited Sending" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:517 +#, python-format +msgid "Retrieving progress for personal challenge without user information" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_base_timezone +msgid "Set your Timezone" +msgstr "" + +#. module: gamification +#: help:gamification.badge,goal_definition_ids:0 +msgid "The users that have succeeded theses goals will receive automatically the badge." +msgstr "" + +#. module: gamification +#: help:gamification.badge,image:0 +msgid "This field holds the image used for the badge, limited to 256x256" +msgstr "" + +#. module: gamification +#: view:gamification.goal.wizard:0 +msgid "Update" +msgstr "" + +#. module: gamification +#: field:gamification.goal,line_id:0 +msgid "Goal Line" +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_max_number:0 +msgid "Limitation Number" +msgstr "" + +#. module: gamification +#: model:ir.module.category,name:gamification.module_goal_category +msgid "Gamification" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,state:0 +#: view:gamification.goal:0 +#: selection:gamification.goal,state:0 +msgid "Draft" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Reject" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,model_id:0 +msgid "The model object for the field to evaluate" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.goal_definition_list_action +msgid "<p class=\"oe_view_nocontent_create\">\n" +" Click to create a goal definition. \n" +" </p>\n" +" <p>\n" +" A goal definition is a technical model of goal defining a condition to reach.\n" +" The dates, values to reach or users are defined in goal instance.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: help:gamification.badge.user,sender_id:0 +msgid "The user who has send the badge" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_count:0 +msgid "The number of time this badge has been received." +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +#: field:gamification.goal.definition,model_id:0 +msgid "Model" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_challenge +msgid "Gamification challenge" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_max:0 +msgid "Check to set a monthly limit per person of sending this badge" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_badge +msgid "Gamification badge" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.action_new_simplified_res_users +msgid "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." +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Security rules to define who is allowed to manually grant badges. Not enforced for administrator." +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,display_mode:0 +msgid "Progressive (using numerical values)" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_goal +msgid "Gamification goal instance" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:685 +#, python-format +msgid "<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge." +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_is_follower:0 +#: field:gamification.challenge,message_is_follower:0 +#: field:gamification.goal,message_is_follower:0 +msgid "Is a Follower" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,full_suffix:0 +msgid "Full Suffix" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Advanced Options" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,start_date:0 +#: field:gamification.goal,start_date:0 +msgid "Start Date" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_second_id:0 +msgid "For 2nd user" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Data" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,condition:0 +msgid "Condition" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,user_id:0 +#: field:gamification.badge.user.wizard,user_id:0 +#: view:gamification.goal:0 +#: field:gamification.goal,user_id:0 +msgid "User" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,domain:0 +msgid "Filter Domain" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Schedule" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,computation_mode:0 +msgid "Automatic: number of records" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_max_number:0 +msgid "The maximum number of time this badge can be sent per month per person." +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_res_groups +msgid "Access Groups" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "Formating Options" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,compute_code:0 +msgid "Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id." +msgstr "" + +#. module: gamification +#: field:gamification.challenge,user_ids:0 +#: model:ir.model,name:gamification.model_res_users +msgid "Users" +msgstr "" + +#. module: gamification +#: selection:gamification.badge,rule_auth:0 +msgid "A selected list of users" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.challenge.line:0 +msgid "Challenge Lines" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_auth_user_ids:0 +msgid "Only these people can give this badge" +msgstr "" + +#. module: gamification +#: field:gamification.badge,active:0 +msgid "Active" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,action_id:0 +msgid "The action that will be called to update the goal value." +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "You can still grant" +msgstr "" + +#. module: gamification +#: field:gamification.goal,computation_mode:0 +msgid "Computation mode" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,action_id:0 +msgid "Action" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_base_invite +msgid "Invite new Users" +msgstr "" + +#. module: gamification +#: field:gamification.goal,definition_description:0 +msgid "Definition Description" +msgstr "" + +#. module: gamification +#: model:gamification.badge,name:gamification.badge_problem_solver +msgid "Problem Solver" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Target: less than" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_this_month:0 +msgid "The number of time this badge has been received this month." +msgstr "" + +#. module: gamification +#: help:gamification.goal,last_update:0 +msgid "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." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Depending on the Display mode, reports will be individual or shared." +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Start goal" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,period:0 +msgid "Periodicity" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,create_uid:0 +msgid "Creator" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:692 +#, python-format +msgid "<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,compute_code:0 +msgid "Python Code" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Rewards for challenges" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Send Report" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Period" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,name:0 +msgid "Challenge Name" +msgstr "" + +#. module: gamification +#: model:ir.ui.menu,name:gamification.gamification_menu +msgid "Gamification Tools" +msgstr "" + +#. module: gamification +#: field:gamification.badge,goal_definition_ids:0 +msgid "Rewarded by" +msgstr "" + +#. module: gamification +#: field:gamification.badge,remaining_sending:0 +msgid "Remaining Sending Allowed" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,state:0 +msgid "Done" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,visibility_mode:0 +#: field:gamification.goal,definition_display:0 +msgid "Display Mode" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +#: view:gamification.goal.wizard:0 +msgid "Cancel" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "There is no reward upon completion of this challenge." +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,monetary:0 +msgid "The target and current value are defined in the company currency." +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,condition:0 +msgid "The higher the better" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_base_company_logo +msgid "Set your Company Logo" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,state:0 +msgid "In Progress" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Goal Reached" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_my:0 +msgid "The number of time the current user has received this badge." +msgstr "" + +#. module: gamification +#: help:gamification.challenge,report_message_group_id:0 +msgid "Group that will receive a copy of the report in addition to the user" +msgstr "" + +#. module: gamification +#: field:gamification.goal,target_goal:0 +msgid "To Reach" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,res_id_field:0 +msgid "The field name on the user profile (res.users) containing the value for res_id for action." +msgstr "" + +#. module: gamification +#: help:gamification.goal,remind_update_delay:0 +msgid "The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified." +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +msgid "Who would you like to reward?" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge.line,condition:0 +msgid "<=" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Configure Challenge" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Monthly" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,user_ids:0 +msgid "List of users participating to the challenge" +msgstr "" + +#. module: gamification +#: model:email.template,body_html:gamification.email_template_goal_reminder +msgid "<header><strong>Reminder ${object.name}</strong>\n" +" </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>\n" +"\n" +" <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>" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +#: field:gamification.goal.wizard,current:0 +msgid "Current" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "To" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:225 +#, python-format +msgid "You are not in the user allowed list." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "í" +msgstr "" + +#. module: gamification +#: field:gamification.badge,challenge_ids:0 +msgid "Reward of Challenges" +msgstr "" + +#. module: gamification +#: field:gamification.goal,definition_condition:0 +msgid "Definition Condition" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,autojoin_group_id:0 +msgid "Group of users whose members will be automatically added to user_ids once the challenge is started" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +#: model:ir.actions.act_window,name:gamification.goal_definition_list_action +#: model:ir.ui.menu,name:gamification.gamification_definition_menu +msgid "Goal Definitions" +msgstr "" + +#. module: gamification +#: model:gamification.badge,name:gamification.badge_hidden +msgid "Hidden" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.goal:0 +msgid "days" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,report_template_id:0 +msgid "Report Template" +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_auth:0 +msgid "Allowance to Grant" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,computation_mode:0 +msgid "Automatic: sum on a field" +msgstr "" + +#. module: gamification +#: model:gamification.challenge,name:gamification.challenge_base_discover +msgid "Complete your Profile" +msgstr "" + +#. module: gamification +#: field:gamification.badge,description:0 +#: field:gamification.challenge,description:0 +msgid "Description" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:223 +#, python-format +msgid "This badge can not be sent by users." +msgstr "" + +#. module: gamification +#: model:email.template,body_html:gamification.email_template_badge_received +msgid "<p>Congratulation, you have received the badge <strong>${object.badge_id.name}</strong> !\n" +" % if object.sender_id\n" +" This badge was granted by <strong>${object.sender_id.name}</strong>.\n" +" % endif\n" +" </p>\n" +"\n" +" % if object.comment\n" +" <p><em>${object.comment}</em></p>\n" +" % endif\n" +" " +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_this_month:0 +msgid "Monthly total" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_third_id:0 +msgid "For 3rd user" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +msgid "Describe what they did and why it matters (will be public)" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:227 +#, python-format +msgid "You do not have the required badges." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "e.g. Monthly Sales Objectives" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,line_ids:0 +msgid "List of goals that will be set" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Badge List" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,visibility_mode:0 +msgid "Leader Board (Group Ranking)" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Reached when current value is" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/goal.py:351 +#, python-format +msgid "Can not modify the configuration of a started goal" +msgstr "" + +#. module: gamification +#: field:gamification.badge,unique_owner_ids:0 +msgid "Unique Owners" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:665 +#, python-format +msgid "The challenge %s is finished." +msgstr "" + +#. module: gamification +#: field:gamification.challenge,end_date:0 +#: view:gamification.goal:0 +#: field:gamification.goal,end_date:0 +msgid "End Date" +msgstr "" + +#. module: gamification +#: help:gamification.badge,owner_ids:0 +msgid "The list of instances of this badge granted to users" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Goal Failed" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,res_id_field:0 +msgid "ID Field of user" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,name:0 +msgid "Name" +msgstr "" + +#. module: gamification +#. openerp-web +#: code:addons/gamification/static/src/xml/gamification.xml:105 +#, python-format +msgid "Invited Challenges" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Reset Completion" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,domain:0 +msgid "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)]." +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_my_monthly_sending:0 +msgid "The number of time the current user has sent this badge this month." +msgstr "" + +#. module: gamification +#: help:gamification.goal,challenge_id:0 +msgid "Challenge that generated the goal, assign challenge to users to generate goals with a value in this field." +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_auth_user_ids:0 +msgid "Authorized Users" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.goal_list_action +msgid "<p class=\"oe_view_nocontent_create\">\n" +" Click to create a goal. \n" +" </p>\n" +" <p>\n" +" A goal is defined by a user and a goal definition.\n" +" Goals can be created automatically by using challenges.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_failure:0 +msgid "Reward Bests if not Succeeded?" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,field_id:0 +msgid "Field to Sum" +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_summary:0 +#: field:gamification.challenge,message_summary:0 +#: field:gamification.goal,message_summary:0 +msgid "Summary" +msgstr "" + +#. module: gamification +#: model:gamification.badge,name:gamification.badge_good_job +msgid "Good Job" +msgstr "" + +#. module: gamification +#. openerp-web +#: code:addons/gamification/static/src/xml/gamification.xml:32 +#: code:addons/gamification/static/src/xml/gamification.xml:54 +#, python-format +msgid "Target:" +msgstr "" + +#. module: gamification +#: view:gamification.goal.wizard:0 +msgid "Set the current value you have reached for this goal" +msgstr "" + +#. module: gamification +#: selection:gamification.badge,rule_auth:0 +msgid "Everyone" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "How to compute the goal?" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "Search Goal Definitions" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_nbr_following +msgid "Mail Group Following" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Accept" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Grant this Badge" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: model:ir.actions.act_window,name:gamification.challenge_list_action +#: model:ir.ui.menu,name:gamification.gamification_challenge_menu +msgid "Challenges" +msgstr "" + +#. module: gamification +#: model:res.groups,name:gamification.group_goal_manager +msgid "Manager" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "HR Challenges" +msgstr "" + +#. module: gamification +#: field:gamification.goal,remind_update_delay:0 +msgid "Remind delay" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "No monthly sending limit" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "Reached" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user:0 +msgid "the" +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_count:0 +msgid "Total" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,period:0 +msgid "Period of automatic goal assigment. If none is selected, should be launched manually." +msgstr "" + diff --git a/addons/gamification/models/__init__.py b/addons/gamification/models/__init__.py new file mode 100644 index 00000000000..812a08b1e19 --- /dev/null +++ b/addons/gamification/models/__init__.py @@ -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 diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py new file mode 100644 index 00000000000..1d879393ed2 --- /dev/null +++ b/addons/gamification/models/badge.py @@ -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
\ No newline at end of file diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py new file mode 100644 index 00000000000..fec8e04c4b7 --- /dev/null +++ b/addons/gamification/models/challenge.py @@ -0,0 +1,817 @@ +# -*- 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: + # FIXME: allow to restrict to a subset of users + 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) + if canceled_goal_ids: + 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 + 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, + }) + if goal_ids: + 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, + } diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py new file mode 100644 index 00000000000..8e79c43c8a6 --- /dev/null +++ b/addons/gamification/models/goal.py @@ -0,0 +1,395 @@ +# -*- 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, readonly=True, + help="Challenge that generated the goal, assign challenge to users to generate goals with a value in this field."), + 'start_date': fields.date('Start Date'), + 'end_date': fields.date('End Date'), # no start and end = always active + 'target_goal': fields.float('To Reach', + required=True, + track_visibility='always'), # no goal = global index + 'current': fields.float('Current Value', required=True, track_visibility='always'), + 'completeness': fields.function(_get_completion, type='float', string='Completeness'), + 'state': fields.selection([ + ('draft', 'Draft'), + ('inprogress', 'In progress'), + ('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 diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py new file mode 100644 index 00000000000..d25d725a4ac --- /dev/null +++ b/addons/gamification/models/res_users.py @@ -0,0 +1,137 @@ +# -*- 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) + challenge_obj.generate_goals_from_challenge(cr, uid, challenge_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) + challenge_obj.generate_goals_from_challenge(cr, uid, challenge_ids, 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 + lines = challenge_obj._get_serialized_challenge_lines(cr, uid, challenge, user_id, restrict_top=MAX_VISIBILITY_RANKING, context=context) + if lines: + all_goals_info.append({ + 'id': challenge.id, + 'name': challenge.name, + 'visibility_mode': challenge.visibility_mode, + 'currency': user.company_id.currency_id.id, + 'lines': lines, + }) + + 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) + challenge_obj.generate_goals_from_challenge(cr, uid, challenge_ids, context=context) + return write_res diff --git a/addons/gamification/security/gamification_security.xml b/addons/gamification/security/gamification_security.xml new file mode 100644 index 00000000000..904ba50721e --- /dev/null +++ b/addons/gamification/security/gamification_security.xml @@ -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), + '&', + ('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> diff --git a/addons/gamification/security/ir.model.access.csv b/addons/gamification/security/ir.model.access.csv new file mode 100644 index 00000000000..932c5d97c62 --- /dev/null +++ b/addons/gamification/security/ir.model.access.csv @@ -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 diff --git a/addons/gamification/static/description/icon.png b/addons/gamification/static/description/icon.png Binary files differnew file mode 100644 index 00000000000..52a75f726ba --- /dev/null +++ b/addons/gamification/static/description/icon.png diff --git a/addons/gamification/static/img/badge_good_job-image.png b/addons/gamification/static/img/badge_good_job-image.png Binary files differnew file mode 100644 index 00000000000..293bb019e7b --- /dev/null +++ b/addons/gamification/static/img/badge_good_job-image.png diff --git a/addons/gamification/static/img/badge_hidden-image.png b/addons/gamification/static/img/badge_hidden-image.png Binary files differnew file mode 100644 index 00000000000..2b9040d5764 --- /dev/null +++ b/addons/gamification/static/img/badge_hidden-image.png diff --git a/addons/gamification/static/img/badge_idea-image.png b/addons/gamification/static/img/badge_idea-image.png Binary files differnew file mode 100644 index 00000000000..5f4917b263d --- /dev/null +++ b/addons/gamification/static/img/badge_idea-image.png diff --git a/addons/gamification/static/img/badge_problem_solver-image.png b/addons/gamification/static/img/badge_problem_solver-image.png Binary files differnew file mode 100644 index 00000000000..52a75f726ba --- /dev/null +++ b/addons/gamification/static/img/badge_problem_solver-image.png diff --git a/addons/gamification/static/src/css/Makefile b/addons/gamification/static/src/css/Makefile new file mode 100644 index 00000000000..3a6a70cf59b --- /dev/null +++ b/addons/gamification/static/src/css/Makefile @@ -0,0 +1,3 @@ +gamification.css: gamification.sass + sass --trace -t expanded gamification.sass gamification.css + diff --git a/addons/gamification/static/src/css/gamification.css b/addons/gamification/static/src/css/gamification.css new file mode 100644 index 00000000000..4ac3a0340fa --- /dev/null +++ b/addons/gamification/static/src/css/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; +} diff --git a/addons/gamification/static/src/css/gamification.sass b/addons/gamification/static/src/css/gamification.sass new file mode 100644 index 00000000000..3bb6b570e66 --- /dev/null +++ b/addons/gamification/static/src/css/gamification.sass @@ -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 diff --git a/addons/gamification/static/src/js/gamification.js b/addons/gamification/static/src/js/gamification.js new file mode 100644 index 00000000000..2a8db5f369d --- /dev/null +++ b/addons/gamification/static/src/js/gamification.js @@ -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); + } + }, + }); + +}; diff --git a/addons/gamification/static/src/xml/gamification.xml b/addons/gamification/static/src/xml/gamification.xml new file mode 100644 index 00000000000..6316ca886f0 --- /dev/null +++ b/addons/gamification/static/src/xml/gamification.xml @@ -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: <= + </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: <= + </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> diff --git a/addons/gamification/tests/__init__.py b/addons/gamification/tests/__init__.py new file mode 100644 index 00000000000..9cafd242b28 --- /dev/null +++ b/addons/gamification/tests/__init__.py @@ -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, +] diff --git a/addons/gamification/tests/test_challenge.py b/addons/gamification/tests/test_challenge.py new file mode 100644 index 00000000000..6b93da727ac --- /dev/null +++ b/addons/gamification/tests/test_challenge.py @@ -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")
\ No newline at end of file diff --git a/addons/gamification/views/badge.xml b/addons/gamification/views/badge.xml new file mode 100644 index 00000000000..0b37e62051d --- /dev/null +++ b/addons/gamification/views/badge.xml @@ -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> diff --git a/addons/gamification/views/challenge.xml b/addons/gamification/views/challenge.xml new file mode 100644 index 00000000000..075afcaccfa --- /dev/null +++ b/addons/gamification/views/challenge.xml @@ -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>
\ No newline at end of file diff --git a/addons/gamification/views/goal.xml b/addons/gamification/views/goal.xml new file mode 100644 index 00000000000..1c408e3ab11 --- /dev/null +++ b/addons/gamification/views/goal.xml @@ -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" /> + </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 < record.last_update.raw_value & record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value < record.last_update.raw_value & 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 > 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_challenge_menu" parent="gamification_menu" action="challenge_list_action" sequence="0"/> + <menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="10"/> + <menuitem id="gamification_definition_menu" parent="gamification_menu" action="goal_definition_list_action" sequence="20"/> + <menuitem id="gamification_badge_menu" parent="gamification_menu" action="badge_list_action" sequence="30"/> + + </data> +</openerp> diff --git a/addons/gamification/wizard/__init__.py b/addons/gamification/wizard/__init__.py new file mode 100644 index 00000000000..638fbef4373 --- /dev/null +++ b/addons/gamification/wizard/__init__.py @@ -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 diff --git a/addons/gamification/wizard/grant_badge.py b/addons/gamification/wizard/grant_badge.py new file mode 100644 index 00000000000..44f739fff4e --- /dev/null +++ b/addons/gamification/wizard/grant_badge.py @@ -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 diff --git a/addons/gamification/wizard/grant_badge.xml b/addons/gamification/wizard/grant_badge.xml new file mode 100644 index 00000000000..e03dbac4096 --- /dev/null +++ b/addons/gamification/wizard/grant_badge.xml @@ -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> diff --git a/addons/gamification/wizard/update_goal.py b/addons/gamification/wizard/update_goal.py new file mode 100644 index 00000000000..cbda3fefbce --- /dev/null +++ b/addons/gamification/wizard/update_goal.py @@ -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 {} diff --git a/addons/gamification/wizard/update_goal.xml b/addons/gamification/wizard/update_goal.xml new file mode 100644 index 00000000000..793efab90ff --- /dev/null +++ b/addons/gamification/wizard/update_goal.xml @@ -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> |
