1# -*- coding: utf-8 -*- 2# Part of Odoo. See LICENSE file for full copyright and licensing details. 3 4from collections import defaultdict 5from dateutil.relativedelta import relativedelta 6from pytz import utc 7 8from odoo import api, fields, models 9 10 11def timezone_datetime(time): 12 if not time.tzinfo: 13 time = time.replace(tzinfo=utc) 14 return time 15 16 17class ResourceMixin(models.AbstractModel): 18 _name = "resource.mixin" 19 _description = 'Resource Mixin' 20 21 resource_id = fields.Many2one( 22 'resource.resource', 'Resource', 23 auto_join=True, index=True, ondelete='restrict', required=True) 24 company_id = fields.Many2one( 25 'res.company', 'Company', 26 default=lambda self: self.env.company, 27 index=True, related='resource_id.company_id', store=True, readonly=False) 28 resource_calendar_id = fields.Many2one( 29 'resource.calendar', 'Working Hours', 30 default=lambda self: self.env.company.resource_calendar_id, 31 index=True, related='resource_id.calendar_id', store=True, readonly=False) 32 tz = fields.Selection( 33 string='Timezone', related='resource_id.tz', readonly=False, 34 help="This field is used in order to define in which timezone the resources will work.") 35 36 @api.model 37 def create(self, values): 38 if not values.get('resource_id'): 39 resource_vals = {'name': values.get(self._rec_name)} 40 tz = (values.pop('tz', False) or 41 self.env['resource.calendar'].browse(values.get('resource_calendar_id')).tz) 42 if tz: 43 resource_vals['tz'] = tz 44 resource = self.env['resource.resource'].create(resource_vals) 45 values['resource_id'] = resource.id 46 return super(ResourceMixin, self).create(values) 47 48 def copy_data(self, default=None): 49 if default is None: 50 default = {} 51 resource = self.resource_id.copy() 52 default['resource_id'] = resource.id 53 default['company_id'] = resource.company_id.id 54 default['resource_calendar_id'] = resource.calendar_id.id 55 return super(ResourceMixin, self).copy_data(default) 56 57 # YTI TODO: Remove me in master 58 def _get_work_days_data(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None): 59 self.ensure_one() 60 return self._get_work_days_data_batch( 61 from_datetime, 62 to_datetime, 63 compute_leaves=compute_leaves, 64 calendar=calendar, 65 domain=domain 66 )[self.id] 67 68 def _get_work_days_data_batch(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None): 69 """ 70 By default the resource calendar is used, but it can be 71 changed using the `calendar` argument. 72 73 `domain` is used in order to recognise the leaves to take, 74 None means default value ('time_type', '=', 'leave') 75 76 Returns a dict {'days': n, 'hours': h} containing the 77 quantity of working time expressed as days and as hours. 78 """ 79 resources = self.mapped('resource_id') 80 mapped_employees = {e.resource_id.id: e.id for e in self} 81 result = {} 82 83 # naive datetimes are made explicit in UTC 84 from_datetime = timezone_datetime(from_datetime) 85 to_datetime = timezone_datetime(to_datetime) 86 87 mapped_resources = defaultdict(lambda: self.env['resource.resource']) 88 for record in self: 89 mapped_resources[calendar or record.resource_calendar_id] |= record.resource_id 90 91 for calendar, calendar_resources in mapped_resources.items(): 92 if not calendar: 93 for calendar_resource in calendar_resources: 94 result[calendar_resource.id] = {'days': 0, 'hours': 0} 95 continue 96 day_total = calendar._get_resources_day_total(from_datetime, to_datetime, calendar_resources) 97 98 # actual hours per day 99 if compute_leaves: 100 intervals = calendar._work_intervals_batch(from_datetime, to_datetime, calendar_resources, domain) 101 else: 102 intervals = calendar._attendance_intervals_batch(from_datetime, to_datetime, calendar_resources) 103 104 for calendar_resource in calendar_resources: 105 result[calendar_resource.id] = calendar._get_days_data(intervals[calendar_resource.id], day_total[calendar_resource.id]) 106 107 # convert "resource: result" into "employee: result" 108 return {mapped_employees[r.id]: result[r.id] for r in resources} 109 110 # YTI TODO: Remove me in master 111 def _get_leave_days_data(self, from_datetime, to_datetime, calendar=None, domain=None): 112 self.ensure_one() 113 return self._get_leave_days_data_batch( 114 from_datetime, 115 to_datetime, 116 calendar=calendar, 117 domain=domain 118 )[self.id] 119 120 def _get_leave_days_data_batch(self, from_datetime, to_datetime, calendar=None, domain=None): 121 """ 122 By default the resource calendar is used, but it can be 123 changed using the `calendar` argument. 124 125 `domain` is used in order to recognise the leaves to take, 126 None means default value ('time_type', '=', 'leave') 127 128 Returns a dict {'days': n, 'hours': h} containing the number of leaves 129 expressed as days and as hours. 130 """ 131 resources = self.mapped('resource_id') 132 mapped_employees = {e.resource_id.id: e.id for e in self} 133 result = {} 134 135 # naive datetimes are made explicit in UTC 136 from_datetime = timezone_datetime(from_datetime) 137 to_datetime = timezone_datetime(to_datetime) 138 139 mapped_resources = defaultdict(lambda: self.env['resource.resource']) 140 for record in self: 141 mapped_resources[calendar or record.resource_calendar_id] |= record.resource_id 142 143 for calendar, calendar_resources in mapped_resources.items(): 144 day_total = calendar._get_resources_day_total(from_datetime, to_datetime, calendar_resources) 145 146 # compute actual hours per day 147 attendances = calendar._attendance_intervals_batch(from_datetime, to_datetime, calendar_resources) 148 leaves = calendar._leave_intervals_batch(from_datetime, to_datetime, calendar_resources, domain) 149 150 for calendar_resource in calendar_resources: 151 result[calendar_resource.id] = calendar._get_days_data( 152 attendances[calendar_resource.id] & leaves[calendar_resource.id], 153 day_total[calendar_resource.id] 154 ) 155 156 # convert "resource: result" into "employee: result" 157 return {mapped_employees[r.id]: result[r.id] for r in resources} 158 159 def _adjust_to_calendar(self, start, end): 160 resource_results = self.resource_id._adjust_to_calendar(start, end) 161 # change dict keys from resources to associated records. 162 return { 163 record: resource_results[record.resource_id] 164 for record in self 165 } 166 167 def list_work_time_per_day(self, from_datetime, to_datetime, calendar=None, domain=None): 168 """ 169 By default the resource calendar is used, but it can be 170 changed using the `calendar` argument. 171 172 `domain` is used in order to recognise the leaves to take, 173 None means default value ('time_type', '=', 'leave') 174 175 Returns a list of tuples (day, hours) for each day 176 containing at least an attendance. 177 """ 178 resource = self.resource_id 179 calendar = calendar or self.resource_calendar_id 180 181 # naive datetimes are made explicit in UTC 182 if not from_datetime.tzinfo: 183 from_datetime = from_datetime.replace(tzinfo=utc) 184 if not to_datetime.tzinfo: 185 to_datetime = to_datetime.replace(tzinfo=utc) 186 187 intervals = calendar._work_intervals_batch(from_datetime, to_datetime, resource, domain)[resource.id] 188 result = defaultdict(float) 189 for start, stop, meta in intervals: 190 result[start.date()] += (stop - start).total_seconds() / 3600 191 return sorted(result.items()) 192 193 def list_leaves(self, from_datetime, to_datetime, calendar=None, domain=None): 194 """ 195 By default the resource calendar is used, but it can be 196 changed using the `calendar` argument. 197 198 `domain` is used in order to recognise the leaves to take, 199 None means default value ('time_type', '=', 'leave') 200 201 Returns a list of tuples (day, hours, resource.calendar.leaves) 202 for each leave in the calendar. 203 """ 204 resource = self.resource_id 205 calendar = calendar or self.resource_calendar_id 206 207 # naive datetimes are made explicit in UTC 208 if not from_datetime.tzinfo: 209 from_datetime = from_datetime.replace(tzinfo=utc) 210 if not to_datetime.tzinfo: 211 to_datetime = to_datetime.replace(tzinfo=utc) 212 213 attendances = calendar._attendance_intervals_batch(from_datetime, to_datetime, resource)[resource.id] 214 leaves = calendar._leave_intervals_batch(from_datetime, to_datetime, resource, domain)[resource.id] 215 result = [] 216 for start, stop, leave in (leaves & attendances): 217 hours = (stop - start).total_seconds() / 3600 218 result.append((start.date(), hours, leave)) 219 return result 220