hr_leave.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. # -*- coding:utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from collections import defaultdict
  4. from datetime import datetime, date
  5. from odoo import api, fields, models, _
  6. from odoo.exceptions import ValidationError
  7. from odoo.osv.expression import AND
  8. from odoo.tools import format_date
  9. class HrLeaveType(models.Model):
  10. _inherit = 'hr.leave.type'
  11. work_entry_type_id = fields.Many2one('hr.work.entry.type', string='Work Entry Type')
  12. class HrLeave(models.Model):
  13. _inherit = 'hr.leave'
  14. def _create_resource_leave(self):
  15. """
  16. Add a resource leave in calendars of contracts running at the same period.
  17. This is needed in order to compute the correct number of hours/days of the leave
  18. according to the contract's calender.
  19. """
  20. resource_leaves = super(HrLeave, self)._create_resource_leave()
  21. for resource_leave in resource_leaves:
  22. resource_leave.work_entry_type_id = resource_leave.holiday_id.holiday_status_id.work_entry_type_id.id
  23. resource_leave_values = []
  24. for leave in self.filtered(lambda l: l.employee_id):
  25. contracts = leave.employee_id.sudo()._get_contracts(leave.date_from, leave.date_to, states=['open'])
  26. for contract in contracts:
  27. if contract and contract.resource_calendar_id != leave.employee_id.resource_calendar_id:
  28. resource_leave_values += [{
  29. 'name': _("%s: Time Off", leave.employee_id.name),
  30. 'holiday_id': leave.id,
  31. 'resource_id': leave.employee_id.resource_id.id,
  32. 'work_entry_type_id': leave.holiday_status_id.work_entry_type_id.id,
  33. 'time_type': leave.holiday_status_id.time_type,
  34. 'date_from': max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time())),
  35. 'date_to': min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time())),
  36. 'calendar_id': contract.resource_calendar_id.id,
  37. }]
  38. return resource_leaves | self.env['resource.calendar.leaves'].create(resource_leave_values)
  39. def _get_overlapping_contracts(self, contract_states=None):
  40. self.ensure_one()
  41. if contract_states is None:
  42. contract_states = [
  43. '|',
  44. ('state', 'not in', ['draft', 'cancel']),
  45. '&',
  46. ('state', '=', 'draft'),
  47. ('kanban_state', '=', 'done')
  48. ]
  49. domain = AND([contract_states, [
  50. ('employee_id', '=', self.employee_id.id),
  51. ('date_start', '<=', self.date_to),
  52. '|',
  53. ('date_end', '>=', self.date_from),
  54. '&',
  55. ('date_end', '=', False),
  56. ('state', '!=', 'close')
  57. ]])
  58. return self.env['hr.contract'].sudo().search(domain)
  59. @api.constrains('date_from', 'date_to')
  60. def _check_contracts(self):
  61. """
  62. A leave cannot be set across multiple contracts.
  63. Note: a leave can be across multiple contracts despite this constraint.
  64. It happens if a leave is correctly created (not across multiple contracts) but
  65. contracts are later modifed/created in the middle of the leave.
  66. """
  67. for holiday in self.filtered('employee_id'):
  68. contracts = holiday._get_overlapping_contracts()
  69. if len(contracts.resource_calendar_id) > 1:
  70. state_labels = {e[0]: e[1] for e in contracts._fields['state']._description_selection(self.env)}
  71. raise ValidationError(
  72. _("""A leave cannot be set across multiple contracts with different working schedules.
  73. Please create one time off for each contract.
  74. Time off:
  75. %s
  76. Contracts:
  77. %s""",
  78. holiday.display_name,
  79. '\n'.join(_(
  80. "Contract %s from %s to %s, status: %s",
  81. contract.name,
  82. format_date(self.env, contract.date_start),
  83. format_date(self.env, contract.date_start) if contract.date_end else _("undefined"),
  84. state_labels[contract.state]
  85. ) for contract in contracts)))
  86. def _cancel_work_entry_conflict(self):
  87. """
  88. Creates a leave work entry for each hr.leave in self.
  89. Check overlapping work entries with self.
  90. Work entries completely included in a leave are archived.
  91. e.g.:
  92. |----- work entry ----|---- work entry ----|
  93. |------------------- hr.leave ---------------|
  94. ||
  95. vv
  96. |----* work entry ****|
  97. |************ work entry leave --------------|
  98. """
  99. if not self:
  100. return
  101. # 1. Create a work entry for each leave
  102. work_entries_vals_list = []
  103. for leave in self:
  104. contracts = leave.employee_id.sudo()._get_contracts(leave.date_from, leave.date_to, states=['open', 'close'])
  105. for contract in contracts:
  106. # Generate only if it has aleady been generated
  107. if leave.date_to >= contract.date_generated_from and leave.date_from <= contract.date_generated_to:
  108. work_entries_vals_list += contracts._get_work_entries_values(leave.date_from, leave.date_to)
  109. new_leave_work_entries = self.env['hr.work.entry'].create(work_entries_vals_list)
  110. if new_leave_work_entries:
  111. # 2. Fetch overlapping work entries, grouped by employees
  112. start = min(self.mapped('date_from'), default=False)
  113. stop = max(self.mapped('date_to'), default=False)
  114. work_entry_groups = self.env['hr.work.entry']._read_group([
  115. ('date_start', '<', stop),
  116. ('date_stop', '>', start),
  117. ('employee_id', 'in', self.employee_id.ids),
  118. ], ['work_entry_ids:array_agg(id)', 'employee_id'], ['employee_id', 'date_start', 'date_stop'], lazy=False)
  119. work_entries_by_employee = defaultdict(lambda: self.env['hr.work.entry'])
  120. for group in work_entry_groups:
  121. employee_id = group.get('employee_id')[0]
  122. work_entries_by_employee[employee_id] |= self.env['hr.work.entry'].browse(group.get('work_entry_ids'))
  123. # 3. Archive work entries included in leaves
  124. included = self.env['hr.work.entry']
  125. overlappping = self.env['hr.work.entry']
  126. for work_entries in work_entries_by_employee.values():
  127. # Work entries for this employee
  128. new_employee_work_entries = work_entries & new_leave_work_entries
  129. previous_employee_work_entries = work_entries - new_leave_work_entries
  130. # Build intervals from work entries
  131. leave_intervals = new_employee_work_entries._to_intervals()
  132. conflicts_intervals = previous_employee_work_entries._to_intervals()
  133. # Compute intervals completely outside any leave
  134. # Intervals are outside, but associated records are overlapping.
  135. outside_intervals = conflicts_intervals - leave_intervals
  136. overlappping |= self.env['hr.work.entry']._from_intervals(outside_intervals)
  137. included |= previous_employee_work_entries - overlappping
  138. overlappping.write({'leave_id': False})
  139. included.write({'active': False})
  140. def write(self, vals):
  141. if not self:
  142. return True
  143. skip_check = not bool({'employee_id', 'state', 'date_from', 'date_to'} & vals.keys())
  144. start = min(self.mapped('date_from') + [fields.Datetime.from_string(vals.get('date_from', False)) or datetime.max])
  145. stop = max(self.mapped('date_to') + [fields.Datetime.from_string(vals.get('date_to', False)) or datetime.min])
  146. employee_ids = self.employee_id.ids
  147. if 'employee_id' in vals and vals['employee_id']:
  148. employee_ids += [vals['employee_id']]
  149. with self.env['hr.work.entry']._error_checking(start=start, stop=stop, skip=skip_check, employee_ids=employee_ids):
  150. return super().write(vals)
  151. @api.model_create_multi
  152. def create(self, vals_list):
  153. start_dates = [v.get('date_from') for v in vals_list if v.get('date_from')]
  154. stop_dates = [v.get('date_to') for v in vals_list if v.get('date_to')]
  155. if any(vals.get('holiday_type', 'employee') == 'employee' and not vals.get('multi_employee', False) and not vals.get('employee_id', False) for vals in vals_list):
  156. raise ValidationError(_("There is no employee set on the time off. Please make sure you're logged in the correct company."))
  157. employee_ids = {v['employee_id'] for v in vals_list if v.get('employee_id')}
  158. with self.env['hr.work.entry']._error_checking(start=min(start_dates, default=False), stop=max(stop_dates, default=False), employee_ids=employee_ids):
  159. return super().create(vals_list)
  160. def action_confirm(self):
  161. start = min(self.mapped('date_from'), default=False)
  162. stop = max(self.mapped('date_to'), default=False)
  163. with self.env['hr.work.entry']._error_checking(start=start, stop=stop, employee_ids=self.employee_id.ids):
  164. return super().action_confirm()
  165. def _get_leaves_on_public_holiday(self):
  166. return super()._get_leaves_on_public_holiday().filtered(
  167. lambda l: l.holiday_status_id.work_entry_type_id.code not in ['LEAVE110', 'LEAVE280'])
  168. def _validate_leave_request(self):
  169. super(HrLeave, self)._validate_leave_request()
  170. self.sudo()._cancel_work_entry_conflict() # delete preexisting conflicting work_entries
  171. return True
  172. def action_refuse(self):
  173. """
  174. Override to archive linked work entries and recreate attendance work entries
  175. where the refused leave was.
  176. """
  177. res = super(HrLeave, self).action_refuse()
  178. self._regen_work_entries()
  179. return res
  180. def _action_user_cancel(self, reason):
  181. res = super()._action_user_cancel(reason)
  182. self.sudo()._regen_work_entries()
  183. return res
  184. def _regen_work_entries(self):
  185. """
  186. Called when the leave is refused or cancelled to regenerate the work entries properly for that period.
  187. """
  188. work_entries = self.env['hr.work.entry'].sudo().search([('leave_id', 'in', self.ids)])
  189. work_entries.write({'active': False})
  190. # Re-create attendance work entries
  191. vals_list = []
  192. for work_entry in work_entries:
  193. vals_list += work_entry.contract_id._get_work_entries_values(work_entry.date_start, work_entry.date_stop)
  194. self.env['hr.work.entry'].create(vals_list)
  195. def _get_number_of_days(self, date_from, date_to, employee_id):
  196. """ If an employee is currently working full time but asks for time off next month
  197. where he has a new contract working only 3 days/week. This should be taken into
  198. account when computing the number of days for the leave (2 weeks leave = 6 days).
  199. Override this method to get number of days according to the contract's calendar
  200. at the time of the leave.
  201. """
  202. days = super(HrLeave, self)._get_number_of_days(date_from, date_to, employee_id)
  203. if employee_id:
  204. employee = self.env['hr.employee'].browse(employee_id)
  205. # Use sudo otherwise base users can't compute number of days
  206. contracts = employee.sudo()._get_contracts(date_from, date_to, states=['open', 'close'])
  207. contracts |= employee.sudo()._get_incoming_contracts(date_from, date_to)
  208. calendar = contracts[:1].resource_calendar_id if contracts else None # Note: if len(contracts)>1, the leave creation will crash because of unicity constaint
  209. # We force the company in the domain as we are more than likely in a compute_sudo
  210. domain = [('company_id', 'in', self.env.company.ids + self.env.context.get('allowed_company_ids', []))]
  211. result = employee._get_work_days_data_batch(date_from, date_to, calendar=calendar, domain=domain)[employee.id]
  212. if self.request_unit_half and result['hours'] > 0:
  213. result['days'] = 0.5
  214. return result
  215. return days
  216. def _get_calendar(self):
  217. self.ensure_one()
  218. if self.date_from and self.date_to:
  219. contracts = self.employee_id.sudo()._get_contracts(self.date_from, self.date_to, states=['open', 'close'])
  220. contracts |= self.employee_id.sudo()._get_incoming_contracts(self.date_from, self.date_to)
  221. contract_calendar = contracts[:1].resource_calendar_id if contracts else None
  222. return contract_calendar or self.employee_id.resource_calendar_id or self.env.company.resource_calendar_id
  223. return super()._get_calendar()
  224. def _compute_can_cancel(self):
  225. super()._compute_can_cancel()
  226. cancellable_leaves = self.filtered('can_cancel')
  227. work_entries = self.env['hr.work.entry'].sudo().search([('state', '=', 'validated'), ('leave_id', 'in', cancellable_leaves.ids)])
  228. leave_ids = work_entries.mapped('leave_id').ids
  229. for leave in cancellable_leaves:
  230. leave.can_cancel = leave.id not in leave_ids