odoo-addon-hr-appraisal-oca 16.0.1.0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/hr_appraisal_oca/README.rst +218 -0
- odoo/addons/hr_appraisal_oca/__init__.py +2 -0
- odoo/addons/hr_appraisal_oca/__manifest__.py +28 -0
- odoo/addons/hr_appraisal_oca/data/mail_activity_type_data.xml +9 -0
- odoo/addons/hr_appraisal_oca/data/mail_template_data.xml +167 -0
- odoo/addons/hr_appraisal_oca/i18n/es.po +1201 -0
- odoo/addons/hr_appraisal_oca/i18n/hr_appraisal_oca.pot +1086 -0
- odoo/addons/hr_appraisal_oca/models/__init__.py +5 -0
- odoo/addons/hr_appraisal_oca/models/hr_appraisal.py +444 -0
- odoo/addons/hr_appraisal_oca/models/hr_appraisal_tag.py +21 -0
- odoo/addons/hr_appraisal_oca/models/hr_appraisal_template.py +31 -0
- odoo/addons/hr_appraisal_oca/models/hr_employee.py +125 -0
- odoo/addons/hr_appraisal_oca/models/res_config_settings.py +16 -0
- odoo/addons/hr_appraisal_oca/readme/CONTRIBUTORS.md +6 -0
- odoo/addons/hr_appraisal_oca/readme/DESCRIPTION.md +5 -0
- odoo/addons/hr_appraisal_oca/readme/USAGE.md +95 -0
- odoo/addons/hr_appraisal_oca/security/hr_appraisal_security.xml +58 -0
- odoo/addons/hr_appraisal_oca/security/ir.model.access.csv +10 -0
- odoo/addons/hr_appraisal_oca/static/description/banner.png +0 -0
- odoo/addons/hr_appraisal_oca/static/description/icon.png +0 -0
- odoo/addons/hr_appraisal_oca/static/description/index.html +535 -0
- odoo/addons/hr_appraisal_oca/tests/__init__.py +1 -0
- odoo/addons/hr_appraisal_oca/tests/test_hr_appraisal.py +120 -0
- odoo/addons/hr_appraisal_oca/views/hr_appraisal_form_view.xml +465 -0
- odoo/addons/hr_appraisal_oca/views/hr_appraisal_tag_form_view.xml +47 -0
- odoo/addons/hr_appraisal_oca/views/hr_appraisal_template_form_view.xml +106 -0
- odoo/addons/hr_appraisal_oca/views/hr_employee_form_view.xml +42 -0
- odoo/addons/hr_appraisal_oca/views/res_config_settings_views.xml +59 -0
- odoo/addons/hr_appraisal_oca/wizard/__init__.py +2 -0
- odoo/addons/hr_appraisal_oca/wizard/hr_appraisal_request_wizard_view.xml +40 -0
- odoo/addons/hr_appraisal_oca/wizard/hr_appraisal_wizard.py +159 -0
- odoo/addons/hr_appraisal_oca/wizard/hr_appraisal_wizard_form_view.xml +32 -0
- odoo/addons/hr_appraisal_oca/wizard/send_mail_with_template_wizard.py +52 -0
- odoo_addon_hr_appraisal_oca-16.0.1.0.0.3.dist-info/METADATA +233 -0
- odoo_addon_hr_appraisal_oca-16.0.1.0.0.3.dist-info/RECORD +37 -0
- odoo_addon_hr_appraisal_oca-16.0.1.0.0.3.dist-info/WHEEL +5 -0
- odoo_addon_hr_appraisal_oca-16.0.1.0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# Copyright 2025 Fundacion Esment - Estefanía Bauzá
|
|
2
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
from odoo import _, api, fields, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HrAppraisal(models.Model):
|
|
10
|
+
_name = "hr.appraisal"
|
|
11
|
+
_inherit = ["mail.thread", "mail.activity.mixin"]
|
|
12
|
+
_rec_name = "employee_id"
|
|
13
|
+
_description = "Employee Appraisal"
|
|
14
|
+
_order = "state desc, date_close, id desc"
|
|
15
|
+
|
|
16
|
+
employee_id = fields.Many2one(
|
|
17
|
+
"hr.employee",
|
|
18
|
+
string="Employee",
|
|
19
|
+
required=True,
|
|
20
|
+
default=lambda self: self._default_employee_id(),
|
|
21
|
+
readonly=True,
|
|
22
|
+
states={"1_new": [("readonly", False)]},
|
|
23
|
+
domain="[('id', 'in', employee_domain_ids)]",
|
|
24
|
+
)
|
|
25
|
+
employee_domain_ids = fields.Many2many(
|
|
26
|
+
"hr.employee",
|
|
27
|
+
compute="_compute_employee_domain_ids",
|
|
28
|
+
)
|
|
29
|
+
manager_ids = fields.Many2many(
|
|
30
|
+
"hr.employee",
|
|
31
|
+
"hr_appraisal_managers_rel",
|
|
32
|
+
"hr_appraisal_id",
|
|
33
|
+
compute="_compute_manager_ids",
|
|
34
|
+
inverse="_inverse_manager_ids",
|
|
35
|
+
domain="[('id', '!=', employee_id)]",
|
|
36
|
+
check_company=True,
|
|
37
|
+
required=True,
|
|
38
|
+
store=True,
|
|
39
|
+
)
|
|
40
|
+
date_close = fields.Date(
|
|
41
|
+
string="Appraisal Date",
|
|
42
|
+
required=True,
|
|
43
|
+
help="Closing date of the current appraisal",
|
|
44
|
+
)
|
|
45
|
+
job_id = fields.Many2one(
|
|
46
|
+
"hr.job", string="Job Position", related="employee_id.job_id"
|
|
47
|
+
)
|
|
48
|
+
department_id = fields.Many2one(
|
|
49
|
+
"hr.department", "Department", compute="_compute_department"
|
|
50
|
+
)
|
|
51
|
+
company_id = fields.Many2one(
|
|
52
|
+
"res.company",
|
|
53
|
+
string="Company",
|
|
54
|
+
related="employee_id.company_id",
|
|
55
|
+
store=True,
|
|
56
|
+
)
|
|
57
|
+
appraisal_template_id = fields.Many2one(
|
|
58
|
+
"hr.appraisal.template",
|
|
59
|
+
string="Appraisal Template",
|
|
60
|
+
check_company=True,
|
|
61
|
+
)
|
|
62
|
+
state = fields.Selection(
|
|
63
|
+
[("1_new", "To Confirm"), ("2_pending", "Confirmed"), ("3_done", "Done")],
|
|
64
|
+
string="Status",
|
|
65
|
+
default="1_new",
|
|
66
|
+
index=True,
|
|
67
|
+
required=True,
|
|
68
|
+
tracking=True,
|
|
69
|
+
)
|
|
70
|
+
employee_feedback = fields.Html(
|
|
71
|
+
compute="_compute_employee_feedback", store=True, readonly=False
|
|
72
|
+
)
|
|
73
|
+
manager_feedback = fields.Html(
|
|
74
|
+
compute="_compute_manager_feedback", store=True, readonly=False
|
|
75
|
+
)
|
|
76
|
+
employee_feedback_published = fields.Boolean(default=True, tracking=True)
|
|
77
|
+
manager_feedback_published = fields.Boolean(default=True, tracking=True)
|
|
78
|
+
can_see_employee_publish = fields.Boolean(
|
|
79
|
+
default=False,
|
|
80
|
+
compute="_compute_can_see_employee_manager_publish",
|
|
81
|
+
)
|
|
82
|
+
can_see_manager_publish = fields.Boolean(
|
|
83
|
+
default=False,
|
|
84
|
+
compute="_compute_can_see_employee_manager_publish",
|
|
85
|
+
)
|
|
86
|
+
employee_appraisal_count = fields.Integer(
|
|
87
|
+
string="Appraisal Count", related="employee_id.appraisal_count"
|
|
88
|
+
)
|
|
89
|
+
color = fields.Integer(string="Color Index")
|
|
90
|
+
created_by = fields.Many2one("res.users", default=lambda self: self.env.uid)
|
|
91
|
+
employee_user_id = fields.Many2one(
|
|
92
|
+
"res.users",
|
|
93
|
+
related="employee_id.user_id",
|
|
94
|
+
string="Employee User",
|
|
95
|
+
)
|
|
96
|
+
manager_user_ids = fields.Many2many(
|
|
97
|
+
"res.users",
|
|
98
|
+
string="Manager Users",
|
|
99
|
+
compute="_compute_manager_user",
|
|
100
|
+
)
|
|
101
|
+
is_manager = fields.Boolean(compute="_compute_is_manager")
|
|
102
|
+
activity_ids = fields.One2many("mail.activity", "res_id", "Activities")
|
|
103
|
+
note = fields.Html(
|
|
104
|
+
string="Private Note",
|
|
105
|
+
help="The content of this note is not visible by the Employee.",
|
|
106
|
+
)
|
|
107
|
+
tag_ids = fields.Many2many("hr.appraisal.tag", string="Tags")
|
|
108
|
+
active = fields.Boolean(default=True)
|
|
109
|
+
employee_feedback_template = fields.Html(compute="_compute_feedback_templates")
|
|
110
|
+
manager_feedback_template = fields.Html(compute="_compute_feedback_templates")
|
|
111
|
+
|
|
112
|
+
@api.model
|
|
113
|
+
def default_get(self, fields_list):
|
|
114
|
+
"""Set default template and initialize feedback fields.
|
|
115
|
+
|
|
116
|
+
- If user already provided values, preserve them.
|
|
117
|
+
- If no template is provided, use the default from config.
|
|
118
|
+
- If no feedback is provided, populate from the selected template.
|
|
119
|
+
"""
|
|
120
|
+
res = super().default_get(fields_list)
|
|
121
|
+
if not res.get("appraisal_template_id"):
|
|
122
|
+
default_template_id = int(
|
|
123
|
+
self.env["ir.config_parameter"]
|
|
124
|
+
.sudo()
|
|
125
|
+
.get_param("hr_appraisal_oca.default_appraisal_template_id", 0)
|
|
126
|
+
)
|
|
127
|
+
if default_template_id:
|
|
128
|
+
res["appraisal_template_id"] = default_template_id
|
|
129
|
+
return res
|
|
130
|
+
|
|
131
|
+
@api.model
|
|
132
|
+
def _default_employee_id(self):
|
|
133
|
+
"""
|
|
134
|
+
Return the default employee for the appraisal.
|
|
135
|
+
|
|
136
|
+
- None if the user is an HR Officer or has subordinates.
|
|
137
|
+
- Otherwise, the current user's employee.
|
|
138
|
+
|
|
139
|
+
:return: Employee ID or False.
|
|
140
|
+
:rtype: int | bool
|
|
141
|
+
"""
|
|
142
|
+
employee, subordinates = self._get_current_employee_and_subordinates()
|
|
143
|
+
if not employee or self.env.user.has_group(
|
|
144
|
+
"hr_appraisal_oca.group_appraisal_hr_officer"
|
|
145
|
+
):
|
|
146
|
+
return False
|
|
147
|
+
if subordinates.filtered(lambda e: e.id != employee.id):
|
|
148
|
+
return False
|
|
149
|
+
return employee.id
|
|
150
|
+
|
|
151
|
+
def _get_current_employee_and_subordinates(self):
|
|
152
|
+
"""
|
|
153
|
+
Get the current user's employee and their subordinates
|
|
154
|
+
(direct or indirect).
|
|
155
|
+
|
|
156
|
+
:return: Tuple (employee, subordinates recordset).
|
|
157
|
+
Employee is None if not found or user is HR Officer.
|
|
158
|
+
:rtype: tuple(hr.employee | None, recordset(hr.employee))
|
|
159
|
+
"""
|
|
160
|
+
user = self.env.user
|
|
161
|
+
Employee = self.env["hr.employee"]
|
|
162
|
+
if user.has_group("hr_appraisal_oca.group_appraisal_hr_officer"):
|
|
163
|
+
return None, Employee
|
|
164
|
+
employee = user.employee_ids[:1]
|
|
165
|
+
if not employee:
|
|
166
|
+
return None, Employee
|
|
167
|
+
subordinates = Employee.search([("id", "child_of", employee.id)])
|
|
168
|
+
return employee, subordinates
|
|
169
|
+
|
|
170
|
+
@api.depends_context("uid")
|
|
171
|
+
@api.depends("state")
|
|
172
|
+
def _compute_employee_domain_ids(self):
|
|
173
|
+
Employee = self.env["hr.employee"]
|
|
174
|
+
user = self.env.user
|
|
175
|
+
if user.has_group("hr_appraisal_oca.group_appraisal_hr_officer"):
|
|
176
|
+
allowed = Employee.search([])
|
|
177
|
+
else:
|
|
178
|
+
employee, subordinates = self._get_current_employee_and_subordinates()
|
|
179
|
+
if employee:
|
|
180
|
+
ids = subordinates.ids if subordinates else []
|
|
181
|
+
if employee.id not in ids:
|
|
182
|
+
ids.append(employee.id)
|
|
183
|
+
allowed = Employee.browse(ids)
|
|
184
|
+
else:
|
|
185
|
+
allowed = Employee.browse()
|
|
186
|
+
for rec in self:
|
|
187
|
+
rec.employee_domain_ids = allowed
|
|
188
|
+
|
|
189
|
+
@api.depends("employee_id", "manager_ids")
|
|
190
|
+
def _compute_manager_user(self):
|
|
191
|
+
self.manager_user_ids = [(6, 0, self.manager_ids.user_id.ids)]
|
|
192
|
+
|
|
193
|
+
@api.depends("appraisal_template_id")
|
|
194
|
+
def _compute_employee_feedback(self):
|
|
195
|
+
for appraisal in self.filtered(lambda a: a.state == "1_new"):
|
|
196
|
+
appraisal.employee_feedback = appraisal.employee_feedback_template
|
|
197
|
+
|
|
198
|
+
@api.depends("appraisal_template_id")
|
|
199
|
+
def _compute_manager_feedback(self):
|
|
200
|
+
for appraisal in self.filtered(lambda a: a.state == "1_new"):
|
|
201
|
+
appraisal.manager_feedback = appraisal.manager_feedback_template
|
|
202
|
+
|
|
203
|
+
@api.depends("appraisal_template_id")
|
|
204
|
+
def _compute_feedback_templates(self):
|
|
205
|
+
for appraisal in self:
|
|
206
|
+
template = appraisal.appraisal_template_id
|
|
207
|
+
appraisal.employee_feedback_template = (
|
|
208
|
+
template.appraisal_employee_feedback_template
|
|
209
|
+
if appraisal.appraisal_template_id
|
|
210
|
+
else False
|
|
211
|
+
)
|
|
212
|
+
appraisal.manager_feedback_template = (
|
|
213
|
+
template.appraisal_manager_feedback_template
|
|
214
|
+
if appraisal.appraisal_template_id
|
|
215
|
+
else False
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@api.depends("employee_id")
|
|
219
|
+
def _compute_manager_ids(self):
|
|
220
|
+
for record in self:
|
|
221
|
+
if record.employee_id.parent_id:
|
|
222
|
+
record.manager_ids = record.employee_id.parent_id
|
|
223
|
+
else:
|
|
224
|
+
record.manager_ids = False
|
|
225
|
+
|
|
226
|
+
def _inverse_manager_ids(self):
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
def write(self, vals):
|
|
230
|
+
close_appraisal = vals.get("state") == "3_done"
|
|
231
|
+
appraisal_activity_ref = None
|
|
232
|
+
if close_appraisal:
|
|
233
|
+
vals["date_close"] = datetime.date.today()
|
|
234
|
+
appraisal_activity_ref = self.env.ref(
|
|
235
|
+
"hr_appraisal_oca.mail_act_hr_appraisal_cfr"
|
|
236
|
+
)
|
|
237
|
+
if close_appraisal and appraisal_activity_ref:
|
|
238
|
+
# Check and mark activities as "done"
|
|
239
|
+
for appraisal in self:
|
|
240
|
+
activities = appraisal.activity_ids.filtered(
|
|
241
|
+
lambda act: act.activity_type_id == appraisal_activity_ref
|
|
242
|
+
)
|
|
243
|
+
if activities:
|
|
244
|
+
activities.action_feedback()
|
|
245
|
+
return super().write(vals)
|
|
246
|
+
|
|
247
|
+
@api.depends("employee_id")
|
|
248
|
+
def _compute_department(self):
|
|
249
|
+
for appraisal in self:
|
|
250
|
+
if appraisal.employee_id:
|
|
251
|
+
appraisal.department_id = appraisal.employee_id.department_id
|
|
252
|
+
else:
|
|
253
|
+
appraisal.department_id = False
|
|
254
|
+
|
|
255
|
+
@api.depends_context("uid")
|
|
256
|
+
@api.depends("state", "employee_id")
|
|
257
|
+
def _compute_is_manager(self):
|
|
258
|
+
"""Compute if the current user is a manager for this record."""
|
|
259
|
+
user = self.env.user
|
|
260
|
+
is_hr_officer = user.has_group("hr_appraisal_oca.group_appraisal_hr_officer")
|
|
261
|
+
employee = user.employee_ids[:1]
|
|
262
|
+
is_manager_user = False
|
|
263
|
+
if employee:
|
|
264
|
+
is_manager_user = bool(
|
|
265
|
+
self.env["hr.employee"].search_count([("parent_id", "=", employee.id)])
|
|
266
|
+
)
|
|
267
|
+
for record in self:
|
|
268
|
+
record.is_manager = (
|
|
269
|
+
False
|
|
270
|
+
if record.employee_user_id.id == user.id
|
|
271
|
+
else is_hr_officer or is_manager_user
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def _visibility_role(self, rec, user_emp, uid):
|
|
275
|
+
if user_emp and rec.employee_id == user_emp:
|
|
276
|
+
return "user_employee"
|
|
277
|
+
user_is_assigned_manager = uid in rec.manager_ids.mapped("user_id").ids
|
|
278
|
+
if rec.is_manager and user_is_assigned_manager:
|
|
279
|
+
return "user_manager"
|
|
280
|
+
if rec.is_manager:
|
|
281
|
+
return "record_manager"
|
|
282
|
+
return "other"
|
|
283
|
+
|
|
284
|
+
@api.depends_context("uid")
|
|
285
|
+
@api.depends("state", "employee_id", "manager_ids")
|
|
286
|
+
def _compute_can_see_employee_manager_publish(self):
|
|
287
|
+
MAPPING = {
|
|
288
|
+
("1_new", "user_employee"): (True, False),
|
|
289
|
+
("1_new", "user_manager"): (True, True),
|
|
290
|
+
("2_pending", "user_employee"): (True, False),
|
|
291
|
+
("2_pending", "user_manager"): (False, True),
|
|
292
|
+
("2_pending", "record_manager"): (True, True),
|
|
293
|
+
("3_done", "user_employee"): (True, False),
|
|
294
|
+
("3_done", "user_manager"): (False, True),
|
|
295
|
+
("3_done", "record_manager"): (False, True),
|
|
296
|
+
}
|
|
297
|
+
user_employee = self.env.user.employee_ids[:1]
|
|
298
|
+
user_id = self.env.user.id
|
|
299
|
+
for rec in self:
|
|
300
|
+
role = self._visibility_role(rec, user_employee, user_id)
|
|
301
|
+
rec.can_see_employee_publish, rec.can_see_manager_publish = MAPPING.get(
|
|
302
|
+
(rec.state, role), (False, False)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
@api.onchange("employee_id", "manager_ids", "state")
|
|
306
|
+
def _onchange_visibility_flags(self):
|
|
307
|
+
# FIXME: review how to execute the method without using this onchange
|
|
308
|
+
# in the context of user = employee without subordinates.
|
|
309
|
+
self._compute_can_see_employee_manager_publish()
|
|
310
|
+
|
|
311
|
+
def action_confirm(self):
|
|
312
|
+
"""
|
|
313
|
+
Confirm the appraisal by setting its state to 'pending'
|
|
314
|
+
and resetting feedback flags.
|
|
315
|
+
|
|
316
|
+
- Sends confirmation emails to the employee and managers.
|
|
317
|
+
- Creates CFR activities for the employee and managers
|
|
318
|
+
if they have associated users.
|
|
319
|
+
"""
|
|
320
|
+
self.state = "2_pending"
|
|
321
|
+
self.employee_feedback_published = False
|
|
322
|
+
self.manager_feedback_published = False
|
|
323
|
+
template = "hr_appraisal_oca.mail_template_appraisal_confirmation"
|
|
324
|
+
if self.employee_id.work_email:
|
|
325
|
+
self._send_email(
|
|
326
|
+
self.employee_id.user_id, template, self.employee_id.work_email
|
|
327
|
+
)
|
|
328
|
+
if self.employee_user_id.id:
|
|
329
|
+
user_id = int(self.employee_user_id.id)
|
|
330
|
+
self._create_activity_cfr(user_id)
|
|
331
|
+
for record in self:
|
|
332
|
+
for manager in record.manager_ids:
|
|
333
|
+
if manager.work_email:
|
|
334
|
+
self._send_email(manager.user_id, template, manager.work_email)
|
|
335
|
+
if manager.user_id.id:
|
|
336
|
+
user_id = int(manager.user_id.id)
|
|
337
|
+
self._create_activity_cfr(user_id)
|
|
338
|
+
|
|
339
|
+
def action_done(self):
|
|
340
|
+
"""
|
|
341
|
+
Mark the appraisal as done, publish feedback flags, send completion emails,
|
|
342
|
+
and log the status change.
|
|
343
|
+
|
|
344
|
+
- Sets state to 'done' and marks feedback as published.
|
|
345
|
+
- Sends completion emails to the employee and all managers with valid emails.
|
|
346
|
+
- Posts a message indicating the appraisal was completed by the current user.
|
|
347
|
+
"""
|
|
348
|
+
self.state = "3_done"
|
|
349
|
+
self.employee_feedback_published = True
|
|
350
|
+
self.manager_feedback_published = True
|
|
351
|
+
template = "hr_appraisal_oca.mail_template_appraisal_completed"
|
|
352
|
+
if self.employee_id.work_email:
|
|
353
|
+
self._send_email(
|
|
354
|
+
self.employee_id.user_id, template, self.employee_id.work_email
|
|
355
|
+
)
|
|
356
|
+
for record in self:
|
|
357
|
+
for manager in record.manager_ids:
|
|
358
|
+
if manager.work_email:
|
|
359
|
+
self._send_email(manager.user_id, template, manager.work_email)
|
|
360
|
+
|
|
361
|
+
def action_back(self):
|
|
362
|
+
self.state = "1_new"
|
|
363
|
+
|
|
364
|
+
def _send_email(self, recipient_users, template, email):
|
|
365
|
+
if not email or not recipient_users:
|
|
366
|
+
return
|
|
367
|
+
ctx = {"recipient_users": recipient_users}
|
|
368
|
+
self.env["send.email.with.template"].send_email_with_template(
|
|
369
|
+
template,
|
|
370
|
+
self.id,
|
|
371
|
+
email,
|
|
372
|
+
ctx,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def _create_activity_cfr(self, user_id):
|
|
376
|
+
activity_type = (
|
|
377
|
+
self.env.ref(
|
|
378
|
+
"hr_appraisal_oca.mail_act_hr_appraisal_cfr",
|
|
379
|
+
raise_if_not_found=False,
|
|
380
|
+
)
|
|
381
|
+
or self.env["mail.activity.type"]
|
|
382
|
+
)
|
|
383
|
+
if activity_type:
|
|
384
|
+
self.activity_schedule(
|
|
385
|
+
"hr_appraisal_oca.mail_act_hr_appraisal_cfr",
|
|
386
|
+
date_deadline=self.date_close,
|
|
387
|
+
summary=_("Appraisal Form to Fill"),
|
|
388
|
+
note=_(
|
|
389
|
+
"Fill appraisal for %(employee)s ", employee=self.employee_id.name
|
|
390
|
+
),
|
|
391
|
+
user_id=user_id,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def action_open_employee_appraisals(self):
|
|
395
|
+
return {
|
|
396
|
+
"name": _("Previous Appraisals"),
|
|
397
|
+
"type": "ir.actions.act_window",
|
|
398
|
+
"view_mode": "tree,form",
|
|
399
|
+
"res_model": "hr.appraisal",
|
|
400
|
+
"domain": [("employee_id", "=", self.employee_id.id)],
|
|
401
|
+
"context": dict(
|
|
402
|
+
self.env.context,
|
|
403
|
+
group_by=["date_close:month"],
|
|
404
|
+
search_default_group_by=False,
|
|
405
|
+
),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
def action_publish_employee_feedback(self):
|
|
409
|
+
if (
|
|
410
|
+
not self.employee_feedback_published
|
|
411
|
+
and self.employee_id.user_id.id != self.env.user.id
|
|
412
|
+
):
|
|
413
|
+
view_id = self.env.ref("hr_appraisal_oca.hr_appraisal_wizard_form_view").id
|
|
414
|
+
view_item = [(view_id, "form")]
|
|
415
|
+
return {
|
|
416
|
+
"name": _("Confirmation"),
|
|
417
|
+
"view_type": "form",
|
|
418
|
+
"view_mode": "form",
|
|
419
|
+
"view_id": view_id,
|
|
420
|
+
"res_model": "hr.appraisal.wizard",
|
|
421
|
+
"views": view_item,
|
|
422
|
+
"type": "ir.actions.act_window",
|
|
423
|
+
"target": "new",
|
|
424
|
+
"context": {
|
|
425
|
+
"default_res_model": "hr.appraisal",
|
|
426
|
+
"default_res_id": self.id,
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
else:
|
|
430
|
+
self.employee_feedback_published = not self.employee_feedback_published
|
|
431
|
+
|
|
432
|
+
def action_publish_manager_feedback(self):
|
|
433
|
+
self.manager_feedback_published = not self.manager_feedback_published
|
|
434
|
+
|
|
435
|
+
def action_send_appraisal_request(self):
|
|
436
|
+
if self.employee_id:
|
|
437
|
+
return {
|
|
438
|
+
"type": "ir.actions.act_window",
|
|
439
|
+
"view_mode": "form",
|
|
440
|
+
"res_model": "hr.appraisal.request.wizard",
|
|
441
|
+
"target": "new",
|
|
442
|
+
"name": _("Appraisal Request"),
|
|
443
|
+
"context": {"default_appraisal_id": self.id},
|
|
444
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright 2025 Fundacion Esment - Estefanía Bauzá
|
|
2
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from random import randint
|
|
5
|
+
|
|
6
|
+
from odoo import fields, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HrAppraisalTag(models.Model):
|
|
10
|
+
_name = "hr.appraisal.tag"
|
|
11
|
+
_description = "Appraisal Tags"
|
|
12
|
+
|
|
13
|
+
def _get_default_color(self):
|
|
14
|
+
return randint(1, 11)
|
|
15
|
+
|
|
16
|
+
name = fields.Char("Tag Name", required=True)
|
|
17
|
+
color = fields.Integer(default=_get_default_color)
|
|
18
|
+
|
|
19
|
+
_sql_constraints = [
|
|
20
|
+
("name_uniq", "unique (name)", "Tag name already exists !"),
|
|
21
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Copyright 2025 Fundacion Esment - Estefanía Bauzá
|
|
2
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import fields, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HrAppraisalTemplate(models.Model):
|
|
8
|
+
_name = "hr.appraisal.template"
|
|
9
|
+
_description = "HR Appraisal Templates"
|
|
10
|
+
_rec_name = "description"
|
|
11
|
+
|
|
12
|
+
description = fields.Text(required=True)
|
|
13
|
+
company_id = fields.Many2one(
|
|
14
|
+
"res.company", default=lambda self: self.env.user.company_id
|
|
15
|
+
)
|
|
16
|
+
appraisal_employee_feedback_template = fields.Html(
|
|
17
|
+
string="Employee Feedback", translate=True
|
|
18
|
+
)
|
|
19
|
+
appraisal_manager_feedback_template = fields.Html(
|
|
20
|
+
string="Manager Feedback", translate=True
|
|
21
|
+
)
|
|
22
|
+
is_default = fields.Boolean(compute="_compute_is_default")
|
|
23
|
+
|
|
24
|
+
def _compute_is_default(self):
|
|
25
|
+
default_template_id = int(
|
|
26
|
+
self.env["ir.config_parameter"]
|
|
27
|
+
.sudo()
|
|
28
|
+
.get_param("hr_appraisal_oca.default_appraisal_template_id", 0)
|
|
29
|
+
)
|
|
30
|
+
for rec in self:
|
|
31
|
+
rec.is_default = rec.id == default_template_id
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Copyright 2025 Fundacion Esment - Estefanía Bauzá
|
|
2
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import _, api, fields, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HrEmployeeBase(models.AbstractModel):
|
|
8
|
+
_inherit = "hr.employee.base"
|
|
9
|
+
|
|
10
|
+
last_appraisal_id = fields.Many2one(
|
|
11
|
+
"hr.appraisal",
|
|
12
|
+
compute="_compute_last_appraisal_id",
|
|
13
|
+
search="_search_last_appraisal_id",
|
|
14
|
+
)
|
|
15
|
+
can_open_last_appraisal = fields.Boolean(compute="_compute_can_open_last_appraisal")
|
|
16
|
+
|
|
17
|
+
def action_open_last_appraisal(self):
|
|
18
|
+
self.ensure_one()
|
|
19
|
+
action = {
|
|
20
|
+
"type": "ir.actions.act_window",
|
|
21
|
+
"res_model": "hr.appraisal",
|
|
22
|
+
"context": dict(self.env.context, default_employee_id=self.id),
|
|
23
|
+
}
|
|
24
|
+
if self.ongoing_appraisal_count > 1:
|
|
25
|
+
action.update(
|
|
26
|
+
{
|
|
27
|
+
"name": _("New and Pending Appraisals"),
|
|
28
|
+
"view_mode": "tree,form",
|
|
29
|
+
"domain": [
|
|
30
|
+
("employee_id", "=", self.id),
|
|
31
|
+
("state", "!=", "3_done"),
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
else:
|
|
36
|
+
action.update(
|
|
37
|
+
{
|
|
38
|
+
"view_mode": "form",
|
|
39
|
+
"res_id": self.last_appraisal_id.id,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
return action
|
|
43
|
+
|
|
44
|
+
def _search_last_appraisal_id(self, operator, value):
|
|
45
|
+
appraisals = self.env["hr.appraisal"].search([("id", operator, value)])
|
|
46
|
+
return [("id", "in", appraisals.mapped("employee_id").ids)]
|
|
47
|
+
|
|
48
|
+
def _compute_last_appraisal_id(self):
|
|
49
|
+
for employee in self:
|
|
50
|
+
last_appraisal = self.env["hr.appraisal"].search(
|
|
51
|
+
[("employee_id", "=", employee.id)],
|
|
52
|
+
order="create_date desc",
|
|
53
|
+
limit=1,
|
|
54
|
+
)
|
|
55
|
+
employee.last_appraisal_id = last_appraisal
|
|
56
|
+
|
|
57
|
+
def _compute_can_open_last_appraisal(self):
|
|
58
|
+
"""
|
|
59
|
+
Check if the employee has a last appraisal and if the user
|
|
60
|
+
is allowed to open it (HR Officer can see all).
|
|
61
|
+
"""
|
|
62
|
+
user = self.env.user
|
|
63
|
+
is_hr_officer = user.has_group("hr_appraisal_oca.group_appraisal_hr_officer")
|
|
64
|
+
for employee in self:
|
|
65
|
+
is_self = employee.user_id and employee.user_id.id == user.id
|
|
66
|
+
allowed = is_hr_officer or user in employee.allowed_user_ids or is_self
|
|
67
|
+
employee.can_open_last_appraisal = bool(
|
|
68
|
+
allowed and employee.last_appraisal_id
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class HrEmployee(models.Model):
|
|
73
|
+
_inherit = "hr.employee"
|
|
74
|
+
|
|
75
|
+
appraisal_count = fields.Integer(
|
|
76
|
+
compute="_compute_appraisal_count",
|
|
77
|
+
store=True,
|
|
78
|
+
groups="hr.group_hr_user",
|
|
79
|
+
)
|
|
80
|
+
appraisal_ids = fields.One2many("hr.appraisal", "employee_id", string="Appraisal")
|
|
81
|
+
last_appraisal_state = fields.Selection(
|
|
82
|
+
related="last_appraisal_id.state", string="Status"
|
|
83
|
+
)
|
|
84
|
+
ongoing_appraisal_count = fields.Integer(
|
|
85
|
+
compute="_compute_ongoing_appraisal_count",
|
|
86
|
+
store=True,
|
|
87
|
+
groups="hr.group_hr_user",
|
|
88
|
+
)
|
|
89
|
+
allowed_user_ids = fields.Many2many(
|
|
90
|
+
"res.users",
|
|
91
|
+
relation="hr_employee_manager_user_rel",
|
|
92
|
+
column1="employee_id",
|
|
93
|
+
column2="user_id",
|
|
94
|
+
string="Allowed Users",
|
|
95
|
+
compute="_compute_allowed_user_ids",
|
|
96
|
+
store=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@api.depends("parent_id")
|
|
100
|
+
def _compute_allowed_user_ids(self):
|
|
101
|
+
"""Compute all allowed user IDs by traversing the manager hierarchy."""
|
|
102
|
+
for employee in self:
|
|
103
|
+
allowed_users = set()
|
|
104
|
+
visited = set()
|
|
105
|
+
current = employee.parent_id
|
|
106
|
+
while current and current.id not in visited:
|
|
107
|
+
visited.add(current.id)
|
|
108
|
+
if current.user_id:
|
|
109
|
+
allowed_users.add(current.user_id.id)
|
|
110
|
+
current = current.parent_id
|
|
111
|
+
employee.allowed_user_ids = [(6, 0, list(allowed_users))]
|
|
112
|
+
|
|
113
|
+
@api.depends("appraisal_ids")
|
|
114
|
+
def _compute_appraisal_count(self):
|
|
115
|
+
for employee in self:
|
|
116
|
+
employee.appraisal_count = len(employee.appraisal_ids)
|
|
117
|
+
|
|
118
|
+
@api.depends("appraisal_ids.state")
|
|
119
|
+
def _compute_ongoing_appraisal_count(self):
|
|
120
|
+
for employee in self:
|
|
121
|
+
employee.ongoing_appraisal_count = len(
|
|
122
|
+
employee.appraisal_ids.filtered(
|
|
123
|
+
lambda a: a.state in ["1_new", "2_pending"]
|
|
124
|
+
)
|
|
125
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright 2025 Fundacion Esment - Estefanía Bauzá
|
|
2
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import fields, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ResConfigSettings(models.TransientModel):
|
|
8
|
+
_inherit = "res.config.settings"
|
|
9
|
+
|
|
10
|
+
appraisal_default_template_id = fields.Many2one(
|
|
11
|
+
comodel_name="hr.appraisal.template",
|
|
12
|
+
string="Default Appraisal Template",
|
|
13
|
+
config_parameter="hr_appraisal_oca.default_appraisal_template_id",
|
|
14
|
+
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
|
15
|
+
help="Default template used for appraisals",
|
|
16
|
+
)
|