hr_employee.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import datetime
  4. from dateutil.relativedelta import relativedelta
  5. from odoo import _, api, fields, models
  6. from odoo.exceptions import UserError
  7. from odoo.tools.float_utils import float_round
  8. from odoo.addons.resource.models.resource import HOURS_PER_DAY
  9. import pytz
  10. class HrEmployeeBase(models.AbstractModel):
  11. _inherit = "hr.employee.base"
  12. leave_manager_id = fields.Many2one(
  13. 'res.users', string='Time Off',
  14. compute='_compute_leave_manager', store=True, readonly=False,
  15. domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
  16. help='Select the user responsible for approving "Time Off" of this employee.\n'
  17. 'If empty, the approval is done by an Administrator or Approver (determined in settings/users).')
  18. remaining_leaves = fields.Float(
  19. compute='_compute_remaining_leaves', string='Remaining Paid Time Off',
  20. help='Total number of paid time off allocated to this employee, change this value to create allocation/time off request. '
  21. 'Total based on all the time off types without overriding limit.')
  22. current_leave_state = fields.Selection(compute='_compute_leave_status', string="Current Time Off Status",
  23. selection=[
  24. ('draft', 'New'),
  25. ('confirm', 'Waiting Approval'),
  26. ('refuse', 'Refused'),
  27. ('validate1', 'Waiting Second Approval'),
  28. ('validate', 'Approved'),
  29. ('cancel', 'Cancelled')
  30. ])
  31. leave_date_from = fields.Date('From Date', compute='_compute_leave_status')
  32. leave_date_to = fields.Date('To Date', compute='_compute_leave_status')
  33. leaves_count = fields.Float('Number of Time Off', compute='_compute_remaining_leaves')
  34. allocation_count = fields.Float('Total number of days allocated.', compute='_compute_allocation_count')
  35. allocations_count = fields.Integer('Total number of allocations', compute="_compute_allocation_count")
  36. show_leaves = fields.Boolean('Able to see Remaining Time Off', compute='_compute_show_leaves')
  37. is_absent = fields.Boolean('Absent Today', compute='_compute_leave_status', search='_search_absent_employee')
  38. allocation_display = fields.Char(compute='_compute_allocation_count')
  39. allocation_remaining_display = fields.Char(compute='_compute_allocation_remaining_display')
  40. hr_icon_display = fields.Selection(selection_add=[('presence_holiday_absent', 'On leave'),
  41. ('presence_holiday_present', 'Present but on leave')])
  42. def _get_remaining_leaves(self):
  43. """ Helper to compute the remaining leaves for the current employees
  44. :returns dict where the key is the employee id, and the value is the remain leaves
  45. """
  46. self._cr.execute("""
  47. SELECT
  48. sum(h.number_of_days) AS days,
  49. h.employee_id
  50. FROM
  51. (
  52. SELECT holiday_status_id, number_of_days,
  53. state, employee_id
  54. FROM hr_leave_allocation
  55. UNION ALL
  56. SELECT holiday_status_id, (number_of_days * -1) as number_of_days,
  57. state, employee_id
  58. FROM hr_leave
  59. ) h
  60. join hr_leave_type s ON (s.id=h.holiday_status_id)
  61. WHERE
  62. s.active = true AND h.state='validate' AND
  63. s.requires_allocation='yes' AND
  64. h.employee_id in %s
  65. GROUP BY h.employee_id""", (tuple(self.ids),))
  66. return dict((row['employee_id'], row['days']) for row in self._cr.dictfetchall())
  67. def _compute_remaining_leaves(self):
  68. remaining = {}
  69. if self.ids:
  70. remaining = self._get_remaining_leaves()
  71. for employee in self:
  72. value = float_round(remaining.get(employee.id, 0.0), precision_digits=2)
  73. employee.leaves_count = value
  74. employee.remaining_leaves = value
  75. def _compute_allocation_count(self):
  76. # Don't get allocations that are expired
  77. current_date = datetime.date.today()
  78. data = self.env['hr.leave.allocation']._read_group([
  79. ('employee_id', 'in', self.ids),
  80. ('holiday_status_id.active', '=', True),
  81. ('holiday_status_id.requires_allocation', '=', 'yes'),
  82. ('state', '=', 'validate'),
  83. ('date_from', '<=', current_date),
  84. '|',
  85. ('date_to', '=', False),
  86. ('date_to', '>=', current_date),
  87. ], ['number_of_days:sum', 'employee_id'], ['employee_id'])
  88. rg_results = dict((d['employee_id'][0], {"employee_id_count": d['employee_id_count'], "number_of_days": d['number_of_days']}) for d in data)
  89. for employee in self:
  90. result = rg_results.get(employee.id)
  91. employee.allocation_count = float_round(result['number_of_days'], precision_digits=2) if result else 0.0
  92. employee.allocation_display = "%g" % employee.allocation_count
  93. employee.allocations_count = result['employee_id_count'] if result else 0.0
  94. def _compute_allocation_remaining_display(self):
  95. allocations = self.env['hr.leave.allocation'].search([('employee_id', 'in', self.ids)])
  96. leaves_taken = allocations.holiday_status_id._get_employees_days_per_allocation(self.ids)
  97. for employee in self:
  98. employee_remaining_leaves = 0
  99. for leave_type in leaves_taken[employee.id]:
  100. if leave_type.requires_allocation == 'no':
  101. continue
  102. for allocation in leaves_taken[employee.id][leave_type]:
  103. if allocation:
  104. virtual_remaining_leaves = leaves_taken[employee.id][leave_type][allocation]['virtual_remaining_leaves']
  105. employee_remaining_leaves += virtual_remaining_leaves\
  106. if leave_type.request_unit in ['day', 'half_day']\
  107. else virtual_remaining_leaves / (employee.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
  108. employee.allocation_remaining_display = "%g" % float_round(employee_remaining_leaves, precision_digits=2)
  109. def _compute_presence_state(self):
  110. super()._compute_presence_state()
  111. employees = self.filtered(lambda employee: employee.hr_presence_state != 'present' and employee.is_absent)
  112. employees.update({'hr_presence_state': 'absent'})
  113. def _compute_presence_icon(self):
  114. super()._compute_presence_icon()
  115. employees_absent = self.filtered(lambda employee:
  116. employee.hr_presence_state != 'present'
  117. and employee.is_absent)
  118. employees_absent.update({'hr_icon_display': 'presence_holiday_absent'})
  119. employees_present = self.filtered(lambda employee:
  120. employee.hr_presence_state == 'present'
  121. and employee.is_absent)
  122. employees_present.update({'hr_icon_display': 'presence_holiday_present'})
  123. def _compute_leave_status(self):
  124. # Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule
  125. holidays = self.env['hr.leave'].sudo().search([
  126. ('employee_id', 'in', self.ids),
  127. ('date_from', '<=', fields.Datetime.now()),
  128. ('date_to', '>=', fields.Datetime.now()),
  129. ('state', '=', 'validate'),
  130. ])
  131. leave_data = {}
  132. for holiday in holidays:
  133. leave_data[holiday.employee_id.id] = {}
  134. leave_data[holiday.employee_id.id]['leave_date_from'] = holiday.date_from.date()
  135. leave_data[holiday.employee_id.id]['leave_date_to'] = holiday.date_to.date()
  136. leave_data[holiday.employee_id.id]['current_leave_state'] = holiday.state
  137. for employee in self:
  138. employee.leave_date_from = leave_data.get(employee.id, {}).get('leave_date_from')
  139. employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to')
  140. employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
  141. employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id, {}).get('current_leave_state') in ['validate']
  142. @api.depends('parent_id')
  143. def _compute_leave_manager(self):
  144. for employee in self:
  145. previous_manager = employee._origin.parent_id.user_id
  146. manager = employee.parent_id.user_id
  147. if manager and employee.leave_manager_id == previous_manager or not employee.leave_manager_id:
  148. employee.leave_manager_id = manager
  149. elif not employee.leave_manager_id:
  150. employee.leave_manager_id = False
  151. def _compute_show_leaves(self):
  152. show_leaves = self.env['res.users'].has_group('hr_holidays.group_hr_holidays_user')
  153. for employee in self:
  154. if show_leaves or employee.user_id == self.env.user:
  155. employee.show_leaves = True
  156. else:
  157. employee.show_leaves = False
  158. def _search_absent_employee(self, operator, value):
  159. if operator not in ('=', '!=') or not isinstance(value, bool):
  160. raise UserError(_('Operation not supported'))
  161. # This search is only used for the 'Absent Today' filter however
  162. # this only returns employees that are absent right now.
  163. today_date = datetime.datetime.utcnow().date()
  164. today_start = fields.Datetime.to_string(today_date)
  165. today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
  166. holidays = self.env['hr.leave'].sudo().search([
  167. ('employee_id', '!=', False),
  168. ('state', '=', 'validate'),
  169. ('date_from', '<=', today_end),
  170. ('date_to', '>=', today_start),
  171. ])
  172. operator = ['in', 'not in'][(operator == '=') != value]
  173. return [('id', operator, holidays.mapped('employee_id').ids)]
  174. @api.model_create_multi
  175. def create(self, vals_list):
  176. if self.env.context.get('salary_simulation'):
  177. return super().create(vals_list)
  178. approver_group = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
  179. group_updates = []
  180. for vals in vals_list:
  181. if 'parent_id' in vals:
  182. manager = self.env['hr.employee'].browse(vals['parent_id']).user_id
  183. vals['leave_manager_id'] = vals.get('leave_manager_id', manager.id)
  184. if approver_group and vals.get('leave_manager_id'):
  185. group_updates.append((4, vals['leave_manager_id']))
  186. if group_updates:
  187. approver_group.sudo().write({'users': group_updates})
  188. return super().create(vals_list)
  189. def write(self, values):
  190. if 'parent_id' in values:
  191. manager = self.env['hr.employee'].browse(values['parent_id']).user_id
  192. if manager:
  193. to_change = self.filtered(lambda e: e.leave_manager_id == e.parent_id.user_id or not e.leave_manager_id)
  194. to_change.write({'leave_manager_id': values.get('leave_manager_id', manager.id)})
  195. old_managers = self.env['res.users']
  196. if 'leave_manager_id' in values:
  197. old_managers = self.mapped('leave_manager_id')
  198. if values['leave_manager_id']:
  199. leave_manager = self.env['res.users'].browse(values['leave_manager_id'])
  200. old_managers -= leave_manager
  201. approver_group = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
  202. if approver_group and not leave_manager.has_group('hr_holidays.group_hr_holidays_responsible'):
  203. leave_manager.sudo().write({'groups_id': [(4, approver_group.id)]})
  204. res = super(HrEmployeeBase, self).write(values)
  205. # remove users from the Responsible group if they are no longer leave managers
  206. old_managers.sudo()._clean_leave_responsible_users()
  207. if 'parent_id' in values or 'department_id' in values:
  208. today_date = fields.Datetime.now()
  209. hr_vals = {}
  210. if values.get('parent_id') is not None:
  211. hr_vals['manager_id'] = values['parent_id']
  212. if values.get('department_id') is not None:
  213. hr_vals['department_id'] = values['department_id']
  214. holidays = self.env['hr.leave'].sudo().search(['|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
  215. holidays.write(hr_vals)
  216. allocations = self.env['hr.leave.allocation'].sudo().search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
  217. allocations.write(hr_vals)
  218. return res
  219. class HrEmployee(models.Model):
  220. _inherit = 'hr.employee'
  221. current_leave_id = fields.Many2one('hr.leave.type', compute='_compute_current_leave', string="Current Time Off Type",
  222. groups="hr.group_hr_user")
  223. def _compute_current_leave(self):
  224. self.current_leave_id = False
  225. holidays = self.env['hr.leave'].sudo().search([
  226. ('employee_id', 'in', self.ids),
  227. ('date_from', '<=', fields.Datetime.now()),
  228. ('date_to', '>=', fields.Datetime.now()),
  229. ('state', '=', 'validate'),
  230. ])
  231. for holiday in holidays:
  232. employee = self.filtered(lambda e: e.id == holiday.employee_id.id)
  233. employee.current_leave_id = holiday.holiday_status_id.id
  234. def _get_user_m2o_to_empty_on_archived_employees(self):
  235. return super()._get_user_m2o_to_empty_on_archived_employees() + ['leave_manager_id']
  236. def action_time_off_dashboard(self):
  237. return {
  238. 'name': _('Time Off Dashboard'),
  239. 'type': 'ir.actions.act_window',
  240. 'res_model': 'hr.leave',
  241. 'views': [[self.env.ref('hr_holidays.hr_leave_employee_view_dashboard').id, 'calendar']],
  242. 'domain': [('employee_id', 'in', self.ids)],
  243. 'context': {
  244. 'employee_id': self.ids,
  245. },
  246. }
  247. def _get_contextual_employee(self):
  248. if self.env.context.get('employee_id'):
  249. return self.browse(self.env.context['employee_id'])
  250. return self.env.user.employee_id
  251. def _is_leave_user(self):
  252. return self == self.env.user.employee_id and self.user_has_groups('hr_holidays.group_hr_holidays_user')
  253. def get_stress_days(self, start_date, end_date):
  254. all_days = {}
  255. self = self or self.env.user.employee_id
  256. stress_days = self._get_stress_days(start_date, end_date)
  257. for stress_day in stress_days:
  258. num_days = (stress_day.end_date - stress_day.start_date).days
  259. for d in range(num_days + 1):
  260. all_days[str(stress_day.start_date + relativedelta(days=d))] = stress_day.color
  261. return all_days
  262. @api.model
  263. def get_special_days_data(self, date_start, date_end):
  264. return {
  265. 'stressDays': self.get_stress_days_data(date_start, date_end),
  266. 'bankHolidays': self.get_public_holidays_data(date_start, date_end),
  267. }
  268. @api.model
  269. def get_public_holidays_data(self, date_start, date_end):
  270. self = self._get_contextual_employee()
  271. employee_tz = pytz.timezone(self._get_tz() if self else self.env.user.tz or 'utc')
  272. public_holidays = self._get_public_holidays(date_start, date_end).sorted('date_from')
  273. return list(map(lambda bh: {
  274. 'id': -bh.id,
  275. 'colorIndex': 0,
  276. 'end': datetime.datetime.combine(bh.date_to.astimezone(employee_tz), datetime.datetime.max.time()).isoformat(),
  277. 'endType': "datetime",
  278. 'isAllDay': True,
  279. 'start': datetime.datetime.combine(bh.date_from.astimezone(employee_tz), datetime.datetime.min.time()).isoformat(),
  280. 'startType': "datetime",
  281. 'title': bh.name,
  282. }, public_holidays))
  283. def _get_public_holidays(self, date_start, date_end):
  284. domain = [
  285. ('resource_id', '=', False),
  286. ('company_id', 'in', self.env.companies.ids),
  287. ('date_from', '<=', date_end),
  288. ('date_to', '>=', date_start),
  289. ]
  290. # a user with hr_holidays permissions will be able to see all public holidays from his calendar
  291. if not self._is_leave_user():
  292. domain += [
  293. '|',
  294. ('calendar_id', '=', False),
  295. ('calendar_id', '=', self.resource_calendar_id.id),
  296. ]
  297. return self.env['resource.calendar.leaves'].search(domain)
  298. @api.model
  299. def get_stress_days_data(self, date_start, date_end):
  300. self = self._get_contextual_employee()
  301. stress_days = self._get_stress_days(date_start, date_end).sorted('start_date')
  302. return list(map(lambda sd: {
  303. 'id': -sd.id,
  304. 'colorIndex': sd.color,
  305. 'end': datetime.datetime.combine(sd.end_date, datetime.datetime.max.time()).isoformat(),
  306. 'endType': "datetime",
  307. 'isAllDay': True,
  308. 'start': datetime.datetime.combine(sd.start_date, datetime.datetime.min.time()).isoformat(),
  309. 'startType': "datetime",
  310. 'title': sd.name,
  311. }, stress_days))
  312. def _get_stress_days(self, start_date, end_date):
  313. domain = [
  314. ('start_date', '<=', end_date),
  315. ('end_date', '>=', start_date),
  316. ('company_id', 'in', self.env.companies.ids),
  317. ]
  318. # a user with hr_holidays permissions will be able to see all stress days from his calendar
  319. if not self._is_leave_user():
  320. domain += [
  321. '|',
  322. ('resource_calendar_id', '=', False),
  323. ('resource_calendar_id', '=', self.resource_calendar_id.id),
  324. ]
  325. if self.department_id:
  326. domain += [
  327. '|',
  328. ('department_ids', '=', False),
  329. ('department_ids', 'parent_of', self.department_id.id),
  330. ]
  331. else:
  332. domain += [('department_ids', '=', False)]
  333. return self.env['hr.leave.stress.day'].search(domain)