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