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 (). +# +# 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 . +# +############################################################################## + +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 (). +# +# 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 . +# +############################################################################## +{ + '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 @@ + + + + + Good Job + You did great at your job. + everyone + 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 + RU5ErkJggg== + + + + Problem Solver + No one can solve challenges like you do. + everyone + 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 + + + + Hidden + You have found the hidden badge + nobody + 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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== + + + + + + Brilliant + With your brilliant ideas, you are an inspiration to others. + everyone + True + 2 + iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABmJLR0QAvQBcABUZb0TlAAAACXBI + WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QMdCTAQSodN2wAAIABJREFUeNrsvXmUbVd93/nZe5/x + zlPN9eZRehqRkBACZGywmWwwNnjAcXDbTsftOEknbnevtnt5OXHshHjICrEhcRaJzdDGYIyZBEgI + DJLQLCG9eX41j7du3fmeYe/+49yq9yQG6UlV9QT99lpn1V3vVd177vl99+/3/Y0brq6r6+q6uq6u + q+vqurqurqvr6rq6rq6r6+q6uq6uq+vq+sFf4ur3/7ZlrgLgB2P5QBbIOpIbPYsbLCmuk4KDAnYK + KdLP/QOtzaKBGWO4EBtzMoh4phPzGLAKtIAGEF8FwMv3uxwC/lfgna8csfOvHnecW0YdO+9KpBBY + EqQAJUCIb//q2hi0AW0g1hAbQy+GYwtB/NhcENw/0QtWenwL+Evgw30w6KsAuHJrFPiVsYx8w2u2 + ubdeM2D7e4oW2/MKCcQmuYwx31Hgz7eMASEMUoh14FS7mnMrMSerIU/NBWcemAofBv4a+EIfDOYq + ADZf6K/bVZC/c6jiHHr3tSn2lS3Ti4yI9IsT9OUDI/kcRyWa4vOnunzhTIep1ejPVgM+BHzr+8VU + fD8BYAD4zBt3ua/6zVdlsZQwxhixFQJ/oYCwJOaLZ3rijx+qd4OYXwY+AUQvZ63w/QCAt2Zs/uS3 + Xp3bf9OwTc6RJjaIF6vWNxsIUghjQJytRXz46Vbt6xPBJ4F/BgQvRyC8nAHwI3sK6o/efsC/6V3X + pkwnNOgN3vHGGB6bjqn4mu1FGyU35r1Nn0CkbGmOL4fifzzV0t+YDH4f+COg+XICwssRAIWUxad+ + +ebM6991jW+CePM2+qnFAK9yLccnZrltuEHWEcgN/jBjDEoK5pox/9d9NX1hVf8k8MW+RrgKgOes + P/6Za/1/8Us3ppWthAEjNusWjTE8MmczOryTIIroVE9yoGJjq817JMYYztYi/re7a7OR5k7gwpV2 + I9XLRPCHxrLyvj94ff7tP77fFwZEous3TxjaQNWUsC0HKSXTy8sMZQSO2tw9UfKV+blDqWwYm39+ + ZDHKAV+7kiB4OQDgvW/a4375A28uDRQ8KRJPbvMVU72r6ahBqtVlMuk0F5aajKRjfFtsms3pv68Q + wB3jLreNOnc8MtP7yXbI3UD9SnCDK2oClODrf/7m4mv3lSx0wpy2zI//h/MRe3ddyzOHjzA6OkKs + HKzeBa4pb64ZeI7raJRA/O/31HhyLnwrcA8Q/v9BAwxsz8kT/+2txeu35Syzxu63yq2LtGEhHiDo + dmg2G4RhyPbRYc7OLDKel1sCgDVtoI3hTXt8U3Dlex6eCQaBe/uxgx9YANx626hz9P1vKhR8K5H6 + Vvvzta7ByYxxfmICx7bRWpPN5Wj0IkpugGdtHRiFEBhjxHWDttmZt269f6J3rYbPbxUI5BYL/65X + jzmPvu9H8pbZIlv/ndZsUyCkoNVq4TgOxhiCICCdytIITN8cbaEdFoJII16zzeFDP156p4T7gfxW + mOitBMA/ftc1/tf+7evzJr6Cwo+1YaHr0+n2CMMQ27YBqFarDBVzPD3TI7xCUXwhBGM5Zf76neWb + gGeA0mbLaKsA8PP/+IbU//yXt2eNMVeWeC40Y0YHBzl+/gKObZFOJ2UBrVYLYQzSztGJDMZcsWCd + qKSk+bt3lcezDt8Aipspp60AwOt+8oD30V+9OW06obni4fv5tiLl2Kwsr+A6NuVyGcdxEEKwUqux + Y2iQlU685WbgWR4CiJwrzUfeUb4mbXMPkNssc7DZAHjlnePOP/z6rRl68ZXP3WhjiGSWRruDKwXZ + bA6tNYODgwC0221sJVlqG4IrXOZhjBEZR5oPvKV0M/BVILMZINhMAKTvHHe++gc/nDcvl6xdpEHY + aeaWVnBsRbFYZHV1lUwmgxCCTqeDpSQTNeiEV9QMrLuJ23LK/NmbCjcB7wdS3y9uoBxKiQv/6ceK + pSvJ9p+7Hp7oMjI0zhNHT5D3HQYGBjDGEEURnU6HMAxxHAdpuaRkm1y/lOyKagIQg2llXEvc9Phs + OMsGF5tsigZwFZ/64NtKw9+t9u6KsH9j6Moiq602OorwfR8hJVJKjDF4nodUikajwWgpx8SqJnyZ + VPvF2oifO5Qyd+1w3w9cA1gvZwD80h+8Pv/2nCMNLwfhCwFC0oslY0MDzCyuUEi7lCsVpFIIpZCW + Ra5QQCmFTgoB6RqPbmTQCK709xBCEGsjfu91OQZT4nMb6R5uNAC2/dge90O3jDhJudYVFrqQEoQE + IVjsCjzPp9Ftk8/lSGWzKNsmBoRSeKkUbiqFtCwQEtvP0Qg0SNV/j4vvdcVAYOB9byiMAR8E0i87 + DpB1ePSDbymVwitl9/uCT36KRHjSItaab1V9BiuDnJ+aZnigTKFcxrIdwjjC9XyQEsuyieIYISWl + Qp6FRpOhnIOSEtPPTgvWqoWvDBBKnjRScM2T8+ETwJmXygc2UgP8xvt+pLA/1FcoxbgmeClBWWDZ + COUgbJuuSjMysoPZWp1isUChUsFNpXHTaWzPx8/lsH2PfLmMn8kQC8jmc3RUgVC6GNsBaSOUjZE2 + KAVCYK6AeYgN4j3Xp82OnPwQUH6pMtwoDTD02m3uV951bcqYrZb/pbteKpAKIW1QNsJ2wHJZCF3K + w9s5O7tAOZdmaGQ0Eb7vExlNtlBAI0hns/SiEMu28fwUru/jqhDfsRB9oSMEQrAufGH697CFqXxj + jLhx2HE/c7I7QJJCDq4oALIOH/yLt5Wuj/UWl2mvpZClxEgLqaxkpzoOwnYRtge2z7LJkylUqDab + DA8NUBoaxEn5OL6HFlAol4kxZPN5hJQI28JxPdKZLJ2wTca1kSrhAGINbEb0M3n98MwWIl8IwWBK + mpWOPnR8OfooUONFVhVtBABe+Y4D/vtvHra3TvjPsvX9Xa8ssByE4yFsL/nppYiVT5AaxvEdImJG + x4bIFQu4aQ837aFc1QdARDafxc+mkm4gS1IsFZlabVHMOFjKSsxL/3MTLdC/lzW9t4XcINZG7ClZ + 6pPHOncCHwe6VwQABVf8v//pxwo7tNlC4a+xMalAKYTlIOxk12O5CM9H+mlkKsUKDuPbd7LYblIq + ZRneth03owjjCXq9kwS9EwTBWWI9j+1GpLI57FQey1GkMymk7eLYGttRCGlhpEo+W8j+ljd9Usi6 + STBGbzoQhBDkHGkcxdDjc+EXgNkXQwhf6l3u+Ve3Z06/bZ+/dRZw7cH37T2yb+sdF2G7SMdDej7K + 9xGeS40cY2M7OX3hEWx9nl77LEGnQT6vSGUreH4WhCQMu7SbyzRWWsRIUtm95PLXUiq/grnZc/im + C90ucbeD7nYxvS6EPUzQxUQBIo5AR6Bj0EmL4GZvCWMMsYGf/MTSA+2QdwDLl0tGXmpE6Qtv2OVt + ofDFdxS+sD1w3ETwKR/lp1G+j0qlyEvJuZPvxwTz+PkyN93+asb3vw7jDWCIQBiQFkL5CCsHQlGd + +DonHv8cC9NfYm767ymN/Di2P4RxbIRjEVsKrSRxR6w/bvPc3aQ3v09UCIElMHeOu3fec673SuAr + l0sIXwpIR37j1szET13jq3grcvxrvn3f5mNZCMtNbL3rIv0UMuVjpdMoP42VSjE1+TlazSn279nO + odvfg/AKyN7TyM59YBbA0qB04kjJPq+QOUzqbZjse0BbzB77CA998X8gVZ7tu36WuNsjaneI2y10 + u4PuttHdLgQdiEKIQoSOwMSgDWILtocl4a6/Wvwc8F6gejnIe9GCy9j88d/8VOVfOWoLiE+f7Rsh + QVqJH245SCdR89JPoVIpVCaNlcpgnJiJs58m5Qa86nXvJlfeDvXPIqLDCCuGzCgmdT3C3QYqk4BK + hKBXIT4HvSdANzHOzZD9TYJwmMP3/i6nnn6Ake1vwXNGiRt1olYT3emg2y10t4sIAkzYgzhE6BhM + nABgE7OKxoCS8If316Mvn+vdAJzmMiqLXywJVLuL1t0/ccDfGta7xviVQkjVZ/su0nORvo9MJzvf + ymTA1Zw7+deMjpR4/Zt/DdtMo6r/DZw2ovw6GPlnUHgbwr8G7FFQA8klB8DaBvaN4L8FnEMQnUS0 + /xylBKM3/3tcp8XJJz6BmynhpQYxxmBMX9Ubc8nPS9R/X/hiExWjNoaMI+WXz/YUSaNJb7MB8I9+ + 93W5nxxIb0FRcZ/1GykRUmGUjbQdhOsg/YTsqXQaO5NB+hZnTnyU3bu28crX/Cws/S0yeAAGfwQx + 9puQPgCW0xeOvOhRrEX0DAknIASRRdg3QHgcwqeg8ykK+/6c8mCOIw/+FenCNmwng9E6qRvQGkxy + GW3AaCQmeb9NjhEIIdhdtMzHj7R2BZqPkoyyMZsGgG05+Ye/cVt2bxhvQeBnPcpnIaSFtO1E+K6H + 8D1UKoWVTmHnCpw7/TcMln1ufe3PIRb/BinOYnb9DiJ/J6j4op1f4xOIb0/wiOeAz7kJET2N0C1M + +6/J7voT/JTFqSc+SnHoFQgERsf9S4MxmDhOdn58CTAuEdZzmfx3Ve/9SxuIYkMvMjQDTaOr0cZg + q4vl67FBlH3p3T8Z/C0wzwssK38xXoC4ZcR9cy/69i6e44sxK0EKRMIKpUi+giVAiYQQGQxKCETi + MKP6c3sEa79rMAKk0STBN4lUCmWB7UhcBZYWOEph2zbCc1Fpn+ryEwjT4lWv/V+QK19EyhnY8x8Q + buGi8M3ajpcXBWzWwrgWiDVhiYvqW/hgXYcJjiJFD7P4U+x79VdYvPAIMxc+zfD2n0YHhl4PugJ6 + kSCMJGHHJugFEEdEkUZrQ6R1wjcMaAxSJJnGRMgCKWSi7YRMnq1QKCnWR9QYY9A6Rgr4+4fP8qN7 + LW4ccbCkINaGm4cdaUneG2mO9c2A2QwA/OLtow7aPHvjBJHmyTnBjTuL6xMzjDGEYbiOZoRAyrUk + CkiRZNm0TL6wFIJICKSUaCkw668VoVSEjkXo2tieTdfySNk+ws3g+Bnmjz3Ea17zQ6hoCRkchj3/ + GtwsqDDxGnQfaWu73vSFby6J5q2DI764B7XBOLcjojMYYyNMDb3ye9zy9g/zuT+5FkvU8Muj2CqN + JdMI0ULTJoi6RFGXWEfEIkITExGjI00UxYRRTKxjtNGYWBPFMRhNbAxhFBPFMXEco9C4UlCpVLj2 + 2muZm5sjjGPCKOR8zXBgwCbjJJqg5EtcyS9Emj/sh4fjzQDAb9wx7hA/B1uRgcnFKq1uwGuv3UWs + DdVqlXQ6TaPZZHJmlkgn9Ej2XRdjIO7vNnMJXKWQSClQEhxl4Tg2juuQyvlkC2mypRz5Ug4nZbCU + y9Lk3QwOlBkd249Y+hBm+EcR6b0go+TTzJqNV0k4V4vncIA+J0Cv2+x1LSBAiCG0KCLpYIxAtD6B + k/019t/2Hk4/9nfkh99JbXmR1eVV6tUGjZUGzXqL9mqHsNfDhBE6jjFGo3VC2tYUkFoncsm1ZhHy + +QyjY+N0ux3arRaVSoXFxUVqrS73HzlNFBuGcz6xvjiiRglh3rIv5X7iWPtGYAbobDQA7DvH7R2W + xMTxs3mNFII3HszyjdMt7n7sGLfs285ApcJKrUa302H76Ai24/SBsQzJOJV1VWu4GFZNFESCaqkU + yrZxfAffd3E8B8e1sRwLx1UIZajXznDrrXdgeheQTgjF14Ls9Xe8uqjqhUiEL2Ty70ZetPkiyY4b + o/saIkruo39vQg2g46WLGcDqb3L9j36cEw//TyxRJZf1kXGMbcCXkqzj0vV7BJ0QHUVorRFrmhFQ + /XI0y7LwPA/btnEch1QqxeTkJN1ul+WlRXzfZ+fOnUgMD5+a4OTMMsWcwx2DDgMpiXtJG5sB8XPX + +XziWPtWkkriDQeA/6N7/GIYfzupdRTsKtlYB7M8PdXmm4fPsHfbIAfGh0in01SrVRr1OkopXnHT + TXS7XXq93sWbN+aiqbiUMEmJsiwc38ZLu3hZj1Q+RTqfIV3M4qQ0UdhhdHwfsv5hyI2CWwQZgLEv + Eb7sC1xhUAgkRva5wPocin7gxujklQgxQpH0rLsg/MR8SQHRLJiAwZ13EdRPMTTyBjKpFu1Uk3a2 + TbfZodvo0W2HxGECAmNMQhSFSHwPIbAsC8dx8DyPXq/HyZMn6fUSL65cLlMpl5larPLA8Qv0wohd + wz67ijbjOcVwWuKoZxPKii/NYEq8ZqFtPkgy4PJ7Zgkvq5jAFty1u2Cp+DswVykEOVewq6C4Y1ea + V+7PsrK0yD1PHAMhGRwcJJvNYozh+PHjNBoNcrnc+o2LSx6K6hM8y7ZxbAfXdfBSHl7aI5X2SWfS + pLIpMtksjdUTDA+P4cgmwmpD4dUgurAmqHUXT/YJmEIImQSVjARhJ8ElkVwahV7zOIyNMFYCGh0j + pI/Ax4gk92G6DzC05420m7NYjsKyJMpWKEsilURaMkkgrtUOCEHU1wbGGGSf+yilmJub49ixY/R6 + PZRS7Nmzh5Tv8+jpSR4+ehojDa/Yleb6IYeDZYttOUXKfvZIm7Uew7Gsuomkt/B5vbzLAoCU/HzW + /e6ds1II0rZgPKe4dsDmpj1ZxvKSh54+yonpRSqlIoODgwghWFpa4ty5c7iui2VZ6xpASEmv26Ve + rxP0ehiSB2VJiVIKZank4UqJcixWlo4xNr4HE8+AC3hDlwRh+gAwqi/8NcFbCGEjhIPAQmAjcBBm + 7d8Umr6mMBboBka4GHyE9EF4gA/tuxnc9cP0OlWElFi2wnEdHDeZOtLrdamt1qjVV9eFXl1ZIYqi + pO6gT5RPnz7N3NwcxhiKxSLj4+OsNls8cOQ0Ry7MUi773LbDZ3/JYl/JopKSOOo7y8EA2/JWFhgC + 7A01ARJ+ImOL5w1KOAoqvsS3bHKe5MxiwNT8PA80GtywZzujoyNUqyu0Wi3Onj3L0FBiJrrdLug+ + IyYhS61Wi06vTTu0yZJBuQI3dhNXU0KnuUixfCdGnkHYgOpHXdb5hbhoBqREYGFY0wQKQ9/1AhAK + QZx4DCQcQAuDCJcRIosxnXW6aoSG3tPkhg4RBYY4Dmh3WqxUV2jWmoTtCMexKVdKzC/UODc5jYkj + hgcqOLaNUopms8ni4mISkFGKSqWC77kcnZzjyPlZlKu4aWea0azFeFYlLP8FhN6vG7D5zMnuncBj + QHujAJDeV7ZTSgqjX0DyR0lB1klYfNqWlLI2F2ZafOPJY9x8cDcDpTKpVIpqtcr8/Dye57Ft2zZ6 + vR6ZdJpOt8vZC5Ok8zl2bBulNJDD9izCKKTRqoML0k1cNsvzQHYSDaBngT19965fwEG/VtCoflWP + wvTVvTD9Sh/67N9IkHFC9CCJ6es2SA+hExsuhAYRoXUDS2UA6AXtpJi0XCbjZ1mar3L85AQTE7OU + HJed46Pr6lopxcz0NEEQIITA8zwGBgZodXt88YmTBN02lZLH7rLNWFYxklFkHIH1AsfYXVOxAW4E + vEvcnJcMgPGbhmwup7tXCIFnwVBakrIFWSdDZjlg8txZTrkZXntoL67rsri4SLfb5cyZMwyPjJDy + fRCCQwf3o4HZ+TmOzc4wNFzi4N5tlItlUoUMqWwmidUoCbYBS0DwJLh39VW/uCh8ZD/oogALYxRC + 2BcBAhhhEFwSCMJAdB6E2w8KGRARxsRJ0Agv+R0J2WweohZnpqeYmFqgubxKyXW4/dAB4kAThxFK + SlZrNVZqtYQMAoODg7iOzem5KjMz0zRCODCWZiyjGM8pyimFq7isDqW9JYtkF5B6TmDjJQFg+NCA + zYtZSgpyLrjKImMLLviKqbk2X3/iGfbu3MHo8DD1RoOVlRVmZ2bI5XIMDg0RRhG2UuzfvQtpQyPq + cW5mjsVem7GxAcbthNDFaHBsUA7QAr0IYugiw+nvfGMkyETtJyHcvvspZN/dS+IByaxPCxPNJN5C + olqSnY+LMVFiKoTfz/3D9OIyC7NL9Lpttpdz2MUiYSei0+yiwyRqNj8/T6fTQWtNyvcpFgrECB4+ + fp7J5TqFgsstIw7juWTX59wXvuufSwXKvsgtd4z/fHmoywHAwIGy9aKz21IIPMswnEnYa97Lcm6h + y4nTZ6kODrJ/fJBR32dxaYl6o0Gn22VsfBzLUsSxxvFdxsp5/IxHV/cIwpDF2irKydFsNigOFTCx + gxAeJngS4b31YmSvX8UrEOtt35rEFGAkJk4EZNbKuSQQzoPuYozdDxGHGGMhcDAEiRaQAwTdBYQE + yxhGS3lEtkC73qG92l5n+r1ewPzMDLof+yhVKqRTKaardR47fhakYN94irFswu4rKYlnvfihlVob + saNgpZY7ofd8RP9yADBSTknTi158AkgIga2g6CdfMOv4FLI2c7OLfPWpKnfdcIDhoSHq9Tqtdpup + yUkK5TIjo0NJ+VMcE2tNPpcnU8qiPIv53m5mJyfZfu1eaB/BGAcRz0B0AuxDieDXKnj7O19rA1Kg + Y03f+STWOomkmJCoO4mlBAKFwUYSo42VaBIsDBZohfRvpjb3NI7rUi4UaK+2affaSTZQG5SymJud + pN3oghA4ts3w0BA6jvn60fPo9iqpjLNu60ezirz70odUaQPDKekmrsr3dgVfsBuYtRndqE5fKQQp + WzCaVRys2OzbmWNbQfHE4aMcmZynXCwyPDwMQrBaqzE5OZUIzQhMnAhKRwbHdti289VcOHGYSOzD + YGOEhcHDhMcxwWEEQeIFNL4B3aN9M2Anc95ZC8EaTNQg7s0QdqdB2Jj6BeTs/4lYPYXWdn+vJN4D + xkZriUy/kfnTX8JND6AjjY4NJtbYlk2702FqcpIwiIjjmEqlwvjoKNPVBvc/dZTl2iqpvMv1Iy4H + Kza7ixZFb2MmlGkDlQQAzoZpgKwrhzeyrmXNXSz7Et8SZJ0U55YV84vzfKPZ4oY929mxfTvL1RU6 + 7Q7Tk1OMbBvBTVXQsQat0UGMlfJQSjF9eorxHa9HREf6kT6Fiecx3SZCDiKiL0InjWx+Cq0Oou09 + GLENLTNobTBGQBxDbw6r9yBW/FmIHYw3moSqjcLEAiEUWltJvYCzi9lTX6JQPEgcxX39IpibmWVl + pUYURti2w9D2YWKtefz0BBemZxGuxXXbU4xmFeNZRd6T2HLjKqv6AHD6AFAbAoCULYY2o7JJSUHG + AUcp0o5HMW0zudDl4WeOccPBfYwNVohjTbW+xNLiIpGJ2O5vJ440URghQkGxcgPHHvk64/v/KSY6 + izB9lo+FkW7CW6whhDiPjm+D1qeQpoU0BjTouNBPB9QQNmgsTFCCwRG0lUXokKTmQyCFwmiDyryG + xtIZVmYPM3Tj61FGsLJaZWZyhnarg4kMuWwe302zWm9x32OH0XHI8GCK0YxkPGete0cbNaV8HQAY + Cp6SGwoAJbDZpCWEwLVgMCVJWYK045OqOcxMnOfM3DJvuOUQOwu7WFpZoNVsMXlhgjExjuM5hN2Q + bPFGps99kie/9iA3vOZnUcHn+/Zb9P18A/btEEyivYNo73XoKIJgGdk7hRDToEO0GCBSB8Hbi5f+ + 7xj2g07cw6TWQ6JRYO9B5d7E1/7kGvIDd9BpNmmuNKhXV4mCEBNrBitDEBu+9tQZmotzaAU7BnxG + 05LxnKLoJXH8zSioMUasUX9rowAg2ILOJyUFWRd2KousI5j0FXPVDvc9/CQHD+xj/84x2kGbRqvO + 3PQMURwyMDpEEHbYse/tnH7qw1TGDzG+562Yzj8k9YNG9IXoIygjommwhjHCw9jbiazdaC3RRiDQ + aB3hsAJxRGzfkVT0iKTc32iJ5RRwh36Vxz/3z7GtFLa7g1a9TrvZodsJ8ByfQrrE1FyNpw6fYrHa + IJOxuTZjGM9bDKckaedFu3cvcEOtzzqUl8jOvORcwFYsuR48UuwvKfYN2QzmFadPneDRI2exlMv4 + 2DZMBCtLK8xOTdNtB+jI5dAtv8Ijn/lDZi70wP8JtE58f2PWagF2QLTcL9k3iSunQzA90B101MUS + Ghkewcj9GFJJjl4rokiDLOAO/wqH7/s3TD3zScZ3vo1us0OvHRIFMcV8Ed9Nc2pigUeePEK11WW8 + JDk0aHGw4rA9q8i6myv8tf0avUB7/YIBEJkXXmm6ESbBVoKCJ9lVcjgwZLNj2CdcXeQrD3+LZr3D + 0OAIvpui3ejQbXZp1OrYqsC2a3+eh//ut5k+X8N4bySOzbom1PaNoCMkHQRxvyxN9ws4Y5QEo3tI + oYnFQUwMBpswNGiypMd/icc+8+uceui/cujmX2N5uUbQCRBGks8UMJHgnkePcPzUWdyUw4Ehl4ND + KfYUFANphWexJTOHBBDE31Zn85IAYBo9Pb3VMxGUAN+CkbRgb0kxVrEpe4ZHn/gWjx05S9bLUC4M + 0G12WK2u0m50yPojjO//aR797G/T7eXQokIcgzYKZI5eVCZoTyOVj5Q2UimkVEhpY1kOSik6HU0s + RpKwcazo9gL88p1MHP5bpo9+lh37foZuq8vM3AqenSLlZTgxsci9Dz5Jq9Uml3XYmZPsKwi2ZQV5 + V2LJrWsfFwIaQRz1Q8AbAgAW2ua8FJitHp0m+oWiBQd25iW7i5Ji1qK5MMuD3zpJHMQ06x267YD5 + 2TmUUVj2EOXhV3H0oc8grDHiOE5iCAgiMUirWQdhI5WPstNYdhrLSiNkmubqJN0wjZAlEBbtboCO + YtzsHp64+/9hePuPYwmP1VqT2mqb6nKNsBNx+NQFYmDIk+zMCHbnBYMpiadAcLEaaGvMKNQ6JuTi + wZZmI+oBJhvB1h/TtsZgLGFIKc1IRrArJ8jKHlnXo7q0QrveJu7G1KsNOs0ulnCQ1jCN+ira+Ggt + CKMYjMRyR2k0ekmsQLoI4SKkC9IFIVlensNOXQdI2p2QOOrnErCTv9MuxJKJmUVEp0dtsUqn1WXf + 8ABDnmFPSTKWFuRtsGRSJi62eNNIAYstHSQ57e9dGHo5AFg8XY24MsusP0hHGkouSGUxki9w5sRZ + gk6Ia6fotQIunJvAlh6OU2Rm4jjYO/vhXkXRUnzjAAAZNklEQVQUG2yvTC/0WJg9h5AWQiZt30Ip + mo0VeoGP7Q7R6gVoLVDSxksNEUVtWoGF7+WZnV+hUa0zUh4g6ERcODfBtmKZtNRU3ASoUuikN1CY + i+VtWwiAmVbcJWkU1RuVC1g+uxJx3YC95eeaJIOZ9HpBpgS0zNJptanXGni+j+ekUY5Ls95kfnaR + ke1j1Bo96rU6+dQedDCHjtuEscbxixw/fhjlZohig9YaJRRTE2cIux16vZUkyytsbCeDldvJ/OQR + pD1EtxUyMT1P1nbJeFminma1sUIpW6EbKkzUQ1gJuRRi63c/gCWEObsSdUh6AzZMAyycrcVXbGSe + ECJ5mCZpssim8ywsLvVj7x46NKScLJZ0mZmeY7XWYdv4Ldz/lc8ivWtotxss1RaYnDtFN26ysrrC + 4sIxer3ztLtTVBsTHD52jFq3zakLzzC/NEO9vkB1+Qzp8jV8876PMzJ0PcfPTeNpQT5bptcJsZVD + s96g02qirTytbq9fNHJR+FttNtuhEZ2IFZKpIRsGgLlnFkK4gocein4Dx9G5Dtl0ivOTkwjAdz2C + bkgcaAZKw3iWz8TENNuGD3DqxAkefvBhVOHNDA7G7Nlls39fFsf1CDWMjA8wsm0AZTsoN89tr3sV + Ow7uR/o255cFY9e9lyceuocTR56m03Www5hSfoA4hHazQzaTxcQxJ06eYudAibO1pLmDKyR8gGPL + ISQj5LpsYFVwNNOIpoPYXNGxmcbAfDfD6mqddjM58UNJSRgEBL0esQbXzuBKF89O8+pbX88XPn83 + Tz72BF7pF8A6hBQBr7h1B4NDuX5Tr2BgMM+rXnN9/zNihne+grve8F5OHj/CJz7yF9xy7R2klIMU + DjqGsBcQBSEZP4WSknq9TrPZRLhlepG+YsIHOLIYARwjqQf8nhtWXZ5tYffb9vu3+daVw8ByW+Pm + xjl+bgLikKGBAdKZNKLfP2DZNiurNSqDIyjX45vPHOHC3BxTk1WqyzXGd72KTPkuUukcqVQmqeoR + aSy7QDa/Dc/fT3ngh4EBvnrPF/jExz7Kwb17GSuW8YRFbXmJXquJNBB2u0RBovLrtRUsxyGVziHD + FbLOlRs0/aljHc6txh8kmRXQ2CgSaALNR+o9/etF78pFkBdaglTJYrnWYDBtk06l0Fqj46T9am52 + Fj+Xw3UcJmZnOXziLGN7HF55R5aHH5ji6w98gOuuG+Ktb/shKoO3I2wPW1j9bF/M8vIUn/zkf+XR + h47juBYTUxPsGi4yoxSDjkXKTzG3uEjW99FxRNDrkctmUVJyYWKKO2+/lSeOxVR2K/wr8JikgNlm + NNsX/PNODrussnADpx6d7nV3FywvvgJMINaGqaZPxWoQhyHpVD4Z8BzHxLFGa02zUWdk2zam5xcI + XY9MyqO63MW2Ne/+hX1IOcjDDy3we//uc0gBrmcn4SatCYIIY2DHjhSveOUQcdQkDEqcPHeenZUy + xyem2JZN4dgOtWqVtOetN7XYtk2j3WBhaZmhoe00gmlcS22pFjDGYClhztXioyQHUT5vh/DltoY1 + P32iu/qLN2bcdrj1XOB8NWT74CjfPHyCjCNIZ5KSbKM1SkrmZmcZGh9nuVZntt3l4N6d/Oq738W/ + /vd/zFOPjTEyJqmUx/jpd+/i3T/zKhYWmtRqrf7Di1AqBN2hvlqjulzj7NkOM1ML3HXrLZRzWSbO + T3B+Zp7hfJ6VhXlsmRSQaq0ZGRmh1WpxYXqOG689wNKKoODxrNatrSDJhxdC0Y05StIW9ryjYi5X + SYWzLf3NyXq85cI3xrDYdTEIWp0eac9NyscBg6HTbqP7pdYnJmfIew4D5Qpff/Qxhkbz3PbqIuXS + IUZHfoxi7nUoy2F0rMyBA6Ps2lVmZCRDOu1gDPR6EROTy5w4OkHQC7jthutxHZdtpQJzK3XmqiuU + SmVWVlbW+/2y2Sye67Jar6PjmJMrit4WHz4lBeYjh9sAJ/oAiDcaABp43/0TvS0/TkUb0FaGlUaD + lIJ8Notl22AMtmWzslJlYHCAx4+fJm0Jdo4khaRPnzjF7r1Zut0AJb11MK1rxv6hFqI/IubcuXlm + 52qMDOe59fb9eL7H1x95FCEE2UyG3QM5jl+YXQ9QdTodDBCFIel0Gqk103OL7BrdznJHb+nhU93I + iMdnggVgos8B9EYDAOCpJ+d6S7bcWiVwaimkUihw4vwUWUeRyWbXeAkr1Sq5XI7TF6ZxhWbv+GgC + Gh2ze3yUk8dqBIFgYelhFhbvodF6uD8TwtBotJmZWeTCxBwLi1WGhgtsGysn/YhKMjBUYmZhEdnX + NJVSmT2DBR45cZZKpZIcN9f3TweGhnBtyfmpaUq+zYWGtWWnjhhjWGprejGfBpZ4Aa3hLxYAwTcm + w8+aLVb/Ew2PTrdHux3g2hae7/eLOgxhHNPo9qjWalyzeyeWUug4mbzxltfeSb3W5egzKyiVxbJH + iSOfZqNFs9kk5dmMjQ2wc8cIgwNFPNdGSNDaMDdfZ/LcDAd3bKfX7UKsMVozOjTIWD7DkbOTpDMZ + ms0mRkrSvk82k0HrmOrqahIZDPT6QIjNXLYS5stnO4GBJ/oACDYLABr42F8+3d6yqGAr0AyXy0zM + zpN2IJdP2D9CJG3m2SzT0zPceGAPUqqkxh+Io5CPfvZz7NpX5JbbygwP3UGpcIhC/kbSmQzZbBZl + W2ht0Lrfoi4kq6sdpqar7NgxyO7927n/yadoNlaJorD/u5odY8O4UrPa7q5PCe31egwND5O2JVOL + y1RyeSbr8VoF+qZzwA8/06mSDIh6wdPDXwwADPDA50+1F+ItmhA9Uzf4nsdirU7akeQLSRVvGAR4 + nse58+c5uGcXSiq0jlFS0qjXWFpYYGG5RqHoEkURy8uP993JtSlqieiUkv0e/SonTkzh+S5794wQ + RSEDgwWW6w2Wl6vMzMzQareSPgIEe3buoF6rgVK0WklzaCqVwnEcJmYWSNuCM40UvU32mY0xfPFM + F+DTfQC02MwxcYBphSy8eY/3joyzudEObQyPztq4tsPk9BylbIrKwACyP2ql0WgwNDSE63kIqbBs + m+XVVbpBiFEWpUqFB588TbGUY2S0DGqKSC/16/xiup2AeqNJdXmVVMZjfLxMHAXMzS5x/Mh5nnj0 + GK+69hrynsfC/AL1apWw10MiiOOIYjbL7Ow8xUJufeRLFIa02y1ioRgfHkRFK6RtsSkxAWMMjhLm + g483GzNN/RfAUS5jaPRLuaPhmwat8//5TUV3M4nO9GpElTGePn0euk22jY1SLpcTUtaf9yeVhbJt + hG1TXV1FuS5eJsvQ2DaypRJPTk7zyOHDtFodCoUs11y/l+HRZCaB47oEvYCVlRoL88vMzcwzP7tI + HMX4vsfOkWFu3XcAR0cszkwzNXEB0+vi2zaFbBZJMhcwDgMsZSUVTJbimWeO0JMONx7cy/zCGe4Y + d/A2J4Rual0tfuqTy58nOVzy4b4JeGHh/ZcSln9qIfrI8eXol3cX1KYlPpY6Fm7OotNuUfbtZKzM + +iENSZmXsixa7TarjSbKcfBsl0wmR68X8PThU1gpn7e85tWEQnJmYponnzgLjx0jikLW2kalkjhO + itjY7Nt7kD3jIwgdE/c6zExcoNEJ2VHMMjo8wqmTJ2k1mrQaDcrFIp7tIJWVTEAzEMeabDbD4twy + Jo6Z7mZph90XNNzhxazf/uoqwBeA6b76f8HrpcSpDPBgZMyv/9AOz9abMDE81oZTq8kotNryMqVC + gWKxmBCXvvAdx2Fxucpqo45BkMnmyOULrLa7PHJ2BiE0xZQFaCwpqVQqnF/2ePs738vb3vGLvPb1 + b+cVt7+R7btegXLLrDY1u0bzEHYJOy2iTotu0KHbqDM9u4hnOwwUcjQaLRr1Br1OBykFju2s8woh + BEpKWo061XaPW/fvZGZ5gaHMRoeGDa3AiA883vq8TqaCHQealxU8eokAqN99uveJyUYsNiMwNL0a + UcoXOXZuEldJcmu+vzFJ9k8ppmdmaLWaCAT5XI6073Nmdon7njmDQ4+SE6HCNqLXIWw3CZqrjJcs + vvKVe1leXkKpRChB0GV+fh5bGaJOk147Eb7udbDiAM82WPQ4cfo0c7UWu8bHyOby1FstFhcWWFmp + 9iORydCn4ZERcimPZqNB1Ouy2EsOotxQ108K8+/ur8eR4d5+8KfOFh8YEQK/9fvfaLz3v7ypkBwW + uUEIN8ZwZtWmUtQ0mh2KRR8/lUqGuipFFMfMzc1g+rttYHAAjeCrR86x0gm4ZnueSgpyMsQVAUQC + jcaYmF0Fm3Y35uMf/zipVArP81ipVrEtycEhUDpERgEiDpE6QJgAR4bYaYkyDtXZC0wvpLlpfIB8 + 2ufMuXNEQUC302F4YADlOtiWRalUZrU5SbPbJRRZmsEKvmU2rBfwqflQPDwT3gecIxkMedmnh21E + qiJeauvl125z31T2Ny7zEWlDTReZrzUJ23UGSkWy2Syu41BbXaVarYIxZNIZyuUy7V7El545RxSH + XDOaYjCjGEgpso7ElkkDpiUuzi0eykkqGYktImzTYbwg2JbXOCbEI8IxYf8KcE2IS0RKanypkZYg + bDY4v1BjbKDCcKXE/PIKvXaboNtBSkm+fwJZvVZjqVbn1oO7eejMNPtK9oZ0BmVsYf7vr67Wljv6 + /cAzffsfXQkAaODUvee6v/yua1IpuUEHRk/XY6Q3yNFzE2QsGBsbw/M8FhcXk8oboFAsUsjnOLdY + 494j5ylnFNeOeoxlFMMZRcYV2BIkBiUMst9jYIkYC42nYvKOpujEpGSER4hPiKMDbBNg62D9tUOE + S0RaxmQsg+spoiDg3PQCjpfi0K5xmp0eSysrdNttut0uQ0NDLC0t0mq1KORz9ITHkNdNegVewjMy + xvCxI23xpTO9jwMPvhjbv5EAAIgizb05V/zq9YPOSx6Pb4zhoWmLjJ/mzPlphooZypUKs7Oz64MU + h4aGsC2Lx87P8ciZGfYP++yr2OzKW4xmFVlbYKmku8haE34yBhJFjDIxNvG6oF2TCN7SAXbcw4oD + bN3D1iEOEY6JcIhxhSZtGXK2Ie1baGOYnVmgFkr2bxvCdV1ml1fotVq0W80kTdxsYBDk8wVEWHvJ + x9JHGn7na6tHgpiP9Xf/HC/yCNmNAoAGGo/Ohte9asw5WPLlS0J4ta3BG+LY+SmU7jE+OsLy8jJx + HK+PVBNS8qnHTjFXa/KKHWl2FS12Fy0G08kMIkuAJQRKaCzo73ydgEDEWDrG0iG2CbFNlAheB9hx + f+ebxAy4JIK3iXDQWEJjS3CkIWMZUo5EOhYL88scnVvluh2jDFZKTM0v0263MDpGKcVqfZVd46N8 + 6dgs1w3aOC9yEogSmF/9/IpYaJk/BZ4GTvEizwzcSADQtz//8K2F4F0/f10691IOk5hqCJRX5OT5 + SXaNDBBFEVEUUSwWKRWLTCzX+eyTp8m4cPM2n50Fxa6CRdlXSc89a3MizcX+aBMjTIxcvyKkDlE6 + wtIhSodYcYBlQqQOsXSYvDYRysR94CTnGEgMEo2tTH+eAXieRa8bcGxyAS+d5eD2EertHtVqjW3j + YzQbDTzXJV0YJE19fcT75Qr/y2e74rOnuh8EHgEOc5mHRG0mAAwQ1Hvm6JmV8D137bhYLnW5od/D + Sw62ZbNarzNYKtLr9RgcHMTzPJ6ZWODrx6fZNeBwaNhjV8Fie94i7ybj5Z91O+tDDTXJiCiDNDHo + GKGjBAg6Oe9P6AipI4QOUXGUAMTESDRSa2S/v0/Sf91PAUtBf+CVIOVbBLHh/IUFtONyYHwYIyUT + kxOMj42xtLzMvvFhppeXGc7Iy/IGjDFcWI3Fb967+i3gU8C3Xizx2ywA0LdD8+dX48XrB+0fG8ta + xlzm+drzLU1sD3Ly3AS7xoZpt1qMjIwgheCzT53h3GKNV+5MsafssLtoMZa1+kOT1/oIL2Egxqyf + TJIcXtLv1SMZCCm1Xhe81FFfQ+j+T4PQMcLoi7/fB5Tpv6/oz6OWJMe3ZBxB1lNYns3M9CLnVrtc + t2OYUqnE9MwMoyOjCAHfmm2xr8jlRAaNrYT4p1+oLjRD/rS/80/zPGNgrwQA1mIDZ+4523vFK0ed + 3ZXL5AOH5wXZbB7fdYijkEq5zEqry989fgopYm4eT7GzkEzVGrhkiuZ6VQ/PBcLaoU0XBY/WCK0T + zWCec60J3ST9fcIYhNF9JXtR8KIPhvXPFGBLQdqRZB2JlbKprbY5NbPMSKXMYKlIq93G8zzS6Szd + 7gqV1PNHBpMAmxD/5PMrTNT1+/rCP9KP95uXIwAMSTXq1x6c6v7om/f6A64lzAtJPMXaMB/kyefy + dDttCvkcR6aX+OIzFxjJ29ww5rO7aLGzcHGk2nPB9e1AuHge0bpW6P9bImhzyaUv/h96XYOsgefS + eSs8S/gXAaikSKaeuZJMyqYdxDxzdg7L89k+WMIAlYzHE1NV9hbF9xwLt5bp+y+PNXlgKvhj4Mk+ + 659/sax/KwCw5hV0uxH3fPlM9x+9/YDvWfL5QVBtawJnEEyMNnDP4QscnV7mhnGfA4OJyh/PJoOT + n89+XiqU7waIi8Jeu3jWa/msv/nO7/kd4+tC4CpIO4JcysJ2FOcmlphuhowU0gRhSBebAaeLb3+X + se/GoKQwH36mLT56uP0x4P6+3Z/kMg6G3Mx08AtZaeDmgsdX/+adFet7BYmMMTw4CaXiCM1ewKcf + P4Ul4YZxn7GsYnveouxv3mStTcnTGkMQw3JHc74WcmSizWooeMON+2nF0GtM8prt7re5hGuJzr96 + ui3+8unWp4G/7+/+Uxth97cSAALIAq/cV1J/94E3lzLwnd3DWBv++6N1pJXmzEKNwazFgSGX8dzF + YYqW/P4Q/HcKa692NVONmGemO8ytxixHFq0w4t/+SIG8K58FGtcS5mOHO+IDjzc/DXweeIqk1LvJ + BpfhbUXbQgBUqx3zjfvOd9/xpj2+a6tvNwfGwFwzYmqlzY6Kw74Bl90Fi+05i6z7/Sv8iyZBkHEk + +ZRFaED3AgoZm2sqNqm+GTDJYZDm40fb4s8fb30R+Ew/2HOSyzgN9OWkAdafAckZNjfYki/8zU+V + UzlXPit7GGvDSlczWU9OpBpIJydkeN9HKv+FmIRuDEvtmPmWRgI78oqiL5HJKBHxR99scPeZ7seB + ey8R/iqbVIC7lU9WAjng2pTFh37/9fkDNw056EuCRWFs6MXJyaK24vt6138vEMQGulFyRJ5vC1T/ + VJt/8aUahxejPwce7bt7p3gROf6Xmwl4rnu4Gmru+9LZ3vasK/ddP2ATrx18KAWOSlwjKX7whL/m + nUiRfE8nAbmZbWnxL79UWzlZjf9j394/zcXW7k0tKVZX4BkEfVQ//MhMsHhqJbrzddtdJYXY0IKS + 7xcwPDMfin/yhZUTtZ750766/xZJgUebLei7uJJP2wOGgZt9xX/+D28sjF9XsY3+AQeB6TNeJYX5 + P+6ticdmw7/sq/wzJHn92b6m3JKlruCziEgqWJciw313n+560434hjvGXdQPKAD6Lh73nuvxW1+p + XTizEn+QpJVrLbw7v5FBnpe7BriUHGaAceB64A/+zV253a8edxA/IGbBGIMthZluxuIDjzU735gM + vkRSxTvZ9++n+j6+3up7ezk9WReoALuAH0pZ/O6fvaVo7chZ3/dmQQrM+x5siLvPdJ+A/6+9c3lt + IorC+G+SmSSaPhJqYxOh2KiLFlsR3boRlwU3XWtX/jv6X7gRXYrdSgtduDPaSh9aUSlpWm3znplc + F+dMGcRFEW0zcT64XCYh5HLPd+7jzLn34ylQ1SF/GznI2TmrtvVbryaQ8HERKAPzV/LJxcW5bPbu + VJq2Fx2PtxMWHd/wZLXO68/tN/UuS8BH9fpNJI3rTLy+nwkQwNGYQQm4DCyUc8l7C9PnS3cmU4yk + E8br9Vvb5YSRk4RK1WVpq+09X2u/A14Cn5C07W0kiePwtOf6qBEgaFtKiVAEJhE51Efz19LFB7ND + zmjGInjLaE7xNu7Ay8P/13INb6td83j1qLPbMMvAKzX0N+TQxlckotflDC/bjBIBfiXCMFAALgFl + C26OpLk/W0hNPJzLcr3gmI5nrH99e5kRyUHshJwtf7He4tlai1rTX2m6LOvQXtP6i67sD/vN8FEi + QLitNqKHm0fk0Sd0irg15FjTswX7ajlv52YuOMyMOxSHksbvCSl80XLkJJkpQR5b0jo2Nj1j8WHf + pVJ1Wa95bBx4O2t73oYGb9aRqF1NDb6LJGs2dLtr+rlTo4ikjgpZIKe7hzElRh64rVvK0o2LdmZ6 + zE5P5exUcdhOZGxJ1kgfh5tFMdg3oiXp9gxd31DvGnYOPW9z3+u83/M6W9/9pi7eKsAWkop9BBzo + Sn4PSdNqcIJbumMC/L32B/p45zSeMIK8eRzV56x+l0KijwULxi2LcX0G8DG0jFys9MOI9x6o93pq + 0DYSnq3rXB6UI+R3wd38JmodOEhQnXicECnCJaPxhkBV0yakMa5e66vRXTV8S43fUgK09fOwJEuk + PWiQEU7tS+jUEdRBsUIEMGpQ/ze1iaKH/+8E+JM+MHGXxIgRI0aMGDFixBho/AQiCqY+GRZzJwAA + AABJRU5ErkJggg== + + + + + + + + Received Badge + Congratulation, you have received the badge ${object.badge_id.name} ! + % if object.sender_id + This badge was granted by ${object.sender_id.name}. + % endif +

+ + % if object.comment +

${object.comment}

+ % endif + ]]>
+
+
+
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 @@ + + + + + Run Goal Challenge Checker + 1 + days + -1 + + gamification.challenge + _cron_update + () + + + \ 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 @@ + + + + + + + + Reminder for Goal Update + + Reminder ${object.name} + + +

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.

+ +

If you have not changed your score yet, you can use the button "The current value is up to date" to indicate so.

+ ]]>
+
+ + + Simple Challenge Report Progress + + ${object.name} + +

The following message contains the current progress for the challenge ${object.name}

+ +% if object.visibility_mode == 'personal': + + + + + + + + % for line in ctx["challenge_lines"]: + = 100: + style="font-weight:bold;" + % endif + > + + + + + + % endfor +
GoalTargetCurrentCompleteness
${line['name']}${line['target']} + % if line['suffix']: + ${line['suffix']} + % endif + ${line['current']} + % if line['suffix']: + ${line['suffix']} + % endif + ${line['completeness']} %
+% else: + % for line in ctx["challenge_lines"]: + + + + + + + + + + + % for goal in line['goals']: + = 100: + style="font-weight:bold;" + % endif + > + + + + + + % endfor +
${line['name']}
#PersonCompletenessCurrent
${goal['rank']}${goal['name']}${goal['completeness']}%${goal['current']}/${line['target']} + % if line['suffix']: + ${line['suffix']} + % endif +
+ +

