222 lines
9.7 KiB
Python
222 lines
9.7 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from collections import defaultdict
|
||
|
from pytz import utc
|
||
|
|
||
|
from odoo import api, fields, models
|
||
|
from .utils import timezone_datetime
|
||
|
|
||
|
|
||
|
class ResourceMixin(models.AbstractModel):
|
||
|
_name = "resource.mixin"
|
||
|
_description = 'Resource Mixin'
|
||
|
|
||
|
resource_id = fields.Many2one(
|
||
|
'resource.resource', 'Resource',
|
||
|
auto_join=True, index=True, ondelete='restrict', required=True)
|
||
|
company_id = fields.Many2one(
|
||
|
'res.company', 'Company',
|
||
|
default=lambda self: self.env.company,
|
||
|
index=True, related='resource_id.company_id', precompute=True, store=True, readonly=False)
|
||
|
resource_calendar_id = fields.Many2one(
|
||
|
'resource.calendar', 'Working Hours',
|
||
|
default=lambda self: self.env.company.resource_calendar_id,
|
||
|
index=True, related='resource_id.calendar_id', store=True, readonly=False)
|
||
|
tz = fields.Selection(
|
||
|
string='Timezone', related='resource_id.tz', readonly=False,
|
||
|
help="This field is used in order to define in which timezone the resources will work.")
|
||
|
|
||
|
@api.model_create_multi
|
||
|
def create(self, vals_list):
|
||
|
resources_vals_list = []
|
||
|
calendar_ids = [vals['resource_calendar_id'] for vals in vals_list if vals.get('resource_calendar_id')]
|
||
|
calendars_tz = {calendar.id: calendar.tz for calendar in self.env['resource.calendar'].browse(calendar_ids)}
|
||
|
for vals in vals_list:
|
||
|
if not vals.get('resource_id'):
|
||
|
resources_vals_list.append(
|
||
|
self._prepare_resource_values(
|
||
|
vals,
|
||
|
vals.pop('tz', False) or calendars_tz.get(vals.get('resource_calendar_id'))
|
||
|
)
|
||
|
)
|
||
|
if resources_vals_list:
|
||
|
resources = self.env['resource.resource'].create(resources_vals_list)
|
||
|
resources_iter = iter(resources.ids)
|
||
|
for vals in vals_list:
|
||
|
if not vals.get('resource_id'):
|
||
|
vals['resource_id'] = next(resources_iter)
|
||
|
return super(ResourceMixin, self.with_context(check_idempotence=True)).create(vals_list)
|
||
|
|
||
|
def _prepare_resource_values(self, vals, tz):
|
||
|
resource_vals = {'name': vals.get(self._rec_name)}
|
||
|
if tz:
|
||
|
resource_vals['tz'] = tz
|
||
|
company_id = vals.get('company_id', self.env.company.id)
|
||
|
if company_id:
|
||
|
resource_vals['company_id'] = company_id
|
||
|
calendar_id = vals.get('resource_calendar_id')
|
||
|
if calendar_id:
|
||
|
resource_vals['calendar_id'] = calendar_id
|
||
|
return resource_vals
|
||
|
|
||
|
def copy_data(self, default=None):
|
||
|
if default is None:
|
||
|
default = {}
|
||
|
|
||
|
resource_default = {}
|
||
|
if 'company_id' in default:
|
||
|
resource_default['company_id'] = default['company_id']
|
||
|
if 'resource_calendar_id' in default:
|
||
|
resource_default['calendar_id'] = default['resource_calendar_id']
|
||
|
resource = self.resource_id.copy(resource_default)
|
||
|
|
||
|
default['resource_id'] = resource.id
|
||
|
default['company_id'] = resource.company_id.id
|
||
|
default['resource_calendar_id'] = resource.calendar_id.id
|
||
|
return super().copy_data(default)
|
||
|
|
||
|
def _get_calendar(self, date_from=None):
|
||
|
self.ensure_one()
|
||
|
return self.resource_calendar_id or self.company_id.resource_calendar_id
|
||
|
|
||
|
def _get_work_days_data_batch(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None):
|
||
|
"""
|
||
|
By default the resource calendar is used, but it can be
|
||
|
changed using the `calendar` argument.
|
||
|
|
||
|
`domain` is used in order to recognise the leaves to take,
|
||
|
None means default value ('time_type', '=', 'leave')
|
||
|
|
||
|
Returns a dict {'days': n, 'hours': h} containing the
|
||
|
quantity of working time expressed as days and as hours.
|
||
|
"""
|
||
|
resources = self.mapped('resource_id')
|
||
|
mapped_employees = {e.resource_id.id: e.id for e in self}
|
||
|
result = {}
|
||
|
|
||
|
# naive datetimes are made explicit in UTC
|
||
|
from_datetime = timezone_datetime(from_datetime)
|
||
|
to_datetime = timezone_datetime(to_datetime)
|
||
|
|
||
|
mapped_resources = defaultdict(lambda: self.env['resource.resource'])
|
||
|
for record in self:
|
||
|
mapped_resources[calendar or record._get_calendar(from_datetime)] |= record.resource_id
|
||
|
|
||
|
for calendar, calendar_resources in mapped_resources.items():
|
||
|
if not calendar:
|
||
|
for calendar_resource in calendar_resources:
|
||
|
result[calendar_resource.id] = {'days': 0, 'hours': 0}
|
||
|
continue
|
||
|
|
||
|
# actual hours per day
|
||
|
if compute_leaves:
|
||
|
intervals = calendar._work_intervals_batch(from_datetime, to_datetime, calendar_resources, domain)
|
||
|
else:
|
||
|
intervals = calendar._attendance_intervals_batch(from_datetime, to_datetime, calendar_resources)
|
||
|
|
||
|
for calendar_resource in calendar_resources:
|
||
|
result[calendar_resource.id] = calendar._get_attendance_intervals_days_data(intervals[calendar_resource.id])
|
||
|
|
||
|
# convert "resource: result" into "employee: result"
|
||
|
return {mapped_employees[r.id]: result[r.id] for r in resources}
|
||
|
|
||
|
def _get_leave_days_data_batch(self, from_datetime, to_datetime, calendar=None, domain=None):
|
||
|
"""
|
||
|
By default the resource calendar is used, but it can be
|
||
|
changed using the `calendar` argument.
|
||
|
|
||
|
`domain` is used in order to recognise the leaves to take,
|
||
|
None means default value ('time_type', '=', 'leave')
|
||
|
|
||
|
Returns a dict {'days': n, 'hours': h} containing the number of leaves
|
||
|
expressed as days and as hours.
|
||
|
"""
|
||
|
resources = self.mapped('resource_id')
|
||
|
mapped_employees = {e.resource_id.id: e.id for e in self}
|
||
|
result = {}
|
||
|
|
||
|
# naive datetimes are made explicit in UTC
|
||
|
from_datetime = timezone_datetime(from_datetime)
|
||
|
to_datetime = timezone_datetime(to_datetime)
|
||
|
|
||
|
mapped_resources = defaultdict(lambda: self.env['resource.resource'])
|
||
|
for record in self:
|
||
|
mapped_resources[calendar or record.resource_calendar_id] |= record.resource_id
|
||
|
|
||
|
for calendar, calendar_resources in mapped_resources.items():
|
||
|
# compute actual hours per day
|
||
|
attendances = calendar._attendance_intervals_batch(from_datetime, to_datetime, calendar_resources)
|
||
|
leaves = calendar._leave_intervals_batch(from_datetime, to_datetime, calendar_resources, domain)
|
||
|
|
||
|
for calendar_resource in calendar_resources:
|
||
|
result[calendar_resource.id] = calendar._get_attendance_intervals_days_data(
|
||
|
attendances[calendar_resource.id] & leaves[calendar_resource.id]
|
||
|
)
|
||
|
|
||
|
# convert "resource: result" into "employee: result"
|
||
|
return {mapped_employees[r.id]: result[r.id] for r in resources}
|
||
|
|
||
|
def _adjust_to_calendar(self, start, end):
|
||
|
resource_results = self.resource_id._adjust_to_calendar(start, end)
|
||
|
# change dict keys from resources to associated records.
|
||
|
return {
|
||
|
record: resource_results[record.resource_id]
|
||
|
for record in self
|
||
|
}
|
||
|
|
||
|
def list_work_time_per_day(self, from_datetime, to_datetime, calendar=None, domain=None):
|
||
|
"""
|
||
|
By default the resource calendar is used, but it can be
|
||
|
changed using the `calendar` argument.
|
||
|
|
||
|
`domain` is used in order to recognise the leaves to take,
|
||
|
None means default value ('time_type', '=', 'leave')
|
||
|
|
||
|
Returns a list of tuples (day, hours) for each day
|
||
|
containing at least an attendance.
|
||
|
"""
|
||
|
resource = self.resource_id
|
||
|
calendar = calendar or self.resource_calendar_id or self.company_id.resource_calendar_id
|
||
|
|
||
|
# naive datetimes are made explicit in UTC
|
||
|
if not from_datetime.tzinfo:
|
||
|
from_datetime = from_datetime.replace(tzinfo=utc)
|
||
|
if not to_datetime.tzinfo:
|
||
|
to_datetime = to_datetime.replace(tzinfo=utc)
|
||
|
|
||
|
compute_leaves = self.env.context.get('compute_leaves', True)
|
||
|
intervals = calendar._work_intervals_batch(from_datetime, to_datetime, resource, domain, compute_leaves=compute_leaves)[resource.id]
|
||
|
result = defaultdict(float)
|
||
|
for start, stop, meta in intervals:
|
||
|
result[start.date()] += (stop - start).total_seconds() / 3600
|
||
|
return sorted(result.items())
|
||
|
|
||
|
def list_leaves(self, from_datetime, to_datetime, calendar=None, domain=None):
|
||
|
"""
|
||
|
By default the resource calendar is used, but it can be
|
||
|
changed using the `calendar` argument.
|
||
|
|
||
|
`domain` is used in order to recognise the leaves to take,
|
||
|
None means default value ('time_type', '=', 'leave')
|
||
|
|
||
|
Returns a list of tuples (day, hours, resource.calendar.leaves)
|
||
|
for each leave in the calendar.
|
||
|
"""
|
||
|
resource = self.resource_id
|
||
|
calendar = calendar or self.resource_calendar_id
|
||
|
|
||
|
# naive datetimes are made explicit in UTC
|
||
|
if not from_datetime.tzinfo:
|
||
|
from_datetime = from_datetime.replace(tzinfo=utc)
|
||
|
if not to_datetime.tzinfo:
|
||
|
to_datetime = to_datetime.replace(tzinfo=utc)
|
||
|
|
||
|
attendances = calendar._attendance_intervals_batch(from_datetime, to_datetime, resource)[resource.id]
|
||
|
leaves = calendar._leave_intervals_batch(from_datetime, to_datetime, resource, domain)[resource.id]
|
||
|
result = []
|
||
|
for start, stop, leave in (leaves & attendances):
|
||
|
hours = (stop - start).total_seconds() / 3600
|
||
|
result.append((start.date(), hours, leave))
|
||
|
return result
|