odoo/addons/resource/faces/resource.py

869 lines
25 KiB
Python

#@+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
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: