From 4ce02e58fa52c09a4b2a3b3cbc6d319daca9f279 Mon Sep 17 00:00:00 2001
From: karuna
Date: Thu, 13 Nov 2025 12:43:31 +0530
Subject: [PATCH] Rewards Module for PMS
---
.../project_kudos_plus/__init__.py | 1 +
.../project_kudos_plus/__manifest__.py | 22 ++++
.../project_kudos_plus/data/badge_data.xml | 25 ++++
.../project_kudos_plus/data/ir_cron.xml | 11 ++
.../project_kudos_plus/data/mail_template.xml | 13 ++
.../project_kudos_plus/models/__init__.py | 6 +
.../models/project_badge.py | 36 ++++++
.../models/project_kudos.py | 121 ++++++++++++++++++
.../models/project_kudos_leaderboard.py | 30 +++++
.../models/project_task_inherit.py | 24 ++++
.../security/ir.model.access.csv | 4 +
.../project_kudos_plus/views/badge_views.xml | 36 ++++++
.../project_kudos_plus/views/kudos_views.xml | 50 ++++++++
.../views/leaderboard_views.xml | 19 +++
.../project_kudos_plus/views/menu.xml | 13 ++
15 files changed, 411 insertions(+)
create mode 100644 addons_extensions/project_kudos_plus/__init__.py
create mode 100644 addons_extensions/project_kudos_plus/__manifest__.py
create mode 100644 addons_extensions/project_kudos_plus/data/badge_data.xml
create mode 100644 addons_extensions/project_kudos_plus/data/ir_cron.xml
create mode 100644 addons_extensions/project_kudos_plus/data/mail_template.xml
create mode 100644 addons_extensions/project_kudos_plus/models/__init__.py
create mode 100644 addons_extensions/project_kudos_plus/models/project_badge.py
create mode 100644 addons_extensions/project_kudos_plus/models/project_kudos.py
create mode 100644 addons_extensions/project_kudos_plus/models/project_kudos_leaderboard.py
create mode 100644 addons_extensions/project_kudos_plus/models/project_task_inherit.py
create mode 100644 addons_extensions/project_kudos_plus/security/ir.model.access.csv
create mode 100644 addons_extensions/project_kudos_plus/views/badge_views.xml
create mode 100644 addons_extensions/project_kudos_plus/views/kudos_views.xml
create mode 100644 addons_extensions/project_kudos_plus/views/leaderboard_views.xml
create mode 100644 addons_extensions/project_kudos_plus/views/menu.xml
diff --git a/addons_extensions/project_kudos_plus/__init__.py b/addons_extensions/project_kudos_plus/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/addons_extensions/project_kudos_plus/__manifest__.py b/addons_extensions/project_kudos_plus/__manifest__.py
new file mode 100644
index 000000000..6f1d42d02
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/__manifest__.py
@@ -0,0 +1,22 @@
+{
+ 'name': 'Project Kudos+',
+ 'version': '18.0.1.0',
+ 'summary': 'Employee reward and recognition system for project tasks',
+ 'description': """Employee Reward and recognition system based on project tasks""",
+ 'category': 'Human Resources',
+ 'author': 'Karuna',
+ 'depends': ['project', 'hr', 'mail'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'data/mail_template.xml',
+ 'data/badge_data.xml',
+ 'data/ir_cron.xml',
+ 'views/kudos_views.xml',
+ 'views/badge_views.xml',
+ 'views/leaderboard_views.xml',
+ 'views/menu.xml',
+ ],
+ 'installable': True,
+ 'application': True,
+ 'license': 'LGPL-3',
+}
diff --git a/addons_extensions/project_kudos_plus/data/badge_data.xml b/addons_extensions/project_kudos_plus/data/badge_data.xml
new file mode 100644
index 000000000..e1734a46a
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/data/badge_data.xml
@@ -0,0 +1,25 @@
+
+
+ Bronze Badge
+ bronze
+ 10
+
+
+
+ Silver Badge
+ silver
+ 25
+
+
+
+ Gold Badge
+ gold
+ 50
+
+
+
+ Platinum Badge
+ platinum
+ 100
+
+
diff --git a/addons_extensions/project_kudos_plus/data/ir_cron.xml b/addons_extensions/project_kudos_plus/data/ir_cron.xml
new file mode 100644
index 000000000..05bda2c55
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/data/ir_cron.xml
@@ -0,0 +1,11 @@
+
+
+ Assign Kudos Badges
+
+ code
+ model._cron_assign_badges()
+ 1
+ weeks
+ True
+
+
diff --git a/addons_extensions/project_kudos_plus/data/mail_template.xml b/addons_extensions/project_kudos_plus/data/mail_template.xml
new file mode 100644
index 000000000..876d7e1da
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/data/mail_template.xml
@@ -0,0 +1,13 @@
+
+
+ Kudos Appreciation Email
+
+ π Great job, {{ object.employee_id.name }}!
+ Hi {{ object.employee_id.name }},
+ Congratulations on completing {{ object.task_id.name }} before the deadline!
+ Your effort and commitment are greatly appreciated. π
+ β Project Kudos+ Team
+ ]]>
+
+
diff --git a/addons_extensions/project_kudos_plus/models/__init__.py b/addons_extensions/project_kudos_plus/models/__init__.py
new file mode 100644
index 000000000..9f1e265a5
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/models/__init__.py
@@ -0,0 +1,6 @@
+from . import project_kudos
+from . import project_badge
+from . import project_task_inherit
+from . import project_kudos_leaderboard
+
+# from . import leaderboard
diff --git a/addons_extensions/project_kudos_plus/models/project_badge.py b/addons_extensions/project_kudos_plus/models/project_badge.py
new file mode 100644
index 000000000..db4d4486e
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/models/project_badge.py
@@ -0,0 +1,36 @@
+from odoo import fields, models, api
+
+class ProjectKudosBadge(models.Model):
+ _name = 'project.kudos.badge'
+ _description = 'Kudos Achievement Badges'
+
+ name = fields.Char(required=True)
+ level = fields.Selection([
+ ('bronze', 'Bronze'),
+ ('silver', 'Silver'),
+ ('gold', 'Gold'),
+ ('platinum', 'Platinum'),
+ ], required=True)
+ threshold = fields.Integer(string="Points Required", required=True)
+ employee_ids = fields.Many2many(
+ 'hr.employee',
+ 'project_kudos_badge_employee_rel', # <-- add this line
+ 'badge_id',
+ 'employee_id',
+ string="Awarded Employees"
+ )
+
+ @api.model
+ def _cron_assign_badges(self):
+ employees = self.env['hr.employee'].search([])
+ for emp in employees:
+ total_points = sum(self.env['project.kudos'].search([
+ ('employee_id', '=', emp.id)
+ ]).mapped('points_awarded'))
+
+ badge = self.search([('threshold', '<=', total_points)],
+ order='threshold desc',
+ limit=1)
+ if badge and emp not in badge.employee_ids:
+ badge.employee_ids = [(4, emp.id)]
+ emp.message_post(body=f"π
Congratulations {emp.name}! You earned the {badge.level.title()} Badge.")
diff --git a/addons_extensions/project_kudos_plus/models/project_kudos.py b/addons_extensions/project_kudos_plus/models/project_kudos.py
new file mode 100644
index 000000000..2f85f96b5
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/models/project_kudos.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models
+from markupsafe import Markup
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class ProjectKudos(models.Model):
+ _name = 'project.kudos'
+ _description = 'Employee Task Reward'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _order = 'date_awarded desc'
+
+ name = fields.Char(string="Reward Title", default="Task Completion Reward", tracking=True)
+ employee_id = fields.Many2one('hr.employee', string="Employee", required=True, tracking=True)
+ task_id = fields.Many2one('project.task', string="Task", required=True, tracking=True)
+ project_id = fields.Many2one('project.project', related='task_id.project_id', store=True, tracking=True)
+ completed_early_by = fields.Float(string="Hours Early", tracking=True)
+ points_awarded = fields.Integer(string="Points", default=10, tracking=True)
+ date_awarded = fields.Datetime(default=fields.Datetime.now, tracking=True)
+
+ # β
New: show badges this employee currently has
+ badge_ids = fields.Many2many(
+ 'project.kudos.badge',
+ string="Badges",
+ compute='_compute_badges',
+ store=False,
+ readonly=True
+ )
+
+ @api.depends('employee_id')
+ def _compute_badges(self):
+ """Compute badges linked to the selected employee."""
+ Badge = self.env['project.kudos.badge']
+ for rec in self:
+ if rec.employee_id:
+ rec.badge_ids = Badge.search([('employee_ids', 'in', rec.employee_id.id)])
+ else:
+ rec.badge_ids = [(5, 0, 0)] # clear
+
+ @api.onchange('employee_id')
+ def _onchange_employee_id(self):
+ """Show badges immediately when selecting employee."""
+ for rec in self:
+ rec._compute_badges()
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = super(ProjectKudos, self).create(vals_list)
+ template = self.env.ref('project_kudos_plus.mail_template_kudos_notify', raise_if_not_found=False)
+
+ # Ensure Kudos Announcements channel exists
+ channel = None
+ if 'discuss.channel' in self.env.registry.models:
+ channel = self.env['discuss.channel'].sudo().search(
+ [('name', '=', 'Kudos Announcements')], limit=1
+ )
+ if not channel:
+ channel = self.env['discuss.channel'].sudo().create({
+ 'name': 'Kudos Announcements',
+ 'public': 'comment',
+ })
+ else:
+ _logger.warning("Discuss module not found, skipping channel announcement.")
+
+ for rec in records:
+ # Send appreciation email
+ if template and rec.employee_id.work_email:
+ template.email_to = rec.employee_id.work_email
+ template.send_mail(rec.id, force_send=True)
+
+ # β
Message content (HTML safe)
+ message = Markup(
+ "π KUDOS ALERT! π
"
+ f"π Employee: {rec.employee_id.name}
"
+ f"π Task Completed: {rec.task_id.name}
"
+ f"π Kudos Points Earned: {rec.points_awarded}
"
+ "π Outstanding performance! Your dedication and hard work made this happen.
"
+ "Let's keep up the momentum! πͺπ₯
"
+ "β Project Kudos+ Team"
+ )
+
+ # Post to recordβs chatter
+ rec.message_post(
+ body=message,
+ subject=f"π Kudos for {rec.employee_id.name}",
+ message_type='comment',
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ # Post to Kudos Announcements channel
+ if channel:
+ try:
+ channel.message_post(
+ body=message,
+ subject=f"π Kudos for {rec.employee_id.name}",
+ message_type='comment',
+ subtype_xmlid='mail.mt_comment'
+ )
+ except Exception as e:
+ _logger.error(f"Failed to post to Kudos Announcements channel: {e}")
+
+ # β
FIXED: use rec.employee_id instead of undefined 'employee'
+ total_points = sum(
+ self.search([('employee_id', '=', rec.employee_id.id)]).mapped('points_awarded')
+ )
+
+ badge = self.env['project.kudos.badge'].search(
+ [('threshold', '<=', total_points)],
+ order='threshold desc',
+ limit=1
+ )
+
+ if badge and rec.employee_id not in badge.employee_ids:
+ badge.employee_ids = [(4, rec.employee_id.id)]
+ rec.employee_id.message_post(
+ body=f"π
Congratulations {rec.employee_id.name}! You earned the {badge.level.title()} Badge!"
+ )
+
+ return records
diff --git a/addons_extensions/project_kudos_plus/models/project_kudos_leaderboard.py b/addons_extensions/project_kudos_plus/models/project_kudos_leaderboard.py
new file mode 100644
index 000000000..a5bd595b9
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/models/project_kudos_leaderboard.py
@@ -0,0 +1,30 @@
+from odoo import fields, models
+
+class ProjectKudosLeaderboard(models.Model):
+ _name = 'project.kudos.leaderboard'
+ _description = 'Kudos Leaderboard'
+ _auto = False
+ _order = 'total_points DESC'
+
+ employee_id = fields.Many2one('hr.employee', string='Employee', readonly=True)
+ total_points = fields.Float(string='Total Points', readonly=True)
+ badge_names = fields.Char(string='Badges Earned', readonly=True)
+
+ def init(self):
+ self.env.cr.execute("""DROP VIEW IF EXISTS project_kudos_leaderboard CASCADE;""")
+ self.env.cr.execute("""
+ CREATE OR REPLACE VIEW project_kudos_leaderboard AS (
+ SELECT
+ ROW_NUMBER() OVER() AS id,
+ e.id AS employee_id,
+ COALESCE(SUM(k.points_awarded), 0) AS total_points,
+ COALESCE(STRING_AGG(DISTINCT b.name, ', '), '') AS badge_names
+ FROM hr_employee e
+ LEFT JOIN project_kudos k ON k.employee_id = e.id
+ LEFT JOIN project_kudos_badge_employee_rel r ON r.employee_id = e.id
+ LEFT JOIN project_kudos_badge b ON b.id = r.badge_id
+ GROUP BY e.id
+ HAVING COALESCE(SUM(k.points_awarded), 0) > 0 OR COUNT(b.id) > 0
+ ORDER BY total_points DESC
+ )
+ """)
diff --git a/addons_extensions/project_kudos_plus/models/project_task_inherit.py b/addons_extensions/project_kudos_plus/models/project_task_inherit.py
new file mode 100644
index 000000000..1a87a4808
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/models/project_task_inherit.py
@@ -0,0 +1,24 @@
+from odoo import models, fields, api
+
+class ProjectTask(models.Model):
+ _inherit = 'project.task'
+
+ def write(self, vals):
+ res = super(ProjectTask, self).write(vals)
+ for task in self:
+ if 'stage_id' in vals:
+ stage = self.env['project.task.type'].browse(vals['stage_id'])
+ if stage.name.lower() == 'done' and task.date_deadline:
+ if fields.Datetime.now() <= task.date_deadline:
+ self.env['project.kudos'].create({
+ 'employee_id': task.user_id.employee_id.id,
+ 'task_id': task.id,
+ 'completed_early_by': (task.date_deadline - fields.Datetime.now()).total_seconds() / 3600,
+ 'points_awarded': 10
+ })
+ emp = task.user_id.employee_id
+ emp.kudos_points = emp.kudos_points + 10 if emp.kudos_points else 10
+ template = self.env.ref('project_kudos_plus.mail_template_kudos')
+ template.send_mail(task.id, force_send=True)
+ task.message_post(body=f"π Kudos! {task.user_id.name} completed this task before the deadline.")
+ return res
diff --git a/addons_extensions/project_kudos_plus/security/ir.model.access.csv b/addons_extensions/project_kudos_plus/security/ir.model.access.csv
new file mode 100644
index 000000000..c1996d3ce
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/security/ir.model.access.csv
@@ -0,0 +1,4 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_project_kudos_user,project.kudos user,model_project_kudos,base.group_user,1,1,1,1
+access_project_kudos_badge_user,project.kudos.badge user,model_project_kudos_badge,base.group_user,1,1,1,1
+access_project_kudos_leaderboard,project.kudos.leaderboard user,model_project_kudos_leaderboard,base.group_user,1,0,0,0
diff --git a/addons_extensions/project_kudos_plus/views/badge_views.xml b/addons_extensions/project_kudos_plus/views/badge_views.xml
new file mode 100644
index 000000000..9dc9ea3db
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/views/badge_views.xml
@@ -0,0 +1,36 @@
+
+
+ project.kudos.badge.list
+ project.kudos.badge
+
+
+
+
+
+
+
+
+
+
+ project.kudos.badge.form
+ project.kudos.badge
+
+
+
+
+
+
+ Badges
+ project.kudos.badge
+ list,form
+
+
diff --git a/addons_extensions/project_kudos_plus/views/kudos_views.xml b/addons_extensions/project_kudos_plus/views/kudos_views.xml
new file mode 100644
index 000000000..4e1fa2a7d
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/views/kudos_views.xml
@@ -0,0 +1,50 @@
+
+
+
+ project.kudos.list
+ project.kudos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ project.kudos.form
+ project.kudos
+
+
+
+
+
+
+
+ Kudos Log
+ project.kudos
+ list,form
+
+
diff --git a/addons_extensions/project_kudos_plus/views/leaderboard_views.xml b/addons_extensions/project_kudos_plus/views/leaderboard_views.xml
new file mode 100644
index 000000000..998208a5c
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/views/leaderboard_views.xml
@@ -0,0 +1,19 @@
+
+
+ project.kudos.leaderboard.tree
+ project.kudos.leaderboard
+
+
+
+
+
+
+
+
+
+
+ Leaderboard
+ project.kudos.leaderboard
+ list
+
+
diff --git a/addons_extensions/project_kudos_plus/views/menu.xml b/addons_extensions/project_kudos_plus/views/menu.xml
new file mode 100644
index 000000000..4b67fa898
--- /dev/null
+++ b/addons_extensions/project_kudos_plus/views/menu.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+