resource/models/resource_mixin.py

222 lines
9.7 KiB
Python
Raw Permalink Normal View History

2024-05-03 15:16:16 +03:00
# -*- 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