[REM] resource, project: removed faces library and its related
code in resource and project. This library was once used to perfom resource allocation: scheduling tasks and things like that. However this feature cannot be accessed through the interface since a long time. The feature is now removed from the project code. The library has been removed. Some code in resource used only in the project code has also been removed, as well as a test linked to this code.
This commit is contained in:
@@ -2,14 +2,12 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, date
|
||||
from dateutil import relativedelta
|
||||
from lxml import etree
|
||||
import time
|
||||
|
||||
from openerp import api
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp import tools
|
||||
from openerp.addons.resource.faces import task as Task
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
from openerp.exceptions import UserError
|
||||
@@ -315,104 +313,6 @@ class project(osv.osv):
|
||||
self.setActive(cr, uid, child_ids, value, context=None)
|
||||
return True
|
||||
|
||||
def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
|
||||
context = context or {}
|
||||
if type(ids) in (long, int,):
|
||||
ids = [ids]
|
||||
projects = self.browse(cr, uid, ids, context=context)
|
||||
|
||||
for project in projects:
|
||||
if (not project.members) and force_members:
|
||||
raise UserError(_("You must assign members on the project '%s'!") % (project.name,))
|
||||
|
||||
resource_pool = self.pool.get('resource.resource')
|
||||
|
||||
result = "from openerp.addons.resource.faces import *\n"
|
||||
result += "import datetime\n"
|
||||
for project in self.browse(cr, uid, ids, context=context):
|
||||
u_ids = [i.id for i in project.members]
|
||||
if project.user_id and (project.user_id.id not in u_ids):
|
||||
u_ids.append(project.user_id.id)
|
||||
for task in project.tasks:
|
||||
if task.user_id and (task.user_id.id not in u_ids):
|
||||
u_ids.append(task.user_id.id)
|
||||
calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
|
||||
resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
|
||||
for key, vals in resource_objs.items():
|
||||
result +='''
|
||||
class User_%s(Resource):
|
||||
efficiency = %s
|
||||
''' % (key, vals.get('efficiency', False))
|
||||
|
||||
result += '''
|
||||
def Project():
|
||||
'''
|
||||
return result
|
||||
|
||||
def _schedule_project(self, cr, uid, project, context=None):
|
||||
resource_pool = self.pool.get('resource.resource')
|
||||
calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
|
||||
working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
|
||||
# TODO: check if we need working_..., default values are ok.
|
||||
puids = [x.id for x in project.members]
|
||||
if project.user_id:
|
||||
puids.append(project.user_id.id)
|
||||
result = """
|
||||
def Project_%d():
|
||||
start = \'%s\'
|
||||
working_days = %s
|
||||
resource = %s
|
||||
""" % (
|
||||
project.id,
|
||||
project.date_start or time.strftime('%Y-%m-%d'), working_days,
|
||||
'|'.join(['User_'+str(x) for x in puids]) or 'None'
|
||||
)
|
||||
vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
|
||||
if vacation:
|
||||
result+= """
|
||||
vacation = %s
|
||||
""" % ( vacation, )
|
||||
return result
|
||||
|
||||
#TODO: DO Resource allocation and compute availability
|
||||
def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
|
||||
if context == None:
|
||||
context = {}
|
||||
allocation = {}
|
||||
return allocation
|
||||
|
||||
def schedule_tasks(self, cr, uid, ids, context=None):
|
||||
context = context or {}
|
||||
if type(ids) in (long, int,):
|
||||
ids = [ids]
|
||||
projects = self.browse(cr, uid, ids, context=context)
|
||||
result = self._schedule_header(cr, uid, ids, False, context=context)
|
||||
for project in projects:
|
||||
result += self._schedule_project(cr, uid, project, context=context)
|
||||
result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
|
||||
|
||||
local_dict = {}
|
||||
exec result in local_dict
|
||||
projects_gantt = Task.BalancedProject(local_dict['Project'])
|
||||
|
||||
for project in projects:
|
||||
project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
|
||||
for task in project.tasks:
|
||||
if task.stage_id and task.stage_id.fold:
|
||||
continue
|
||||
|
||||
p = getattr(project_gantt, 'Task_%d' % (task.id,))
|
||||
|
||||
self.pool.get('project.task').write(cr, uid, [task.id], {
|
||||
'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}, context=context)
|
||||
if (not task.user_id) and (p.booked_resource):
|
||||
self.pool.get('project.task').write(cr, uid, [task.id], {
|
||||
'user_id': int(p.booked_resource[0].name[5:]),
|
||||
}, context=context)
|
||||
return True
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
@@ -387,10 +387,6 @@
|
||||
<field name="stage_id" ref="project_stage_0"/>
|
||||
</record>
|
||||
|
||||
<!-- Schedule tasks to assign users and dates -->
|
||||
<function model="project.project" name="schedule_tasks"
|
||||
eval="[ref('project_project_3'), ref('project_project_4'), ref('project_project_5')], {'install_mode': True}"/>
|
||||
|
||||
<record id="message_task_1" model="mail.message">
|
||||
<field name="model">project.task</field>
|
||||
<field name="res_id" ref="project_task_22"/>
|
||||
|
||||
@@ -67,9 +67,8 @@ class TestProjectFlow(TestProjectBase):
|
||||
self.assertEqual(pigs.state, 'close')
|
||||
# Re-open
|
||||
pigs.state = 'open'
|
||||
# Re-convert into a template and schedule tasks
|
||||
# Re-convert into a template
|
||||
pigs.set_template()
|
||||
pigs.schedule_tasks()
|
||||
# Copy the project
|
||||
new_project = pigs.copy()
|
||||
self.assertEqual(len(new_project.tasks), 2, 'project: copied project should have copied task')
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import resource
|
||||
import faces
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from pcalendar import Calendar, WorkingDate, StartDate, EndDate, Minutes
|
||||
|
||||
from task import Project, BalancedProject, AdjustedProject, Task, \
|
||||
STRICT, SLOPPY, SMART, Multi, YearlyMax, WeeklyMax, MonthlyMax, \
|
||||
DailyMax, VariableLoad
|
||||
|
||||
from resource import Resource
|
||||
@@ -1,79 +0,0 @@
|
||||
#@+leo-ver=4
|
||||
#@+node:@file observer.py
|
||||
#@@language python
|
||||
#@<< Copyright >>
|
||||
#@+node:<< Copyright >>
|
||||
############################################################################
|
||||
# Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
|
||||
# mreithinger@web.de
|
||||
#
|
||||
# This file is part of faces.
|
||||
#
|
||||
# faces is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# faces 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
############################################################################
|
||||
|
||||
#@-node:<< Copyright >>
|
||||
#@nl
|
||||
"""
|
||||
This module contains the base class for all observer objects
|
||||
"""
|
||||
#@<< Imports >>
|
||||
#@+node:<< Imports >>
|
||||
#@-node:<< Imports >>
|
||||
#@nl
|
||||
_is_source_ = True
|
||||
#@+others
|
||||
#@+node:class Observer
|
||||
class Observer(object):
|
||||
"""
|
||||
Base Class for all charts and reports.
|
||||
|
||||
@var visible: Specifies if the observer is visible
|
||||
at the navigation bar inside the gui.
|
||||
|
||||
@var link_view: syncronizes the marked objects in all views.
|
||||
|
||||
"""
|
||||
#@ << declarations >>
|
||||
#@+node:<< declarations >>
|
||||
__type_name__ = None
|
||||
__type_image__ = None
|
||||
visible = True
|
||||
link_view = True
|
||||
|
||||
__attrib_completions__ = { "visible" : 'visible = False',
|
||||
"link_view" : "link_view = False" }
|
||||
|
||||
|
||||
#@-node:<< declarations >>
|
||||
#@nl
|
||||
|
||||
#@ @+others
|
||||
#@+node:register_editors
|
||||
def register_editors(cls, registry):
|
||||
pass
|
||||
|
||||
register_editors = classmethod(register_editors)
|
||||
|
||||
#@-node:register_editors
|
||||
#@-others
|
||||
|
||||
#@-node:class Observer
|
||||
#@-others
|
||||
factories = { }
|
||||
clear_cache_funcs = {}
|
||||
#@-node:@file observer.py
|
||||
#@-leo
|
||||
@@ -1,951 +0,0 @@
|
||||
#@+leo-ver=4
|
||||
#@+node:@file pcalendar.py
|
||||
#@@language python
|
||||
#@<< Copyright >>
|
||||
#@+node:<< Copyright >>
|
||||
############################################################################
|
||||
# Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
|
||||
# mreithinger@web.de
|
||||
#
|
||||
# This file is part of faces.
|
||||
#
|
||||
# faces is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# faces 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
############################################################################
|
||||
|
||||
#@-node:<< Copyright >>
|
||||
#@nl
|
||||
"""
|
||||
This module contains all classes and functions for the project plan calendar
|
||||
"""
|
||||
#@<< Imports >>
|
||||
#@+node:<< Imports >>
|
||||
from string import *
|
||||
import datetime
|
||||
import time
|
||||
import re
|
||||
import locale
|
||||
import bisect
|
||||
import sys
|
||||
|
||||
TIME_RANGE_PATTERN = re.compile("(\\d+):(\\d+)\\s*-\\s*(\\d+):(\\d+)")
|
||||
TIME_DELTA_PATTERN = re.compile("([-+]?\\d+(\\.\\d+)?)([dwmyMH])")
|
||||
|
||||
DEFAULT_MINIMUM_TIME_UNIT = 15
|
||||
DEFAULT_WORKING_DAYS_PER_WEEK = 5
|
||||
DEFAULT_WORKING_DAYS_PER_MONTH = 20
|
||||
DEFAULT_WORKING_DAYS_PER_YEAR = 200
|
||||
DEFAULT_WORKING_HOURS_PER_DAY = 8
|
||||
|
||||
DEFAULT_WORKING_TIMES = ( (8 * 60, 12 * 60 ),
|
||||
(13 * 60, 17 * 60 ) )
|
||||
DEFAULT_WORKING_DAYS = { 0 : DEFAULT_WORKING_TIMES,
|
||||
1 : DEFAULT_WORKING_TIMES,
|
||||
2 : DEFAULT_WORKING_TIMES,
|
||||
3 : DEFAULT_WORKING_TIMES,
|
||||
4 : DEFAULT_WORKING_TIMES,
|
||||
5 : (),
|
||||
6 : () }
|
||||
|
||||
#@-node:<< Imports >>
|
||||
#@nl
|
||||
#@+others
|
||||
#@+node:to_time_range
|
||||
def to_time_range(src):
|
||||
"""
|
||||
converts a string to a timerange, i.e
|
||||
(from, to)
|
||||
from, to are ints, specifing the minutes since midnight
|
||||
"""
|
||||
|
||||
if not src: return ()
|
||||
|
||||
mo = TIME_RANGE_PATTERN.match(src)
|
||||
if not mo:
|
||||
raise ValueError("%s is no time range" % src)
|
||||
|
||||
from_time = int(mo.group(1)) * 60 + int(mo.group(2))
|
||||
to_time = int(mo.group(3)) * 60 + int(mo.group(4))
|
||||
return from_time, to_time
|
||||
#@-node:to_time_range
|
||||
#@+node:to_datetime
|
||||
def to_datetime(src):
|
||||
"""
|
||||
a tolerant conversion function to convert different strings
|
||||
to a datetime.dateime
|
||||
"""
|
||||
|
||||
#to get the original value for wrappers
|
||||
new = getattr(src, "_value", src)
|
||||
while new is not src:
|
||||
src = new
|
||||
new = getattr(src, "_value", src)
|
||||
|
||||
if isinstance(src, _WorkingDateBase):
|
||||
src = src.to_datetime()
|
||||
|
||||
if isinstance(src, datetime.datetime):
|
||||
return src
|
||||
|
||||
src = str(src)
|
||||
|
||||
formats = [ "%x %H:%M",
|
||||
"%x",
|
||||
"%Y-%m-%d %H:%M",
|
||||
"%y-%m-%d %H:%M",
|
||||
"%d.%m.%Y %H:%M",
|
||||
"%d.%m.%y %H:%M",
|
||||
"%Y%m%d %H:%M",
|
||||
"%d/%m/%y %H:%M",
|
||||
"%d/%m/%Y %H:%M",
|
||||
"%d/%m/%Y",
|
||||
"%d/%m/%y",
|
||||
"%Y-%m-%d",
|
||||
"%y-%m-%d",
|
||||
"%d.%m.%Y",
|
||||
"%d.%m.%y",
|
||||
"%Y%m%d" ]
|
||||
for f in formats:
|
||||
try:
|
||||
conv = time.strptime(src, f)
|
||||
|
||||
return datetime.datetime(*conv[0:-3])
|
||||
except Exception, e:
|
||||
pass
|
||||
|
||||
raise TypeError("'%s' (%s) is not a datetime" % (src, str(type(src))))
|
||||
#@-node:
|
||||
#@+node:_to_days
|
||||
def _to_days(src):
|
||||
"""
|
||||
converts a string of the day abreviations mon, tue, wed,
|
||||
thu, fri, sat, sun to a dir with correct weekday indices.
|
||||
For Example
|
||||
convert_to_days('mon, tue, thu') results in
|
||||
{ 0:1, 1:1, 3:1 }
|
||||
"""
|
||||
|
||||
tokens = src.split(",")
|
||||
result = { }
|
||||
for t in tokens:
|
||||
try:
|
||||
index = { "mon" : 0,
|
||||
"tue" : 1,
|
||||
"wed" : 2,
|
||||
"thu" : 3,
|
||||
"fri" : 4,
|
||||
"sat" : 5,
|
||||
"sun" : 6 } [ lower(t.strip()) ]
|
||||
result[index] = 1
|
||||
except:
|
||||
raise ValueError("%s is not a day" % (t))
|
||||
|
||||
return result
|
||||
#@-node:_to_days
|
||||
#@+node:_add_to_time_spans
|
||||
def _add_to_time_spans(src, to_add, is_free):
|
||||
if not isinstance(to_add, (tuple, list)):
|
||||
to_add = (to_add,)
|
||||
|
||||
tmp = []
|
||||
for start, end, f in src:
|
||||
tmp.append((start, True, f))
|
||||
tmp.append((end, False, f))
|
||||
|
||||
for v in to_add:
|
||||
if isinstance(v, (tuple, list)):
|
||||
start = to_datetime(v[0])
|
||||
end = to_datetime(v[1])
|
||||
else:
|
||||
start = to_datetime(v)
|
||||
end = start.replace(hour=0, minute=0) + datetime.timedelta(1)
|
||||
|
||||
tmp.append((start, start <= end, is_free))
|
||||
tmp.append((end, start > end, is_free))
|
||||
|
||||
tmp.sort()
|
||||
|
||||
# 0: date
|
||||
# 1: is_start
|
||||
# 2: is_free
|
||||
sequence = []
|
||||
free_count = 0
|
||||
work_count = 0
|
||||
last = None
|
||||
for date, is_start, is_free in tmp:
|
||||
if is_start:
|
||||
if is_free:
|
||||
if not free_count and not work_count:
|
||||
last = date
|
||||
|
||||
free_count += 1
|
||||
else:
|
||||
if not work_count:
|
||||
if free_count: sequence.append((last, date, True))
|
||||
last = date
|
||||
work_count += 1
|
||||
else:
|
||||
if is_free:
|
||||
assert(free_count > 0)
|
||||
free_count -= 1
|
||||
if not free_count and not work_count:
|
||||
sequence.append((last, date, True))
|
||||
else:
|
||||
assert(work_count > 0)
|
||||
work_count -= 1
|
||||
if not work_count: sequence.append((last, date, False))
|
||||
if free_count: last = date
|
||||
|
||||
return tuple(sequence)
|
||||
#@-node:_add_to_time_spans
|
||||
#@+node:to_timedelta
|
||||
def to_timedelta(src, cal=None, is_duration=False):
|
||||
"""
|
||||
converts a string to a datetime.timedelta. If cal is specified
|
||||
it will be used for getting the working times. if is_duration=True
|
||||
working times will not be considered. Valid units are
|
||||
d for Days
|
||||
w for Weeks
|
||||
m for Months
|
||||
y for Years
|
||||
H for Hours
|
||||
M for Minutes
|
||||
"""
|
||||
|
||||
cal = cal or _default_calendar
|
||||
if isinstance(src, datetime.timedelta):
|
||||
return datetime.timedelta(src.days, seconds=src.seconds, calendar=cal)
|
||||
|
||||
if isinstance(src, (long, int, float)):
|
||||
src = "%sM" % str(src)
|
||||
|
||||
if not isinstance(src, basestring):
|
||||
raise ValueError("%s is not a duration" % (repr(src)))
|
||||
|
||||
src = src.strip()
|
||||
|
||||
if is_duration:
|
||||
d_p_w = 7
|
||||
d_p_m = 30
|
||||
d_p_y = 360
|
||||
d_w_h = 24
|
||||
else:
|
||||
d_p_w = cal.working_days_per_week
|
||||
d_p_m = cal.working_days_per_month
|
||||
d_p_y = cal.working_days_per_year
|
||||
d_w_h = cal.working_hours_per_day
|
||||
|
||||
def convert_minutes(minutes):
|
||||
minutes = int(minutes)
|
||||
hours = minutes / 60
|
||||
minutes = minutes % 60
|
||||
days = hours / d_w_h
|
||||
hours = hours % d_w_h
|
||||
return [ days, 0, 0, 0, minutes, hours ]
|
||||
|
||||
def convert_days(value):
|
||||
days = int(value)
|
||||
value -= days
|
||||
value *= d_w_h
|
||||
hours = int(value)
|
||||
value -= hours
|
||||
value *= 60
|
||||
minutes = round(value)
|
||||
return [ days, 0, 0, 0, minutes, hours ]
|
||||
|
||||
sum_args = [ 0, 0, 0, 0, 0, 0 ]
|
||||
|
||||
split = src.split(" ")
|
||||
for s in split:
|
||||
mo = TIME_DELTA_PATTERN.match(s)
|
||||
if not mo:
|
||||
raise ValueError(src +
|
||||
" is not a valid duration: valid"
|
||||
" units are: d w m y M H")
|
||||
|
||||
unit = mo.group(3)
|
||||
val = float(mo.group(1))
|
||||
|
||||
if unit == 'd':
|
||||
args = convert_days(val)
|
||||
elif unit == 'w':
|
||||
args = convert_days(val * d_p_w)
|
||||
elif unit == 'm':
|
||||
args = convert_days(val * d_p_m)
|
||||
elif unit == 'y':
|
||||
args = convert_days(val * d_p_y)
|
||||
elif unit == 'M':
|
||||
args = convert_minutes(val)
|
||||
elif unit == 'H':
|
||||
args = convert_minutes(val * 60)
|
||||
|
||||
sum_args = [ a + b for a, b in zip(sum_args, args) ]
|
||||
|
||||
sum_args = tuple(sum_args)
|
||||
return datetime.timedelta(*sum_args)
|
||||
#@-node:to_timedelta
|
||||
#@+node:timedelta_to_str
|
||||
def timedelta_to_str(delta, format, cal=None, is_duration=False):
|
||||
cal = cal or _default_calendar
|
||||
if is_duration:
|
||||
d_p_w = 7
|
||||
d_p_m = 30
|
||||
d_p_y = 365
|
||||
d_w_h = 24
|
||||
else:
|
||||
d_p_w = cal.working_days_per_week
|
||||
d_p_m = cal.working_days_per_month
|
||||
d_p_y = cal.working_days_per_year
|
||||
d_w_h = cal.working_hours_per_day
|
||||
|
||||
has_years = format.find("%y") > -1
|
||||
has_minutes = format.find("%M") > -1
|
||||
has_hours = format.find("%H") > -1 or has_minutes
|
||||
has_days = format.find("%d") > -1
|
||||
has_weeks = format.find("%w") > -1
|
||||
has_months = format.find("%m") > -1
|
||||
|
||||
result = format
|
||||
days = delta.days
|
||||
|
||||
d_r = (days, format)
|
||||
minutes = delta.seconds / 60
|
||||
|
||||
def rebase(d_r, cond1, cond2, letter, divisor):
|
||||
#rebase the days
|
||||
if not cond1: return d_r
|
||||
|
||||
days, result = d_r
|
||||
|
||||
if cond2:
|
||||
val = days / divisor
|
||||
if not val:
|
||||
result = re.sub("{[^{]*?%" + letter + "[^}]*?}", "", result)
|
||||
|
||||
result = result.replace("%" + letter, str(val))
|
||||
days %= divisor
|
||||
else:
|
||||
result = result.replace("%" + letter,
|
||||
locale.format("%.2f",
|
||||
(float(days) / divisor)))
|
||||
|
||||
return (days, result)
|
||||
|
||||
d_r = rebase(d_r, has_years, has_months or has_weeks or has_days, "y", d_p_y)
|
||||
d_r = rebase(d_r, has_months, has_weeks or has_days, "m", d_p_m)
|
||||
d_r = rebase(d_r, has_weeks, has_days, "w", d_p_w)
|
||||
days, result = d_r
|
||||
|
||||
if not has_days:
|
||||
minutes += days * d_w_h * 60
|
||||
days = 0
|
||||
|
||||
if has_hours:
|
||||
if not days:
|
||||
result = re.sub("{[^{]*?%d[^}]*?}", "", result)
|
||||
|
||||
result = result.replace("%d", str(days))
|
||||
else:
|
||||
result = result.replace("%d",
|
||||
"%.2f" % (days + float(minutes)
|
||||
/ (d_w_h * 60)))
|
||||
|
||||
if has_hours:
|
||||
if has_minutes:
|
||||
val = minutes / 60
|
||||
if not val:
|
||||
result = re.sub("{[^{]*?%H[^}]*?}", "", result)
|
||||
|
||||
result = result.replace("%H", str(val))
|
||||
minutes %= 60
|
||||
else:
|
||||
result = result.replace("%H", "%.2f" % (float(minutes) / 60))
|
||||
|
||||
if not minutes:
|
||||
result = re.sub("{[^{]*?%M[^}]*?}", "", result)
|
||||
|
||||
result = result.replace("%M", str(minutes))
|
||||
|
||||
result = result.replace("{", "")
|
||||
result = result.replace("}", "")
|
||||
return result.strip()
|
||||
#@-node:timedelta_to_str
|
||||
#@+node:strftime
|
||||
def strftime(dt, format):
|
||||
"""
|
||||
an extended version of strftime, that introduces some new
|
||||
directives:
|
||||
%IW iso week number
|
||||
%IY iso year
|
||||
%IB full month name appropriate to iso week
|
||||
%ib abbreviated month name appropriate to iso week
|
||||
%im month as decimal number appropriate to iso week
|
||||
"""
|
||||
iso = dt.isocalendar()
|
||||
if iso[0] != dt.year:
|
||||
iso_date = dt.replace(day=1, month=1)
|
||||
format = format \
|
||||
.replace("%IB", iso_date.strftime("%B"))\
|
||||
.replace("%ib", iso_date.strftime("%b"))\
|
||||
.replace("%im", iso_date.strftime("%m"))
|
||||
else:
|
||||
format = format \
|
||||
.replace("%IB", "%B")\
|
||||
.replace("%ib", "%b")\
|
||||
.replace("%im", "%m")
|
||||
|
||||
format = format \
|
||||
.replace("%IW", str(iso[1]))\
|
||||
.replace("%IY", str(iso[0]))\
|
||||
|
||||
return dt.strftime(format)
|
||||
#@-node:strftime
|
||||
#@+node:union
|
||||
def union(*calendars):
|
||||
"""
|
||||
returns a calendar that unifies all working times
|
||||
"""
|
||||
#@ << check arguments >>
|
||||
#@+node:<< check arguments >>
|
||||
if len(calendars) == 1:
|
||||
calendars = calendars[0]
|
||||
#@nonl
|
||||
#@-node:<< check arguments >>
|
||||
#@nl
|
||||
#@ << intersect vacations >>
|
||||
#@+node:<< intersect vacations >>
|
||||
free_time = []
|
||||
for c in calendars:
|
||||
for start, end, is_free in c.time_spans:
|
||||
if is_free:
|
||||
free_time.append((start, False))
|
||||
free_time.append((end, True))
|
||||
|
||||
count = len(calendars)
|
||||
open = 0
|
||||
time_spans = []
|
||||
free_time.sort()
|
||||
for date, is_end in free_time:
|
||||
if is_end:
|
||||
if open == count:
|
||||
time_spans.append((start, date, True))
|
||||
open -= 1
|
||||
else:
|
||||
open += 1
|
||||
start = date
|
||||
#@-node:<< intersect vacations >>
|
||||
#@nl
|
||||
#@ << unify extra worktime >>
|
||||
#@+node:<< unify extra worktime >>
|
||||
for c in calendars:
|
||||
for start, end, is_free in c.time_spans:
|
||||
if not is_free:
|
||||
time_spans = _add_to_time_spans(time_spans, start, end)
|
||||
#@nonl
|
||||
#@-node:<< unify extra worktime >>
|
||||
#@nl
|
||||
#@ << unify working times >>
|
||||
#@+node:<< unify working times >>
|
||||
working_times = {}
|
||||
for d in range(0, 7):
|
||||
times = []
|
||||
for c in calendars:
|
||||
for start, end in c.working_times.get(d, []):
|
||||
times.append((start, False))
|
||||
times.append((end, True))
|
||||
|
||||
times.sort()
|
||||
open = 0
|
||||
ti = []
|
||||
start = None
|
||||
for time, is_end in times:
|
||||
if not is_end:
|
||||
if not start: start = time
|
||||
open += 1
|
||||
else:
|
||||
open -= 1
|
||||
if not open:
|
||||
ti.append((start, time))
|
||||
start = None
|
||||
|
||||
if ti:
|
||||
working_times[d] = ti
|
||||
#@-node:<< unify working times >>
|
||||
#@nl
|
||||
#@ << create result calendar >>
|
||||
#@+node:<< create result calendar >>
|
||||
result = Calendar()
|
||||
result.working_times = working_times
|
||||
result.time_spans = time_spans
|
||||
result._recalc_working_time()
|
||||
result._build_mapping()
|
||||
#@nonl
|
||||
#@-node:<< create result calendar >>
|
||||
#@nl
|
||||
return result
|
||||
#@nonl
|
||||
#@-node:union
|
||||
#@+node:class _CalendarItem
|
||||
class _CalendarItem(int):
|
||||
#@ << class _CalendarItem declarations >>
|
||||
#@+node:<< class _CalendarItem declarations >>
|
||||
__slots__ = ()
|
||||
calender = None
|
||||
|
||||
|
||||
#@-node:<< class _CalendarItem declarations >>
|
||||
#@nl
|
||||
#@ @+others
|
||||
#@+node:__new__
|
||||
def __new__(cls, val):
|
||||
try:
|
||||
return int.__new__(cls, val)
|
||||
except OverflowError:
|
||||
return int.__new__(cls, sys.maxint)
|
||||
#@-node:__new__
|
||||
#@+node:round
|
||||
def round(self, round_up=True):
|
||||
m_t_u = self.calendar.minimum_time_unit
|
||||
|
||||
minutes = int(self)
|
||||
base = (minutes / m_t_u) * m_t_u
|
||||
minutes %= m_t_u
|
||||
|
||||
round_up = round_up and minutes > 0 or minutes > m_t_u / 2
|
||||
if round_up: base += m_t_u
|
||||
return self.__class__(base)
|
||||
#@-node:round
|
||||
#@-others
|
||||
#@-node:class _CalendarItem
|
||||
#@+node:class _Minutes
|
||||
class _Minutes(_CalendarItem):
|
||||
#@ << class _Minutes declarations >>
|
||||
#@+node:<< class _Minutes declarations >>
|
||||
__slots__ = ()
|
||||
STR_FORMAT = "{%dd}{ %HH}{ %MM}"
|
||||
|
||||
|
||||
#@-node:<< class _Minutes declarations >>
|
||||
#@nl
|
||||
#@ @+others
|
||||
#@+node:__new__
|
||||
def __new__(cls, src=0, is_duration=False):
|
||||
"""
|
||||
converts a timedelta in working minutes.
|
||||
"""
|
||||
if isinstance(src, cls) or type(src) is int:
|
||||
return _CalendarItem.__new__(cls, src)
|
||||
|
||||
cal = cls.calendar
|
||||
if not isinstance(src, datetime.timedelta):
|
||||
src = to_timedelta(src, cal, is_duration)
|
||||
|
||||
d_w_h = is_duration and 24 or cal.working_hours_per_day
|
||||
src = src.days * d_w_h * 60 + src.seconds / 60
|
||||
return _CalendarItem.__new__(cls, src)
|
||||
#@-node:__new__
|
||||
#@+node:__cmp__
|
||||
def __cmp__(self, other):
|
||||
return cmp(int(self), int(self.__class__(other)))
|
||||
#@-node:__cmp__
|
||||
#@+node:__add__
|
||||
def __add__(self, other):
|
||||
try:
|
||||
return self.__class__(int(self) + int(self.__class__(other)))
|
||||
except:
|
||||
return NotImplemented
|
||||
#@-node:__add__
|
||||
#@+node:__sub__
|
||||
def __sub__(self, other):
|
||||
try:
|
||||
return self.__class__(int(self) - int(self.__class__(other)))
|
||||
except:
|
||||
return NotImplemented
|
||||
#@-node:__sub__
|
||||
#@+node:to_timedelta
|
||||
def to_timedelta(self, is_duration=False):
|
||||
d_w_h = is_duration and 24 or self.calendar.working_hours_per_day
|
||||
minutes = int(self)
|
||||
hours = minutes / 60
|
||||
minutes = minutes % 60
|
||||
days = hours / d_w_h
|
||||
hours = hours % d_w_h
|
||||
return datetime.timedelta(days, hours=hours, minutes=minutes)
|
||||
#@nonl
|
||||
#@-node:to_timedelta
|
||||
#@+node:strftime
|
||||
def strftime(self, format=None, is_duration=False):
|
||||
td = self.to_timedelta(is_duration)
|
||||
return timedelta_to_str(td, format or self.STR_FORMAT,
|
||||
self.calendar, is_duration)
|
||||
#@nonl
|
||||
#@-node:strftime
|
||||
#@-others
|
||||
#@-node:class _Minutes
|
||||
#@+node:class _WorkingDateBase
|
||||
class _WorkingDateBase(_CalendarItem):
|
||||
"""
|
||||
A daytetime which has only valid values within the
|
||||
workingtimes of a specific calendar
|
||||
"""
|
||||
#@ << class _WorkingDateBase declarations >>
|
||||
#@+node:<< class _WorkingDateBase declarations >>
|
||||
timetuple = True
|
||||
STR_FORMAT = "%x %H:%M"
|
||||
_minutes = _Minutes
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
#@-node:<< class _WorkingDateBase declarations >>
|
||||
#@nl
|
||||
#@ @+others
|
||||
#@+node:__new__
|
||||
def __new__(cls, src):
|
||||
#cls.__bases__[0] is the base of
|
||||
#the calendar specific StartDate and EndDate
|
||||
|
||||
if isinstance(src, cls.__bases__[0]) or type(src) in (int, float):
|
||||
return _CalendarItem.__new__(cls, src)
|
||||
|
||||
|
||||
src = cls.calendar.from_datetime(to_datetime(src))
|
||||
return _CalendarItem.__new__(cls, src)
|
||||
#@-node:__new__
|
||||
#@+node:__repr__
|
||||
def __repr__(self):
|
||||
return self.strftime()
|
||||
#@-node:__repr__
|
||||
#@+node:to_datetime
|
||||
def to_datetime(self):
|
||||
return self.to_starttime()
|
||||
#@-node:to_datetime
|
||||
#@+node:to_starttime
|
||||
def to_starttime(self):
|
||||
return self.calendar.to_starttime(self)
|
||||
#@-node:to_starttime
|
||||
#@+node:to_endtime
|
||||
def to_endtime(self):
|
||||
return self.calendar.to_endtime(self)
|
||||
#@-node:to_endtime
|
||||
#@+node:__cmp__
|
||||
def __cmp__(self, other):
|
||||
return cmp(int(self), int(self.__class__(other)))
|
||||
#@-node:__cmp__
|
||||
#@+node:__add__
|
||||
def __add__(self, other):
|
||||
try:
|
||||
return self.__class__(int(self) + int(self._minutes(other)))
|
||||
except ValueError, e:
|
||||
raise e
|
||||
except:
|
||||
return NotImplemented
|
||||
#@-node:__add__
|
||||
#@+node:__sub__
|
||||
def __sub__(self, other):
|
||||
if isinstance(other, (datetime.timedelta, str, _Minutes)):
|
||||
try:
|
||||
other = self._minutes(other)
|
||||
except:
|
||||
pass
|
||||
|
||||
if isinstance(other, self._minutes):
|
||||
return self.__class__(int(self) - int(other))
|
||||
|
||||
try:
|
||||
return self._minutes(int(self) - int(self.__class__(other)))
|
||||
except:
|
||||
return NotImplemented
|
||||
#@-node:__sub__
|
||||
#@+node:strftime
|
||||
def strftime(self, format=None):
|
||||
return strftime(self.to_datetime(), format or self.STR_FORMAT)
|
||||
#@-node:strftime
|
||||
#@-others
|
||||
#@-node:class _WorkingDateBase
|
||||
#@+node:class Calendar
|
||||
class Calendar(object):
|
||||
"""
|
||||
A calendar to specify working times and vacations.
|
||||
The calendars epoch start at 1.1.1979
|
||||
"""
|
||||
#@ << declarations >>
|
||||
#@+node:<< declarations >>
|
||||
# january the first must be a monday
|
||||
EPOCH = datetime.datetime(1979, 1, 1)
|
||||
minimum_time_unit = DEFAULT_MINIMUM_TIME_UNIT
|
||||
working_days_per_week = DEFAULT_WORKING_DAYS_PER_WEEK
|
||||
working_days_per_month = DEFAULT_WORKING_DAYS_PER_MONTH
|
||||
working_days_per_year = DEFAULT_WORKING_DAYS_PER_YEAR
|
||||
working_hours_per_day = DEFAULT_WORKING_HOURS_PER_DAY
|
||||
now = EPOCH
|
||||
|
||||
|
||||
#@-node:<< declarations >>
|
||||
#@nl
|
||||
#@ @+others
|
||||
#@+node:__init__
|
||||
def __init__(self):
|
||||
|
||||
self.time_spans = ()
|
||||
self._dt_num_can = ()
|
||||
self._num_dt_can = ()
|
||||
self.working_times = { }
|
||||
self._recalc_working_time()
|
||||
self._make_classes()
|
||||
#@-node:__init__
|
||||
#@+node:__or__
|
||||
def __or__(self, other):
|
||||
if isinstance(other, Calendar):
|
||||
return union(self, other)
|
||||
|
||||
return NotImplemented
|
||||
#@nonl
|
||||
#@-node:__or__
|
||||
#@+node:clone
|
||||
def clone(self):
|
||||
result = Calendar()
|
||||
result.working_times = self.working_times.copy()
|
||||
result.time_spans = self.time_spans
|
||||
result._recalc_working_time()
|
||||
result._build_mapping()
|
||||
return result
|
||||
#@nonl
|
||||
#@-node:clone
|
||||
#@+node:set_working_days
|
||||
def set_working_days(self, day_range, trange, *further_tranges):
|
||||
"""
|
||||
Sets the working days of an calendar
|
||||
day_range is a string of day abbreviations like 'mon, tue'
|
||||
trange and further_tranges is a time range string like
|
||||
'8:00-10:00'
|
||||
"""
|
||||
time_ranges = [ trange ] + list(further_tranges)
|
||||
time_ranges = filter(bool, map(to_time_range, time_ranges))
|
||||
days = _to_days(day_range)
|
||||
|
||||
for k in days.keys():
|
||||
self.working_times[k] = time_ranges
|
||||
|
||||
self._recalc_working_time()
|
||||
self._build_mapping()
|
||||
#@-node:set_working_days
|
||||
#@+node:set_vacation
|
||||
def set_vacation(self, value):
|
||||
"""
|
||||
Sets vacation time.
|
||||
value is either a datetime literal or
|
||||
a sequence of items that can be
|
||||
a datetime literals and or pair of datetime literals
|
||||
"""
|
||||
self.time_spans = _add_to_time_spans(self.time_spans, value, True)
|
||||
self._build_mapping()
|
||||
#@-node:set_vacation
|
||||
#@+node:set_extra_work
|
||||
def set_extra_work(self, value):
|
||||
"""
|
||||
Sets extra working time
|
||||
value is either a datetime literal or
|
||||
a sequence of items that can be
|
||||
a datetime literals and or pair of datetime literals
|
||||
"""
|
||||
self.time_spans = _add_to_time_spans(self.time_spans, value, False)
|
||||
self._build_mapping()
|
||||
#@-node:set_extra_work
|
||||
#@+node:from_datetime
|
||||
def from_datetime(self, value):
|
||||
assert(isinstance(value, datetime.datetime))
|
||||
delta = value - self.EPOCH
|
||||
days = delta.days
|
||||
minutes = delta.seconds / 60
|
||||
|
||||
# calculate the weektime
|
||||
weeks = days / 7
|
||||
wtime = self.week_time * weeks
|
||||
|
||||
# calculate the daytime
|
||||
days %= 7
|
||||
dtime = sum(self.day_times[:days])
|
||||
|
||||
# calculate the minute time
|
||||
slots = self.working_times.get(days, DEFAULT_WORKING_DAYS[days])
|
||||
mtime = 0
|
||||
for start, end in slots:
|
||||
if minutes > end:
|
||||
mtime += end - start
|
||||
else:
|
||||
if minutes > start:
|
||||
mtime += minutes - start
|
||||
break
|
||||
|
||||
result = wtime + dtime + mtime
|
||||
|
||||
# map exceptional timespans
|
||||
dt_num_can = self._dt_num_can
|
||||
pos = bisect.bisect(dt_num_can, (value,)) - 1
|
||||
if pos >= 0:
|
||||
start, end, nstart, nend, cend = dt_num_can[pos]
|
||||
if value < end:
|
||||
if nstart < nend:
|
||||
delta = value - start
|
||||
delta = delta.days * 24 * 60 + delta.seconds / 60
|
||||
result = nstart + delta
|
||||
else:
|
||||
result = nstart
|
||||
else:
|
||||
result += (nend - cend) # == (result - cend) + nend
|
||||
|
||||
return result
|
||||
#@-node:from_datetime
|
||||
#@+node:split_time
|
||||
def split_time(self, value):
|
||||
#map exceptional timespans
|
||||
num_dt_can = self._num_dt_can
|
||||
pos = bisect.bisect(num_dt_can, (value, sys.maxint)) - 1
|
||||
if pos >= 0:
|
||||
nstart, nend, start, end, cend = num_dt_can[pos]
|
||||
if value < nend:
|
||||
value = start + datetime.timedelta(minutes=value - nstart)
|
||||
delta = value - self.EPOCH
|
||||
return delta.days / 7, delta.days % 7, delta.seconds / 60, -1
|
||||
else:
|
||||
value += (cend - nend) # (value - nend + cend)
|
||||
#calculate the weeks since the epoch
|
||||
|
||||
weeks = value / self.week_time
|
||||
value %= self.week_time
|
||||
|
||||
#calculate the remaining days
|
||||
days = 0
|
||||
for day_time in self.day_times:
|
||||
if value < day_time: break
|
||||
value -= day_time
|
||||
days += 1
|
||||
|
||||
#calculate the remaining minutes
|
||||
minutes = 0
|
||||
slots = self.working_times.get(days, DEFAULT_WORKING_DAYS[days])
|
||||
index = 0
|
||||
for start, end in slots:
|
||||
delta = end - start
|
||||
if delta > value:
|
||||
minutes = start + value
|
||||
break
|
||||
else:
|
||||
value -= delta
|
||||
|
||||
index += 1
|
||||
|
||||
return weeks, days, minutes, index
|
||||
#@-node:split_time
|
||||
#@+node:to_starttime
|
||||
def to_starttime(self, value):
|
||||
weeks, days, minutes, index = self.split_time(value)
|
||||
return self.EPOCH + datetime.timedelta(weeks=weeks,
|
||||
days=days,
|
||||
minutes=minutes)
|
||||
#@-node:to_starttime
|
||||
#@+node:to_endtime
|
||||
def to_endtime(self, value):
|
||||
return self.to_starttime(value - 1) + datetime.timedelta(minutes=1)
|
||||
#@-node:to_endtime
|
||||
#@+node:get_working_times
|
||||
def get_working_times(self, day):
|
||||
return self.working_times.get(day, DEFAULT_WORKING_DAYS[day])
|
||||
#@-node:get_working_times
|
||||
#@+node:_build_mapping
|
||||
def _build_mapping(self):
|
||||
self._dt_num_can = self._num_dt_can = ()
|
||||
dt_num_can = []
|
||||
num_dt_can = []
|
||||
|
||||
delta = self.Minutes()
|
||||
for start, end, is_free in self.time_spans:
|
||||
cstart = self.StartDate(start)
|
||||
cend = self.EndDate(end)
|
||||
nstart = cstart + delta
|
||||
|
||||
if not is_free:
|
||||
d = end - start
|
||||
d = d.days * 24 * 60 + d.seconds / 60
|
||||
nend = nstart + d
|
||||
else:
|
||||
nend = nstart
|
||||
|
||||
delta += (nend - nstart) - (cend - cstart)
|
||||
dt_num_can.append((start, end, nstart, nend, cend))
|
||||
num_dt_can.append((nstart, nend, start, end, cend))
|
||||
|
||||
self._dt_num_can = tuple(dt_num_can)
|
||||
self._num_dt_can = tuple(num_dt_can)
|
||||
|
||||
#@-node:_build_mapping
|
||||
#@+node:_recalc_working_time
|
||||
def _recalc_working_time(self):
|
||||
def slot_sum_time(day):
|
||||
slots = self.working_times.get(day, DEFAULT_WORKING_DAYS[day])
|
||||
return sum(map(lambda slot: slot[1] - slot[0], slots))
|
||||
|
||||
self.day_times = map(slot_sum_time, range(0, 7))
|
||||
self.week_time = sum(self.day_times)
|
||||
|
||||
#@-node:_recalc_working_time
|
||||
#@+node:_make_classes
|
||||
def _make_classes(self):
|
||||
#ensure that the clases are instance specific
|
||||
class minutes(_Minutes):
|
||||
calendar = self
|
||||
__slots__ = ()
|
||||
|
||||
class db(_WorkingDateBase):
|
||||
calendar = self
|
||||
_minutes = minutes
|
||||
__slots__ = ()
|
||||
|
||||
class wdt(db): __slots__ = ()
|
||||
class edt(db):
|
||||
__slots__ = ()
|
||||
|
||||
def to_datetime(self):
|
||||
return self.to_endtime()
|
||||
|
||||
self.Minutes, self.StartDate, self.EndDate = minutes, wdt, edt
|
||||
|
||||
self.WorkingDate = self.StartDate
|
||||
|
||||
#@-node:_make_classes
|
||||
#@-others
|
||||
|
||||
|
||||
_default_calendar = Calendar()
|
||||
|
||||
WorkingDate = _default_calendar.WorkingDate
|
||||
StartDate = _default_calendar.StartDate
|
||||
EndDate = _default_calendar.EndDate
|
||||
Minutes = _default_calendar.Minutes
|
||||
#@-node:class Calendar
|
||||
#@-others
|
||||
|
||||
if __name__ == '__main__':
|
||||
cal = Calendar()
|
||||
|
||||
start = EndDate("10.1.2005")
|
||||
|
||||
delay = Minutes("4H")
|
||||
|
||||
start2 = cal.StartDate(start)
|
||||
|
||||
start3 = cal.StartDate("10.1.2005")
|
||||
#@-node:@file pcalendar.py
|
||||
#@-leo
|
||||
@@ -1,54 +0,0 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2005 by Reithinger GmbH
|
||||
# mreithinger@web.de
|
||||
#
|
||||
# This file is part of faces.
|
||||
#
|
||||
# faces is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# faces 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
############################################################################
|
||||
|
||||
import gettext
|
||||
import os.path
|
||||
import locale
|
||||
import sys
|
||||
|
||||
def _get_translation():
|
||||
try:
|
||||
return gettext.translation("faces")
|
||||
except:
|
||||
try:
|
||||
if sys.frozen:
|
||||
path = os.path.dirname(sys.argv[0])
|
||||
path = os.path.join(path, "resources", "faces", "locale")
|
||||
else:
|
||||
path = os.path.split(__file__)[0]
|
||||
path = os.path.join(path, "locale")
|
||||
|
||||
return gettext.translation("faces", path)
|
||||
except Exception, e:
|
||||
return None
|
||||
|
||||
def get_gettext():
|
||||
trans = _get_translation()
|
||||
if trans: return trans.ugettext
|
||||
return lambda msg: msg
|
||||
|
||||
|
||||
def get_encoding():
|
||||
trans = _get_translation()
|
||||
if trans: return trans.charset()
|
||||
return locale.getpreferredencoding()
|
||||
|
||||
@@ -1,866 +0,0 @@
|
||||
#@+leo-ver=4
|
||||
#@+node:@file resource.py
|
||||
#@@language python
|
||||
#@<< Copyright >>
|
||||
#@+node:<< Copyright >>
|
||||
############################################################################
|
||||
# Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
|
||||
# mreithinger@web.de
|
||||
#
|
||||
# This file is part of faces.
|
||||
#
|
||||
# faces is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# faces 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
############################################################################
|
||||
|
||||
#@-node:<< Copyright >>
|
||||
#@nl
|
||||
#@<< Imports >>
|
||||
#@+node:<< Imports >>
|
||||
import pcalendar
|
||||
import datetime
|
||||
import utils
|
||||
import string
|
||||
import bisect
|
||||
import plocale
|
||||
#@-node:<< Imports >>
|
||||
#@nl
|
||||
|
||||
_is_source = True
|
||||
_to_datetime = pcalendar.to_datetime
|
||||
_ = plocale.get_gettext()
|
||||
|
||||
#@+others
|
||||
#@+node:_isattrib
|
||||
#@+doc
|
||||
#@nonl
|
||||
# is used to find snapshot attributes
|
||||
#@-doc
|
||||
#@@code
|
||||
def _isattrib(obj, a):
|
||||
return a[0] != "_" \
|
||||
and not callable(getattr(obj, a)) \
|
||||
and not a.endswith("_members") \
|
||||
and a not in ("name")
|
||||
#@-node:_isattrib
|
||||
#@+node:class ResourceCalendar
|
||||
class ResourceCalendar(object):
|
||||
"""
|
||||
The resource calendar saves the load time of a resource.
|
||||
Is ia sequence of time intervals of loads. An example of
|
||||
such a sequence is:
|
||||
[ (datetime.min, 0),
|
||||
(2006/1/1, 1.0),
|
||||
(2006/1/10, 0.5),
|
||||
(2006/1/15, 0) ]
|
||||
|
||||
That means the resource:
|
||||
is free till january the first 2006
|
||||
is fully booked from january the first to january 10th
|
||||
is half booked from january 10th to january 15th
|
||||
is free since january 15th
|
||||
"""
|
||||
|
||||
#@ @+others
|
||||
#@+node:__init__
|
||||
def __init__(self, src=None):
|
||||
if src:
|
||||
self.bookings = list(src.bookings)
|
||||
else:
|
||||
self.bookings = [ (datetime.datetime.min, 0) ]
|
||||
#@-node:__init__
|
||||
#@+node:__str__
|
||||
def __str__(self):
|
||||
return str(self.bookings)
|
||||
#@-node:__str__
|
||||
#@+node:__repr__
|
||||
def __repr__(self):
|
||||
return "<ResourceCalendar %s>" % (str(self))
|
||||
#@-node:__repr__
|
||||
#@+node:add_load
|
||||
def add_load(self, start, end, load):
|
||||
start = _to_datetime(start)
|
||||
end = _to_datetime(end)
|
||||
|
||||
bookings = self.bookings
|
||||
|
||||
# the load will be converted in an integer to avoid
|
||||
# rouning problems
|
||||
load = int(load * 10000)
|
||||
|
||||
start_item = (start, 0)
|
||||
start_pos = bisect.bisect_left(bookings, start_item)
|
||||
|
||||
left_load = 0
|
||||
left_load = bookings[start_pos - 1][1]
|
||||
|
||||
if start_pos < len(bookings) and bookings[start_pos][0] == start:
|
||||
prev_load = bookings[start_pos][1]
|
||||
if prev_load + load == left_load:
|
||||
del bookings[start_pos]
|
||||
else:
|
||||
bookings[start_pos] = (start, prev_load + load)
|
||||
start_pos += 1
|
||||
else:
|
||||
bookings.insert(start_pos, (start, load + left_load))
|
||||
start_pos += 1
|
||||
|
||||
item = (datetime.datetime.min, 0)
|
||||
for i in range(start_pos, len(bookings)):
|
||||
end_pos = i
|
||||
item = bookings[i]
|
||||
if item[0] >= end: break
|
||||
bookings[i] = (item[0], item[1] + load)
|
||||
else:
|
||||
end_pos = len(bookings)
|
||||
|
||||
left_load = bookings[end_pos - 1][1]
|
||||
if item[0] == end:
|
||||
if item[1] == left_load:
|
||||
del bookings[end_pos]
|
||||
else:
|
||||
bookings.insert(end_pos, (end, left_load - load))
|
||||
#@-node:add_load
|
||||
#@+node:end_of_booking_interval
|
||||
def end_of_booking_interval(self, date):
|
||||
date = _to_datetime(date)
|
||||
bookings = self.bookings
|
||||
date_item = (date, 999999)
|
||||
date_pos = bisect.bisect_left(bookings, date_item) - 1
|
||||
next_date = datetime.datetime.max
|
||||
load = 0
|
||||
|
||||
try:
|
||||
book_item = bookings[date_pos]
|
||||
load = bookings[date_pos][1] / 10000.0
|
||||
next_date = bookings[date_pos + 1][0]
|
||||
except:
|
||||
pass
|
||||
|
||||
return next_date, load
|
||||
#@-node:end_of_booking_interval
|
||||
#@+node:find_free_time
|
||||
def find_free_time(self, start, length, load, max_load):
|
||||
bookings = self.bookings
|
||||
|
||||
if isinstance(start, datetime.datetime):
|
||||
adjust_date = _to_datetime
|
||||
else:
|
||||
adjust_date = start.calendar.EndDate
|
||||
|
||||
start = _to_datetime(start)
|
||||
load = int(load * 10000)
|
||||
max_load = int(max_load * 10000)
|
||||
lb = len(bookings)
|
||||
|
||||
def next_possible(index):
|
||||
while index < lb:
|
||||
sd, lo = bookings[index]
|
||||
if lo + load <= max_load:
|
||||
break
|
||||
|
||||
index += 1
|
||||
|
||||
sd = adjust_date(max(start, sd))
|
||||
ed = sd + length
|
||||
end = _to_datetime(ed)
|
||||
|
||||
index += 1
|
||||
while index < lb:
|
||||
date, lo = bookings[index]
|
||||
|
||||
if date >= end:
|
||||
#I found a good start date
|
||||
return None, sd
|
||||
|
||||
if lo + load > max_load:
|
||||
return index + 1, None
|
||||
|
||||
index += 1
|
||||
|
||||
return None, sd
|
||||
|
||||
start_item = (start, 1000000)
|
||||
i = bisect.bisect_left(bookings, start_item) - 1
|
||||
|
||||
next_start = None
|
||||
while not next_start and i < lb:
|
||||
i, next_start = next_possible(i)
|
||||
|
||||
assert(next_start is not None)
|
||||
return next_start
|
||||
#@-node:find_free_time
|
||||
#@+node:get_bookings
|
||||
def get_bookings(self, start, end):
|
||||
start = _to_datetime(start)
|
||||
end = _to_datetime(end)
|
||||
bookings = self.bookings
|
||||
start_item = (start, 0)
|
||||
start_pos = bisect.bisect_left(bookings, start_item)
|
||||
if start_pos >= len(bookings) or bookings[start_pos][0] > start:
|
||||
start_pos -= 1
|
||||
|
||||
end_item = (end, 0)
|
||||
end_pos = bisect.bisect_left(bookings, end_item)
|
||||
return start_pos, end_pos, bookings
|
||||
#@-node:get_bookings
|
||||
#@+node:get_load
|
||||
def get_load(self, date):
|
||||
date = _to_datetime(date)
|
||||
bookings = self.bookings
|
||||
item = (date, 100000)
|
||||
pos = bisect.bisect_left(bookings, item) - 1
|
||||
return bookings[pos][1] / 10000.0
|
||||
#@-node:get_load
|
||||
#@-others
|
||||
#@-node:class ResourceCalendar
|
||||
#@+node:class _ResourceBase
|
||||
class _ResourceBase(object):
|
||||
pass
|
||||
|
||||
#@-node:class _ResourceBase
|
||||
#@+node:class _MetaResource
|
||||
class _MetaResource(type):
|
||||
doc_template = """
|
||||
A resource class. The resources default attributes can
|
||||
be changed when the class ist instanciated, i.e.
|
||||
%(name)s(max_load=2.0)
|
||||
|
||||
@var max_load:
|
||||
Specify the maximal allowed load sum of all simultaneously
|
||||
allocated tasks of a resource. A ME{max_load} of 1.0 (default)
|
||||
means the resource may be fully allocated. A ME{max_load} of 1.3
|
||||
means the resource may be allocated with 30%% overtime.
|
||||
|
||||
@var title:
|
||||
Specifies an alternative more descriptive name for the task.
|
||||
|
||||
@var efficiency:
|
||||
The efficiency of a resource can be used for two purposes. First
|
||||
you can use it as a crude way to model a team. A team of 5 people
|
||||
should have an efficiency of 5.0. Keep in mind that you cannot
|
||||
track the member of the team individually if you use this
|
||||
feature. The other use is to model performance variations between
|
||||
your resources.
|
||||
|
||||
@var vacation:
|
||||
Specifies the vacation of the resource. This attribute is
|
||||
specified as a list of date literals or date literal intervals.
|
||||
Be aware that the end of an interval is excluded, i.e. it is
|
||||
the first working date.
|
||||
"""
|
||||
|
||||
#@ @+others
|
||||
#@+node:__init__
|
||||
def __init__(self, name, bases, dict_):
|
||||
super(_MetaResource, self).__init__(name, bases, dict_)
|
||||
self.name = name
|
||||
self.title = dict_.get("title", name)
|
||||
self._calendar = { None: ResourceCalendar() }
|
||||
self._tasks = { }
|
||||
self.__set_vacation()
|
||||
self.__add_resource(bases[0])
|
||||
self.__doc__ = dict_.get("__doc__", self.doc_template) % locals()
|
||||
#@-node:__init__
|
||||
#@+node:__or__
|
||||
def __or__(self, other):
|
||||
return self().__or__(other)
|
||||
#@-node:__or__
|
||||
#@+node:__and__
|
||||
def __and__(self, other):
|
||||
return self().__and__(other)
|
||||
#@-node:__and__
|
||||
#@+node:__cmp__
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.name, getattr(other, "name", None))
|
||||
#@-node:__cmp__
|
||||
#@+node:__repr__
|
||||
def __repr__(self):
|
||||
return "<Resource %s>" % self.name
|
||||
#@-node:__repr__
|
||||
#@+node:__str__
|
||||
def __str__(self):
|
||||
return repr(self)
|
||||
#@-node:__str__
|
||||
#@+node:__set_vacation
|
||||
def __set_vacation(self):
|
||||
vacation = self.vacation
|
||||
|
||||
if isinstance(vacation, (tuple, list)):
|
||||
for v in vacation:
|
||||
if isinstance(v, (tuple, list)):
|
||||
self.add_vacation(v[0], v[1])
|
||||
else:
|
||||
self.add_vacation(v)
|
||||
else:
|
||||
self.add_vacation(vacation)
|
||||
#@-node:__set_vacation
|
||||
#@+node:__add_resource
|
||||
def __add_resource(self, base):
|
||||
if issubclass(base, _ResourceBase):
|
||||
members = getattr(base, base.__name__ + "_members", [])
|
||||
members.append(self)
|
||||
setattr(base, base.__name__ + "_members", members)
|
||||
#@-node:__add_resource
|
||||
#@+node:get_members
|
||||
def get_members(self):
|
||||
return getattr(self, self.__name__ + "_members", [])
|
||||
#@-node:get_members
|
||||
#@+node:add_vacation
|
||||
def add_vacation(self, start, end=None):
|
||||
start_date = _to_datetime(start)
|
||||
|
||||
if not end:
|
||||
end_date = start_date.replace(hour=23, minute=59)
|
||||
else:
|
||||
end_date = _to_datetime(end)
|
||||
|
||||
for cal in self._calendar.itervalues():
|
||||
cal.add_load(start_date, end_date, 1)
|
||||
|
||||
tp = Booking()
|
||||
tp.start = start_date
|
||||
tp.end = end_date
|
||||
tp.book_start = start_date
|
||||
tp.book_end = end_date
|
||||
tp.work_time = end_date - start_date
|
||||
tp.load = 1.0
|
||||
tp.name = tp.title = _("(vacation)")
|
||||
tp._id = ""
|
||||
self._tasks.setdefault("", []).append(tp)
|
||||
#@-node:add_vacation
|
||||
#@+node:calendar
|
||||
def calendar(self, scenario):
|
||||
try:
|
||||
return self._calendar[scenario]
|
||||
except KeyError:
|
||||
cal = self._calendar[scenario] = ResourceCalendar(self._calendar[None])
|
||||
return cal
|
||||
|
||||
#@-node:calendar
|
||||
#@-others
|
||||
#@-node:class _MetaResource
|
||||
#@+node:make_team
|
||||
def make_team(resource):
|
||||
members = resource.get_members()
|
||||
if not members:
|
||||
return resource
|
||||
|
||||
result = make_team(members[0])
|
||||
for r in members[1:]:
|
||||
result = result & make_team(r)
|
||||
|
||||
return result
|
||||
#@-node:make_team
|
||||
#@+node:class Booking
|
||||
class Booking(object):
|
||||
"""
|
||||
A booking unit for a task.
|
||||
"""
|
||||
#@ << declarations >>
|
||||
#@+node:<< declarations >>
|
||||
book_start = datetime.datetime.min
|
||||
book_end = datetime.datetime.max
|
||||
actual = False
|
||||
_id = ""
|
||||
|
||||
#@-node:<< declarations >>
|
||||
#@nl
|
||||
#@ @+others
|
||||
#@+node:__init__
|
||||
def __init__(self, task=None):
|
||||
self.__task = task
|
||||
#@-node:__init__
|
||||
#@+node:__cmp__
|
||||
def __cmp__(self, other):
|
||||
return cmp(self._id, other._id)
|
||||
#@-node:__cmp__
|
||||
#@+node:path
|
||||
def path(self):
|
||||
first_dot = self._id.find(".")
|
||||
return "root" + self._id[first_dot:]
|
||||
|
||||
path = property(path)
|
||||
#@nonl
|
||||
#@-node:path
|
||||
#@+node:_idendity_
|
||||
def _idendity_(self):
|
||||
return self._id
|
||||
#@-node:_idendity_
|
||||
#@+node:__getattr__
|
||||
def __getattr__(self, name):
|
||||
if self.__task:
|
||||
return getattr(self.__task, name)
|
||||
|
||||
raise AttributeError("'%s' is not a valid attribute" % (name))
|
||||
#@-node:__getattr__
|
||||
#@-others
|
||||
#@-node:class Booking
|
||||
#@+node:class ResourceList
|
||||
class ResourceList(list):
|
||||
#@ @+others
|
||||
#@+node:__init__
|
||||
def __init__(self, *args):
|
||||
if args: self.extend(args)
|
||||
#@-node:__init__
|
||||
#@-others
|
||||
#@-node:class ResourceList
|
||||
#@+node:class Resource
|
||||
class Resource(_ResourceBase):
|
||||
#@ << declarations >>
|
||||
#@+node:<< declarations >>
|
||||
__metaclass__ = _MetaResource
|
||||
__attrib_completions__ = {\
|
||||
"max_load": 'max_load = ',
|
||||
"title": 'title = "|"',
|
||||
"efficiency": 'efficiency = ',
|
||||
"vacation": 'vacation = [("|2002-02-01", "2002-02-05")]' }
|
||||
|
||||
__type_image__ = "resource16"
|
||||
|
||||
max_load = None # the maximum sum load for all task
|
||||
vacation = ()
|
||||
efficiency = 1.0
|
||||
|
||||
|
||||
#@-node:<< declarations >>
|
||||
#@nl
|
||||
#@ @+others
|
||||
#@+node:__init__
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(self, k, v)
|
||||
#@-node:__init__
|
||||
#@+node:_idendity_
|
||||
def _idendity_(cls):
|
||||
return "resource:" + cls.__name__
|
||||
|
||||
_idendity_ = classmethod(_idendity_)
|
||||
#@-node:_idendity_
|
||||
#@+node:__repr__
|
||||
def __repr__(self):
|
||||
return "<Resource %s>" % self.__class__.__name__
|
||||
#@-node:__repr__
|
||||
#@+node:__str__
|
||||
def __str__(self):
|
||||
return repr(self)
|
||||
#@-node:__str__
|
||||
#@+node:__call__
|
||||
def __call__(self):
|
||||
return self
|
||||
#@-node:__call__
|
||||
#@+node:__hash__
|
||||
def __hash__(self):
|
||||
return hash(self.__class__)
|
||||
#@-node:__hash__
|
||||
#@+node:__cmp__
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.name, other.name)
|
||||
#@-node:__cmp__
|
||||
#@+node:__or__
|
||||
def __or__(self, other):
|
||||
if type(other) is _MetaResource:
|
||||
other = other()
|
||||
|
||||
result = Resource()
|
||||
result._subresource = _OrResourceGroup(self, other)
|
||||
return result
|
||||
#@-node:__or__
|
||||
#@+node:__and__
|
||||
def __and__(self, other):
|
||||
if type(other) is _MetaResource:
|
||||
other = other()
|
||||
|
||||
result = Resource()
|
||||
result._subresource = _AndResourceGroup(self, other)
|
||||
return result
|
||||
#@-node:__and__
|
||||
#@+node:_permutation_count
|
||||
def _permutation_count(self):
|
||||
if hasattr(self, "_subresource"):
|
||||
return self._subresource._permutation_count()
|
||||
|
||||
return 1
|
||||
#@-node:_permutation_count
|
||||
#@+node:_get_resources
|
||||
def _get_resources(self, state):
|
||||
if hasattr(self, "_subresource"):
|
||||
result = self._subresource._get_resources(state)
|
||||
|
||||
if self.name != "Resource":
|
||||
result.name = self.name
|
||||
|
||||
if self.title != "Resource":
|
||||
result.title = self.title
|
||||
|
||||
return result
|
||||
|
||||
result = ResourceList(self)
|
||||
return result
|
||||
#@-node:_get_resources
|
||||
#@+node:all_members
|
||||
def all_members(self):
|
||||
if hasattr(self, "_subresource"):
|
||||
return self._subresource.all_members()
|
||||
|
||||
return [ self.__class__ ]
|
||||
#@-node:all_members
|
||||
#@+node:unbook_tasks_of_project
|
||||
def unbook_tasks_of_project(cls, project_id, scenario):
|
||||
try:
|
||||
task_list = cls._tasks[scenario]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
add_load = cls.calendar(scenario).add_load
|
||||
for task_id, bookings in task_list.items():
|
||||
if task_id.startswith(project_id):
|
||||
for item in bookings:
|
||||
add_load(item.book_start, item.book_end, -item.load)
|
||||
|
||||
del task_list[task_id]
|
||||
|
||||
if not task_list:
|
||||
del cls._tasks[scenario]
|
||||
|
||||
unbook_tasks_of_project = classmethod(unbook_tasks_of_project)
|
||||
#@-node:unbook_tasks_of_project
|
||||
#@+node:unbook_task
|
||||
def unbook_task(cls, task):
|
||||
identdity = task._idendity_()
|
||||
scenario = task.scenario
|
||||
|
||||
try:
|
||||
task_list = cls._tasks[scenario]
|
||||
bookings = task_list[identdity]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
add_load = cls.calendar(scenario).add_load
|
||||
for b in bookings:
|
||||
add_load(b.book_start, b.book_end, -b.load)
|
||||
|
||||
del task_list[identdity]
|
||||
if not task_list:
|
||||
del cls._tasks[scenario]
|
||||
|
||||
unbook_task = classmethod(unbook_task)
|
||||
#@-node:unbook_task
|
||||
#@+node:correct_bookings
|
||||
def correct_bookings(cls, task):
|
||||
#correct the booking data with the actual task data
|
||||
try:
|
||||
tasks = cls._tasks[task.scenario][task._idendity_()]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
for t in tasks:
|
||||
t.start = task.start.to_datetime()
|
||||
t.end = task.end.to_datetime()
|
||||
|
||||
correct_bookings = classmethod(correct_bookings)
|
||||
#@-node:correct_bookings
|
||||
#@+node:book_task
|
||||
def book_task(cls, task, start, end, load, work_time, actual):
|
||||
if not work_time: return
|
||||
|
||||
start = _to_datetime(start)
|
||||
end = _to_datetime(end)
|
||||
|
||||
identdity = task._idendity_()
|
||||
task_list = cls._tasks.setdefault(task.scenario, {})
|
||||
bookings = task_list.setdefault(identdity, [])
|
||||
add_load = cls.calendar(task.scenario).add_load
|
||||
|
||||
tb = Booking(task)
|
||||
tb.book_start = start
|
||||
tb.book_end = end
|
||||
tb._id = identdity
|
||||
tb.load = load
|
||||
tb.start = _to_datetime(task.start)
|
||||
tb.end = _to_datetime(task.end)
|
||||
tb.title = task.title
|
||||
tb.name = task.name
|
||||
tb.work_time = int(work_time)
|
||||
tb.actual = actual
|
||||
bookings.append(tb)
|
||||
result = add_load(start, end, load)
|
||||
return result
|
||||
|
||||
book_task = classmethod(book_task)
|
||||
#@-node:book_task
|
||||
#@+node:length_of
|
||||
def length_of(cls, task):
|
||||
cal = task.root.calendar
|
||||
bookings = cls.get_bookings(task)
|
||||
return sum(map(lambda b: task._to_delta(b.work_time).round(), bookings))
|
||||
|
||||
length_of = classmethod(length_of)
|
||||
#@-node:length_of
|
||||
#@+node:done_of
|
||||
def done_of(self, task):
|
||||
cal = task.root.calendar
|
||||
now = cal.now
|
||||
bookings = self.get_bookings(task)
|
||||
|
||||
if task.__dict__.has_key("effort"):
|
||||
efficiency = self.efficiency * task.efficiency
|
||||
else:
|
||||
efficiency = 1
|
||||
|
||||
def book_done(booking):
|
||||
if booking.book_start >= now:
|
||||
return 0
|
||||
|
||||
factor = 1
|
||||
if booking.book_end > now:
|
||||
start = task._to_start(booking.book_start)
|
||||
end = task._to_end(booking.book_end)
|
||||
cnow = task._to_start(now)
|
||||
factor = float(cnow - start) / ((end - start) or 1)
|
||||
|
||||
return factor * booking.work_time * efficiency
|
||||
|
||||
return task._to_delta(sum(map(book_done, bookings)))
|
||||
#@-node:done_of
|
||||
#@+node:todo_of
|
||||
def todo_of(self, task):
|
||||
cal = task.root.calendar
|
||||
now = cal.now
|
||||
|
||||
bookings = self.get_bookings(task)
|
||||
if task.__dict__.has_key("effort"):
|
||||
efficiency = self.efficiency * task.efficiency
|
||||
else:
|
||||
efficiency = 1
|
||||
|
||||
def book_todo(booking):
|
||||
if booking.book_end <= now:
|
||||
return 0
|
||||
|
||||
factor = 1
|
||||
if booking.book_start < now:
|
||||
start = task._to_start(booking.book_start)
|
||||
end = task._to_end(booking.book_end)
|
||||
cnow = task._to_start(now)
|
||||
factor = float(end - cnow) / ((end - start) or 1)
|
||||
|
||||
return factor * booking.work_time * efficiency
|
||||
|
||||
return task._to_delta(sum(map(book_todo, bookings)))
|
||||
#@-node:todo_of
|
||||
#@+node:get_bookings
|
||||
def get_bookings(cls, task):
|
||||
return cls._tasks.get(task.scenario, {}).get(task._idendity_(), ())
|
||||
|
||||
get_bookings = classmethod(get_bookings)
|
||||
#@-node:get_bookings
|
||||
#@+node:get_bookings_at
|
||||
def get_bookings_at(cls, start, end, scenario):
|
||||
result = []
|
||||
|
||||
try:
|
||||
items = cls._tasks[scenario].iteritems()
|
||||
except KeyError:
|
||||
return ()
|
||||
|
||||
for task_id, bookings in items:
|
||||
result += [ booking for booking in bookings
|
||||
if booking.book_start < end
|
||||
and booking.book_end > start ]
|
||||
|
||||
vacations = cls._tasks.get("", ())
|
||||
result += [ booking for booking in vacations
|
||||
if booking.book_start < end
|
||||
and booking.book_end > start ]
|
||||
|
||||
return result
|
||||
|
||||
get_bookings_at = classmethod(get_bookings_at)
|
||||
#@-node:get_bookings_at
|
||||
#@+node:find_free_time
|
||||
def find_free_time(cls, start, length, load, max_load, scenario):
|
||||
return cls.calendar(scenario).find_free_time(start, length, load, max_load)
|
||||
|
||||
find_free_time = classmethod(find_free_time)
|
||||
#@-node:find_free_time
|
||||
#@+node:get_load
|
||||
def get_load(cls, date, scenario):
|
||||
return cls.calendar(scenario).get_load(date)
|
||||
|
||||
get_load = classmethod(get_load)
|
||||
#@-node:get_load
|
||||
#@+node:end_of_booking_interval
|
||||
def end_of_booking_interval(cls, date, task):
|
||||
return cls.calendar(task.scenario).end_of_booking_interval(date)
|
||||
|
||||
end_of_booking_interval = classmethod(end_of_booking_interval)
|
||||
#@-node:end_of_booking_interval
|
||||
#@+node:snapshot
|
||||
def snapshot(self):
|
||||
from task import _as_string
|
||||
def isattrib(a):
|
||||
if a == "max_load" and self.max_load is None: return False
|
||||
if a in ("name", "title", "vacation"): return False
|
||||
return _isattrib(self, a)
|
||||
|
||||
attribs = filter(isattrib, dir(self))
|
||||
attribs = map(lambda a: "%s=%s" % (a, _as_string(getattr(self, a))),
|
||||
attribs)
|
||||
|
||||
return self.name + "(%s)" % ", ".join(attribs)
|
||||
#@-node:snapshot
|
||||
#@-others
|
||||
#@-node:class Resource
|
||||
#@+node:class _ResourceGroup
|
||||
|
||||
|
||||
class _ResourceGroup(object):
|
||||
#@ @+others
|
||||
#@+node:__init__
|
||||
def __init__(self, *args):
|
||||
self.resources = []
|
||||
for a in args:
|
||||
self.__append(a)
|
||||
#@-node:__init__
|
||||
#@+node:all_members
|
||||
def all_members(self):
|
||||
group = reduce(lambda a, b: a + b.all_members(),
|
||||
self.resources, [])
|
||||
group = map(lambda r: (r, True), group)
|
||||
group = dict(group)
|
||||
group = group.keys()
|
||||
return group
|
||||
#@-node:all_members
|
||||
#@+node:_permutation_count
|
||||
def _permutation_count(self):
|
||||
abstract
|
||||
#@-node:_permutation_count
|
||||
#@+node:_refactor
|
||||
def _refactor(self, arg):
|
||||
pass
|
||||
#@-node:_refactor
|
||||
#@+node:__append
|
||||
def __append(self, arg):
|
||||
if isinstance(arg, self.__class__):
|
||||
self.resources += arg.resources
|
||||
for r in arg.resources:
|
||||
self._refactor(r)
|
||||
return
|
||||
elif isinstance(arg, Resource):
|
||||
subresources = getattr(arg, "_subresource", None)
|
||||
if subresources:
|
||||
self.__append(subresources)
|
||||
return
|
||||
else:
|
||||
self.resources.append(arg)
|
||||
else:
|
||||
assert(isinstance(arg, _ResourceGroup))
|
||||
self.resources.append(arg)
|
||||
|
||||
self._refactor(arg)
|
||||
#@-node:__append
|
||||
#@+node:__str__
|
||||
def __str__(self):
|
||||
op = lower(self.__class__.__name__[0:-13])
|
||||
return "(" + \
|
||||
string.join([str(r) for r in self.resources],
|
||||
" " + op + " ") + \
|
||||
")"
|
||||
#@-node:__str__
|
||||
#@-others
|
||||
#@-node:class _ResourceGroup
|
||||
#@+node:class _OrResourceGroup
|
||||
|
||||
|
||||
class _OrResourceGroup(_ResourceGroup):
|
||||
#@ @+others
|
||||
#@+node:_get_resources
|
||||
def _get_resources(self, state):
|
||||
for r in self.resources:
|
||||
c = r._permutation_count()
|
||||
if c <= state:
|
||||
state -= c
|
||||
else:
|
||||
return r._get_resources(state)
|
||||
|
||||
assert(0)
|
||||
#@-node:_get_resources
|
||||
#@+node:_permutation_count
|
||||
def _permutation_count(self):
|
||||
return sum([ r._permutation_count() for r in self.resources])
|
||||
#@-node:_permutation_count
|
||||
#@-others
|
||||
#@-node:class _OrResourceGroup
|
||||
#@+node:class _AndResourceGroup
|
||||
|
||||
|
||||
class _AndResourceGroup(_ResourceGroup):
|
||||
#@ @+others
|
||||
#@+node:__init__
|
||||
def __init__(self, *args):
|
||||
self.factors = [ 1 ]
|
||||
_ResourceGroup.__init__(self, *args)
|
||||
#@-node:__init__
|
||||
#@+node:_refactor
|
||||
def _refactor(self, arg):
|
||||
count = arg._permutation_count()
|
||||
self.factors = [ count * f for f in self.factors ]
|
||||
self.factors.append(1)
|
||||
#@-node:_refactor
|
||||
#@+node:_permutation_count
|
||||
#print "AndResourceGroup", count, arg, self.factors
|
||||
|
||||
|
||||
def _permutation_count(self):
|
||||
return self.factors[0]
|
||||
#@-node:_permutation_count
|
||||
#@+node:_get_resources
|
||||
def _get_resources(self, state):
|
||||
"""delivers None when there are duplicate resources"""
|
||||
result = []
|
||||
for i in range(1, len(self.factors)):
|
||||
f = self.factors[i]
|
||||
substate = state / f
|
||||
state %= f
|
||||
result.append(self.resources[i - 1]._get_resources(substate))
|
||||
|
||||
result = ResourceList(*list(utils.flatten(result)))
|
||||
dupl_test = { }
|
||||
for r in result:
|
||||
if dupl_test.has_key(r):
|
||||
return None
|
||||
else:
|
||||
dupl_test[r] = 1
|
||||
|
||||
return result
|
||||
#@-node:_get_resources
|
||||
#@+node:_has_duplicates
|
||||
def _has_duplicates(self, state):
|
||||
resources = self._get_resources(state)
|
||||
tmp = { }
|
||||
for r in resources:
|
||||
if tmp.has_key(r):
|
||||
return True
|
||||
|
||||
tmp[r] = 1
|
||||
|
||||
return False
|
||||
#@-node:_has_duplicates
|
||||
#@-others
|
||||
#@-node:class _AndResourceGroup
|
||||
#@-others
|
||||
#@-node:@file resource.py
|
||||
#@-leo
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,113 +0,0 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2005 by Reithinger GmbH
|
||||
# mreithinger@web.de
|
||||
#
|
||||
# This file is part of faces.
|
||||
#
|
||||
# faces is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# faces 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
############################################################################
|
||||
|
||||
import faces.pcalendar as pcal
|
||||
import openerp.tools as tools
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
|
||||
class TimeScale(object):
|
||||
def __init__(self, calendar):
|
||||
self.data_calendar = calendar
|
||||
self._create_chart_calendar()
|
||||
self.now = self.to_num(self.data_calendar.now)
|
||||
|
||||
|
||||
def to_datetime(self, xval):
|
||||
return xval.to_datetime()
|
||||
|
||||
|
||||
def to_num(self, date):
|
||||
return self.chart_calendar.WorkingDate(date)
|
||||
|
||||
|
||||
def is_free_slot(self, value):
|
||||
dt1 = self.chart_calendar.to_starttime(value)
|
||||
dt2 = self.data_calendar.to_starttime\
|
||||
(self.data_calendar.from_datetime(dt1))
|
||||
return dt1 != dt2
|
||||
|
||||
|
||||
def is_free_day(self, value):
|
||||
dt1 = self.chart_calendar.to_starttime(value)
|
||||
dt2 = self.data_calendar.to_starttime\
|
||||
(self.data_calendar.from_datetime(dt1))
|
||||
return dt1.date() != dt2.date()
|
||||
|
||||
|
||||
def _create_chart_calendar(self):
|
||||
dcal = self.data_calendar
|
||||
ccal = self.chart_calendar = pcal.Calendar()
|
||||
ccal.minimum_time_unit = 1
|
||||
|
||||
#pad worktime slots of calendar (all days should be equally long)
|
||||
slot_sum = lambda slots: sum(map(lambda slot: slot[1] - slot[0], slots))
|
||||
day_sum = lambda day: slot_sum(dcal.get_working_times(day))
|
||||
|
||||
max_work_time = max(map(day_sum, range(7)))
|
||||
|
||||
#working_time should have 2/3
|
||||
sum_time = 3 * max_work_time / 2
|
||||
|
||||
#now create timeslots for ccal
|
||||
def create_time_slots(day):
|
||||
src_slots = dcal.get_working_times(day)
|
||||
slots = [0, src_slots, 24*60]
|
||||
slots = tuple(tools.flatten(slots))
|
||||
slots = zip(slots[:-1], slots[1:])
|
||||
|
||||
#balance non working slots
|
||||
work_time = slot_sum(src_slots)
|
||||
non_work_time = sum_time - work_time
|
||||
|
||||
non_slots = filter(lambda s: s not in src_slots, slots)
|
||||
non_slots = map(lambda s: (s[1] - s[0], s), non_slots)
|
||||
non_slots.sort()
|
||||
|
||||
slots = []
|
||||
i = 0
|
||||
for l, s in non_slots:
|
||||
delta = non_work_time / (len(non_slots) - i)
|
||||
delta = min(l, delta)
|
||||
non_work_time -= delta
|
||||
slots.append((s[0], s[0] + delta))
|
||||
i += 1
|
||||
|
||||
slots.extend(src_slots)
|
||||
slots.sort()
|
||||
return slots
|
||||
|
||||
min_delta = sys.maxint
|
||||
for i in range(7):
|
||||
slots = create_time_slots(i)
|
||||
ccal.working_times[i] = slots
|
||||
min_delta = min(min_delta, min(map(lambda s: s[1] - s[0], slots)))
|
||||
|
||||
ccal._recalc_working_time()
|
||||
|
||||
self.slot_delta = min_delta
|
||||
self.day_delta = sum_time
|
||||
self.week_delta = ccal.week_time
|
||||
|
||||
|
||||
_default_scale = TimeScale(pcal._default_calendar)
|
||||
@@ -1,121 +0,0 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2005 by Reithinger GmbH
|
||||
# mreithinger@web.de
|
||||
#
|
||||
# This file is part of faces.
|
||||
#
|
||||
# faces is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# faces 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
############################################################################
|
||||
|
||||
import observer
|
||||
import os.path
|
||||
import sys
|
||||
import os.path
|
||||
|
||||
_call_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
|
||||
|
||||
def get_installation_path():
|
||||
try:
|
||||
if sys.frozen:
|
||||
path = _call_dir
|
||||
else:
|
||||
raise AttributeError()
|
||||
except AttributeError:
|
||||
path = os.path.abspath(observer.__file__)
|
||||
path = os.path.split(path)[0]
|
||||
|
||||
path = os.path.normcase(path)
|
||||
return path
|
||||
|
||||
|
||||
def get_resource_path():
|
||||
try:
|
||||
if sys.frozen:
|
||||
path = _call_dir
|
||||
path = os.path.join(path, "resources", "faces", "gui")
|
||||
else:
|
||||
raise AttributeError()
|
||||
except AttributeError:
|
||||
path = get_installation_path()
|
||||
path = os.path.join(path, "gui", "resources")
|
||||
|
||||
path = os.path.normcase(path)
|
||||
return path
|
||||
|
||||
|
||||
def get_template_path():
|
||||
try:
|
||||
if sys.frozen:
|
||||
path = _call_dir
|
||||
path = os.path.join(path, "resources", "faces", "templates")
|
||||
else:
|
||||
raise AttributeError()
|
||||
except AttributeError:
|
||||
path = get_installation_path()
|
||||
path = os.path.join(path, "templates")
|
||||
|
||||
path = os.path.normcase(path)
|
||||
return path
|
||||
|
||||
|
||||
def get_howtos_path():
|
||||
try:
|
||||
if sys.frozen:
|
||||
path = _call_dir
|
||||
else:
|
||||
raise AttributeError()
|
||||
except AttributeError:
|
||||
path = get_installation_path()
|
||||
|
||||
path = os.path.join(path, "howtos")
|
||||
path = os.path.normcase(path)
|
||||
return path
|
||||
|
||||
|
||||
|
||||
def flatten(items):
|
||||
if isinstance(items, tuple):
|
||||
items = list(items)
|
||||
|
||||
if not isinstance(items, list):
|
||||
yield items
|
||||
|
||||
stack = [iter(items)]
|
||||
while stack:
|
||||
for item in stack[-1]:
|
||||
if isinstance(item, tuple):
|
||||
item = list(item)
|
||||
|
||||
if isinstance(item, list):
|
||||
stack.append(iter(item))
|
||||
break
|
||||
yield item
|
||||
else:
|
||||
stack.pop()
|
||||
|
||||
|
||||
def do_yield():
|
||||
pass
|
||||
|
||||
|
||||
def progress_start(title, maximum, message=""):
|
||||
pass
|
||||
|
||||
def progress_update(value, message=""):
|
||||
pass
|
||||
|
||||
def progress_end():
|
||||
pass
|
||||
@@ -688,115 +688,6 @@ class resource_resource(osv.osv):
|
||||
default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name))
|
||||
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.
|
||||
|
||||
NOTE: Used in project/project.py
|
||||
"""
|
||||
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
|
||||
|
||||
NOTE: used in project/project.py, and in generate_resources
|
||||
"""
|
||||
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.datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
|
||||
dt_end = datetime.datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
|
||||
no = dt_end - dt_start
|
||||
[leave_list.append((dt_start + datetime.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
|
||||
|
||||
NOTE: used in project/project.py
|
||||
"""
|
||||
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 UserError(_('Make sure the Working time has been configured with proper week days!'))
|
||||
hour_from_str = hours_time_string(week['hour_from'])
|
||||
hour_to_str = hours_time_string(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
|
||||
|
||||
|
||||
class resource_calendar_leaves(osv.osv):
|
||||
_name = "resource.calendar.leaves"
|
||||
|
||||
@@ -8,14 +8,6 @@
|
||||
|
||||
-
|
||||
I had Project of OpenERP Integration of 50 Hours with three human resource assigned on it. I have started project from this week start.
|
||||
-
|
||||
First, I generate the resource detail and check the resource Efficiency assigned.
|
||||
-
|
||||
!python {model: resource.resource}: |
|
||||
from openerp.tools import float_compare
|
||||
resources = self.generate_resources(cr, uid, [ref('base.user_root'),ref('base.user_demo')], ref('timesheet_group1'), context)
|
||||
for resource in resources.values():
|
||||
assert float_compare(resource.get('efficiency', 0.0), 1.0, precision_digits=2) == 0, 'Invalid resource generated.'
|
||||
-
|
||||
I check per day work hour availability of the Resource based on Working Calendar Assigned to each resource, for first day of the week.
|
||||
-
|
||||
|
||||
Reference in New Issue
Block a user