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