|
| 1 | +# Part of Odoo. See LICENSE file for full copyright and licensing details. |
| 2 | + |
| 3 | +from pytz import UTC, timezone |
| 4 | +from datetime import datetime |
| 5 | +from collections import defaultdict |
| 6 | +from functools import reduce |
| 7 | + |
| 8 | +from odoo import api, models |
| 9 | + |
| 10 | +from odoo.osv import expression |
| 11 | +from odoo.addons.resource.models.utils import Intervals |
| 12 | + |
| 13 | + |
| 14 | +class Partner(models.Model): |
| 15 | + _inherit = ['res.partner'] |
| 16 | + |
| 17 | + def _get_employees_from_attendees(self, everybody=False): |
| 18 | + domain = [ |
| 19 | + ('company_id', 'in', self.env.companies.ids), |
| 20 | + ('work_contact_id', '!=', False), |
| 21 | + ] |
| 22 | + if not everybody: |
| 23 | + domain = expression.AND([ |
| 24 | + domain, |
| 25 | + [('work_contact_id', 'in', self.ids)] |
| 26 | + ]) |
| 27 | + return dict(self.env['hr.employee'].sudo()._read_group(domain, groupby=['work_contact_id'], aggregates=['id:recordset'])) |
| 28 | + |
| 29 | + def _get_schedule(self, start_period, stop_period, everybody=False, merge=True): |
| 30 | + """ |
| 31 | + This method implements the general case where employees might have different resource calendars at different |
| 32 | + times, even though this is not the case with only this module installed. |
| 33 | + This way it will work with these other modules by just overriding |
| 34 | + `_get_calendar_periods`. |
| 35 | +
|
| 36 | + :param datetime start_period: the start of the period |
| 37 | + :param datetime stop_period: the stop of the period |
| 38 | + :param boolean everybody: represents the "everybody" filter on calendar |
| 39 | + :param boolean merge: specifies if calendar's work_intervals needs to be merged |
| 40 | + :return: schedule (merged or not) by partner |
| 41 | + :rtype: defaultdict |
| 42 | + """ |
| 43 | + employees_by_partner = self._get_employees_from_attendees(everybody) |
| 44 | + if not employees_by_partner: |
| 45 | + return {} |
| 46 | + interval_by_calendar = defaultdict() |
| 47 | + calendar_periods_by_employee = defaultdict(list) |
| 48 | + employees_by_calendar = defaultdict(list) |
| 49 | + |
| 50 | + # Compute employee's calendars's period and order employee by his involved calendars |
| 51 | + employees = sum(employees_by_partner.values(), start=self.env['hr.employee']) |
| 52 | + calendar_periods_by_employee = employees._get_calendar_periods(start_period, stop_period) |
| 53 | + for employee, calendar_periods in calendar_periods_by_employee.items(): |
| 54 | + for (start, stop, calendar) in calendar_periods: |
| 55 | + employees_by_calendar[calendar].append(employee) |
| 56 | + |
| 57 | + # Compute all work intervals per calendar |
| 58 | + for calendar, employees in employees_by_calendar.items(): |
| 59 | + work_intervals = calendar._work_intervals_batch(start_period, stop_period, resources=employees, tz=timezone(calendar.tz)) |
| 60 | + del work_intervals[False] |
| 61 | + # Merge all employees intervals to avoid to compute it multiples times |
| 62 | + if merge: |
| 63 | + interval_by_calendar[calendar] = reduce(Intervals.__and__, work_intervals.values()) |
| 64 | + else: |
| 65 | + interval_by_calendar[calendar] = work_intervals |
| 66 | + |
| 67 | + # Compute employee's schedule based own his calendar's periods |
| 68 | + schedule_by_employee = defaultdict(list) |
| 69 | + for employee, calendar_periods in calendar_periods_by_employee.items(): |
| 70 | + employee_interval = Intervals([]) |
| 71 | + for (start, stop, calendar) in calendar_periods: |
| 72 | + interval = Intervals([(start, stop, self.env['resource.calendar'])]) |
| 73 | + if merge: |
| 74 | + calendar_interval = interval_by_calendar[calendar] |
| 75 | + else: |
| 76 | + calendar_interval = interval_by_calendar[calendar][employee.id] |
| 77 | + employee_interval = employee_interval | (calendar_interval & interval) |
| 78 | + schedule_by_employee[employee] = employee_interval |
| 79 | + |
| 80 | + # Compute partner's schedule equals to the union between his employees's schedule |
| 81 | + schedules = defaultdict() |
| 82 | + for partner, employees in employees_by_partner.items(): |
| 83 | + partner_schedule = Intervals([]) |
| 84 | + for employee in employees: |
| 85 | + if schedule_by_employee[employee]: |
| 86 | + partner_schedule = partner_schedule | schedule_by_employee[employee] |
| 87 | + schedules[partner] = partner_schedule |
| 88 | + return schedules |
| 89 | + |
| 90 | + @api.model |
| 91 | + def get_working_hours_for_all_attendees(self, attendee_ids, date_from, date_to, everybody=False): |
| 92 | + |
| 93 | + start_period = datetime.fromisoformat(date_from).replace(hour=0, minute=0, second=0, tzinfo=UTC) |
| 94 | + stop_period = datetime.fromisoformat(date_to).replace(hour=23, minute=59, second=59, tzinfo=UTC) |
| 95 | + |
| 96 | + schedule_by_partner = self.env['res.partner'].browse(attendee_ids)._get_schedule(start_period, stop_period, everybody) |
| 97 | + if not schedule_by_partner: |
| 98 | + return [] |
| 99 | + return self._interval_to_business_hours(reduce(Intervals.__and__, schedule_by_partner.values())) |
| 100 | + |
| 101 | + def _interval_to_business_hours(self, working_intervals): |
| 102 | + # This is the format expected by the fullcalendar library to do the overlay |
| 103 | + return [{ |
| 104 | + "daysOfWeek": [interval[0].weekday() + 1], |
| 105 | + "startTime": interval[0].astimezone(timezone(self.env.user.tz)).strftime("%H:%M"), |
| 106 | + "endTime": interval[1].astimezone(timezone(self.env.user.tz)).strftime("%H:%M"), |
| 107 | + } for interval in working_intervals] if working_intervals else [{ |
| 108 | + # 7 is used a dummy value to gray the full week |
| 109 | + # Returning an empty list would leave the week uncolored |
| 110 | + "daysOfWeek": [7], |
| 111 | + "startTime": datetime.today().strftime("00:00"), |
| 112 | + "endTime": datetime.today().strftime("00:00"), |
| 113 | + }] |
0 commit comments