+ + % endfor +% endif + ]]>
+
+ +
+ + + + + + Set your Timezone + Configure your profile and specify your timezone + count + boolean + + [('id','=',user.id),('partner_id.tz', '!=', False)] + + user.id + + + + Set your Company Data + Write some information about your company (specify at least a name) + count + boolean + + [('user_ids', 'in', [user.id]), ('name', '=', 'Your Company')] + lower + + user.company_id.id + + + + + + Create User + ir.actions.act_window + res.users + form + current + + {'default_groups_ref': ['base.group_user']} + 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. + + + + Invite new Users + Create at least another user + boolean + count + + [('id', '!=', user.id)] + + + + + Mail Group Following + Follow mail groups to receive news + python + result = pool.get('mail.followers').search(cr, uid, [('res_model', '=', 'mail.group'), ('partner_id', '=', object.user_id.partner_id.id)], count=True, context=context) + + + + + + + Complete your Profile + once + personal + never + + inprogress + other + + + + Setup your Company + once + personal + never + + inprogress + other + + + + + + 1 + + + + + + 1 + + + + + 0 + + + + + 1 + + + + +
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 @@ +
+
+
+

Drive Engagement with Gamification

+

Leverage natural desire for competition

+

+ Reinforce good habits and improve win rates with real-time recognition and rewards inspired by game mechanics. Align teams around clear business objectives with challenges, personal objectives and team leader boards. +

