2018-01-16 06:58:15 +01:00
# -*- coding: utf-8 -*-
2018-01-16 11:34:37 +01:00
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
2018-01-16 06:58:15 +01:00
import datetime
import math
import pytz
from collections import namedtuple
from datetime import timedelta
from dateutil import rrule
from dateutil . relativedelta import relativedelta
from operator import itemgetter
2018-01-16 11:34:37 +01:00
from flectra import api , fields , models , _
from flectra . addons . base . res . res_partner import _tz_get
from flectra . exceptions import ValidationError
from flectra . tools . float_utils import float_compare
2018-01-16 06:58:15 +01:00
def float_to_time ( float_hour ) :
2018-07-13 11:26:56 +02:00
if float_hour == 24.0 :
return datetime . time . max
2018-01-16 06:58:15 +01:00
return datetime . time ( int ( math . modf ( float_hour ) [ 1 ] ) , int ( 60 * math . modf ( float_hour ) [ 0 ] ) , 0 )
def to_naive_user_tz ( datetime , record ) :
tz_name = record . _context . get ( ' tz ' ) or record . env . user . tz
tz = tz_name and pytz . timezone ( tz_name ) or pytz . UTC
return pytz . UTC . localize ( datetime . replace ( tzinfo = None ) , is_dst = False ) . astimezone ( tz ) . replace ( tzinfo = None )
def to_naive_utc ( datetime , record ) :
tz_name = record . _context . get ( ' tz ' ) or record . env . user . tz
tz = tz_name and pytz . timezone ( tz_name ) or pytz . UTC
return tz . localize ( datetime . replace ( tzinfo = None ) , is_dst = False ) . astimezone ( pytz . UTC ) . replace ( tzinfo = None )
def to_tz ( datetime , tz_name ) :
2018-04-05 10:25:40 +02:00
tz = pytz . timezone ( tz_name ) if tz_name else pytz . UTC
2018-01-16 06:58:15 +01:00
return pytz . UTC . localize ( datetime . replace ( tzinfo = None ) , is_dst = False ) . astimezone ( tz ) . replace ( tzinfo = None )
class ResourceCalendar ( models . Model ) :
""" Calendar model for a resource. It has
- attendance_ids : list of resource . calendar . attendance that are a working
interval in a given weekday .
- leave_ids : list of leaves linked to this calendar . A leave can be general
or linked to a specific resource , depending on its resource_id .
All methods in this class use intervals . An interval is a tuple holding
( begin_datetime , end_datetime ) . A list of intervals is therefore a list of
tuples , holding several intervals of work or leaves . """
_name = " resource.calendar "
_description = " Resource Calendar "
_interval_obj = namedtuple ( ' Interval ' , ( ' start_datetime ' , ' end_datetime ' , ' data ' ) )
@api.model
def default_get ( self , fields ) :
res = super ( ResourceCalendar , self ) . default_get ( fields )
if not res . get ( ' name ' ) and res . get ( ' company_id ' ) :
res [ ' name ' ] = _ ( ' Working Hours of %s ' ) % self . env [ ' res.company ' ] . browse ( res [ ' company_id ' ] ) . name
return res
def _get_default_attendance_ids ( self ) :
return [
( 0 , 0 , { ' name ' : _ ( ' Monday Morning ' ) , ' dayofweek ' : ' 0 ' , ' hour_from ' : 8 , ' hour_to ' : 12 } ) ,
( 0 , 0 , { ' name ' : _ ( ' Monday Evening ' ) , ' dayofweek ' : ' 0 ' , ' hour_from ' : 13 , ' hour_to ' : 17 } ) ,
( 0 , 0 , { ' name ' : _ ( ' Tuesday Morning ' ) , ' dayofweek ' : ' 1 ' , ' hour_from ' : 8 , ' hour_to ' : 12 } ) ,
( 0 , 0 , { ' name ' : _ ( ' Tuesday Evening ' ) , ' dayofweek ' : ' 1 ' , ' hour_from ' : 13 , ' hour_to ' : 17 } ) ,
( 0 , 0 , { ' name ' : _ ( ' Wednesday Morning ' ) , ' dayofweek ' : ' 2 ' , ' hour_from ' : 8 , ' hour_to ' : 12 } ) ,
( 0 , 0 , { ' name ' : _ ( ' Wednesday Evening ' ) , ' dayofweek ' : ' 2 ' , ' hour_from ' : 13 , ' hour_to ' : 17 } ) ,
( 0 , 0 , { ' name ' : _ ( ' Thursday Morning ' ) , ' dayofweek ' : ' 3 ' , ' hour_from ' : 8 , ' hour_to ' : 12 } ) ,
( 0 , 0 , { ' name ' : _ ( ' Thursday Evening ' ) , ' dayofweek ' : ' 3 ' , ' hour_from ' : 13 , ' hour_to ' : 17 } ) ,
( 0 , 0 , { ' name ' : _ ( ' Friday Morning ' ) , ' dayofweek ' : ' 4 ' , ' hour_from ' : 8 , ' hour_to ' : 12 } ) ,
( 0 , 0 , { ' name ' : _ ( ' Friday Evening ' ) , ' dayofweek ' : ' 4 ' , ' hour_from ' : 13 , ' hour_to ' : 17 } )
]
name = fields . Char ( required = True )
company_id = fields . Many2one (
' res.company ' , ' Company ' ,
default = lambda self : self . env [ ' res.company ' ] . _company_default_get ( ) )
attendance_ids = fields . One2many (
' resource.calendar.attendance ' , ' calendar_id ' , ' Working Time ' ,
copy = True , default = _get_default_attendance_ids )
leave_ids = fields . One2many (
' resource.calendar.leaves ' , ' calendar_id ' , ' Leaves ' )
global_leave_ids = fields . One2many (
' resource.calendar.leaves ' , ' calendar_id ' , ' Global Leaves ' ,
domain = [ ( ' resource_id ' , ' = ' , False ) ]
)
# --------------------------------------------------
# Utility methods
# --------------------------------------------------
def _merge_kw ( self , kw , kw_ext ) :
new_kw = dict ( kw , * * kw_ext )
new_kw . update (
attendances = kw . get ( ' attendances ' , self . env [ ' resource.calendar.attendance ' ] ) | kw_ext . get ( ' attendances ' , self . env [ ' resource.calendar.attendance ' ] ) ,
leaves = kw . get ( ' leaves ' , self . env [ ' resource.calendar.leaves ' ] ) | kw_ext . get ( ' leaves ' , self . env [ ' resource.calendar.leaves ' ] )
)
return new_kw
def _interval_new ( self , start_datetime , end_datetime , kw = None ) :
kw = kw if kw is not None else dict ( )
kw . setdefault ( ' attendances ' , self . env [ ' resource.calendar.attendance ' ] )
kw . setdefault ( ' leaves ' , self . env [ ' resource.calendar.leaves ' ] )
return self . _interval_obj ( start_datetime , end_datetime , kw )
def _interval_exclude_left ( self , interval , interval_dst ) :
return self . _interval_obj (
interval . start_datetime > interval_dst . end_datetime and interval . start_datetime or interval_dst . end_datetime ,
interval . end_datetime ,
self . _merge_kw ( interval . data , interval_dst . data )
)
def _interval_exclude_right ( self , interval , interval_dst ) :
return self . _interval_obj (
interval . start_datetime ,
interval . end_datetime < interval_dst . start_datetime and interval . end_datetime or interval_dst . start_datetime ,
self . _merge_kw ( interval . data , interval_dst . data )
)
def _interval_or ( self , interval , interval_dst ) :
return self . _interval_obj (
interval . start_datetime < interval_dst . start_datetime and interval . start_datetime or interval_dst . start_datetime ,
interval . end_datetime > interval_dst . end_datetime and interval . end_datetime or interval_dst . end_datetime ,
self . _merge_kw ( interval . data , interval_dst . data )
)
def _interval_and ( self , interval , interval_dst ) :
2018-04-05 10:25:40 +02:00
if interval . start_datetime > interval_dst . end_datetime or interval . end_datetime < interval_dst . start_datetime :
return None
2018-01-16 06:58:15 +01:00
return self . _interval_obj (
interval . start_datetime > interval_dst . start_datetime and interval . start_datetime or interval_dst . start_datetime ,
interval . end_datetime < interval_dst . end_datetime and interval . end_datetime or interval_dst . end_datetime ,
self . _merge_kw ( interval . data , interval_dst . data )
)
def _interval_merge ( self , intervals ) :
""" Sort intervals based on starting datetime and merge overlapping intervals.
: return list cleaned : sorted intervals merged without overlap """
intervals = sorted ( intervals , key = itemgetter ( 0 ) ) # sort on first datetime
cleaned = [ ]
working_interval = None
while intervals :
current_interval = intervals . pop ( 0 )
if not working_interval : # init
working_interval = self . _interval_new ( * current_interval )
elif working_interval [ 1 ] < current_interval [ 0 ] : # interval is disjoint
cleaned . append ( working_interval )
working_interval = self . _interval_new ( * current_interval )
elif working_interval [ 1 ] < current_interval [ 1 ] : # union of greater intervals
working_interval = self . _interval_or ( working_interval , current_interval )
if working_interval : # handle void lists
cleaned . append ( working_interval )
return cleaned
@api.model
def _interval_remove_leaves ( self , interval , leave_intervals ) :
""" Remove leave intervals from a base interval
: param tuple interval : an interval ( see above ) that is the base interval
from which the leave intervals will be removed
: param list leave_intervals : leave intervals to remove
: return list intervals : ordered intervals with leaves removed """
intervals = [ ]
leave_intervals = self . _interval_merge ( leave_intervals )
current_interval = interval
for leave in leave_intervals :
# skip if ending before the current start datetime
if leave [ 1 ] < = current_interval [ 0 ] :
continue
# skip if starting after current end datetime; break as leaves are ordered and
# are therefore all out of range
if leave [ 0 ] > = current_interval [ 1 ] :
break
# begins within current interval: close current interval and begin a new one
# that begins at the leave end datetime
if current_interval [ 0 ] < leave [ 0 ] < current_interval [ 1 ] :
intervals . append ( self . _interval_exclude_right ( current_interval , leave ) )
current_interval = self . _interval_exclude_left ( interval , leave )
# ends within current interval: set current start datetime as leave end datetime
if current_interval [ 0 ] < = leave [ 1 ] :
current_interval = self . _interval_exclude_left ( interval , leave )
if current_interval and current_interval [ 0 ] < interval [ 1 ] : # remove intervals moved outside base interval due to leaves
intervals . append ( current_interval )
return intervals
@api.model
def _interval_schedule_hours ( self , intervals , hour , backwards = False ) :
""" Schedule hours in intervals. The last matching interval is truncated
to match the specified hours . This method can be applied backwards meaning
scheduling hours going in the past . In that case truncating last interval
is done accordingly . If number of hours to schedule is greater than possible
scheduling in the given intervals , returned result equals intervals .
: param list intervals : a list of time intervals
: param int / float hours : number of hours to schedule . It will be converted
into a timedelta , but should be submitted as an
int or float
: param boolean backwards : schedule starting from last hour
: return list results : a list of time intervals """
if backwards :
intervals . reverse ( ) # first interval is the last working interval of the day
results = [ ]
res = timedelta ( )
limit = timedelta ( hours = hour )
for interval in intervals :
res + = interval [ 1 ] - interval [ 0 ]
if res > limit and not backwards :
interval = ( interval [ 0 ] , interval [ 1 ] + relativedelta ( seconds = ( limit - res ) . total_seconds ( ) ) )
elif res > limit :
interval = ( interval [ 0 ] + relativedelta ( seconds = ( res - limit ) . total_seconds ( ) ) , interval [ 1 ] )
results . append ( interval )
if res > limit :
break
if backwards :
results . reverse ( ) # return interval with increasing starting times
return results
# --------------------------------------------------
# Date and hours computation
# --------------------------------------------------
@api.multi
def _get_day_attendances ( self , day_date , start_time , end_time ) :
""" Given a day date, return matching attendances. Those can be limited
by starting and ending time objects . """
self . ensure_one ( )
weekday = day_date . weekday ( )
attendances = self . env [ ' resource.calendar.attendance ' ]
for attendance in self . attendance_ids . filtered (
lambda att :
int ( att . dayofweek ) == weekday and
not ( att . date_from and fields . Date . from_string ( att . date_from ) > day_date ) and
not ( att . date_to and fields . Date . from_string ( att . date_to ) < day_date ) ) :
if start_time and float_to_time ( attendance . hour_to ) < start_time :
continue
if end_time and float_to_time ( attendance . hour_from ) > end_time :
continue
attendances | = attendance
return attendances
@api.multi
def _get_weekdays ( self ) :
""" Return the list of weekdays that contain at least one working
interval . """
self . ensure_one ( )
return list ( { int ( d ) for d in self . attendance_ids . mapped ( ' dayofweek ' ) } )
@api.multi
def _get_next_work_day ( self , day_date ) :
""" Get following date of day_date, based on resource.calendar. """
self . ensure_one ( )
weekdays = self . _get_weekdays ( )
weekday = next ( ( item for item in weekdays if item > day_date . weekday ( ) ) , weekdays [ 0 ] )
days = weekday - day_date . weekday ( )
if days < 0 :
days = 7 + days
return day_date + relativedelta ( days = days )
@api.multi
def _get_previous_work_day ( self , day_date ) :
""" Get previous date of day_date, based on resource.calendar. """
self . ensure_one ( )
weekdays = self . _get_weekdays ( )
weekdays . reverse ( )
weekday = next ( ( item for item in weekdays if item < day_date . weekday ( ) ) , weekdays [ 0 ] )
days = weekday - day_date . weekday ( )
if days > 0 :
days = days - 7
return day_date + relativedelta ( days = days )
@api.multi
def _get_leave_intervals ( self , resource_id = None , start_datetime = None , end_datetime = None ) :
""" Get the leaves of the calendar. Leaves can be filtered on the resource,
and on a start and end datetime .
Leaves are encoded from a given timezone given by their tz field . COnverting
them in naive user timezone require to use the leave timezone , not the current
user timezone . For example people managing leaves could be from different
timezones and the correct one is the one used when encoding them .
: return list leaves : list of time intervals """
self . ensure_one ( )
if resource_id :
domain = [ ' | ' , ( ' resource_id ' , ' = ' , resource_id ) , ( ' resource_id ' , ' = ' , False ) ]
else :
domain = [ ( ' resource_id ' , ' = ' , False ) ]
if start_datetime :
# domain += [('date_to', '>', fields.Datetime.to_string(to_naive_utc(start_datetime, self.env.user)))]
domain + = [ ( ' date_to ' , ' > ' , fields . Datetime . to_string ( start_datetime + timedelta ( days = - 1 ) ) ) ]
if end_datetime :
# domain += [('date_from', '<', fields.Datetime.to_string(to_naive_utc(end_datetime, self.env.user)))]
2018-04-05 10:25:40 +02:00
domain + = [ ( ' date_from ' , ' < ' , fields . Datetime . to_string ( end_datetime + timedelta ( days = 1 ) ) ) ]
2018-01-16 06:58:15 +01:00
leaves = self . env [ ' resource.calendar.leaves ' ] . search ( domain + [ ( ' calendar_id ' , ' = ' , self . id ) ] )
filtered_leaves = self . env [ ' resource.calendar.leaves ' ]
for leave in leaves :
if start_datetime :
leave_date_to = to_tz ( fields . Datetime . from_string ( leave . date_to ) , leave . tz )
if not leave_date_to > = start_datetime :
continue
if end_datetime :
leave_date_from = to_tz ( fields . Datetime . from_string ( leave . date_from ) , leave . tz )
if not leave_date_from < = end_datetime :
continue
filtered_leaves + = leave
return [ self . _interval_new (
to_tz ( fields . Datetime . from_string ( leave . date_from ) , leave . tz ) ,
to_tz ( fields . Datetime . from_string ( leave . date_to ) , leave . tz ) ,
{ ' leaves ' : leave } ) for leave in filtered_leaves ]
def _iter_day_attendance_intervals ( self , day_date , start_time , end_time ) :
""" Get an iterator of all interval of current day attendances. """
for calendar_working_day in self . _get_day_attendances ( day_date , start_time , end_time ) :
from_time = float_to_time ( calendar_working_day . hour_from )
to_time = float_to_time ( calendar_working_day . hour_to )
dt_f = datetime . datetime . combine ( day_date , max ( from_time , start_time ) )
dt_t = datetime . datetime . combine ( day_date , min ( to_time , end_time ) )
yield self . _interval_new ( dt_f , dt_t , { ' attendances ' : calendar_working_day } )
@api.multi
def _get_day_work_intervals ( self , day_date , start_time = None , end_time = None , compute_leaves = False , resource_id = None ) :
""" Get the working intervals of the day given by day_date based on
current calendar . Input should be given in current user timezone and
output is given in naive UTC , ready to be used by the orm or webclient .
: param time start_time : time object that is the beginning hours in user TZ
: param time end_time : time object that is the ending hours in user TZ
: param boolean compute_leaves : indicates whether to compute the
leaves based on calendar and resource .
: param int resource_id : the id of the resource to take into account when
computing the work intervals . Leaves notably are
filtered according to the resource .
: return list intervals : list of time intervals in UTC """
self . ensure_one ( )
if not start_time :
start_time = datetime . time . min
if not end_time :
end_time = datetime . time . max
working_intervals = [ att_interval for att_interval in self . _iter_day_attendance_intervals ( day_date , start_time , end_time ) ]
# filter according to leaves
if compute_leaves :
leaves = self . _get_leave_intervals (
resource_id = resource_id ,
start_datetime = datetime . datetime . combine ( day_date , start_time ) ,
end_datetime = datetime . datetime . combine ( day_date , end_time ) )
working_intervals = [
sub_interval
for interval in working_intervals
for sub_interval in self . _interval_remove_leaves ( interval , leaves ) ]
# adapt tz
return [ self . _interval_new (
to_naive_utc ( interval [ 0 ] , self . env . user ) ,
to_naive_utc ( interval [ 1 ] , self . env . user ) ,
interval [ 2 ] ) for interval in working_intervals ]
def _get_day_leave_intervals ( self , day_date , start_time , end_time , resource_id ) :
""" Get the leave intervals of the day given by day_date based on current
calendar . Input should be given in current user timezone and
output is given in naive UTC , ready to be used by the orm or webclient .
: param time start_time : time object that is the beginning hours in user TZ
: param time end_time : time object that is the ending hours in user TZ
: param int resource_id : the id of the resource to take into account when
computing the leaves .
: return list intervals : list of time intervals in UTC """
self . ensure_one ( )
if not start_time :
start_time = datetime . time . min
if not end_time :
end_time = datetime . time . max
working_intervals = [ att_interval for att_interval in self . _iter_day_attendance_intervals ( day_date , start_time , end_time ) ]
leaves_intervals = self . _get_leave_intervals (
resource_id = resource_id ,
start_datetime = datetime . datetime . combine ( day_date , start_time ) ,
end_datetime = datetime . datetime . combine ( day_date , end_time ) )
2018-04-05 10:25:40 +02:00
final_intervals = [ i for i in
[ self . _interval_and ( leave_interval , work_interval )
for leave_interval in leaves_intervals
for work_interval in working_intervals ] if i ]
2018-01-16 06:58:15 +01:00
# adapt tz
return [ self . _interval_new (
to_naive_utc ( interval [ 0 ] , self . env . user ) ,
to_naive_utc ( interval [ 1 ] , self . env . user ) ,
interval [ 2 ] ) for interval in final_intervals ]
# --------------------------------------------------
# Main computation API
# --------------------------------------------------
def _iter_work_intervals ( self , start_dt , end_dt , resource_id , compute_leaves = True ) :
""" Lists the current resource ' s work intervals between the two provided
datetimes ( inclusive ) expressed in UTC , for each worked day . """
if not end_dt :
end_dt = datetime . datetime . combine ( start_dt . date ( ) , datetime . time . max )
start_dt = to_naive_user_tz ( start_dt , self . env . user )
end_dt = to_naive_user_tz ( end_dt , self . env . user )
for day in rrule . rrule ( rrule . DAILY ,
dtstart = start_dt ,
until = end_dt ,
byweekday = self . _get_weekdays ( ) ) :
start_time = datetime . time . min
if day . date ( ) == start_dt . date ( ) :
start_time = start_dt . time ( )
end_time = datetime . time . max
if day . date ( ) == end_dt . date ( ) and end_dt . time ( ) != datetime . time ( ) :
end_time = end_dt . time ( )
intervals = self . _get_day_work_intervals (
day . date ( ) ,
start_time = start_time ,
end_time = end_time ,
compute_leaves = compute_leaves ,
resource_id = resource_id )
if intervals :
yield intervals
def _iter_leave_intervals ( self , start_dt , end_dt , resource_id ) :
""" Lists the current resource ' s leave intervals between the two provided
datetimes ( inclusive ) expressed in UTC . """
if not end_dt :
end_dt = datetime . datetime . combine ( start_dt . date ( ) , datetime . time . max )
start_dt = to_naive_user_tz ( start_dt , self . env . user )
end_dt = to_naive_user_tz ( end_dt , self . env . user )
for day in rrule . rrule ( rrule . DAILY ,
dtstart = start_dt ,
until = end_dt ,
byweekday = self . _get_weekdays ( ) ) :
start_time = datetime . time . min
if day . date ( ) == start_dt . date ( ) :
start_time = start_dt . time ( )
end_time = datetime . time . max
if day . date ( ) == end_dt . date ( ) and end_dt . time ( ) != datetime . time ( ) :
end_time = end_dt . time ( )
intervals = self . _get_day_leave_intervals (
day . date ( ) ,
start_time ,
end_time ,
resource_id )
if intervals :
yield intervals
def _iter_work_hours_count ( self , from_datetime , to_datetime , resource_id ) :
""" Lists the current resource ' s work hours count between the two provided
datetime expressed in naive UTC . """
for interval in self . _iter_work_intervals ( from_datetime , to_datetime , resource_id ) :
td = timedelta ( )
for work_interval in interval :
td + = work_interval [ 1 ] - work_interval [ 0 ]
yield ( interval [ 0 ] [ 0 ] . date ( ) , td . total_seconds ( ) / 3600.0 )
def _iter_work_days ( self , from_date , to_date , resource_id ) :
""" Lists the current resource ' s work days between the two provided
dates ( inclusive ) expressed in naive UTC .
Work days are the company or service ' s open days (as defined by the
resource . calendar ) minus the resource ' s own leaves.
: param datetime . date from_date : start of the interval to check for
work days ( inclusive )
: param datetime . date to_date : end of the interval to check for work
days ( inclusive )
: rtype : list ( datetime . date )
"""
for interval in self . _iter_work_intervals (
datetime . datetime ( from_date . year , from_date . month , from_date . day ) ,
datetime . datetime ( to_date . year , to_date . month , to_date . day ) ,
resource_id ) :
yield interval [ 0 ] [ 0 ] . date ( )
@api.multi
def _is_work_day ( self , date , resource_id ) :
""" Whether the provided date is a work day for the subject resource.
: type date : datetime . date
: rtype : bool """
return bool ( next ( self . _iter_work_days ( date , date , resource_id ) , False ) )
@api.multi
def get_work_hours_count ( self , start_dt , end_dt , resource_id , compute_leaves = True ) :
""" Count number of work hours between two datetimes. For compute_leaves,
resource_id : see _get_day_work_intervals . """
res = timedelta ( )
for intervals in self . _iter_work_intervals ( start_dt , end_dt , resource_id , compute_leaves = compute_leaves ) :
for interval in intervals :
res + = interval [ 1 ] - interval [ 0 ]
return res . total_seconds ( ) / 3600.0
# --------------------------------------------------
# Scheduling API
# --------------------------------------------------
@api.multi
def _schedule_hours ( self , hours , day_dt , compute_leaves = False , resource_id = None ) :
""" Schedule hours of work, using a calendar and an optional resource to
compute working and leave days . This method can be used backwards , i . e .
scheduling days before a deadline . For compute_leaves , resource_id :
see _get_day_work_intervals . This method does not use rrule because
rrule does not allow backwards computation .
: param int hours : number of hours to schedule . Use a negative number to
compute a backwards scheduling .
: param datetime day_dt : reference date to compute working days . If days is
> 0 date is the starting date . If days is < 0
date is the ending date .
: return list intervals : list of time intervals in naive UTC """
self . ensure_one ( )
backwards = ( hours < 0 )
intervals = [ ]
remaining_hours , iterations = abs ( hours * 1.0 ) , 0
day_dt_tz = to_naive_user_tz ( day_dt , self . env . user )
current_datetime = day_dt_tz
call_args = dict ( compute_leaves = compute_leaves , resource_id = resource_id )
while float_compare ( remaining_hours , 0.0 , precision_digits = 2 ) in ( 1 , 0 ) and iterations < 1000 :
if backwards :
call_args [ ' end_time ' ] = current_datetime . time ( )
else :
call_args [ ' start_time ' ] = current_datetime . time ( )
working_intervals = self . _get_day_work_intervals ( current_datetime . date ( ) , * * call_args )
if working_intervals :
new_working_intervals = self . _interval_schedule_hours ( working_intervals , remaining_hours , backwards = backwards )
res = timedelta ( )
for interval in working_intervals :
res + = interval [ 1 ] - interval [ 0 ]
remaining_hours - = res . total_seconds ( ) / 3600.0
intervals = intervals + new_working_intervals if not backwards else new_working_intervals + intervals
# get next day
if backwards :
current_datetime = datetime . datetime . combine ( self . _get_previous_work_day ( current_datetime ) , datetime . time ( 23 , 59 , 59 ) )
else :
current_datetime = datetime . datetime . combine ( self . _get_next_work_day ( current_datetime ) , datetime . time ( ) )
# avoid infinite loops
iterations + = 1
return intervals
@api.multi
def plan_hours ( self , hours , day_dt , compute_leaves = False , resource_id = None ) :
""" Return datetime after having planned hours """
res = self . _schedule_hours ( hours , day_dt , compute_leaves , resource_id )
if res and hours < 0.0 :
return res [ 0 ] [ 0 ]
elif res :
return res [ - 1 ] [ 1 ]
return False
@api.multi
def _schedule_days ( self , days , day_dt , compute_leaves = False , resource_id = None ) :
""" Schedule days of work, using a calendar and an optional resource to
compute working and leave days . This method can be used backwards , i . e .
scheduling days before a deadline . For compute_leaves , resource_id :
see _get_day_work_intervals . This method does not use rrule because
rrule does not allow backwards computation .
: param int days : number of days to schedule . Use a negative number to
compute a backwards scheduling .
: param date day_dt : reference datetime to compute working days . If days is > 0
date is the starting date . If days is < 0 date is the
ending date .
: return list intervals : list of time intervals in naive UTC """
backwards = ( days < 0 )
intervals = [ ]
planned_days , iterations = 0 , 0
day_dt_tz = to_naive_user_tz ( day_dt , self . env . user )
current_datetime = day_dt_tz . replace ( hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
while planned_days < abs ( days ) and iterations < 100 :
working_intervals = self . _get_day_work_intervals (
current_datetime . date ( ) ,
compute_leaves = compute_leaves , resource_id = resource_id )
if not self or working_intervals : # no calendar -> no working hours, but day is considered as worked
planned_days + = 1
intervals + = working_intervals
# get next day
if backwards :
current_datetime = self . _get_previous_work_day ( current_datetime )
else :
current_datetime = self . _get_next_work_day ( current_datetime )
# avoid infinite loops
iterations + = 1
return intervals
@api.multi
def plan_days ( self , days , day_dt , compute_leaves = False , resource_id = None ) :
""" Returns the datetime of a days scheduling. """
res = self . _schedule_days ( days , day_dt , compute_leaves , resource_id )
return res and res [ - 1 ] [ 1 ] or False
class ResourceCalendarAttendance ( models . Model ) :
_name = " resource.calendar.attendance "
_description = " Work Detail "
_order = ' dayofweek, hour_from '
name = fields . Char ( required = True )
dayofweek = fields . Selection ( [
( ' 0 ' , ' Monday ' ) ,
( ' 1 ' , ' Tuesday ' ) ,
( ' 2 ' , ' Wednesday ' ) ,
( ' 3 ' , ' Thursday ' ) ,
( ' 4 ' , ' Friday ' ) ,
( ' 5 ' , ' Saturday ' ) ,
( ' 6 ' , ' Sunday ' )
] , ' Day of Week ' , required = True , index = True , default = ' 0 ' )
date_from = fields . Date ( string = ' Starting Date ' )
date_to = fields . Date ( string = ' End Date ' )
2018-07-13 11:26:56 +02:00
hour_from = fields . Float ( string = ' Work from ' , required = True , index = True ,
help = " Start and End time of working. \n "
" A specific value of 24:00 is interpreted as 23:59:59.999999. " )
2018-01-16 06:58:15 +01:00
hour_to = fields . Float ( string = ' Work to ' , required = True )
calendar_id = fields . Many2one ( " resource.calendar " , string = " Resource ' s Calendar " , required = True , ondelete = ' cascade ' )
class ResourceResource ( models . Model ) :
_name = " resource.resource "
_description = " Resource Detail "
@api.model
def default_get ( self , fields ) :
res = super ( ResourceResource , self ) . default_get ( fields )
if not res . get ( ' calendar_id ' ) and res . get ( ' company_id ' ) :
company = self . env [ ' res.company ' ] . browse ( res [ ' company_id ' ] )
res [ ' calendar_id ' ] = company . resource_calendar_id . id
return res
name = fields . Char ( required = True )
active = fields . Boolean (
' Active ' , default = True , track_visibility = ' onchange ' ,
help = " If the active field is set to False, it will allow you to hide the resource record without removing it. " )
company_id = fields . Many2one ( ' res.company ' , string = ' Company ' , default = lambda self : self . env [ ' res.company ' ] . _company_default_get ( ) )
resource_type = fields . Selection ( [
( ' user ' , ' Human ' ) ,
( ' material ' , ' Material ' ) ] , string = ' Resource Type ' ,
default = ' user ' , required = True )
user_id = fields . Many2one ( ' res.users ' , string = ' User ' , help = ' Related user name for the resource to manage its access. ' )
time_efficiency = fields . Float (
' Efficiency Factor ' , default = 100 , required = True ,
help = " This field is used to calculate the the expected duration of a work order at this work center. For example, if a work order takes one hour and the efficiency factor is 100 % , then the expected duration will be one hour. If the efficiency factor is 200 % , however the expected duration will be 30 minutes. " )
calendar_id = fields . Many2one (
" resource.calendar " , string = ' Working Time ' ,
default = lambda self : self . env [ ' res.company ' ] . _company_default_get ( ) . resource_calendar_id ,
required = True ,
help = " Define the schedule of resource " )
_sql_constraints = [
( ' check_time_efficiency ' , ' CHECK(time_efficiency>0) ' , ' Time efficiency must be strictly positive ' ) ,
]
@api.multi
@api.constrains ( ' time_efficiency ' )
def _check_time_efficiency ( self ) :
for record in self :
if record . time_efficiency == 0 :
raise ValidationError ( _ ( ' The efficiency factor cannot be equal to 0. ' ) )
@api.model
def create ( self , values ) :
if values . get ( ' company_id ' ) and not values . get ( ' calendar_id ' ) :
values [ ' calendar_id ' ] = self . env [ ' res.company ' ] . browse ( values [ ' company_id ' ] ) . resource_calendar_id . id
return super ( ResourceResource , self ) . create ( values )
@api.multi
def copy ( self , default = None ) :
self . ensure_one ( )
if default is None :
default = { }
if not default . get ( ' name ' ) :
default . update ( name = _ ( ' %s (copy) ' ) % ( self . name ) )
return super ( ResourceResource , self ) . copy ( default )
@api.onchange ( ' company_id ' )
def _onchange_company_id ( self ) :
if self . company_id :
self . calendar_id = self . company_id . resource_calendar_id . id
class ResourceCalendarLeaves ( models . Model ) :
_name = " resource.calendar.leaves "
_description = " Leave Detail "
name = fields . Char ( ' Reason ' )
company_id = fields . Many2one (
' res.company ' , related = ' calendar_id.company_id ' , string = " Company " ,
readonly = True , store = True )
calendar_id = fields . Many2one ( ' resource.calendar ' , ' Working Hours ' )
date_from = fields . Datetime ( ' Start Date ' , required = True )
date_to = fields . Datetime ( ' End Date ' , required = True )
tz = fields . Selection (
2018-04-05 10:25:40 +02:00
_tz_get , string = ' Timezone ' , default = lambda self : self . _context . get ( ' tz ' ) or self . env . user . tz or ' UTC ' ,
2018-01-16 06:58:15 +01:00
help = " Timezone used when encoding the leave. It is used to correctly "
" localize leave hours when computing time intervals. " )
resource_id = fields . Many2one (
" resource.resource " , ' Resource ' ,
help = " If empty, this is a generic holiday for the company. If a resource is set, the holiday/leave is only for this resource " )
@api.constrains ( ' date_from ' , ' date_to ' )
def check_dates ( self ) :
if self . filtered ( lambda leave : leave . date_from > leave . date_to ) :
raise ValidationError ( _ ( ' Error! leave start-date must be lower then leave end-date. ' ) )
@api.onchange ( ' resource_id ' )
def onchange_resource ( self ) :
if self . resource_id :
self . calendar_id = self . resource_id . calendar_id