[MERGE] OPW 578792: project_issue/resource: fix calc. of open/close & working hours to open/close on issue + has to be timezone aware
bzr revid: xal@openerp.com-20121031155007-fjuo7xbgfnoe7q6m
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
#-*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
@@ -65,7 +65,9 @@ class project_issue(crm.crm_case, osv.osv):
|
||||
"""
|
||||
|
||||
res = super(project_issue, self).case_open(cr, uid, ids, *args)
|
||||
self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
|
||||
for issue in self.browse(cr, uid, ids):
|
||||
if not issue.date_open:
|
||||
self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
|
||||
for (id, name) in self.name_get(cr, uid, ids):
|
||||
message = _("Issue '%s' has been opened.") % name
|
||||
self.log(cr, uid, id, message)
|
||||
@@ -99,6 +101,13 @@ class project_issue(crm.crm_case, osv.osv):
|
||||
|
||||
res = {}
|
||||
for issue in self.browse(cr, uid, ids, context=context):
|
||||
|
||||
# if the working hours on the project are not defined, use default ones (8 -> 12 and 13 -> 17 * 5), represented by None
|
||||
if not issue.project_id or not issue.project_id.resource_calendar_id:
|
||||
working_hours = None
|
||||
else:
|
||||
working_hours = issue.project_id.resource_calendar_id.id
|
||||
|
||||
res[issue.id] = {}
|
||||
for field in fields:
|
||||
duration = 0
|
||||
@@ -112,18 +121,24 @@ class project_issue(crm.crm_case, osv.osv):
|
||||
ans = date_open - date_create
|
||||
date_until = issue.date_open
|
||||
#Calculating no. of working hours to open the issue
|
||||
hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
|
||||
hours = cal_obj._interval_hours_get(cr, uid, working_hours,
|
||||
date_create,
|
||||
date_open)
|
||||
date_open,
|
||||
timezone_from_uid=issue.user_id.id or uid,
|
||||
exclude_leaves=False,
|
||||
context=context)
|
||||
elif field in ['working_hours_close','day_close']:
|
||||
if issue.date_closed:
|
||||
date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
|
||||
date_until = issue.date_closed
|
||||
ans = date_close - date_create
|
||||
#Calculating no. of working hours to close the issue
|
||||
hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
|
||||
hours = cal_obj._interval_hours_get(cr, uid, working_hours,
|
||||
date_create,
|
||||
date_close)
|
||||
date_close,
|
||||
timezone_from_uid=issue.user_id.id or uid,
|
||||
exclude_leaves=False,
|
||||
context=context)
|
||||
elif field in ['days_since_creation']:
|
||||
if issue.create_date:
|
||||
days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
|
||||
@@ -142,27 +157,12 @@ class project_issue(crm.crm_case, osv.osv):
|
||||
resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
|
||||
if resource_ids and len(resource_ids):
|
||||
resource_id = resource_ids[0]
|
||||
duration = float(ans.days)
|
||||
if issue.project_id and issue.project_id.resource_calendar_id:
|
||||
duration = float(ans.days) * 24
|
||||
|
||||
new_dates = cal_obj.interval_min_get(cr, uid,
|
||||
issue.project_id.resource_calendar_id.id,
|
||||
date_create,
|
||||
duration, resource=resource_id)
|
||||
no_days = []
|
||||
date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
|
||||
for in_time, out_time in new_dates:
|
||||
if in_time.date not in no_days:
|
||||
no_days.append(in_time.date)
|
||||
if out_time > date_until:
|
||||
break
|
||||
duration = len(no_days)
|
||||
duration = float(ans.days) + float(ans.seconds)/(24*3600)
|
||||
|
||||
if field in ['working_hours_open','working_hours_close']:
|
||||
res[issue.id][field] = hours
|
||||
else:
|
||||
res[issue.id][field] = abs(float(duration))
|
||||
elif field in ['day_open','day_close']:
|
||||
res[issue.id][field] = duration
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@@ -50,13 +50,13 @@ class project_issue_report(osv.osv):
|
||||
'opening_date': fields.date('Date of Opening', readonly=True),
|
||||
'creation_date': fields.date('Creation Date', readonly=True),
|
||||
'date_closed': fields.date('Date of Closing', readonly=True),
|
||||
'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('section_id','=',section_id),('object_id.model', '=', 'project.issue')]"),
|
||||
'categ_id': fields.many2one('crm.case.categ', 'Category', domain="['&',('section_id','=',section_id),('object_id.model', '=', 'project.issue')]"),
|
||||
'type_id': fields.many2one('project.task.type', 'Stage'),
|
||||
'nbr': fields.integer('# of Issues', readonly=True),
|
||||
'working_hours_open': fields.float('Avg. Working Hours to Open', readonly=True, group_operator="avg"),
|
||||
'working_hours_close': fields.float('Avg. Working Hours to Close', readonly=True, group_operator="avg"),
|
||||
'delay_open': fields.float('Avg. Delay to Open', digits=(16,2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to close the project issue"),
|
||||
help="Number of Days to open the project issue"),
|
||||
'delay_close': fields.float('Avg. Delay to Close', digits=(16,2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to close the project issue"),
|
||||
'company_id' : fields.many2one('res.company', 'Company'),
|
||||
@@ -64,9 +64,9 @@ class project_issue_report(osv.osv):
|
||||
'project_id':fields.many2one('project.project', 'Project',readonly=True),
|
||||
'version_id': fields.many2one('project.issue.version', 'Version'),
|
||||
'user_id' : fields.many2one('res.users', 'Assigned to',readonly=True),
|
||||
'partner_id': fields.many2one('res.partner','Partner',domain="[('object_id.model', '=', 'project.issue')]"),
|
||||
'partner_id': fields.many2one('res.partner','Partner'),
|
||||
'channel_id': fields.many2one('crm.case.channel', 'Channel',readonly=True),
|
||||
'task_id': fields.many2one('project.task', 'Task',domain="[('object_id.model', '=', 'project.issue')]" ),
|
||||
'task_id': fields.many2one('project.task', 'Task'),
|
||||
'email': fields.integer('# Emails', size=128, readonly=True),
|
||||
}
|
||||
|
||||
@@ -98,8 +98,8 @@ class project_issue_report(osv.osv):
|
||||
c.channel_id,
|
||||
c.task_id,
|
||||
date_trunc('day',c.create_date) as create_date,
|
||||
extract('epoch' from (c.date_open-c.create_date))/(3600*24) as delay_open,
|
||||
extract('epoch' from (c.date_closed-c.date_open))/(3600*24) as delay_close,
|
||||
c.day_open as delay_open,
|
||||
c.day_close as delay_close,
|
||||
(SELECT count(id) FROM mail_message WHERE model='project.issue' AND res_id=c.id) AS email
|
||||
|
||||
FROM
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<separator orientation="vertical" />
|
||||
|
||||
<field name="project_id"/>
|
||||
<field name="user_id" select="1" widget="selection" filter_domain="[('user_id','ilike',self)]"/>
|
||||
<field name="user_id" select="1" filter_domain="[('user_id','ilike',self)]"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="version_id" widget="selection" />
|
||||
</group>
|
||||
|
||||
+96
-32
@@ -19,7 +19,9 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil import rrule
|
||||
import math
|
||||
from faces import *
|
||||
from osv import fields, osv
|
||||
@@ -28,7 +30,6 @@ from tools.translate import _
|
||||
from itertools import groupby
|
||||
from operator import itemgetter
|
||||
|
||||
|
||||
class resource_calendar(osv.osv):
|
||||
_name = "resource.calendar"
|
||||
_description = "Resource Calendar"
|
||||
@@ -214,43 +215,106 @@ class resource_calendar(osv.osv):
|
||||
"""
|
||||
if not id:
|
||||
return 0.0
|
||||
dt_leave = self._get_leaves(cr, uid, id, resource)
|
||||
hours = 0.0
|
||||
return self._interval_hours_get(cr, uid, id, dt_from, dt_to, resource_id=resource)
|
||||
|
||||
current_hour = dt_from.hour
|
||||
def _interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource_id=False, timezone_from_uid=None, exclude_leaves=True, context=None):
|
||||
""" 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.
|
||||
|
||||
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
|
||||
@param dt_from : date start to calculate hours
|
||||
@param dt_end : date end to calculate hours
|
||||
@param resource_id: optional resource id, If given resource leave will be
|
||||
considered.
|
||||
@param timezone_from_uid: optional uid, if given we will considerer
|
||||
working hours in that user timezone
|
||||
@param exclude_leaves: optionnal, if set to True (default) we will exclude
|
||||
resource leaves from working hours
|
||||
@param context: current request context
|
||||
@return : Total number of working hours based dt_from and dt_end and
|
||||
resource if supplied.
|
||||
"""
|
||||
utc_tz = pytz.timezone('UTC')
|
||||
local_tz = utc_tz
|
||||
|
||||
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 timezone_from_uid:
|
||||
users_obj = self.pool.get('res.users')
|
||||
user_timezone = users_obj.browse(cr, uid, timezone_from_uid, context=context).context_tz
|
||||
if user_timezone:
|
||||
try:
|
||||
local_tz = pytz.timezone(user_timezone)
|
||||
except pytz.UnknownTimeZoneError:
|
||||
pass # fallback to UTC as local timezone
|
||||
|
||||
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))
|
||||
def utc_to_local_zone(naive_datetime):
|
||||
utc_dt = utc_tz.localize(naive_datetime, is_dst=False)
|
||||
return utc_dt.astimezone(local_tz)
|
||||
|
||||
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
|
||||
def float_time_convert(float_val):
|
||||
factor = float_val < 0 and -1 or 1
|
||||
val = abs(float_val)
|
||||
return (factor * int(math.floor(val)), int(round((val % 1) * 60)))
|
||||
|
||||
return (hours/3600)
|
||||
# Get slots hours per day
|
||||
# {day_of_week: [(8, 12), (13, 17), ...], ...}
|
||||
hours_range_per_weekday = {}
|
||||
if id:
|
||||
cr.execute("select dayofweek, hour_from,hour_to from resource_calendar_attendance where calendar_id=%s order by hour_from", (id,))
|
||||
for weekday, hour_from, hour_to in cr.fetchall():
|
||||
weekday = int(weekday)
|
||||
hours_range_per_weekday.setdefault(weekday, [])
|
||||
hours_range_per_weekday[weekday].append((hour_from, hour_to))
|
||||
else:
|
||||
# considering default working hours (Monday -> Friday, 8 -> 12, 13 -> 17)
|
||||
for weekday in range(5):
|
||||
hours_range_per_weekday[weekday] = [(8, 12), (13, 17)]
|
||||
|
||||
## Interval between dt_from - dt_to
|
||||
##
|
||||
## dt_from dt_to
|
||||
## =============|==================|============
|
||||
## [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]
|
||||
##
|
||||
## [ : start of range
|
||||
## ] : end of range
|
||||
##
|
||||
## case 1: range end before interval start (skip)
|
||||
## case 2: range overlap interval start (fit start to internal)
|
||||
## case 3: range within interval
|
||||
## case 4: range overlap interval end (fit end to interval)
|
||||
## case 5: range start after interval end (skip)
|
||||
|
||||
interval_start = utc_to_local_zone(dt_from)
|
||||
interval_end = utc_to_local_zone(dt_to)
|
||||
hours_timedelta = timedelta()
|
||||
|
||||
# Get leaves for requested resource
|
||||
dt_leaves = set([])
|
||||
if exclude_leaves and id:
|
||||
dt_leaves = set(self._get_leaves(cr, uid, id, resource=resource_id))
|
||||
|
||||
for day in rrule.rrule(rrule.DAILY, dtstart=interval_start,
|
||||
until=interval_end+timedelta(days=1),
|
||||
byweekday=hours_range_per_weekday.keys()):
|
||||
if exclude_leaves and day.strftime('%Y-%m-%d') in dt_leaves:
|
||||
# XXX: futher improve leave management to allow for partial day leave
|
||||
continue
|
||||
for (range_from, range_to) in hours_range_per_weekday.get(day.weekday(), []):
|
||||
range_from_hour, range_from_min = float_time_convert(range_from)
|
||||
range_to_hour, range_to_min = float_time_convert(range_to)
|
||||
daytime_start = local_tz.localize(day.replace(hour=range_from_hour, minute=range_from_min, second=0, tzinfo=None))
|
||||
daytime_end = local_tz.localize(day.replace(hour=range_to_hour, minute=range_to_min, second=0, tzinfo=None))
|
||||
|
||||
# case 1 & 5: time range out of interval
|
||||
if daytime_end < interval_start or daytime_start > interval_end:
|
||||
continue
|
||||
# case 2 & 4: adjust start, end to fit within interval
|
||||
daytime_start = max(daytime_start, interval_start)
|
||||
daytime_end = min(daytime_end, interval_end)
|
||||
# case 2+, 4+, 3
|
||||
hours_timedelta += (daytime_end - daytime_start)
|
||||
# return timedelta converted to hours
|
||||
return (hours_timedelta.days * 24.0 + hours_timedelta.seconds / 3600.0)
|
||||
|
||||
resource_calendar()
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
dt_from = now - timedelta(days=now.weekday())
|
||||
dt_to = dt_from+ timedelta(days=6)
|
||||
hours = self.interval_hours_get(cr, uid, ref('timesheet_group1'), dt_from, dt_to, resource=ref('resource_developer'))
|
||||
assert hours > 27 , 'Invalid Total Week working hour calculated'
|
||||
assert hours >= 27 , 'Invalid Total Week working hour calculated'
|
||||
-
|
||||
Project Analysis work is of 20 hours which will start from Week start so i will calculate working schedule for resource Analyst for the same.
|
||||
-
|
||||
|
||||
Reference in New Issue
Block a user