452 lines
22 KiB
Python
452 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# OpenERP, Open Source Management Solution
|
|
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
|
|
from datetime import datetime, timedelta
|
|
import math
|
|
from faces import *
|
|
from osv import fields, osv
|
|
from tools.translate import _
|
|
|
|
from itertools import groupby
|
|
from operator import itemgetter
|
|
|
|
|
|
class resource_calendar(osv.osv):
|
|
_name = "resource.calendar"
|
|
_description = "Resource Calendar"
|
|
_columns = {
|
|
'name' : fields.char("Name", size=64, required=True),
|
|
'company_id' : fields.many2one('res.company', 'Company', required=False),
|
|
'attendance_ids' : fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time'),
|
|
'manager' : fields.many2one('res.users', 'Workgroup Manager'),
|
|
}
|
|
_defaults = {
|
|
'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.calendar', context=context)
|
|
}
|
|
|
|
def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None):
|
|
"""Calculates the Working Total Hours based on Resource Calendar and
|
|
given working day (datetime object).
|
|
|
|
@param resource_calendar_id: resource.calendar browse record
|
|
@param day: datetime object
|
|
|
|
@return: returns the working hours (as float) men should work on the given day if is in the attendance_ids of the resource_calendar_id (i.e if that day is a working day), returns 0.0 otherwise
|
|
"""
|
|
res = 0.0
|
|
for working_day in resource_calendar_id.attendance_ids:
|
|
if (int(working_day.dayofweek) + 1) == day.isoweekday():
|
|
res += working_day.hour_to - working_day.hour_from
|
|
return res
|
|
|
|
def _get_leaves(self, cr, uid, id, resource):
|
|
"""Private Method to Calculate resource Leaves days
|
|
|
|
@param id: resource calendar id
|
|
@param resource: resource id for which leaves will ew calculated
|
|
|
|
@return : returns the list of dates, where resource on leave in
|
|
resource.calendar.leaves object (e.g.['%Y-%m-%d', '%Y-%m-%d'])
|
|
"""
|
|
resource_cal_leaves = self.pool.get('resource.calendar.leaves')
|
|
dt_leave = []
|
|
resource_leave_ids = resource_cal_leaves.search(cr, uid, [('calendar_id','=',id), '|', ('resource_id','=',False), ('resource_id','=',resource)])
|
|
#res_leaves = resource_cal_leaves.read(cr, uid, resource_leave_ids, ['date_from', 'date_to'])
|
|
res_leaves = resource_cal_leaves.browse(cr, uid, resource_leave_ids)
|
|
|
|
for leave in res_leaves:
|
|
dtf = datetime.strptime(leave.date_from, '%Y-%m-%d %H:%M:%S')
|
|
dtt = datetime.strptime(leave.date_to, '%Y-%m-%d %H:%M:%S')
|
|
no = dtt - dtf
|
|
[dt_leave.append((dtf + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
|
|
dt_leave.sort()
|
|
|
|
return dt_leave
|
|
|
|
def interval_min_get(self, cr, uid, id, dt_from, hours, resource=False):
|
|
"""
|
|
Calculates the working Schedule from supplied from date to till hours
|
|
will be satisfied based or resource calendar id. If resource is also
|
|
given then it will consider the resource leave also and than will
|
|
calculates resource working schedule
|
|
|
|
@param dt_from: datetime object, start of working scheduled
|
|
@param hours: float, total number working hours needed scheduled from
|
|
start date
|
|
@param resource : Optional Resource id, if supplied than resource leaves
|
|
will also taken into consideration for calculating working
|
|
schedule.
|
|
@return : List datetime object of working schedule based on supplies
|
|
params
|
|
"""
|
|
if not id:
|
|
td = int(hours)*3
|
|
return [(dt_from - timedelta(hours=td), dt_from)]
|
|
dt_leave = self._get_leaves(cr, uid, id, resource)
|
|
dt_leave.reverse()
|
|
todo = hours
|
|
result = []
|
|
maxrecur = 100
|
|
current_hour = dt_from.hour
|
|
while (todo>0) and maxrecur:
|
|
cr.execute("select hour_from,hour_to from resource_calendar_attendance where dayofweek='%s' and calendar_id=%s order by hour_from desc", (dt_from.weekday(),id))
|
|
for (hour_from,hour_to) in cr.fetchall():
|
|
leave_flag = False
|
|
if (hour_from<current_hour) and (todo>0):
|
|
m = min(hour_to, current_hour)
|
|
if (m-hour_from)>todo:
|
|
hour_from = m-todo
|
|
dt_check = dt_from.strftime('%Y-%m-%d')
|
|
for leave in dt_leave:
|
|
if dt_check == leave:
|
|
dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
|
|
leave_flag = True
|
|
if leave_flag:
|
|
break
|
|
else:
|
|
d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_from)), int((hour_from%1) * 60))
|
|
d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60))
|
|
result.append((d1, d2))
|
|
current_hour = hour_from
|
|
todo -= (m-hour_from)
|
|
dt_from -= timedelta(days=1)
|
|
current_hour = 24
|
|
maxrecur -= 1
|
|
result.reverse()
|
|
return result
|
|
|
|
# def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
|
|
def interval_get_multi(self, cr, uid, date_and_hours_by_cal, resource=False, byday=True):
|
|
def group(lst, key):
|
|
lst.sort(key=itemgetter(key))
|
|
grouped = groupby(lst, itemgetter(key))
|
|
return dict([(k, [v for v in itr]) for k, itr in grouped])
|
|
# END group
|
|
|
|
cr.execute("select calendar_id, dayofweek, hour_from, hour_to from resource_calendar_attendance order by hour_from")
|
|
hour_res = cr.dictfetchall()
|
|
hours_by_cal = group(hour_res, 'calendar_id')
|
|
|
|
results = {}
|
|
|
|
for d, hours, id in date_and_hours_by_cal:
|
|
dt_from = datetime.strptime(d, '%Y-%m-%d %H:%M:%S')
|
|
if not id:
|
|
td = int(hours)*3
|
|
results[(d, hours, id)] = [(dt_from, dt_from + timedelta(hours=td))]
|
|
continue
|
|
|
|
dt_leave = self._get_leaves(cr, uid, id, resource)
|
|
todo = hours
|
|
result = []
|
|
maxrecur = 100
|
|
current_hour = dt_from.hour
|
|
while (todo>0) and maxrecur:
|
|
for (hour_from,hour_to) in [(item['hour_from'], item['hour_to']) for item in hours_by_cal[id] if item['dayofweek'] == str(dt_from.weekday())]:
|
|
leave_flag = False
|
|
if (hour_to>current_hour) and (todo>0):
|
|
m = max(hour_from, current_hour)
|
|
if (hour_to-m)>todo:
|
|
hour_to = m+todo
|
|
dt_check = dt_from.strftime('%Y-%m-%d')
|
|
for leave in dt_leave:
|
|
if dt_check == leave:
|
|
dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
|
|
leave_flag = True
|
|
if leave_flag:
|
|
break
|
|
else:
|
|
d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60))
|
|
d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_to)), int((hour_to%1) * 60))
|
|
result.append((d1, d2))
|
|
current_hour = hour_to
|
|
todo -= (hour_to - m)
|
|
dt_from += timedelta(days=1)
|
|
current_hour = 0
|
|
maxrecur -= 1
|
|
results[(d, hours, id)] = result
|
|
return results
|
|
|
|
def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
|
|
"""Calculates Resource Working Internal Timing Based on Resource Calendar.
|
|
|
|
@param dt_from: start resource schedule calculation.
|
|
@param hours : total number of working hours to be scheduled.
|
|
@param resource: optional resource id, If supplied it will take care of
|
|
resource leave while scheduling.
|
|
@param byday: boolean flag bit enforce day wise scheduling
|
|
|
|
@return : list of scheduled working timing based on resource calendar.
|
|
"""
|
|
res = self.interval_get_multi(cr, uid, [(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)], resource, byday)[(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)]
|
|
return res
|
|
|
|
def interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource=False):
|
|
""" Calculates the Total Working hours based on given start_date to
|
|
end_date, If resource id is supplied that it will consider the source
|
|
leaves also in calculating the hours.
|
|
|
|
@param dt_from : date start to calculate hours
|
|
@param dt_end : date end to calculate hours
|
|
@param resource: optional resource id, If given resource leave will be
|
|
considered.
|
|
|
|
@return : Total number of working hours based dt_from and dt_end and
|
|
resource if supplied.
|
|
"""
|
|
if not id:
|
|
return 0.0
|
|
dt_leave = self._get_leaves(cr, uid, id, resource)
|
|
hours = 0.0
|
|
|
|
current_hour = dt_from.hour
|
|
|
|
while (dt_from <= dt_to):
|
|
cr.execute("select hour_from,hour_to from resource_calendar_attendance where dayofweek='%s' and calendar_id=%s order by hour_from", (dt_from.weekday(),id))
|
|
der = cr.fetchall()
|
|
for (hour_from,hour_to) in der:
|
|
if hours != 0.0:#For first time of the loop only,hours will be 0
|
|
current_hour = hour_from
|
|
leave_flag = False
|
|
if (hour_to>=current_hour):
|
|
dt_check = dt_from.strftime('%Y-%m-%d')
|
|
for leave in dt_leave:
|
|
if dt_check == leave:
|
|
dt_check = datetime.strptime(dt_check, "%Y-%m-%d") + timedelta(days=1)
|
|
leave_flag = True
|
|
|
|
if leave_flag:
|
|
break
|
|
else:
|
|
d1 = dt_from
|
|
d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_to)), int((hour_to%1) * 60))
|
|
|
|
if hours != 0.0:#For first time of the loop only,hours will be 0
|
|
d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(current_hour)), int((current_hour%1) * 60))
|
|
|
|
if dt_from.day == dt_to.day:
|
|
if hour_from <= dt_to.hour <= hour_to:
|
|
d2 = dt_to
|
|
dt_from = d2
|
|
hours += (d2-d1).seconds
|
|
dt_from = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(current_hour)), int((current_hour%1) * 60)) + timedelta(days=1)
|
|
current_hour = 0.0
|
|
|
|
return (hours/3600)
|
|
|
|
resource_calendar()
|
|
|
|
class resource_calendar_attendance(osv.osv):
|
|
_name = "resource.calendar.attendance"
|
|
_description = "Work Detail"
|
|
|
|
_columns = {
|
|
'name' : fields.char("Name", size=64, required=True),
|
|
'dayofweek': fields.selection([('0','Monday'),('1','Tuesday'),('2','Wednesday'),('3','Thursday'),('4','Friday'),('5','Saturday'),('6','Sunday')], 'Day of Week', required=True, select=True),
|
|
'date_from' : fields.date('Starting Date'),
|
|
'hour_from' : fields.float('Work from', required=True, help="Start and End time of working.", select=True),
|
|
'hour_to' : fields.float("Work to", required=True),
|
|
'calendar_id' : fields.many2one("resource.calendar", "Resource's Calendar", required=True),
|
|
}
|
|
|
|
_order = 'dayofweek, hour_from'
|
|
|
|
_defaults = {
|
|
'dayofweek' : '0'
|
|
}
|
|
resource_calendar_attendance()
|
|
|
|
def convert_timeformat(time_string):
|
|
split_list = str(time_string).split('.')
|
|
hour_part = split_list[0]
|
|
mins_part = split_list[1]
|
|
round_mins = int(round(float(mins_part) * 60,-2))
|
|
converted_string = hour_part + ':' + str(round_mins)[0:2]
|
|
return converted_string
|
|
|
|
class resource_resource(osv.osv):
|
|
_name = "resource.resource"
|
|
_description = "Resource Detail"
|
|
_columns = {
|
|
'name' : fields.char("Name", size=64, required=True),
|
|
'code': fields.char('Code', size=16),
|
|
'active' : fields.boolean('Active', 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', 'Company'),
|
|
'resource_type': fields.selection([('user','Human'),('material','Material')], 'Resource Type', required=True),
|
|
'user_id' : fields.many2one('res.users', 'User', help='Related user name for the resource to manage its access.'),
|
|
'time_efficiency' : fields.float('Efficiency Factor', size=8, required=True, help="This field depict the efficiency of the resource to complete tasks. e.g resource put alone on a phase of 5 days with 5 tasks assigned to him, will show a load of 100% for this phase by default, but if we put a efficiency of 200%, then his load will only be 50%."),
|
|
'calendar_id' : fields.many2one("resource.calendar", "Working Time", help="Define the schedule of resource"),
|
|
}
|
|
_defaults = {
|
|
'resource_type' : 'user',
|
|
'time_efficiency' : 1,
|
|
'active' : True,
|
|
'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.resource', context=context)
|
|
}
|
|
|
|
|
|
def copy(self, cr, uid, id, default=None, context=None):
|
|
if default is None:
|
|
default = {}
|
|
if not default.get('name', False):
|
|
default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
|
|
return super(resource_resource, self).copy(cr, uid, id, default, context)
|
|
|
|
def generate_resources(self, cr, uid, user_ids, calendar_id, context=None):
|
|
"""
|
|
Return a list of Resource Class objects for the resources allocated to the phase.
|
|
"""
|
|
resource_objs = {}
|
|
user_pool = self.pool.get('res.users')
|
|
for user in user_pool.browse(cr, uid, user_ids, context=context):
|
|
resource_objs[user.id] = {
|
|
'name' : user.name,
|
|
'vacation': [],
|
|
'efficiency': 1.0,
|
|
}
|
|
|
|
resource_ids = self.search(cr, uid, [('user_id', '=', user.id)], context=context)
|
|
if resource_ids:
|
|
for resource in self.browse(cr, uid, resource_ids, context=context):
|
|
resource_objs[user.id]['efficiency'] = resource.time_efficiency
|
|
resource_cal = resource.calendar_id.id
|
|
if resource_cal:
|
|
leaves = self.compute_vacation(cr, uid, calendar_id, resource.id, resource_cal, context=context)
|
|
resource_objs[user.id]['vacation'] += list(leaves)
|
|
return resource_objs
|
|
|
|
def compute_vacation(self, cr, uid, calendar_id, resource_id=False, resource_calendar=False, context=None):
|
|
"""
|
|
Compute the vacation from the working calendar of the resource.
|
|
|
|
@param calendar_id : working calendar of the project
|
|
@param resource_id : resource working on phase/task
|
|
@param resource_calendar : working calendar of the resource
|
|
"""
|
|
resource_calendar_leaves_pool = self.pool.get('resource.calendar.leaves')
|
|
leave_list = []
|
|
if resource_id:
|
|
leave_ids = resource_calendar_leaves_pool.search(cr, uid, ['|', ('calendar_id', '=', calendar_id),
|
|
('calendar_id', '=', resource_calendar),
|
|
('resource_id', '=', resource_id)
|
|
], context=context)
|
|
else:
|
|
leave_ids = resource_calendar_leaves_pool.search(cr, uid, [('calendar_id', '=', calendar_id),
|
|
('resource_id', '=', False)
|
|
], context=context)
|
|
leaves = resource_calendar_leaves_pool.read(cr, uid, leave_ids, ['date_from', 'date_to'], context=context)
|
|
for i in range(len(leaves)):
|
|
dt_start = datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
|
|
dt_end = datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
|
|
no = dt_end - dt_start
|
|
[leave_list.append((dt_start + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
|
|
leave_list.sort()
|
|
return leave_list
|
|
|
|
def compute_working_calendar(self, cr, uid, calendar_id=False, context=None):
|
|
"""
|
|
Change the format of working calendar from 'Openerp' format to bring it into 'Faces' format.
|
|
@param calendar_id : working calendar of the project
|
|
"""
|
|
if not calendar_id:
|
|
# Calendar is not specified: working days: 24/7
|
|
return [('fri', '8:0-12:0','13:0-17:0'), ('thu', '8:0-12:0','13:0-17:0'), ('wed', '8:0-12:0','13:0-17:0'),
|
|
('mon', '8:0-12:0','13:0-17:0'), ('tue', '8:0-12:0','13:0-17:0')]
|
|
resource_attendance_pool = self.pool.get('resource.calendar.attendance')
|
|
time_range = "8:00-8:00"
|
|
non_working = ""
|
|
week_days = {"0": "mon", "1": "tue", "2": "wed","3": "thu", "4": "fri", "5": "sat", "6": "sun"}
|
|
wk_days = {}
|
|
wk_time = {}
|
|
wktime_list = []
|
|
wktime_cal = []
|
|
week_ids = resource_attendance_pool.search(cr, uid, [('calendar_id', '=', calendar_id)], context=context)
|
|
weeks = resource_attendance_pool.read(cr, uid, week_ids, ['dayofweek', 'hour_from', 'hour_to'], context=context)
|
|
# Convert time formats into appropriate format required
|
|
# and create a list like [('mon', '8:00-12:00'), ('mon', '13:00-18:00')]
|
|
for week in weeks:
|
|
res_str = ""
|
|
day = None
|
|
if week_days.get(week['dayofweek'],False):
|
|
day = week_days[week['dayofweek']]
|
|
wk_days[week['dayofweek']] = week_days[week['dayofweek']]
|
|
else:
|
|
raise osv.except_osv(_('Configuration Error!'),_('Make sure the Working time has been configured with proper week days!'))
|
|
hour_from_str = convert_timeformat(week['hour_from'])
|
|
hour_to_str = convert_timeformat(week['hour_to'])
|
|
res_str = hour_from_str + '-' + hour_to_str
|
|
wktime_list.append((day, res_str))
|
|
# Convert into format like [('mon', '8:00-12:00', '13:00-18:00')]
|
|
for item in wktime_list:
|
|
if wk_time.has_key(item[0]):
|
|
wk_time[item[0]].append(item[1])
|
|
else:
|
|
wk_time[item[0]] = [item[0]]
|
|
wk_time[item[0]].append(item[1])
|
|
for k,v in wk_time.items():
|
|
wktime_cal.append(tuple(v))
|
|
# Add for the non-working days like: [('sat, sun', '8:00-8:00')]
|
|
for k, v in wk_days.items():
|
|
if week_days.has_key(k):
|
|
week_days.pop(k)
|
|
for v in week_days.itervalues():
|
|
non_working += v + ','
|
|
if non_working:
|
|
wktime_cal.append((non_working[:-1], time_range))
|
|
return wktime_cal
|
|
|
|
resource_resource()
|
|
|
|
class resource_calendar_leaves(osv.osv):
|
|
_name = "resource.calendar.leaves"
|
|
_description = "Leave Detail"
|
|
_columns = {
|
|
'name' : fields.char("Name", size=64),
|
|
'company_id' : fields.related('calendar_id','company_id',type='many2one',relation='res.company',string="Company", store=True, readonly=True),
|
|
'calendar_id' : fields.many2one("resource.calendar", "Working Time"),
|
|
'date_from' : fields.datetime('Start Date', required=True),
|
|
'date_to' : fields.datetime('End Date', required=True),
|
|
'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"),
|
|
}
|
|
|
|
def check_dates(self, cr, uid, ids, context=None):
|
|
leave = self.read(cr, uid, ids[0], ['date_from', 'date_to'])
|
|
if leave['date_from'] and leave['date_to']:
|
|
if leave['date_from'] > leave['date_to']:
|
|
return False
|
|
return True
|
|
|
|
_constraints = [
|
|
(check_dates, 'Error! leave start-date must be lower then leave end-date.', ['date_from', 'date_to'])
|
|
]
|
|
|
|
def onchange_resource(self,cr, uid, ids, resource, context=None):
|
|
result = {}
|
|
if resource:
|
|
resource_pool = self.pool.get('resource.resource')
|
|
result['calendar_id'] = resource_pool.browse(cr, uid, resource, context=context).calendar_id.id
|
|
return {'value': result}
|
|
return {'value': {'calendar_id': []}}
|
|
|
|
resource_calendar_leaves()
|
|
|
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|