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.
Files changed (37) hide show
  1. odoo/addons/hr_appraisal_oca/README.rst +218 -0
  2. odoo/addons/hr_appraisal_oca/__init__.py +2 -0
  3. odoo/addons/hr_appraisal_oca/__manifest__.py +28 -0
  4. odoo/addons/hr_appraisal_oca/data/mail_activity_type_data.xml +9 -0
  5. odoo/addons/hr_appraisal_oca/data/mail_template_data.xml +167 -0
  6. odoo/addons/hr_appraisal_oca/i18n/es.po +1201 -0
  7. odoo/addons/hr_appraisal_oca/i18n/hr_appraisal_oca.pot +1086 -0
  8. odoo/addons/hr_appraisal_oca/models/__init__.py +5 -0
  9. odoo/addons/hr_appraisal_oca/models/hr_appraisal.py +444 -0
  10. odoo/addons/hr_appraisal_oca/models/hr_appraisal_tag.py +21 -0
  11. odoo/addons/hr_appraisal_oca/models/hr_appraisal_template.py +31 -0
  12. odoo/addons/hr_appraisal_oca/models/hr_employee.py +125 -0
  13. odoo/addons/hr_appraisal_oca/models/res_config_settings.py +16 -0
  14. odoo/addons/hr_appraisal_oca/readme/CONTRIBUTORS.md +6 -0
  15. odoo/addons/hr_appraisal_oca/readme/DESCRIPTION.md +5 -0
  16. odoo/addons/hr_appraisal_oca/readme/USAGE.md +95 -0
  17. odoo/addons/hr_appraisal_oca/security/hr_appraisal_security.xml +58 -0
  18. odoo/addons/hr_appraisal_oca/security/ir.model.access.csv +10 -0
  19. odoo/addons/hr_appraisal_oca/static/description/banner.png +0 -0
  20. odoo/addons/hr_appraisal_oca/static/description/icon.png +0 -0
  21. odoo/addons/hr_appraisal_oca/static/description/index.html +535 -0
  22. odoo/addons/hr_appraisal_oca/tests/__init__.py +1 -0
  23. odoo/addons/hr_appraisal_oca/tests/test_hr_appraisal.py +120 -0
  24. odoo/addons/hr_appraisal_oca/views/hr_appraisal_form_view.xml +465 -0
  25. odoo/addons/hr_appraisal_oca/views/hr_appraisal_tag_form_view.xml +47 -0
  26. odoo/addons/hr_appraisal_oca/views/hr_appraisal_template_form_view.xml +106 -0
  27. odoo/addons/hr_appraisal_oca/views/hr_employee_form_view.xml +42 -0
  28. odoo/addons/hr_appraisal_oca/views/res_config_settings_views.xml +59 -0
  29. odoo/addons/hr_appraisal_oca/wizard/__init__.py +2 -0
  30. odoo/addons/hr_appraisal_oca/wizard/hr_appraisal_request_wizard_view.xml +40 -0
  31. odoo/addons/hr_appraisal_oca/wizard/hr_appraisal_wizard.py +159 -0
  32. odoo/addons/hr_appraisal_oca/wizard/hr_appraisal_wizard_form_view.xml +32 -0
  33. odoo/addons/hr_appraisal_oca/wizard/send_mail_with_template_wizard.py +52 -0
  34. odoo_addon_hr_appraisal_oca-16.0.1.0.0.3.dist-info/METADATA +233 -0
  35. odoo_addon_hr_appraisal_oca-16.0.1.0.0.3.dist-info/RECORD +37 -0
  36. odoo_addon_hr_appraisal_oca-16.0.1.0.0.3.dist-info/WHEEL +5 -0
  37. odoo_addon_hr_appraisal_oca-16.0.1.0.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ from . import hr_appraisal
2
+ from . import hr_appraisal_tag
3
+ from . import hr_appraisal_template
4
+ from . import hr_employee
5
+ from . import res_config_settings
@@ -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
+ )
@@ -0,0 +1,6 @@
1
+ - [Fundación Esment](https://esment.org/):
2
+ - Estefanía Bauzá
3
+
4
+ - [Tecnativa](https://www.tecnativa.com/):
5
+ - Pedro M. Baeza
6
+ - Christian Ramos
@@ -0,0 +1,5 @@
1
+ This module helps maintain the employee motivation process through periodic performance
2
+ appraisals.
3
+
4
+ Managers can evaluate employee performance and enable employees to perform
5
+ self-assessments. Review forms can be customized according to organizational needs.