+
+

Leaderboards

+
+ +
+

+ Promote leaders and competition amongst sales team with performance ratios. +

+
+
+

Personnal Objectives

+
+ +
+

+ Assign clear goals to users to align them with the company objectives. +

+
+
+

Visual Information

+
+ +
+

+ See in an glance the progress of each user. +

+
+
+
+ + +
+
+

Create custom Challenges

+
+

+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. +

+
+
+
+ +
+
+
+
+ +
+
+

Motivate with Badges

+
+
+ +
+
+
+

+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. +

+
+
+
+ +
+
+

Adapt to any module

+
+

+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. +

+
+
+
+ +
+
+
+
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 (). +# +# 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 . +# +############################################################################## + +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 () +# +# 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 +# +############################################################################## + +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 () +# +# 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 +# +############################################################################## + +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: compute only the results for this subset if gamification.goal ids, if False retrieve every goal of current running challenge + :restrict_top: 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': , + 'description': , + 'condition': , + 'computation_mode': , + 'monetary': <{True,False}>, + 'suffix': , + 'action': <{True,False}>, + 'display_mode': <{progress,boolean}>, + 'target': , + 'own_goal_id': , + 'goals': [ + { + 'id': , + 'rank': , + 'user_id': , + 'name': , + 'state': , + 'completeness': , + 'current': , + } + ] + }, + # if visibility_mode == 'personal' + { + 'id': , + 'name': , + 'description': , + 'condition': , + 'computation_mode': , + 'monetary': <{True,False}>, + 'suffix': , + 'action': <{True,False}>, + 'display_mode': <{progress,boolean}>, + 'target': , + 'state': , + 'completeness': , + 'current': , + } + """ + 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 += _("
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 += _("
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 += _("
Special rewards were sent to the top competing users. The ranking for this challenge is :") + message_body += "
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 += "
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 += "
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 () +# +# 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 +# +############################################################################## + +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 () +# +# 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 +# +############################################################################## + +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': , + 'name': , + 'visibility_mode': , + 'currency': , + '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 @@ + + + + + Gamification + + 17 + + + Manager + + + + + + User can only see his/her goals or goal from the same challenge in board visibility + + + + + + + [ + '|', + ('user_id','=',user.id), + '&', + ('challenge_id.user_ids','in',user.id), + ('challenge_id.visibility_mode','=','ranking')] + + + + Gamification Manager can see any goal + + + + + + + [(1, '=', 1)] + + + + 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 Binary files /dev/null and b/addons/gamification/static/description/icon.png 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 @@ + + +
+
+
+
+
+ +
+
+ e +

+
+ + +
+
+ +
+
+
+
+
+ + + + + + +
+
+ + Target: + + + Target: <= + + + +
+
+
+
+
+
+ + +
+
+
+ +
+
+ + Target: + + + Target: <= + + +
+
+
+
+
+ +
+
+
+ +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ + +
+

Invited Challenges

+
    +
  • + +
  • +
+
+
+ +
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. +# +# 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 . +# +############################################################################## + +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. +# +# 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 . +# +############################################################################## + +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 @@ + + + + + + + Badges + gamification.badge + kanban,tree,form + +

+ Click to create a badge. +

+

+ 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. +

+
+
+ + + Badge List + gamification.badge + + + + + + + + + + + + + Badge Form + gamification.badge + +
+
+
+ +
+
+ +
+
+ + + + +
+ Security rules to define who is allowed to manually grant badges. Not enforced for administrator. +
+ + + + + +
+ + + + + + + + + + + + + + +
+
+
+
+ + + + Badge Kanban View + gamification.badge + + + + + + + + + + + + + + +
+
+
+ +
+
+

+ + + + + / + + + /∞ + + + + +
Can not grant
+
+

+ granted,
+ this month +

+
+
+ +

+
+ + + + + +
+
+
+
+
+
+
+
+ + + + + + Badge User Kanban View + gamification.badge.user + + + + + + + + + +
+
+
+ +
+

+ +

+ +

+
+

Granted by the

+
+
+
+
+
+
+
+
+
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 @@ + + + + + + Challenges List + gamification.challenge + + + + + + + + + + + + gamification.goal + Related Goals + kanban,tree + {'search_default_group_by_definition': True, 'search_default_inprogress': True, 'search_default_challenge_id': active_id, 'default_challenge_id': active_id} + +

+ 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. +

+
+
+ + + Challenge Form + gamification.challenge + +
+
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

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.

+
+
+ + + + + + +
+

Depending on the Display mode, reports will be individual or shared.

+
+ + + +
+ + + + + +
+
+ +
+
+ + +
+
+
+
+ + + Challenge Kanban + gamification.challenge + + + + + + +
+
+ í + +
+
+ +

+ +
+ + + +
+
+
+
+
+
+
+
+ + + Challenges + gamification.challenge + kanban,tree,form + {'search_default_inprogress':True, 'default_inprogress':True} + +

+ Click to create a challenge. +

+

+ 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. +

+
+
+ + + + kanban + + + + + + form + + + + + + + Challenge line list + gamification.challenge.line + + + + + + + + + + + Challenge Search + gamification.challenge + + + + + + + + + + + + + + + + Challenge Wizard + gamification.challenge + +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + +
+ There is no reward upon completion of this challenge. +
+ + + + + + +
+ Even if the challenge is failed, best challengers will be rewarded +
+
+
+
+
+
+ +
+
+ + + Challenge Description + gamification.challenge + form + + new + + +
+
\ 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 @@ + + + + + + + Goals + gamification.goal + tree,form,kanban + {'search_default_group_by_user': True, 'search_default_group_by_definition': True} + +

+ Click to create a goal. +

+

+ A goal is defined by a user and a goal definition. + Goals can be created automatically by using challenges. +

+
+
+ + + Goal List + gamification.goal + + + + + + + + + + + + + + + + + Goal Form + gamification.goal + +
+
+
+ + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + + Goal Search + gamification.goal + + + + + + + + + + + + + + + + + + + + + + + + Goal Kanban View + gamification.goal + + + + + + + + + + + + + + + + + + + +
+
+

+
+ +
+ +
+ +
+ W + N + X +
+
+ + + + + +
+ +
+ Target: less than +
+
+ +
+

+ + From + + + To + +

+
+
+
+
+
+
+
+ + + + + + Goal Definitions + gamification.goal.definition + tree,form + +

+ Click to create a goal definition. +

+

+ 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. +

+
+
+ + + Goal Definitions List + gamification.goal.definition + + + + + + + + + + + Goal Definitions Form + gamification.goal.definition + +
+ + +
+
+
+ + + Goal Definition Search + gamification.goal.definition + + + + + + + + + + + + + + + + + + + + +
+
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 (). +# +# 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 . +# +############################################################################## + +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 () +# +# 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 +# +############################################################################## + +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 @@ + + + + + + Grant Badge User Form + gamification.badge.user.wizard + +
+ Who would you like to reward? + + + + + +
+
+
+
+
+ + + +
+
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 () +# +# 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 +# +############################################################################## + +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 @@ + + + + + + Update the current value of the Goal + gamification.goal.wizard + +
+ Set the current value you have reached for this goal + + + + +
+
+
+
+
+ +
+
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 (). +# +# 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 . +# +############################################################################## 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 (). +# +# 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 . +# +############################################################################## +{ + '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 @@ + + + + + + + Total Invoiced + + sum + True + + + + [('state','!=','cancel'),('user_id','=',user.id),('type','=','out_invoice')] + + + + New Leads + Based on the creation date + count + leads + + + + [('user_id','=',user.id), '|', ('type', '=', 'lead'), ('type', '=', 'opportunity')] + + + + Time to Qualify a Lead + The average number of days to open the case (lower than) + sum + lower + days + + + + [('user_id','=',user.id),('type', '=', 'lead')] + + + + Days to Close a Deal + The average number of days to close the case (lower than) + sum + lower + days + + + + [('user_id','=',user.id)] + + + + + Logged Calls + Log a certain number of calls to reach this goal + count + calls + + + [('user_id','=',user.id),('state','=','done')] + + + + New Opportunities + Based on the opening date + count + opportunities + + + [('user_id','=',user.id),('type','=','opportunity')] + + + + New Sales Orders + Based on the creation date + count + orders + + + [('user_id','=',user.id),('state','not in',('draft', 'sent', 'cancel'))] + + + + Paid Sales Orders + Based on the invoice date + count + orders + + + [('state','=','paid'),('user_id','=',user.id),('type','=','out_invoice')] + + + Total Paid Sales Orders + Based on the invoice date + count + True + + + + [('state','=','paid'),('user_id','=',user.id),('type','=','out_invoice')] + + + + + Customer Refunds + Refund the least customers (lower than) + count + lower + invoices + + + [('state','!=','cancel'),('user_id','=',user.id),('type','=','out_refund')] + + + Total Customer Refunds + The total refunded value is a negative value. Validated when higher (min refunded). + sum + higher + True + + + + [('state','!=','cancel'),('user_id','=',user.id),('type','=','out_refund')] + + + + + + + Monthly Sales Targets + monthly + ranking + + weekly + + + + Lead Acquisition + monthly + ranking + + weekly + + + + + + 20000 + + + + + + + 7 + + 1 + + + + 15 + + 2 + + + + 5 + + 3 + + + + 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 @@ + + + + + + + + inprogress + + + + + + + + + + 2000 + inprogress + + + + 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 @@