resource_calendar_leaves.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from collections import defaultdict
  4. from pytz import timezone, utc
  5. from odoo import api, fields, models, _
  6. class ResourceCalendarLeaves(models.Model):
  7. _inherit = "resource.calendar.leaves"
  8. timesheet_ids = fields.One2many('account.analytic.line', 'global_leave_id', string="Analytic Lines")
  9. def _work_time_per_day(self):
  10. """ Get work time per day based on the calendar and its attendances
  11. 1) Gets all calendars with their characteristics (i.e.
  12. (a) the leaves in it,
  13. (b) the resources which have a leave,
  14. (c) the oldest and
  15. (d) the latest leave dates
  16. ) for leaves in self.
  17. 2) Search the attendances based on the characteristics retrieved for each calendar.
  18. The attendances found are the ones between the date_from of the oldest leave
  19. and the date_to of the most recent leave.
  20. 3) Create a dict as result of this method containing:
  21. {
  22. leave: {
  23. max(date_start of work hours, date_start of the leave):
  24. the duration in days of the work including the leave
  25. }
  26. }
  27. """
  28. leaves_read_group = self.env['resource.calendar.leaves']._read_group(
  29. [('id', 'in', self.ids)],
  30. ['calendar_id', 'ids:array_agg(id)', 'resource_ids:array_agg(resource_id)', 'min_date_from:min(date_from)', 'max_date_to:max(date_to)'],
  31. ['calendar_id'],
  32. )
  33. # dict of keys: calendar_id
  34. # and values : { 'date_from': datetime, 'date_to': datetime, resources: self.env['resource.resource'] }
  35. cal_attendance_intervals_dict = {
  36. res['calendar_id'][0]: {
  37. 'date_from': utc.localize(res['min_date_from']),
  38. 'date_to': utc.localize(res['max_date_to']),
  39. 'resources': self.env['resource.resource'].browse(res['resource_ids'] if res['resource_ids'] and res['resource_ids'][0] else []),
  40. 'leaves': self.env['resource.calendar.leaves'].browse(res['ids']),
  41. } for res in leaves_read_group
  42. }
  43. # to easily find the calendar with its id.
  44. calendars_dict = {calendar.id: calendar for calendar in self.calendar_id}
  45. # dict of keys: leave.id
  46. # and values: a dict of keys: date
  47. # and values: number of days
  48. results = defaultdict(lambda: defaultdict(float))
  49. for calendar_id, cal_attendance_intervals_params_entry in cal_attendance_intervals_dict.items():
  50. calendar = calendars_dict[calendar_id]
  51. work_hours_intervals = calendar._attendance_intervals_batch(
  52. cal_attendance_intervals_params_entry['date_from'],
  53. cal_attendance_intervals_params_entry['date_to'],
  54. cal_attendance_intervals_params_entry['resources'],
  55. tz=timezone(calendar.tz)
  56. )
  57. for leave in cal_attendance_intervals_params_entry['leaves']:
  58. work_hours_data = work_hours_intervals[leave.resource_id.id]
  59. for date_from, date_to, dummy in work_hours_data:
  60. if date_to > utc.localize(leave.date_from) and date_from < utc.localize(leave.date_to):
  61. tmp_start = max(date_from, utc.localize(leave.date_from))
  62. tmp_end = min(date_to, utc.localize(leave.date_to))
  63. results[leave.id][tmp_start.date()] += (tmp_end - tmp_start).total_seconds() / 3600
  64. results[leave.id] = sorted(results[leave.id].items())
  65. return results
  66. def _timesheet_create_lines(self):
  67. """ Create timesheet leaves for each employee using the same calendar containing in self.calendar_id
  68. If the employee has already a time off in the same day then no timesheet should be created.
  69. """
  70. work_hours_data = self._work_time_per_day()
  71. employees_groups = self.env['hr.employee']._read_group(
  72. [('resource_calendar_id', 'in', self.calendar_id.ids)],
  73. ['resource_calendar_id', 'ids:array_agg(id)'],
  74. ['resource_calendar_id'])
  75. mapped_employee = {
  76. employee['resource_calendar_id'][0]: self.env['hr.employee'].browse(employee['ids'])
  77. for employee in employees_groups
  78. }
  79. employee_ids_set = set()
  80. employee_ids_set.update(*[line['ids'] for line in employees_groups])
  81. min_date = max_date = None
  82. for values in work_hours_data.values():
  83. for d, dummy in values:
  84. if not min_date and not max_date:
  85. min_date = max_date = d
  86. elif d < min_date:
  87. min_date = d
  88. elif d > max_date:
  89. max_date = d
  90. holidays_read_group = self.env['hr.leave']._read_group([
  91. ('employee_id', 'in', list(employee_ids_set)),
  92. ('date_from', '<=', max_date),
  93. ('date_to', '>=', min_date),
  94. ('state', '=', 'validate'),
  95. ], ['date_from_list:array_agg(date_from)', 'date_to_list:array_agg(date_to)', 'employee_id'], ['employee_id'])
  96. holidays_by_employee = {
  97. line['employee_id'][0]: [
  98. (date_from.date(), date_to.date()) for date_from, date_to in zip(line['date_from_list'], line['date_to_list'])
  99. ] for line in holidays_read_group
  100. }
  101. vals_list = []
  102. for leave in self:
  103. for employee in mapped_employee.get(leave.calendar_id.id, self.env['hr.employee']):
  104. holidays = holidays_by_employee.get(employee.id)
  105. work_hours_list = work_hours_data[leave.id]
  106. for index, (day_date, work_hours_count) in enumerate(work_hours_list):
  107. if not holidays or all(not (date_from <= day_date and date_to >= day_date) for date_from, date_to in holidays):
  108. vals_list.append(
  109. leave._timesheet_prepare_line_values(
  110. index,
  111. employee,
  112. work_hours_list,
  113. day_date,
  114. work_hours_count
  115. )
  116. )
  117. return self.env['account.analytic.line'].sudo().create(vals_list)
  118. def _timesheet_prepare_line_values(self, index, employee_id, work_hours_data, day_date, work_hours_count):
  119. self.ensure_one()
  120. return {
  121. 'name': _("Time Off (%s/%s)", index + 1, len(work_hours_data)),
  122. 'project_id': employee_id.company_id.internal_project_id.id,
  123. 'task_id': employee_id.company_id.leave_timesheet_task_id.id,
  124. 'account_id': employee_id.company_id.internal_project_id.analytic_account_id.id,
  125. 'unit_amount': work_hours_count,
  126. 'user_id': employee_id.user_id.id,
  127. 'date': day_date,
  128. 'global_leave_id': self.id,
  129. 'employee_id': employee_id.id,
  130. 'company_id': employee_id.company_id.id,
  131. }
  132. @api.model_create_multi
  133. def create(self, vals_list):
  134. results = super(ResourceCalendarLeaves, self).create(vals_list)
  135. results_with_leave_timesheet = results.filtered(lambda r: not r.resource_id.id and r.calendar_id.company_id.internal_project_id and r.calendar_id.company_id.leave_timesheet_task_id)
  136. results_with_leave_timesheet and results_with_leave_timesheet._timesheet_create_lines()
  137. return results
  138. def write(self, vals):
  139. date_from, date_to, calendar_id = vals.get('date_from'), vals.get('date_to'), vals.get('calendar_id')
  140. global_time_off_updated = self.env['resource.calendar.leaves']
  141. if date_from or date_to or 'calendar_id' in vals:
  142. global_time_off_updated = self.filtered(lambda r: (date_from is not None and r.date_from != date_from) or (date_to is not None and r.date_to != date_to) or (calendar_id is not None and r.calendar_id.id != calendar_id))
  143. timesheets = global_time_off_updated.sudo().timesheet_ids
  144. if timesheets:
  145. timesheets.write({'global_leave_id': False})
  146. timesheets.unlink()
  147. result = super(ResourceCalendarLeaves, self).write(vals)
  148. if global_time_off_updated:
  149. global_time_offs_with_leave_timesheet = global_time_off_updated.filtered(lambda r: not r.resource_id and r.calendar_id.company_id.internal_project_id and r.calendar_id.company_id.leave_timesheet_task_id)
  150. global_time_offs_with_leave_timesheet.sudo()._timesheet_create_lines()
  151. return result