summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Trigaux <mat@openerp.com>2013-12-24 16:51:38 +0100
committerMartin Trigaux <mat@openerp.com>2013-12-24 16:51:38 +0100
commitaba5888a34b9743a4ae0efc6df858df59b6e9b95 (patch)
tree360abd2f0de843fbd3da94d0741d5b0d5a810cb3
parent68795d8a10c90cbd8321ac86f8da642f6472551d (diff)
parenta42bf998e24eab8fcf498a831c2f49638cd4e4d2 (diff)
[MERGE] [ADD] gamification
New modules gamification, hr_gamification, gamification_sale_crm, portal_gamification. Merry Christmas OpenERP bzr revid: mat@openerp.com-20131224155138-3bbp8m3gyrfsb3ni
-rw-r--r--addons/gamification/__init__.py23
-rw-r--r--addons/gamification/__openerp__.py59
-rw-r--r--addons/gamification/data/badge.xml1142
-rw-r--r--addons/gamification/data/cron.xml16
-rw-r--r--addons/gamification/data/goal_base.xml208
-rw-r--r--addons/gamification/html/index.html86
-rw-r--r--addons/gamification/models/__init__.py25
-rw-r--r--addons/gamification/models/badge.py276
-rw-r--r--addons/gamification/models/challenge.py821
-rw-r--r--addons/gamification/models/goal.py394
-rw-r--r--addons/gamification/models/res_users.py132
-rw-r--r--addons/gamification/security/gamification_security.xml43
-rw-r--r--addons/gamification/security/ir.model.access.csv19
-rw-r--r--addons/gamification/static/description/icon.pngbin0 -> 22343 bytes
-rw-r--r--addons/gamification/static/src/css/Makefile3
-rw-r--r--addons/gamification/static/src/css/gamification.css190
-rw-r--r--addons/gamification/static/src/css/gamification.sass181
-rw-r--r--addons/gamification/static/src/js/gamification.js148
-rw-r--r--addons/gamification/static/src/xml/gamification.xml114
-rw-r--r--addons/gamification/tests/__init__.py26
-rw-r--r--addons/gamification/tests/test_challenge.py89
-rw-r--r--addons/gamification/views/badge.xml194
-rw-r--r--addons/gamification/views/challenge.xml289
-rw-r--r--addons/gamification/views/goal.xml289
-rw-r--r--addons/gamification/wizard/__init__.py23
-rw-r--r--addons/gamification/wizard/grant_badge.py56
-rw-r--r--addons/gamification/wizard/grant_badge.xml33
-rw-r--r--addons/gamification/wizard/update_goal.py44
-rw-r--r--addons/gamification/wizard/update_goal.xml24
-rw-r--r--addons/gamification_sale_crm/__init__.py20
-rw-r--r--addons/gamification_sale_crm/__openerp__.py32
-rw-r--r--addons/gamification_sale_crm/sale_crm_goals.xml173
-rw-r--r--addons/gamification_sale_crm/sale_crm_goals_demo.xml23
-rw-r--r--addons/hr/res_config.py2
-rw-r--r--addons/hr/res_config_view.xml4
-rw-r--r--addons/hr/static/src/css/hr.css2
-rw-r--r--addons/hr_evaluation/hr_evaluation.py4
-rw-r--r--addons/hr_evaluation/security/ir.model.access.csv2
-rw-r--r--addons/hr_gamification/__init__.py23
-rw-r--r--addons/hr_gamification/__openerp__.py40
-rw-r--r--addons/hr_gamification/models/__init__.py22
-rw-r--r--addons/hr_gamification/models/gamification.py113
-rw-r--r--addons/hr_gamification/security/gamification_security.xml17
-rw-r--r--addons/hr_gamification/security/ir.model.access.csv5
-rw-r--r--addons/hr_gamification/static/src/js/gamification.js19
-rw-r--r--addons/hr_gamification/views/gamification.xml104
-rw-r--r--addons/hr_gamification/wizard/__init__.py22
-rw-r--r--addons/hr_gamification/wizard/grant_badge.py60
-rw-r--r--addons/hr_gamification/wizard/grant_badge.xml46
-rw-r--r--addons/portal_gamification/__init__.py21
-rw-r--r--addons/portal_gamification/__openerp__.py42
-rw-r--r--addons/portal_gamification/security/ir.model.access.csv7
-rw-r--r--addons/portal_gamification/security/portal_security.xml9
53 files changed, 5757 insertions, 2 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..dc9f1212ae1
--- /dev/null
+++ b/addons/gamification/data/badge.xml
@@ -0,0 +1,1142 @@
+<?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">iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABmJLR0QAxACcAA+BYKlhAAAACXBI
+ WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QMdCSITq3pssgAAIABJREFUeNrtvXmcXVWZ7/191h7O
+ UPOUVOZKQmaSMAYIiDIIiIKCXqFRabyK2tfb9qA92Gq3b3uV621xbBC1VbRFEEUmgaDM8xCmEMhI
+ 5qkypypVZ9p7Pe8fa5+qSshQVakkBdb6fA4pqs7ZZ+/1/NYzDzC0htbQGlpDa2gNraE1tIbW0Bpa
+ Q+vPaMnb6DkEaAAmApUDeG0L7AbWAZsATV5DABgE9+4lBL+8tpL3njYzGDlrklfRUGv88psUQPtG
+ LxFJPqtEMbpzl8bL19vc/NdL29a26mPAb4EnEnC8pQEhb8H7zaZTjAwDrqipMFefMdsfdf5pIVPG
+ eXgGjS2iA0wOETCCiiAbtlgenF/i4fnF/PrN9jar3LBrNwsTLhENAeDwLANMS4VceOqx/vsmjfHO
+ PHm6z+xJPoGPFiPEWnfQ5TA9kbu2IiIEvgPDyg2WZxdGLF0drVm4Iv7DivX2HuBPCRB0CACHfm8G
+ mO17fPXy88PTzj05bGyuN1RXiqOJIqraxbKP1Cp/pwBioFBUtu5SXlsRddx0X2HN0jX2euBHQJyI
+ iSEA9PGegsBnehjwzfednjrvkx9IUV0hGsVwFOjdK+4AijGiIsjD8yP+685ca+s2+/mOPHcAnYOV
+ Iww2AHjA6ZPHmU+dPiu44uJ3hDK22WihiFhVQJBBzLPKIijw0WIJuffJIk+8XFz42Mvx94BfA7nB
+ BoTBsp0GqAa+/1cfTF307lPC2uH1BgG1qKCDm/D74gYigjFoe6fKK0tL8Q9+k1+8plX/BngkEQ1D
+ ACize9/jjKkt5qYvXpVtnjDSI7aD/7T3RV8wRhSQ/7w1z60PFH4RxXwB2D4Y9APvKJ/6caOb5JpL
+ z05975+vylYOqzNqFRF5exC/7FNQVQHhHcf7zJ7sH7ezzX5gbatuBJYebRDIUQTeWXNn+T/69KXp
+ CZPHeFhFVR2vf7u4J/fgBG6z1Qi0d6rMe6ZY+PZN+d8CnwSKR0s3kKPwfUEq5OPnzglv+IePpgn8
+ o2LJHXUdwfdEF6+O5Z9/sHv+pu1cBqw6GtzAO8LEb2pukB9c9b7Ml/7msjQgqqryZ0N9nJUgIlir
+ 0lRnOG1mMLJ1u714zSb7egICfTsCwAAjJo+V2778iYr3nXNyoKUY/tyI/2bdAK2rMpwyM6grxXrJ
+ a2/EOeD5I8kJvCP0HcMmjDJP/vtnKmdNHutpbJ0j7c+V+D1xYBUJfTh9VhB4np79wqK480iCwDsC
+ 1589e4r3yP/5TMXY8SONRrEOEX4vkQCCVZXjJgdeJiXnLFgWtcf2yIDgcALAACe+6wT/d1/5RHbM
+ 8HqjUTRE/APpBYDOmOiZbFrOfXZh5AOPH24QeIeR+KNPnGpu/6crs8c01JhE0x8i/kFMRRHghCk+
+ qUDOfO71KAc8ezhB4B0m4jdNHG3u+Mcrs7NaRnpq7dDJ771NLsRW5fgpPrHVuS8vjVcCrx8uEHiH
+ 4RnSFWnu+n+fqzht6jhPo8h5wYbo3zedQBWdPsEPtuyIz1m+1v4RaD0cJqIZ4OuFwPe+8/cVZ0wa
+ 42kpkfm9J74OISABgapKKkA/e1mmpmWk3AdUHQZ6DegFA2O4/G8uT/3l9PGe9I/tD7GJPf0EKrUV
+ 6DWfrWisr+ZuID3Qm+QN4HVa3nG8f9unP5CpDHwZEvr9WHavZEYRpw801Xo6aYw/9r6nSxngYQYw
+ nDwQABAgU13BA9/6XMW4mirzJt++anfi7BAu3ryi2GJVsbYMBLAWIpvslyLD6414PjNfXBw/zwC6
+ jP0Bkvv//s9XZY8d2WQ0X1QBiC20F5T22BJZUAGj4CPUZYSUCGKShH758wOGtYpVaCtatueUjljp
+ KEFH0dJRgKwvHFPrMbJa8A14nuiFc8PK+a9HX395aTwf2DEQloE/ABzknR97T/DJs08MNF9UUYUX
+ t5a4e3WRbXml0yqlGGJ1t+sJ1KYcAFpqDKOyhklVPpMbDKKCMWDepoBQVWILnZHy9KaIV7dHvLEr
+ ZmekFGKlGENHHopFx1cnVhn+/vgMxw7z8IzKqCajf3Fe6sSXl3Z+CfgiUBgY07P/n22cMdH86muf
+ rjhveL3ROFaZv63Ef7ySo6juDaUSDK+EaaOhrsKFQ9tysGEbrNgGRQuBDxrDuCrD6SN9zhkeUh0a
+ PAOeeWuDQdWd9CiG5e0R96ws8fSWEkVNcghx7N4mXLJjdzdVPIFTh/n888kZarOCMaLpEPnyDTnu
+ e6p4KjD/UPWBQ+EAfjrFuy84NTyvucEQxyptJeXmpQWK1p3iK+bA350D40YCJbqz5U239rFpK7y0
+ Dl5aA/PXWJ7dXuS3y4s0pw3njwo5rsFnRKXB995aQCif9vW7Y17ZEnPvhiKr2i2xQlUI02pgWj2c
+ 1SKc1AyT6wUTgC3BE6uVrz+mPLkSXtgasWpHzKy0jwiSL8IXPpLmuYXF67a1cSaHmGja3x01QNX0
+ 8Wbh9z5fOTqbFrVW5aXNJb7xSo6ihUtPhJ98FKLI6QMAkuiCZWW3XNDnJWzfKqzdDqu2w5Mr4b+f
+ g7Z24ZxRAR8+JkV1KHh7AUFVBxHRu5W49qLy08U5Xtkes62g5CKo8OETx8GFE4Wp9cKICogT8WhV
+ URUExTdCZwk+fKvl0dVw7uiAvz05QzpMOIMR/dPzxehfb8j9NfCz5Hj1nwO0/C88QFdd32ulIgA+
+ f8UFmdHVFUIUI1GsLN5hKSp0FuGaS6EUOdYmsidGu+iXgCEqV/UojKiBEVXCKWPgH89W7npN+Ze7
+ ijzxRImvnJhhRIVHZKwryFO3ebF1m142NqJYKSbXVKXrveWffYFIoT1SFO2q7nPJOoJ1v+265/Ln
+ JNG6rHY/TjkVPLLK9pIlskJHUXm8tcSuWLvUtM+dCP/6DsGnS7Mnl8j6sgtYku8vRhAY+Pu5whOr
+ lWc2RxRLShgInhGsIidO8YO5s72rnnolvgdY318u4DuNtE8FjgZomHWM+cKFcwNyBQsIpRgW7Iwo
+ xXDeDGisgkK+9yxm7/cZgTgWLpkOF0yBr85TvvhMJ8XoIPxL9vxZ9vWz7s8Fe2DfZF8cmgp4CqeO
+ hmvOFmY1Cfmioj2+aH/XM4l7bmItzBgOL29RVm63HJv2ulx39dWGM2aFp768JPeOzjy34fIK+weA
+ NTf0yZwIgC994aPZdKHoMrrAabDL2mIKMVxyvJNlhyqxRaAYQyDKN94rXDgV8nEiSvZiveXflU/q
+ HpTTfVBT90NtPbCXem8Q6f6uqY5TnDYSKkMhFylCXxJelZHVwtQGZeFmeGFLxLQRXpfSZhU966RA
+ bn8k/5Vla/WeRAxovwDQR9k//rxTgvNbRhqxCfuLLeyMHPFLJXjnFMfWD3SStJcnSgCrglo4rWUf
+ lq8ehHh6EAD0/J3d+4PiVPN9Xb8LALb7Sd70dwVrKMVvznotP39ZhJT/tYkCrSqkfJhSLxijLN0V
+ E8UQ+l3XksZa4dxTUtOWrc1fCNzWH13A78f7z7/gVH+86enps/DKthgxMLoGqtLdJk5Z6V+/C7bu
+ dvI35bn31KShOt1tBtn+qKo9kbQ3qvZFNNnPz+znGvsCkezFfeRgIJbkY4oR59hpK0Bru7ItB2va
+ YEM7DM/Ce4+BtJGug3VMA6R9WN1hiSNFw+7gWrEEl58bctO9+X9t6+QP9KMquS8AEKB2zgzv/RNG
+ +0ZEurbCKqxtt1iFMU0gPRQ/I9Beggt/4ACQChzx67PQUAEja+G4UTBnLJwwLjk0JSjF2q0YvZWd
+ P7iy8pQRTCg8vVa59w3l5U3w2gZYvQtswVUZpz344UXCR2ZBybrPjq2B0INcrOSKSkV2TwuoIiP6
+ F+enpv7o9sK7gHn0sUdBXwDg+x5jJ43xzmpuMMS2mzbWwvpijFoYVgW+6X740Iebn4TVOxySO4vu
+ 1drWDavbXnKASRt470z42BzltHFCFCn2LQwBTZRZi/CLhcp/PKlsyrvf7+6EqMywU5DCHZxn1ikf
+ nO64hKrSXCl44rTK1pzSUE2PGK5QKMGlZ6f40e2FTwEPJY4hHWgACBBUZblq7qzAWW+JKCp7unZ3
+ OHFZme5miZI4ix9b5lh/WdZVG/AQsC4+0CVPY/j9fOVnT8G4BuXq0+HCqUpLvRB65WjZ4AaEKhhR
+ jBFW7RTuW2H51lOwoQMyAVQYqFHDxFAYVWUYlzFUeMJ920os7Yhp3Q2dJahOuR2szoKfmLDbc7qn
+ LuX2U7IpeP87g9l3PlqaCLx2uACQaaozHzlxqk+S1r0H0nPW2bzVoUN9l2j1YflGZ9qowgzf4+Ss
+ T13GkA3AmG42by1sKVrW5yzP5Et85R7l+w/DOVOUL5wNUxuFaBBDoHziC7HwnWeVWxfBkm2O81Wl
+ 4OS0x/QKn5YKj2xaSIdC4Dt/iRfAN1fEtJeglBTHqkJdKlEOFXYV7JvaHZXpMHeWP+rOR0tzgSV9
+ MQn9PrzvnWefFNb6Hl3Vuz2VwM7YbUDg70UhA1s63MYEwJwqn+nDfWqrDGGQgEW6r9MSeUzPKSfs
+ 8rlxfZ7VRcudr8IfFsJvPq6c2SJdYdPBtgTYWVQ+8BtYuCVpaBRAU0q4pCZkfI1HfbWhKiOEgTj3
+ toFCQSlGHnZFEhPo6TTr4SXtLOk++12JCKOHed6MCebM11bY3/TFJDS9Zf/Ax94zN6AUvVktV6Aj
+ 2v/3lRKCVYvQWOHRVGdoqBFqq4SaSqGmInlVCY01wsgmQ8sIj4tGhEhyh4UYvngXBMHgTRzzDdyz
+ BJ7Z4DieMXBMaPj0iDSzmgNamj2a6w01VUJFBtIpIfQhCKSLe/hmLw4q7LEHqvt2f49sNIxt9s4A
+ 6vvCJP1eAiA9tcU7fVSToVB6sxzWxOMpAqV9qCChB/nIoS2dglTo0G+M7BOSnufMpaqMdPsMBEpR
+ klQwiPWAOIltAIwzhqtGZxjRaKitFFIh+N6+731jzmLUOY2CZF+0R+xEcW5v3Y/wyaaFsc3e2IpM
+ aUJHjnW9FQOml+859ZyTg8rYJqXu+1iZJLq3u+jYVU/20FiR+M9du7Uk3n8gD6DgGQg86TLBRWDr
+ bu0+HoNUBOSjbngOSxmaG4SGaiGT2j/xrYU3dls8heq0Evrd7nDUiUYO5KRUwSo66xiPVMj7+mLd
+ mV5yibmnzPDCON53RFYEMgmx2vN7uUcjGFHrfle0LmjS25UNpMvRIgKbB12HnTevjh4AyIZCRVoI
+ gv1wu+SUR7GyaGeEAMOykPG7EdVWSJJpeLMLvOf+W4vMmOAR+nIuLktLBgIAAgQjGmRWY63n6X52
+ X4AwSe/aurvbDSwJT5zQ5LxaHaqUrPv5oFFcAd/bM3Qcx7CrY/AyARHoKHVvfV3gMpwOdLtWoRjB
+ ig6L58HIKul6PgF2dTqxIuoihAeyQaorhImjvRlAppeHu1cAGDZ9ojcmDByr2d+DZ3znoty43bF6
+ 7eE6ndycIF1he0GJrfbqINen99T4fQ/WbBucAChrJh09krRCIwdNYoktzN9YIrbOYphQ2y1CRZTW
+ MgAE0v7+PaOqgrXo8VN8gDPoZcKv6cXfR7SM8JoCf99yu1z4MarCYIA12x2rL781juGU8VAouast
+ 2R1TLHXLtQOdJmNcEKgsB30Db2x3qVKDTRKUn3d7vntT06ZbfO2baEoUwXObYyJVKkKY1NCT6wmt
+ 7S6nUgSqwv0X2YhAFCPHTe4CgN8bMdAbAAwbVif1+1NgSLTe8WmvywpYsrE7ph1bmDUGUr57sKW5
+ iHxBnUZ7EDlgcGyvZxxm5bZkUwcjBzCwMRFRpQiqgwPHMmILG9pilrc7N3pDCqYME5I0FYzAyp0u
+ JG6AurQcMIM6tjB5jAE4vreK4MEA4FVVyLDKrEkf7LROq/WIYwhD5/r1elzZWJg7yekGG0vKmvaY
+ UunAeoDgQNSUcopg2YJYshmXaTEYZYCnXTGOQgFqQ+my4fdJsBhe2RzTmneRn3MnCqJgrXOTi8Dy
+ 7UoUO0o1p81BIo9KJi2MGWYmJ76bQ+IAAgQN1TK+tlIOZIVgDDSkDWnfsekHFoF4PaKoChdMJ3Ei
+ wT0bi+SKyoEMAhHH6htSphsQAos3KvgySPuuCqt3uiCPjSHlmf1SILbKrpzlwfUlJ+YsXDbTcVBj
+ HDfZnoOVu7rFSUX6wHWWiY6mE0ebDK7x5iEDwM+kZEx1VjjQxYw4Z8+IrME38PwK2NGZ2PsJCM6a
+ CpkkqXHh7phF2yKnCxyADRgjNKRkD1Csb4difvCJACPQmYednVBKXDCp/ehNZdn/6JqIJe0xYuG4
+ ETB7lOxhJm/pUFbscNyvJeslmdEHPjTWImNGGB8Y1xtL4GBvCFIhI6sq5KAKW9oXplV5Xdr6rfOT
+ uEBi646ugbkTuhM/7lxbZHfeEsf7dwYZAy0ZryuKWE4CfWPr4LMEjIGlm+nKyfHoznbeW2bHMWza
+ Zfn5soIT1Hm45nyhWNjT0trQASu3ugM0tdYj8A+eH6EKoxs9A4wYEBEQ+NRnMwcDgAtuzKj2CYwD
+ wC+eTHYh0YKbqlys3zMO0Us6Yx5cWyJXdGVS+1MumyuMyyxO7qgQwYqtzkQaVHEAgYWt3VtenQR7
+ 9iaBY/3KV+d3EscKEZw/E45vdjqSkXKGlDJviXvGWOCUJv+gHKAMgIZaDNB4qBxAAM/3pLrMug+o
+ LRoYV20YnngrVm+Dx5eQJDa4ypiPnAzTm518tAq/XVfgpc0l8vsBgQiMrTAuRy558FwJlm5VPBlc
+ eoAY5dXW7v+fmPFI+bIHp7JWacsp334xx4YOF9odWQtfPUvwTfd7jSgRwk2vgDXQkjbUV5o31UTs
+ DwBNNcbQHRSSQ9IBPE8rA196xQKbsh5TazwEaC/Azc8550+ZhVem4NaroSbjWHnOwncX5ViwxekD
+ e4PAJKJleChdAZFiDMu3JNcdLAZAUmzw7Bp1OyowNitkwu4Ta61SKMGPX8nz/JYIVec9/eUHDVMa
+ uwsnrELoCTe9orR1QsnA2cMD0qlu0/pgxkgmjeAGZx2yDmBCT8LeVGQZ46JdF40OqU6SPOYthAXr
+ 97RTh1XC81+E0YnHK6/wfxZ08uS6EoW9QGAMpEM4Juv0gLJptHqnUzIHkwu4mIdl2xOvKDCq0iMM
+ JGHpSrEENy4scN+6EhJD1oP7rzTMGZmYeQhWHSfNW/i3BxUNoDEQjhvmkwqkl3qPEvgiQMUh6wBG
+ 8PyAsLdHzfdgWKXhQ6PcR9bvhP83z/2+zFKsQmMG7vormDUqqeoRuG5Zjj+tLZAvlhNOEuUyMEyu
+ 9rp8rZI4g7Z1MmgiQ4KT/0V1LTyqAqEpK/iJuVqK4GcL89y2skBKYWQN3PVRwwkjlHxJuk62ESXl
+ w7/80dIROeqc2uDTVOUyh3pXG9nVbzscCA4gpg9eNyOQSQmnjww5tz4g5cPdr8LX7oFUtkcuPNBS
+ B7d/GqYMd7ZvzsL1Swr8flWeXL4bBL4PY6oMVT08kcu3wNbOwZMX4BllwSaIk2KYWl9orjJ44moj
+ b1lU4A+rS3jqMqF/+UHDcc1QiGQPtu4ZYd4y5ZbXHWUaAsNZo0IqM2YPx1pvOJJnCA7ZE5gkIdje
+ 1l8mE7WorhAuHhcyu8Yj5cE198F/PdLtHSwnkNSm4Pl/gjMmdBeQ/mp5kZ8vztGRc7/zBMbXeIwI
+ TbetCzy1qs/jAA+rDFi40cl4MTA8Y2jIGmKFP60ocdOyAnGk1KXgtr8wnDTKOcFMD/1AgFU7lC8/
+ pLTnwTfC+0YFjKs3pML9h5P3x5HKM60ODQAKccRB0hHeDIJUCMNqPK4al+a0ap+KAL7wO7jmfke8
+ rkQHXCXRHZ+GK+ckBDVw76YS31nYyc4OV2uQCQ2z6jwXBFIIPLh/sTMFBwMGtnbA65sTrRnh2DoP
+ z8D9K4r852s5pAhTG+GeKx3bL5ScPNeE+NlQWLJNueoO5fWN4HnCxc0B7xwbUJmVLhHaW5+0On1L
+ 94pT9UsE2GKJTlX6xG49I2TTMKrB44rxKT41Kk3ag289AOd9H3YWnOewDDJP4DsfhL8721W7ADy9
+ NeJLL3Syo9NiBN41InBZDkne3KPLYHfEUe8/KAKtu9X5AJKGFic1Bjy8rsiNSwpoBDNHwU0fNkxu
+ UHI9ZD7qiH/3YuWim5SXNkJFVvj42JD3TAiprzakgr72RRBKMapKO72YamoOJgFiS2epH/Mwfc8l
+ PjY3epw4KuBLLVmmhR4vrYfJ/wb3Lu7uD1SOF/z7xXDtpd1fviZn+dtnOnhjZ0TWN5zWGHSVnBkP
+ bn1RCL2jywWshdc2w/YdLm5/Uo3Pqt0RN79RpFBSpjbCrz9saKlz9f/GlOP9rujlVwuUK29XtnZC
+ fcpwdUuKU8cFNNaaxPSTPgOyrd1aYFtv2PZBOUAp0vZcoe9brKpOKQyhoVZoafb4+DEpTq/yyRXh
+ QzfAP/0eclF3JVEhB589E371MWhMQy4Pm/PKl+Z3cu+qIufUBQQxFIrOJLzhMUVSRy85wFoHxFte
+ Ag0gZYRJFYafvlGgo0N513i446PC6BqIYun6jJ8ktn7zCeXTdyidMUyq8PjrSWmOHxPQVGvIpB0n
+ 7Q9H2rRDLb0cSnUwDhCVSmzbnevfDsfqEBz6UFMhjGowfGBsiv9RHxIX4LsPwpyvuV5BqURnLRTh
+ gqnC7VdDYwryOWfz37i8wLWv5ynlXag1Kjofw8sr6KOMHEjt39U8PLQIIg+mZTxu3VRkV4fy4RPg
+ px+ExgqhGGnXyfeNkouED91iufYpxRo4s87nk5NTTB7p0VAtpMP+Eb8sqHfsUgtspXu4df8BkC+x
+ ZXen9gOJQtlyE3E2cXXW0NJkOGt8wF+1pGjwhaVbYfpX4HcvdGfPxBZmjoBF/58woyk5bcDqnKW9
+ p2oj8F9PKb5/5C0Cqy5799pHQD2oFOH1XExnSfnATPj2hUJ1KikVM4JVB4L17XDpry0Pr3Ca/jsb
+ Ai6flGLsMI/aKumzxr8vj+zazdbSPer+kDhAsSOna3ft1jeZAaq6x8ta9+oZ3u2pvMRW8Qxk08KY
+ Jo93TAj5mylpTq32CYDLfgxfud0FewIPSrFQESjPfUn4+BxXH+eT/CvJzwIPLIENO45scEgTRbe1
+ Q7nxRRf0ylslyiufPAl+dKmQDbrzHcqa/ssblct+ozy1AkJfeO/wgMsnp2huNNRUOk55KI2wkgxq
+ XbvJWmBtb0TAwZhnGMc6Yc6M4PyWkXvmhCUzX/b52reTSJIQr6t8TYVCfYVhRq1PS2BY1Wl5ZJky
+ bwFcMFNorFCKkeMi7znW9RF4fBF4nrMaPHE331mAqcNh9ugkf7C/vQT25qM92cw+9Jt0SvjKfcrz
+ 65J3luA7H4C/PV3wjRLbshtYyKaF3y1QPn67smYnVKeFq8akeMe4gGH1hsq0uFDvIZo0Iornifzw
+ 9/nt7Z38EDfS3h6KEhh35NnanrP5gdS1jRHCAGoqhZGNhlPHB3x+aprxFYZFm+C0rysPLwWTlA4L
+ 8IX3CDd/RghND+Mm8bT9/mXYmQN7hJiAZ4RFG5V7Frv/T/lw7QfgyhPKlUw9klgM/OIF5VN3KjsL
+ LsPp6pYUJ471GV5vqMiA7w+ULSt05pQNW3Q5LjPh0JVAYPOGLXZHFA3sJooIvucqZuprhEkjPf5x
+ dpYzGgJ254VLr4frHgbjuSqhXA4umqU88nnhhNFJYEjdBv9pIby6XvHM4TcJrbrill+8pKzfAVUB
+ fO8iuPoUR/qysmdECTzh+mcs//tupSOGsRnDpyemmDXapykx87wBzGzxDCxdG4MrEe/VMEqvFwBJ
+ 11bKBWccFwz3vYEf/FAuAwt9IRUKU6o9GkR4vc0ybxHMXwHvP8HZ+8VIGFYJ7z/O9RNctKH7Ceav
+ hs+eJZRKB3AODYAIEJQ3dsDVv3VdTu75BJw9waVyWZVE6VNSgfA/f6tc96w7iidW+Vw5McWkEb5z
+ 8IQDS/ykGYfe91RJnn89uj4BQeFQASCAtOf0nPefGU7MpA6P261LN/CdbtBc4TEhZVjUZnmtVbn/
+ VTh7ulCbcQpVyocPnyqEIjyyzN3ltjZXh3/mJHlT+fpAAKDcEEs8uPhnSlUK5l0tTKiD2Ep5DiDG
+ QC4SPn6rcvsix6FOqPL56KSQsU0Do+nv75bDEPn53QXWttovJY6g0qECACDIFZjy7jnByY21xhxO
+ 9moSbpAKhbqsYWaFx8YO5dVWy7yFTtmbOkqIIohKwjunKqe1wEtrXUnak6vg3VNhVK0QxfvgBP0E
+ gE1OdegLV9+qFGK45QphXK0DmzHS5eDZWRA+e4fyh9fBejC3zudjk1KMaPScph8cvpa3ubzykzvy
+ S3fn+Dmwi170Ee4NADygcli9ueikab63d3eQw8ENPAOB7worp1Z62Bhe2mq5+1XXRuZd0wUbK6VI
+ aGkQLjne9Rx6dQUs2gYXTBOy4T4o3E8ACI74X77XUlshXHuR0FghSZq7c/EGntJWFN77U+XJ1RB7
+ cGlzyPsnpGhuMFRX9CWm33fzz/PQV5bG8sdninfmi9yfWAA6EAAwQOfO3fqXHz4nlT0SmraI4Hlu
+ wzKhYUKlh4lhSbvloaXQ3glnHOOiZDZ2rP9DJwgThsMPH3GNFN4xQfCMk8si/QOAVdfa0SLMW+Tk
+ +mfnCn5ywXKPJM8I69uFs25QVux0puolzSHvHh8yvF6oyg6MmXcg969nkHlPF3lxSfzNKGYpkB8o
+ AACkdrTp8e+ZG06qzh65ZMyeesG4SkM9hhUdMQ8vgdZ2OGOycxZZdWJh9ijhQyfCb+bDss0wd6Jr
+ LhX37FfcSwBYW5bTwgvrlTiCS2YKPcehWOuSQZZsgytuhpU7XE+D9w0POX98yLB6oTIjA2jm7X/l
+ i8o9TxQ3LF5tfwxsoJcNInoLAA+gMiMXnzrT11J05Dq3lxsrhoHQXGEYFxhWd1oeX6E8sgTeN1uo
+ TiuxCmqV+grhoplCKoBb5wvjGqAuSULdo7ypT8hyAAAOSklEQVTlIAAQcRk7r7QKLTXCsSMk8XZK
+ V9Om0IcVO+Cin8Pq7YAPHxuV4uzxIU21QsURIj6gazdbufOx0mPbdult9GGaSG8BYIDOXe16xaVn
+ pdJH2u/ufAYOBLVpw+SMx5L2mOU7lLtegvNmCHWZpKYu8YePqxdOmwib2117ttDruwiIrTCmWkj7
+ dDXscSByaenLtsNp/6m0FZwz5+qxaeaMDWiqM4743pE5Jaoqi1ZF9ub7izcBT9CHGQK9BYAAYWx1
+ 5NQWf/boYeaITwPtqRxWpoQZVR6r2yxLtjuv4anjhdG1SmS7Ey5EoKnChZttHzmAtY6d792t1Dl5
+ hJc3KJf8QmkrQtoTLhsZcuo4R/xsemBt/IMQH4Bb7i+2LlltrwdW0Ic2cb0FgAJeMaJQUykfOuXY
+ wByNhMw9lUNnIbQX4MXNzkI4bYIwoclZB2WZ31Vr30cdQOTNjiCXtu3q9T74S6V1t9vBK8ekOD1J
+ 4jiSxC/vSWce/bcf515U5b+AnfRhjIzpAwByqqxcujp+YdPWo9uoz/edcjWq0XDF5JBTG3125OHC
+ 65Qn3yjn0A2cnNLkv56BrXnhXdcrG9tBDXxidJpTxhwd4rsaALjjsYLGltuAdvrYK7gvACgCm15Y
+ HD+0fF2sR3tUi+87JWtEveET09IcV+1TsPDRG5Xn1rje/ANlsqoqvies2amcc72lvehS3i4fmWLO
+ WP+Is/1uUAq5Avx6XmE98CD9mB/UF89enDgXHrzvqdKmnvLnqIHAE7IZoanOcPW0FKfU+axrh4/d
+ CBvaSPwAA2OObmxTPvU7ZU2bSwM7pzHgXQnbz6SO9Ml33p9UiN76QIGd7VyXaP7FwwkATZwLyx6a
+ X3py1cZYjREdFCBIw/B6w8empJhW6bGpDc7+rmKlZxJmf0+Z6+L1tYfg2ZVOJZhZ5XPRhJCmOvfd
+ R0rb30v26452lfufKa6FLs9fn0fI9dW3HyU+5h995+Z8PvAYFCOCHQiE5nrDRyaG1KWE1na4/Cfq
+ mk31s35ArYv1//JF5aYnlTiAcRnDVZNSzs5Py1EhfqLUysPzS2zcan+OC/zk6Yfi4/XjUChgN27V
+ +nEjvOOnjPU0io/+rGgRB4TK0NDeqSzvsKzaDiOq4cQx4ubw9tEMDH3h2TVw5c2K9aEmED51TIaW
+ YR7Vlc5LeTSeW1Vp61B+eU9+8Yr1+n1gNS70q4ebA4ALMe4AfnLTvPz23XkVGQTdGsrOosqscPHE
+ kKzvXMC/edG5jfsqoo0Bz4e/u8NxDxUn98c3GqoOY2CnN2dQRHTB8jh6akF8L7AyOf39Ms1Mv+4A
+ OoBNq9bb6+97suR8Y4OgRst1KnG1iecOD4gFnnoDlmzumzLoQrvC3QuczY/A8NBwwnD/sId0D6L3
+ IYjmCyrfvTm3Cvh1IpL7na/VXwCUgB25IvPueqzw0vZdlsFSqu0ZIRMK540NHIuP4L7X+nZ7xrhG
+ FPcudp0/jcCUKo+x9R7plBx5jb/H1mfSwnduzrFxq34L2JiYfvZIAoDkCzuB1YtX25vueLTYaXq2
+ OD/qSiFUZQ1jMgbrw4PL3Ob15dY2t8OCDYluIcLJTR6VGZeyfrSo7/vCQ/OLMu/p0u3A44d6+g8F
+ AGVdYCcw76d3FR55aUlE4KODgROIgTCAusCAgdWbQfo4IG9nHtbscuzfMzC1wScM5KgWo27dYfnF
+ H/Jro5hrgc391fwHCgBlv8Bm4Btf/1nH6o1brXje0fcNlNemokUUsqm+M8lYSfoYQo0nXUMujpbW
+ b1XlzseKuQXL7XW4WcHtHOLo+EMFQNkv0Aas27iVL3735lzkcgWOnlWg6nruPL6uxOa8RWI4bzpo
+ 3IehrQqVAQyvSaptY+2a8nk0FD9jRBevivUndxQeAO6llwmfh8MPsD9OEAH5dZttXF8jc6a0+AbV
+ w3paylxGkxm91kJHUVm9y3LvihI3LingRTCyzvUeqK90fXh74wcQcZ3MdnbCkyvcd+wsKmMqDWlv
+ z5L27mijHJZn9D3RDVut/PW1u9/IF/iHxOYfkNMPAxfT9YAaYGJFmm985+8rz501ydMoGlgHkaoS
+ W9hdUFbusmzJWbZ3Wrbmlda8ZUde2dKu7MhZxMKH5sDnz4HJja4DN7ZHw6NyGdneY2ETQBmjFCLh
+ 1lfgWw8rG1qhsdrQmBUq00I2ECoDl5vQmDbMaPAYW+N6+QxEyreqYoxo226Vf/jB7h2vLrefBJ4D
+ ttCLfP8jDQBw3akbgEmZFP99yzeqxg2rNRoPUOJIbJUFrTHXLcizrjPG1+7egZoYpqlQmTsBzp8h
+ /I8TlYpyo0Z1eYN7zJs6AABIWrsZI9hYsCiLtwh/fF15cZOyZqsb+ZovCGrA+BCrMr3e56unZKhI
+ D4ipqKVI5T9+lSvd80Tpf+Omgm6gHxG/IwUAcBNQhwMzxzXLT67928oRo5qMJhM/5VBOw4rtls89
+ 1uHaq1ph0jDl7CnCCWNgYiNMbMaNVY1AI6UYyx5j5fcgei8AsMf08HKpV7llWnkUQwlWb1ceWqp8
+ 7WFo7YQZ9R7XnFFBOuyfWHAVvm7Dfj2vEF33u8L3gJ/jqn0PWux5NHSAvf0DJaC4azfLVm6ITz5p
+ ul+dTR+aSmAVFm6OeGJDhLVu+vjvPgNXnipMb4YRNRAmzRrjOBk3vy90H1JlkBBbd/2o5DqC2xjq
+ MsrIGvjtq7C9E2KE94wJkiaR0mfig1KZNfz0rrz88LbCjcBPDxfxDwcASJSTArBrwxbdsGxdfNq7
+ 54QZz7gCiv6AQBVqfcOqHTFrOi226Pz05yQFInsPmN57osgehzqh6wHHvvcAQLnSt+v9yeAKT8AP
+ 4J7F8Je3wLJtLlj0d1PTDK8xpIK+1wGIKKlQ9IbbCvLj2ws3AdclSl/bQCl9h1sE9DQvK4FRwIVz
+ pvtf+vL/zNQNqzf9ihyqKvkibNtpeXZtxEOtJTa3W2wIFx6nnDwOJjZBZej69HjGJYKWp3AK3aKg
+ S3tnT9avPQc0xq4DSGxdVlHJusYVuwuwZies2AKvtirPrhTSJaiuFI6r8XnXKFfvX1fVt3hBwvax
+ Fm5/pGi//5v8b6OY7+ACPTsGyuQ7kgAoc5cyCM6cNcn7ly98JDNmcj/Dx3HSb3d3p7Jrt7KhzbJu
+ V8y2DmVzp2VbUYkDJZt2MfzQcwDwZM/UTt3r356+S1ueSYCLJBZj1+W8UEra10VuElh92mn+9Rlh
+ eKUwutqjKusqgLIZIfB6bwkkhSaaDuG63xXkv+/N/3cp4jpgFa7RU+kw0uiwp/Z6uKbFo4ATxg43
+ X/3m57LHjB3uqWr/OEEcu9ayhaLjCoWSki8ohZLuMY9wIJw2sg9R0bOUPRW6hk5h4Nq7+P6+B0Qc
+ jPi+B1/5Uaf88ZnSdcAvE7ZfTvHirQyAMgiyQDMwI5vi3772mexxpxwbYAS1/QSCJs6fWBOWbZPU
+ r8PprktG5JmyeCm/pG8af1nZM0Zo3W659le59ideia4F/gCswcVYIo5AYOVIObYNrov6MGC87/GJ
+ y94dXv7xi9Imm5ZDLjLp6RU87Bsmh+b5c/cqZNPoE69Ect1vc8uXrLbfxlX0lH38pSNElyNa3SG4
+ bur1wFjg3OOmeF/4+mey1XXVrtKo3OH47brKLD/w4Ybb8nLjPYVHreX/JsreJvqZ2DnYzMCDmYh5
+ XC7Bhk3bdP7vHy5OGd1sGkY0eCYVOA/IYQ4jHFmi0/VA6hmRlRtjueYXubY7Hi39UJXrgDcS4ncc
+ Djt/sAGgDIJiAoIdUcyTD8+P8qs3xeOHNZiKMcOMOGXu7YECAcJAtDOv8vO7C9xwe/7RhcvtfwDz
+ Ek2/3xm9bzURsC+9IAXUAiOByZ7h05ecFb7zc5dlCHw0HgTZxv1n9862N0Z4+tWIb9+U27Jhq/2W
+ tTyXePa2Jqc+OtoAPdrf7ydWQlMChLm1VXzyc5dlxp40zQ+a6gxJM1IZ7FyhLOONQXJ5ZfUmy2/+
+ VNg+7+nSQ8DPEiWvFZfKVTwaLH+wAaAnNwiBqsRSGAG8e8YE790nT/ePf8/ckGNGG4olNLYqyVyc
+ QXPSk1RtwgAtllQeeK7E4y+Vtjz2cvRgscQfgYWJnN+Ji+bFDJIs2sF2nLzEUqhOOMIoY5hakeEj
+ Z8wOT/zL96aYOMpoKXZ61dHmCOXevJ6BYknl7sdL/Pr+fNvmHXpLKeLBxKZvTZw6nQm7t4Npwwcj
+ P5UeQKjC5Rg0AZOB9x8/xZtz8Zlh1fTxXlhfbah0U03Vjd3pf8CpN/K8zHlEoBQ5l/SWHaoPPF/s
+ uOvR4rrdOe4DHsAlbWxJWH0nvWzbOgSAfQMhxMUUahIwNAOnDKuTmdNavGkTRnvjjxntyaxJHiMa
+ DQIaxXRZEiAHjvwd0IBzgSXPQz2DdBZgyaqI11ZaVq6Pd72xLl66YHm8EHgZeDVR7HYkzpzcYDzx
+ byUA9LxHkyiLZa5QkziUmkQYlk4x2zdySnODTJs92U8fN9ln5kSPpnrjmkV0KZHdEb83bYJ0j7Ax
+ yWSyjpyyfG3MgmUxryyLWbIm2tCZ11eKJZ4ulngDF6zZksj2tkSrLw4mGf92AMD+uEIKF2iqTEBR
+ RXf08VjgGM8wZlyz1I8c5gVNteINq/e8hhrxaivFJKPWKZZUc0XVjhzxzna1rTtsvHGLjddssp07
+ 2nVD4qVbkihyHYm3ri055R3JSS8kRLdvFcK/VQGwt+VQ5gxh8krjYg7ZHj+neoiQ6uSV7nGdKDm1
+ uYSg7cnJLiYOmnzyt1wPYheSz5UDNvpW3cS3i+tdeoiKMijKr6DHz16Pf3sKe5uc4LgHYcuvUo+/
+ 2bfiKf9zAMD+nkv2AZC9/7Z3SujeL95OBB9aQ2toDa2hNbSG1tAaWkOL/x9rLN0eoKc8bQAAAABJ
+ RU5ErkJggg==</field>
+ </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">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</field>
+ </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">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
+ oZLwTwSrpfvBrHNcSUesOjyfJLWUiLcDPe1haF///Zh9t1HQUWnFS9BX3I8e/xzHFh2TVUtN+Lbz
+ vXeNyl1Q937yIlQiWMLFMViuueWucSU84WK7R7jWAjcAr06aDOj4AOv7+ugevpnD+mYMu9hyYaOF
+ iWGEGAnPEVn4LqcmxzAKk8wXqQAPA89QO5jsuqlinaPKOF+zHqGqeIQKESsRLOGFHZebfOq6rjZH
+ uNqd7xOOeN0NbAWG1rWjOgZuIJvcixUZohjqQ2sbrW2MJppvGgOtTNrVEpXiPIPlQ5w9f5r5hQmc
+ UO4c8BjwLMulgFyhynmEyhWpHLW1q7IIlQiWcHX9Y3iEy+u6XPFq8whX3PnzVzgCNggwkDToHryR
+ waGdHK7uaIqGSRglenKPUZl9hEPnl9zfrgCPAA+xXBzPrVdWdMTIFaqsR6TyLC+kuweUJfQTwRKu
+ oZ+8ritMLQnVdV5tjoC5n7jz6QTWABud8HFzbwzKoR6i8U529LcxG7uJaNsI58p9KKrYNg0/22hh
+ olTNPSllclPkAOfm57HSJ8hmZykUcxRLRco2aWr5UsdZruRpeZxUyRP25Txila9bm6rUrU+JUIlg
+ CddRuJTHdYXrnFf9x3VdEeeziVrJm80sp1BEN3VAJrqFdV0xThk30hk1wEximQnaI7Bg92FrN31V
+ o7DRujazjYsUzdROyHbBg6vadzbGc79uM/KE7DyZkqZLzZEqwTBjZLMLRAunmEnlmSsDy4vgM9TS
+ D445guO9zcgrUgWPULkCVagTKa+bkrBPBEtYBfEyLuK8vO6r/uP+ecT5++4a2Q5gOzBKrWLqBYRN
+ xZo2g5F2E6J9nGEn6zpM+tqTPFJ8yfMertPMcUv0ANmyJlvRpIo2qaJNoVTilsgTnM9ajGcsZvMX
+ TckoA2eAk44wTXuExVswseIJ5Yp1QlXwfLzhXn3IJyIlgiU0oA/rxStUJ2BRj2DFnE+9eLkCZnrW
+ yFyn5mbkez9uvtgLXchb9giKKyZZj2NKsZxW4Doib4hWL1DecK9UJ1Tur0uef7fq+X+JSIlgCT4W
+ L/cT8giYG0J6xapeuCKev296RMzwfFV1X72uj4uEWF6h8N7zqD3CVC9Q9SLlFar678uev29dZE1K
+ REoESwhI/xp1ImZ6RMwrZOG6UNHrukIe4fJ+6kXsYoKl60TKFZIXEimvWFU8604Vz59XX8BBiUiJ
+ YAlN4r4uJmKuEzPqhOlS319MtLybApdyU7pOYNzvq3XCVf/VrnNOlxInESkRLKEFRKw+nPSKWb2w
+ 1f/auIjDulg4eLFQ0Ou67Iv8nr7ER8RJBEsQnjcm1AsIm6pzbJdCv4CIvdD3iDgJIljC9R4vVzKO
+ 9FX+mSAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAI
+ giAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIghBg/j+I/UnoNLmiuQAAAABJRU5ErkJggg==</field>
+ <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">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
+ AABJRU5ErkJggg==</field>
+ </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/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..ae27aa96719
--- /dev/null
+++ b/addons/gamification/models/challenge.py
@@ -0,0 +1,821 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>
+#
+##############################################################################
+
+from openerp import SUPERUSER_ID
+from openerp.osv import fields, osv
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
+from openerp.tools.translate import _
+
+from datetime import date, datetime, timedelta
+import calendar
+import logging
+_logger = logging.getLogger(__name__)
+
+# display top 3 in ranking, could be db variable
+MAX_VISIBILITY_RANKING = 3
+
+def start_end_date_for_period(period, default_start_date=False, default_end_date=False):
+ """Return the start and end date for a goal period based on today
+
+ :return: (start_date, end_date), datetime.date objects, False if the period is
+ not defined or unknown"""
+ today = date.today()
+ if period == 'daily':
+ start_date = today
+ end_date = start_date
+ elif period == 'weekly':
+ delta = timedelta(days=today.weekday())
+ start_date = today - delta
+ end_date = start_date + timedelta(days=7)
+ elif period == 'monthly':
+ month_range = calendar.monthrange(today.year, today.month)
+ start_date = today.replace(day=1)
+ end_date = today.replace(day=month_range[1])
+ elif period == 'yearly':
+ start_date = today.replace(month=1, day=1)
+ end_date = today.replace(month=12, day=31)
+ else: # period == 'once':
+ start_date = default_start_date # for manual goal, start each time
+ end_date = default_end_date
+
+ if start_date and end_date:
+ return (start_date.strftime(DF), end_date.strftime(DF))
+ else:
+ return (start_date, end_date)
+
+
+class gamification_challenge(osv.Model):
+ """Gamification challenge
+
+ Set of predifined objectives assigned to people with rules for recurrence and
+ rewards
+
+ If 'user_ids' is defined and 'period' is different than 'one', the set will
+ be assigned to the users for each period (eg: every 1st of each month if
+ 'monthly' is selected)
+ """
+
+ _name = 'gamification.challenge'
+ _description = 'Gamification challenge'
+ _inherit = 'mail.thread'
+
+ def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the next report date based on the last report date and report
+ period.
+
+ :return: a string in DEFAULT_SERVER_DATE_FORMAT representing the date"""
+ res = {}
+ for challenge in self.browse(cr, uid, ids, context):
+ last = datetime.strptime(challenge.last_report_date, DF).date()
+ if challenge.report_message_frequency == 'daily':
+ next = last + timedelta(days=1)
+ res[challenge.id] = next.strftime(DF)
+ elif challenge.report_message_frequency == 'weekly':
+ next = last + timedelta(days=7)
+ res[challenge.id] = next.strftime(DF)
+ elif challenge.report_message_frequency == 'monthly':
+ month_range = calendar.monthrange(last.year, last.month)
+ next = last.replace(day=month_range[1]) + timedelta(days=1)
+ res[challenge.id] = next.strftime(DF)
+ elif challenge.report_message_frequency == 'yearly':
+ res[challenge.id] = last.replace(year=last.year + 1).strftime(DF)
+ # frequency == 'once', reported when closed only
+ else:
+ res[challenge.id] = False
+
+ return res
+
+ def _get_categories(self, cr, uid, context=None):
+ return [
+ ('hr', 'Human Ressources / Engagement'),
+ ('other', 'Settings / Gamification Tools'),
+ ]
+
+ def _get_report_template(self, cr, uid, context=None):
+ try:
+ return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'simple_report_template')[1]
+ except ValueError:
+ return False
+
+ _order = 'end_date, start_date, name, id'
+ _columns = {
+ 'name': fields.char('Challenge Name', required=True, translate=True),
+ 'description': fields.text('Description', translate=True),
+ 'state': fields.selection([
+ ('draft', 'Draft'),
+ ('inprogress', 'In Progress'),
+ ('done', 'Done'),
+ ],
+ string='State', required=True, track_visibility='onchange'),
+ 'manager_id': fields.many2one('res.users',
+ string='Responsible', help="The user responsible for the challenge."),
+
+ 'user_ids': fields.many2many('res.users', 'user_ids',
+ string='Users',
+ help="List of users participating to the challenge"),
+ 'autojoin_group_id': fields.many2one('res.groups',
+ string='Auto-subscription Group',
+ help='Group of users whose members will be automatically added to user_ids once the challenge is started'),
+
+ 'period': fields.selection([
+ ('once', 'Non recurring'),
+ ('daily', 'Daily'),
+ ('weekly', 'Weekly'),
+ ('monthly', 'Monthly'),
+ ('yearly', 'Yearly')
+ ],
+ string='Periodicity',
+ help='Period of automatic goal assigment. If none is selected, should be launched manually.',
+ required=True),
+ 'start_date': fields.date('Start Date',
+ help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
+ 'end_date': fields.date('End Date',
+ help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
+
+ 'invited_user_ids': fields.many2many('res.users', 'invited_user_ids',
+ string="Suggest to users"),
+
+ 'line_ids': fields.one2many('gamification.challenge.line', 'challenge_id',
+ string='Lines',
+ help="List of goals that will be set",
+ required=True),
+
+ 'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"),
+ 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"),
+ 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"),
+ 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"),
+ 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'),
+
+ 'visibility_mode': fields.selection([
+ ('personal', 'Individual Goals'),
+ ('ranking', 'Leader Board (Group Ranking)'),
+ ],
+ string="Display Mode", required=True),
+
+ 'report_message_frequency': fields.selection([
+ ('never', 'Never'),
+ ('onchange', 'On change'),
+ ('daily', 'Daily'),
+ ('weekly', 'Weekly'),
+ ('monthly', 'Monthly'),
+ ('yearly', 'Yearly')
+ ],
+ string="Report Frequency", required=True),
+ 'report_message_group_id': fields.many2one('mail.group',
+ string='Send a copy to',
+ help='Group that will receive a copy of the report in addition to the user'),
+ 'report_template_id': fields.many2one('email.template', string="Report Template", required=True),
+ 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after',
+ help="Never reminded if no value or zero is specified."),
+ 'last_report_date': fields.date('Last Report Date'),
+ 'next_report_date': fields.function(_get_next_report_date,
+ type='date', string='Next Report Date', store=True),
+
+ 'category': fields.selection(lambda s, *a, **k: s._get_categories(*a, **k),
+ string="Appears in", help="Define the visibility of the challenge through menus", required=True),
+ }
+
+ _defaults = {
+ 'period': 'once',
+ 'state': 'draft',
+ 'visibility_mode': 'personal',
+ 'report_message_frequency': 'never',
+ 'last_report_date': fields.date.today,
+ 'start_date': fields.date.today,
+ 'manager_id': lambda s, cr, uid, c: uid,
+ 'category': 'hr',
+ 'reward_failure': False,
+ 'report_template_id': lambda s, *a, **k: s._get_report_template(*a, **k),
+ }
+
+
+ def create(self, cr, uid, vals, context=None):
+ """Overwrite the create method to add the user of groups"""
+
+ # add users when change the group auto-subscription
+ if vals.get('autojoin_group_id'):
+ new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
+
+ if not vals.get('user_ids'):
+ vals['user_ids'] = []
+ vals['user_ids'] += [(4, user.id) for user in new_group.users]
+
+ create_res = super(gamification_challenge, self).create(cr, uid, vals, context=context)
+
+ # subscribe new users to the challenge
+ if vals.get('user_ids'):
+ # done with browse after super to be sure catch all after orm process
+ challenge = self.browse(cr, uid, create_res, context=context)
+ self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
+
+ return create_res
+
+ def write(self, cr, uid, ids, vals, context=None):
+ if isinstance(ids, (int,long)):
+ ids = [ids]
+
+ # add users when change the group auto-subscription
+ if vals.get('autojoin_group_id'):
+ new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
+
+ if not vals.get('user_ids'):
+ vals['user_ids'] = []
+ vals['user_ids'] += [(4, user.id) for user in new_group.users]
+
+ if vals.get('state') == 'inprogress':
+ # starting a challenge
+ if not vals.get('autojoin_group_id'):
+ # starting challenge, add users in autojoin group
+ if not vals.get('user_ids'):
+ vals['user_ids'] = []
+ for challenge in self.browse(cr, uid, ids, context=context):
+ if challenge.autojoin_group_id:
+ vals['user_ids'] += [(4, user.id) for user in challenge.autojoin_group_id.users]
+
+ self.generate_goals_from_challenge(cr, uid, ids, context=context)
+
+ elif vals.get('state') == 'done':
+ self.check_challenge_reward(cr, uid, ids, force=True, context=context)
+
+ elif vals.get('state') == 'draft':
+ # resetting progress
+ if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', 'in', ['inprogress', 'inprogress_update'])], context=context):
+ raise osv.except_osv("Error", "You can not reset a challenge with unfinished goals.")
+
+ write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
+
+ # subscribe new users to the challenge
+ if vals.get('user_ids'):
+ # done with browse after super if changes in groups
+ for challenge in self.browse(cr, uid, ids, context=context):
+ self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
+
+ return write_res
+
+
+ ##### Update #####
+
+ def _cron_update(self, cr, uid, context=None, ids=False):
+ """Daily cron check.
+
+ - Start planned challenges (in draft and with start_date = today)
+ - Create the missing goals (eg: modified the challenge to add lines)
+ - Update every running challenge
+ """
+ # start planned challenges
+ planned_challenge_ids = self.search(cr, uid, [
+ ('state', '=', 'draft'),
+ ('start_date', '<=', fields.date.today())])
+ self.write(cr, uid, planned_challenge_ids, {'state': 'inprogress'}, context=context)
+
+ # close planned challenges
+ planned_challenge_ids = self.search(cr, uid, [
+ ('state', '=', 'inprogress'),
+ ('end_date', '>=', fields.date.today())])
+ self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context)
+
+ if not ids:
+ ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
+
+ return self._update_all(cr, uid, ids, context=context)
+
+ def _update_all(self, cr, uid, ids, context=None):
+ """Update the challenges and related goals
+
+ :param list(int) ids: the ids of the challenges to update, if False will
+ update only challenges in progress."""
+ if isinstance(ids, (int,long)):
+ ids = [ids]
+
+ goal_obj = self.pool.get('gamification.goal')
+
+ # we use yesterday to update the goals that just ended
+ yesterday = date.today() - timedelta(days=1)
+ goal_ids = goal_obj.search(cr, uid, [
+ ('challenge_id', 'in', ids),
+ '|',
+ ('state', 'in', ('inprogress', 'inprogress_update')),
+ '&',
+ ('state', 'in', ('reached', 'failed')),
+ '|',
+ ('end_date', '>=', yesterday.strftime(DF)),
+ ('end_date', '=', False)
+ ], context=context)
+ # update every running goal already generated linked to selected challenges
+ goal_obj.update(cr, uid, goal_ids, context=context)
+
+ for challenge in self.browse(cr, uid, ids, context=context):
+ if challenge.autojoin_group_id:
+ # check in case of new users in challenge, this happens if manager removed users in challenge manually
+ self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context)
+ self.generate_goals_from_challenge(cr, uid, [challenge.id], context=context)
+
+ # goals closed but still opened at the last report date
+ closed_goals_to_report = goal_obj.search(cr, uid, [
+ ('challenge_id', '=', challenge.id),
+ ('start_date', '>=', challenge.last_report_date),
+ ('end_date', '<=', challenge.last_report_date)
+ ])
+
+ if len(closed_goals_to_report) > 0:
+ # some goals need a final report
+ self.report_progress(cr, uid, challenge, subset_goal_ids=closed_goals_to_report, context=context)
+
+ if fields.date.today() == challenge.next_report_date:
+ self.report_progress(cr, uid, challenge, context=context)
+
+ self.check_challenge_reward(cr, uid, ids, context=context)
+ return True
+
+ def quick_update(self, cr, uid, challenge_id, context=None):
+ """Update all the goals of a challenge, no generation of new goals"""
+ goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', '=', challenge_id)], context=context)
+ self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context)
+ return True
+
+
+ def action_check(self, cr, uid, ids, context=None):
+ """Check a challenge
+
+ Create goals that haven't been created yet (eg: if added users)
+ Recompute the current value for each goal related"""
+ return self._update_all(cr, uid, ids=ids, context=context)
+
+ def action_report_progress(self, cr, uid, ids, context=None):
+ """Manual report of a goal, does not influence automatic report frequency"""
+ if isinstance(ids, (int,long)):
+ ids = [ids]
+ for challenge in self.browse(cr, uid, ids, context):
+ self.report_progress(cr, uid, challenge, context=context)
+ return True
+
+
+ ##### Automatic actions #####
+
+ def generate_goals_from_challenge(self, cr, uid, ids, context=None):
+ """Generate the goals for each line and user.
+
+ If goals already exist for this line and user, the line is skipped. This
+ can be called after each change in the list of users or lines.
+ :param list(int) ids: the list of challenge concerned"""
+
+ for challenge in self.browse(cr, uid, ids, context):
+ (start_date, end_date) = start_end_date_for_period(challenge.period)
+
+ # if no periodicity, use challenge dates
+ if not start_date and challenge.start_date:
+ start_date = challenge.start_date
+ if not end_date and challenge.end_date:
+ end_date = challenge.end_date
+
+ for line in challenge.line_ids:
+ for user in challenge.user_ids:
+
+ goal_obj = self.pool.get('gamification.goal')
+ domain = [('line_id', '=', line.id), ('user_id', '=', user.id)]
+ if start_date:
+ domain.append(('start_date', '=', start_date))
+
+ # goal already existing for this line ?
+ if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
+
+ # resume canceled goals
+ domain.append(('state', '=', 'canceled'))
+ canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
+ goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
+ goal_obj.update(cr, uid, canceled_goal_ids, context=context)
+
+ # skip to next user
+ continue
+
+ values = {
+ 'definition_id': line.definition_id.id,
+ 'line_id': line.id,
+ 'user_id': user.id,
+ 'target_goal': line.target_goal,
+ 'state': 'inprogress',
+ }
+
+ if start_date:
+ values['start_date'] = start_date
+ if end_date:
+ values['end_date'] = end_date
+
+ if challenge.remind_update_delay:
+ values['remind_update_delay'] = challenge.remind_update_delay
+
+ new_goal_id = goal_obj.create(cr, uid, values, context)
+
+ goal_obj.update(cr, uid, [new_goal_id], context=context)
+
+ return True
+
+ ##### JS utilities #####
+
+ def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
+ """Return a serialised version of the goals information
+
+ :challenge: browse record of challenge to compute
+ :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
+ :restrict_goal_ids: <list(int)> compute only the results for this subset if gamification.goal ids, if False retrieve every goal of current running challenge
+ :restrict_top: <int> for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
+ restrict_goal_ids has priority over restrict_top
+
+ format list
+ # if visibility_mode == 'ranking'
+ {
+ 'name': <gamification.goal.description name>,
+ 'description': <gamification.goal.description description>,
+ 'condition': <reach condition {lower,higher}>,
+ 'computation_mode': <target computation {manually,count,sum,python}>,
+ 'monetary': <{True,False}>,
+ 'suffix': <value suffix>,
+ 'action': <{True,False}>,
+ 'display_mode': <{progress,boolean}>,
+ 'target': <challenge line target>,
+ 'own_goal_id': <gamification.goal id where user_id == uid>,
+ 'goals': [
+ {
+ 'id': <gamification.goal id>,
+ 'rank': <user ranking>,
+ 'user_id': <res.users id>,
+ 'name': <res.users name>,
+ 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
+ 'completeness': <percentage>,
+ 'current': <current value>,
+ }
+ ]
+ },
+ # if visibility_mode == 'personal'
+ {
+ 'id': <gamification.goal id>,
+ 'name': <gamification.goal.description name>,
+ 'description': <gamification.goal.description description>,
+ 'condition': <reach condition {lower,higher}>,
+ 'computation_mode': <target computation {manually,count,sum,python}>,
+ 'monetary': <{True,False}>,
+ 'suffix': <value suffix>,
+ 'action': <{True,False}>,
+ 'display_mode': <{progress,boolean}>,
+ 'target': <challenge line target>,
+ 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
+ 'completeness': <percentage>,
+ 'current': <current value>,
+ }
+ """
+ goal_obj = self.pool.get('gamification.goal')
+ (start_date, end_date) = start_end_date_for_period(challenge.period)
+
+ res_lines = []
+ for line in challenge.line_ids:
+ line_data = {
+ 'name': line.definition_id.name,
+ 'description': line.definition_id.description,
+ 'condition': line.definition_id.condition,
+ 'computation_mode': line.definition_id.computation_mode,
+ 'monetary': line.definition_id.monetary,
+ 'suffix': line.definition_id.suffix,
+ 'action': True if line.definition_id.action_id else False,
+ 'display_mode': line.definition_id.display_mode,
+ 'target': line.target_goal,
+ }
+ domain = [
+ ('line_id', '=', line.id),
+ ('state', '!=', 'draft'),
+ ]
+ if restrict_goal_ids:
+ domain.append(('ids', 'in', restrict_goal_ids))
+ else:
+ # if no subset goals, use the dates for restriction
+ if start_date:
+ domain.append(('start_date', '=', start_date))
+ if end_date:
+ domain.append(('end_date', '=', end_date))
+
+ if challenge.visibility_mode == 'personal':
+ if not user_id:
+ raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information"))
+ domain.append(('user_id', '=', user_id))
+ sorting = goal_obj._order
+ limit = 1
+ # initialise in case search returns no results
+ line_data.update({
+ 'id': 0,
+ 'current': 0,
+ 'completeness': 0,
+ 'state': 'draft',
+ })
+ else:
+ line_data.update({
+ 'own_goal_id': False,
+ 'goals': [],
+ })
+ sorting = "completeness desc, current desc"
+ limit = False
+
+ goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
+ ranking = 0
+ for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
+ if challenge.visibility_mode == 'personal':
+ # limit=1 so only one result
+ line_data.update({
+ 'id': goal.id,
+ 'current': goal.current,
+ 'completeness': goal.completeness,
+ 'state': goal.state,
+ })
+ else:
+ ranking += 1
+ if user_id and goal.user_id.id == user_id:
+ line_data['own_goal_id'] = goal.id
+ elif restrict_top and ranking > restrict_top:
+ # not own goal, over top, skipping
+ continue
+
+ line_data['goals'].append({
+ 'id': goal.id,
+ 'user_id': goal.user_id.id,
+ 'name': goal.user_id.name,
+ 'rank': ranking,
+ 'current': goal.current,
+ 'completeness': goal.completeness,
+ 'state': goal.state,
+ })
+ res_lines.append(line_data)
+ return res_lines
+
+ ##### Reporting #####
+
+ def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
+ """Post report about the progress of the goals
+
+ :param challenge: the challenge object that need to be reported
+ :param users: the list(res.users) of users that are concerned by
+ the report. If False, will send the report to every user concerned
+ (goal users and group that receive a copy). Only used for challenge with
+ a visibility mode set to 'personal'.
+ :param goal_ids: the list(int) of goal ids linked to the challenge for
+ the report. If not specified, use the goals for the current challenge
+ period. This parameter can be used to produce report for previous challenge
+ periods.
+ :param subset_goal_ids: a list(int) of goal ids to restrict the report
+ """
+ if context is None:
+ context = {}
+
+ temp_obj = self.pool.get('email.template')
+ ctx = context.copy()
+ if challenge.visibility_mode == 'ranking':
+ lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
+
+ ctx.update({'challenge_lines': lines_boards})
+ body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
+
+ # send to every follower of the challenge
+ self.message_post(cr, uid, challenge.id,
+ body=body_html,
+ context=context,
+ subtype='mail.mt_comment')
+ if challenge.report_message_group_id:
+ self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
+ body=body_html,
+ context=context,
+ subtype='mail.mt_comment')
+
+ else:
+ # generate individual reports
+ for user in users or challenge.user_ids:
+ goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
+ if not goals:
+ continue
+
+ ctx.update({'challenge_lines': goals})
+ body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
+
+ # send message only to users, not on the challenge
+ self.message_post(cr, uid, 0,
+ body=body_html,
+ partner_ids=[(4, user.partner_id.id)],
+ context=context,
+ subtype='mail.mt_comment')
+ if challenge.report_message_group_id:
+ self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
+ body=body_html,
+ context=context,
+ subtype='mail.mt_comment')
+ return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
+
+ ##### Challenges #####
+ def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
+ """The user accept the suggested challenge"""
+ user_id = user_id or uid
+ user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
+ message = "%s has joined the challenge" % user.name
+ self.message_post(cr, uid, challenge_ids, body=message, context=context)
+ self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
+ return self.generate_goals_from_challenge(cr, uid, challenge_ids, context=context)
+
+ def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
+ """The user discard the suggested challenge"""
+ user_id = user_id or uid
+ user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
+ message = "%s has refused the challenge" % user.name
+ self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
+ return self.write(cr, uid, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
+
+ def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
+ result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
+ id = result and result[1] or False
+ result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
+ result['res_id'] = challenge_id
+ return result
+
+ def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
+ """Actions for the end of a challenge
+
+ If a reward was selected, grant it to the correct users.
+ Rewards granted at:
+ - the end date for a challenge with no periodicity
+ - the end of a period for challenge with periodicity
+ - when a challenge is manually closed
+ (if no end date, a running challenge is never rewarded)
+ """
+ if isinstance(ids, (int,long)):
+ ids = [ids]
+ context = context or {}
+ for challenge in self.browse(cr, uid, ids, context=context):
+ (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
+ yesterday = date.today() - timedelta(days=1)
+ if end_date == yesterday.strftime(DF) or force:
+ # open chatter message
+ message_body = _("The challenge %s is finished." % challenge.name)
+
+ # reward for everybody succeeding
+ rewarded_users = []
+ if challenge.reward_id:
+ for user in challenge.user_ids:
+ reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [
+ ('challenge_id', '=', challenge.id),
+ ('user_id', '=', user.id),
+ ('start_date', '=', start_date),
+ ('end_date', '=', end_date),
+ ('state', '=', 'reached')
+ ], context=context)
+ if len(reached_goal_ids) == len(challenge.line_ids):
+ self.reward_user(cr, uid, user.id, challenge.reward_id.id, context)
+ rewarded_users.append(user)
+
+ if rewarded_users:
+ message_body += _("<br/>Reward (badge %s) for every succeeding user was sent to %s." % (challenge.reward_id.name, ", ".join([user.name for user in rewarded_users])))
+ else:
+ message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
+
+ # reward bests
+ if challenge.reward_first_id:
+ (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context)
+ if first_user:
+ self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, context)
+ message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
+ message_body += "<br/> 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
+ else:
+ message_body += _("Nobody reached the required conditions to receive special badges.")
+
+ if second_user and challenge.reward_second_id:
+ self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, context)
+ message_body += "<br/> 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
+ if third_user and challenge.reward_third_id:
+ self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, context)
+ message_body += "<br/> 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
+
+ self.message_post(cr, uid, challenge.id, body=message_body, context=context)
+ return True
+
+ def get_top3_users(self, cr, uid, challenge, context=None):
+ """Get the top 3 users for a defined challenge
+
+ Ranking criterias:
+ 1. succeed every goal of the challenge
+ 2. total completeness of each goal (can be over 100)
+ Top 3 is computed only for users succeeding every goal of the challenge,
+ except if reward_failure is True, in which case every user is
+ considered.
+ :return: ('first', 'second', 'third'), tuple containing the res.users
+ objects of the top 3 users. If no user meets the criterias for a rank,
+ it is set to False. Nobody can receive a rank is noone receives the
+ higher one (eg: if 'second' == False, 'third' will be False)
+ """
+ goal_obj = self.pool.get('gamification.goal')
+ (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
+ challengers = []
+ for user in challenge.user_ids:
+ all_reached = True
+ total_completness = 0
+ # every goal of the user for the running period
+ goal_ids = goal_obj.search(cr, uid, [
+ ('challenge_id', '=', challenge.id),
+ ('user_id', '=', user.id),
+ ('start_date', '=', start_date),
+ ('end_date', '=', end_date)
+ ], context=context)
+ for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
+ if goal.state != 'reached':
+ all_reached = False
+ if goal.definition_condition == 'higher':
+ # can be over 100
+ total_completness += 100.0 * goal.current / goal.target_goal
+ elif goal.state == 'reached':
+ # for lower goals, can not get percentage so 0 or 100
+ total_completness += 100
+
+ challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
+ sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
+
+ if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
+ # nobody succeeded
+ return (False, False, False)
+ if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
+ # only one user succeeded
+ return (sorted_challengers[0]['user'], False, False)
+ if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
+ # only one user succeeded
+ return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
+ return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
+
+ def reward_user(self, cr, uid, user_id, badge_id, context=None):
+ """Create a badge user and send the badge to him
+
+ :param user_id: the user to reward
+ :param badge_id: the concerned badge
+ """
+ badge_user_obj = self.pool.get('gamification.badge.user')
+ user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context)
+ return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
+
+
+class gamification_challenge_line(osv.Model):
+ """Gamification challenge line
+
+ Predifined goal for 'gamification_challenge'
+ These are generic list of goals with only the target goal defined
+ Should only be created for the gamification_challenge object
+ """
+
+ _name = 'gamification.challenge.line'
+ _description = 'Gamification generic goal for challenge'
+ _order = "sequence, id"
+
+ def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
+ goal_definition = self.pool.get('gamification.goal.definition')
+ if not definition_id:
+ return {'value': {'definition_id': False}}
+ goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
+ ret = {
+ 'value': {
+ 'condition': goal_definition.condition,
+ 'definition_full_suffix': goal_definition.full_suffix
+ }
+ }
+ return ret
+
+ _columns = {
+ 'name': fields.related('definition_id', 'name', string="Name"),
+ 'challenge_id': fields.many2one('gamification.challenge',
+ string='Challenge',
+ required=True,
+ ondelete="cascade"),
+ 'definition_id': fields.many2one('gamification.goal.definition',
+ string='Goal Definition',
+ required=True,
+ ondelete="cascade"),
+ 'target_goal': fields.float('Target Value to Reach',
+ required=True),
+ 'sequence': fields.integer('Sequence',
+ help='Sequence number for ordering'),
+ 'condition': fields.related('definition_id', 'condition', type="selection",
+ readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
+ 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
+ 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
+ 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),
+ }
+
+ _default = {
+ 'sequence': 1,
+ }
diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py
new file mode 100644
index 00000000000..8792e219d71
--- /dev/null
+++ b/addons/gamification/models/goal.py
@@ -0,0 +1,394 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>
+#
+##############################################################################
+
+from openerp import SUPERUSER_ID
+from openerp.osv import fields, osv
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
+from openerp.tools.safe_eval import safe_eval
+from openerp.tools.translate import _
+
+import logging
+import time
+from datetime import date, datetime, timedelta
+
+_logger = logging.getLogger(__name__)
+
+
+class gamification_goal_definition(osv.Model):
+ """Goal definition
+
+ A goal definition contains the way to evaluate an objective
+ Each module wanting to be able to set goals to the users needs to create
+ a new gamification_goal_definition
+ """
+ _name = 'gamification.goal.definition'
+ _description = 'Gamification goal definition'
+
+ def _get_suffix(self, cr, uid, ids, field_name, arg, context=None):
+ res = dict.fromkeys(ids, '')
+ for goal in self.browse(cr, uid, ids, context=context):
+ if goal.suffix and not goal.monetary:
+ res[goal.id] = goal.suffix
+ elif goal.monetary:
+ # use the current user's company currency
+ user = self.pool.get('res.users').browse(cr, uid, uid, context)
+ if goal.suffix:
+ res[goal.id] = "%s %s" % (user.company_id.currency_id.symbol, goal.suffix)
+ else:
+ res[goal.id] = user.company_id.currency_id.symbol
+ else:
+ res[goal.id] = ""
+ return res
+
+ _columns = {
+ 'name': fields.char('Goal Definition', required=True, translate=True),
+ 'description': fields.text('Goal Description'),
+ 'monetary': fields.boolean('Monetary Value', help="The target and current value are defined in the company currency."),
+ 'suffix': fields.char('Suffix', help="The unit of the target and current values", translate=True),
+ 'full_suffix': fields.function(_get_suffix, type="char", string="Full Suffix", help="The currency and suffix field"),
+ 'computation_mode': fields.selection([
+ ('manually', 'Recorded manually'),
+ ('count', 'Automatic: number of records'),
+ ('sum', 'Automatic: sum on a field'),
+ ('python', 'Automatic: execute a specific Python code'),
+ ],
+ string="Computation Mode",
+ help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.",
+ required=True),
+ 'display_mode': fields.selection([
+ ('progress', 'Progressive (using numerical values)'),
+ ('boolean', 'Exclusive (done or not-done)'),
+ ],
+ string="Displayed as", required=True),
+ 'model_id': fields.many2one('ir.model',
+ string='Model',
+ help='The model object for the field to evaluate'),
+ 'field_id': fields.many2one('ir.model.fields',
+ string='Field to Sum',
+ help='The field containing the value to evaluate'),
+ 'field_date_id': fields.many2one('ir.model.fields',
+ string='Date Field',
+ help='The date to use for the time period evaluated'),
+ 'domain': fields.char("Filter Domain",
+ help="Domain for filtering records. The rule can contain reference to 'user' that is a browse record of the current user, e.g. [('user_id', '=', user.id)].",
+ required=True),
+ 'compute_code': fields.text('Python Code',
+ help="Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id."),
+ 'condition': fields.selection([
+ ('higher', 'The higher the better'),
+ ('lower', 'The lower the better')
+ ],
+ string='Goal Performance',
+ help='A goal is considered as completed when the current value is compared to the value to reach',
+ required=True),
+ 'action_id': fields.many2one('ir.actions.act_window', string="Action",
+ help="The action that will be called to update the goal value."),
+ 'res_id_field': fields.char("ID Field of user",
+ help="The field name on the user profile (res.users) containing the value for res_id for action.")
+ }
+
+ _defaults = {
+ 'condition': 'higher',
+ 'computation_mode': 'manually',
+ 'domain': "[]",
+ 'monetary': False,
+ 'display_mode': 'progress',
+ }
+
+ def number_following(self, cr, uid, model_name="mail.thread", context=None):
+ """Return the number of 'model_name' objects the user is following
+
+ The model specified in 'model_name' must inherit from mail.thread
+ """
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ return self.pool.get('mail.followers').search(cr, uid, [('res_model', '=', model_name), ('partner_id', '=', user.partner_id.id)], count=True, context=context)
+
+
+
+class gamification_goal(osv.Model):
+ """Goal instance for a user
+
+ An individual goal for a user on a specified time period"""
+
+ _name = 'gamification.goal'
+ _description = 'Gamification goal instance'
+ _inherit = 'mail.thread'
+
+ def _get_completion(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the percentage of completeness of the goal, between 0 and 100"""
+ res = dict.fromkeys(ids, 0.0)
+ for goal in self.browse(cr, uid, ids, context=context):
+ if goal.definition_condition == 'higher':
+ if goal.current >= goal.target_goal:
+ res[goal.id] = 100.0
+ else:
+ res[goal.id] = round(100.0 * goal.current / goal.target_goal, 2)
+ elif goal.current < goal.target_goal:
+ # a goal 'lower than' has only two values possible: 0 or 100%
+ res[goal.id] = 100.0
+ else:
+ res[goal.id] = 0.0
+ return res
+
+ def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
+ goal_definition = self.pool.get('gamification.goal.definition')
+ if not definition_id:
+ return {'value': {'definition_id': False}}
+ goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
+ return {'value': {'computation_mode': goal_definition.computation_mode, 'definition_condition': goal_definition.condition}}
+
+ _columns = {
+ 'definition_id': fields.many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade"),
+ 'user_id': fields.many2one('res.users', string='User', required=True),
+ 'line_id': fields.many2one('gamification.challenge.line', string='Goal Line', ondelete="cascade"),
+ 'challenge_id': fields.related('line_id', 'challenge_id',
+ string="Challenge",
+ type='many2one',
+ relation='gamification.challenge',
+ store=True),
+ 'start_date': fields.date('Start Date'),
+ 'end_date': fields.date('End Date'), # no start and end = always active
+ 'target_goal': fields.float('To Reach',
+ required=True,
+ track_visibility='always'), # no goal = global index
+ 'current': fields.float('Current Value', required=True, track_visibility='always'),
+ 'completeness': fields.function(_get_completion, type='float', string='Completeness'),
+ 'state': fields.selection([
+ ('draft', 'Draft'),
+ ('inprogress', 'In progress'),
+ ('inprogress_update', 'In progress (to update)'),
+ ('reached', 'Reached'),
+ ('failed', 'Failed'),
+ ('canceled', 'Canceled'),
+ ],
+ string='State',
+ required=True,
+ track_visibility='always'),
+
+ 'computation_mode': fields.related('definition_id', 'computation_mode', type='char', string="Computation mode"),
+ 'remind_update_delay': fields.integer('Remind delay',
+ help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified."),
+ 'last_update': fields.date('Last Update',
+ help="In case of manual goal, reminders are sent if the goal as not been updated for a while (defined in challenge). Ignored in case of non-manual goal or goal not linked to a challenge."),
+
+ 'definition_description': fields.related('definition_id', 'description', type='char', string='Definition Description', readonly=True),
+ 'definition_condition': fields.related('definition_id', 'condition', type='char', string='Definition Condition', readonly=True),
+ 'definition_suffix': fields.related('definition_id', 'full_suffix', type="char", string="Suffix", readonly=True),
+ 'definition_display': fields.related('definition_id', 'display_mode', type="char", string="Display Mode", readonly=True),
+ }
+
+ _defaults = {
+ 'current': 0,
+ 'state': 'draft',
+ 'start_date': fields.date.today,
+ }
+ _order = 'create_date desc, end_date desc, definition_id, id'
+
+ def _check_remind_delay(self, cr, uid, goal, context=None):
+ """Verify if a goal has not been updated for some time and send a
+ reminder message of needed.
+
+ :return: data to write on the goal object
+ """
+ if goal.remind_update_delay and goal.last_update:
+ delta_max = timedelta(days=goal.remind_update_delay)
+ last_update = datetime.strptime(goal.last_update, DF).date()
+ if date.today() - last_update > delta_max and goal.state == 'inprogress':
+ # generate a remind report
+ temp_obj = self.pool.get('email.template')
+ template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context)
+ body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context)
+
+ self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment')
+ return {'state': 'inprogress_update'}
+ return {}
+
+ def update(self, cr, uid, ids, context=None):
+ """Update the goals to recomputes values and change of states
+
+ If a manual goal is not updated for enough time, the user will be
+ reminded to do so (done only once, in 'inprogress' state).
+ If a goal reaches the target value, the status is set to reached
+ If the end date is passed (at least +1 day, time not considered) without
+ the target value being reached, the goal is set as failed."""
+ if context is None:
+ context = {}
+
+ for goal in self.browse(cr, uid, ids, context=context):
+ towrite = {}
+ if goal.state in ('draft', 'canceled'):
+ # skip if goal draft or canceled
+ continue
+
+ if goal.definition_id.computation_mode == 'manually':
+ towrite.update(self._check_remind_delay(cr, uid, goal, context))
+
+ elif goal.definition_id.computation_mode == 'python':
+ # execute the chosen method
+ cxt = {
+ 'self': self.pool.get('gamification.goal'),
+ 'object': goal,
+ 'pool': self.pool,
+ 'cr': cr,
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'uid': uid,
+ 'result': False,
+ 'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time
+ }
+ code = goal.definition_id.compute_code.strip()
+ safe_eval(code, cxt, mode="exec", nocopy=True)
+ # the result of the evaluated codeis put in the 'result' local variable, propagated to the context
+ result = cxt.get('result', False)
+ if result and type(result) in (float, int, long):
+ if result != goal.current:
+ towrite['current'] = result
+ else:
+ _logger.exception(_('Invalid return content from the evaluation of %s' % code))
+
+ else: # count or sum
+ obj = self.pool.get(goal.definition_id.model_id.model)
+ field_date_name = goal.definition_id.field_date_id.name
+
+ # eval the domain with user replaced by goal user object
+ domain = safe_eval(goal.definition_id.domain, {'user': goal.user_id})
+
+ # add temporal clause(s) to the domain if fields are filled on the goal
+ if goal.start_date and field_date_name:
+ domain.append((field_date_name, '>=', goal.start_date))
+ if goal.end_date and field_date_name:
+ domain.append((field_date_name, '<=', goal.end_date))
+
+ if goal.definition_id.computation_mode == 'sum':
+ field_name = goal.definition_id.field_id.name
+ res = obj.read_group(cr, uid, domain, [field_name], [''], context=context)
+ new_value = res and res[0][field_name] or 0.0
+
+ else: # computation mode = count
+ new_value = obj.search(cr, uid, domain, context=context, count=True)
+
+ # avoid useless write if the new value is the same as the old one
+ if new_value != goal.current:
+ towrite['current'] = new_value
+
+ # check goal target reached
+ if (goal.definition_condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.definition_condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal):
+ towrite['state'] = 'reached'
+
+ # check goal failure
+ elif goal.end_date and fields.date.today() > goal.end_date:
+ towrite['state'] = 'failed'
+ if towrite:
+ self.write(cr, uid, [goal.id], towrite, context=context)
+ return True
+
+ def action_start(self, cr, uid, ids, context=None):
+ """Mark a goal as started.
+
+ This should only be used when creating goals manually (in draft state)"""
+ self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
+ return self.update(cr, uid, ids, context=context)
+
+ def action_reach(self, cr, uid, ids, context=None):
+ """Mark a goal as reached.
+
+ If the target goal condition is not met, the state will be reset to In
+ Progress at the next goal update until the end date."""
+ return self.write(cr, uid, ids, {'state': 'reached'}, context=context)
+
+ def action_fail(self, cr, uid, ids, context=None):
+ """Set the state of the goal to failed.
+
+ A failed goal will be ignored in future checks."""
+ return self.write(cr, uid, ids, {'state': 'failed'}, context=context)
+
+ def action_cancel(self, cr, uid, ids, context=None):
+ """Reset the completion after setting a goal as reached or failed.
+
+ This is only the current state, if the date and/or target criterias
+ match the conditions for a change of state, this will be applied at the
+ next goal update."""
+ return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
+
+ def create(self, cr, uid, vals, context=None):
+ """Overwrite the create method to add a 'no_remind_goal' field to True"""
+ if context is None:
+ context = {}
+ context['no_remind_goal'] = True
+ return super(gamification_goal, self).create(cr, uid, vals, context=context)
+
+ def write(self, cr, uid, ids, vals, context=None):
+ """Overwrite the write method to update the last_update field to today
+
+ If the current value is changed and the report frequency is set to On
+ change, a report is generated
+ """
+ if context is None:
+ context = {}
+ vals['last_update'] = fields.date.today()
+ result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context)
+ for goal in self.browse(cr, uid, ids, context=context):
+ if goal.state != "draft" and ('definition_id' in vals or 'user_id' in vals):
+ # avoid drag&drop in kanban view
+ raise osv.except_osv(_('Error!'), _('Can not modify the configuration of a started goal'))
+
+ if vals.get('current'):
+ if 'no_remind_goal' in context:
+ # new goals should not be reported
+ continue
+
+ if goal.challenge_id and goal.challenge_id.report_message_frequency == 'onchange':
+ self.pool.get('gamification.challenge').report_progress(cr, SUPERUSER_ID, goal.challenge_id, users=[goal.user_id], context=context)
+ return result
+
+ def get_action(self, cr, uid, goal_id, context=None):
+ """Get the ir.action related to update the goal
+
+ In case of a manual goal, should return a wizard to update the value
+ :return: action description in a dictionnary
+ """
+ goal = self.browse(cr, uid, goal_id, context=context)
+
+ if goal.definition_id.action_id:
+ # open a the action linked to the goal
+ action = goal.definition_id.action_id.read()[0]
+
+ if goal.definition_id.res_id_field:
+ current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ action['res_id'] = safe_eval(goal.definition_id.res_id_field, {'user': current_user})
+
+ # if one element to display, should see it in form mode if possible
+ action['views'] = [(view_id, mode) for (view_id, mode) in action['views'] if mode == 'form'] or action['views']
+ return action
+
+ if goal.computation_mode == 'manually':
+ # open a wizard window to update the value manually
+ action = {
+ 'name': _("Update %s") % goal.definition_id.name,
+ 'id': goal_id,
+ 'type': 'ir.actions.act_window',
+ 'views': [[False, 'form']],
+ 'target': 'new',
+ 'context': {'default_goal_id': goal_id, 'default_current': goal.current},
+ 'res_model': 'gamification.goal.wizard'
+ }
+ return action
+
+ return False
diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py
new file mode 100644
index 00000000000..408ddddf082
--- /dev/null
+++ b/addons/gamification/models/res_users.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>
+#
+##############################################################################
+
+from openerp.osv import osv
+from challenge import MAX_VISIBILITY_RANKING
+
+class res_users_gamification_group(osv.Model):
+ """ Update of res.users class
+ - if adding groups to an user, check gamification.challenge linked to
+ this group, and the user. This is done by overriding the write method.
+ """
+ _name = 'res.users'
+ _inherit = ['res.users']
+
+ def write(self, cr, uid, ids, vals, context=None):
+ """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge"""
+ write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context)
+ if vals.get('groups_id'):
+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
+ user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
+ user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
+
+ challenge_obj = self.pool.get('gamification.challenge')
+ challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context)
+ if challenge_ids:
+ challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context)
+ return write_res
+
+ def create(self, cr, uid, vals, context=None):
+ """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge"""
+ write_res = super(res_users_gamification_group, self).create(cr, uid, vals, context=context)
+ if vals.get('groups_id'):
+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
+ user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
+ user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
+
+ challenge_obj = self.pool.get('gamification.challenge')
+ challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context)
+ if challenge_ids:
+ challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, write_res)]}, context=context)
+ return write_res
+
+ # def get_goals_todo_info(self, cr, uid, context=None):
+
+ def get_serialised_gamification_summary(self, cr, uid, context=None):
+ return self._serialised_goals_summary(cr, uid, user_id=uid, context=context)
+
+ def _serialised_goals_summary(self, cr, uid, user_id, context=None):
+ """Return a serialised list of goals assigned to the user, grouped by challenge
+
+ [
+ {
+ 'id': <gamification.challenge id>,
+ 'name': <gamification.challenge name>,
+ 'visibility_mode': <visibility {ranking,personal}>,
+ 'currency': <res.currency id>,
+ 'lines': [(see gamification_challenge._get_serialized_challenge_lines() format)]
+ },
+ ]
+ """
+ all_goals_info = []
+ challenge_obj = self.pool.get('gamification.challenge')
+
+ user = self.browse(cr, uid, uid, context=context)
+ challenge_ids = challenge_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
+ for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context):
+ # serialize goals info to be able to use it in javascript
+ all_goals_info.append({
+ 'id': challenge.id,
+ 'name': challenge.name,
+ 'visibility_mode': challenge.visibility_mode,
+ 'currency': user.company_id.currency_id.id,
+ 'lines': challenge_obj._get_serialized_challenge_lines(cr, uid, challenge, user_id, restrict_top=MAX_VISIBILITY_RANKING, context=context),
+ })
+
+ return all_goals_info
+
+ def get_challenge_suggestions(self, cr, uid, context=None):
+ """Return the list of challenges suggested to the user"""
+ challenge_info = []
+ challenge_obj = self.pool.get('gamification.challenge')
+ challenge_ids = challenge_obj.search(cr, uid, [('invited_user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
+ for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context):
+ values = {
+ 'id': challenge.id,
+ 'name': challenge.name,
+ 'description': challenge.description,
+ }
+ challenge_info.append(values)
+ return challenge_info
+
+
+class res_groups_gamification_group(osv.Model):
+ """ Update of res.groups class
+ - if adding users from a group, check gamification.challenge linked to
+ this group, and the user. This is done by overriding the write method.
+ """
+ _name = 'res.groups'
+ _inherit = 'res.groups'
+
+ # No need to overwrite create as very unlikely to be the value in the autojoin_group_id field
+ def write(self, cr, uid, ids, vals, context=None):
+ """Overwrite to autosubscribe users if add users to a group marked as autojoin, these will be added to the challenge"""
+ write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context)
+ if vals.get('users'):
+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
+ user_ids = [command[1] for command in vals['users'] if command[0] == 4]
+ user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]]
+
+ challenge_obj = self.pool.get('gamification.challenge')
+ challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', ids)], context=context)
+ if challenge_ids:
+ challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context)
+ return write_res
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),
+ '&amp;',
+ ('challenge_id.user_ids','in',user.id),
+ ('challenge_id.visibility_mode','=','ranking')]</field>
+ </record>
+
+ <record id="goal_gamification_manager_visibility" model="ir.rule">
+ <field name="name">Gamification Manager can see any goal</field>
+ <field name="model_id" ref="model_gamification_goal"/>
+ <field name="groups" eval="[(4, ref('group_goal_manager'))]"/>
+ <field name="perm_read" eval="True"/>
+ <field name="perm_write" eval="True"/>
+ <field name="perm_create" eval="False"/>
+ <field name="perm_unlink" eval="False"/>
+ <field name="domain_force">[(1, '=', 1)]</field>
+ </record>
+
+ </data>
+</openerp>
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
new file mode 100644
index 00000000000..52a75f726ba
--- /dev/null
+++ b/addons/gamification/static/description/icon.png
Binary files differ
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: &lt;=
+ </t>
+ <span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="line.target"/></span>
+ <t t-if="line.suffix"><t t-esc="line.suffix"/></t>
+ </div>
+ </t>
+ </div>
+ </div>
+ </div>
+ </t>
+
+ <t t-if="challenge.visibility_mode == 'ranking'">
+ <div t-foreach="challenge.lines" t-as="line" class="oe_goals_list oe_table">
+ <div class="oe_row">
+ <div class="oe_cell oe_thead" colspan="3" t-attf-title="#{line.description ? line.description : ''}">
+ <strong><t t-esc="line.name"/></strong>
+ <br/>
+ <div class="oe_grey">
+ <t t-if="line.definition_condition == 'higher'">
+ Target:
+ </t>
+ <t t-if="line.definition_condition == 'lower'">
+ Target: &lt;=
+ </t>
+ <span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="line.target" /></span><t t-if="line.suffix"> <t t-esc="line.suffix"/></t>
+ </div>
+ </div>
+ </div>
+ <div t-foreach="line.goals" t-as="goal" t-attf-class="#{goal.id == line.own_goal_id ? 'oe_bold' : ''}">
+ <div t-attf-class="oe_row oe_goal_outer_box #{goal.state == 'reached' ? 'oe_goal_reached' : ''} #{line.display_mode != 'progress' ? 'oe_no_progress' : ''}">
+ <t t-if="line.display_mode == 'progress'">
+ <div class="oe_goal_progress_background"></div>
+ <div class="oe_goal_progress" t-attf-style="#{line.display_mode == 'progress' ? 'width: '+goal.completeness+'%;' : 'width:0;'}"></div>
+ </t>
+
+ <div class="oe_cell col0"><t t-esc="goal.rank" /></div>
+ <div class="oe_cell col1"><img class="oe_user_avatar" t-attf-alt="#{goal.name}" t-attf-data-id="#{goal.user_id}"/></div>
+ <div class="oe_cell col2">
+ <t t-if="line.display_mode == 'progress'">
+ <!-- progress, action on current value -->
+ <t t-esc="goal.name"/><br/>
+ <t t-if="goal.id != line.own_goal_id or (line.computation_mode != 'manually' and !line.action)">
+ <span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="goal.current" /></span><t t-if="line.suffix"> <t t-esc="line.suffix"/></t>
+ </t>
+ <t t-if="goal.id == line.own_goal_id and (line.action or line.computation_mode == 'manually')">
+ <a class="oe_goal_action" t-att-id="line.own_goal_id">
+ <span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="goal.current" /></span><t t-if="line.suffix"> <t t-esc="line.suffix"/></t>
+ </a>
+ </t>
+ </t>
+ <t t-if="line.display_mode != 'progress'">
+ <!-- not progress, action on user name -->
+ <t t-if="goal.id != line.own_goal_id or (line.computation_mode != 'manually' and !line.action)">
+ <t t-esc="goal.name"/>
+ </t>
+ <t t-if="goal.id == line.own_goal_id and (line.action or line.computation_mode == 'manually')">
+ <a class="oe_goal_action" t-att-id="line.own_goal_id"><t t-esc="goal.name"/></a>
+ </t>
+ </t>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+ </div>
+ </t>
+
+ <t t-name="gamification.ChallengeSuggestion">
+ <div class="oe_goal">
+ <h4>Invited Challenges</h4>
+ <ul t-foreach="challenges" t-as="challenge" class="oe_goals_list">
+ <li>
+ <strong><a class="oe_challenge_reply" t-attf-title="#{challenge.description.value ? challenge.description.value : ''}" t-attf-id="{challenge.id}" title="more details..."><t t-esc="challenge.name"/></a></strong>
+ </li>
+ </ul>
+ </div>
+ </t>
+
+</templates>
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..a269f710c02
--- /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" attrs="{'readonly':[('state','!=','draft')]}"/>
+ </group>
+ <group string="Schedule">
+ <field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/>
+ <field name="end_date" />
+ <field name="computation_mode" invisible="1"/>
+
+ <label for="remind_update_delay" attrs="{'invisible':[('computation_mode','!=', 'manually')]}"/>
+ <div attrs="{'invisible':[('computation_mode','!=', 'manually')]}">
+ <field name="remind_update_delay" class="oe_inline"/>
+ days
+ </div>
+ <field name="last_update" groups="base.group_no_one"/>
+ </group>
+ <group string="Data" colspan="4">
+ <label for="target_goal" />
+ <div>
+ <field name="target_goal" attrs="{'readonly':[('state','!=','draft')]}" class="oe_inline"/>
+ <field name="definition_suffix" class="oe_inline"/>
+ </div>
+ <label for="current" />
+ <div>
+ <field name="current" class="oe_inline"/>
+ <button string="refresh" type="object" name="update" class="oe_link" attrs="{'invisible':['|',('computation_mode', '=', 'manually'),('state', '=', 'draft')]}" />
+ <div class="oe_grey" attrs="{'invisible':[('definition_id', '=', False)]}">
+ Reached when current value is <strong><field name="definition_condition" class="oe_inline"/></strong> than the target.
+ </div>
+ </div>
+ </group>
+ </group>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="goal_search_view" model="ir.ui.view">
+ <field name="name">Goal Search</field>
+ <field name="model">gamification.goal</field>
+ <field name="arch" type="xml">
+ <search string="Search Goals">
+ <filter name="my" string="My Goals" domain="[('user_id', '=', uid)]"/>
+ <separator/>
+ <filter name="draft" string="Draft" domain="[('state', '=', 'draft')]"/>
+ <filter name="inprogress" string="Current"
+ domain="[
+ '|',
+ ('state', 'in', ('inprogress', 'inprogress_update')),
+ ('end_date', '>=', context_today().strftime('%%Y-%%m-%%d'))
+ ]"/>
+ <filter name="closed" string="Passed" domain="[('state', 'in', ('reached', 'failed'))]"/>
+ <separator/>
+
+ <field name="user_id"/>
+ <field name="definition_id"/>
+ <field name="challenge_id"/>
+ <group expand="0" string="Group By...">
+ <filter name="group_by_user" string="User" domain="[]" context="{'group_by':'user_id'}"/>
+ <filter name="group_by_definition" string="Goal Definition" domain="[]" context="{'group_by':'definition_id'}"/>
+ <filter string="State" domain="[]" context="{'group_by':'state'}"/>
+ <filter string="End Date" domain="[]" context="{'group_by':'end_date'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record id="goal_kanban_view" model="ir.ui.view" >
+ <field name="name">Goal Kanban View</field>
+ <field name="model">gamification.goal</field>
+ <field name="arch" type="xml">
+ <kanban version="7.0" class="oe_background_grey">
+ <field name="definition_id"/>
+ <field name="user_id"/>
+ <field name="current"/>
+ <field name="completeness"/>
+ <field name="state"/>
+ <field name="target_goal"/>
+ <field name="definition_condition"/>
+ <field name="definition_suffix"/>
+ <field name="definition_display"/>
+ <field name="start_date"/>
+ <field name="end_date"/>
+ <field name="last_update"/>
+ <templates>
+ <t t-name="kanban-tooltip">
+ <field name="definition_description"/>
+ </t>
+ <t t-name="kanban-box">
+ <div t-attf-class="oe_kanban_card oe_gamification_goal oe_kanban_goal #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}">
+ <div class="oe_kanban_content">
+ <p><h4 class="oe_goal_name" tooltip="kanban-tooltip"><field name="definition_id" /></h4></p>
+ <div class="oe_kanban_left">
+ <img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" width="24" height="24" />
+ </div>
+ <field name="user_id" />
+ <div class="oe_goal_state_block">
+ <t t-if="record.definition_display.raw_value == 'boolean'">
+ <div class="oe_goal_state oe_e">
+ <t t-if="record.state.raw_value=='reached'"><span class="oe_green" title="Goal Reached">W</span></t>
+ <t t-if="record.state.raw_value=='inprogress' || record.state.raw_value=='inprogress_update'"><span title="Goal in Progress">N</span></t>
+ <t t-if="record.state.raw_value=='failed'"><span class="oe_red" title="Goal Failed">X</span></t>
+ </div>
+ </t>
+ <t t-if="record.definition_display.raw_value == 'progress'">
+ <t t-if="record.definition_condition.raw_value =='higher'">
+ <field name="current" widget="gauge" style="width:160px; height: 120px;" options="{'max_field': 'target_goal', 'label_field': 'definition_suffix'}" />
+ </t>
+ <t t-if="record.definition_condition.raw_value != 'higher'">
+ <div t-attf-class="oe_goal_state #{record.current.raw_value == record.target_goal.raw_value+1 ? 'oe_orange' : record.current.raw_value &gt; record.target_goal.raw_value ? 'oe_red' : 'oe_green'}">
+ <t t-esc="record.current.raw_value" />
+ </div>
+ <em>Target: less than <t t-esc="record.target_goal.raw_value" /></em>
+ </t>
+ </t>
+
+ </div>
+ <p>
+ <t t-if="record.start_date.value">
+ From <t t-esc="record.start_date.value" />
+ </t>
+ <t t-if="record.end_date.value">
+ To <t t-esc="record.end_date.value" />
+ </t>
+ </p>
+ </div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
+
+ <!-- Goal definitions view -->
+
+ <record id="goal_definition_list_action" model="ir.actions.act_window">
+ <field name="name">Goal Definitions</field>
+ <field name="res_model">gamification.goal.definition</field>
+ <field name="view_mode">tree,form</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click to create a goal definition.
+ </p>
+ <p>
+ A goal definition is a technical model of goal defining a condition to reach.
+ The dates, values to reach or users are defined in goal instance.
+ </p>
+ </field>
+ </record>
+
+ <record id="goal_definition_list_view" model="ir.ui.view">
+ <field name="name">Goal Definitions List</field>
+ <field name="model">gamification.goal.definition</field>
+ <field name="arch" type="xml">
+ <tree string="Goal Definitions">
+ <field name="name"/>
+ <field name="computation_mode"/>
+ </tree>
+ </field>
+ </record>
+
+
+ <record id="goal_definition_form_view" model="ir.ui.view">
+ <field name="name">Goal Definitions Form</field>
+ <field name="model">gamification.goal.definition</field>
+ <field name="arch" type="xml">
+ <form string="Goal definitions" version="7.0">
+ <sheet>
+ <label for="name" class="oe_edit_only"/>
+ <h1>
+ <field name="name" class="oe_inline"/>
+ </h1>
+ <label for="description" class="oe_edit_only"/>
+ <div>
+ <field name="description" class="oe_inline"/>
+ </div>
+
+ <group string="How to compute the goal?">
+
+ <field widget="radio" name="computation_mode"/>
+
+ <!-- Hide the fields below if manually -->
+ <field name="model_id" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
+ <field name="field_id" attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}" domain="[('model_id','=',model_id)]" class="oe_inline"/>
+ <field name="field_date_id" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))]}" domain="[('ttype', 'in', ('date', 'datetime')), ('model_id','=',model_id)]" class="oe_inline"/>
+ <field name="domain" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
+ <field name="compute_code" attrs="{'invisible':[('computation_mode','!=','python')], 'required':[('computation_mode','=','python')]}" placeholder="e.g. result = pool.get('mail.followers').search(cr, uid, [('res_model', '=', 'mail.group'), ('partner_id', '=', object.user_id.partner_id.id)], count=True, context=context)"/>
+ <field name="condition" widget="radio"/>
+ </group>
+ <group string="Formating Options">
+ <field name="display_mode" widget="radio" />
+ <field name="suffix" placeholder="e.g. days"/>
+ <field name="monetary"/>
+ </group>
+ <group string="Clickable Goals" attrs="{'invisible': [('computation_mode', '=', 'manually')]}">
+ <field name="action_id" class="oe_inline"/>
+ <field name="res_id_field" attrs="{'invisible': [('action_id', '=', False)]}" class="oe_inline"/>
+ </group>
+
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="goal_definition_search_view" model="ir.ui.view">
+ <field name="name">Goal Definition Search</field>
+ <field name="model">gamification.goal.definition</field>
+ <field name="arch" type="xml">
+ <search string="Search Goal Definitions">
+ <field name="name"/>
+ <field name="model_id"/>
+ <field name="field_id"/>
+ <group expand="0" string="Group By...">
+ <filter string="Model" domain="[]" context="{'group_by':'model_id'}"/>
+ <filter string="Computation Mode" domain="[]" context="{'group_by':'computation_mode'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <!-- menus in settings - technical feature required -->
+ <menuitem id="gamification_menu" name="Gamification Tools" parent="base.menu_administration" groups="base.group_no_one" />
+ <menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="0"/>
+ <menuitem id="gamification_challenge_menu" parent="gamification_menu" action="challenge_list_action" sequence="10"/>
+ <menuitem id="gamification_definition_menu" parent="gamification_menu" action="goal_definition_list_action" sequence="20"/>
+ <menuitem id="gamification_badge_menu" parent="gamification_menu" action="badge_list_action" sequence="30"/>
+
+ </data>
+</openerp>
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>
diff --git a/addons/gamification_sale_crm/__init__.py b/addons/gamification_sale_crm/__init__.py
new file mode 100644
index 00000000000..e14c014a1ef
--- /dev/null
+++ b/addons/gamification_sale_crm/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
diff --git a/addons/gamification_sale_crm/__openerp__.py b/addons/gamification_sale_crm/__openerp__.py
new file mode 100644
index 00000000000..b2ec9a10428
--- /dev/null
+++ b/addons/gamification_sale_crm/__openerp__.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+{
+ 'name': 'CRM Gamification',
+ 'version': '1.0',
+ 'author': 'OpenERP SA',
+ 'category': 'hidden',
+ 'depends': ['gamification','sale_crm'],
+ 'description': """Example of goal definitions and challenges that can be used related to the usage of the CRM Sale module.""",
+
+ 'data': ['sale_crm_goals.xml'],
+ 'demo': ['sale_crm_goals_demo.xml'],
+ 'auto_install': True,
+}
diff --git a/addons/gamification_sale_crm/sale_crm_goals.xml b/addons/gamification_sale_crm/sale_crm_goals.xml
new file mode 100644
index 00000000000..b48cc8a3117
--- /dev/null
+++ b/addons/gamification_sale_crm/sale_crm_goals.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0"?>
+<openerp>
+ <data>
+
+ <!-- goal definitions -->
+ <record model="gamification.goal.definition" id="definition_crm_tot_invoices">
+ <field name="name">Total Invoiced</field>
+ <field name="description"></field>
+ <field name="computation_mode">sum</field>
+ <field name="monetary">True</field>
+ <field name="model_id" eval="ref('account.model_account_invoice_report')" />
+ <field name="field_id" eval="ref('account.field_account_invoice_report_price_total')" />
+ <field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
+ <field name="domain">[('state','!=','cancel'),('user_id','=',user.id),('type','=','out_invoice')]</field>
+ </record>
+
+ <record model="gamification.goal.definition" id="definition_crm_nbr_new_leads">
+ <field name="name">New Leads</field>
+ <field name="description">Based on the creation date</field>
+ <field name="computation_mode">count</field>
+ <field name="suffix">leads</field>
+ <field name="model_id" eval="ref('crm.model_crm_lead')" />
+ <field name="field_date_id" eval="ref('crm.field_crm_lead_create_date')" />
+ <!-- lead AND opportunity as don't want to be penalised for lead converted to opportunity -->
+ <field name="domain">[('user_id','=',user.id), '|', ('type', '=', 'lead'), ('type', '=', 'opportunity')]</field>
+ </record>
+
+ <record model="gamification.goal.definition" id="definition_crm_lead_delay_open">
+ <field name="name">Time to Qualify a Lead</field>
+ <field name="description">The average number of days to open the case (lower than)</field>
+ <field name="computation_mode">sum</field>
+ <field name="condition">lower</field>
+ <field name="suffix">days</field>
+ <field name="model_id" eval="ref('crm.model_crm_lead_report')" />
+ <field name="field_id" eval="ref('crm.field_crm_lead_report_delay_close')" />
+ <field name="field_date_id" eval="ref('crm.field_crm_lead_report_date_closed')" />
+ <field name="domain">[('user_id','=',user.id),('type', '=', 'lead')]</field>
+ </record>
+
+ <record model="gamification.goal.definition" id="definition_crm_lead_delay_close">
+ <field name="name">Days to Close a Deal</field>
+ <field name="description">The average number of days to close the case (lower than)</field>
+ <field name="computation_mode">sum</field>
+ <field name="condition">lower</field>
+ <field name="suffix">days</field>
+ <field name="model_id" eval="ref('crm.model_crm_lead_report')" />
+ <field name="field_id" eval="ref('crm.field_crm_lead_report_delay_open')" />
+ <field name="field_date_id" eval="ref('crm.field_crm_lead_report_opening_date')" />
+ <field name="domain">[('user_id','=',user.id)]</field>
+ </record>
+
+
+ <record model="gamification.goal.definition" id="definition_crm_nbr_call">
+ <field name="name">Logged Calls</field>
+ <field name="description">Log a certain number of calls to reach this goal</field>
+ <field name="computation_mode">count</field>
+ <field name="suffix">calls</field>
+ <field name="model_id" eval="ref('crm.model_crm_phonecall')" />
+ <field name="field_date_id" eval="ref('crm.field_crm_phonecall_date_closed')" />
+ <field name="domain">[('user_id','=',user.id),('state','=','done')]</field>
+ </record>
+
+ <record model="gamification.goal.definition" id="definition_crm_nbr_new_opportunities">
+ <field name="name">New Opportunities</field>
+ <field name="description">Based on the opening date</field>
+ <field name="computation_mode">count</field>
+ <field name="suffix">opportunities</field>
+ <field name="model_id" eval="ref('crm.model_crm_lead')" />
+ <field name="field_date_id" eval="ref('crm.field_crm_lead_date_open')" />
+ <field name="domain">[('user_id','=',user.id),('type','=','opportunity')]</field>
+ </record>
+
+ <record model="gamification.goal.definition" id="definition_crm_nbr_sale_order_created">
+ <field name="name">New Sales Orders</field>
+ <field name="description">Based on the creation date</field>
+ <field name="computation_mode">count</field>
+ <field name="suffix">orders</field>
+ <field name="model_id" eval="ref('sale.model_sale_order')" />
+ <field name="field_date_id" eval="ref('sale.field_sale_order_date_order')" />
+ <field name="domain">[('user_id','=',user.id),('state','not in',('draft', 'sent', 'cancel'))]</field>
+ </record>
+
+ <record model="gamification.goal.definition" id="definition_crm_nbr_paid_sale_order">
+ <field name="name">Paid Sales Orders</field>
+ <field name="description">Based on the invoice date</field>
+ <field name="computation_mode">count</field>
+ <field name="suffix">orders</field>
+ <field name="model_id" eval="ref('account.model_account_invoice_report')" />
+ <field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
+ <field name="domain">[('state','=','paid'),('user_id','=',user.id),('type','=','out_invoice')]</field>
+ </record>
+ <record model="gamification.goal.definition" id="definition_crm_tot_paid_sale_order">
+ <field name="name">Total Paid Sales Orders</field>
+ <field name="description">Based on the invoice date</field>
+ <field name="computation_mode">count</field>
+ <field name="monetary">True</field>
+ <field name="model_id" eval="ref('account.model_account_invoice_report')" />
+ <field name="field_id" eval="ref('account.field_account_invoice_report_price_total')" />
+ <field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
+ <field name="domain">[('state','=','paid'),('user_id','=',user.id),('type','=','out_invoice')]</field>
+ </record>
+
+
+ <record model="gamification.goal.definition" id="definition_crm_nbr_customer_refunds">
+ <field name="name">Customer Refunds</field>
+ <field name="description">Refund the least customers (lower than)</field>
+ <field name="computation_mode">count</field>
+ <field name="condition">lower</field>
+ <field name="suffix">invoices</field>
+ <field name="model_id" eval="ref('account.model_account_invoice_report')" />
+ <field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
+ <field name="domain">[('state','!=','cancel'),('user_id','=',user.id),('type','=','out_refund')]</field>
+ </record>
+ <record model="gamification.goal.definition" id="definition_crm_tot_customer_refunds">
+ <field name="name">Total Customer Refunds</field>
+ <field name="description">The total refunded value is a negative value. Validated when higher (min refunded).</field>
+ <field name="computation_mode">sum</field>
+ <field name="condition">higher</field>
+ <field name="monetary">True</field>
+ <field name="model_id" eval="ref('account.model_account_invoice_report')" />
+ <field name="field_id" eval="ref('account.field_account_invoice_report_price_total')" />
+ <field name="field_date_id" eval="ref('account.field_account_invoice_report_day')" />
+ <field name="domain">[('state','!=','cancel'),('user_id','=',user.id),('type','=','out_refund')]</field>
+ </record>
+
+
+
+ <!-- challenges -->
+ <record model="gamification.challenge" id="challenge_crm_sale">
+ <field name="name">Monthly Sales Targets</field>
+ <field name="period">monthly</field>
+ <field name="visibility_mode">ranking</field>
+ <field name="autojoin_group_id" eval="ref('base.group_sale_salesman')" />
+ <field name="report_message_frequency">weekly</field>
+ </record>
+
+ <record model="gamification.challenge" id="challenge_crm_marketing">
+ <field name="name">Lead Acquisition</field>
+ <field name="period">monthly</field>
+ <field name="visibility_mode">ranking</field>
+ <field name="autojoin_group_id" eval="ref('base.group_sale_salesman')" />
+ <field name="report_message_frequency">weekly</field>
+ </record>
+
+ <!-- lines -->
+ <record model="gamification.challenge.line" id="line_crm_sale1">
+ <field name="definition_id" eval="ref('definition_crm_tot_invoices')" />
+ <field name="target_goal">20000</field>
+ <field name="challenge_id" eval="ref('challenge_crm_sale')" />
+ </record>
+
+
+ <record model="gamification.challenge.line" id="line_crm_marketing1">
+ <field name="definition_id" eval="ref('definition_crm_nbr_new_leads')" />
+ <field name="target_goal">7</field>
+ <field name="challenge_id" eval="ref('challenge_crm_marketing')" />
+ <field name="sequence">1</field>
+ </record>
+ <record model="gamification.challenge.line" id="line_crm_marketing2">
+ <field name="definition_id" eval="ref('definition_crm_lead_delay_open')" />
+ <field name="target_goal">15</field>
+ <field name="challenge_id" eval="ref('challenge_crm_marketing')" />
+ <field name="sequence">2</field>
+ </record>
+ <record model="gamification.challenge.line" id="line_crm_marketing3">
+ <field name="definition_id" eval="ref('definition_crm_nbr_new_opportunities')" />
+ <field name="target_goal">5</field>
+ <field name="challenge_id" eval="ref('challenge_crm_marketing')" />
+ <field name="sequence">3</field>
+ </record>
+
+ </data>
+</openerp>
diff --git a/addons/gamification_sale_crm/sale_crm_goals_demo.xml b/addons/gamification_sale_crm/sale_crm_goals_demo.xml
new file mode 100644
index 00000000000..fd69c36162f
--- /dev/null
+++ b/addons/gamification_sale_crm/sale_crm_goals_demo.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<openerp>
+ <data>
+
+ <!-- challenges -->
+ <record model="gamification.challenge" id="challenge_crm_sale">
+ <field name="user_ids" eval="[(4,ref('base.user_demo'))]" />
+ <field name="state">inprogress</field>
+ </record>
+
+ <!-- goals -->
+ <record model="gamification.goal" id="goal_crm_sale1">
+ <field name="definition_id" eval="ref('definition_crm_tot_invoices')" />
+ <field name="user_id" eval="ref('base.user_demo')" />
+ <field name="line_id" eval="ref('line_crm_sale1')" />
+ <field name="start_date" eval="time.strftime('%Y-%m-01')" />
+ <field name="end_date" eval="time.strftime('%Y-%m-31')" />
+ <field name="target_goal">2000</field>
+ <field name="state">inprogress</field>
+ </record>
+
+ </data>
+</openerp>
diff --git a/addons/hr/res_config.py b/addons/hr/res_config.py
index 147c4e4d5b9..94c029262ff 100644
--- a/addons/hr/res_config.py
+++ b/addons/hr/res_config.py
@@ -42,6 +42,8 @@ class hr_config_settings(osv.osv_memory):
help ="""This installs the module hr_contract."""),
'module_hr_evaluation': fields.boolean('Organize employees periodic evaluation',
help ="""This installs the module hr_evaluation."""),
+ 'module_hr_gamification': fields.boolean('Drive engagement with challenges and badges',
+ help ="""This installs the module hr_gamification."""),
'module_account_analytic_analysis': fields.boolean('Allow invoicing based on timesheets (the sale application will be installed)',
help ="""This installs the module account_analytic_analysis, which will install sales management too."""),
'module_hr_payroll': fields.boolean('Manage payroll',
diff --git a/addons/hr/res_config_view.xml b/addons/hr/res_config_view.xml
index 632ceb56918..3dc1795139c 100644
--- a/addons/hr/res_config_view.xml
+++ b/addons/hr/res_config_view.xml
@@ -56,6 +56,10 @@
<field name="module_hr_expense" class="oe_inline"/>
<label for="module_hr_expense"/>
</div>
+ <div>
+ <field name="module_hr_gamification" class="oe_inline"/>
+ <label for="module_hr_gamification"/>
+ </div>
</div>
</group>
<group name="timesheet_grp">
diff --git a/addons/hr/static/src/css/hr.css b/addons/hr/static/src/css/hr.css
index 951ec4d02e3..be21ca7e7a6 100644
--- a/addons/hr/static/src/css/hr.css
+++ b/addons/hr/static/src/css/hr.css
@@ -72,4 +72,4 @@
.openerp .oe_employee_vignette .oe_followers {
width: auto;
float: none;
-}
+} \ No newline at end of file
diff --git a/addons/hr_evaluation/hr_evaluation.py b/addons/hr_evaluation/hr_evaluation.py
index 09c47b89153..462154e9e4a 100644
--- a/addons/hr_evaluation/hr_evaluation.py
+++ b/addons/hr_evaluation/hr_evaluation.py
@@ -101,6 +101,8 @@ Thanks,
class hr_employee(osv.osv):
_name = "hr.employee"
_inherit="hr.employee"
+
+
_columns = {
'evaluation_plan_id': fields.many2one('hr_evaluation.plan', 'Appraisal Plan'),
'evaluation_date': fields.date('Next Appraisal Date', help="The date of the next appraisal is computed by the appraisal plan's dates (first appraisal + periodicity)."),
@@ -123,6 +125,8 @@ class hr_employee(osv.osv):
obj_evaluation.button_plan_in_progress(cr, uid, [plan_id], context=context)
return True
+
+
class hr_evaluation(osv.osv):
_name = "hr_evaluation.evaluation"
_inherit = "mail.thread"
diff --git a/addons/hr_evaluation/security/ir.model.access.csv b/addons/hr_evaluation/security/ir.model.access.csv
index a304ee84c10..4be3d74b7d8 100644
--- a/addons/hr_evaluation/security/ir.model.access.csv
+++ b/addons/hr_evaluation/security/ir.model.access.csv
@@ -39,4 +39,4 @@ access_survey_response_hr_employee,survey.response.employee,survey.model_survey_
access_survey_question_column_heading_hr_employee,survey.question.column.heading.employee,survey.model_survey_question_column_heading,base.group_user,1,0,0,0
access_survey_response_line_hr_employee,survey.response.line.employee,survey.model_survey_response_line,base.group_user,1,1,1,0
access_survey_response_answer_hr_employee,survey.response.answer.hr.employee,survey.model_survey_response_answer,base.group_user,1,1,1,0
-access_survey_tbl_column_heading_hr_employee,survey.tbl.column.heading,survey.model_survey_tbl_column_heading,base.group_user,1,1,1,0
+access_survey_tbl_column_heading_hr_employee,survey.tbl.column.heading,survey.model_survey_tbl_column_heading,base.group_user,1,1,1,0 \ No newline at end of file
diff --git a/addons/hr_gamification/__init__.py b/addons/hr_gamification/__init__.py
new file mode 100644
index 00000000000..1896704c7fc
--- /dev/null
+++ b/addons/hr_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 \ No newline at end of file
diff --git a/addons/hr_gamification/__openerp__.py b/addons/hr_gamification/__openerp__.py
new file mode 100644
index 00000000000..c75a4454437
--- /dev/null
+++ b/addons/hr_gamification/__openerp__.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+{
+ 'name': 'HR Gamification',
+ 'version': '1.0',
+ 'author': 'OpenERP SA',
+ 'category': 'hidden',
+ 'depends': ['gamification', 'hr'],
+ 'description': """Use the HR ressources for the gamification process.
+
+The HR officer can now manage challenges and badges.
+This allow the user to send badges to employees instead of simple users.
+Badge received are displayed on the user profile.
+""",
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'security/gamification_security.xml',
+ 'wizard/grant_badge.xml',
+ 'views/gamification.xml',
+ ],
+ 'js': ['static/src/js/gamification.js'],
+}
diff --git a/addons/hr_gamification/models/__init__.py b/addons/hr_gamification/models/__init__.py
new file mode 100644
index 00000000000..51cf01dfa09
--- /dev/null
+++ b/addons/hr_gamification/models/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import gamification
diff --git a/addons/hr_gamification/models/gamification.py b/addons/hr_gamification/models/gamification.py
new file mode 100644
index 00000000000..b2aa9039cb1
--- /dev/null
+++ b/addons/hr_gamification/models/gamification.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>
+#
+##############################################################################
+
+from openerp.osv import fields, osv
+
+
+class hr_gamification_badge_user(osv.Model):
+ """User having received a badge"""
+
+ _name = 'gamification.badge.user'
+ _inherit = ['gamification.badge.user']
+
+ _columns = {
+ 'employee_id': fields.many2one("hr.employee", string='Employee'),
+ }
+
+ def _check_employee_related_user(self, cr, uid, ids, context=None):
+ for badge_user in self.browse(cr, uid, ids, context=context):
+ if badge_user.user_id and badge_user.employee_id:
+ if badge_user.employee_id not in badge_user.user_id.employee_ids:
+ return False
+ return True
+
+ _constraints = [
+ (_check_employee_related_user, "The selected employee does not correspond to the selected user.", ['employee_id']),
+ ]
+
+
+class gamification_badge(osv.Model):
+ _name = 'gamification.badge'
+ _inherit = ['gamification.badge']
+
+ def get_granted_employees(self, cr, uid, badge_ids, context=None):
+ if context is None:
+ context = {}
+
+ employee_ids = []
+ badge_user_ids = self.pool.get('gamification.badge.user').search(cr, uid, [('badge_id', 'in', badge_ids), ('employee_id', '!=', False)], context=context)
+ for badge_user in self.pool.get('gamification.badge.user').browse(cr, uid, badge_user_ids, context):
+ employee_ids.append(badge_user.employee_id.id)
+ # remove duplicates
+ employee_ids = list(set(employee_ids))
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Granted Employees',
+ 'view_mode': 'kanban,tree,form',
+ 'view_type': 'form',
+ 'res_model': 'hr.employee',
+ 'domain': [('id', 'in', employee_ids)]
+ }
+
+
+class hr_employee(osv.osv):
+ _name = "hr.employee"
+ _inherit = "hr.employee"
+
+ def _get_employee_goals(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the list of goals assigned to the employee"""
+ res = {}
+ for employee in self.browse(cr, uid, ids, context=context):
+ res[employee.id] = self.pool.get('gamification.goal').search(cr,uid,[('user_id', '=', employee.user_id.id), ('challenge_id.category', '=', 'hr')], context=context)
+ return res
+
+ def _get_employee_badges(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the list of badge_users assigned to the employee"""
+ res = {}
+ for employee in self.browse(cr, uid, ids, context=context):
+ res[employee.id] = self.pool.get('gamification.badge.user').search(cr, uid, [
+ '|',
+ ('employee_id', '=', employee.id),
+ '&',
+ ('employee_id', '=', False),
+ ('user_id', '=', employee.user_id.id)
+ ], context=context)
+ return res
+
+ def _has_badges(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the list of badge_users assigned to the employee"""
+ res = {}
+ for employee in self.browse(cr, uid, ids, context=context):
+ employee_badge_ids = self.pool.get('gamification.badge.user').search(cr, uid, [
+ '|',
+ ('employee_id', '=', employee.id),
+ '&',
+ ('employee_id', '=', False),
+ ('user_id', '=', employee.user_id.id)
+ ], context=context)
+ res[employee.id] = len(employee_badge_ids) > 0
+ return res
+
+ _columns = {
+ 'goal_ids': fields.function(_get_employee_goals, type="one2many", obj='gamification.goal', string="Employee HR Goals"),
+ 'badge_ids': fields.function(_get_employee_badges, type="one2many", obj='gamification.badge.user', string="Employee Badges"),
+ 'has_badges': fields.function(_has_badges, type="boolean", string="Has Badges"),
+ }
diff --git a/addons/hr_gamification/security/gamification_security.xml b/addons/hr_gamification/security/gamification_security.xml
new file mode 100644
index 00000000000..e0f9a6cf918
--- /dev/null
+++ b/addons/hr_gamification/security/gamification_security.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" ?>
+<openerp>
+ <data noupdate="1">
+
+ <record id="goal_officer_visibility" model="ir.rule">
+ <field name="name">HR Officer can see any goal</field>
+ <field name="model_id" ref="gamification.model_gamification_goal"/>
+ <field name="groups" eval="[(4, ref('base.group_hr_user'))]"/>
+ <field name="perm_read" eval="True"/>
+ <field name="perm_write" eval="True"/>
+ <field name="perm_create" eval="False"/>
+ <field name="perm_unlink" eval="False"/>
+ <field name="domain_force">[(1, '=', 1)]</field>
+ </record>
+
+ </data>
+</openerp>
diff --git a/addons/hr_gamification/security/ir.model.access.csv b/addons/hr_gamification/security/ir.model.access.csv
new file mode 100644
index 00000000000..b89db7cd104
--- /dev/null
+++ b/addons/hr_gamification/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+challenge_officer,"Challenge Officer",gamification.model_gamification_challenge,base.group_hr_user,1,1,1,1
+challenge_line_officer,"Challenge Line Officer",gamification.model_gamification_challenge_line,base.group_hr_user,1,1,1,1
+badge_officer,"Badge Officer",gamification.model_gamification_badge,base.group_hr_user,1,1,1,1
+badge_user_officer,"Badge-user Officer",gamification.model_gamification_badge_user,base.group_hr_user,1,1,1,1 \ No newline at end of file
diff --git a/addons/hr_gamification/static/src/js/gamification.js b/addons/hr_gamification/static/src/js/gamification.js
new file mode 100644
index 00000000000..a9126464a10
--- /dev/null
+++ b/addons/hr_gamification/static/src/js/gamification.js
@@ -0,0 +1,19 @@
+openerp.hr_gamification = function(instance) {
+ instance.web_kanban.KanbanRecord.include({
+ on_card_clicked: function() {
+ if (this.view.dataset.model === 'gamification.badge.user') {
+ var action = {
+ type: 'ir.actions.act_window',
+ res_model: 'gamification.badge',
+ view_mode: 'form',
+ view_type: 'form,kanban,tree',
+ views: [[false, 'form']],
+ res_id: this.record.badge_id.raw_value[0]
+ };
+ this.do_action(action);
+ } else {
+ this._super.apply(this, arguments);
+ }
+ }
+ });
+};
diff --git a/addons/hr_gamification/views/gamification.xml b/addons/hr_gamification/views/gamification.xml
new file mode 100644
index 00000000000..e2d45948b95
--- /dev/null
+++ b/addons/hr_gamification/views/gamification.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+
+ <record id="hr_badge_form_view" model="ir.ui.view">
+ <field name="name">Badge Form</field>
+ <field name="model">gamification.badge</field>
+ <field name="inherit_id" ref="gamification.badge_form_view"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[@class='oe_right oe_button_box']" position="inside">
+ <button string="Granted Employees" type="object" name="get_granted_employees" attrs="{'invisible': [('stat_count','=',0)]}" />
+ </xpath>
+ </field>
+ </record>
+
+ <!-- HR Employee -->
+
+ <record id="hr_hr_employee_view_form" model="ir.ui.view">
+ <field name="name">hr.hr.employee.view.form</field>
+ <field name="model">hr.employee</field>
+ <field name="inherit_id" ref="hr.view_employee_form"/>
+ <field name="arch" type="xml">
+
+ <xpath expr="//page[@string='Public Information']" position="before">
+ <page string="Received Badges" attrs="{'invisible': [('user_id', '=', False)]}">
+ <field name="has_badges" invisible="1"/>
+ <button string="Grant a Badge" type="action" name="%(action_reward_wizard)d"/> to reward this employee for a good action
+ <div class="oe_view_nocontent" attrs="{'invisible': [('has_badges', '=', True)]}">
+ <p class="oe_view_nocontent_create">
+ Click to grant this employee his first badge
+ </p><p class="oe_grey">
+ Badges are rewards of good work. Give them to people you believe deserve it.
+ </p>
+ </div>
+ <field name="badge_ids" widget="many2many_kanban" />
+ </page>
+ </xpath>
+ <xpath expr="//page[@string='Public Information']" position="after">
+ <page string="Goals">
+ <field name="goal_ids" widget="many2many_kanban" />
+ </page>
+ </xpath>
+
+ </field>
+ </record>
+
+
+ <record id="goals_menu_groupby_action2" model="ir.actions.act_window">
+ <field name="res_model">gamification.goal</field>
+ <field name="view_type">form</field>
+ <field name="name">Goals History</field>
+ <field name="view_mode">tree,kanban</field>
+ <field name="context">{'search_default_group_by_user': True, 'search_default_group_by_type': True}</field>
+ <field name="domain">[('challenge_id.category', '=', 'hr')]</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click to create a goal.
+ </p>
+ <p>
+ A goal is defined by a user and a goal type.
+ Goals can be created automatically by using challenges.
+ </p>
+ </field>
+ </record>
+
+
+ <record id="challenge_list_action2" model="ir.actions.act_window">
+ <field name="name">Challenges</field>
+ <field name="res_model">gamification.challenge</field>
+ <field name="view_mode">kanban,tree,form</field>
+ <field name="domain">[('category', '=', 'hr')]</field>
+ <field name="context">{'search_default_inprogress':True, 'default_inprogress':True}</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click to create a challenge.
+ </p>
+ <p>
+ Assign a list of goals to chosen users to evaluate them.
+ The challenge can use a period (weekly, monthly...) for automatic creation of goals.
+ The goals are created for the specified users or member of the group.
+ </p>
+ </field>
+ </record>
+ <record id="challenge_list_action2_view1" model="ir.actions.act_window.view">
+ <field eval="1" name="sequence"/>
+ <field name="view_mode">kanban</field>
+ <field name="act_window_id" ref="challenge_list_action2"/>
+ <field name="view_id" ref="gamification.view_challenge_kanban"/>
+ </record>
+ <record id="challenge_list_action2_view2" model="ir.actions.act_window.view">
+ <field eval="10" name="sequence"/>
+ <field name="view_mode">form</field>
+ <field name="act_window_id" ref="challenge_list_action2"/>
+ <field name="view_id" ref="gamification.challenge_form_view"/>
+ </record>
+
+ <menuitem id="menu_hr_gamification" parent="hr.menu_hr_root" name="Engagement" sequence="40"/>
+
+ <menuitem id="gamification_badge_menu_hr" parent="menu_hr_gamification" action="gamification.badge_list_action" />
+ <menuitem id="gamification_challenge_menu_hr" parent="menu_hr_gamification" action="challenge_list_action2" groups="base.group_hr_user"/>
+ <menuitem id="gamification_goal_menu_hr" parent="menu_hr_gamification" action="goals_menu_groupby_action2" groups="base.group_hr_user"/>
+
+ </data>
+</openerp>
diff --git a/addons/hr_gamification/wizard/__init__.py b/addons/hr_gamification/wizard/__init__.py
new file mode 100644
index 00000000000..bb521f011bb
--- /dev/null
+++ b/addons/hr_gamification/wizard/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import grant_badge \ No newline at end of file
diff --git a/addons/hr_gamification/wizard/grant_badge.py b/addons/hr_gamification/wizard/grant_badge.py
new file mode 100644
index 00000000000..b8a51044d4c
--- /dev/null
+++ b/addons/hr_gamification/wizard/grant_badge.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>
+#
+##############################################################################
+
+from openerp.osv import fields, osv
+from openerp.tools.translate import _
+
+class hr_grant_badge_wizard(osv.TransientModel):
+ _name = 'gamification.badge.user.wizard'
+ _inherit = ['gamification.badge.user.wizard']
+
+ _columns = {
+ 'employee_id': fields.many2one("hr.employee", string='Employee', required=True),
+ 'user_id': fields.related("employee_id", "user_id",
+ type="many2one", relation="res.users",
+ store=True, string='User')
+ }
+
+ def action_grant_badge(self, cr, uid, ids, context=None):
+ """Wizard action for sending a badge to a chosen employee"""
+ if context is None:
+ context = {}
+
+ badge_user_obj = self.pool.get('gamification.badge.user')
+
+ for wiz in self.browse(cr, uid, ids, context=context):
+ if not wiz.user_id:
+ raise osv.except_osv(_('Warning!'), _('You can send badges only to employees linked to a user.'))
+
+ if uid == wiz.user_id.id:
+ raise osv.except_osv(_('Warning!'), _('You can not send a badge to yourself'))
+
+ values = {
+ 'user_id': wiz.user_id.id,
+ 'sender_id': uid,
+ 'badge_id': wiz.badge_id.id,
+ 'employee_id': wiz.employee_id.id,
+ 'comment': wiz.comment,
+ }
+
+ badge_user = badge_user_obj.create(cr, uid, values, context=context)
+ result = badge_user_obj._send_badge(cr, uid, [badge_user], context=context)
+ return result \ No newline at end of file
diff --git a/addons/hr_gamification/wizard/grant_badge.xml b/addons/hr_gamification/wizard/grant_badge.xml
new file mode 100644
index 00000000000..6dcf2d2e7c4
--- /dev/null
+++ b/addons/hr_gamification/wizard/grant_badge.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+
+ <record id="view_badge_wizard_grant_employee" model="ir.ui.view">
+ <field name="name">Grant Badge Employee Form</field>
+ <field name="model">gamification.badge.user.wizard</field>
+ <field name="inherit_id" ref="gamification.view_badge_wizard_grant" />
+ <field name="arch" type="xml">
+ <data>
+ <xpath expr="//field[@name='user_id']" position="replace">
+ <field name="employee_id" nolabel="1" domain="[('user_id', '!=', False),('user_id', '!=', uid)]" />
+ </xpath>
+ </data>
+ </field>
+ </record>
+
+ <record id="view_badge_wizard_reward" model="ir.ui.view">
+ <field name="name">Reward Employee Badge Form</field>
+ <field name="model">gamification.badge.user.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Reward Employee with" version="7.0">
+ What are you thank for?
+ <group>
+ <field name="employee_id" invisible="1" />
+ <field name="badge_id" nolabel="1" colspan="4" />
+ <field name="comment" nolabel="1" placeholder="Describe what they did and why it matters (will be public)" />
+ </group>
+ <footer>
+ <button string="Reward Employee" type="object" name="action_grant_badge" class="oe_highlight" /> or
+ <button string="Cancel" special="cancel" class="oe_link"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <act_window domain="[]" id="action_reward_wizard"
+ name="Reward Employee"
+ target="new"
+ res_model="gamification.badge.user.wizard"
+ context="{'default_employee_id': active_id, 'employee_id': active_id}"
+ view_type="form" view_mode="form"
+ view_id="view_badge_wizard_reward"/>
+
+ </data>
+</openerp>
diff --git a/addons/portal_gamification/__init__.py b/addons/portal_gamification/__init__.py
new file mode 100644
index 00000000000..484faca7b68
--- /dev/null
+++ b/addons/portal_gamification/__init__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
diff --git a/addons/portal_gamification/__openerp__.py b/addons/portal_gamification/__openerp__.py
new file mode 100644
index 00000000000..e20e54be6e3
--- /dev/null
+++ b/addons/portal_gamification/__openerp__.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+{
+ 'name': 'Portal Gamification',
+ 'version': '1',
+ 'category': 'Tools',
+ 'complexity': 'easy',
+ 'description': """
+This module adds security rules for gamification to allow portal users to participate to challenges
+===================================================================================================
+ """,
+ 'author': 'OpenERP SA',
+ 'depends': ['gamification','portal'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'security/portal_security.xml',
+ ],
+ 'installable': True,
+ 'auto_install': True,
+ 'category': 'Hidden',
+}
+
diff --git a/addons/portal_gamification/security/ir.model.access.csv b/addons/portal_gamification/security/ir.model.access.csv
new file mode 100644
index 00000000000..4f21669cf55
--- /dev/null
+++ b/addons/portal_gamification/security/ir.model.access.csv
@@ -0,0 +1,7 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+goal_portal,"Goal Portal",gamification.model_gamification_goal,portal.group_portal,1,1,0,0
+goal_definition_portal,"Goal Definition Portal",gamification.model_gamification_goal_definition,portal.group_portal,1,0,0,0
+challenge_portal,"Goal Challenge Portal",gamification.model_gamification_challenge,portal.group_portal,1,0,0,0
+challenge_line_portal,"Challenge Line Portal",gamification.model_gamification_challenge_line,portal.group_portal,1,0,0,0
+badge_portal,"Badge Portal",gamification.model_gamification_badge,portal.group_portal,1,0,0,0
+badge_user_portal,"Badge-user Portal",gamification.model_gamification_badge_user,portal.group_portal,1,1,1,0
diff --git a/addons/portal_gamification/security/portal_security.xml b/addons/portal_gamification/security/portal_security.xml
new file mode 100644
index 00000000000..6b46fe96519
--- /dev/null
+++ b/addons/portal_gamification/security/portal_security.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record id="gamification.goal_user_visibility" model="ir.rule">
+ <field name="groups" eval="[(4, ref('portal.group_portal'))]"/>
+ </record>
+
+ </data>
+</openerp>