From a6909d0cc0960163d00d5065fb4df50fbf1e0882 Mon Sep 17 00:00:00 2001 From: "pap (openerp)" Date: Wed, 20 Jan 2010 18:14:10 +0530 Subject: [PATCH 01/30] [IMP:moved _check_dates function from module project_long_term to module project] bzr revid: pap@tinyerp.co.in-20100120124410-gl06rvx06c4vo1xw --- addons/project/project.py | 17 ++++++++++------- addons/project_long_term/project.py | 11 ----------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/addons/project/project.py b/addons/project/project.py index 89cf75e454c..0865c24857b 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -324,13 +324,13 @@ class task(osv.osv): default['work_ids'] = [] return super(task, self).copy_data(cr, uid, id, default, context) - def _check_date(self,cr,uid,ids): - for res in self.browse(cr,uid,ids): - if res.date_start and res.date_end: - if res.date_start > res.date_end: - return False - return True - + def _check_dates(self, cr, uid, ids): + task = self.read(cr, uid, ids[0],['date_start','date_end']) + if task['date_start'] and task['date_end']: + if task['date_start'] > task['date_end']: + return False + return True + _columns = { 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the task without removing it."), 'name': fields.char('Task summary', size=128, required=True), @@ -380,6 +380,9 @@ class task(osv.osv): } _order = "sequence, priority, date_deadline, id" + _constraints = [ + (_check_dates, 'Error! task start-date must be lower then task end-date.', ['date_start', 'date_end']) + ] # # Override view according to the company definition # diff --git a/addons/project_long_term/project.py b/addons/project_long_term/project.py index c5734263ea5..3f16b33ea31 100644 --- a/addons/project_long_term/project.py +++ b/addons/project_long_term/project.py @@ -125,17 +125,6 @@ class task(osv.osv): _columns = { 'phase_id': fields.many2one('project.phase', 'Project Phase') } - def _check_dates(self, cr, uid, ids): - leave = self.read(cr, uid, ids[0],['date_start','date_end']) - if leave['date_start'] and leave['date_end']: - if leave['date_start'] > leave['date_end']: - return False - return True - - _constraints = [ - (_check_dates, 'Error! task start-date must be lower then task end-date.', ['date_start', 'date_end']) - ] - task() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From c0a0d84cd74b8988315cbd0fd92e3a2ff0ad8950 Mon Sep 17 00:00:00 2001 From: "Rvo (Open ERP)" Date: Thu, 28 Jan 2010 15:25:55 +0530 Subject: [PATCH 02/30] [ADD] Task Scheduler On Project Module with faces scheduling tool bzr revid: rvo@tinyerp.co.in-20100128095555-fuk85f2amxy38zle --- addons/project/project_wizard.xml | 11 + addons/project/wizard/__init__.py | 6 +- addons/project/wizard/compute_tasks_date.py | 183 + addons/resource/faces_new/__init__.py | 30 + addons/resource/faces_new/observer.py | 79 + addons/resource/faces_new/pcalendar.py | 964 +++++ addons/resource/faces_new/plocale.py | 54 + addons/resource/faces_new/resource.py | 866 +++++ addons/resource/faces_new/task.py | 3855 +++++++++++++++++++ addons/resource/faces_new/timescale.py | 113 + addons/resource/faces_new/utils.py | 124 + 11 files changed, 6283 insertions(+), 2 deletions(-) create mode 100644 addons/project/wizard/compute_tasks_date.py create mode 100644 addons/resource/faces_new/__init__.py create mode 100644 addons/resource/faces_new/observer.py create mode 100644 addons/resource/faces_new/pcalendar.py create mode 100644 addons/resource/faces_new/plocale.py create mode 100644 addons/resource/faces_new/resource.py create mode 100755 addons/resource/faces_new/task.py create mode 100644 addons/resource/faces_new/timescale.py create mode 100644 addons/resource/faces_new/utils.py diff --git a/addons/project/project_wizard.xml b/addons/project/project_wizard.xml index a9b7ade39b5..785dd9f599c 100644 --- a/addons/project/project_wizard.xml +++ b/addons/project/project_wizard.xml @@ -3,10 +3,21 @@ + + + + + + diff --git a/addons/project/wizard/__init__.py b/addons/project/wizard/__init__.py index b724bd6e7ca..9d90ce8ed90 100644 --- a/addons/project/wizard/__init__.py +++ b/addons/project/wizard/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2010 Tiny SPRL (). # @@ -15,13 +15,15 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . +# along with this program. If not, see . # ############################################################################## import close_task import task_delegate +import compute_tasks_date import duplicate_template_wizard + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/project/wizard/compute_tasks_date.py b/addons/project/wizard/compute_tasks_date.py new file mode 100644 index 00000000000..89a062a48bd --- /dev/null +++ b/addons/project/wizard/compute_tasks_date.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import wizard +import pooler +from tools.translate import _ +import datetime +from resource.faces_new import * + +compute_form = """ +
+ + + +""" + +compute_fields = { + 'project_id': {'string':'Project', 'type':'many2one', 'relation': 'project.project', 'required':'True'}, + +} +def timeformat_convert(cr, uid, time_string, context={}): + strg = str(time_string)[-2:] + last_strg = str(time_string)[0:-2] + if strg != '.0': + if '.' not in strg: + strg = '.' + strg + last_strg = str(time_string)[:-3] + + new_strg = round(float(strg) / 0.016666667) + converted_strg = last_strg + ':' + str(new_strg)[:-2] + else: + converted_strg = last_strg + ':00' + + return converted_strg + +class wizard_compute_tasks(wizard.interface): + + def _compute_date(self, cr, uid, data, context): + + project_id = data['form']['project_id'] + project = pooler.get_pool(cr.dbname).get('project.project').browse(cr,uid,project_id) + task_ids = pooler.get_pool(cr.dbname).get('project.task').search(cr,uid,[('project_id','=',project_id)]) + if task_ids: + task_obj = pooler.get_pool(cr.dbname).get('project.task').browse(cr,uid,task_ids) + task_1 = task_obj[0] + if task_1.date_start: + date_dt = datetime.datetime.strptime(task_1.date_start,"%Y-%m-%d %H:%M:%S") + else: + date_dt = datetime.datetime.strptime(project.date_start,"%Y-%m-%d") + print 'Start Date of Project:::',date_dt + pdt = datetime.datetime.strftime(date_dt,"%Y-%m-%d %H:%M") + + class user(Resource): + pass + + for i in range(len(task_obj)): + final_lst = [] + leaves = [] + if task_obj[i].user_id.id: + resource_id = pooler.get_pool(cr.dbname).get('resource.resource').search(cr,uid,[('user_id','=',task_obj[i].user_id.id)]) + resource_obj = pooler.get_pool(cr.dbname).get('resource.resource').browse(cr,uid,resource_id)[0] + if resource_obj.calendar_id: + time_range = "8:00-8:00" + b = "" + wk = {"0":"mon","1":"tue","2":"wed","3":"thu","4":"fri"} + wk_days = {} + wk_time = {} + tlist = [] + week_ids = pooler.get_pool(cr.dbname).get('resource.calendar.week').search(cr,uid,[('calendar_id','=',resource_obj.calendar_id.id)]) + week_obj = pooler.get_pool(cr.dbname).get('resource.calendar.week').read(cr,uid,week_ids,['dayofweek','hour_from','hour_to']) + + for week in week_obj: + res_str = "" + if wk.has_key(week['dayofweek']): + day = wk[week['dayofweek']] + wk_days[week['dayofweek']] = wk[week['dayofweek']] + + hour_from_str = timeformat_convert(cr,uid,week['hour_from']) + hour_to_str = timeformat_convert(cr,uid,week['hour_to']) + res_str = hour_from_str + '-' + hour_to_str + tlist.append((day,res_str)) + + for item in tlist: + 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(): + final_lst.append(tuple(v)) + print 'final Dictionary List:::',final_lst + + for k,v in wk_days.items(): + if wk.has_key(k): + wk.pop(k) + for v in wk.itervalues(): + b += v + ',' + final_lst.append((b[:-1],time_range)) + print 'Final Tlist:::',tlist + + resource_leave_ids = pooler.get_pool(cr.dbname).get('resource.calendar.leaves').search(cr,uid,[('resource_id','=',resource_id)]) + if resource_leave_ids: + res_leaves = pooler.get_pool(cr.dbname).get('resource.calendar.leaves').read(cr,uid,resource_leave_ids,['date_from','date_to']) + for leave in range(len(res_leaves)): + dt_start = datetime.datetime.strptime(res_leaves[leave]['date_from'],'%Y-%m-%d %H:%M:%S') + dt_end = datetime.datetime.strptime(res_leaves[leave]['date_to'],'%Y-%m-%d %H:%M:%S') + no = dt_end - dt_start + leave_days = no.days + 1 + [leaves.append((dt_start + datetime.timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(leave_days))] + leaves.sort() + print 'Dt_leaves::',leaves + + + hours = task_obj[i].remaining_hours / task_obj[i].occupation_rate + hours = str(hours)[:-2] + 'H' + print 'Hours:::',hours + if i == 0: + ndt = pdt + else: + data = pooler.get_pool(cr.dbname).get('project.task').read(cr,uid,[task_obj[i-1].id],['date_end'])[0] + ndt = data['date_end'][0:16] + + def Project_1(): + resource = user + title = project.name + start = ndt + effort = hours + + if final_lst or leaves: + working_days = final_lst + vacation = tuple(leaves) + + def task1(): + start = ndt + effort = hours + title = task_obj[i].name + + project_1 = BalancedProject(Project_1) + s_date = project_1.calendar.WorkingDate(project_1.task1.start).to_datetime() + e_date = project_1.calendar.WorkingDate(project_1.task1.end).to_datetime() + print 'Start Date::::',s_date,task_obj[i].name + print 'End Date::::',e_date,task_obj[i].name + pooler.get_pool(cr.dbname).get('project.task').write(cr,uid,[task_obj[i].id],{'date_start':s_date,'date_end':e_date}) + + return {} + states = { + 'init': { + 'actions': [], + 'result': {'type':'form', 'arch':compute_form, 'fields':compute_fields, 'state':[ + ('end', 'Cancel'), + ('ok', 'Ok') + + ]}, + }, + 'ok': { + 'actions': [_compute_date], + 'result': {'type':'state', 'state':'end'}, + } + } +wizard_compute_tasks('wizard.compute.tasks') + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/resource/faces_new/__init__.py b/addons/resource/faces_new/__init__.py new file mode 100644 index 00000000000..d1d52daf742 --- /dev/null +++ b/addons/resource/faces_new/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +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 + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/resource/faces_new/observer.py b/addons/resource/faces_new/observer.py new file mode 100644 index 00000000000..411b2c5395b --- /dev/null +++ b/addons/resource/faces_new/observer.py @@ -0,0 +1,79 @@ +#@+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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 diff --git a/addons/resource/faces_new/pcalendar.py b/addons/resource/faces_new/pcalendar.py new file mode 100644 index 00000000000..3668a93a37a --- /dev/null +++ b/addons/resource/faces_new/pcalendar.py @@ -0,0 +1,964 @@ +#@+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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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) + print 'Return Date:::',datetime.datetime(*conv[0:-3]) + 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' + """ + print 'Working Days:::',self,day_range,trange,further_tranges + 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)) +# seconds = time.mktime(value.timetuple()) +# print 'Seconds:::',seconds +# return int(seconds) + 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") + print "start", start.strftime(), type(start) + + delay = Minutes("4H") + print "delay", delay, delay.strftime() + + print "Start", cal.StartDate is StartDate + print "base", cal.StartDate.__bases__[0] == StartDate.__bases__[0] + print "type", type(start) + + print "convert start" + start2 = cal.StartDate(start) + print "convert end" + + start3 = cal.StartDate("10.1.2005") + print "start2", start2.strftime(), type(start2) +#@-node:@file pcalendar.py +#@-leo diff --git a/addons/resource/faces_new/plocale.py b/addons/resource/faces_new/plocale.py new file mode 100644 index 00000000000..3dcd35c7cae --- /dev/null +++ b/addons/resource/faces_new/plocale.py @@ -0,0 +1,54 @@ +############################################################################ +# 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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################ + +import gettext +import os.path +import locale + + +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() + diff --git a/addons/resource/faces_new/resource.py b/addons/resource/faces_new/resource.py new file mode 100644 index 00000000000..7cc2741fc8e --- /dev/null +++ b/addons/resource/faces_new/resource.py @@ -0,0 +1,866 @@ +#@+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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 "" % (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 "" % 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 "" % 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 diff --git a/addons/resource/faces_new/task.py b/addons/resource/faces_new/task.py new file mode 100755 index 00000000000..ed36965838a --- /dev/null +++ b/addons/resource/faces_new/task.py @@ -0,0 +1,3855 @@ +#@+leo-ver=4 +#@+node:@file task.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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################ + +#@-node:<< Copyright >> +#@nl +""" +This module contains all classes for project plan objects +""" +#@<< Imports >> +#@+node:<< Imports >> +import pcalendar +import resource +import types +import sys +import datetime +import operator as op +import warnings +import locale +import weakref +import opcode +import new +try: + set +except NameError: + from sets import Set as set +#@-node:<< Imports >> +#@nl + +_is_source = True + +STRICT = 3 +SLOPPY = 2 +SMART = 1 + +#@+others +#@+node:Exceptions +#@+node:class AttributeError +class AttributeError(AttributeError): + #@ << class AttributeError declarations >> + #@+node:<< class AttributeError declarations >> + is_frozen = False + + + #@-node:<< class AttributeError declarations >> + #@nl +#@-node:class AttributeError +#@+node:class RecursionError +class RecursionError(Exception): + """This exception is raised in cas of cirular dependencies + within an project""" + #@ << class RecursionError declarations >> + #@+node:<< class RecursionError declarations >> + pass + + + #@-node:<< class RecursionError declarations >> + #@nl +#@-node:class RecursionError +#@+node:class _IncompleteError +class _IncompleteError(Exception): + """This exception is raised, when there is not enough + data specified to calculate as task""" + #@ @+others + #@+node:__init__ + def __init__(self, *args): + if isinstance(args[0], (basestring)): + Exception.__init__(self, *args) + else: + Exception.__init__(self, + "not enough data for calculating task, "\ + "maybe you have a recursive reference", + *args) + #@-node:__init__ + #@-others +#@-node:class _IncompleteError +#@-node:Exceptions +#@+node:Proxies for self referencing +#@+node:class _MeProxy +class _MeProxy(object): + """ + A Proxy class for the me attribute of tasks in the compile case + """ + #@ << declarations >> + #@+node:<< declarations >> + __slots__ = "task" + + #@-node:<< declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, task): + object.__setattr__(self, "task", task) + #@-node:__init__ + #@+node:__getattr__ + def __getattr__(self, name): + if self.task._is_frozen: + return getattr(self.task, name) + + if name in ("name", "up", "root", "path", + "depth", "index", "calendar", + "children", "resource", "balance"): + return getattr(self.task, name) + + value = self.task.__dict__.get(name, _NEVER_USED_) + def make_val(default): + if value is _NEVER_USED_: return default + return value + + if name in ("start", "end"): + return self.task._to_start(make_val("1.1.2006")) + + if name in ("length", "effort", "duration", "todo", "done", + "buffer", "performed", "performed_effort", + "performed_end", "performed_start", + "performed_work_time" ): + return self.task._to_delta(make_val("0d")) + + if name in ("complete", "priority", "efficiency"): + return make_val(0) + + if value is _NEVER_USED_: + raise AttributeError("'%s' is not a valid attribute." % (name)) + + return value + #@-node:__getattr__ + #@+node:__setattr__ + def __setattr__(self, name, value): + self.task._set_attrib(name, value) + #@-node:__setattr__ + #@+node:__iter__ + def __iter__(self): + return iter(self.task) + #@nonl + #@-node:__iter__ + #@+node:add_attrib + def add_attrib(self, name_or_iter, val=None): + if not isinstance(name_or_iter, str): + for n, v in name_or_iter: + setattr(self, n, v) + else: + setattr(self, name_or_iter, val) + #@-node:add_attrib + #@-others +#@nonl +#@-node:class _MeProxy +#@+node:class _MeProxyRecalc +class _MeProxyRecalc(_MeProxy): + """ + A Proxy class for the me attribute of tasks in the recalc case + """ + #@ @+others + #@+node:__setattr__ + def __setattr__(self, name, value): + if self.task._properties.has_key(name): + self.task._set_attrib(name, value) + #@-node:__setattr__ + #@-others +#@-node:class _MeProxyRecalc +#@+node:class _MeProxyError +class _MeProxyError(_MeProxy): + #@ << declarations >> + #@+node:<< declarations >> + __slots__ = ("task", "attrib", "exc") + + #@-node:<< declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, task, attrib, exc): + _MeProxy.__init__(self, task) + object.__setattr__(self, "attrib", attrib) + object.__setattr__(self, "exc", exc) + #@-node:__init__ + #@+node:__setattr__ + def __setattr__(self, name, value): + if name == self.attrib or not self.attrib: + raise self.exc + #@-node:__setattr__ + #@-others +#@-node:class _MeProxyError +#@+node:class _MeProxyWarn +class _MeProxyWarn(_MeProxy): + #@ << declarations >> + #@+node:<< declarations >> + __slots__ = ("task", "attrib", "message") + + #@-node:<< declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, task, attrib, message): + _MeProxy.__init__(self, task) + object.__setattr__(self, "attrib", attrib) + object.__setattr__(self, "message", message) + #@-node:__init__ + #@+node:__setattr__ + def __setattr__(self, name, value): + if name == self.attrib or not self.attrib: + warnings.warn(self.message, RuntimeWarning, 2) + if not self.attrib: + #warn only one time! + object.__setattr__(self, "attrib", 1) + #@-node:__setattr__ + #@-others +#@-node:class _MeProxyWarn +#@-node:Proxies for self referencing +#@+node:Task instrumentation +#@+doc +# This section contains code for byte code instrumenting +# the task functions +#@-doc +#@nonl +#@+node:_int_to_arg +def _int_to_arg(value): + return value % 256, value / 256 +#@-node:_int_to_arg +#@+node:_correct_labels +def _correct_labels(old_code, new_code): + #@ << localize dot variables >> + #@+node:<< localize dot variables >> + hasjrel = opcode.hasjrel + hasjabs = opcode.hasjabs + HAVE_ARGUMENT = opcode.HAVE_ARGUMENT + #@nonl + #@-node:<< localize dot variables >> + #@nl + #@ << loop initialization >> + #@+node:<< loop initialization >> + labels = {} + old_new_map = {} # map old code offset to new code offset + n = len(old_code) + i = 0 + j = 0 + #@nonl + #@-node:<< loop initialization >> + #@nl + while i < n: + op = old_code[i] + nop = new_code[j] + old_new_map[i] = j + i = i + 1 + j = j + 1 + if op >= HAVE_ARGUMENT: + oparg = old_code[i] + old_code[i + 1] * 256 + i = i + 2 + j = j + 2 + if nop != op: + j += 3 # skip the 3 addition opcodes for attrib access + else: + #@ << add label if necessary >> + #@+node:<< add label if necessary >> + label = -1 + if op in hasjrel: + label = i + oparg + elif op in hasjabs: + label = oparg + if label >= 0: + labels[i] = label + #@nonl + #@-node:<< add label if necessary >> + #@nl + + for offset, label in labels.iteritems(): + new_offset = old_new_map[offset] + new_label = old_new_map[label] + op = new_code[new_offset - 3] + #change jump arguments + if op in hasjrel: + jump = _int_to_arg(new_label - new_offset) + new_code[new_offset - 2:new_offset] = jump + elif op in hasjabs: + new_code[new_offset - 2:new_offset] = _int_to_arg(new_label) +#@nonl +#@-node:_correct_labels +#@+node:_instrument +def _instrument(func): + #@ << localize dot variables >> + #@+node:<< localize dot variables >> + opname = opcode.opname + opmap = opcode.opmap + jumps = opcode.hasjrel + opcode.hasjabs + HAVE_ARGUMENT = opcode.HAVE_ARGUMENT + co = func.func_code + local_names = co.co_varnames + all_names = list(co.co_names) + global_names = set() + #@-node:<< localize dot variables >> + #@nl + #@ << define local functions list_to_dict and is_local >> + #@+node:<< define local functions list_to_dict and is_local >> + def list_to_dict(l): + return dict([(t[1], t[0]) for t in enumerate(l)]) + + def is_local(name): + return name[0] == "_" and name != "__constraint__" + #@nonl + #@-node:<< define local functions list_to_dict and is_local >> + #@nl + + #convert code + #@ << loop initialization >> + #@+node:<< loop initialization >> + # all_name_map maps names to the all_names index + # (same like all_names.index()) + all_name_map = list_to_dict(all_names) + if not all_name_map.has_key("me"): + all_name_map["me"] = len(all_names) + all_names.append("me") + + # + for ln in local_names: + if not all_name_map.has_key(ln): + all_name_map[ln] = len(all_names) + all_names.append(ln) + # + + new_local_names = filter(is_local, local_names) + new_local_name_map = list_to_dict(new_local_names) + + me_arg = _int_to_arg(all_name_map["me"]) + old_lnotab = map(ord, co.co_lnotab) + new_lnotab = [] + tab_pos = 0 + try: + next_tab_point = old_lnotab[0] + except IndexError: + next_tab_point = None + + last_tab_point = 0 + code = map(ord, co.co_code) + new_code = [] + has_labels = False + n = len(code) + i = 0 + #@nonl + #@-node:<< loop initialization >> + #@nl + while i < n: + if i == next_tab_point: + #@ << calculate new tab point >> + #@+node:<< calculate new tab point >> + increment = len(new_code) - last_tab_point + new_lnotab.extend((increment, old_lnotab[tab_pos + 1])) + tab_pos += 2 + try: + next_tab_point = i + old_lnotab[tab_pos] + last_tab_point = len(new_code) + except IndexError: + next_tab_point = -1 + #@nonl + #@-node:<< calculate new tab point >> + #@nl + + op = code[i] + i += 1 + if op >= HAVE_ARGUMENT: + #@ << calculate argument >> + #@+node:<< calculate argument >> + arg0 = code[i] + arg1 = code[i+1] + oparg = arg0 + arg1 * 256 + #@nonl + #@-node:<< calculate argument >> + #@nl + i += 2 + + if opname[op] == "LOAD_GLOBAL": + global_names.add(oparg) + + elif opname[op] == "STORE_FAST": + #@ << change "store fast" to "store attribute" >> + #@+node:<< change "store fast" to "store attribute" >> + name = local_names[oparg] + if not is_local(name): + new_code.append(opmap["LOAD_GLOBAL"]) + new_code.extend(me_arg) + op = opmap["STORE_ATTR"] + arg0, arg1 = _int_to_arg(all_name_map[name]) + else: + arg0, arg1 = _int_to_arg(new_local_name_map[name]) + #@nonl + #@-node:<< change "store fast" to "store attribute" >> + #@nl + + elif opname[op] == "LOAD_FAST": + #@ << change "load fast" to "load attribute" >> + #@+node:<< change "load fast" to "load attribute" >> + name = local_names[oparg] + if not is_local(name): + new_code.append(opmap["LOAD_GLOBAL"]) + new_code.extend(me_arg) + op = opmap["LOAD_ATTR"] + arg0, arg1 = _int_to_arg(all_name_map[name]) + else: + arg0, arg1 = _int_to_arg(new_local_name_map[name]) + #@nonl + #@-node:<< change "load fast" to "load attribute" >> + #@nl + + elif op in jumps: + has_labels = True + + new_code.extend((op, arg0, arg1)) + else: + new_code.append(op) + + if has_labels: + _correct_labels(code, new_code) + + #@ << create new code and function objects and return >> + #@+node:<< create new code and function objects and return >> + new_code = "".join(map(chr, new_code)) + new_lnotab = "".join(map(chr, new_lnotab)) + new_co = new.code(co.co_argcount, + len(new_local_names), + max(co.co_stacksize, 2), + co.co_flags, + new_code, + co.co_consts, + tuple(all_names), + tuple(new_local_names), + co.co_filename, + co.co_name, + co.co_firstlineno, + new_lnotab, + co.co_freevars, + co.co_cellvars) + + + func = new.function(new_co, + func.func_globals, + func.func_name, + func.func_defaults, + func.func_closure) + func.global_names = tuple([all_names[index] for index in global_names]) + return func + #@nonl + #@-node:<< create new code and function objects and return >> + #@nl +#@nonl +#@-node:_instrument +#@-node:Task instrumentation +#@+node:Wrappers +#@+node:class _Path +class _Path(object): + """ + This class represents an instrumented path, to + a task. If it points to an attribute of a task, it + not only returns the value of the attribute. You can also + find out the source attribute (task and attribute name) + of the value. + """ + #@ @+others + #@+node:__init__ + def __init__(self, task, path_str): + self._task = task + self._path_str = path_str + #@-node:__init__ + #@+node:__getattr__ + def __getattr__(self, name): + new = getattr(self._task, name) + if isinstance(new, Task): + return _Path(new, self._path_str + "." + name) + + return _ValueWrapper(new, [(self._task, name)]) + #@-node:__getattr__ + #@+node:__str__ + def __str__(self): + return self._path_str + #@-node:__str__ + #@+node:__iter__ + def __iter__(self): + return iter(self._task) + #@nonl + #@-node:__iter__ + #@-others + +#@-node:class _Path +#@+node:_val +#helper functions for _ValueWrapper +#---------------------------------- + +def _val(val): + if isinstance(val, _ValueWrapper): + return val._value + + return val +#@-node:_val +#@+node:_ref +def _ref(val): + if isinstance(val, _ValueWrapper): + return val._ref + + return [] +#@-node:_ref +#@+node:_sref +def _sref(val, ref): + if isinstance(val, _ValueWrapper): + val._ref = ref +#@nonl +#@-node:_sref +#@+node:_refsum +def _refsum(refs): + return reduce(lambda a, b: a + b, refs, []) +#@nonl +#@-node:_refsum +#@+node:class _ValueWrapper + + +class _ValueWrapper(object): + """ + This class represents a value, of a task attribute or + a return value of a task method. It contains the value, + and the supplier of that value + """ + #@ @+others + #@+node:__init__ + def __init__(self, value, ref): + self._value = value + self._ref = ref + #@-node:__init__ + #@+node:unicode + def unicode(self, *args): + if isinstance(self._value, str): + return unicode(self._value, *args) + + return unicode(self._value) + #@nonl + #@-node:unicode + #@+node:_vw + def _vw(self, operand, *args): + refs = _refsum(map(_ref, args)) + vals = map(_val, args) + result = operand(*vals) + return self.__class__(result, refs) + #@-node:_vw + #@+node:_cmp + def _cmp(self, operand, *args): + refs = _refsum(map(_ref, args)) + vals = map(_val, args) + result = operand(*vals) + map(lambda a: _sref(a, refs), args) + return result + #@-node:_cmp + #@+node:__getattr__ + def __getattr__(self, name): return getattr(self._value, name) + #@-node:__getattr__ + #@+node:__getitem__ + def __getitem__(self, slice): + return self.__class__(self._value[slice], self._ref) + #@nonl + #@-node:__getitem__ + #@+node:__str__ + def __str__(self): return str(self._value) + #@-node:__str__ + #@+node:__unicode__ + def __unicode__(self): return unicode(self._value) + #@nonl + #@-node:__unicode__ + #@+node:__repr__ + def __repr__(self): return repr(self._value) + #@-node:__repr__ + #@+node:__nonzero__ + def __nonzero__(self): return bool(self._value) + #@-node:__nonzero__ + #@+node:__lt__ + def __lt__(self, other): return self._cmp(op.lt, self, other) + #@-node:__lt__ + #@+node:__le__ + def __le__(self, other): return self._cmp(op.le, self, other) + #@-node:__le__ + #@+node:__eq__ + def __eq__(self, other): return self._cmp(op.eq, self, other) + #@-node:__eq__ + #@+node:__ne__ + def __ne__(self, other): return self._cmp(op.ne, self, other) + #@-node:__ne__ + #@+node:__gt__ + def __gt__(self, other): return self._cmp(op.gt, self, other) + #@-node:__gt__ + #@+node:__ge__ + def __ge__(self, other): return self._cmp(op.ge, self, other) + #@-node:__ge__ + #@+node:__add__ + def __add__(self, other): return self._vw(op.add, self, other) + #@nonl + #@-node:__add__ + #@+node:__sub__ + def __sub__(self, other): return self._vw(op.sub, self, other) + #@-node:__sub__ + #@+node:__mul__ + def __mul__(self, other): return self._vw(op.mul, self, other) + #@-node:__mul__ + #@+node:__floordiv__ + def __floordiv__(self, other): return self._vw(op.floordiv, self, other) + #@-node:__floordiv__ + #@+node:__mod__ + def __mod__(self, other): return self._vw(op.mod, self, other) + #@-node:__mod__ + #@+node:__divmod__ + def __divmod__(self, other): return self._vw(op.divmod, self, other) + #@-node:__divmod__ + #@+node:__pow__ + def __pow__(self, other): return self._vw(op.pow, self, other) + #@-node:__pow__ + #@+node:__lshift__ + def __lshift__(self, other): return self._vw(op.lshift, self, other) + #@-node:__lshift__ + #@+node:__rshift__ + def __rshift__(self, other): return self._vw(op.rshift, self, other) + #@-node:__rshift__ + #@+node:__and__ + def __and__(self, other): return self._vw(op.and_, self, other) + #@-node:__and__ + #@+node:__xor__ + def __xor__(self, other): return self._vw(op.xor, self, other) + #@-node:__xor__ + #@+node:__or__ + def __or__(self, other): return self._vw(op.or_, self, other) + #@-node:__or__ + #@+node:__div__ + def __div__(self, other): return self._vw(op.div, self, other) + #@-node:__div__ + #@+node:__radd__ + def __radd__(self, other): return self._vw(op.add, other, self) + #@-node:__radd__ + #@+node:__rsub__ + def __rsub__(self, other): return self._vw(op.sub, other, self) + #@-node:__rsub__ + #@+node:__rmul__ + def __rmul__(self, other): return self._vw(op.mul, other, self) + #@-node:__rmul__ + #@+node:__rdiv__ + def __rdiv__(self, other): return self._vw(op.div, other, self) + #@-node:__rdiv__ + #@+node:__rtruediv__ + def __rtruediv__(self, other): return self._vw(op.truediv, other, self) + #@-node:__rtruediv__ + #@+node:__rfloordiv__ + def __rfloordiv__(self, other): return self._vw(op.floordiv, other, self) + #@-node:__rfloordiv__ + #@+node:__rmod__ + def __rmod__(self, other): return self._vw(op.mod, other, self) + #@-node:__rmod__ + #@+node:__rdivmod__ + def __rdivmod__(self, other): return self._vw(op.divmod, other, self) + #@-node:__rdivmod__ + #@+node:__rpow__ + def __rpow__(self, other): return self._vw(op.pow, other, self) + #@-node:__rpow__ + #@+node:__rlshift__ + def __rlshift__(self, other): return self._vw(op.lshift, other, self) + #@-node:__rlshift__ + #@+node:__rrshift__ + def __rrshift__(self, other): return self._vw(op.rshift, other, self) + #@-node:__rrshift__ + #@+node:__rand__ + def __rand__(self, other): return self._vw(op.and_, other, self) + #@-node:__rand__ + #@+node:__rxor__ + def __rxor__(self, other): return self._vw(op.xor, other, self) + #@-node:__rxor__ + #@+node:__ror__ + def __ror__(self, other): return self._vw(op.or_, other, self) + #@-node:__ror__ + #@+node:__int__ + def __int__(self): return int(self._value) + #@-node:__int__ + #@+node:__long__ + def __long__(self): return long(self._value) + #@-node:__long__ + #@+node:__float__ + def __float__(self): return float(self._value) + #@-node:__float__ + #@+node:__len__ + def __len__(self): return len(self._value) + #@-node:__len__ + #@+node:__iter__ + def __iter__(self): return iter(self._value) + #@-node:__iter__ + #@+node:__hash__ + def __hash__(self): return hash(self._value) + #@-node:__hash__ + #@-others +#@-node:class _ValueWrapper +#@-node:Wrappers +#@+node:Utilities +#@+node:class _NEVER_USED_ +class _NEVER_USED_: + pass + +#@-node:class _NEVER_USED_ +#@+node:class _StringConverter +class _StringConverter(object): + """This class is a helper for the to_string mechanism + of tasks""" + #@ @+others + #@+node:__init__ + def __init__(self, source, format=None): + self.source = source + self.format = format + #@-node:__init__ + #@+node:__getitem__ + def __getitem__(self, format): + return _StringConverter(self.source, format) + #@-node:__getitem__ + #@+node:__getattr__ + def __getattr__(self, name): + class StrWrapper(object): + def __init__(self, value, name, source, format): + self._value = value + self.name = name + self.source = source + self.format = format + + def __call__(self, arg): + formatter = self.source.formatter(self.name, + arg, + self.format) + return formatter(self._value(arg)) + + value = getattr(self.source, name) + if callable(value): + #for methods the wrapper has to + return StrWrapper(value, name, self.source, self.format) + + formatter = self.source.formatter(name, format=self.format) + return formatter(value) + #@-node:__getattr__ + #@-others +#@-node:class _StringConverter +#@+node:Multi +def Multi(val, **kwargs): + """returns a directory for mutlivalued attributes""" + return dict(_default=val, **kwargs) +#@nonl +#@-node:Multi +#@+node:create_relative_path +def create_relative_path(from_, to_): + """ + creates a relative path from absolute path + from_ to absolute path to_ + """ + from_ = from_.split(".") + to_ = to_.split(".") + + for i, parts in enumerate(zip(from_, to_)): + from_part, to_part = parts + if from_part != to_part: + break + + from_ = from_[i:] + to_ = to_[i:] + return "up." * len(from_) + ".".join(to_) +#@nonl +#@-node:create_relative_path +#@+node:create_absolute_path +def create_absolute_path(from_, to_): + """ + creates a absolute path from absolute path + from_ to relative path to_ + """ + from_ = from_.split(".") + to_ = to_.split(".") + + for i, part in enumerate(to_): + if part != "up": + break + + from_ = from_[:-i] + to_ = to_[i:] + return "%s.%s" % (".".join(from_), ".".join(to_)) + + +#@-node:create_absolute_path +#@+node:_split_path +def _split_path(path): + try: + index = path.rindex(".") + return path[:index], path[index + 1:] + except: + return path +#@-node:_split_path +#@+node:_to_datetime +_to_datetime = pcalendar.to_datetime +#@nonl +#@-node:_to_datetime +#@+node:_get_tasks_of_sources +def _get_tasks_of_sources(task, attrib_filter="end,start,effort,length,duration"): + #return all source tasks, this task is dependend on + + dep_tasks = {} + + while task: + for dep in task._sources.values(): + for d in dep: + path, attrib = _split_path(d) + if attrib and attrib_filter.find(attrib) >= 0: + dep_tasks[path] = True + + task = task.up + + return dep_tasks.keys() +#@-node:_get_tasks_of_sources +#@+node:_build_balancing_list +def _build_balancing_list(tasks): + """ + Returns a specialy sorted list of tasks. + If the tasks will allocate resources in the sorting order of that list + correct balancing is ensured + """ + + # first sort the list for attributes + index = 0 + balancing_list = [(-t.priority, t.balance, index, t) for index, t in enumerate(tasks)] + balancing_list.sort() + + #print + #for p, b, i, t in balancing_list: + # print p, b, i, t.path + + balancing_list = [ t for p, b, i, t in balancing_list ] + + #now correct the presorted list: + #if task a is dependent on task b, b will be moved before a + + done_map = { } + count = len(balancing_list) + while len(done_map) < count: + for i in range(count): + to_inspect = balancing_list[i] + if done_map.has_key(to_inspect): + continue + + done_map[to_inspect] = True + break + else: + break + + #@ << define inspect_depends_on >> + #@+node:<< define inspect_depends_on >> + inspect_path = to_inspect.path + "." + sources = _get_tasks_of_sources(to_inspect) + sources = [ s + "." for s in sources + if not inspect_path.startswith(s) ] + + # the if in the later line ignores assignments like + # like start = up.start (i.e. references to parents) + # this will be handled in the second if of inspect_depends_on + # and can cause errors otherwise + + def inspect_depends_on(task): + cmp_path = task.path + "." + for src in sources: + if cmp_path.startswith(src): + #task is a source of to_inspect + return True + + if inspect_path.startswith(cmp_path): + #to_inspect is a child of task + return True + + return False + #@nonl + #@-node:<< define inspect_depends_on >> + #@nl + for j in range(i + 1, count): + check_task = balancing_list[j] + if done_map.has_key(check_task): + continue + + if inspect_depends_on(check_task): + del balancing_list[j] + balancing_list.insert(i, check_task) + i += 1 # to_inspect is now at i + 1 + + + return balancing_list +#@-node:_build_balancing_list +#@+node:_as_string +def _as_string(val): + if isinstance(val, basestring): + return '"""%s"""' % val.replace("\n", "\\n") + + if isinstance(val, pcalendar._WorkingDateBase): + return '"%s"' % val.strftime("%Y-%m-%d %H:%M") + + if isinstance(val, datetime.datetime): + return '"%s"' % val.strftime("%Y-%m-%d %H:%M") + + if isinstance(val, datetime.timedelta): + return '"%id %iM"' % (val.days, val.seconds / 60) + + if isinstance(val, tuple): + result = map(_as_string, val) + return "(%s)" % ", ".join(result) + + if isinstance(val, list): + result = map(_as_string, val) + return "[%s]" % ", ".join(result) + + if isinstance(val, resource.Resource): + return val._as_string() + + if isinstance(val, Task): + return val.path + + return str(val) +#@-node:_as_string +#@+node:_step_tasks +def _step_tasks(task): + if isinstance(task, Task): + yield task + + stack = [iter(task.children)] + while stack: + for task in stack[-1]: + yield task + + if task.children: + stack.append(iter(task.children)) + break + else: + stack.pop() +#@-node:_step_tasks +#@-node:Utilities +#@+node:Cache +instrumentation_cache = {} +balancing_cache = {} + +def clear_cache(): + instrumentation_cache.clear() + balancing_cache.clear() +#@nonl +#@-node:Cache +#@+node:Resource Allocators +#@+others +#@+node:VariableLoad +def VariableLoad(limit=0): + """ + Allocates the resource with maximal possible load. + If limit is given, a the load is at least limit or more. + """ + try: + balance = me.balance + except NameError: + balance = SLOPPY + + if balance != SLOPPY: + raise RuntimeError("You may specify variable_load only with balance=SLOPPY") + + return -limit +#@-node:VariableLoad +#@+node:_calc_load +def _calc_load(task, resource): + #changed at the resource instance + load = resource.__dict__.get("load") + if load is not None: return load + + load = task.__dict__.get("load") + if load is not None: return load + + #inherited by the task + return min(task.load, task.max_load, resource.max_load or 100.0) +#@-node:_calc_load +#@+node:_calc_maxload +def _calc_maxload(task, resource): + #changed at the resource instance + max_load = resource.__dict__.get("max_load") + if max_load: return max_load + + #an explicit load can overwrite max_load + load = max(resource.__dict__.get("load", 0), + task.__dict__.get("load"), 0) + + #change at the task + max_load = task.__dict__.get("max_load") + if max_load: return max(max_load, load) + + #inherited by the resource + max_load = resource.max_load + if max_load: return max(max_load, load) + + #inherited by the task + return max(task.max_load, load) +#@-node:_calc_maxload +#@+node:class AllocationAlgorithm +class AllocationAlgorithm(object): + """This class is a base for resource allocation algorithms""" + #@ @+others + #@+node:test_allocation + def test_allocation(self, task, resource): + """This method simulates the allocation of a specific resource. + It returns a list of values representing the state of the allocation. + The task allocator calls test_allocation for every alternative resource. + It compares the first items of all return lists, and allocates the + resource with the minum first item value""" + return (task.end, ) + #@-node:test_allocation + #@+node:allocate + def allocate(self, task, state): + """This method eventually allocates a specific resource. + State is the return list of test_allocation""" + pass + #@-node:allocate + #@-others +#@-node:class AllocationAlgorithm +#@+node:class StrictAllocator +class StrictAllocator(AllocationAlgorithm): + """This class implements the STRICT resource allocation""" + #@ @+others + #@+node:_distribute_len_loads + def _distribute_len_loads(self, task, resource, effort, length): + # A special load calculation, if effort and length are given. + # and the resources have a defined maxload, the load must be + # individually calculated for each resource. + + # Formulars: r=resources, t=task + # effort = length * efficiency(t) * sum[load(r) * effiency(r)] + # ==> sum_load = sum[load(r) * effiency(r)] + # = effort / (length * efficiency(t)) + # + + sum_load = float(effort) / (task.efficiency * length) + + # algorithm: + # The goal is to distribute the load (norm_load) equally + # to all resources. If a resource has a max_load(r) < norm_load + # the load of this resource will be max_load(r), and the other + # resources will have another (higher) norm_load + + max_loads = map(lambda r: (_calc_maxload(task, r), r), resource) + max_loads.sort() + + efficiency_sum = sum(map(lambda r: r.efficiency, resource)) + norm_load = sum_load / efficiency_sum + + loads = {} + for max_load, r in max_loads[:-1]: + if max_load < norm_load: + loads[r] = max_load + efficiency_sum -= r.efficiency + sum_load -= max_load * r.efficiency + norm_load = sum_load / efficiency_sum + else: + loads[r] = norm_load + + max_load, r = max_loads[-1] + loads[r] = norm_load + return loads + #@-node:_distribute_len_loads + #@+node:test_allocation + def test_allocation(self, task, resource): + effort = task.__dict__.get("effort") + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + if task.performed_end: + start = to_start(max(task.performed_end, + task.root.calendar.now, + task.start)) + else: + start = task.start + if task.root.has_actual_data and task.complete == 0: + start = max(start, to_start(task.root.calendar.now)) + + base_start = to_start(task.performed_start or task.start) + calc_load = lambda r: _calc_load(task, r) + loads = map(lambda r: (r, calc_load(r)), resource) + + length = task.__dict__.get("length") + duration = task.__dict__.get("duration") + end = task.__dict__.get("end") + + #@ << correct length >> + #@+node:<< correct length >> + if length is not None: + length = to_delta(max(length - (task.start - base_start), 0)) + #@nonl + #@-node:<< correct length >> + #@nl + #@ << correct duration >> + #@+node:<< correct duration >> + if duration is not None: + delta = task.start.to_datetime() - base_start.to_datetime() + delta = to_delta(delta, True) + duration = to_delta(max(duration - delta, 0), True) + #@nonl + #@-node:<< correct duration >> + #@nl + #@ << check end >> + #@+node:<< check end >> + if end is not None: + length = end - start + if length <= 0: return False + #@nonl + #@-node:<< check end >> + #@nl + #@ << correct effort and (re)calculate length >> + #@+node:<< correct effort and (re)calculate length >> + if effort is not None: + effort -= task.performed_effort + effort = to_delta(max(effort, 0)) + if effort <= 0: return False + + if length is not None: + #if length and effort is set, the load will be calculated + length = length or task.calendar.minimum_time_unit + loads = self._distribute_len_loads(task, resource, + effort, length) + def calc_load(res): + return loads[res] + else: + #the length depends on the count of resources + factor = sum(map(lambda a: a[0].efficiency * a[1], + loads)) * task.efficiency + length = effort / factor + #@nonl + #@-node:<< correct effort and (re)calculate length >> + #@nl + #@ << set adjust_date and delta >> + #@+node:<< set adjust_date and delta >> + if length is not None: + adjust_date = lambda date: date + delta = to_delta(length).round() + else: + assert(duration is not None) + adjust_date = _to_datetime + delta = datetime.timedelta(minutes=duration) + #@nonl + #@-node:<< set adjust_date and delta >> + #@nl + + # find the earliest start date + start, book_load\ + = self.balance(task, start, delta, adjust_date, + calc_load, resource) + + end = to_end(start + delta) + start = to_start(start) + + if effort is None: + #length is frozen ==> a new effort will be calculated + factor = sum(map(lambda a: a[1], loads)) + length = end - start + + effort = to_delta(length * factor\ + + task.performed_effort).round() + + return (end, book_load), resource, calc_load, start, effort + #@-node:test_allocation + #@+node:allocate + def allocate(self, task, state): + # now really book the resource + end_bl, resource, calc_load, start, effort = state + end = end_bl[0] + cal = task.root.calendar + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + task.start = task.performed_start \ + and to_start(task.performed_start) \ + or to_start(start) + + task.end = end + task._unfreeze("length") + task._unfreeze("duration") + length = end - start + + for r in resource: + book_load = calc_load(r) + work_time = to_delta(length * book_load).round() + r.book_task(task, start, end, book_load, work_time, False) + + #the following lines are important to be exactly at this + #positions in that order: + # done and todo are dependend on: + # - the existence of effort (if effort was set or not set) + # - book_task (they can only be calculated, if the task is booked) + # - booked_resource (to get the booked tasks) + task.booked_resource = resource + task.done = task.done + task.todo = task.todo + task.length = end - task.start + task.effort = to_delta(effort + task.performed_effort) + #@-node:allocate + #@+node:balance + #now effort exists always + + + def balance(self, task, start, delta, adjust_date, + calc_load, resource): + book_load = max(map(lambda r: r.get_load(task.start, task.scenario), resource)) + return start, book_load + #@-node:balance + #@-others +#@-node:class StrictAllocator +#@+node:class SmartAllocator + + +class SmartAllocator(StrictAllocator): + #@ @+others + #@+node:balance + def balance(self, task, start, delta, adjust_date, + calc_load, resource): + #find the earliest start date, at which all + #resources in the team are free + + cal = task.root.calendar + to_start = task._to_start + start = adjust_date(start) + scenario = task.scenario + + while True: + #we have finished, when all resources have the + #same next free start date + for r in resource: + max_load = _calc_maxload(task, r) + load = calc_load(r) + + #find the next free time of the resource + s = r.find_free_time(start, delta, load, max_load, scenario) + if s != start: + s = to_start(s) + start = adjust_date(s) + break + else: + #only one resource + break + + return start, 1.0 + #@-node:balance + #@-others +#@-node:class SmartAllocator +#@+node:class SloppyAllocator + + +class SloppyAllocator(AllocationAlgorithm): + #@ @+others + #@+node:test_allocation + def test_allocation(self, task, resource): + if task.__dict__.has_key("effort"): + return self.test_allocation_effort(task, resource) + + return self.test_allocation_length(task, resource) + #@-node:test_allocation + #@+node:test_allocation_length + def test_allocation_length(self, task, resource): + #length is frozen ==> effort will be calculated + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + end = task.end + if task.performed_end: + start = to_start(max(task.performed_end, + task.root.calendar.now, + start)) + else: + start = task.start + + base_start = to_start(task.performed_start or task.start) + length = to_delta(max(task.length - (start - base_start), 0)) + sum_effort = 0 + intervals = [] + scenario = task.scenario + for r in resource: + date = start + max_load = _calc_maxload(task, r) + book_load = _calc_load(task, r) + + while date < end: + #find free time intervals and add them for booking + endi, load = r.end_of_booking_interval(date, task) + endi = min(endi, end) + endi = to_end(endi) + + if book_load <= 0: + #variable book_load ==> calc the maxmimal possible book_load >= (the given book_load) + used_book_load = - book_load + diff_load = max_load - load + if diff_load and diff_load >= book_load: + used_book_load = diff_load + else: + used_book_load = max_load + else: + used_book_load = book_load + + if max_load - load >= used_book_load: + intervals.append((r, used_book_load, date, endi)) + sum_effort = (endi - date) * used_book_load + + date = to_start(endi) + + return -sum_effort, end, resource, intervals + #@-node:test_allocation_length + #@+node:test_allocation_effort + def test_allocation_effort(self, task, resource): + #effort is frozen ==> length will be calculated + + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + intervals = [] + effort = task.__dict__.get("effort") + + if task.performed_end: + next_date = to_start(max(task.performed_end, + task.root.calendar.now, + task.start)) + else: + next_date = task.start + if task.root.has_actual_data and task.complete == 0: + next_date = max(next_date, to_start(task.root.calendar.now)) + + #walks chronologicly through the booking + #intervals of each resource, and reduces + #the effort for each free interval + #until it becomes 0 + + alloc_effort = effort + effort -= task.performed_effort + while effort > 0: + date = next_date + + interval_resource = [] + interval_end = to_start(sys.maxint) + factor = 0 + + for r in resource: + max_load = _calc_maxload(task, r) + book_load = _calc_load(task, r) + end, load = r.end_of_booking_interval(date, task) + interval_end = to_start(min(end, interval_end)) + + if book_load <= 0: + #variable book_load ==> calc the maxmimal possible book_load >= (the given book_load) + book_load = - book_load + diff_load = max_load - load + if diff_load and diff_load >= book_load: + book_load = diff_load + else: + book_load = max_load + + if book_load + load <= max_load: + resource_factor = book_load * r.efficiency + interval_resource.append((r, book_load, resource_factor)) + factor += resource_factor + + + + next_date = interval_end + if factor: + factor *= task.efficiency + length = to_delta(effort / factor).round() + end = date + length + + if interval_end >= end: + next_date = interval_end = end + effort = 0 + book_end = end + else: + book_end = interval_end + length = book_end - date + minus_effort = length * factor + effort -= minus_effort + + book_end = to_end(book_end) + intervals.append((date, book_end, length, interval_resource)) + + return next_date, alloc_effort, resource, intervals + #@-node:test_allocation_effort + #@+node:allocate + def allocate(self, task, state): + if task.__dict__.has_key("effort"): self.allocate_effort(task, state) + else: self.allocate_length(task, state) + #@-node:allocate + #@+node:allocate_length + def allocate_length(self, task, state): + # now really book the resource + neg_sum_effort, end, resource, intervals = state + + cal = task.root.calendar + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + task.start = to_start(task.performed_start or task.start) + task.end = to_end(end) + task._unfreeze("length") + task._unfreeze("duration") + + effort = 0 + for r, load, s, e in intervals: + work_time = to_delta((e - s) * load).round() + effort += work_time + r.book_task(task, s, e, load, work_time, False) + + #see comment at StrictAllocator.allocate + task.booked_resource = resource + task.done = task.done + task.todo = task.todo + task.effort = to_delta(effort + task.performed_effort).round() + #@-node:allocate_length + #@+node:allocate_effort + def allocate_effort(self, task, state): + # now really book the resource + end, effort, resource, intervals = state + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + task.start = task.performed_start \ + and to_start(task.performed_start) \ + or to_start(intervals[0][0]) + task.end = to_end(end) + task._unfreeze("length") + task._unfreeze("duration") + + for start, end, length, resources in intervals: + for r, load, factor in resources: + work_time = to_delta(length * load) + r.book_task(task, start, end, load, work_time, False) + + task.booked_resource = resource + task.done = task.done + task.todo = task.todo + task.effort = to_delta(effort) + task.length = task.end - task.start + #@-node:allocate_effort + #@-others +#@-node:class SloppyAllocator +#@-others + +_smart_allocator = SmartAllocator() +_sloppy_allocator = SloppyAllocator() +_strict_allocator = StrictAllocator() +_allocators = { SMART: _smart_allocator, + SLOPPY: _sloppy_allocator, + STRICT: _strict_allocator } + +_allocator_strings = { SMART: "SMART", + SLOPPY: "SLOPPY", + STRICT: "STRICT" } +#@-node:Resource Allocators +#@+node:Load Calculators +#@+node:YearlyMax +def YearlyMax(value): + """ + Calculates a load parameter with a maximal yearly workload + """ + #@ << calculate calendar and time_diff >> + #@+node:<< calculate calendar and time_diff >> + try: + cal = me.calendar + except NameError: + cal = pcalendar._default_calendar + + time_diff = cal.Minutes(value) + #@nonl + #@-node:<< calculate calendar and time_diff >> + #@nl + return float(time_diff) / \ + (cal.working_days_per_year \ + * cal.working_hours_per_day \ + * 60) +#@nonl +#@-node:YearlyMax +#@+node:WeeklyMax +def WeeklyMax(value): + """ + Calculates a load parameter with a maximal weekly workload + """ + #@ << calculate calendar and time_diff >> + #@+node:<< calculate calendar and time_diff >> + try: + cal = me.calendar + except NameError: + cal = pcalendar._default_calendar + + time_diff = cal.Minutes(value) + #@nonl + #@-node:<< calculate calendar and time_diff >> + #@nl + return float(time_diff) / \ + (cal.working_days_per_week \ + * cal.working_hours_per_day \ + * 60) + +#@-node:WeeklyMax +#@+node:MonthlyMax +def MonthlyMax(value): + """ + Calculates a load parameter with a maximal monthly workload + """ + #@ << calculate calendar and time_diff >> + #@+node:<< calculate calendar and time_diff >> + try: + cal = me.calendar + except NameError: + cal = pcalendar._default_calendar + + time_diff = cal.Minutes(value) + #@nonl + #@-node:<< calculate calendar and time_diff >> + #@nl + return float(time_diff) / \ + (cal.working_days_per_month \ + * cal.working_hours_per_day \ + * 60) + +#@-node:MonthlyMax +#@+node:DailyMax +def DailyMax(value): + """ + Calculates a load parameter with a maximal daily workload + """ + #@ << calculate calendar and time_diff >> + #@+node:<< calculate calendar and time_diff >> + try: + cal = me.calendar + except NameError: + cal = pcalendar._default_calendar + + time_diff = cal.Minutes(value) + #@nonl + #@-node:<< calculate calendar and time_diff >> + #@nl + return float(time_diff) / (cal.working_hours_per_day * 60) +#@-node:DailyMax +#@-node:Load Calculators +#@+node:Task +#@+node:class _TaskProperty +class _TaskProperty(object): + #@ @+others + #@+node:__init__ + def __init__(self, method): + self.method = method + #@-node:__init__ + #@+node:__get__ + def __get__(self, instance, owner): + if not instance: + return None + + return instance._wrap_attrib(self.method) + #@-node:__get__ + #@-others +#@-node:class _TaskProperty +#@+node:class _RoundingTaskProperty +class _RoundingTaskProperty(object): + #@ @+others + #@+node:__init__ + def __init__(self, method, name): + self.method = method + self.name = name + #@-node:__init__ + #@+node:__get__ + def __get__(self, instance, owner): + if not instance: + return None + + result = instance._wrap_attrib(self.method).round() + if instance._is_frozen: + #correct the attrib to the rounded value + setattr(instance, self.name, result) + + return result + #@-node:__get__ + #@-others +#@-node:class _RoundingTaskProperty +#@+node:class Task +class Task(object): + #@ << description >> + #@+node:<< description >> + """ + This class represents a single task in the project tree. A task + can have other child tasks, or is a leaf of the tree. Resources + will be allocated only to leafes. You will never create task + objects by your self, they are created indirectly by Projects. + + @var root: + Returns the root project task. + + @var up: + Returns the parent task. + + @var title: + Specifies an alternative more descriptive name for the task. + + @var start: + The start date of the task. Valid values are expressions and + strings specifing a datatime + + @var end: + The end date of the task. Valid values are expressions and + strings. + + @var effort: + Specifies the effort needed to complete the task. Valid values + are expressions and strings. (Todo: What happens, in case of + specified performance data...) + + + @var length: + Specifies the time the task occupies the resources. This is + working time, not calendar time. 7d means 7 working days, not one + week. Whether a day is considered a working day or not depends on + the defined working hours and global vacations. + + @var duration: + Specifies the time the task occupies the resources. This is + calendar time, not working time. 7d means one week. + + @var buffer: + Specifies the time a task can be delayed, without moving dependend + milestones. A Task with a buffer S{<=} 0d is part of the critical + chain. This attribute is readonly. + + @var complete: + Specifies what percentage of the task is already completed. + + @var todo: + Specifies the effort, which needs to be done to complete a + task. This is another (indirect) way to specify the ME{complete} + attribute. + + @var done: + Specifies the work effort, which has been already done. This + attribute is readonly. + + @var estimated_effort: + Specifies the estimated_effort given by setting the effort property. + + @var performed: + Specifies a list of actual working times performed on the task. + The format is: C{[ (resource, from, to, time), ... ]} + + @var performed_work_time: + Specifies the sum of all working times. This attribute is + readonly. + + @var performed_effort: + Specifies the complete effort of all working times. This attribute is + readonly. + + @var performed_start: + The start date of the performed data. + + @var performed_end: + The end date of the performed data. + + @var performed_resource: + The resources who have already performed on the task. This attribute is readonly. + + + @var balance: + Specifies the resource allocation type. Possible values are + CO{STRICT}, CO{SLOPPY}, CO{SMART}. + + @var resource: + Specifies the possible resources, that may be allocated for the + task. + + @var booked_resource: + Specifies the allocated resources of a task. This attribute is + readonly. + + @var load: + Specifies the daily load of a resource for an allocation of the + specified task. A load of 1.0 (default) means the resource is + allocated for as many hours as specified by + ME{working_hours_per_day}. A load of 0.5 means half that many + hours. + + @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 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 milestone: + Specified if the task is a milestone. The possible values are + C{True} or "later". If the start date of the milestone is not + a valid working date, the milestone will appear at the previous + working date before the given start date. If "later" is specified + the milestone will appear at the next valid working date. + A milestone has always an effort of 0d. + + @var priority: + Specifies a priority between 1 and 1000. A task with higher + priority is more likely to get the requested resources. The + default priority is 500. + + @var children: + Specifies a list of all subtasks. A task without children is + called a leaf task index{leaf task} otherwise it is called a + parent task index{parent task}. This attribute is readonly. + + @var depth: + Specifies the depth of the task within the hierachy. This + attribute is readonly. + + @var index: + Specifies a structural index number. This attribute is readonly. + + @var path: + Specifies the path. + + @var copy_src: + Specifies the path to an other task. When you set this attribute, + all attributes (except of ME{start} and ME{end}) of copy_src will + be copied to the current task. This is usefull if you want to + define the same task, in diffent project definitions. It acts like + a task link. + + @var scenario: + The scenario which is currently evaluated. This attribute is readonly. + + @var dont_inherit: + A list of attribute names, which will be not inherited by + subtasks. + + @var calendar: + Specifies the task calendar. + + @var working_days_per_week: + Specifies the days within a working week. This value is used + internally to convert time differences from weeks to days. The + default value is 5 days. + + @var working_days_per_month: + Specifies the days within a working month. This value is used + internally to convert time differences from months to days. The + default value is 20 days. + + @var working_days_per_year: + Specifies the days within a working year. This value is used + internally to convert time differences from years to days The + default value is 200 days. + + @var working_hours_per_day: + Specifies the hours within a working day. This value is used + internally to convert time differences from are entered in days to + hours. The default value is 8 hours. + + @var minimum_time_unit: + Specifies the minimum resolution in minutes for the task + scheduling. The default value is 15 minutes. + + @var vacation: + Specifies a public vacation for the calendar. 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. + + @var extra_work: + Specifies additional worktime. 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. + + @var working_days: + Specifies the weekly working time within calendar. The format of + this attribute is: [ (day_range, time_range, ...), (day_range, time_range, ...), ... ]. + day_range is a comma sperated string of week days. Valid values + are mon, tue, wed, thu, fri, sat, sun. + time_range is string specifing a time interval like + 8:00-10:00. You can specified any number of time_ranges, following + the first. + + @var now: + Specifies the current daytime and is a date literal. ME{now} is + used to calculate several task attributes. + + """ + #@nonl + #@-node:<< description >> + #@nl + #@ << declarations >> + #@+node:<< declarations >> + # Variables for the gui interface + _date_completion = { "Date": 'Date("|")', + "max": "max(|)", + "min": "min(|)", + "Multi" : "Multi(|)" } + + + _delta_completion = { "Delta" : 'Delta("|")', + "Multi" : "Multi(|)" } + + + __attrib_completions__ = { \ + "def NewTask():" : "def |NewTask():\n", + "milestone": 'milestone = True', + "start": 'start = ', + "end": 'end = ', + "effort": 'effort = "|"', + "duration": 'duration = "|"', + "length": 'length = "|"', + "todo": 'todo = "|"', + "done": 'done = "|"', + "title": 'title = "|"', + "load": 'load = ', + "max_load": 'max_load = ', + "efficiency": 'efficiency = ', + "complete": 'complete = ', + "copy_src": 'copy_src =', + "__constraint__": '__constraint__():\n|"', + "priority": 'priority = ', + "balance" : 'balance = ', + "resource": 'resource = ', + "performed" : 'performed = [(|resource, "2002-02-01", "2002-02-05", "2H"),]', + "add_attrib": "add_attrib(|'name', None)", + "working_days_per_week": 'working_days_per_week = ', + "working_days_per_month": 'working_days_per_month = ', + "working_days_per_year": 'working_days_per_year = ', + "working_hours_per_day": 'working_hours_per_day = ', + "minimum_time_unit": 'minimum_time_unit = ', + "vacation": 'vacation = [("|2002-02-01", "2002-02-05")]', + "extra_work": 'extra_work = [("|2002-02-01", "2002-02-05")]', + "working_days" : 'working_days = ["|mon,tue,wed,thu,fri", "8:00-12:00", "13:00-17:00"]', + "now": 'now = "|"', + "calendar" : 'calendar = ', + "#load": { "YearlyMax": 'YearlyMax("|")', + "WeeklyMax": 'WeeklyMax("|")', + "MonthlyMax": 'MonthlyMax("|")', + "DailyMax": 'DailyMax("|")', + "VariableLoad" : "VariableLoad(|)"}, + "#max_load": { "YearlyMax": 'YearlyMax("|")', + "WeeklyMax": 'WeeklyMax("|")', + "MonthlyMax": 'MonthlyMax("|")', + "DailyMax": 'DailyMax("|")' }, + "#start": _date_completion, + "#end": _date_completion, + "#effort": _delta_completion, + "#duration": _delta_completion, + "#length": _delta_completion, + "#todo": _delta_completion, + "#done": _delta_completion, + "#resource" : "get_resource_completions", + "#calendar" : "get_calendar_completions", + "#balance": { "STRICT": "STRICT", + "SMART": "SMART", + "SLOPPY": "SLOPPY" } } + + + formats = { "start" : "%x %H:%M", + "end" : "%x %H:%M", + "performed_start" : "%x %H:%M", + "performed_end" : "%x %H:%M", + "load" : "%.2f", + "length" : "%dd{ %HH}{ %MM}", + "effort" : "%dd{ %HH}{ %MM}", + "estimated_effort" : "%dd{ %HH}{ %MM}", + "performed_effort" : "%dd{ %HH}{ %MM}", + "duration" : "%dd{ %HH}{ %MM}", + "complete" : "%i", + "priority" : "%i", + "todo" : "%dd{ %HH}{ %MM}", + "done" : "%dd{ %HH}{ %MM}", + "efficiency" : "%.2f", + "buffer" : "%dd{ %HH}{ %MM}", + "costs" : "%.2f", + "sum" : "%.2f", + "max" : "%.2f", + "min" : "%.2f", + "milestone" : "%s", + "resource" : "%s", + "booked_resource" : "%s", + "performed_resource" : "%s" } + + _constraint = None + _is_frozen = False + _is_compiled = False + _is_parent_referer = False + + scenario = None # only for autocompletion + milestone = False + performed = () + performed_resource = () + booked_resource = () + _performed_resource_length = () + _resource_length = () + dont_inherit = () + performed_start = None + performed_end = None + performed_work_time = pcalendar.Minutes(0) + + _setting_hooks = {} + #@nonl + #@-node:<< declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, func, name, parent=None, index=1): + + assert(type(func) == types.FunctionType) + + func_key = (func.func_code, func.func_closure and id(func.func_closure)) + + try: + instrumented = instrumentation_cache[func_key] + except KeyError: + instrumented = _instrument(func) + instrumented.org_code = func_key + instrumentation_cache[func_key] = instrumented + + func.task_func = instrumented # will be used in the gui + self._function = instrumented + self.name = name + self.up = parent + self.children = [] + self._sources = {} # all tasks, I am linked to + self._dependencies = {} # all tasks that link to me + self._original_values = {} + self._properties = {} # a registry of all non standard attributes + self.title = self.name + self.root = parent and parent.root or self + self.scenario = self.root.scenario + self.path = parent and parent.path + "." + name or name + self.depth = len(self.path.split(".")) - 1 + self.index = parent and ("%s.%i" % (parent.index, index)) \ + or str(index) + if self.formats.has_key(name): + raise AttributeError("Task name '%s' hides attribute of parent." \ + % name) + + cal = self.calendar + self._to_delta = cal.Minutes + self._to_start = cal.StartDate + self._to_end = cal.EndDate + + #@-node:__init__ + #@+node:__iter__ + def __iter__(self): + return _step_tasks(self) + #@-node:__iter__ + #@+node:__repr__ + def __repr__(self): + return "" % self.name + #@-node:__repr__ + #@+node:__cmp__ + def __cmp__(self, other): + try: + return cmp(self.path, other.path) + except Exception: + return cmp(self.path, other) + #@-node:__cmp__ + #@+node:__getattr__ + def __getattr__(self, name): + try: + if name[0] != "_": + parent = self.up + while parent: + if name not in parent.dont_inherit: + result = getattr(parent, name) + if not (isinstance(result, Task) and result.up == parent): + return result + + parent = parent.up + except AttributeError: + pass + except IndexError: + raise AttributeError() + + exception = AttributeError("'%s' is not a valid attribute of '%s'" + % (name, self.path)) + exception.is_frozen = self._is_frozen + raise exception + #@-node:__getattr__ + #@+node:_idendity_ + def _idendity_(self): return self.root.id + self.path[4:] + #@-node:_idendity_ + #@+node:_set_hook + def _set_hook(cls, attrib_name, function=None): + if function: + cls._setting_hooks[attrib_name] = function + else: + try: + del cls._setting_hooks[attrib_name] + except KeyError: pass + + + _set_hook = classmethod(_set_hook) + #@nonl + #@-node:_set_hook + #@+node:Public methods + #@+node:to_string + def to_string(self): return _StringConverter(self) + to_string = property(to_string) + #@nonl + #@-node:to_string + #@+node:indent_name + def indent_name(self, ident=" "): + """ + returns a indented name, according to its depth in the hierachy. + """ + + return ident * self.depth + self.name + + indent_name.attrib_method = True + indent_name.__call_completion__ = "indent_name()" + #@-node:indent_name + #@+node:costs + def costs(self, cost_name, mode="ep"): + """ + calculates the resource costs for the task. + cost_name is the name of a rate attribute of the reosurce + mode is character combination: + e calculates the estimated costs + p calculates the performed costs + ==> pe calculates all costs + """ + + if self.children: + return sum([ c.costs(cost_name, mode) for c in self.children]) + + costs = 0 + if 'e' in mode: + costs += sum(map(lambda rl: getattr(rl[0], cost_name) * rl[1], + self._resource_length)) + + if 'p' in mode: + costs += sum(map(lambda rl: getattr(rl[0], cost_name) * rl[1], + self._performed_resource_length)) + + costs /= (60.0 * self.root.calendar.working_hours_per_day) + return round(costs, 2) + + costs.attrib_method = True + costs.__call_completion__ = 'costs("|")' + #@-node:costs + #@+node:sum + def sum(self, attrib_name): + val = 0 + + if self.children: + val += sum(map(lambda c: c.sum(attrib_name), self.children)) + if self.is_inherited(attrib_name): + return val + + if attrib_name not in self.dont_inherit: + return val + + return val + getattr(self, attrib_name) + + sum.attrib_method = True + sum.__call_completion__ = 'sum("|")' + + #@-node:sum + #@+node:min + def min(self, attrib_name): + if self.children: + return min(map(lambda c: c.min(attrib_name), self.children)) + + return getattr(self, attrib_name) + + min.attrib_method = True + min.__call_completion__ = 'min("|")' + + #@-node:min + #@+node:max + def max(self, attrib_name): + if self.children: + return max(map(lambda c: c.max(attrib_name), self.children)) + + return getattr(self, attrib_name) + + max.attrib_method = True + max.__call_completion__ = 'max("|")' + + #@-node:max + #@+node:all_resources + def all_resources(self): + result = self._all_resources_as_dict() + result = result.keys() + result.sort() + return result + #@-node:all_resources + #@+node:get_task + def get_task(self, path=None): + """ + Returns a task with the given path. + """ + + if not path: + return self + + names = path.split(".") + rest = ".".join(names[1:]) + result = getattr(self, names[0], None) + return isinstance(result, Task) and result.get_task(rest) or None + #@-node:get_task + #@+node:snapshot + def snapshot(self, indent="", name=None): + text = indent + "def %s():\n" % (name or self.name) + indent += " " + for name in ("priority", "balance", "complete", + "milestone", "end", "start", "effort", "load"): + val = getattr(self, name, None) + if val is None: + continue + + if name[0] == "_": + name = name[1:] + + text += "%s%s = %s\n" % (indent, name, _as_string(val)) + + for name in self._properties: + if name.startswith("performed"): continue + val = getattr(self, name, None) + try: + if issubclass(val, resource.Resource): continue + except TypeError: + pass + text += "%s%s = %s\n" % (indent, name, _as_string(val)) + + resources = tuple(self._iter_booked_resources()) + if resources: + text += "%sresource = \\\n" % indent + def make_resource(res): + return "%s %s" \ + % (indent, res.snapshot()) + + text += "&\\\n".join(map(make_resource, resources)) + "\n" + + def make_resource_booking(res): + def make_booking(booking): + return '%s (%s, "%s", "%s", "%sM"),' \ + % (indent, res.name, + booking.book_start.strftime("%Y%m%d %H:%M"), + booking.book_end.strftime("%Y%m%d %H:%M"), + booking.work_time) + + return "\n".join(map(make_booking, res.get_bookings(self))) + + + text += "%sperformed = [\n" % indent + text += "\n".join(map(make_resource_booking, resources)) + "]" + + + child_text = map(lambda c: c.snapshot(indent), self.children) + text += "\n\n" + text += "".join(child_text) + + return text + #@-node:snapshot + #@+node:is_inherited + def is_inherited(self, attrib_name): + return not self.__dict__.has_key(attrib_name) + #@-node:is_inherited + #@+node:formatter + def formatter(self, attrib_name, arg=None, format=None): + """returns a function which is able + to convert the value of the given attrib_name to a string""" + + formats = self.formats + format = format or formats.get(attrib_name) + + if attrib_name in ("start", "end", "length", "effort", + "done", "todo", "buffer", "estimated_effort", + "performed_effort", "performed_start", "performed_end"): + def save_strftime(v): + try: + return v.strftime(format) + #except AttributeError: some bug avoid catching this exception + except Exception: + return str(v) + + return save_strftime + + if attrib_name == "duration": + def save_strftime(v): + try: + return v.strftime(format, True) + except AttributeError: + return str(v) + + return save_strftime + + if attrib_name in ("booked_resource", "performed_resource"): + def get_resource_name(v): + title = getattr(v, "title", None) + if title: return title + return ", ".join([r.title for r in v]) + return get_resource_name + + if arg and attrib_name in ("costs", "sum", "max", "min"): + format = formats.get("%s(%s)" % (attrib_name, arg), format) + + if format: + return lambda v: locale.format(format, v, True) + + return str + #@-node:formatter + #@-node:Public methods + #@+node:Resource allocation Methods + #@+node:_all_resources_as_dict + def _all_resources_as_dict(self): + if self.children: + result = {} + for c in self.children: + result.update(c._all_resources_as_dict()) + + return result + + if self.resource: + return dict(map(lambda r: (r, 1), self.resource.all_members())) + + return {} + #@-node:_all_resources_as_dict + #@+node:_test_allocation + def _test_allocation(self, resource_state, allocator): + resource = self.resource._get_resources(resource_state) + if not resource: + return False + + return allocator.test_allocation(self, resource) + #@-node:_test_allocation + #@+node:_allocate + def _allocate(self, state, allocator): + allocator.allocate(self, state) + #activate cache for done and todo + + if self.start.to_datetime() > self.end.to_datetime(): + #this can happen when performed effort are + #during non working time + tmp = self.start + self.start = self.end + self.end = tmp + + for r in self.performed_resource: + r.correct_bookings(self) + + self._resource_length = map(lambda r: (weakref.proxy(r), \ + r.length_of(self)), + self._iter_booked_resources()) + #@-node:_allocate + #@+node:_convert_performed + def _convert_performed(self, all_resources): + performed = self.performed + if not performed: return False + + if not isinstance(performed, (tuple, list)) \ + or not isinstance(performed[0], (tuple, list)) \ + or not len(performed[0]) >= 3: + self._raise(TypeError("""The format of the performed attribute must be: + [( res_name, start_literal, end_literal, working_time ), ... ]. + """), "performed") + + round_down_delta = self.root.calendar.minimum_time_unit / 2 + round_down_delta = datetime.timedelta(minutes=round_down_delta) + + def convert_item(index): + item = performed[index] + res, start, end = item[:3] + if isinstance(res, str): + found = filter(lambda r: r.name == res, all_resources) + if found: res = found[0] + + try: + if not isinstance(res, (resource.Resource, + resource._MetaResource)): + raise ValueError("the resource '%s' is unknown." % res) + + start = _to_datetime(start) + end = _to_datetime(end) + + if len(item) > 3: + working_time = self._to_delta(item[3]).round() + else: + working_time = self._to_delta(end - start, True) + + return ((res, start, end, working_time), index) + except Exception, exc: + self._raise(exc.__class__("Item %i: %s" \ + % (index + 1, str(exc))), + "performed") + + converted = dict(map(convert_item, range(len(performed)))) + converted = converted.items() + converted.sort() + + #check for overlapping items + last_res = None + for item, index in converted: + res, start, end, work_time = item + if last_res == res and start < last_end: + self._warn("Items %i, %i: %s and %s are overlapping." \ + % (last_index + 1, index + 1, + str(performed[last_index]), + str(performed[index])), + "performed") + + last_res = res + last_end = end + last_index = index + + self._performed = map(lambda x: x[0], converted) + return True + #@-node:_convert_performed + #@+node:_allocate_performed + def _allocate_performed(self, performed): + if not performed: return + + to_delta = self._to_delta + to_start = self._to_start + to_end = self._to_end + + last = datetime.datetime.min + first = datetime.datetime.max + effort = 0 + work_time_sum = 0 + zero_minutes = to_delta(0) + minimum_time_unit = to_delta(self.calendar.minimum_time_unit) + summary = {} + + for item in performed: + res, start, end, work_time = item + effort += work_time * self.efficiency * res.efficiency + work_time_sum += work_time + + res = res() + ss, es, wts = summary.get(res, (datetime.datetime.max, + datetime.datetime.min, + zero_minutes)) + summary[res] = (min(ss, start), max(es, end), wts + work_time) + + for r, v in summary.iteritems(): + start, end, work_time = v + assert(start.__class__ is datetime.datetime) + assert(end.__class__ is datetime.datetime) + + #the booking limits should be inside the workingtime + #to display them correct in resource charts + cstart = to_start(start).to_datetime() + if cstart > start: cstart = to_end(start).to_datetime() + + cend = to_end(end).to_datetime() + if cend < end: cend = to_start(end).to_datetime() + + if self.root.is_snapshot: + delta = to_end(cend) - to_start(cstart) + else: + delta = to_delta(cend - cstart).round() + + if not delta: + delta = minimum_time_unit + + book_load = float(work_time) / delta + r().book_task(self, cstart, cend, book_load, work_time, True) + last = max(end, last) + first = min(start, first) + + self._performed_resource_length = tuple([ (r, v[2]) for r, v in summary.iteritems() ]) + self.performed_resource = tuple(summary.keys()) + self.performed_end = last + self.performed_start = first + self.performed_effort = to_delta(effort) + self.performed_work_time = to_delta(work_time_sum) + self._check_completion() + #@-node:_allocate_performed + #@+node:_iter_booked_resources + def _iter_booked_resources(self): + result = dict(map(lambda r: (r, 1), self.performed_resource)) + result.update(dict(map(lambda r: (r, 1), self.booked_resource))) + return result.iterkeys() + #@-node:_iter_booked_resources + #@-node:Resource allocation Methods + #@+node:Compile Methods + #@+node:_generate + def _generate(self, deferred=None): + do_raise = False + deferred = deferred or [ self ] + while deferred: + new_deferred = [] + for task in deferred: + task._compile(new_deferred, do_raise) + + do_raise = deferred == new_deferred + deferred = new_deferred + #@-node:_generate + #@+node:_recalc_properties + def _recalc_properties(self): + if not self._properties: return + self.__compile_function([], False, _MeProxyRecalc(self)) + self._is_compiled = True + #@-node:_recalc_properties + #@+node:_compile + def _compile(self, deferred, do_raise): + self.dont_inherit = () + self._constraint = None + self._original_values.clear() + self._properties.clear() + + try: + self.__at_compile + #@ << raise child recursion error >> + #@+node:<< raise child recursion error >> + self._raise(RecursionError("A child defines a "\ + "recursive definition at %s" % self.path)) + #@-node:<< raise child recursion error >> + #@nl + except AttributeError: + self.__at_compile = self, "" + + try: + self.__compile_function(deferred, do_raise, _MeProxy(self)) + finally: + del self.__at_compile + + for c in self.children: + if not c._is_compiled: + c._compile(deferred, do_raise) + + if self._is_compiled: + self.__check_milestone() + self.__check_task() + self.root.has_actual_data |= self.__dict__.has_key("performed") + + #@-node:_compile + #@+node:__compile_function + def __compile_function(self, deferred, do_raise, me_instance): + self._is_compiled = self._is_frozen + + restore_globals = [] + globals_ = self._function.func_globals + + #@ << set function global values >> + #@+node:<< set function global values >> + def to_value_wrapper(a): + if isinstance(a, _ValueWrapper): + return a + + return _ValueWrapper(a, [(None, None)]) + + def my_max(*args): + return max(map(to_value_wrapper, args)) + + def my_min(*args): + return min(map(to_value_wrapper, args)) + + globals_["me"] = me_instance + + if self._is_compiled: + globals_["up"] = self.up + globals_["root"] = self.root + else: + globals_["up"] = _Path(self.up, "up") + globals_["root"] = _Path(self.root, "root") + + globals_["Delta"] = self._to_delta + globals_["Date"] = self._to_start + globals_["max"] = my_max + globals_["min"] = my_min + globals_["add_attrib"] = me_instance.add_attrib + #@nonl + #@-node:<< set function global values >> + #@nl + #@ << set me in global functions >> + #@+node:<< set me in global functions >> + #@+at + # Is used for functions like YearlyMax, MonthlyMax, .... + #@-at + #@@code + for name in self._function.global_names: + try: + obj = globals_[name] + if isinstance(obj, types.FunctionType): + fg = obj.func_globals + if not fg.has_key("me") and "me" in obj.func_code.co_names: + restore_globals.append(fg) + fg["me"] = me_instance + except KeyError: continue + #@nonl + #@-node:<< set me in global functions >> + #@nl + try: + #@ << eval function >> + #@+node:<< eval function >> + if do_raise: + try: + self._function() + self._is_compiled = True + except _IncompleteError, e: + src = e.args[1] + if src is not self: + self.__at_compile = e.args[1:] + src._compile([], True) + + raise + else: + try: + self._function() + self._is_compiled = True + except AttributeError, e: + #print "AttributeError:", e, self.name, e.is_frozen, do_raise + deferred.append(self) + except _IncompleteError: + #print "_IncompleteError:", id(self), self.name, do_raise + deferred.append(self) + except RecursionError: + self._is_parent_referer = True + deferred.append(self) + #@nonl + #@-node:<< eval function >> + #@nl + finally: + for fg in restore_globals: + del fg["me"] + + #@-node:__compile_function + #@-node:Compile Methods + #@+node:Setting methods + #@+node:_set_attrib + def _set_attrib(self, name, value): + + if value is _NEVER_USED_: return + + try: + value = self._setting_hooks[name](self, name, value) + except KeyError: pass + + if name == "__constraint__": + self._constraint = value + return + + if type(value) == types.FunctionType: + if value.func_code.co_argcount == 0: + #@ << add child task >> + #@+node:<< add child task >> + try: + task = self.__dict__[name] + except KeyError: + task = Task(value, name, self, len(self.children) + 1) + self.children.append(task) + setattr(self, task.name, task) + return + #@nonl + #@-node:<< add child task >> + #@nl + + if name[0] == "_": + #private vars will not be set + return + + if isinstance(value, _Path): + value = value._task + + set_method = getattr(self, "_set_" + name, None) + if set_method: + #@ << set standard attribute >> + #@+node:<< set standard attribute >> + if type(value) == types.DictionaryType: + self.root.all_scenarios.update(value.keys()) + value = value.get(self.scenario, value["_default"]) + + self.__set_sources(name, value) + self._original_values[name] = value + set_method(_val(value)) + #@nonl + #@-node:<< set standard attribute >> + #@nl + else: + #@ << set userdefined attribute >> + #@+node:<< set userdefined attribute >> + if callable( getattr(self.__class__, name, None)): + raise NameError('You may not use "%s" as attribute' % name) + + setattr(self, name, value) + self._properties[name] = True + self.__set_sources(name, value) + #@nonl + #@-node:<< set userdefined attribute >> + #@nl + #@-node:_set_attrib + #@+node:read only attributes + #@+node:_set_name + def _set_name(self, value): + raise AttributeError("The attribute 'name' is readonly.") + #@nonl + #@-node:_set_name + #@+node:_set_done + def _set_done(self, value): + raise AttributeError("The attribute 'done' is readonly.") + #@nonl + #@-node:_set_done + #@+node:_set_performed_work_time + def _set_performed_work_time(self, value): + raise AttributeError("The attribute 'performed_work_time' is readonly.") + #@nonl + #@-node:_set_performed_work_time + #@+node:_set_booked_resource + def _set_booked_resource(self, value): + raise AttributeError("The attribute 'booked_resource' is readonly.") + #@nonl + #@-node:_set_booked_resource + #@+node:_set_performed_effort + def _set_performed_effort(self, value): + raise AttributeError("The attribute 'performed_effort' is readonly.") + #@nonl + #@-node:_set_performed_effort + #@+node:_set_children + def _set_children(self, value): + raise AttributeError("The attribute 'children' is readonly.") + #@nonl + #@-node:_set_children + #@+node:_set_depth + def _set_depth(self, value): + raise AttributeError("The attribute 'depth' is readonly.") + #@nonl + #@-node:_set_depth + #@+node:_set_index + def _set_index(self, value): + raise AttributeError("The attribute 'index' is readonly.") + #@nonl + #@-node:_set_index + #@+node:_set_scenario + def _set_scenario(self, value): + raise AttributeError("The attribute 'scenario' is readonly.") + #@nonl + #@-node:_set_scenario + #@+node:_set_buffer + def _set_buffer(self, value): + raise AttributeError("The attribute 'buffer' is readonly.") + #@nonl + #@-node:_set_buffer + #@-node:read only attributes + #@+node:_set_start + def _set_start(self, value): + self.__start_class = value.__class__ + self.start = self._to_start(value).round() + #@-node:_set_start + #@+node:_set_end + def _set_end(self, value): + self.end = self._to_end(value) + #@-node:_set_end + #@+node:_set_max_load + def _set_max_load(self, max_load): + self.max_load = float(max_load) + #@-node:_set_max_load + #@+node:_set_load + def _set_load(self, load): + self.load = float(load) + #@-node:_set_load + #@+node:_set_length + def _set_length(self, value): + self.length = self._to_delta(value).round() + #@-node:_set_length + #@+node:_set_effort + def _set_effort(self, value): + self.effort = self._to_delta(value).round() + #@-node:_set_effort + #@+node:_set_duration + def _set_duration(self, value): + self.duration = self._to_delta(value, True).round() + #@-node:_set_duration + #@+node:_set_complete + def _set_complete(self, value): + self.complete = value + #@-node:_set_complete + #@+node:_set_done + def _set_done(self, value): + self.done = self._to_delta(value).round() + #@-node:_set_done + #@+node:_set_todo + def _set_todo(self, value): + self.todo = self._to_delta(value).round() + #@-node:_set_todo + #@+node:_set_milestone + def _set_milestone(self, value): + self.milestone = value + #@-node:_set_milestone + #@+node:_set_resource + def _set_resource(self, value): + if not value: + self.resource = None + return + + if isinstance(value, (tuple, list)): + value = reduce(lambda a, b: a & b, value) + print 'Value:::',value + self.resource = value() + + #@-node:_set_resource + #@+node:_set_copy_src + def _set_copy_src(self, value): + if isinstance(value, _MeProxy): + raise RuntimeError("Cannot copy me.") + + if not value._is_compiled: + raise _IncompleteError(value, "copy_src") + + if value.resource and not self.resource: + self.resource = value.resource + + if value.balance and not self.balance: + self.balance = value.balance + + copy_parms = ("priority", "todo", "complete", + "_constraint", "load", "length", + "effort", "duration") + + for p in copy_parms: + v = value.__dict__.get(p) + if v: setattr(self, p, v) + + self.copy_src = value + self._properties.update(value._properties) + for k in value._properties.iterkeys(): + setattr(self, k, getattr(value, k)) + #@-node:_set_copy_src + #@+node:__set_sources + def __set_sources(self, attrib_name, value): + #@ << find references >> + #@+node:<< find references >> + def make_ref(val): + if isinstance(val, _ValueWrapper): + return val._ref + + if isinstance(val, Task): + return [(val, "")] + + return [] + + if isinstance(value, (list, tuple)): + sources = _refsum(map(make_ref, value)) + else: + sources = make_ref(value) + #@nonl + #@-node:<< find references >> + #@nl + if not sources: return + + #track only dependcies within the same project + root = self.root + sources = [ task.path + "." + attrib + for task, attrib in sources + if task and task.root is root ] + self._sources[attrib_name] = tuple(sources) + attr_path = self.path + "." + attrib_name + + #set dependencies of my sources + for d in sources: + path, attrib = _split_path(d) + task = self.get_task(path) + r_d = task._dependencies + d_l = r_d.setdefault(attrib, {}) + d_l[attr_path] = True + #@-node:__set_sources + #@+node:Calendar Setters + #@+node:_set_calendar + def _set_calendar(self, value): + self.calendar = value + self._to_delta = value.Minutes + self._to_start = value.StartDate + self._to_end = value.EndDate + self.__renew_dates() + + #@-node:_set_calendar + #@+node:__renew_dates + def __renew_dates(self): + for attrib in ("effort", "start", "end", "length", "todo"): + try: + + self._set_attrib(attrib, self._original_values[attrib]) + except KeyError: + pass + + #@-node:__renew_dates + #@+node:__make_calendar + def __make_calendar(self): + if not "calendar" in self.__dict__: + cal = self.calendar = self.calendar.clone() + self._to_delta = cal.Minutes + self._to_start = cal.StartDate + self._to_end = cal.EndDate + #@nonl + #@-node:__make_calendar + #@+node:_set_vacation + def _set_vacation(self, value): + self.__make_calendar() + self.calendar.set_vacation(value) + self._properties["vacation"] = True + self.vacation = value + self.__renew_dates() + #@-node:_set_vacation + #@+node:_set_extra_work + def _set_extra_work(self, value): + self.__make_calendar() + self.calendar.set_extra_work(value) + self._properties["extra_work"] = True + self.extra_work = value + self.__renew_dates() + #@-node:_set_extra_work + #@+node:_set_working_days + def _set_working_days(self, value): + + if type(value[0]) is str: + value = (value, ) + + self.working_days = value + self._properties["working_days"] = True + self.__make_calendar() + + for v in value: + day_range = v[0] + tranges = tuple(v[1:]) + self.calendar.set_working_days(day_range, *tranges) + + self.__renew_dates() + #@nonl + #@-node:_set_working_days + #@+node:_set_minimum_time_unit + def _set_minimum_time_unit(self, value): + self.__make_calendar() + self.calendar.minimum_time_unit = value + self._properties["minimum_time_unit"] = True + #@-node:_set_minimum_time_unit + #@+node:_get_minimum_time_unit + def _get_minimum_time_unit(self): + return self.calendar.minimum_time_unit + + minimum_time_unit = property(_get_minimum_time_unit) + #@-node:_get_minimum_time_unit + #@+node:_set_working_days_per_week + def _set_working_days_per_week(self, value): + + self.__make_calendar() + self.calendar.working_days_per_week = value + self._properties["working_days_per_week"] = True + #@-node:_set_working_days_per_week + #@+node:_get_working_days_per_week + def _get_working_days_per_week(self): + return self.calendar.working_days_per_week + + working_days_per_week = property(_get_working_days_per_week) + #@-node:_get_working_days_per_week + #@+node:_set_working_days_per_month + def _set_working_days_per_month(self, value): + self.__make_calendar() + self.calendar.working_days_per_month = value + self._properties["working_days_per_month"] = True + #@-node:_set_working_days_per_month + #@+node:_get_working_days_per_month + def _get_working_days_per_month(self): + return self.calendar.working_days_per_month + + working_days_per_month = property(_get_working_days_per_month) + #@-node:_get_working_days_per_month + #@+node:_set_working_days_per_year + def _set_working_days_per_year(self, value): + self.__make_calendar() + self.calendar.working_days_per_year = value + self._properties["working_days_per_year"] = True + #@-node:_set_working_days_per_year + #@+node:_get_working_days_per_year + def _get_working_days_per_year(self): + return self.calendar.working_days_per_year + + working_days_per_year = property(_get_working_days_per_year) + #@-node:_get_working_days_per_year + #@+node:_set_working_hours_per_day + def _set_working_hours_per_day(self, value): + self.__make_calendar() + self.calendar.working_hours_per_day = value + self._properties["set_working_hours_per_day"] = True + #@-node:_set_working_hours_per_day + #@+node:_get_working_hours_per_day + def _get_working_hours_per_day(self): + return self.calendar.working_hours_per_day + + working_hours_per_day = property(_get_working_hours_per_day) + #@-node:_get_working_hours_per_day + #@+node:_set_now + def _set_now(self, value): + proxy = weakref.proxy + self.calendar.now = _to_datetime(value) + #@-node:_set_now + #@-node:Calendar Setters + #@-node:Setting methods + #@+node:Freezer Methods + #@+node:_unfreeze + def _unfreeze(self, attrib_name): + if self.__dict__.has_key(attrib_name): + del self.__dict__[attrib_name] + #@-node:_unfreeze + #@+node:_wrap_attrib + def _wrap_attrib(self, method): + attrib_name = method.__name__[7:] + recursion_attrib = "_rec" + attrib_name + + try: + dest, dattr = self.__at_compile + raise RecursionError("Recursive definition of %s(%s) and %s(%s)" \ + % (self.path, attrib_name, dest.path, dattr)) + except AttributeError: pass + + if not self._is_compiled: + raise _IncompleteError(self, attrib_name) + + try: + getattr(self, recursion_attrib) + raise RecursionError(self, attrib_name) + except AttributeError: pass + + setattr(self, recursion_attrib, True) + + try: + result = method(self) + + if self._is_frozen: + setattr(self, attrib_name, result) + + return result + finally: + delattr(self, recursion_attrib) + #@-node:_wrap_attrib + #@+node:_find_frozen + def _find_frozen(self, attrib_name, default=None): + value = self.__dict__.get(attrib_name) + if value is not None: + return value + + up = self.up + return up and up._find_frozen(attrib_name) or default + #@-node:_find_frozen + #@-node:Freezer Methods + #@+node:Calculation Methods + #@+node:__calc_performed_effort + def __calc_performed_effort(self): + if self.children: + return self._to_delta(sum([ t.performed_effort for t in self.children ])) + + return pcalendar.Minutes(0) + + performed_effort = _TaskProperty(__calc_performed_effort) + #@-node:__calc_performed_effort + #@+node:__calc_estimated_effort + def __calc_estimated_effort(self): + if self.children: + return self._to_delta(sum([ t.estimated_effort for t in self.children ])) + + return self.effort + + estimated_effort = _TaskProperty(__calc_estimated_effort) + #@-node:__calc_estimated_effort + #@+node:__calc_start + def __calc_start(self): + to_start = self._to_start + + if self.children: + try: + return min([ to_start(t.start) for t in self.children + if not t._is_parent_referer ]) + except ValueError: + #@ << raise child recursion error >> + #@+node:<< raise child recursion error >> + self._raise(RecursionError("A child defines a "\ + "recursive definition at %s" % self.path)) + #@-node:<< raise child recursion error >> + #@nl + + try: + end = self.end + duration = self.__dict__.get("duration") + if duration is not None: + start = end.to_datetime() - datetime.timedelta(minutes=duration) + else: + start = end - self.length + + return to_start(start) + + except RecursionError: + start = self._find_frozen("start") + if start: return to_start(start) + #@ << raise recursion error >> + #@+node:<< raise recursion error >> + raise RecursionError("you have to specify a "\ + "start or an end at %s" % self.path) + #@nonl + #@-node:<< raise recursion error >> + #@nl + + start = _TaskProperty(__calc_start) + + #@-node:__calc_start + #@+node:__calc_end + def __calc_end(self): + to_end = self._to_end + + if self.children: + try: + return max([ to_end(t.end) for t in self.children + if not t._is_parent_referer ]) + except ValueError: + #@ << raise child recursion error >> + #@+node:<< raise child recursion error >> + self._raise(RecursionError("A child defines a "\ + "recursive definition at %s" % self.path)) + #@-node:<< raise child recursion error >> + #@nl + + try: + start = self.start + duration = self.__dict__.get("duration") + if duration is not None: + end = start.to_datetime() + datetime.timedelta(minutes=duration) + else: + end = start + self.length + + return to_end(end) + + except RecursionError: + end = self._find_frozen("end") + if end: return to_end(end) + #@ << raise recursion error >> + #@+node:<< raise recursion error >> + raise RecursionError("you have to specify a "\ + "start or an end at %s" % self.path) + #@nonl + #@-node:<< raise recursion error >> + #@nl + + + end = _TaskProperty(__calc_end) + #@-node:__calc_end + #@+node:__calc_load + def __calc_load(self): + length = self.__dict__.get("length") + effort = self.__dict__.get("effort") + + if length is not None and effort is not None: + return float(effort) / (float(length) or 1.0) + + load = self._find_frozen("load") + if load is not None: return load + return 1.0 + + load = _TaskProperty(__calc_load) + #@-node:__calc_load + #@+node:__calc_length + def __calc_length(self): + effort = self.__dict__.get("effort") + if effort is None: + return self.end - self.start + + return self._to_delta(effort / self.load) + + length = _RoundingTaskProperty(__calc_length, "length") + #@-node:__calc_length + #@+node:__calc_duration + def __calc_duration(self): + return self._to_delta(self.end.to_datetime()\ + - self.start.to_datetime(), True) + + duration = _TaskProperty(__calc_duration) + #@-node:__calc_duration + #@+node:__calc_effort + def __calc_effort(self): + if self.children: + return self._to_delta(sum([ t.effort for t in self.children ])) + + return self._to_delta(self.length * self.load) + + effort = _RoundingTaskProperty(__calc_effort, "effort") + #@-node:__calc_effort + #@+node:__calc_done + def __calc_done(self): + if self.children: + dones = map(lambda t: t.done, self.children) + return self._to_delta(sum(dones)) + + res = self._iter_booked_resources() + done = sum(map(lambda r: r.done_of(self), res)) + + complete = self.__dict__.get("complete") + todo = self.__dict__.get("todo") + + if not done and complete == 100 or todo == 0: + #if now is not set + done = self.effort + + return self._to_delta(done) + + done = _TaskProperty(__calc_done) + #@-node:__calc_done + #@+node:__calc_buffer + def __calc_buffer(self): + if self.children: + return self._to_delta(min(map(lambda t: t.buffer, self.children))) + + scenario = self.scenario + end = self.end + old_end = self.__dict__.get("end") + + #@ << find all tasks, that depend on my end >> + #@+node:<< find all tasks, that depend on my end >> + deps = { } + task = self + while task: + deps.update(task._dependencies.get("end", {})) + task = task.up + #@nonl + #@-node:<< find all tasks, that depend on my end >> + #@nl + + #@ << define unfreeze_parents >> + #@+node:<< define unfreeze_parents >> + def unfreeze_parents(): + task = self.up + while task: + task._unfreeze("end") + task = task.up + #@nonl + #@-node:<< define unfreeze_parents >> + #@nl + + buffers = [ ] + for d in deps.keys(): + path, attrib = _split_path(d) + if attrib != "start": + continue + + #@ << calculate buffer to descendant 'd' >> + #@+node:<< calculate buffer to descendant 'd' >> + unfreeze_parents() + + # the following code considers a expressione like + # start = predecessor.end + Delta("1d") the buffer + # calculation must be aware of the 1d delay. + # (therefore a simple succ_start - end would be + # incorrect) + # Solution: Simluate a later end and calculate the + # real delay + + succ_task = self.get_task(path) + simulated_task = Task(succ_task._function, + succ_task.name, + succ_task.up, 1) + + current_start = succ_task.start + simulated_end = current_start + self.end = current_start + + simulated_task._generate() + simulated_start = simulated_task.start + + unfreeze_parents() + if old_end: self.end = old_end + else: self._unfreeze("end") + del simulated_task + + current_delay = current_start - end + simulated_delay = simulated_start - simulated_end + real_delay = current_delay - simulated_delay + try: + buffer_ = real_delay + succ_task.buffer + except RecursionError, err: + self._raise(err) + #@nonl + #@-node:<< calculate buffer to descendant 'd' >> + #@nl + + buffers.append(buffer_) + if not buffer_: + break + + if buffers: + return self._to_delta(min(buffers)) + + return not self.milestone \ + and self.root.end - end \ + or self._to_delta(0) + + buffer = _TaskProperty(__calc_buffer) + #@-node:__calc_buffer + #@+node:__calc_complete + def __calc_complete(self): + done = self.done + todo = self.todo + return int(100.0 * done / ((done + todo) or 1)) + + complete = _TaskProperty(__calc_complete) + #@-node:__calc_complete + #@+node:__calc_todo + def __calc_todo(self): + complete = self.__dict__.get("complete") + if complete: + # effort = done + todo + # done done + # complete = ------ ==> todo = -------- - done + # effort complete + complete = float(complete) + done = self.done + if done: + done = float(done) + return self._to_delta(done * 100.0 / complete - done) + return self._to_delta(self.effort * complete / 100.0) + + if self.children: + todos = map(lambda t: t.todo, self.children) + return self._to_delta(sum(todos)) + + todo = sum(map(lambda r: r.todo_of(self), self.booked_resource)) + return self._to_delta(max(todo, self.effort - self.done)) + + todo = _TaskProperty(__calc_todo) + #@-node:__calc_todo + #@-node:Calculation Methods + #@+node:Check Methods + #@+node:__check_task + def __check_task(self): + if self.children: return + + start = self._find_frozen("start") + end = self._find_frozen("end") + + if not (start or end): + self._raise(ValueError("You must specify either a"\ + " start or an end attribute")) + + if start and end: return + + length = self.__dict__.get("length") + duration = self.__dict__.get("duration") + effort = self.__dict__.get("effort") + if not (effort or length or duration): + #set a default value + self._set_effort("1d") + #self._raise(ValueError("You must specify either a"\ + # " length or a duration or "\ + # "an effort attribute")) + #@-node:__check_task + #@+node:__check_milestone + def __check_milestone(self): + if not self.milestone: return + + self.length = self._to_delta(0) + start = self.__dict__.get("start") + if not start: + self._raise(ValueError("Milestone must have start attribute"), + "milstone") + + if self.__start_class.__name__ == "edt": + #the milestone is probably dependent on the end date of + #an other task (see edt in pcalendar) ==> start at the end date + self.start = self.end = self._to_end(self.start) + else: + self.start = self.end = self._to_start(self.start) + + #@-node:__check_milestone + #@+node:_check_completion + def _check_completion(self): + if not self.performed_effort: return + if self.root.is_snapshot: return + + # allocation is not done yet ==> self.todo, self.done, + # self.complete cannot be calculated + if self._find_frozen("complete", 0) < 100 \ + and self.__dict__.get("todo", 1) > 0: + return + + start = self.performed_start + end = self.performed_end + #ensure that self.start.to_datetime() < self.end.to_datetime() + cstart = self._to_start(start) + if cstart.to_datetime() > start: cstart = self._to_end(start) + + cend = self._to_end(end) + if cend.to_datetime() < end: cend = self._to_start(end) + + self.start = cstart + self.end = cend + + if self.performed_effort != self.effort: + self.estimated_effort = self.effort + self.effort = self.performed_effort + #@-node:_check_completion + #@+node:check + def check(self): + if self._constraint and self._is_compiled: + globals_ = self._function.func_globals + globals_["me"] = self + globals_["up"] = self.up + globals_["root"] = self.root + globals_["assert_"] = self.__assert + self._constraint() + #@-node:check + #@-node:Check Methods + #@+node:Error Methods + #@+node:__assert + def __assert(self, value): + if not value: + warnings.warn('assertion in scenario: "%s"' % self.scenario, + RuntimeWarning, 2) + #@-node:__assert + #@+node:_warn + def _warn(self, message, attrib=None, level=2): + self.__compile_function([], True, _MeProxyWarn(self, attrib, message)) + #@-node:_warn + #@+node:_raise + def _raise(self, exc, attrib=None): + self.__compile_function([], True, _MeProxyError(self, attrib, exc)) + raise exc + #@-node:_raise + #@-node:Error Methods + #@-others +#@nonl +#@-node:class Task +#@-node:Task +#@+node:Projects +#@+node:class _ProjectBase +class _ProjectBase(Task): + """ + Base class for all projects. + """ + #@ << class _ProjectBase declarations >> + #@+node:<< class _ProjectBase declarations >> + __attrib_completions__ = { } + __attrib_completions__.update(Task.__attrib_completions__) + del __attrib_completions__["milestone"] #project cannot be milestones + + priority = 500 + efficiency = 1.0 + max_load = 1.0 + balance = 0 + resource = None + copy_src = None + has_actual_data = False + is_snapshot = False + + #@-node:<< class _ProjectBase declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, top_task, scenario="_default", id=""): + self.calendar = pcalendar.Calendar() + Task.__init__(self, top_task, top_task.func_name) + self.id = id or self.name + self.scenario = scenario + self.all_scenarios = set(("_default",)) + self.path = "root" + self._globals = top_task.func_globals.copy() + self._generate() + + #@-node:__init__ + #@+node:_idendity_ + def _idendity_(self): return self.id + #@-node:_idendity_ + #@+node:_restore_globals + def _restore_globals(self): + self._function.func_globals.clear() + self._function.func_globals.update(self._globals) + del self._globals + #@-node:_restore_globals + #@+node:free + def free(self): + all_resources = self.all_resources() + for r in all_resources: + r().unbook_tasks_of_project(self.id, self.scenario) + + for t in self: + t.booked_resource = () + + return all_resources + #@-node:free + #@+node:_get_balancing_list + def _get_balancing_list(self): + + try: + cached_list = balancing_cache[self._function.org_code] + if len(cached_list) != len(tuple(self)): + # different scenarios can have different tasks + raise KeyError() + + except KeyError: + cached_list = _build_balancing_list(self) + balancing_cache[self._function.org_code] = cached_list + else: + cached_list = [ self.get_task(t.path) for t in cached_list ] + + return cached_list + #@-node:_get_balancing_list + #@+node:snapshot + def snapshot(self, indent="", name=None): + text = Task.snapshot(self, indent, name) + + lines = text.splitlines(True) + indent += " " + + def make_resource(r): + return '%sclass %s(Resource): title = "%s"\n' \ + % (indent, r.name, r.title) + + now = datetime.datetime.now().strftime("%x %H:%M") + resource_text = map(lambda r: make_resource(r), self.all_resources()) + lines.insert(1, "%sfrom faces import Resource\n" % indent) + lines.insert(2, "".join(resource_text) + "\n") + lines.insert(3, '%snow = "%s"\n' % (indent, now)) + lines.insert(4, '%sis_snapshot = True\n' % indent) + return "".join(lines) + #@-node:snapshot + #@-others +#@-node:class _ProjectBase +#@+node:class Project +class Project(_ProjectBase): + """ + Generates a Project without allocating resources. + + @param top_task: Specifies the highest function of a project definiton. + + @param scenario: Specifies the name of the scenario which should be scheduled. + + @param id: Specifiess a unique idenfication name to distinguish the project from + other projects in the resource database. The default value for id + is the name of top_task. + """ + #@ << class Project declarations >> + #@+node:<< class Project declarations >> + __call_completion__ = 'Project(|top_task, scenario="_default", id=None)' + + #@-node:<< class Project declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, top_task, scenario="_default", id=None): + _ProjectBase.__init__(self, top_task, scenario, id) + no_snapshot = not self.is_snapshot + for t in self: + t._is_frozen = True + t._recalc_properties() + no_snapshot and t.check() + + self._restore_globals() + + #@-node:__init__ + #@-others + +#@-node:class Project +#@+node:class _AllocationPoject +class _AllocationPoject(_ProjectBase): + #@ @+others + #@+node:unfreeze_parents + def unfreeze_parents(self): + if self.has_actual_data: + for t in filter(lambda t: t.children, self): + if not t._original_values.has_key("start"): t._unfreeze("start") + if not t._original_values.has_key("end"): t._unfreeze("end") + #@-node:unfreeze_parents + #@-others +#@-node:class _AllocationPoject +#@+node:class BalancedProject +class BalancedProject(_AllocationPoject): + """ + Generates a project with allocated resources. The tasks are balanced + to fit the resources load conditions. + """ + #@ << class BalancedProject declarations >> + #@+node:<< class BalancedProject declarations >> + __call_completion__ = """BalancedProject(|top_task, scenario="_default", + id=None, balance=SMART, performed=None)""" + + #@-node:<< class BalancedProject declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, top_task, scenario="_default", + id=None, balance=SMART, performed=None): + _AllocationPoject.__init__(self, top_task, scenario, id) + self.balance = balance + if performed: + self._distribute_performed(performed) + self.has_actual_data = True + + no_snapshot = not self.is_snapshot + if no_snapshot: + self.allocate() + else: + self.allocate_snapshot() + + for t in self: + t._is_frozen = True + t._recalc_properties() + no_snapshot and t.check() + + self._restore_globals() + #@nonl + #@-node:__init__ + #@+node:allocate_snapshot + def allocate_snapshot(self): + all_resources = self.free() + scenario = self.scenario + has_actual_data = True + for t in self: + if not t.resource or t.milestone or t.children: + continue + + t._convert_performed(all_resources) + t._allocate_performed(t._performed) + #@-node:allocate_snapshot + #@+node:allocate + def allocate(self): + all_resources = self.free() + balancing_list = self._get_balancing_list() + scenario = self.scenario + + #for t in balancing_list: + # print t.path + + for t in balancing_list: + t._compile([], True) + + if not t.resource or t.milestone or t.children: + continue + + if t._convert_performed(all_resources): + has_actual_data = True + + try: + t._allocate_performed(t._performed) + except AttributeError: + pass + + allocator = _allocators[t.balance] + min_val = None + min_state = None + for p in range(t.resource._permutation_count()): + state = t._test_allocation(p, allocator) + + if not state: continue + + to_minimize = state[0] + if not min_val or min_val > to_minimize: + min_val = to_minimize + min_state = state + + if min_state: + t._allocate(min_state, allocator) + elif t.performed_start: + # t could not be allocated ==> + # performance data holds all information + t.start = t._to_start(t.performed_start) + t.end = t._to_end(t.performed_end) + + self.unfreeze_parents() + #@-node:allocate + #@+node:_distribute_performed + def _distribute_performed(self, performed): + project_id = self._idendity_() + plen = len(project_id) + + performed = filter(lambda item: item[0].startswith(project_id), + performed) + performed.sort() + + task = None + for item in performed: + path = item[0] + rpath = "root" + path[plen:] + task = self.get_task(rpath) + + if not task: + #@ << extract task in activity path >> + #@+node:<< extract task in activity path >> + #@+at + # A performed path can have sub activities appended to the + # task path. + # like: + # + # root.parent1.parent2.task.subactivity + # + # here rhe correct task path is: + # + # root.parent1.parent2.task + # + #@-at + #@@code + orpath = rpath + while not task: + #path can specify a sub module + #find the correct path to the module + try: + last_dot = rpath.rindex(".", 0, len(rpath)) + except ValueError: + break + + rpath = rpath[:last_dot] + task = self.get_task(rpath) + + item = list(item) + item.append(orpath[len(rpath):]) + #@nonl + #@-node:<< extract task in activity path >> + #@nl + + if not task or task.children: + self._warn("The performance data contain " + "a task with id '%s'. But such " + "a task does not exist in your " + "project." % path) + continue + + if not isinstance(task.performed, list): + task.performed = list(task.performed) + + task.performed.append(item[1:]) + #@nonl + #@-node:_distribute_performed + #@-others +#@-node:class BalancedProject +#@+node:class AdjustedProject +class AdjustedProject(_AllocationPoject): + """ + Generates a project with allocated resources. The tasks are + adjusted to the actual tracking data and balanced to fit the + resources load conditions. + """ + #@ << class AdjustedProject declarations >> + #@+node:<< class AdjustedProject declarations >> + __call_completion__ = 'AdjustedProject(|base_project)' + + #@-node:<< class AdjustedProject declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, base_project): + _AllocationPoject.__init__(self, base_project._function, + base_project.scenario, + base_project.id) + + self.balance = base_project.balance + self.has_actual_data = base_project.has_actual_data + self.allocate(base_project) + for t in self: + t._is_frozen = True + t._recalc_properties() + t.check() + + self._restore_globals() + + + #@-node:__init__ + #@+node:allocate + def allocate(self, base): + balancing_list = self._get_balancing_list() + scenario = self.scenario + cal = self.calendar + now = cal.now + + #for t in balancing_list: + # print t.path + + #@ << free the resources, we have to rebook >> + #@+node:<< free the resources, we have to rebook >> + for t in balancing_list: + src = base.get_task(t.path) + if src.end > now or src.complete < 100: + for r in src._iter_booked_resources(): + r.unbook_task(src) + #@nonl + #@-node:<< free the resources, we have to rebook >> + #@nl + + for t in balancing_list: + src = base.get_task(t.path) + + if src.end <= now and src.complete == 100: + #@ << copy the attribs of complete tasks >> + #@+node:<< copy the attribs of complete tasks >> + t.effort = src.effort + t.load = src.load + t.start = src.start + t.end = src.end + t.done = src.done + t.todo = src.todo + t.booked_resource = src.booked_resource + t.performed_resource = src.performed_resource + t._unfreeze("length") + t._unfreeze("duration") + #@nonl + #@-node:<< copy the attribs of complete tasks >> + #@nl + continue + + t._compile([], True) + if not t.resource or t.milestone or t.children: + continue + + # now allocate the uncomplete tasks + #@ << allocate performed data >> + #@+node:<< allocate performed data >> + try: + t._performed = src._performed + t._allocate_performed(t._performed) + except AttributeError: + pass + #@nonl + #@-node:<< allocate performed data >> + #@nl + allocator = _allocators[t.balance] + + if src.start >= now: + #@ << allocate tasks, that have not begun yet >> + #@+node:<< allocate tasks, that have not begun yet >> + min_val = None + min_state = None + for p in range(t.resource._permutation_count()): + state = t._test_allocation(p, allocator) + if not state: continue + + to_minimize = state[0] + if not min_val or min_val > to_minimize: + min_val = to_minimize + min_state = state + + if min_state: + t._allocate(min_state, allocator) + elif t.performed_start: + t.start = t._to_start(t.performed_start) + t.end = t._to_end(t.performed_end) + #@-node:<< allocate tasks, that have not begun yet >> + #@nl + else: + #@ << allocate tasks, that are allready at work >> + #@+node:<< allocate tasks, that are allready at work >> + if t.__dict__.has_key("effort"): + t.effort = t._to_delta(src.done + src.todo).round() + + resource = src.booked_resource or src.performed_resource + state = allocator.test_allocation(t, resource) + if state: + t._allocate(state, allocator) + #@nonl + #@-node:<< allocate tasks, that are allready at work >> + #@nl + + self.unfreeze_parents() + #@nonl + #@-node:allocate + #@-others +#@-node:class AdjustedProject +#@-node:Projects +#@-others + + +""" + Atttribute mit Bedeutung: + + calendar + -------- + minimum_time_unit |int in minutes| + working_days_per_week |int in days | + working_days_per_month|int in days | + working_days_per_year |int in days | + working_hours_per_day |int in hours | + vacation | [ one_day, (from, to), .. ] | + working_days + now + + + + Task + ----- + load + start + end + length + effort + duration + resource + booked_resource + + milestone + complete + done + todo + priority + efficiency + buffer + + children + depth + index + path + dont_inherit + + performed_effort + performed_end + performed_start + + sum() + min() + max() + costs() + indent_name() + max_load + + copy_src (set: copy all attributes of another task + get: reference of copy) + + balance + + for gantt + ----- + line + accumulate + + + + + Resource + ---------- + efficiency + load + vacation + max_load + +""" +#@-node:@file task.py +#@-leo diff --git a/addons/resource/faces_new/timescale.py b/addons/resource/faces_new/timescale.py new file mode 100644 index 00000000000..215766eb958 --- /dev/null +++ b/addons/resource/faces_new/timescale.py @@ -0,0 +1,113 @@ +############################################################################ +# 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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################ + +import faces.pcalendar as pcal +import matplotlib.cbook as cbook +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(cbook.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) diff --git a/addons/resource/faces_new/utils.py b/addons/resource/faces_new/utils.py new file mode 100644 index 00000000000..f7d61fb1dc9 --- /dev/null +++ b/addons/resource/faces_new/utils.py @@ -0,0 +1,124 @@ +############################################################################ +# 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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 + + + From 401b6c536d89e72d64437c4f5be8d10f88fbb07b Mon Sep 17 00:00:00 2001 From: "Harry (Open ERP)" Date: Fri, 29 Jan 2010 11:36:44 +0530 Subject: [PATCH 03/30] [IMP] security : move login, access, check method into res.users object bzr revid: hmo@tinyerp.com-20100129060644-q846msahr5mtqvsr --- bin/addons/base/res/res_user.py | 53 +++++++++++++++++++++++++++++++++ bin/service/security.py | 48 ++++++----------------------- 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/bin/addons/base/res/res_user.py b/bin/addons/base/res/res_user.py index 7b9b4d113bc..c85f2544e3c 100644 --- a/bin/addons/base/res/res_user.py +++ b/bin/addons/base/res/res_user.py @@ -23,7 +23,9 @@ from osv import fields,osv from osv.orm import except_orm import tools import pytz +import pooler from tools.translate import _ +from service import security class groups(osv.osv): _name = "res.groups" @@ -119,6 +121,7 @@ def _companies_get(self,cr, uid, context={}): class users(osv.osv): __admin_ids = {} + _uid_cache = {} _name = "res.users" def get_current_company(self, cr, uid): @@ -260,6 +263,56 @@ class users(osv.osv): data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my') return dataobj.browse(cr, uid, data_id, context).res_id + + def login(self, db, login, password): + if not password: + return False + cr = pooler.get_db(db).cursor() + cr.execute('select id from res_users where login=%s and password=%s and active', (tools.ustr(login), tools.ustr(password))) + res = cr.fetchone() + cr.close() + if res: + return res[0] + else: + return False + + def check_super(self, passwd): + if passwd == tools.config['admin_passwd']: + return True + else: + raise security.ExceptionNoTb('AccessDenied') + + def check(self, db, uid, passwd): + if not passwd: + return False + cached_pass = self._uid_cache.get(db, {}).get(uid) + if (cached_pass is not None) and cached_pass == passwd: + return True + cr = pooler.get_db(db).cursor() + cr.execute('select count(1) from res_users where id=%s and password=%s and active=%s', (int(uid), passwd, True)) + res = cr.fetchone()[0] + cr.close() + if not bool(res): + raise security.ExceptionNoTb('AccessDenied') + if res: + if self._uid_cache.has_key(db): + ulist = self._uid_cache[db] + ulist[uid] = passwd + else: + self._uid_cache[db] = {uid:passwd} + return bool(res) + + def access(self, db, uid, passwd, sec_level, ids): + if not passwd: + return False + cr = pooler.get_db(db).cursor() + cr.execute('select id from res_users where id=%s and password=%s', (uid, passwd)) + res = cr.fetchone() + cr.close() + if not res: + raise security.ExceptionNoTb('Bad username or password') + return res[0] + _constraints = [ (_check_company, 'This user can not connect using this company !', ['company_id']), ] diff --git a/bin/service/security.py b/bin/service/security.py index ea87208c2dd..3ecbea73094 100644 --- a/bin/service/security.py +++ b/bin/service/security.py @@ -22,8 +22,6 @@ import pooler import tools -_uid_cache = {} - # When rejecting a password, we need to give as little info as possible class ExceptionNoTb(Exception): def __init__(self, msg ): @@ -32,16 +30,9 @@ class ExceptionNoTb(Exception): self.args = (msg, '') def login(db, login, password): - if not password: - return False - cr = pooler.get_db(db).cursor() - cr.execute('select id from res_users where login=%s and password=%s and active', (tools.ustr(login), tools.ustr(password))) - res = cr.fetchone() - cr.close() - if res: - return res[0] - else: - return False + pool = pooler.get_pool(db) + user_obj = pool.get('res.users') + return user_obj.login(db, login, password) def check_super(passwd): if passwd == tools.config['admin_passwd']: @@ -50,35 +41,14 @@ def check_super(passwd): raise ExceptionNoTb('AccessDenied') def check(db, uid, passwd): - if not passwd: - return False - cached_pass = _uid_cache.get(db, {}).get(uid) - if (cached_pass is not None) and cached_pass == passwd: - return True - cr = pooler.get_db(db).cursor() - cr.execute('select count(1) from res_users where id=%s and password=%s and active=%s', (int(uid), passwd, True)) - res = cr.fetchone()[0] - cr.close() - if not bool(res): - raise ExceptionNoTb('AccessDenied') - if res: - if _uid_cache.has_key(db): - ulist = _uid_cache[db] - ulist[uid] = passwd - else: - _uid_cache[db] = {uid:passwd} - return bool(res) + pool = pooler.get_pool(db) + user_obj = pool.get('res.users') + return user_obj.check(db, uid, passwd) def access(db, uid, passwd, sec_level, ids): - if not passwd: - return False - cr = pooler.get_db(db).cursor() - cr.execute('select id from res_users where id=%s and password=%s', (uid, passwd)) - res = cr.fetchone() - cr.close() - if not res: - raise ExceptionNoTb('Bad username or password') - return res[0] + pool = pooler.get_pool(db) + user_obj = pool.get('res.users') + return user_obj.access(db, uid, passwd, sec_level, ids) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 404bf5ca1ac193cae02fe0b0397ef20d283c3a5a Mon Sep 17 00:00:00 2001 From: "HDA (OpenERP)" Date: Fri, 29 Jan 2010 12:10:57 +0530 Subject: [PATCH 04/30] [FIX] module:hr_timesheet_sheet postgres view bzr revid: hda@tinyerp.com-20100129064057-jc3piy42kusbr716 --- addons/hr_timesheet_sheet/hr_timesheet_sheet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py index c63e19bd582..1cf2eba9660 100644 --- a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py @@ -463,10 +463,12 @@ class hr_attendance(osv.osv): FROM hr_timesheet_sheet_sheet s \ LEFT JOIN (hr_attendance a \ LEFT JOIN hr_employee e \ + LEFT JOIN resource_resource r \ + ON (e.resource_id = r.id) \ ON (a.employee_id = e.id)) \ ON (s.date_to >= date_trunc('day',a.name) \ AND s.date_from <= a.name \ - AND s.user_id = e.user_id) \ + AND s.user_id = r.user_id) \ WHERE a.id in (" + ",".join([str(x) for x in ids]) + ") \ GROUP BY a.id") res = dict(cursor.fetchall()) From 70fcb3da8d7dd881bc91a89adc9a9ed16cc79862 Mon Sep 17 00:00:00 2001 From: "Harry (Open ERP)" Date: Fri, 29 Jan 2010 12:22:34 +0530 Subject: [PATCH 05/30] [ADD] users_ldap: support authentication by ldap server bzr revid: hmo@tinyerp.com-20100129065234-058rl4od3j4s6red --- addons/users_ldap/__init__.py | 23 ++++ addons/users_ldap/__terp__.py | 40 ++++++ addons/users_ldap/users_ldap.py | 174 ++++++++++++++++++++++++++ addons/users_ldap/users_ldap_view.xml | 35 ++++++ 4 files changed, 272 insertions(+) create mode 100644 addons/users_ldap/__init__.py create mode 100644 addons/users_ldap/__terp__.py create mode 100644 addons/users_ldap/users_ldap.py create mode 100644 addons/users_ldap/users_ldap_view.xml diff --git a/addons/users_ldap/__init__.py b/addons/users_ldap/__init__.py new file mode 100644 index 00000000000..be78d7ff6c1 --- /dev/null +++ b/addons/users_ldap/__init__.py @@ -0,0 +1,23 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import users_ldap +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/users_ldap/__terp__.py b/addons/users_ldap/__terp__.py new file mode 100644 index 00000000000..ecdbf43d1b1 --- /dev/null +++ b/addons/users_ldap/__terp__.py @@ -0,0 +1,40 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + "name" : "Authenticate users with ldap server", + "version" : "0.1", + "depends" : ["base"], + "author" : "Tiny", + "description": """Add support for authentication by ldap server""", + "website" : "http://www.openerp.com", + "category" : "Generic Modules/Others", + "init_xml" : [ + ], + "demo_xml" : [ + ], + "update_xml" : [ + "users_ldap_view.xml", + ], + "active": False, + "installable": True +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/users_ldap/users_ldap.py b/addons/users_ldap/users_ldap.py new file mode 100644 index 00000000000..17919103fc3 --- /dev/null +++ b/addons/users_ldap/users_ldap.py @@ -0,0 +1,174 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from osv import fields,osv +from service import security +import pooler +import tools +from service import security +try: + import ldap + from ldap.filter import filter_format +except ImportError: + import netsvc + logger = netsvc.Logger() + logger.notifyChannel("init", netsvc.LOG_WARNING, "could not import ldap!") + + +class CompanyLDAP(osv.osv): + _name = 'res.company.ldap' + _order = 'sequence' + _rec_name = 'ldap_server' + _columns = { + 'sequence': fields.integer('Sequence'), + 'company': fields.many2one('res.company', 'Company', required=True, + ondelete='cascade'), + 'ldap_server': fields.char('LDAP Server address', size=64, required=True), + 'ldap_server_port': fields.integer('LDAP Server port', required=True), + 'ldap_binddn': fields.char('LDAP binddn', size=64, required=True), + 'ldap_password': fields.char('LDAP password', size=64, required=True), + 'ldap_filter': fields.char('LDAP filter', size=64, required=True), + 'ldap_base': fields.char('LDAP base', size=64, required=True), + 'user': fields.many2one('res.users', 'Model user', + help="Model used for user creation"), + 'create_user': fields.boolean('Create user', + help="Create the user if not in database"), + } + _defaults = { + 'ldap_server': lambda *a: '127.0.0.1', + 'ldap_server_port': lambda *a: 389, + 'sequence': lambda *a: 10, + 'create_user': lambda *a: True, + } + +CompanyLDAP() + + +class res_company(osv.osv): + _inherit = "res.company" + + _columns = { + 'ldaps': fields.one2many('res.company.ldap', 'company', 'LDAP Parameters'), + } +res_company() + +class users(osv.osv): + _inherit = "res.users" + def login(self, db, login, password): + ret = super(users,self).login(db, login, password) + if ret: + return ret + pool = pooler.get_pool(db) + cr = pooler.get_db(db).cursor() + action_obj = pool.get('ir.actions.actions') + cr.execute(""" + SELECT id, company, ldap_server, ldap_server_port, ldap_binddn, ldap_password, + ldap_filter, ldap_base, \"user\", create_user + FROM res_company_ldap + WHERE ldap_server != '' and ldap_binddn != '' ORDER BY sequence""") + for res_company_ldap in cr.dictfetchall(): + try: + l = ldap.open(res_company_ldap['ldap_server'], res_company_ldap['ldap_server_port']) + if l.simple_bind_s(res_company_ldap['ldap_binddn'], res_company_ldap['ldap_password']): + base = res_company_ldap['ldap_base'] + scope = ldap.SCOPE_SUBTREE + filter = filter_format(res_company_ldap['ldap_filter'], (login,)) + retrieve_attributes = None + result_id = l.search(base, scope, filter, retrieve_attributes) + timeout = 60 + result_type, result_data = l.result(result_id, timeout) + if not result_data: + continue + if result_type == ldap.RES_SEARCH_RESULT and len(result_data) == 1: + dn = result_data[0][0] + name = result_data[0][1]['cn'][0] + if l.bind_s(dn, passwd): + l.unbind() + cr.execute("SELECT id FROM res_users WHERE login=%s",(tools.ustr(login),)) + res = cr.fetchone() + if res: + cr.close() + return res[0] + if not res_company_ldap['create_user']: + continue + action_id = action_obj.search(cr, 1, [('usage', '=', 'menu')])[0] + if res_company_ldap['user']: + res = self.copy(cr, 1, res_company_ldap['user'], + default={'active': True}) + self.write(cr, 1, res, { + 'name': name, + 'login': login.encode('utf-8'), + 'company_id': res_company_ldap['company'], + }) + else: + res = self.create(cr, 1, { + 'name': name, + 'login': login.encode('utf-8'), + 'company_id': res_company_ldap['company'], + 'action_id': action_id, + 'menu_id': action_id, + }) + cr.commit() + cr.close() + return res + l.unbind() + except Exception, e: + continue + cr.close() + return False + + def check(self, db, uid, passwd): + try: + return super(users,self).check(db, uid, passwd) + except: # AccessDenied + pass + pool = pooler.get_pool(db) + cr = pooler.get_db(db).cursor() + user = self.browse(cr, 1, uid) + if user and user.company_id.ldaps: + for res_company_ldap in user.company_id.ldaps: + try: + l = ldap.open(res_company_ldap.ldap_server, res_company_ldap.ldap_server_port) + if l.simple_bind_s(res_company_ldap.ldap_binddn, + res_company_ldap.ldap_password): + base = res_company_ldap.ldap_base + scope = ldap.SCOPE_SUBTREE + filter = filter_format(res_company_ldap.ldap_filter, (user.login,)) + retrieve_attributes = None + result_id = l.search(base, scope, filter, retrieve_attributes) + timeout = 60 + result_type, result_data = l.result(result_id, timeout) + if result_data and result_type == ldap.RES_SEARCH_RESULT and len(result_data) == 1: + dn = result_data[0][0] + name = result_data[0][1]['cn'] + if l.bind_s(dn, passwd): + l.unbind() + self._uid_cache.setdefault(db, {})[uid] = passwd + cr.close() + return True + l.unbind() + except Exception, e: + pass + cr.close() + raise security.ExceptionNoTb('AccessDenied') + +users() +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/users_ldap/users_ldap_view.xml b/addons/users_ldap/users_ldap_view.xml new file mode 100644 index 00000000000..976743287f3 --- /dev/null +++ b/addons/users_ldap/users_ldap_view.xml @@ -0,0 +1,35 @@ + + + + + res.company.form.inherit.users_ldap + res.company + form + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+
+
+
From 557de246930f0b8eaf4c77c7003369f82dcb4314 Mon Sep 17 00:00:00 2001 From: "Rvo (Open ERP)" Date: Fri, 29 Jan 2010 13:18:00 +0530 Subject: [PATCH 06/30] [FIX] Renamed faces and removed print statements in it bzr revid: rvo@tinyerp.co.in-20100129074800-cjtob9irn861g2ff --- addons/resource/faces/__init__.py | 30 + addons/resource/faces/observer.py | 79 + addons/resource/faces/pcalendar.py | 960 +++++++ addons/resource/faces/plocale.py | 54 + addons/resource/faces/resource.py | 866 +++++++ addons/resource/faces/task.py | 3855 ++++++++++++++++++++++++++++ addons/resource/faces/timescale.py | 113 + addons/resource/faces/utils.py | 124 + 8 files changed, 6081 insertions(+) create mode 100644 addons/resource/faces/__init__.py create mode 100644 addons/resource/faces/observer.py create mode 100644 addons/resource/faces/pcalendar.py create mode 100644 addons/resource/faces/plocale.py create mode 100644 addons/resource/faces/resource.py create mode 100755 addons/resource/faces/task.py create mode 100644 addons/resource/faces/timescale.py create mode 100644 addons/resource/faces/utils.py diff --git a/addons/resource/faces/__init__.py b/addons/resource/faces/__init__.py new file mode 100644 index 00000000000..d1d52daf742 --- /dev/null +++ b/addons/resource/faces/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +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 + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/resource/faces/observer.py b/addons/resource/faces/observer.py new file mode 100644 index 00000000000..411b2c5395b --- /dev/null +++ b/addons/resource/faces/observer.py @@ -0,0 +1,79 @@ +#@+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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 diff --git a/addons/resource/faces/pcalendar.py b/addons/resource/faces/pcalendar.py new file mode 100644 index 00000000000..b894a2b66a7 --- /dev/null +++ b/addons/resource/faces/pcalendar.py @@ -0,0 +1,960 @@ +#@+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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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") + print "start", start.strftime(), type(start) + + delay = Minutes("4H") + print "delay", delay, delay.strftime() + + print "Start", cal.StartDate is StartDate + print "base", cal.StartDate.__bases__[0] == StartDate.__bases__[0] + print "type", type(start) + + print "convert start" + start2 = cal.StartDate(start) + print "convert end" + + start3 = cal.StartDate("10.1.2005") + print "start2", start2.strftime(), type(start2) +#@-node:@file pcalendar.py +#@-leo diff --git a/addons/resource/faces/plocale.py b/addons/resource/faces/plocale.py new file mode 100644 index 00000000000..3dcd35c7cae --- /dev/null +++ b/addons/resource/faces/plocale.py @@ -0,0 +1,54 @@ +############################################################################ +# 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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################ + +import gettext +import os.path +import locale + + +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() + diff --git a/addons/resource/faces/resource.py b/addons/resource/faces/resource.py new file mode 100644 index 00000000000..7cc2741fc8e --- /dev/null +++ b/addons/resource/faces/resource.py @@ -0,0 +1,866 @@ +#@+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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 "" % (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 "" % 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 "" % 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 diff --git a/addons/resource/faces/task.py b/addons/resource/faces/task.py new file mode 100755 index 00000000000..6fdb28db3f6 --- /dev/null +++ b/addons/resource/faces/task.py @@ -0,0 +1,3855 @@ +#@+leo-ver=4 +#@+node:@file task.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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################ + +#@-node:<< Copyright >> +#@nl +""" +This module contains all classes for project plan objects +""" +#@<< Imports >> +#@+node:<< Imports >> +import pcalendar +import resource +import types +import sys +import datetime +import operator as op +import warnings +import locale +import weakref +import opcode +import new +try: + set +except NameError: + from sets import Set as set +#@-node:<< Imports >> +#@nl + +_is_source = True + +STRICT = 3 +SLOPPY = 2 +SMART = 1 + +#@+others +#@+node:Exceptions +#@+node:class AttributeError +class AttributeError(AttributeError): + #@ << class AttributeError declarations >> + #@+node:<< class AttributeError declarations >> + is_frozen = False + + + #@-node:<< class AttributeError declarations >> + #@nl +#@-node:class AttributeError +#@+node:class RecursionError +class RecursionError(Exception): + """This exception is raised in cas of cirular dependencies + within an project""" + #@ << class RecursionError declarations >> + #@+node:<< class RecursionError declarations >> + pass + + + #@-node:<< class RecursionError declarations >> + #@nl +#@-node:class RecursionError +#@+node:class _IncompleteError +class _IncompleteError(Exception): + """This exception is raised, when there is not enough + data specified to calculate as task""" + #@ @+others + #@+node:__init__ + def __init__(self, *args): + if isinstance(args[0], (basestring)): + Exception.__init__(self, *args) + else: + Exception.__init__(self, + "not enough data for calculating task, "\ + "maybe you have a recursive reference", + *args) + #@-node:__init__ + #@-others +#@-node:class _IncompleteError +#@-node:Exceptions +#@+node:Proxies for self referencing +#@+node:class _MeProxy +class _MeProxy(object): + """ + A Proxy class for the me attribute of tasks in the compile case + """ + #@ << declarations >> + #@+node:<< declarations >> + __slots__ = "task" + + #@-node:<< declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, task): + object.__setattr__(self, "task", task) + #@-node:__init__ + #@+node:__getattr__ + def __getattr__(self, name): + if self.task._is_frozen: + return getattr(self.task, name) + + if name in ("name", "up", "root", "path", + "depth", "index", "calendar", + "children", "resource", "balance"): + return getattr(self.task, name) + + value = self.task.__dict__.get(name, _NEVER_USED_) + def make_val(default): + if value is _NEVER_USED_: return default + return value + + if name in ("start", "end"): + return self.task._to_start(make_val("1.1.2006")) + + if name in ("length", "effort", "duration", "todo", "done", + "buffer", "performed", "performed_effort", + "performed_end", "performed_start", + "performed_work_time" ): + return self.task._to_delta(make_val("0d")) + + if name in ("complete", "priority", "efficiency"): + return make_val(0) + + if value is _NEVER_USED_: + raise AttributeError("'%s' is not a valid attribute." % (name)) + + return value + #@-node:__getattr__ + #@+node:__setattr__ + def __setattr__(self, name, value): + self.task._set_attrib(name, value) + #@-node:__setattr__ + #@+node:__iter__ + def __iter__(self): + return iter(self.task) + #@nonl + #@-node:__iter__ + #@+node:add_attrib + def add_attrib(self, name_or_iter, val=None): + if not isinstance(name_or_iter, str): + for n, v in name_or_iter: + setattr(self, n, v) + else: + setattr(self, name_or_iter, val) + #@-node:add_attrib + #@-others +#@nonl +#@-node:class _MeProxy +#@+node:class _MeProxyRecalc +class _MeProxyRecalc(_MeProxy): + """ + A Proxy class for the me attribute of tasks in the recalc case + """ + #@ @+others + #@+node:__setattr__ + def __setattr__(self, name, value): + if self.task._properties.has_key(name): + self.task._set_attrib(name, value) + #@-node:__setattr__ + #@-others +#@-node:class _MeProxyRecalc +#@+node:class _MeProxyError +class _MeProxyError(_MeProxy): + #@ << declarations >> + #@+node:<< declarations >> + __slots__ = ("task", "attrib", "exc") + + #@-node:<< declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, task, attrib, exc): + _MeProxy.__init__(self, task) + object.__setattr__(self, "attrib", attrib) + object.__setattr__(self, "exc", exc) + #@-node:__init__ + #@+node:__setattr__ + def __setattr__(self, name, value): + if name == self.attrib or not self.attrib: + raise self.exc + #@-node:__setattr__ + #@-others +#@-node:class _MeProxyError +#@+node:class _MeProxyWarn +class _MeProxyWarn(_MeProxy): + #@ << declarations >> + #@+node:<< declarations >> + __slots__ = ("task", "attrib", "message") + + #@-node:<< declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, task, attrib, message): + _MeProxy.__init__(self, task) + object.__setattr__(self, "attrib", attrib) + object.__setattr__(self, "message", message) + #@-node:__init__ + #@+node:__setattr__ + def __setattr__(self, name, value): + if name == self.attrib or not self.attrib: + warnings.warn(self.message, RuntimeWarning, 2) + if not self.attrib: + #warn only one time! + object.__setattr__(self, "attrib", 1) + #@-node:__setattr__ + #@-others +#@-node:class _MeProxyWarn +#@-node:Proxies for self referencing +#@+node:Task instrumentation +#@+doc +# This section contains code for byte code instrumenting +# the task functions +#@-doc +#@nonl +#@+node:_int_to_arg +def _int_to_arg(value): + return value % 256, value / 256 +#@-node:_int_to_arg +#@+node:_correct_labels +def _correct_labels(old_code, new_code): + #@ << localize dot variables >> + #@+node:<< localize dot variables >> + hasjrel = opcode.hasjrel + hasjabs = opcode.hasjabs + HAVE_ARGUMENT = opcode.HAVE_ARGUMENT + #@nonl + #@-node:<< localize dot variables >> + #@nl + #@ << loop initialization >> + #@+node:<< loop initialization >> + labels = {} + old_new_map = {} # map old code offset to new code offset + n = len(old_code) + i = 0 + j = 0 + #@nonl + #@-node:<< loop initialization >> + #@nl + while i < n: + op = old_code[i] + nop = new_code[j] + old_new_map[i] = j + i = i + 1 + j = j + 1 + if op >= HAVE_ARGUMENT: + oparg = old_code[i] + old_code[i + 1] * 256 + i = i + 2 + j = j + 2 + if nop != op: + j += 3 # skip the 3 addition opcodes for attrib access + else: + #@ << add label if necessary >> + #@+node:<< add label if necessary >> + label = -1 + if op in hasjrel: + label = i + oparg + elif op in hasjabs: + label = oparg + if label >= 0: + labels[i] = label + #@nonl + #@-node:<< add label if necessary >> + #@nl + + for offset, label in labels.iteritems(): + new_offset = old_new_map[offset] + new_label = old_new_map[label] + op = new_code[new_offset - 3] + #change jump arguments + if op in hasjrel: + jump = _int_to_arg(new_label - new_offset) + new_code[new_offset - 2:new_offset] = jump + elif op in hasjabs: + new_code[new_offset - 2:new_offset] = _int_to_arg(new_label) +#@nonl +#@-node:_correct_labels +#@+node:_instrument +def _instrument(func): + #@ << localize dot variables >> + #@+node:<< localize dot variables >> + opname = opcode.opname + opmap = opcode.opmap + jumps = opcode.hasjrel + opcode.hasjabs + HAVE_ARGUMENT = opcode.HAVE_ARGUMENT + co = func.func_code + local_names = co.co_varnames + all_names = list(co.co_names) + global_names = set() + #@-node:<< localize dot variables >> + #@nl + #@ << define local functions list_to_dict and is_local >> + #@+node:<< define local functions list_to_dict and is_local >> + def list_to_dict(l): + return dict([(t[1], t[0]) for t in enumerate(l)]) + + def is_local(name): + return name[0] == "_" and name != "__constraint__" + #@nonl + #@-node:<< define local functions list_to_dict and is_local >> + #@nl + + #convert code + #@ << loop initialization >> + #@+node:<< loop initialization >> + # all_name_map maps names to the all_names index + # (same like all_names.index()) + all_name_map = list_to_dict(all_names) + if not all_name_map.has_key("me"): + all_name_map["me"] = len(all_names) + all_names.append("me") + + # + for ln in local_names: + if not all_name_map.has_key(ln): + all_name_map[ln] = len(all_names) + all_names.append(ln) + # + + new_local_names = filter(is_local, local_names) + new_local_name_map = list_to_dict(new_local_names) + + me_arg = _int_to_arg(all_name_map["me"]) + old_lnotab = map(ord, co.co_lnotab) + new_lnotab = [] + tab_pos = 0 + try: + next_tab_point = old_lnotab[0] + except IndexError: + next_tab_point = None + + last_tab_point = 0 + code = map(ord, co.co_code) + new_code = [] + has_labels = False + n = len(code) + i = 0 + #@nonl + #@-node:<< loop initialization >> + #@nl + while i < n: + if i == next_tab_point: + #@ << calculate new tab point >> + #@+node:<< calculate new tab point >> + increment = len(new_code) - last_tab_point + new_lnotab.extend((increment, old_lnotab[tab_pos + 1])) + tab_pos += 2 + try: + next_tab_point = i + old_lnotab[tab_pos] + last_tab_point = len(new_code) + except IndexError: + next_tab_point = -1 + #@nonl + #@-node:<< calculate new tab point >> + #@nl + + op = code[i] + i += 1 + if op >= HAVE_ARGUMENT: + #@ << calculate argument >> + #@+node:<< calculate argument >> + arg0 = code[i] + arg1 = code[i+1] + oparg = arg0 + arg1 * 256 + #@nonl + #@-node:<< calculate argument >> + #@nl + i += 2 + + if opname[op] == "LOAD_GLOBAL": + global_names.add(oparg) + + elif opname[op] == "STORE_FAST": + #@ << change "store fast" to "store attribute" >> + #@+node:<< change "store fast" to "store attribute" >> + name = local_names[oparg] + if not is_local(name): + new_code.append(opmap["LOAD_GLOBAL"]) + new_code.extend(me_arg) + op = opmap["STORE_ATTR"] + arg0, arg1 = _int_to_arg(all_name_map[name]) + else: + arg0, arg1 = _int_to_arg(new_local_name_map[name]) + #@nonl + #@-node:<< change "store fast" to "store attribute" >> + #@nl + + elif opname[op] == "LOAD_FAST": + #@ << change "load fast" to "load attribute" >> + #@+node:<< change "load fast" to "load attribute" >> + name = local_names[oparg] + if not is_local(name): + new_code.append(opmap["LOAD_GLOBAL"]) + new_code.extend(me_arg) + op = opmap["LOAD_ATTR"] + arg0, arg1 = _int_to_arg(all_name_map[name]) + else: + arg0, arg1 = _int_to_arg(new_local_name_map[name]) + #@nonl + #@-node:<< change "load fast" to "load attribute" >> + #@nl + + elif op in jumps: + has_labels = True + + new_code.extend((op, arg0, arg1)) + else: + new_code.append(op) + + if has_labels: + _correct_labels(code, new_code) + + #@ << create new code and function objects and return >> + #@+node:<< create new code and function objects and return >> + new_code = "".join(map(chr, new_code)) + new_lnotab = "".join(map(chr, new_lnotab)) + new_co = new.code(co.co_argcount, + len(new_local_names), + max(co.co_stacksize, 2), + co.co_flags, + new_code, + co.co_consts, + tuple(all_names), + tuple(new_local_names), + co.co_filename, + co.co_name, + co.co_firstlineno, + new_lnotab, + co.co_freevars, + co.co_cellvars) + + + func = new.function(new_co, + func.func_globals, + func.func_name, + func.func_defaults, + func.func_closure) + func.global_names = tuple([all_names[index] for index in global_names]) + return func + #@nonl + #@-node:<< create new code and function objects and return >> + #@nl +#@nonl +#@-node:_instrument +#@-node:Task instrumentation +#@+node:Wrappers +#@+node:class _Path +class _Path(object): + """ + This class represents an instrumented path, to + a task. If it points to an attribute of a task, it + not only returns the value of the attribute. You can also + find out the source attribute (task and attribute name) + of the value. + """ + #@ @+others + #@+node:__init__ + def __init__(self, task, path_str): + self._task = task + self._path_str = path_str + #@-node:__init__ + #@+node:__getattr__ + def __getattr__(self, name): + new = getattr(self._task, name) + if isinstance(new, Task): + return _Path(new, self._path_str + "." + name) + + return _ValueWrapper(new, [(self._task, name)]) + #@-node:__getattr__ + #@+node:__str__ + def __str__(self): + return self._path_str + #@-node:__str__ + #@+node:__iter__ + def __iter__(self): + return iter(self._task) + #@nonl + #@-node:__iter__ + #@-others + +#@-node:class _Path +#@+node:_val +#helper functions for _ValueWrapper +#---------------------------------- + +def _val(val): + if isinstance(val, _ValueWrapper): + return val._value + + return val +#@-node:_val +#@+node:_ref +def _ref(val): + if isinstance(val, _ValueWrapper): + return val._ref + + return [] +#@-node:_ref +#@+node:_sref +def _sref(val, ref): + if isinstance(val, _ValueWrapper): + val._ref = ref +#@nonl +#@-node:_sref +#@+node:_refsum +def _refsum(refs): + return reduce(lambda a, b: a + b, refs, []) +#@nonl +#@-node:_refsum +#@+node:class _ValueWrapper + + +class _ValueWrapper(object): + """ + This class represents a value, of a task attribute or + a return value of a task method. It contains the value, + and the supplier of that value + """ + #@ @+others + #@+node:__init__ + def __init__(self, value, ref): + self._value = value + self._ref = ref + #@-node:__init__ + #@+node:unicode + def unicode(self, *args): + if isinstance(self._value, str): + return unicode(self._value, *args) + + return unicode(self._value) + #@nonl + #@-node:unicode + #@+node:_vw + def _vw(self, operand, *args): + refs = _refsum(map(_ref, args)) + vals = map(_val, args) + result = operand(*vals) + return self.__class__(result, refs) + #@-node:_vw + #@+node:_cmp + def _cmp(self, operand, *args): + refs = _refsum(map(_ref, args)) + vals = map(_val, args) + result = operand(*vals) + map(lambda a: _sref(a, refs), args) + return result + #@-node:_cmp + #@+node:__getattr__ + def __getattr__(self, name): return getattr(self._value, name) + #@-node:__getattr__ + #@+node:__getitem__ + def __getitem__(self, slice): + return self.__class__(self._value[slice], self._ref) + #@nonl + #@-node:__getitem__ + #@+node:__str__ + def __str__(self): return str(self._value) + #@-node:__str__ + #@+node:__unicode__ + def __unicode__(self): return unicode(self._value) + #@nonl + #@-node:__unicode__ + #@+node:__repr__ + def __repr__(self): return repr(self._value) + #@-node:__repr__ + #@+node:__nonzero__ + def __nonzero__(self): return bool(self._value) + #@-node:__nonzero__ + #@+node:__lt__ + def __lt__(self, other): return self._cmp(op.lt, self, other) + #@-node:__lt__ + #@+node:__le__ + def __le__(self, other): return self._cmp(op.le, self, other) + #@-node:__le__ + #@+node:__eq__ + def __eq__(self, other): return self._cmp(op.eq, self, other) + #@-node:__eq__ + #@+node:__ne__ + def __ne__(self, other): return self._cmp(op.ne, self, other) + #@-node:__ne__ + #@+node:__gt__ + def __gt__(self, other): return self._cmp(op.gt, self, other) + #@-node:__gt__ + #@+node:__ge__ + def __ge__(self, other): return self._cmp(op.ge, self, other) + #@-node:__ge__ + #@+node:__add__ + def __add__(self, other): return self._vw(op.add, self, other) + #@nonl + #@-node:__add__ + #@+node:__sub__ + def __sub__(self, other): return self._vw(op.sub, self, other) + #@-node:__sub__ + #@+node:__mul__ + def __mul__(self, other): return self._vw(op.mul, self, other) + #@-node:__mul__ + #@+node:__floordiv__ + def __floordiv__(self, other): return self._vw(op.floordiv, self, other) + #@-node:__floordiv__ + #@+node:__mod__ + def __mod__(self, other): return self._vw(op.mod, self, other) + #@-node:__mod__ + #@+node:__divmod__ + def __divmod__(self, other): return self._vw(op.divmod, self, other) + #@-node:__divmod__ + #@+node:__pow__ + def __pow__(self, other): return self._vw(op.pow, self, other) + #@-node:__pow__ + #@+node:__lshift__ + def __lshift__(self, other): return self._vw(op.lshift, self, other) + #@-node:__lshift__ + #@+node:__rshift__ + def __rshift__(self, other): return self._vw(op.rshift, self, other) + #@-node:__rshift__ + #@+node:__and__ + def __and__(self, other): return self._vw(op.and_, self, other) + #@-node:__and__ + #@+node:__xor__ + def __xor__(self, other): return self._vw(op.xor, self, other) + #@-node:__xor__ + #@+node:__or__ + def __or__(self, other): return self._vw(op.or_, self, other) + #@-node:__or__ + #@+node:__div__ + def __div__(self, other): return self._vw(op.div, self, other) + #@-node:__div__ + #@+node:__radd__ + def __radd__(self, other): return self._vw(op.add, other, self) + #@-node:__radd__ + #@+node:__rsub__ + def __rsub__(self, other): return self._vw(op.sub, other, self) + #@-node:__rsub__ + #@+node:__rmul__ + def __rmul__(self, other): return self._vw(op.mul, other, self) + #@-node:__rmul__ + #@+node:__rdiv__ + def __rdiv__(self, other): return self._vw(op.div, other, self) + #@-node:__rdiv__ + #@+node:__rtruediv__ + def __rtruediv__(self, other): return self._vw(op.truediv, other, self) + #@-node:__rtruediv__ + #@+node:__rfloordiv__ + def __rfloordiv__(self, other): return self._vw(op.floordiv, other, self) + #@-node:__rfloordiv__ + #@+node:__rmod__ + def __rmod__(self, other): return self._vw(op.mod, other, self) + #@-node:__rmod__ + #@+node:__rdivmod__ + def __rdivmod__(self, other): return self._vw(op.divmod, other, self) + #@-node:__rdivmod__ + #@+node:__rpow__ + def __rpow__(self, other): return self._vw(op.pow, other, self) + #@-node:__rpow__ + #@+node:__rlshift__ + def __rlshift__(self, other): return self._vw(op.lshift, other, self) + #@-node:__rlshift__ + #@+node:__rrshift__ + def __rrshift__(self, other): return self._vw(op.rshift, other, self) + #@-node:__rrshift__ + #@+node:__rand__ + def __rand__(self, other): return self._vw(op.and_, other, self) + #@-node:__rand__ + #@+node:__rxor__ + def __rxor__(self, other): return self._vw(op.xor, other, self) + #@-node:__rxor__ + #@+node:__ror__ + def __ror__(self, other): return self._vw(op.or_, other, self) + #@-node:__ror__ + #@+node:__int__ + def __int__(self): return int(self._value) + #@-node:__int__ + #@+node:__long__ + def __long__(self): return long(self._value) + #@-node:__long__ + #@+node:__float__ + def __float__(self): return float(self._value) + #@-node:__float__ + #@+node:__len__ + def __len__(self): return len(self._value) + #@-node:__len__ + #@+node:__iter__ + def __iter__(self): return iter(self._value) + #@-node:__iter__ + #@+node:__hash__ + def __hash__(self): return hash(self._value) + #@-node:__hash__ + #@-others +#@-node:class _ValueWrapper +#@-node:Wrappers +#@+node:Utilities +#@+node:class _NEVER_USED_ +class _NEVER_USED_: + pass + +#@-node:class _NEVER_USED_ +#@+node:class _StringConverter +class _StringConverter(object): + """This class is a helper for the to_string mechanism + of tasks""" + #@ @+others + #@+node:__init__ + def __init__(self, source, format=None): + self.source = source + self.format = format + #@-node:__init__ + #@+node:__getitem__ + def __getitem__(self, format): + return _StringConverter(self.source, format) + #@-node:__getitem__ + #@+node:__getattr__ + def __getattr__(self, name): + class StrWrapper(object): + def __init__(self, value, name, source, format): + self._value = value + self.name = name + self.source = source + self.format = format + + def __call__(self, arg): + formatter = self.source.formatter(self.name, + arg, + self.format) + return formatter(self._value(arg)) + + value = getattr(self.source, name) + if callable(value): + #for methods the wrapper has to + return StrWrapper(value, name, self.source, self.format) + + formatter = self.source.formatter(name, format=self.format) + return formatter(value) + #@-node:__getattr__ + #@-others +#@-node:class _StringConverter +#@+node:Multi +def Multi(val, **kwargs): + """returns a directory for mutlivalued attributes""" + return dict(_default=val, **kwargs) +#@nonl +#@-node:Multi +#@+node:create_relative_path +def create_relative_path(from_, to_): + """ + creates a relative path from absolute path + from_ to absolute path to_ + """ + from_ = from_.split(".") + to_ = to_.split(".") + + for i, parts in enumerate(zip(from_, to_)): + from_part, to_part = parts + if from_part != to_part: + break + + from_ = from_[i:] + to_ = to_[i:] + return "up." * len(from_) + ".".join(to_) +#@nonl +#@-node:create_relative_path +#@+node:create_absolute_path +def create_absolute_path(from_, to_): + """ + creates a absolute path from absolute path + from_ to relative path to_ + """ + from_ = from_.split(".") + to_ = to_.split(".") + + for i, part in enumerate(to_): + if part != "up": + break + + from_ = from_[:-i] + to_ = to_[i:] + return "%s.%s" % (".".join(from_), ".".join(to_)) + + +#@-node:create_absolute_path +#@+node:_split_path +def _split_path(path): + try: + index = path.rindex(".") + return path[:index], path[index + 1:] + except: + return path +#@-node:_split_path +#@+node:_to_datetime +_to_datetime = pcalendar.to_datetime +#@nonl +#@-node:_to_datetime +#@+node:_get_tasks_of_sources +def _get_tasks_of_sources(task, attrib_filter="end,start,effort,length,duration"): + #return all source tasks, this task is dependend on + + dep_tasks = {} + + while task: + for dep in task._sources.values(): + for d in dep: + path, attrib = _split_path(d) + if attrib and attrib_filter.find(attrib) >= 0: + dep_tasks[path] = True + + task = task.up + + return dep_tasks.keys() +#@-node:_get_tasks_of_sources +#@+node:_build_balancing_list +def _build_balancing_list(tasks): + """ + Returns a specialy sorted list of tasks. + If the tasks will allocate resources in the sorting order of that list + correct balancing is ensured + """ + + # first sort the list for attributes + index = 0 + balancing_list = [(-t.priority, t.balance, index, t) for index, t in enumerate(tasks)] + balancing_list.sort() + + #print + #for p, b, i, t in balancing_list: + # print p, b, i, t.path + + balancing_list = [ t for p, b, i, t in balancing_list ] + + #now correct the presorted list: + #if task a is dependent on task b, b will be moved before a + + done_map = { } + count = len(balancing_list) + while len(done_map) < count: + for i in range(count): + to_inspect = balancing_list[i] + if done_map.has_key(to_inspect): + continue + + done_map[to_inspect] = True + break + else: + break + + #@ << define inspect_depends_on >> + #@+node:<< define inspect_depends_on >> + inspect_path = to_inspect.path + "." + sources = _get_tasks_of_sources(to_inspect) + sources = [ s + "." for s in sources + if not inspect_path.startswith(s) ] + + # the if in the later line ignores assignments like + # like start = up.start (i.e. references to parents) + # this will be handled in the second if of inspect_depends_on + # and can cause errors otherwise + + def inspect_depends_on(task): + cmp_path = task.path + "." + for src in sources: + if cmp_path.startswith(src): + #task is a source of to_inspect + return True + + if inspect_path.startswith(cmp_path): + #to_inspect is a child of task + return True + + return False + #@nonl + #@-node:<< define inspect_depends_on >> + #@nl + for j in range(i + 1, count): + check_task = balancing_list[j] + if done_map.has_key(check_task): + continue + + if inspect_depends_on(check_task): + del balancing_list[j] + balancing_list.insert(i, check_task) + i += 1 # to_inspect is now at i + 1 + + + return balancing_list +#@-node:_build_balancing_list +#@+node:_as_string +def _as_string(val): + if isinstance(val, basestring): + return '"""%s"""' % val.replace("\n", "\\n") + + if isinstance(val, pcalendar._WorkingDateBase): + return '"%s"' % val.strftime("%Y-%m-%d %H:%M") + + if isinstance(val, datetime.datetime): + return '"%s"' % val.strftime("%Y-%m-%d %H:%M") + + if isinstance(val, datetime.timedelta): + return '"%id %iM"' % (val.days, val.seconds / 60) + + if isinstance(val, tuple): + result = map(_as_string, val) + return "(%s)" % ", ".join(result) + + if isinstance(val, list): + result = map(_as_string, val) + return "[%s]" % ", ".join(result) + + if isinstance(val, resource.Resource): + return val._as_string() + + if isinstance(val, Task): + return val.path + + return str(val) +#@-node:_as_string +#@+node:_step_tasks +def _step_tasks(task): + if isinstance(task, Task): + yield task + + stack = [iter(task.children)] + while stack: + for task in stack[-1]: + yield task + + if task.children: + stack.append(iter(task.children)) + break + else: + stack.pop() +#@-node:_step_tasks +#@-node:Utilities +#@+node:Cache +instrumentation_cache = {} +balancing_cache = {} + +def clear_cache(): + instrumentation_cache.clear() + balancing_cache.clear() +#@nonl +#@-node:Cache +#@+node:Resource Allocators +#@+others +#@+node:VariableLoad +def VariableLoad(limit=0): + """ + Allocates the resource with maximal possible load. + If limit is given, a the load is at least limit or more. + """ + try: + balance = me.balance + except NameError: + balance = SLOPPY + + if balance != SLOPPY: + raise RuntimeError("You may specify variable_load only with balance=SLOPPY") + + return -limit +#@-node:VariableLoad +#@+node:_calc_load +def _calc_load(task, resource): + #changed at the resource instance + load = resource.__dict__.get("load") + if load is not None: return load + + load = task.__dict__.get("load") + if load is not None: return load + + #inherited by the task + return min(task.load, task.max_load, resource.max_load or 100.0) +#@-node:_calc_load +#@+node:_calc_maxload +def _calc_maxload(task, resource): + #changed at the resource instance + max_load = resource.__dict__.get("max_load") + if max_load: return max_load + + #an explicit load can overwrite max_load + load = max(resource.__dict__.get("load", 0), + task.__dict__.get("load"), 0) + + #change at the task + max_load = task.__dict__.get("max_load") + if max_load: return max(max_load, load) + + #inherited by the resource + max_load = resource.max_load + if max_load: return max(max_load, load) + + #inherited by the task + return max(task.max_load, load) +#@-node:_calc_maxload +#@+node:class AllocationAlgorithm +class AllocationAlgorithm(object): + """This class is a base for resource allocation algorithms""" + #@ @+others + #@+node:test_allocation + def test_allocation(self, task, resource): + """This method simulates the allocation of a specific resource. + It returns a list of values representing the state of the allocation. + The task allocator calls test_allocation for every alternative resource. + It compares the first items of all return lists, and allocates the + resource with the minum first item value""" + return (task.end, ) + #@-node:test_allocation + #@+node:allocate + def allocate(self, task, state): + """This method eventually allocates a specific resource. + State is the return list of test_allocation""" + pass + #@-node:allocate + #@-others +#@-node:class AllocationAlgorithm +#@+node:class StrictAllocator +class StrictAllocator(AllocationAlgorithm): + """This class implements the STRICT resource allocation""" + #@ @+others + #@+node:_distribute_len_loads + def _distribute_len_loads(self, task, resource, effort, length): + # A special load calculation, if effort and length are given. + # and the resources have a defined maxload, the load must be + # individually calculated for each resource. + + # Formulars: r=resources, t=task + # effort = length * efficiency(t) * sum[load(r) * effiency(r)] + # ==> sum_load = sum[load(r) * effiency(r)] + # = effort / (length * efficiency(t)) + # + + sum_load = float(effort) / (task.efficiency * length) + + # algorithm: + # The goal is to distribute the load (norm_load) equally + # to all resources. If a resource has a max_load(r) < norm_load + # the load of this resource will be max_load(r), and the other + # resources will have another (higher) norm_load + + max_loads = map(lambda r: (_calc_maxload(task, r), r), resource) + max_loads.sort() + + efficiency_sum = sum(map(lambda r: r.efficiency, resource)) + norm_load = sum_load / efficiency_sum + + loads = {} + for max_load, r in max_loads[:-1]: + if max_load < norm_load: + loads[r] = max_load + efficiency_sum -= r.efficiency + sum_load -= max_load * r.efficiency + norm_load = sum_load / efficiency_sum + else: + loads[r] = norm_load + + max_load, r = max_loads[-1] + loads[r] = norm_load + return loads + #@-node:_distribute_len_loads + #@+node:test_allocation + def test_allocation(self, task, resource): + effort = task.__dict__.get("effort") + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + if task.performed_end: + start = to_start(max(task.performed_end, + task.root.calendar.now, + task.start)) + else: + start = task.start + if task.root.has_actual_data and task.complete == 0: + start = max(start, to_start(task.root.calendar.now)) + + base_start = to_start(task.performed_start or task.start) + calc_load = lambda r: _calc_load(task, r) + loads = map(lambda r: (r, calc_load(r)), resource) + + length = task.__dict__.get("length") + duration = task.__dict__.get("duration") + end = task.__dict__.get("end") + + #@ << correct length >> + #@+node:<< correct length >> + if length is not None: + length = to_delta(max(length - (task.start - base_start), 0)) + #@nonl + #@-node:<< correct length >> + #@nl + #@ << correct duration >> + #@+node:<< correct duration >> + if duration is not None: + delta = task.start.to_datetime() - base_start.to_datetime() + delta = to_delta(delta, True) + duration = to_delta(max(duration - delta, 0), True) + #@nonl + #@-node:<< correct duration >> + #@nl + #@ << check end >> + #@+node:<< check end >> + if end is not None: + length = end - start + if length <= 0: return False + #@nonl + #@-node:<< check end >> + #@nl + #@ << correct effort and (re)calculate length >> + #@+node:<< correct effort and (re)calculate length >> + if effort is not None: + effort -= task.performed_effort + effort = to_delta(max(effort, 0)) + if effort <= 0: return False + + if length is not None: + #if length and effort is set, the load will be calculated + length = length or task.calendar.minimum_time_unit + loads = self._distribute_len_loads(task, resource, + effort, length) + def calc_load(res): + return loads[res] + else: + #the length depends on the count of resources + factor = sum(map(lambda a: a[0].efficiency * a[1], + loads)) * task.efficiency + length = effort / factor + #@nonl + #@-node:<< correct effort and (re)calculate length >> + #@nl + #@ << set adjust_date and delta >> + #@+node:<< set adjust_date and delta >> + if length is not None: + adjust_date = lambda date: date + delta = to_delta(length).round() + else: + assert(duration is not None) + adjust_date = _to_datetime + delta = datetime.timedelta(minutes=duration) + #@nonl + #@-node:<< set adjust_date and delta >> + #@nl + + # find the earliest start date + start, book_load\ + = self.balance(task, start, delta, adjust_date, + calc_load, resource) + + end = to_end(start + delta) + start = to_start(start) + + if effort is None: + #length is frozen ==> a new effort will be calculated + factor = sum(map(lambda a: a[1], loads)) + length = end - start + + effort = to_delta(length * factor\ + + task.performed_effort).round() + + return (end, book_load), resource, calc_load, start, effort + #@-node:test_allocation + #@+node:allocate + def allocate(self, task, state): + # now really book the resource + end_bl, resource, calc_load, start, effort = state + end = end_bl[0] + cal = task.root.calendar + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + task.start = task.performed_start \ + and to_start(task.performed_start) \ + or to_start(start) + + task.end = end + task._unfreeze("length") + task._unfreeze("duration") + length = end - start + + for r in resource: + book_load = calc_load(r) + work_time = to_delta(length * book_load).round() + r.book_task(task, start, end, book_load, work_time, False) + + #the following lines are important to be exactly at this + #positions in that order: + # done and todo are dependend on: + # - the existence of effort (if effort was set or not set) + # - book_task (they can only be calculated, if the task is booked) + # - booked_resource (to get the booked tasks) + task.booked_resource = resource + task.done = task.done + task.todo = task.todo + task.length = end - task.start + task.effort = to_delta(effort + task.performed_effort) + #@-node:allocate + #@+node:balance + #now effort exists always + + + def balance(self, task, start, delta, adjust_date, + calc_load, resource): + book_load = max(map(lambda r: r.get_load(task.start, task.scenario), resource)) + return start, book_load + #@-node:balance + #@-others +#@-node:class StrictAllocator +#@+node:class SmartAllocator + + +class SmartAllocator(StrictAllocator): + #@ @+others + #@+node:balance + def balance(self, task, start, delta, adjust_date, + calc_load, resource): + #find the earliest start date, at which all + #resources in the team are free + + cal = task.root.calendar + to_start = task._to_start + start = adjust_date(start) + scenario = task.scenario + + while True: + #we have finished, when all resources have the + #same next free start date + for r in resource: + max_load = _calc_maxload(task, r) + load = calc_load(r) + + #find the next free time of the resource + s = r.find_free_time(start, delta, load, max_load, scenario) + if s != start: + s = to_start(s) + start = adjust_date(s) + break + else: + #only one resource + break + + return start, 1.0 + #@-node:balance + #@-others +#@-node:class SmartAllocator +#@+node:class SloppyAllocator + + +class SloppyAllocator(AllocationAlgorithm): + #@ @+others + #@+node:test_allocation + def test_allocation(self, task, resource): + if task.__dict__.has_key("effort"): + return self.test_allocation_effort(task, resource) + + return self.test_allocation_length(task, resource) + #@-node:test_allocation + #@+node:test_allocation_length + def test_allocation_length(self, task, resource): + #length is frozen ==> effort will be calculated + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + end = task.end + if task.performed_end: + start = to_start(max(task.performed_end, + task.root.calendar.now, + start)) + else: + start = task.start + + base_start = to_start(task.performed_start or task.start) + length = to_delta(max(task.length - (start - base_start), 0)) + sum_effort = 0 + intervals = [] + scenario = task.scenario + for r in resource: + date = start + max_load = _calc_maxload(task, r) + book_load = _calc_load(task, r) + + while date < end: + #find free time intervals and add them for booking + endi, load = r.end_of_booking_interval(date, task) + endi = min(endi, end) + endi = to_end(endi) + + if book_load <= 0: + #variable book_load ==> calc the maxmimal possible book_load >= (the given book_load) + used_book_load = - book_load + diff_load = max_load - load + if diff_load and diff_load >= book_load: + used_book_load = diff_load + else: + used_book_load = max_load + else: + used_book_load = book_load + + if max_load - load >= used_book_load: + intervals.append((r, used_book_load, date, endi)) + sum_effort = (endi - date) * used_book_load + + date = to_start(endi) + + return -sum_effort, end, resource, intervals + #@-node:test_allocation_length + #@+node:test_allocation_effort + def test_allocation_effort(self, task, resource): + #effort is frozen ==> length will be calculated + + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + intervals = [] + effort = task.__dict__.get("effort") + + if task.performed_end: + next_date = to_start(max(task.performed_end, + task.root.calendar.now, + task.start)) + else: + next_date = task.start + if task.root.has_actual_data and task.complete == 0: + next_date = max(next_date, to_start(task.root.calendar.now)) + + #walks chronologicly through the booking + #intervals of each resource, and reduces + #the effort for each free interval + #until it becomes 0 + + alloc_effort = effort + effort -= task.performed_effort + while effort > 0: + date = next_date + + interval_resource = [] + interval_end = to_start(sys.maxint) + factor = 0 + + for r in resource: + max_load = _calc_maxload(task, r) + book_load = _calc_load(task, r) + end, load = r.end_of_booking_interval(date, task) + interval_end = to_start(min(end, interval_end)) + + if book_load <= 0: + #variable book_load ==> calc the maxmimal possible book_load >= (the given book_load) + book_load = - book_load + diff_load = max_load - load + if diff_load and diff_load >= book_load: + book_load = diff_load + else: + book_load = max_load + + if book_load + load <= max_load: + resource_factor = book_load * r.efficiency + interval_resource.append((r, book_load, resource_factor)) + factor += resource_factor + + + + next_date = interval_end + if factor: + factor *= task.efficiency + length = to_delta(effort / factor).round() + end = date + length + + if interval_end >= end: + next_date = interval_end = end + effort = 0 + book_end = end + else: + book_end = interval_end + length = book_end - date + minus_effort = length * factor + effort -= minus_effort + + book_end = to_end(book_end) + intervals.append((date, book_end, length, interval_resource)) + + return next_date, alloc_effort, resource, intervals + #@-node:test_allocation_effort + #@+node:allocate + def allocate(self, task, state): + if task.__dict__.has_key("effort"): self.allocate_effort(task, state) + else: self.allocate_length(task, state) + #@-node:allocate + #@+node:allocate_length + def allocate_length(self, task, state): + # now really book the resource + neg_sum_effort, end, resource, intervals = state + + cal = task.root.calendar + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + task.start = to_start(task.performed_start or task.start) + task.end = to_end(end) + task._unfreeze("length") + task._unfreeze("duration") + + effort = 0 + for r, load, s, e in intervals: + work_time = to_delta((e - s) * load).round() + effort += work_time + r.book_task(task, s, e, load, work_time, False) + + #see comment at StrictAllocator.allocate + task.booked_resource = resource + task.done = task.done + task.todo = task.todo + task.effort = to_delta(effort + task.performed_effort).round() + #@-node:allocate_length + #@+node:allocate_effort + def allocate_effort(self, task, state): + # now really book the resource + end, effort, resource, intervals = state + to_start = task._to_start + to_end = task._to_end + to_delta = task._to_delta + + task.start = task.performed_start \ + and to_start(task.performed_start) \ + or to_start(intervals[0][0]) + task.end = to_end(end) + task._unfreeze("length") + task._unfreeze("duration") + + for start, end, length, resources in intervals: + for r, load, factor in resources: + work_time = to_delta(length * load) + r.book_task(task, start, end, load, work_time, False) + + task.booked_resource = resource + task.done = task.done + task.todo = task.todo + task.effort = to_delta(effort) + task.length = task.end - task.start + #@-node:allocate_effort + #@-others +#@-node:class SloppyAllocator +#@-others + +_smart_allocator = SmartAllocator() +_sloppy_allocator = SloppyAllocator() +_strict_allocator = StrictAllocator() +_allocators = { SMART: _smart_allocator, + SLOPPY: _sloppy_allocator, + STRICT: _strict_allocator } + +_allocator_strings = { SMART: "SMART", + SLOPPY: "SLOPPY", + STRICT: "STRICT" } +#@-node:Resource Allocators +#@+node:Load Calculators +#@+node:YearlyMax +def YearlyMax(value): + """ + Calculates a load parameter with a maximal yearly workload + """ + #@ << calculate calendar and time_diff >> + #@+node:<< calculate calendar and time_diff >> + try: + cal = me.calendar + except NameError: + cal = pcalendar._default_calendar + + time_diff = cal.Minutes(value) + #@nonl + #@-node:<< calculate calendar and time_diff >> + #@nl + return float(time_diff) / \ + (cal.working_days_per_year \ + * cal.working_hours_per_day \ + * 60) +#@nonl +#@-node:YearlyMax +#@+node:WeeklyMax +def WeeklyMax(value): + """ + Calculates a load parameter with a maximal weekly workload + """ + #@ << calculate calendar and time_diff >> + #@+node:<< calculate calendar and time_diff >> + try: + cal = me.calendar + except NameError: + cal = pcalendar._default_calendar + + time_diff = cal.Minutes(value) + #@nonl + #@-node:<< calculate calendar and time_diff >> + #@nl + return float(time_diff) / \ + (cal.working_days_per_week \ + * cal.working_hours_per_day \ + * 60) + +#@-node:WeeklyMax +#@+node:MonthlyMax +def MonthlyMax(value): + """ + Calculates a load parameter with a maximal monthly workload + """ + #@ << calculate calendar and time_diff >> + #@+node:<< calculate calendar and time_diff >> + try: + cal = me.calendar + except NameError: + cal = pcalendar._default_calendar + + time_diff = cal.Minutes(value) + #@nonl + #@-node:<< calculate calendar and time_diff >> + #@nl + return float(time_diff) / \ + (cal.working_days_per_month \ + * cal.working_hours_per_day \ + * 60) + +#@-node:MonthlyMax +#@+node:DailyMax +def DailyMax(value): + """ + Calculates a load parameter with a maximal daily workload + """ + #@ << calculate calendar and time_diff >> + #@+node:<< calculate calendar and time_diff >> + try: + cal = me.calendar + except NameError: + cal = pcalendar._default_calendar + + time_diff = cal.Minutes(value) + #@nonl + #@-node:<< calculate calendar and time_diff >> + #@nl + return float(time_diff) / (cal.working_hours_per_day * 60) +#@-node:DailyMax +#@-node:Load Calculators +#@+node:Task +#@+node:class _TaskProperty +class _TaskProperty(object): + #@ @+others + #@+node:__init__ + def __init__(self, method): + self.method = method + #@-node:__init__ + #@+node:__get__ + def __get__(self, instance, owner): + if not instance: + return None + + return instance._wrap_attrib(self.method) + #@-node:__get__ + #@-others +#@-node:class _TaskProperty +#@+node:class _RoundingTaskProperty +class _RoundingTaskProperty(object): + #@ @+others + #@+node:__init__ + def __init__(self, method, name): + self.method = method + self.name = name + #@-node:__init__ + #@+node:__get__ + def __get__(self, instance, owner): + if not instance: + return None + + result = instance._wrap_attrib(self.method).round() + if instance._is_frozen: + #correct the attrib to the rounded value + setattr(instance, self.name, result) + + return result + #@-node:__get__ + #@-others +#@-node:class _RoundingTaskProperty +#@+node:class Task +class Task(object): + #@ << description >> + #@+node:<< description >> + """ + This class represents a single task in the project tree. A task + can have other child tasks, or is a leaf of the tree. Resources + will be allocated only to leafes. You will never create task + objects by your self, they are created indirectly by Projects. + + @var root: + Returns the root project task. + + @var up: + Returns the parent task. + + @var title: + Specifies an alternative more descriptive name for the task. + + @var start: + The start date of the task. Valid values are expressions and + strings specifing a datatime + + @var end: + The end date of the task. Valid values are expressions and + strings. + + @var effort: + Specifies the effort needed to complete the task. Valid values + are expressions and strings. (Todo: What happens, in case of + specified performance data...) + + + @var length: + Specifies the time the task occupies the resources. This is + working time, not calendar time. 7d means 7 working days, not one + week. Whether a day is considered a working day or not depends on + the defined working hours and global vacations. + + @var duration: + Specifies the time the task occupies the resources. This is + calendar time, not working time. 7d means one week. + + @var buffer: + Specifies the time a task can be delayed, without moving dependend + milestones. A Task with a buffer S{<=} 0d is part of the critical + chain. This attribute is readonly. + + @var complete: + Specifies what percentage of the task is already completed. + + @var todo: + Specifies the effort, which needs to be done to complete a + task. This is another (indirect) way to specify the ME{complete} + attribute. + + @var done: + Specifies the work effort, which has been already done. This + attribute is readonly. + + @var estimated_effort: + Specifies the estimated_effort given by setting the effort property. + + @var performed: + Specifies a list of actual working times performed on the task. + The format is: C{[ (resource, from, to, time), ... ]} + + @var performed_work_time: + Specifies the sum of all working times. This attribute is + readonly. + + @var performed_effort: + Specifies the complete effort of all working times. This attribute is + readonly. + + @var performed_start: + The start date of the performed data. + + @var performed_end: + The end date of the performed data. + + @var performed_resource: + The resources who have already performed on the task. This attribute is readonly. + + + @var balance: + Specifies the resource allocation type. Possible values are + CO{STRICT}, CO{SLOPPY}, CO{SMART}. + + @var resource: + Specifies the possible resources, that may be allocated for the + task. + + @var booked_resource: + Specifies the allocated resources of a task. This attribute is + readonly. + + @var load: + Specifies the daily load of a resource for an allocation of the + specified task. A load of 1.0 (default) means the resource is + allocated for as many hours as specified by + ME{working_hours_per_day}. A load of 0.5 means half that many + hours. + + @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 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 milestone: + Specified if the task is a milestone. The possible values are + C{True} or "later". If the start date of the milestone is not + a valid working date, the milestone will appear at the previous + working date before the given start date. If "later" is specified + the milestone will appear at the next valid working date. + A milestone has always an effort of 0d. + + @var priority: + Specifies a priority between 1 and 1000. A task with higher + priority is more likely to get the requested resources. The + default priority is 500. + + @var children: + Specifies a list of all subtasks. A task without children is + called a leaf task index{leaf task} otherwise it is called a + parent task index{parent task}. This attribute is readonly. + + @var depth: + Specifies the depth of the task within the hierachy. This + attribute is readonly. + + @var index: + Specifies a structural index number. This attribute is readonly. + + @var path: + Specifies the path. + + @var copy_src: + Specifies the path to an other task. When you set this attribute, + all attributes (except of ME{start} and ME{end}) of copy_src will + be copied to the current task. This is usefull if you want to + define the same task, in diffent project definitions. It acts like + a task link. + + @var scenario: + The scenario which is currently evaluated. This attribute is readonly. + + @var dont_inherit: + A list of attribute names, which will be not inherited by + subtasks. + + @var calendar: + Specifies the task calendar. + + @var working_days_per_week: + Specifies the days within a working week. This value is used + internally to convert time differences from weeks to days. The + default value is 5 days. + + @var working_days_per_month: + Specifies the days within a working month. This value is used + internally to convert time differences from months to days. The + default value is 20 days. + + @var working_days_per_year: + Specifies the days within a working year. This value is used + internally to convert time differences from years to days The + default value is 200 days. + + @var working_hours_per_day: + Specifies the hours within a working day. This value is used + internally to convert time differences from are entered in days to + hours. The default value is 8 hours. + + @var minimum_time_unit: + Specifies the minimum resolution in minutes for the task + scheduling. The default value is 15 minutes. + + @var vacation: + Specifies a public vacation for the calendar. 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. + + @var extra_work: + Specifies additional worktime. 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. + + @var working_days: + Specifies the weekly working time within calendar. The format of + this attribute is: [ (day_range, time_range, ...), (day_range, time_range, ...), ... ]. + day_range is a comma sperated string of week days. Valid values + are mon, tue, wed, thu, fri, sat, sun. + time_range is string specifing a time interval like + 8:00-10:00. You can specified any number of time_ranges, following + the first. + + @var now: + Specifies the current daytime and is a date literal. ME{now} is + used to calculate several task attributes. + + """ + #@nonl + #@-node:<< description >> + #@nl + #@ << declarations >> + #@+node:<< declarations >> + # Variables for the gui interface + _date_completion = { "Date": 'Date("|")', + "max": "max(|)", + "min": "min(|)", + "Multi" : "Multi(|)" } + + + _delta_completion = { "Delta" : 'Delta("|")', + "Multi" : "Multi(|)" } + + + __attrib_completions__ = { \ + "def NewTask():" : "def |NewTask():\n", + "milestone": 'milestone = True', + "start": 'start = ', + "end": 'end = ', + "effort": 'effort = "|"', + "duration": 'duration = "|"', + "length": 'length = "|"', + "todo": 'todo = "|"', + "done": 'done = "|"', + "title": 'title = "|"', + "load": 'load = ', + "max_load": 'max_load = ', + "efficiency": 'efficiency = ', + "complete": 'complete = ', + "copy_src": 'copy_src =', + "__constraint__": '__constraint__():\n|"', + "priority": 'priority = ', + "balance" : 'balance = ', + "resource": 'resource = ', + "performed" : 'performed = [(|resource, "2002-02-01", "2002-02-05", "2H"),]', + "add_attrib": "add_attrib(|'name', None)", + "working_days_per_week": 'working_days_per_week = ', + "working_days_per_month": 'working_days_per_month = ', + "working_days_per_year": 'working_days_per_year = ', + "working_hours_per_day": 'working_hours_per_day = ', + "minimum_time_unit": 'minimum_time_unit = ', + "vacation": 'vacation = [("|2002-02-01", "2002-02-05")]', + "extra_work": 'extra_work = [("|2002-02-01", "2002-02-05")]', + "working_days" : 'working_days = ["|mon,tue,wed,thu,fri", "8:00-12:00", "13:00-17:00"]', + "now": 'now = "|"', + "calendar" : 'calendar = ', + "#load": { "YearlyMax": 'YearlyMax("|")', + "WeeklyMax": 'WeeklyMax("|")', + "MonthlyMax": 'MonthlyMax("|")', + "DailyMax": 'DailyMax("|")', + "VariableLoad" : "VariableLoad(|)"}, + "#max_load": { "YearlyMax": 'YearlyMax("|")', + "WeeklyMax": 'WeeklyMax("|")', + "MonthlyMax": 'MonthlyMax("|")', + "DailyMax": 'DailyMax("|")' }, + "#start": _date_completion, + "#end": _date_completion, + "#effort": _delta_completion, + "#duration": _delta_completion, + "#length": _delta_completion, + "#todo": _delta_completion, + "#done": _delta_completion, + "#resource" : "get_resource_completions", + "#calendar" : "get_calendar_completions", + "#balance": { "STRICT": "STRICT", + "SMART": "SMART", + "SLOPPY": "SLOPPY" } } + + + formats = { "start" : "%x %H:%M", + "end" : "%x %H:%M", + "performed_start" : "%x %H:%M", + "performed_end" : "%x %H:%M", + "load" : "%.2f", + "length" : "%dd{ %HH}{ %MM}", + "effort" : "%dd{ %HH}{ %MM}", + "estimated_effort" : "%dd{ %HH}{ %MM}", + "performed_effort" : "%dd{ %HH}{ %MM}", + "duration" : "%dd{ %HH}{ %MM}", + "complete" : "%i", + "priority" : "%i", + "todo" : "%dd{ %HH}{ %MM}", + "done" : "%dd{ %HH}{ %MM}", + "efficiency" : "%.2f", + "buffer" : "%dd{ %HH}{ %MM}", + "costs" : "%.2f", + "sum" : "%.2f", + "max" : "%.2f", + "min" : "%.2f", + "milestone" : "%s", + "resource" : "%s", + "booked_resource" : "%s", + "performed_resource" : "%s" } + + _constraint = None + _is_frozen = False + _is_compiled = False + _is_parent_referer = False + + scenario = None # only for autocompletion + milestone = False + performed = () + performed_resource = () + booked_resource = () + _performed_resource_length = () + _resource_length = () + dont_inherit = () + performed_start = None + performed_end = None + performed_work_time = pcalendar.Minutes(0) + + _setting_hooks = {} + #@nonl + #@-node:<< declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, func, name, parent=None, index=1): + + assert(type(func) == types.FunctionType) + + func_key = (func.func_code, func.func_closure and id(func.func_closure)) + + try: + instrumented = instrumentation_cache[func_key] + except KeyError: + instrumented = _instrument(func) + instrumented.org_code = func_key + instrumentation_cache[func_key] = instrumented + + func.task_func = instrumented # will be used in the gui + self._function = instrumented + self.name = name + self.up = parent + self.children = [] + self._sources = {} # all tasks, I am linked to + self._dependencies = {} # all tasks that link to me + self._original_values = {} + self._properties = {} # a registry of all non standard attributes + self.title = self.name + self.root = parent and parent.root or self + self.scenario = self.root.scenario + self.path = parent and parent.path + "." + name or name + self.depth = len(self.path.split(".")) - 1 + self.index = parent and ("%s.%i" % (parent.index, index)) \ + or str(index) + if self.formats.has_key(name): + raise AttributeError("Task name '%s' hides attribute of parent." \ + % name) + + cal = self.calendar + self._to_delta = cal.Minutes + self._to_start = cal.StartDate + self._to_end = cal.EndDate + + #@-node:__init__ + #@+node:__iter__ + def __iter__(self): + return _step_tasks(self) + #@-node:__iter__ + #@+node:__repr__ + def __repr__(self): + return "" % self.name + #@-node:__repr__ + #@+node:__cmp__ + def __cmp__(self, other): + try: + return cmp(self.path, other.path) + except Exception: + return cmp(self.path, other) + #@-node:__cmp__ + #@+node:__getattr__ + def __getattr__(self, name): + try: + if name[0] != "_": + parent = self.up + while parent: + if name not in parent.dont_inherit: + result = getattr(parent, name) + if not (isinstance(result, Task) and result.up == parent): + return result + + parent = parent.up + except AttributeError: + pass + except IndexError: + raise AttributeError() + + exception = AttributeError("'%s' is not a valid attribute of '%s'" + % (name, self.path)) + exception.is_frozen = self._is_frozen + raise exception + #@-node:__getattr__ + #@+node:_idendity_ + def _idendity_(self): return self.root.id + self.path[4:] + #@-node:_idendity_ + #@+node:_set_hook + def _set_hook(cls, attrib_name, function=None): + if function: + cls._setting_hooks[attrib_name] = function + else: + try: + del cls._setting_hooks[attrib_name] + except KeyError: pass + + + _set_hook = classmethod(_set_hook) + #@nonl + #@-node:_set_hook + #@+node:Public methods + #@+node:to_string + def to_string(self): return _StringConverter(self) + to_string = property(to_string) + #@nonl + #@-node:to_string + #@+node:indent_name + def indent_name(self, ident=" "): + """ + returns a indented name, according to its depth in the hierachy. + """ + + return ident * self.depth + self.name + + indent_name.attrib_method = True + indent_name.__call_completion__ = "indent_name()" + #@-node:indent_name + #@+node:costs + def costs(self, cost_name, mode="ep"): + """ + calculates the resource costs for the task. + cost_name is the name of a rate attribute of the reosurce + mode is character combination: + e calculates the estimated costs + p calculates the performed costs + ==> pe calculates all costs + """ + + if self.children: + return sum([ c.costs(cost_name, mode) for c in self.children]) + + costs = 0 + if 'e' in mode: + costs += sum(map(lambda rl: getattr(rl[0], cost_name) * rl[1], + self._resource_length)) + + if 'p' in mode: + costs += sum(map(lambda rl: getattr(rl[0], cost_name) * rl[1], + self._performed_resource_length)) + + costs /= (60.0 * self.root.calendar.working_hours_per_day) + return round(costs, 2) + + costs.attrib_method = True + costs.__call_completion__ = 'costs("|")' + #@-node:costs + #@+node:sum + def sum(self, attrib_name): + val = 0 + + if self.children: + val += sum(map(lambda c: c.sum(attrib_name), self.children)) + if self.is_inherited(attrib_name): + return val + + if attrib_name not in self.dont_inherit: + return val + + return val + getattr(self, attrib_name) + + sum.attrib_method = True + sum.__call_completion__ = 'sum("|")' + + #@-node:sum + #@+node:min + def min(self, attrib_name): + if self.children: + return min(map(lambda c: c.min(attrib_name), self.children)) + + return getattr(self, attrib_name) + + min.attrib_method = True + min.__call_completion__ = 'min("|")' + + #@-node:min + #@+node:max + def max(self, attrib_name): + if self.children: + return max(map(lambda c: c.max(attrib_name), self.children)) + + return getattr(self, attrib_name) + + max.attrib_method = True + max.__call_completion__ = 'max("|")' + + #@-node:max + #@+node:all_resources + def all_resources(self): + result = self._all_resources_as_dict() + result = result.keys() + result.sort() + return result + #@-node:all_resources + #@+node:get_task + def get_task(self, path=None): + """ + Returns a task with the given path. + """ + + if not path: + return self + + names = path.split(".") + rest = ".".join(names[1:]) + result = getattr(self, names[0], None) + return isinstance(result, Task) and result.get_task(rest) or None + #@-node:get_task + #@+node:snapshot + def snapshot(self, indent="", name=None): + text = indent + "def %s():\n" % (name or self.name) + indent += " " + for name in ("priority", "balance", "complete", + "milestone", "end", "start", "effort", "load"): + val = getattr(self, name, None) + if val is None: + continue + + if name[0] == "_": + name = name[1:] + + text += "%s%s = %s\n" % (indent, name, _as_string(val)) + + for name in self._properties: + if name.startswith("performed"): continue + val = getattr(self, name, None) + try: + if issubclass(val, resource.Resource): continue + except TypeError: + pass + text += "%s%s = %s\n" % (indent, name, _as_string(val)) + + resources = tuple(self._iter_booked_resources()) + if resources: + text += "%sresource = \\\n" % indent + def make_resource(res): + return "%s %s" \ + % (indent, res.snapshot()) + + text += "&\\\n".join(map(make_resource, resources)) + "\n" + + def make_resource_booking(res): + def make_booking(booking): + return '%s (%s, "%s", "%s", "%sM"),' \ + % (indent, res.name, + booking.book_start.strftime("%Y%m%d %H:%M"), + booking.book_end.strftime("%Y%m%d %H:%M"), + booking.work_time) + + return "\n".join(map(make_booking, res.get_bookings(self))) + + + text += "%sperformed = [\n" % indent + text += "\n".join(map(make_resource_booking, resources)) + "]" + + + child_text = map(lambda c: c.snapshot(indent), self.children) + text += "\n\n" + text += "".join(child_text) + + return text + #@-node:snapshot + #@+node:is_inherited + def is_inherited(self, attrib_name): + return not self.__dict__.has_key(attrib_name) + #@-node:is_inherited + #@+node:formatter + def formatter(self, attrib_name, arg=None, format=None): + """returns a function which is able + to convert the value of the given attrib_name to a string""" + + formats = self.formats + format = format or formats.get(attrib_name) + + if attrib_name in ("start", "end", "length", "effort", + "done", "todo", "buffer", "estimated_effort", + "performed_effort", "performed_start", "performed_end"): + def save_strftime(v): + try: + return v.strftime(format) + #except AttributeError: some bug avoid catching this exception + except Exception: + return str(v) + + return save_strftime + + if attrib_name == "duration": + def save_strftime(v): + try: + return v.strftime(format, True) + except AttributeError: + return str(v) + + return save_strftime + + if attrib_name in ("booked_resource", "performed_resource"): + def get_resource_name(v): + title = getattr(v, "title", None) + if title: return title + return ", ".join([r.title for r in v]) + return get_resource_name + + if arg and attrib_name in ("costs", "sum", "max", "min"): + format = formats.get("%s(%s)" % (attrib_name, arg), format) + + if format: + return lambda v: locale.format(format, v, True) + + return str + #@-node:formatter + #@-node:Public methods + #@+node:Resource allocation Methods + #@+node:_all_resources_as_dict + def _all_resources_as_dict(self): + if self.children: + result = {} + for c in self.children: + result.update(c._all_resources_as_dict()) + + return result + + if self.resource: + return dict(map(lambda r: (r, 1), self.resource.all_members())) + + return {} + #@-node:_all_resources_as_dict + #@+node:_test_allocation + def _test_allocation(self, resource_state, allocator): + resource = self.resource._get_resources(resource_state) + if not resource: + return False + + return allocator.test_allocation(self, resource) + #@-node:_test_allocation + #@+node:_allocate + def _allocate(self, state, allocator): + allocator.allocate(self, state) + #activate cache for done and todo + + if self.start.to_datetime() > self.end.to_datetime(): + #this can happen when performed effort are + #during non working time + tmp = self.start + self.start = self.end + self.end = tmp + + for r in self.performed_resource: + r.correct_bookings(self) + + self._resource_length = map(lambda r: (weakref.proxy(r), \ + r.length_of(self)), + self._iter_booked_resources()) + #@-node:_allocate + #@+node:_convert_performed + def _convert_performed(self, all_resources): + performed = self.performed + if not performed: return False + + if not isinstance(performed, (tuple, list)) \ + or not isinstance(performed[0], (tuple, list)) \ + or not len(performed[0]) >= 3: + self._raise(TypeError("""The format of the performed attribute must be: + [( res_name, start_literal, end_literal, working_time ), ... ]. + """), "performed") + + round_down_delta = self.root.calendar.minimum_time_unit / 2 + round_down_delta = datetime.timedelta(minutes=round_down_delta) + + def convert_item(index): + item = performed[index] + res, start, end = item[:3] + if isinstance(res, str): + found = filter(lambda r: r.name == res, all_resources) + if found: res = found[0] + + try: + if not isinstance(res, (resource.Resource, + resource._MetaResource)): + raise ValueError("the resource '%s' is unknown." % res) + + start = _to_datetime(start) + end = _to_datetime(end) + + if len(item) > 3: + working_time = self._to_delta(item[3]).round() + else: + working_time = self._to_delta(end - start, True) + + return ((res, start, end, working_time), index) + except Exception, exc: + self._raise(exc.__class__("Item %i: %s" \ + % (index + 1, str(exc))), + "performed") + + converted = dict(map(convert_item, range(len(performed)))) + converted = converted.items() + converted.sort() + + #check for overlapping items + last_res = None + for item, index in converted: + res, start, end, work_time = item + if last_res == res and start < last_end: + self._warn("Items %i, %i: %s and %s are overlapping." \ + % (last_index + 1, index + 1, + str(performed[last_index]), + str(performed[index])), + "performed") + + last_res = res + last_end = end + last_index = index + + self._performed = map(lambda x: x[0], converted) + return True + #@-node:_convert_performed + #@+node:_allocate_performed + def _allocate_performed(self, performed): + if not performed: return + + to_delta = self._to_delta + to_start = self._to_start + to_end = self._to_end + + last = datetime.datetime.min + first = datetime.datetime.max + effort = 0 + work_time_sum = 0 + zero_minutes = to_delta(0) + minimum_time_unit = to_delta(self.calendar.minimum_time_unit) + summary = {} + + for item in performed: + res, start, end, work_time = item + effort += work_time * self.efficiency * res.efficiency + work_time_sum += work_time + + res = res() + ss, es, wts = summary.get(res, (datetime.datetime.max, + datetime.datetime.min, + zero_minutes)) + summary[res] = (min(ss, start), max(es, end), wts + work_time) + + for r, v in summary.iteritems(): + start, end, work_time = v + assert(start.__class__ is datetime.datetime) + assert(end.__class__ is datetime.datetime) + + #the booking limits should be inside the workingtime + #to display them correct in resource charts + cstart = to_start(start).to_datetime() + if cstart > start: cstart = to_end(start).to_datetime() + + cend = to_end(end).to_datetime() + if cend < end: cend = to_start(end).to_datetime() + + if self.root.is_snapshot: + delta = to_end(cend) - to_start(cstart) + else: + delta = to_delta(cend - cstart).round() + + if not delta: + delta = minimum_time_unit + + book_load = float(work_time) / delta + r().book_task(self, cstart, cend, book_load, work_time, True) + last = max(end, last) + first = min(start, first) + + self._performed_resource_length = tuple([ (r, v[2]) for r, v in summary.iteritems() ]) + self.performed_resource = tuple(summary.keys()) + self.performed_end = last + self.performed_start = first + self.performed_effort = to_delta(effort) + self.performed_work_time = to_delta(work_time_sum) + self._check_completion() + #@-node:_allocate_performed + #@+node:_iter_booked_resources + def _iter_booked_resources(self): + result = dict(map(lambda r: (r, 1), self.performed_resource)) + result.update(dict(map(lambda r: (r, 1), self.booked_resource))) + return result.iterkeys() + #@-node:_iter_booked_resources + #@-node:Resource allocation Methods + #@+node:Compile Methods + #@+node:_generate + def _generate(self, deferred=None): + do_raise = False + deferred = deferred or [ self ] + while deferred: + new_deferred = [] + for task in deferred: + task._compile(new_deferred, do_raise) + + do_raise = deferred == new_deferred + deferred = new_deferred + #@-node:_generate + #@+node:_recalc_properties + def _recalc_properties(self): + if not self._properties: return + self.__compile_function([], False, _MeProxyRecalc(self)) + self._is_compiled = True + #@-node:_recalc_properties + #@+node:_compile + def _compile(self, deferred, do_raise): + self.dont_inherit = () + self._constraint = None + self._original_values.clear() + self._properties.clear() + + try: + self.__at_compile + #@ << raise child recursion error >> + #@+node:<< raise child recursion error >> + self._raise(RecursionError("A child defines a "\ + "recursive definition at %s" % self.path)) + #@-node:<< raise child recursion error >> + #@nl + except AttributeError: + self.__at_compile = self, "" + + try: + self.__compile_function(deferred, do_raise, _MeProxy(self)) + finally: + del self.__at_compile + + for c in self.children: + if not c._is_compiled: + c._compile(deferred, do_raise) + + if self._is_compiled: + self.__check_milestone() + self.__check_task() + self.root.has_actual_data |= self.__dict__.has_key("performed") + + #@-node:_compile + #@+node:__compile_function + def __compile_function(self, deferred, do_raise, me_instance): + self._is_compiled = self._is_frozen + + restore_globals = [] + globals_ = self._function.func_globals + + #@ << set function global values >> + #@+node:<< set function global values >> + def to_value_wrapper(a): + if isinstance(a, _ValueWrapper): + return a + + return _ValueWrapper(a, [(None, None)]) + + def my_max(*args): + return max(map(to_value_wrapper, args)) + + def my_min(*args): + return min(map(to_value_wrapper, args)) + + globals_["me"] = me_instance + + if self._is_compiled: + globals_["up"] = self.up + globals_["root"] = self.root + else: + globals_["up"] = _Path(self.up, "up") + globals_["root"] = _Path(self.root, "root") + + globals_["Delta"] = self._to_delta + globals_["Date"] = self._to_start + globals_["max"] = my_max + globals_["min"] = my_min + globals_["add_attrib"] = me_instance.add_attrib + #@nonl + #@-node:<< set function global values >> + #@nl + #@ << set me in global functions >> + #@+node:<< set me in global functions >> + #@+at + # Is used for functions like YearlyMax, MonthlyMax, .... + #@-at + #@@code + for name in self._function.global_names: + try: + obj = globals_[name] + if isinstance(obj, types.FunctionType): + fg = obj.func_globals + if not fg.has_key("me") and "me" in obj.func_code.co_names: + restore_globals.append(fg) + fg["me"] = me_instance + except KeyError: continue + #@nonl + #@-node:<< set me in global functions >> + #@nl + try: + #@ << eval function >> + #@+node:<< eval function >> + if do_raise: + try: + self._function() + self._is_compiled = True + except _IncompleteError, e: + src = e.args[1] + if src is not self: + self.__at_compile = e.args[1:] + src._compile([], True) + + raise + else: + try: + self._function() + self._is_compiled = True + except AttributeError, e: + #print "AttributeError:", e, self.name, e.is_frozen, do_raise + deferred.append(self) + except _IncompleteError: + #print "_IncompleteError:", id(self), self.name, do_raise + deferred.append(self) + except RecursionError: + self._is_parent_referer = True + deferred.append(self) + #@nonl + #@-node:<< eval function >> + #@nl + finally: + for fg in restore_globals: + del fg["me"] + + #@-node:__compile_function + #@-node:Compile Methods + #@+node:Setting methods + #@+node:_set_attrib + def _set_attrib(self, name, value): + + if value is _NEVER_USED_: return + + try: + value = self._setting_hooks[name](self, name, value) + except KeyError: pass + + if name == "__constraint__": + self._constraint = value + return + + if type(value) == types.FunctionType: + if value.func_code.co_argcount == 0: + #@ << add child task >> + #@+node:<< add child task >> + try: + task = self.__dict__[name] + except KeyError: + task = Task(value, name, self, len(self.children) + 1) + self.children.append(task) + setattr(self, task.name, task) + return + #@nonl + #@-node:<< add child task >> + #@nl + + if name[0] == "_": + #private vars will not be set + return + + if isinstance(value, _Path): + value = value._task + + set_method = getattr(self, "_set_" + name, None) + if set_method: + #@ << set standard attribute >> + #@+node:<< set standard attribute >> + if type(value) == types.DictionaryType: + self.root.all_scenarios.update(value.keys()) + value = value.get(self.scenario, value["_default"]) + + self.__set_sources(name, value) + self._original_values[name] = value + set_method(_val(value)) + #@nonl + #@-node:<< set standard attribute >> + #@nl + else: + #@ << set userdefined attribute >> + #@+node:<< set userdefined attribute >> + if callable( getattr(self.__class__, name, None)): + raise NameError('You may not use "%s" as attribute' % name) + + setattr(self, name, value) + self._properties[name] = True + self.__set_sources(name, value) + #@nonl + #@-node:<< set userdefined attribute >> + #@nl + #@-node:_set_attrib + #@+node:read only attributes + #@+node:_set_name + def _set_name(self, value): + raise AttributeError("The attribute 'name' is readonly.") + #@nonl + #@-node:_set_name + #@+node:_set_done + def _set_done(self, value): + raise AttributeError("The attribute 'done' is readonly.") + #@nonl + #@-node:_set_done + #@+node:_set_performed_work_time + def _set_performed_work_time(self, value): + raise AttributeError("The attribute 'performed_work_time' is readonly.") + #@nonl + #@-node:_set_performed_work_time + #@+node:_set_booked_resource + def _set_booked_resource(self, value): + raise AttributeError("The attribute 'booked_resource' is readonly.") + #@nonl + #@-node:_set_booked_resource + #@+node:_set_performed_effort + def _set_performed_effort(self, value): + raise AttributeError("The attribute 'performed_effort' is readonly.") + #@nonl + #@-node:_set_performed_effort + #@+node:_set_children + def _set_children(self, value): + raise AttributeError("The attribute 'children' is readonly.") + #@nonl + #@-node:_set_children + #@+node:_set_depth + def _set_depth(self, value): + raise AttributeError("The attribute 'depth' is readonly.") + #@nonl + #@-node:_set_depth + #@+node:_set_index + def _set_index(self, value): + raise AttributeError("The attribute 'index' is readonly.") + #@nonl + #@-node:_set_index + #@+node:_set_scenario + def _set_scenario(self, value): + raise AttributeError("The attribute 'scenario' is readonly.") + #@nonl + #@-node:_set_scenario + #@+node:_set_buffer + def _set_buffer(self, value): + raise AttributeError("The attribute 'buffer' is readonly.") + #@nonl + #@-node:_set_buffer + #@-node:read only attributes + #@+node:_set_start + def _set_start(self, value): + self.__start_class = value.__class__ + self.start = self._to_start(value).round() + #@-node:_set_start + #@+node:_set_end + def _set_end(self, value): + self.end = self._to_end(value) + #@-node:_set_end + #@+node:_set_max_load + def _set_max_load(self, max_load): + self.max_load = float(max_load) + #@-node:_set_max_load + #@+node:_set_load + def _set_load(self, load): + self.load = float(load) + #@-node:_set_load + #@+node:_set_length + def _set_length(self, value): + self.length = self._to_delta(value).round() + #@-node:_set_length + #@+node:_set_effort + def _set_effort(self, value): + self.effort = self._to_delta(value).round() + #@-node:_set_effort + #@+node:_set_duration + def _set_duration(self, value): + self.duration = self._to_delta(value, True).round() + #@-node:_set_duration + #@+node:_set_complete + def _set_complete(self, value): + self.complete = value + #@-node:_set_complete + #@+node:_set_done + def _set_done(self, value): + self.done = self._to_delta(value).round() + #@-node:_set_done + #@+node:_set_todo + def _set_todo(self, value): + self.todo = self._to_delta(value).round() + #@-node:_set_todo + #@+node:_set_milestone + def _set_milestone(self, value): + self.milestone = value + #@-node:_set_milestone + #@+node:_set_resource + def _set_resource(self, value): + if not value: + self.resource = None + return + + if isinstance(value, (tuple, list)): + value = reduce(lambda a, b: a & b, value) + + self.resource = value() + + #@-node:_set_resource + #@+node:_set_copy_src + def _set_copy_src(self, value): + if isinstance(value, _MeProxy): + raise RuntimeError("Cannot copy me.") + + if not value._is_compiled: + raise _IncompleteError(value, "copy_src") + + if value.resource and not self.resource: + self.resource = value.resource + + if value.balance and not self.balance: + self.balance = value.balance + + copy_parms = ("priority", "todo", "complete", + "_constraint", "load", "length", + "effort", "duration") + + for p in copy_parms: + v = value.__dict__.get(p) + if v: setattr(self, p, v) + + self.copy_src = value + self._properties.update(value._properties) + for k in value._properties.iterkeys(): + setattr(self, k, getattr(value, k)) + #@-node:_set_copy_src + #@+node:__set_sources + def __set_sources(self, attrib_name, value): + #@ << find references >> + #@+node:<< find references >> + def make_ref(val): + if isinstance(val, _ValueWrapper): + return val._ref + + if isinstance(val, Task): + return [(val, "")] + + return [] + + if isinstance(value, (list, tuple)): + sources = _refsum(map(make_ref, value)) + else: + sources = make_ref(value) + #@nonl + #@-node:<< find references >> + #@nl + if not sources: return + + #track only dependcies within the same project + root = self.root + sources = [ task.path + "." + attrib + for task, attrib in sources + if task and task.root is root ] + self._sources[attrib_name] = tuple(sources) + attr_path = self.path + "." + attrib_name + + #set dependencies of my sources + for d in sources: + path, attrib = _split_path(d) + task = self.get_task(path) + r_d = task._dependencies + d_l = r_d.setdefault(attrib, {}) + d_l[attr_path] = True + #@-node:__set_sources + #@+node:Calendar Setters + #@+node:_set_calendar + def _set_calendar(self, value): + self.calendar = value + self._to_delta = value.Minutes + self._to_start = value.StartDate + self._to_end = value.EndDate + self.__renew_dates() + + #@-node:_set_calendar + #@+node:__renew_dates + def __renew_dates(self): + for attrib in ("effort", "start", "end", "length", "todo"): + try: + + self._set_attrib(attrib, self._original_values[attrib]) + except KeyError: + pass + + #@-node:__renew_dates + #@+node:__make_calendar + def __make_calendar(self): + if not "calendar" in self.__dict__: + cal = self.calendar = self.calendar.clone() + self._to_delta = cal.Minutes + self._to_start = cal.StartDate + self._to_end = cal.EndDate + #@nonl + #@-node:__make_calendar + #@+node:_set_vacation + def _set_vacation(self, value): + self.__make_calendar() + self.calendar.set_vacation(value) + self._properties["vacation"] = True + self.vacation = value + self.__renew_dates() + #@-node:_set_vacation + #@+node:_set_extra_work + def _set_extra_work(self, value): + self.__make_calendar() + self.calendar.set_extra_work(value) + self._properties["extra_work"] = True + self.extra_work = value + self.__renew_dates() + #@-node:_set_extra_work + #@+node:_set_working_days + def _set_working_days(self, value): + + if type(value[0]) is str: + value = (value, ) + + self.working_days = value + self._properties["working_days"] = True + self.__make_calendar() + + for v in value: + day_range = v[0] + tranges = tuple(v[1:]) + self.calendar.set_working_days(day_range, *tranges) + + self.__renew_dates() + #@nonl + #@-node:_set_working_days + #@+node:_set_minimum_time_unit + def _set_minimum_time_unit(self, value): + self.__make_calendar() + self.calendar.minimum_time_unit = value + self._properties["minimum_time_unit"] = True + #@-node:_set_minimum_time_unit + #@+node:_get_minimum_time_unit + def _get_minimum_time_unit(self): + return self.calendar.minimum_time_unit + + minimum_time_unit = property(_get_minimum_time_unit) + #@-node:_get_minimum_time_unit + #@+node:_set_working_days_per_week + def _set_working_days_per_week(self, value): + + self.__make_calendar() + self.calendar.working_days_per_week = value + self._properties["working_days_per_week"] = True + #@-node:_set_working_days_per_week + #@+node:_get_working_days_per_week + def _get_working_days_per_week(self): + return self.calendar.working_days_per_week + + working_days_per_week = property(_get_working_days_per_week) + #@-node:_get_working_days_per_week + #@+node:_set_working_days_per_month + def _set_working_days_per_month(self, value): + self.__make_calendar() + self.calendar.working_days_per_month = value + self._properties["working_days_per_month"] = True + #@-node:_set_working_days_per_month + #@+node:_get_working_days_per_month + def _get_working_days_per_month(self): + return self.calendar.working_days_per_month + + working_days_per_month = property(_get_working_days_per_month) + #@-node:_get_working_days_per_month + #@+node:_set_working_days_per_year + def _set_working_days_per_year(self, value): + self.__make_calendar() + self.calendar.working_days_per_year = value + self._properties["working_days_per_year"] = True + #@-node:_set_working_days_per_year + #@+node:_get_working_days_per_year + def _get_working_days_per_year(self): + return self.calendar.working_days_per_year + + working_days_per_year = property(_get_working_days_per_year) + #@-node:_get_working_days_per_year + #@+node:_set_working_hours_per_day + def _set_working_hours_per_day(self, value): + self.__make_calendar() + self.calendar.working_hours_per_day = value + self._properties["set_working_hours_per_day"] = True + #@-node:_set_working_hours_per_day + #@+node:_get_working_hours_per_day + def _get_working_hours_per_day(self): + return self.calendar.working_hours_per_day + + working_hours_per_day = property(_get_working_hours_per_day) + #@-node:_get_working_hours_per_day + #@+node:_set_now + def _set_now(self, value): + proxy = weakref.proxy + self.calendar.now = _to_datetime(value) + #@-node:_set_now + #@-node:Calendar Setters + #@-node:Setting methods + #@+node:Freezer Methods + #@+node:_unfreeze + def _unfreeze(self, attrib_name): + if self.__dict__.has_key(attrib_name): + del self.__dict__[attrib_name] + #@-node:_unfreeze + #@+node:_wrap_attrib + def _wrap_attrib(self, method): + attrib_name = method.__name__[7:] + recursion_attrib = "_rec" + attrib_name + + try: + dest, dattr = self.__at_compile + raise RecursionError("Recursive definition of %s(%s) and %s(%s)" \ + % (self.path, attrib_name, dest.path, dattr)) + except AttributeError: pass + + if not self._is_compiled: + raise _IncompleteError(self, attrib_name) + + try: + getattr(self, recursion_attrib) + raise RecursionError(self, attrib_name) + except AttributeError: pass + + setattr(self, recursion_attrib, True) + + try: + result = method(self) + + if self._is_frozen: + setattr(self, attrib_name, result) + + return result + finally: + delattr(self, recursion_attrib) + #@-node:_wrap_attrib + #@+node:_find_frozen + def _find_frozen(self, attrib_name, default=None): + value = self.__dict__.get(attrib_name) + if value is not None: + return value + + up = self.up + return up and up._find_frozen(attrib_name) or default + #@-node:_find_frozen + #@-node:Freezer Methods + #@+node:Calculation Methods + #@+node:__calc_performed_effort + def __calc_performed_effort(self): + if self.children: + return self._to_delta(sum([ t.performed_effort for t in self.children ])) + + return pcalendar.Minutes(0) + + performed_effort = _TaskProperty(__calc_performed_effort) + #@-node:__calc_performed_effort + #@+node:__calc_estimated_effort + def __calc_estimated_effort(self): + if self.children: + return self._to_delta(sum([ t.estimated_effort for t in self.children ])) + + return self.effort + + estimated_effort = _TaskProperty(__calc_estimated_effort) + #@-node:__calc_estimated_effort + #@+node:__calc_start + def __calc_start(self): + to_start = self._to_start + + if self.children: + try: + return min([ to_start(t.start) for t in self.children + if not t._is_parent_referer ]) + except ValueError: + #@ << raise child recursion error >> + #@+node:<< raise child recursion error >> + self._raise(RecursionError("A child defines a "\ + "recursive definition at %s" % self.path)) + #@-node:<< raise child recursion error >> + #@nl + + try: + end = self.end + duration = self.__dict__.get("duration") + if duration is not None: + start = end.to_datetime() - datetime.timedelta(minutes=duration) + else: + start = end - self.length + + return to_start(start) + + except RecursionError: + start = self._find_frozen("start") + if start: return to_start(start) + #@ << raise recursion error >> + #@+node:<< raise recursion error >> + raise RecursionError("you have to specify a "\ + "start or an end at %s" % self.path) + #@nonl + #@-node:<< raise recursion error >> + #@nl + + start = _TaskProperty(__calc_start) + + #@-node:__calc_start + #@+node:__calc_end + def __calc_end(self): + to_end = self._to_end + + if self.children: + try: + return max([ to_end(t.end) for t in self.children + if not t._is_parent_referer ]) + except ValueError: + #@ << raise child recursion error >> + #@+node:<< raise child recursion error >> + self._raise(RecursionError("A child defines a "\ + "recursive definition at %s" % self.path)) + #@-node:<< raise child recursion error >> + #@nl + + try: + start = self.start + duration = self.__dict__.get("duration") + if duration is not None: + end = start.to_datetime() + datetime.timedelta(minutes=duration) + else: + end = start + self.length + + return to_end(end) + + except RecursionError: + end = self._find_frozen("end") + if end: return to_end(end) + #@ << raise recursion error >> + #@+node:<< raise recursion error >> + raise RecursionError("you have to specify a "\ + "start or an end at %s" % self.path) + #@nonl + #@-node:<< raise recursion error >> + #@nl + + + end = _TaskProperty(__calc_end) + #@-node:__calc_end + #@+node:__calc_load + def __calc_load(self): + length = self.__dict__.get("length") + effort = self.__dict__.get("effort") + + if length is not None and effort is not None: + return float(effort) / (float(length) or 1.0) + + load = self._find_frozen("load") + if load is not None: return load + return 1.0 + + load = _TaskProperty(__calc_load) + #@-node:__calc_load + #@+node:__calc_length + def __calc_length(self): + effort = self.__dict__.get("effort") + if effort is None: + return self.end - self.start + + return self._to_delta(effort / self.load) + + length = _RoundingTaskProperty(__calc_length, "length") + #@-node:__calc_length + #@+node:__calc_duration + def __calc_duration(self): + return self._to_delta(self.end.to_datetime()\ + - self.start.to_datetime(), True) + + duration = _TaskProperty(__calc_duration) + #@-node:__calc_duration + #@+node:__calc_effort + def __calc_effort(self): + if self.children: + return self._to_delta(sum([ t.effort for t in self.children ])) + + return self._to_delta(self.length * self.load) + + effort = _RoundingTaskProperty(__calc_effort, "effort") + #@-node:__calc_effort + #@+node:__calc_done + def __calc_done(self): + if self.children: + dones = map(lambda t: t.done, self.children) + return self._to_delta(sum(dones)) + + res = self._iter_booked_resources() + done = sum(map(lambda r: r.done_of(self), res)) + + complete = self.__dict__.get("complete") + todo = self.__dict__.get("todo") + + if not done and complete == 100 or todo == 0: + #if now is not set + done = self.effort + + return self._to_delta(done) + + done = _TaskProperty(__calc_done) + #@-node:__calc_done + #@+node:__calc_buffer + def __calc_buffer(self): + if self.children: + return self._to_delta(min(map(lambda t: t.buffer, self.children))) + + scenario = self.scenario + end = self.end + old_end = self.__dict__.get("end") + + #@ << find all tasks, that depend on my end >> + #@+node:<< find all tasks, that depend on my end >> + deps = { } + task = self + while task: + deps.update(task._dependencies.get("end", {})) + task = task.up + #@nonl + #@-node:<< find all tasks, that depend on my end >> + #@nl + + #@ << define unfreeze_parents >> + #@+node:<< define unfreeze_parents >> + def unfreeze_parents(): + task = self.up + while task: + task._unfreeze("end") + task = task.up + #@nonl + #@-node:<< define unfreeze_parents >> + #@nl + + buffers = [ ] + for d in deps.keys(): + path, attrib = _split_path(d) + if attrib != "start": + continue + + #@ << calculate buffer to descendant 'd' >> + #@+node:<< calculate buffer to descendant 'd' >> + unfreeze_parents() + + # the following code considers a expressione like + # start = predecessor.end + Delta("1d") the buffer + # calculation must be aware of the 1d delay. + # (therefore a simple succ_start - end would be + # incorrect) + # Solution: Simluate a later end and calculate the + # real delay + + succ_task = self.get_task(path) + simulated_task = Task(succ_task._function, + succ_task.name, + succ_task.up, 1) + + current_start = succ_task.start + simulated_end = current_start + self.end = current_start + + simulated_task._generate() + simulated_start = simulated_task.start + + unfreeze_parents() + if old_end: self.end = old_end + else: self._unfreeze("end") + del simulated_task + + current_delay = current_start - end + simulated_delay = simulated_start - simulated_end + real_delay = current_delay - simulated_delay + try: + buffer_ = real_delay + succ_task.buffer + except RecursionError, err: + self._raise(err) + #@nonl + #@-node:<< calculate buffer to descendant 'd' >> + #@nl + + buffers.append(buffer_) + if not buffer_: + break + + if buffers: + return self._to_delta(min(buffers)) + + return not self.milestone \ + and self.root.end - end \ + or self._to_delta(0) + + buffer = _TaskProperty(__calc_buffer) + #@-node:__calc_buffer + #@+node:__calc_complete + def __calc_complete(self): + done = self.done + todo = self.todo + return int(100.0 * done / ((done + todo) or 1)) + + complete = _TaskProperty(__calc_complete) + #@-node:__calc_complete + #@+node:__calc_todo + def __calc_todo(self): + complete = self.__dict__.get("complete") + if complete: + # effort = done + todo + # done done + # complete = ------ ==> todo = -------- - done + # effort complete + complete = float(complete) + done = self.done + if done: + done = float(done) + return self._to_delta(done * 100.0 / complete - done) + return self._to_delta(self.effort * complete / 100.0) + + if self.children: + todos = map(lambda t: t.todo, self.children) + return self._to_delta(sum(todos)) + + todo = sum(map(lambda r: r.todo_of(self), self.booked_resource)) + return self._to_delta(max(todo, self.effort - self.done)) + + todo = _TaskProperty(__calc_todo) + #@-node:__calc_todo + #@-node:Calculation Methods + #@+node:Check Methods + #@+node:__check_task + def __check_task(self): + if self.children: return + + start = self._find_frozen("start") + end = self._find_frozen("end") + + if not (start or end): + self._raise(ValueError("You must specify either a"\ + " start or an end attribute")) + + if start and end: return + + length = self.__dict__.get("length") + duration = self.__dict__.get("duration") + effort = self.__dict__.get("effort") + if not (effort or length or duration): + #set a default value + self._set_effort("1d") + #self._raise(ValueError("You must specify either a"\ + # " length or a duration or "\ + # "an effort attribute")) + #@-node:__check_task + #@+node:__check_milestone + def __check_milestone(self): + if not self.milestone: return + + self.length = self._to_delta(0) + start = self.__dict__.get("start") + if not start: + self._raise(ValueError("Milestone must have start attribute"), + "milstone") + + if self.__start_class.__name__ == "edt": + #the milestone is probably dependent on the end date of + #an other task (see edt in pcalendar) ==> start at the end date + self.start = self.end = self._to_end(self.start) + else: + self.start = self.end = self._to_start(self.start) + + #@-node:__check_milestone + #@+node:_check_completion + def _check_completion(self): + if not self.performed_effort: return + if self.root.is_snapshot: return + + # allocation is not done yet ==> self.todo, self.done, + # self.complete cannot be calculated + if self._find_frozen("complete", 0) < 100 \ + and self.__dict__.get("todo", 1) > 0: + return + + start = self.performed_start + end = self.performed_end + #ensure that self.start.to_datetime() < self.end.to_datetime() + cstart = self._to_start(start) + if cstart.to_datetime() > start: cstart = self._to_end(start) + + cend = self._to_end(end) + if cend.to_datetime() < end: cend = self._to_start(end) + + self.start = cstart + self.end = cend + + if self.performed_effort != self.effort: + self.estimated_effort = self.effort + self.effort = self.performed_effort + #@-node:_check_completion + #@+node:check + def check(self): + if self._constraint and self._is_compiled: + globals_ = self._function.func_globals + globals_["me"] = self + globals_["up"] = self.up + globals_["root"] = self.root + globals_["assert_"] = self.__assert + self._constraint() + #@-node:check + #@-node:Check Methods + #@+node:Error Methods + #@+node:__assert + def __assert(self, value): + if not value: + warnings.warn('assertion in scenario: "%s"' % self.scenario, + RuntimeWarning, 2) + #@-node:__assert + #@+node:_warn + def _warn(self, message, attrib=None, level=2): + self.__compile_function([], True, _MeProxyWarn(self, attrib, message)) + #@-node:_warn + #@+node:_raise + def _raise(self, exc, attrib=None): + self.__compile_function([], True, _MeProxyError(self, attrib, exc)) + raise exc + #@-node:_raise + #@-node:Error Methods + #@-others +#@nonl +#@-node:class Task +#@-node:Task +#@+node:Projects +#@+node:class _ProjectBase +class _ProjectBase(Task): + """ + Base class for all projects. + """ + #@ << class _ProjectBase declarations >> + #@+node:<< class _ProjectBase declarations >> + __attrib_completions__ = { } + __attrib_completions__.update(Task.__attrib_completions__) + del __attrib_completions__["milestone"] #project cannot be milestones + + priority = 500 + efficiency = 1.0 + max_load = 1.0 + balance = 0 + resource = None + copy_src = None + has_actual_data = False + is_snapshot = False + + #@-node:<< class _ProjectBase declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, top_task, scenario="_default", id=""): + self.calendar = pcalendar.Calendar() + Task.__init__(self, top_task, top_task.func_name) + self.id = id or self.name + self.scenario = scenario + self.all_scenarios = set(("_default",)) + self.path = "root" + self._globals = top_task.func_globals.copy() + self._generate() + + #@-node:__init__ + #@+node:_idendity_ + def _idendity_(self): return self.id + #@-node:_idendity_ + #@+node:_restore_globals + def _restore_globals(self): + self._function.func_globals.clear() + self._function.func_globals.update(self._globals) + del self._globals + #@-node:_restore_globals + #@+node:free + def free(self): + all_resources = self.all_resources() + for r in all_resources: + r().unbook_tasks_of_project(self.id, self.scenario) + + for t in self: + t.booked_resource = () + + return all_resources + #@-node:free + #@+node:_get_balancing_list + def _get_balancing_list(self): + + try: + cached_list = balancing_cache[self._function.org_code] + if len(cached_list) != len(tuple(self)): + # different scenarios can have different tasks + raise KeyError() + + except KeyError: + cached_list = _build_balancing_list(self) + balancing_cache[self._function.org_code] = cached_list + else: + cached_list = [ self.get_task(t.path) for t in cached_list ] + + return cached_list + #@-node:_get_balancing_list + #@+node:snapshot + def snapshot(self, indent="", name=None): + text = Task.snapshot(self, indent, name) + + lines = text.splitlines(True) + indent += " " + + def make_resource(r): + return '%sclass %s(Resource): title = "%s"\n' \ + % (indent, r.name, r.title) + + now = datetime.datetime.now().strftime("%x %H:%M") + resource_text = map(lambda r: make_resource(r), self.all_resources()) + lines.insert(1, "%sfrom faces import Resource\n" % indent) + lines.insert(2, "".join(resource_text) + "\n") + lines.insert(3, '%snow = "%s"\n' % (indent, now)) + lines.insert(4, '%sis_snapshot = True\n' % indent) + return "".join(lines) + #@-node:snapshot + #@-others +#@-node:class _ProjectBase +#@+node:class Project +class Project(_ProjectBase): + """ + Generates a Project without allocating resources. + + @param top_task: Specifies the highest function of a project definiton. + + @param scenario: Specifies the name of the scenario which should be scheduled. + + @param id: Specifiess a unique idenfication name to distinguish the project from + other projects in the resource database. The default value for id + is the name of top_task. + """ + #@ << class Project declarations >> + #@+node:<< class Project declarations >> + __call_completion__ = 'Project(|top_task, scenario="_default", id=None)' + + #@-node:<< class Project declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, top_task, scenario="_default", id=None): + _ProjectBase.__init__(self, top_task, scenario, id) + no_snapshot = not self.is_snapshot + for t in self: + t._is_frozen = True + t._recalc_properties() + no_snapshot and t.check() + + self._restore_globals() + + #@-node:__init__ + #@-others + +#@-node:class Project +#@+node:class _AllocationPoject +class _AllocationPoject(_ProjectBase): + #@ @+others + #@+node:unfreeze_parents + def unfreeze_parents(self): + if self.has_actual_data: + for t in filter(lambda t: t.children, self): + if not t._original_values.has_key("start"): t._unfreeze("start") + if not t._original_values.has_key("end"): t._unfreeze("end") + #@-node:unfreeze_parents + #@-others +#@-node:class _AllocationPoject +#@+node:class BalancedProject +class BalancedProject(_AllocationPoject): + """ + Generates a project with allocated resources. The tasks are balanced + to fit the resources load conditions. + """ + #@ << class BalancedProject declarations >> + #@+node:<< class BalancedProject declarations >> + __call_completion__ = """BalancedProject(|top_task, scenario="_default", + id=None, balance=SMART, performed=None)""" + + #@-node:<< class BalancedProject declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, top_task, scenario="_default", + id=None, balance=SMART, performed=None): + _AllocationPoject.__init__(self, top_task, scenario, id) + self.balance = balance + if performed: + self._distribute_performed(performed) + self.has_actual_data = True + + no_snapshot = not self.is_snapshot + if no_snapshot: + self.allocate() + else: + self.allocate_snapshot() + + for t in self: + t._is_frozen = True + t._recalc_properties() + no_snapshot and t.check() + + self._restore_globals() + #@nonl + #@-node:__init__ + #@+node:allocate_snapshot + def allocate_snapshot(self): + all_resources = self.free() + scenario = self.scenario + has_actual_data = True + for t in self: + if not t.resource or t.milestone or t.children: + continue + + t._convert_performed(all_resources) + t._allocate_performed(t._performed) + #@-node:allocate_snapshot + #@+node:allocate + def allocate(self): + all_resources = self.free() + balancing_list = self._get_balancing_list() + scenario = self.scenario + + #for t in balancing_list: + # print t.path + + for t in balancing_list: + t._compile([], True) + + if not t.resource or t.milestone or t.children: + continue + + if t._convert_performed(all_resources): + has_actual_data = True + + try: + t._allocate_performed(t._performed) + except AttributeError: + pass + + allocator = _allocators[t.balance] + min_val = None + min_state = None + for p in range(t.resource._permutation_count()): + state = t._test_allocation(p, allocator) + + if not state: continue + + to_minimize = state[0] + if not min_val or min_val > to_minimize: + min_val = to_minimize + min_state = state + + if min_state: + t._allocate(min_state, allocator) + elif t.performed_start: + # t could not be allocated ==> + # performance data holds all information + t.start = t._to_start(t.performed_start) + t.end = t._to_end(t.performed_end) + + self.unfreeze_parents() + #@-node:allocate + #@+node:_distribute_performed + def _distribute_performed(self, performed): + project_id = self._idendity_() + plen = len(project_id) + + performed = filter(lambda item: item[0].startswith(project_id), + performed) + performed.sort() + + task = None + for item in performed: + path = item[0] + rpath = "root" + path[plen:] + task = self.get_task(rpath) + + if not task: + #@ << extract task in activity path >> + #@+node:<< extract task in activity path >> + #@+at + # A performed path can have sub activities appended to the + # task path. + # like: + # + # root.parent1.parent2.task.subactivity + # + # here rhe correct task path is: + # + # root.parent1.parent2.task + # + #@-at + #@@code + orpath = rpath + while not task: + #path can specify a sub module + #find the correct path to the module + try: + last_dot = rpath.rindex(".", 0, len(rpath)) + except ValueError: + break + + rpath = rpath[:last_dot] + task = self.get_task(rpath) + + item = list(item) + item.append(orpath[len(rpath):]) + #@nonl + #@-node:<< extract task in activity path >> + #@nl + + if not task or task.children: + self._warn("The performance data contain " + "a task with id '%s'. But such " + "a task does not exist in your " + "project." % path) + continue + + if not isinstance(task.performed, list): + task.performed = list(task.performed) + + task.performed.append(item[1:]) + #@nonl + #@-node:_distribute_performed + #@-others +#@-node:class BalancedProject +#@+node:class AdjustedProject +class AdjustedProject(_AllocationPoject): + """ + Generates a project with allocated resources. The tasks are + adjusted to the actual tracking data and balanced to fit the + resources load conditions. + """ + #@ << class AdjustedProject declarations >> + #@+node:<< class AdjustedProject declarations >> + __call_completion__ = 'AdjustedProject(|base_project)' + + #@-node:<< class AdjustedProject declarations >> + #@nl + #@ @+others + #@+node:__init__ + def __init__(self, base_project): + _AllocationPoject.__init__(self, base_project._function, + base_project.scenario, + base_project.id) + + self.balance = base_project.balance + self.has_actual_data = base_project.has_actual_data + self.allocate(base_project) + for t in self: + t._is_frozen = True + t._recalc_properties() + t.check() + + self._restore_globals() + + + #@-node:__init__ + #@+node:allocate + def allocate(self, base): + balancing_list = self._get_balancing_list() + scenario = self.scenario + cal = self.calendar + now = cal.now + + #for t in balancing_list: + # print t.path + + #@ << free the resources, we have to rebook >> + #@+node:<< free the resources, we have to rebook >> + for t in balancing_list: + src = base.get_task(t.path) + if src.end > now or src.complete < 100: + for r in src._iter_booked_resources(): + r.unbook_task(src) + #@nonl + #@-node:<< free the resources, we have to rebook >> + #@nl + + for t in balancing_list: + src = base.get_task(t.path) + + if src.end <= now and src.complete == 100: + #@ << copy the attribs of complete tasks >> + #@+node:<< copy the attribs of complete tasks >> + t.effort = src.effort + t.load = src.load + t.start = src.start + t.end = src.end + t.done = src.done + t.todo = src.todo + t.booked_resource = src.booked_resource + t.performed_resource = src.performed_resource + t._unfreeze("length") + t._unfreeze("duration") + #@nonl + #@-node:<< copy the attribs of complete tasks >> + #@nl + continue + + t._compile([], True) + if not t.resource or t.milestone or t.children: + continue + + # now allocate the uncomplete tasks + #@ << allocate performed data >> + #@+node:<< allocate performed data >> + try: + t._performed = src._performed + t._allocate_performed(t._performed) + except AttributeError: + pass + #@nonl + #@-node:<< allocate performed data >> + #@nl + allocator = _allocators[t.balance] + + if src.start >= now: + #@ << allocate tasks, that have not begun yet >> + #@+node:<< allocate tasks, that have not begun yet >> + min_val = None + min_state = None + for p in range(t.resource._permutation_count()): + state = t._test_allocation(p, allocator) + if not state: continue + + to_minimize = state[0] + if not min_val or min_val > to_minimize: + min_val = to_minimize + min_state = state + + if min_state: + t._allocate(min_state, allocator) + elif t.performed_start: + t.start = t._to_start(t.performed_start) + t.end = t._to_end(t.performed_end) + #@-node:<< allocate tasks, that have not begun yet >> + #@nl + else: + #@ << allocate tasks, that are allready at work >> + #@+node:<< allocate tasks, that are allready at work >> + if t.__dict__.has_key("effort"): + t.effort = t._to_delta(src.done + src.todo).round() + + resource = src.booked_resource or src.performed_resource + state = allocator.test_allocation(t, resource) + if state: + t._allocate(state, allocator) + #@nonl + #@-node:<< allocate tasks, that are allready at work >> + #@nl + + self.unfreeze_parents() + #@nonl + #@-node:allocate + #@-others +#@-node:class AdjustedProject +#@-node:Projects +#@-others + + +""" + Atttribute mit Bedeutung: + + calendar + -------- + minimum_time_unit |int in minutes| + working_days_per_week |int in days | + working_days_per_month|int in days | + working_days_per_year |int in days | + working_hours_per_day |int in hours | + vacation | [ one_day, (from, to), .. ] | + working_days + now + + + + Task + ----- + load + start + end + length + effort + duration + resource + booked_resource + + milestone + complete + done + todo + priority + efficiency + buffer + + children + depth + index + path + dont_inherit + + performed_effort + performed_end + performed_start + + sum() + min() + max() + costs() + indent_name() + max_load + + copy_src (set: copy all attributes of another task + get: reference of copy) + + balance + + for gantt + ----- + line + accumulate + + + + + Resource + ---------- + efficiency + load + vacation + max_load + +""" +#@-node:@file task.py +#@-leo diff --git a/addons/resource/faces/timescale.py b/addons/resource/faces/timescale.py new file mode 100644 index 00000000000..215766eb958 --- /dev/null +++ b/addons/resource/faces/timescale.py @@ -0,0 +1,113 @@ +############################################################################ +# 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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################ + +import faces.pcalendar as pcal +import matplotlib.cbook as cbook +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(cbook.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) diff --git a/addons/resource/faces/utils.py b/addons/resource/faces/utils.py new file mode 100644 index 00000000000..f7d61fb1dc9 --- /dev/null +++ b/addons/resource/faces/utils.py @@ -0,0 +1,124 @@ +############################################################################ +# 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., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 + + + From afacda2c1dc8e7495aa88496b9db7c3ac70bf127 Mon Sep 17 00:00:00 2001 From: "Rvo (Open ERP)" Date: Fri, 29 Jan 2010 13:19:41 +0530 Subject: [PATCH 07/30] [FIX] Few changes according to working calendar bzr revid: rvo@tinyerp.co.in-20100129074941-ng1blzxb7ih7f4ct --- addons/project/wizard/compute_tasks_date.py | 42 ++++++++++++--------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/addons/project/wizard/compute_tasks_date.py b/addons/project/wizard/compute_tasks_date.py index 89a062a48bd..b7134544f69 100644 --- a/addons/project/wizard/compute_tasks_date.py +++ b/addons/project/wizard/compute_tasks_date.py @@ -23,7 +23,7 @@ import wizard import pooler from tools.translate import _ import datetime -from resource.faces_new import * +from resource.faces import * compute_form = """
@@ -66,25 +66,29 @@ class wizard_compute_tasks(wizard.interface): else: date_dt = datetime.datetime.strptime(project.date_start,"%Y-%m-%d") print 'Start Date of Project:::',date_dt - pdt = datetime.datetime.strftime(date_dt,"%Y-%m-%d %H:%M") - - class user(Resource): - pass + dt = datetime.datetime.strftime(date_dt,"%Y-%m-%d %H:%M") for i in range(len(task_obj)): final_lst = [] leaves = [] if task_obj[i].user_id.id: resource_id = pooler.get_pool(cr.dbname).get('resource.resource').search(cr,uid,[('user_id','=',task_obj[i].user_id.id)]) - resource_obj = pooler.get_pool(cr.dbname).get('resource.resource').browse(cr,uid,resource_id)[0] - if resource_obj.calendar_id: + if resource_id : + resource_obj = pooler.get_pool(cr.dbname).get('resource.resource').browse(cr,uid,resource_id)[0] + if resource_obj.calendar_id: + calendar_id = resource_obj.calendar_id.id + resource_leave_ids = pooler.get_pool(cr.dbname).get('resource.calendar.leaves').search(cr,uid,[('resource_id','=',resource_id)]) + else: + calendar_id = project.resource_calendar_id.id + resource_leave_ids = pooler.get_pool(cr.dbname).get('resource.calendar.leaves').search(cr,uid,[('calendar_id','=',project.resource_calendar_id.id)]) + time_range = "8:00-8:00" - b = "" + non_working = "" wk = {"0":"mon","1":"tue","2":"wed","3":"thu","4":"fri"} wk_days = {} wk_time = {} tlist = [] - week_ids = pooler.get_pool(cr.dbname).get('resource.calendar.week').search(cr,uid,[('calendar_id','=',resource_obj.calendar_id.id)]) + week_ids = pooler.get_pool(cr.dbname).get('resource.calendar.week').search(cr,uid,[('calendar_id','=',calendar_id)]) week_obj = pooler.get_pool(cr.dbname).get('resource.calendar.week').read(cr,uid,week_ids,['dayofweek','hour_from','hour_to']) for week in week_obj: @@ -113,11 +117,11 @@ class wizard_compute_tasks(wizard.interface): if wk.has_key(k): wk.pop(k) for v in wk.itervalues(): - b += v + ',' - final_lst.append((b[:-1],time_range)) - print 'Final Tlist:::',tlist + non_working += v + ',' + if non_working: + final_lst.append((non_working[:-1],time_range)) + print 'Final list After Adding Non-Working:::',final_lst - resource_leave_ids = pooler.get_pool(cr.dbname).get('resource.calendar.leaves').search(cr,uid,[('resource_id','=',resource_id)]) if resource_leave_ids: res_leaves = pooler.get_pool(cr.dbname).get('resource.calendar.leaves').read(cr,uid,resource_leave_ids,['date_from','date_to']) for leave in range(len(res_leaves)): @@ -133,16 +137,20 @@ class wizard_compute_tasks(wizard.interface): hours = task_obj[i].remaining_hours / task_obj[i].occupation_rate hours = str(hours)[:-2] + 'H' print 'Hours:::',hours + + class user(Resource): + pass + if i == 0: - ndt = pdt + new_dt = dt else: data = pooler.get_pool(cr.dbname).get('project.task').read(cr,uid,[task_obj[i-1].id],['date_end'])[0] - ndt = data['date_end'][0:16] + new_dt = data['date_end'][0:16] def Project_1(): resource = user title = project.name - start = ndt + start = new_dt effort = hours if final_lst or leaves: @@ -150,7 +158,7 @@ class wizard_compute_tasks(wizard.interface): vacation = tuple(leaves) def task1(): - start = ndt + start = new_dt effort = hours title = task_obj[i].name From 2c4717e7748dad44f080dc95b075920782c092c2 Mon Sep 17 00:00:00 2001 From: "Rvo (Open ERP)" Date: Fri, 29 Jan 2010 13:21:40 +0530 Subject: [PATCH 08/30] [ADD] Scheduler on phases on project_long_term module bzr revid: rvo@tinyerp.co.in-20100129075140-adrzwvvb18a3f2kl --- addons/project_long_term/__init__.py | 1 + addons/project_long_term/__terp__.py | 3 +- addons/project_long_term/project_wizard.xml | 12 +- addons/project_long_term/wizard/__init__.py | 25 +++ .../wizard/compute_phases_date.py | 202 ++++++++++++++++++ 5 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 addons/project_long_term/wizard/__init__.py create mode 100644 addons/project_long_term/wizard/compute_phases_date.py diff --git a/addons/project_long_term/__init__.py b/addons/project_long_term/__init__.py index 33adeb520b9..5a6ad27710f 100644 --- a/addons/project_long_term/__init__.py +++ b/addons/project_long_term/__init__.py @@ -20,6 +20,7 @@ ############################################################################## import project +import wizard # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/project_long_term/__terp__.py b/addons/project_long_term/__terp__.py index 1b5dfa348ad..23a1b2a6297 100644 --- a/addons/project_long_term/__terp__.py +++ b/addons/project_long_term/__terp__.py @@ -33,7 +33,8 @@ "demo_xml" : [#"project_demo.xml" ], "update_xml": [ - "project_view.xml", + "project_view.xml", + "project_wizard.xml" ], 'installable': True, 'active': False, diff --git a/addons/project_long_term/project_wizard.xml b/addons/project_long_term/project_wizard.xml index 109a003da4a..5fb0b101fa6 100644 --- a/addons/project_long_term/project_wizard.xml +++ b/addons/project_long_term/project_wizard.xml @@ -1,8 +1,16 @@ - + + + + diff --git a/addons/project_long_term/wizard/__init__.py b/addons/project_long_term/wizard/__init__.py new file mode 100644 index 00000000000..a38c9720ebc --- /dev/null +++ b/addons/project_long_term/wizard/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import compute_phases_date + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/project_long_term/wizard/compute_phases_date.py b/addons/project_long_term/wizard/compute_phases_date.py new file mode 100644 index 00000000000..c8838d00b3d --- /dev/null +++ b/addons/project_long_term/wizard/compute_phases_date.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import wizard +import pooler +from tools.translate import _ +import datetime +from resource.faces import * + +compute_form = """ + + + + +""" + +compute_fields = { + 'project_id': {'string':'Project', 'type':'many2one', 'relation': 'project.project', 'required':'True'}, + +} +def timeformat_convert(cr, uid, time_string, context={}): + strg = str(time_string)[-2:] + last_strg = str(time_string)[0:-2] + if strg != '.0': + if '.' not in strg: + strg = '.' + strg + last_strg = str(time_string)[:-3] + + new_strg = round(float(strg) / 0.016666667) + converted_strg = last_strg + ':' + str(new_strg)[:-2] + else: + converted_strg = last_strg + ':00' + + return converted_strg + +class wizard_compute_phases(wizard.interface): + + def _compute_date(self, cr, uid, data, context): + project_id = data['form']['project_id'] + project = pooler.get_pool(cr.dbname).get('project.project').browse(cr,uid,project_id) + phase_ids = pooler.get_pool(cr.dbname).get('project.phase').search(cr,uid,[('project_id','=',project_id)]) + if phase_ids: + phase_obj = pooler.get_pool(cr.dbname).get('project.phase').browse(cr,uid,phase_ids) + phase_1 = phase_obj[0] + if phase_1.date_start: + date_dt = datetime.datetime.strptime(phase_1.date_start,"%Y-%m-%d %H:%M:%S") + else: + date_dt = datetime.datetime.strptime(project.date_start,"%Y-%m-%d") + dt = datetime.datetime.strftime(date_dt,"%Y-%m-%d %H:%M") + + for i in range(len(phase_obj)): + final_lst = [] + leaves = [] +# if phase_obj[i].user_id.id: +# resource_id = pooler.get_pool(cr.dbname).get('resource.resource').search(cr,uid,[('user_id','=',task_obj[i].user_id.id)]) +# resource_obj = pooler.get_pool(cr.dbname).get('resource.resource').browse(cr,uid,resource_id)[0] + if project.resource_calendar_id: + non_working = "" + time_range = "8:00-8:00" + wk = {"0":"mon","1":"tue","2":"wed","3":"thu","4":"fri"} + wk_days = {} + wk_time = {} + tlist = [] + hours = [] + hr = 0 + + week_ids = pooler.get_pool(cr.dbname).get('resource.calendar.week').search(cr,uid,[('calendar_id','=',project.resource_calendar_id.id)]) + week_obj = pooler.get_pool(cr.dbname).get('resource.calendar.week').read(cr,uid,week_ids,['dayofweek','hour_from','hour_to']) + + for week in week_obj: + res_str = "" + if wk.has_key(week['dayofweek']): + day = wk[week['dayofweek']] + wk_days[week['dayofweek']] = wk[week['dayofweek']] + + hour_from_str = timeformat_convert(cr,uid,week['hour_from']) + hour_to_str = timeformat_convert(cr,uid,week['hour_to']) + hours.append(week['hour_from']) + hours.append(week['hour_to']) + res_str = hour_from_str + '-' + hour_to_str + tlist.append((day,res_str)) + + for item in tlist: + 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(): + final_lst.append(tuple(v)) + print 'final Dictionary List:::',final_lst + + for k,v in wk_days.items(): + if wk.has_key(k): + wk.pop(k) + for v in wk.itervalues(): + non_working += v + ',' + if non_working: + final_lst.append((non_working[:-1],time_range)) + print 'Final list After Adding Non-Working:::',final_lst + + leave_ids = pooler.get_pool(cr.dbname).get('resource.calendar.leaves').search(cr,uid,[('calendar_id','=',project.resource_calendar_id.id)]) + if leave_ids: + res_leaves = pooler.get_pool(cr.dbname).get('resource.calendar.leaves').read(cr,uid,leave_ids,['date_from','date_to']) + for leave in range(len(res_leaves)): + dt_start = datetime.datetime.strptime(res_leaves[leave]['date_from'],'%Y-%m-%d %H:%M:%S') + dt_end = datetime.datetime.strptime(res_leaves[leave]['date_to'],'%Y-%m-%d %H:%M:%S') + no = dt_end - dt_start + leave_days = no.days + 1 + [leaves.append((dt_start + datetime.timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(leave_days))] + leaves.sort() + print 'Dt_leavess::',leaves,tuple(leaves) + + for hour in range(0,4): + if hour%2 ==0: + hr += float(hours[hour+1]) - float(hours[hour]) + + man_days = str(phase_obj[i].duration * hr)[:-2] + 'H' + print 'Man-Days:::',man_days + + if i == 0: + new_dt = dt + else: + data = pooler.get_pool(cr.dbname).get('project.phase').read(cr,uid,[phase_obj[i-1].id],['date_end'])[0] + if phase_obj[i].constraint_date_start and data['date_end'] < phase_obj[i].constraint_date_start: + new_dt = phase_obj[i].constraint_date_start[0:16] + else: + new_dt = data['date_end'][0:16] + + class user(Resource): + pass + + def Project_1(): + resource = user + title = project.name + start = new_dt + effort = man_days + + if final_lst or leaves: + working_days = final_lst + vacation = tuple(leaves) + + def phase1(): + start = new_dt + effort = man_days + title = phase_obj[i].name + + project_1 = BalancedProject(Project_1) + start_date = project_1.calendar.WorkingDate(project_1.phase1.start).to_datetime() + e_date = project_1.calendar.WorkingDate(project_1.phase1.end).to_datetime() + if phase_obj[i].constraint_date_end and str(e_date) > phase_obj[i].constraint_date_end: + end_date = phase_obj[i].constraint_date_end + else: + end_date = e_date + + print 'Start Date::::',start_date,phase_obj[i].name + print 'End Date::::',end_date,phase_obj[i].name + pooler.get_pool(cr.dbname).get('project.phase').write(cr,uid,[phase_obj[i].id],{'date_start':start_date,'date_end':end_date}) + + return {} + states = { + 'init': { + 'actions': [], + 'result': {'type':'form', 'arch':compute_form, 'fields':compute_fields, 'state':[ + ('end', 'Cancel'), + ('ok', 'Ok') + + ]}, + }, + 'ok': { + 'actions': [_compute_date], + 'result': {'type':'state', 'state':'end'}, + } + } +wizard_compute_phases('wizard.compute.phases') + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + From 99bca6d3ac621fcbacfdd3ea5708cf1cd95b1711 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Fri, 29 Jan 2010 08:55:16 +0100 Subject: [PATCH 09/30] merge bzr revid: fp@tinyerp.com-20100129075516-yy6bb2pei0kj0utn --- bin/service/security.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/bin/service/security.py b/bin/service/security.py index 3ecbea73094..c74ed2c7aba 100644 --- a/bin/service/security.py +++ b/bin/service/security.py @@ -44,12 +44,3 @@ def check(db, uid, passwd): pool = pooler.get_pool(db) user_obj = pool.get('res.users') return user_obj.check(db, uid, passwd) - -def access(db, uid, passwd, sec_level, ids): - pool = pooler.get_pool(db) - user_obj = pool.get('res.users') - return user_obj.access(db, uid, passwd, sec_level, ids) - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - From 19fb25cd3f3367d85cfd27abee135842c165e176 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Fri, 29 Jan 2010 09:00:35 +0100 Subject: [PATCH 10/30] [IMP] type bzr revid: fp@tinyerp.com-20100129080035-o3tmadogl1evxru0 --- addons/users_ldap/users_ldap.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/users_ldap/users_ldap.py b/addons/users_ldap/users_ldap.py index 17919103fc3..da4364d069c 100644 --- a/addons/users_ldap/users_ldap.py +++ b/addons/users_ldap/users_ldap.py @@ -46,7 +46,7 @@ class CompanyLDAP(osv.osv): 'ldap_password': fields.char('LDAP password', size=64, required=True), 'ldap_filter': fields.char('LDAP filter', size=64, required=True), 'ldap_base': fields.char('LDAP base', size=64, required=True), - 'user': fields.many2one('res.users', 'Model user', + 'user': fields.many2one('res.users', 'Model User', help="Model used for user creation"), 'create_user': fields.boolean('Create user', help="Create the user if not in database"), @@ -63,7 +63,6 @@ CompanyLDAP() class res_company(osv.osv): _inherit = "res.company" - _columns = { 'ldaps': fields.one2many('res.company.ldap', 'company', 'LDAP Parameters'), } From 75865c816159bbb5f689ed9774ca1c6e8e61d603 Mon Sep 17 00:00:00 2001 From: apa-tiny Date: Fri, 29 Jan 2010 13:59:16 +0530 Subject: [PATCH 11/30] [IMP]:Survey:Improved the respons code.Ref.YSA bzr revid: apa@tinyerp.com-20100129082916-8ofo6xlrl36mvykf --- addons/survey/survey.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/addons/survey/survey.py b/addons/survey/survey.py index b30297ff6c0..c6a4a1daf2b 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -932,6 +932,9 @@ class survey_question_wiz(osv.osv_memory): surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' : tools.ustr(response_id)}) else: response_id = int(sur_name_read['response']) + if response_id not in surv_all_resp_obj.search(cr, uid, []): + response_id = surv_all_resp_obj.create(cr, uid, {'response_type':'link', 'user_id':uid, 'date_create':datetime.datetime.now(), 'survey_id' : context['survey_id']}) + surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' : tools.ustr(response_id)}) for key,val in sur_name_read['store_ans'].items(): for field in vals: if field.split('_')[0] == val['question_id']: @@ -1013,7 +1016,6 @@ class survey_question_wiz(osv.osv_memory): if error: for res in resp_id_list: sur_name_read['store_ans'].pop(res) - surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' :0}) raise osv.except_osv(_('Error !'), _("'" + que_rec['question'] + "' \n" + tools.ustr(que_rec['comment_valid_err_msg']))) resp_obj.write(cr, uid, resp_id, {'comment':val1}) sur_name_read['store_ans'][resp_id].update({key1:val1}) @@ -1050,7 +1052,6 @@ class survey_question_wiz(osv.osv_memory): if error: for res in resp_id_list: sur_name_read['store_ans'].pop(res) - surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' :0}) raise osv.except_osv(_('Error !'), _("'" + que_rec['question'] + "' \n" + tools.ustr(que_rec['validation_valid_err_msg']))) if key1.split('_')[1] == "single" : resp_obj.write(cr, uid, resp_id, {'single_text':val1}) @@ -1086,19 +1087,16 @@ class survey_question_wiz(osv.osv_memory): if comment_field and comment_value: for res in resp_id_list: sur_name_read['store_ans'].pop(res) - surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' :0}) raise osv.except_osv(_('Error re !'), _("'" + que_rec['question'] + "' " + tools.ustr(que_rec['make_comment_field_err_msg']))) if que_rec['type'] == "rating_scale" and que_rec['rating_allow_one_column_require'] and len(selected_value) > len(list(set(selected_value))): for res in resp_id_list: sur_name_read['store_ans'].pop(res) - surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' :0}) raise osv.except_osv(_('Error re !'), _("'" + que_rec['question'] + "\n you cannot select same answer more than one times'")) if not select_count: resp_obj.write(cr, uid, resp_id, {'state':'skip'}) if que_rec['numeric_required_sum'] and numeric_sum > que_rec['numeric_required_sum']: for res in resp_id_list: sur_name_read['store_ans'].pop(res) - surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' :0}) raise osv.except_osv(_('Error re !'), _("'" + que_rec['question'] + "' " + tools.ustr(que_rec['numeric_required_sum_err_msg']))) if que_rec['type'] in ['multiple_choice_multiple_ans','matrix_of_choices_only_one_ans','matrix_of_choices_only_multi_ans','matrix_of_drop_down_menus','rating_scale','multiple_textboxes','numerical_textboxes','date','date_and_time'] and que_rec['is_require_answer']: if matrix_list: @@ -1109,7 +1107,6 @@ class survey_question_wiz(osv.osv_memory): (que_rec['required_type'] == 'a range' and (len(list(set(matrix_list))) < que_rec['minimum_req_ans'] or len(list(set(matrix_list))) > que_rec['maximum_req_ans'])): for res in resp_id_list: sur_name_read['store_ans'].pop(res) - surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' :0}) raise osv.except_osv(_('Error !'), _("'" + que_rec['question'] + "' " + tools.ustr(que_rec['req_error_msg']))) elif (que_rec['required_type'] == 'all' and select_count < len(que_rec['answer_choice_ids'])) or \ (que_rec['required_type'] == 'at least' and select_count < que_rec['req_ans']) or \ @@ -1118,12 +1115,10 @@ class survey_question_wiz(osv.osv_memory): (que_rec['required_type'] == 'a range' and (select_count < que_rec['minimum_req_ans'] or select_count > que_rec['maximum_req_ans'])): for res in resp_id_list: sur_name_read['store_ans'].pop(res) - surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' :0}) raise osv.except_osv(_('Error !'), _("'" + que_rec['question'] + "' " + tools.ustr(que_rec['req_error_msg']))) if que_rec['type'] in ['multiple_choice_only_one_ans','single_textbox','comment'] and que_rec['is_require_answer'] and select_count <= 0: for res in resp_id_list: sur_name_read['store_ans'].pop(res) - surv_name_wiz.write(cr, uid, [context['sur_name_id']], {'response' :0}) raise osv.except_osv(_('Error re !'), _("'" + que_rec['question'] + "' " + tools.ustr(que_rec['req_error_msg']))) else: resp_id_list = [] From cb307f9213d43e97c53b6dd4559a3dbf45f104e7 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Fri, 29 Jan 2010 11:17:12 +0100 Subject: [PATCH 12/30] [IMP] rename Service.service_exist to Service.exits (classmethod) bzr revid: chs@tinyerp.com-20100129101712-ok7fd5y0hnow5fds --- bin/netsvc.py | 9 +++++---- bin/report/interface.py | 2 +- bin/wizard/__init__.py | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bin/netsvc.py b/bin/netsvc.py index 1cc39ef2edb..0961aa20366 100644 --- a/bin/netsvc.py +++ b/bin/netsvc.py @@ -38,8 +38,8 @@ import xmlrpclib import release class Service(object): - """ Base class for *Local* services - + """ Base class for *Local* services + Functionality here is trusted, no authentication. """ _services = {} @@ -52,8 +52,9 @@ class Service(object): raise Exception("No group for local services") #GROUPS.setdefault(name, {})[self.__name] = self - def service_exist(self,name): - return Service._services.has_key(name) + @classmethod + def exists(cls, name): + return name in cls._services def exportMethod(self, method): if callable(method): diff --git a/bin/report/interface.py b/bin/report/interface.py index 5ae8fb021c2..9f67bdd6de1 100644 --- a/bin/report/interface.py +++ b/bin/report/interface.py @@ -46,7 +46,7 @@ def toxml(val): class report_int(netsvc.Service): def __init__(self, name, audience='*'): - assert not self.service_exist(name), 'The report "%s" already exist!' % name + assert not self.exists(name), 'The report "%s" already exists!' % name super(report_int, self).__init__(name, audience) if name[0:7]<>'report.': raise Exception, 'ConceptionError, bad report name, should start with "report."' diff --git a/bin/wizard/__init__.py b/bin/wizard/__init__.py index 72811589300..9e9f99397f4 100644 --- a/bin/wizard/__init__.py +++ b/bin/wizard/__init__.py @@ -41,13 +41,13 @@ class except_wizard(Exception): class interface(netsvc.Service): states = {} - + def __init__(self, name): - assert not self.service_exist('wizard.'+name), 'The wizard "%s" already exists!'%name + assert not self.exists('wizard.'+name), 'The wizard "%s" already exists!' % (name,) super(interface, self).__init__('wizard.'+name) self.exportMethod(self.execute) self.wiz_name = name - + def translate_view(self, cr, node, state, lang): if node.get('string'): trans = translate(cr, self.wiz_name+','+state, 'wizard_view', lang, node.get('string').encode('utf8')) From 52e8a6744633a9f547ea4a82949be398b6cab51b Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Fri, 29 Jan 2010 11:28:29 +0100 Subject: [PATCH 13/30] [IMP] add classmethod to remove a service bzr revid: chs@tinyerp.com-20100129102829-i68neaqgoxqkw6ud --- bin/netsvc.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/netsvc.py b/bin/netsvc.py index 0961aa20366..b47faba823e 100644 --- a/bin/netsvc.py +++ b/bin/netsvc.py @@ -56,6 +56,11 @@ class Service(object): def exists(cls, name): return name in cls._services + @classmethod + def remove(cls, name): + if cls.exits(name): + cls._services.pop(name) + def exportMethod(self, method): if callable(method): self._methods[method.__name__] = method From 97d7f15ab8ca6583130f80a66f24c551e2b4bf5d Mon Sep 17 00:00:00 2001 From: "rpa (Open ERP)" Date: Fri, 29 Jan 2010 16:09:44 +0530 Subject: [PATCH 14/30] [IMP]: base_calendar, caldav: Moved unmapped fields to its proper modules * Put proper validation for duplicate field mapping bzr revid: rpa@openerp.co.in-20100129103944-vjep376dv02oftwx --- addons/base_calendar/base_calendar.py | 57 +++++++++++++------- addons/base_calendar/base_calendar_view.xml | 2 +- addons/caldav/caldav_data.xml | 58 +-------------------- addons/crm/crm_meeting_data.xml | 4 +- 4 files changed, 42 insertions(+), 79 deletions(-) diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index 168da55eaf7..71117227fa9 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -194,9 +194,7 @@ class CalDAV(object): def create_ics(self, cr, uid, datas, name, ical, context=None): if not datas: - model = context.get('model', None) - war_str = "No data available" + (model and " for " + model) or "" - raise osv.except_osv(_('Warning !'), _(war_str)) + return for data in datas: vevent = ical.add(name) for field in self.__attribute__.keys(): @@ -333,21 +331,18 @@ class Calendar(CalDAV, osv.osv): } def export_cal(self, cr, uid, datas, vobj='vevent', context={}): - try: - cal = self.browse(cr, uid, datas[0]) - ical = vobject.iCalendar() - for line in cal.line_ids: - if line.name in ('alarm', 'attendee'): - continue - mod_obj = self.pool.get(line.object_id.model) - data_ids = mod_obj.search(cr, uid, eval(line.domain), context=context) - datas = mod_obj.read(cr, uid, data_ids, context=context) - context.update({'model': line.object_id.model}) - self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context) - self.create_ics(cr, uid, datas, line.name, ical, context=context) - return ical.serialize() - except Exception, e: - raise osv.except_osv(('Error !'), (str(e))) + cal = self.browse(cr, uid, datas[0]) + ical = vobject.iCalendar() + for line in cal.line_ids: + if line.name in ('valarm', 'attendee'): + continue + mod_obj = self.pool.get(line.object_id.model) + data_ids = mod_obj.search(cr, uid, eval(line.domain), context=context) + datas = mod_obj.read(cr, uid, data_ids, context=context) + context.update({'model': line.object_id.model}) + self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context) + self.create_ics(cr, uid, datas, line.name, ical, context=context) + return ical.serialize() def import_cal(self, cr, uid, content, data_id=None, context=None): ical_data = base64.decodestring(content) @@ -378,7 +373,7 @@ class basic_calendar_line(osv.osv): _description = 'Calendar Lines' _columns = { 'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \ - ('alarm', 'Alarm'), \ + ('valarm', 'Alarm'), \ ('attendee', 'Attendee')], \ string="Type", size=64), 'object_id': fields.many2one('ir.model', 'Object'), @@ -428,6 +423,30 @@ class basic_calendar_fields(osv.osv): 'fn': lambda *a: 'field', } + def create(self, cr, uid, vals, context={}): + cr.execute('select name from basic_calendar_attributes \ + where id=%s' % (vals.get('name'))) + name = cr.fetchone() + name = name[0] + if name in ('valarm', 'attendee'): + f_obj = self.pool.get('ir.model.fields') + field = f_obj.browse(cr, uid, vals['field_id'], context=context) + relation = field.relation + line_obj = self.pool.get('basic.calendar.lines') + l_id = line_obj.search(cr, uid, [('name', '=', name)]) + if l_id: + line = line_obj.browse(cr, uid, l_id, context=context)[0] + line_rel = line.object_id.model + if (relation != 'NULL') and (not relation == line_rel): + raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name))) + cr.execute("Select count(id) from basic_calendar_fields \ + where name=%s and type_id=%s" % (vals.get('name'), vals.get('type_id'))) + res = cr.fetchone() + if res: + if res[0] > 0: + raise osv.except_osv(_('Warning !'), _('Can not map same field more than once')) + return super(basic_calendar_fields, self).create(cr, uid, vals, context=context) + basic_calendar_fields() class Event(CalDAV, osv.osv_memory): diff --git a/addons/base_calendar/base_calendar_view.xml b/addons/base_calendar/base_calendar_view.xml index 0165e483f53..f5b4a274a95 100644 --- a/addons/base_calendar/base_calendar_view.xml +++ b/addons/base_calendar/base_calendar_view.xml @@ -19,7 +19,7 @@ - +
diff --git a/addons/caldav/caldav_data.xml b/addons/caldav/caldav_data.xml index b909bf1dc82..ae380e8f0d7 100644 --- a/addons/caldav/caldav_data.xml +++ b/addons/caldav/caldav_data.xml @@ -143,7 +143,7 @@ - alarm + valarm @@ -170,13 +170,6 @@ field - - - - - field - - @@ -198,13 +191,6 @@ field - - - - - field - - @@ -219,13 +205,6 @@ field - - - - - field - - @@ -284,13 +263,6 @@ - - - - - field - - @@ -298,13 +270,6 @@ field - - - - - field - - @@ -333,13 +298,6 @@ field - - - - - field - - @@ -347,13 +305,6 @@ field - - - - - field - - @@ -368,13 +319,6 @@ field - - - - - hours - - diff --git a/addons/crm/crm_meeting_data.xml b/addons/crm/crm_meeting_data.xml index e1c6a9cadfb..789708c6a35 100644 --- a/addons/crm/crm_meeting_data.xml +++ b/addons/crm/crm_meeting_data.xml @@ -44,14 +44,14 @@ - + field - + From cc4f5d21b2a6a094fad280cd29fc12ddcd785cf7 Mon Sep 17 00:00:00 2001 From: "HDA (OpenERP)" Date: Fri, 29 Jan 2010 16:20:24 +0530 Subject: [PATCH 15/30] Added menus and removed unused code bzr revid: hda@tinyerp.com-20100129105024-rlqrpvs7w3o7lzus --- addons/resource/faces_new/__init__.py | 30 - addons/resource/faces_new/observer.py | 79 - addons/resource/faces_new/pcalendar.py | 964 ------ addons/resource/faces_new/plocale.py | 54 - addons/resource/faces_new/resource.py | 866 ------ addons/resource/faces_new/task.py | 3855 ------------------------ addons/resource/faces_new/timescale.py | 113 - addons/resource/faces_new/utils.py | 124 - addons/resource/resource_view.xml | 23 +- 9 files changed, 7 insertions(+), 6101 deletions(-) delete mode 100644 addons/resource/faces_new/__init__.py delete mode 100644 addons/resource/faces_new/observer.py delete mode 100644 addons/resource/faces_new/pcalendar.py delete mode 100644 addons/resource/faces_new/plocale.py delete mode 100644 addons/resource/faces_new/resource.py delete mode 100755 addons/resource/faces_new/task.py delete mode 100644 addons/resource/faces_new/timescale.py delete mode 100644 addons/resource/faces_new/utils.py diff --git a/addons/resource/faces_new/__init__.py b/addons/resource/faces_new/__init__.py deleted file mode 100644 index d1d52daf742..00000000000 --- a/addons/resource/faces_new/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -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 - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/resource/faces_new/observer.py b/addons/resource/faces_new/observer.py deleted file mode 100644 index 411b2c5395b..00000000000 --- a/addons/resource/faces_new/observer.py +++ /dev/null @@ -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., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 diff --git a/addons/resource/faces_new/pcalendar.py b/addons/resource/faces_new/pcalendar.py deleted file mode 100644 index 3668a93a37a..00000000000 --- a/addons/resource/faces_new/pcalendar.py +++ /dev/null @@ -1,964 +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., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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) - print 'Return Date:::',datetime.datetime(*conv[0:-3]) - 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' - """ - print 'Working Days:::',self,day_range,trange,further_tranges - 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)) -# seconds = time.mktime(value.timetuple()) -# print 'Seconds:::',seconds -# return int(seconds) - 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") - print "start", start.strftime(), type(start) - - delay = Minutes("4H") - print "delay", delay, delay.strftime() - - print "Start", cal.StartDate is StartDate - print "base", cal.StartDate.__bases__[0] == StartDate.__bases__[0] - print "type", type(start) - - print "convert start" - start2 = cal.StartDate(start) - print "convert end" - - start3 = cal.StartDate("10.1.2005") - print "start2", start2.strftime(), type(start2) -#@-node:@file pcalendar.py -#@-leo diff --git a/addons/resource/faces_new/plocale.py b/addons/resource/faces_new/plocale.py deleted file mode 100644 index 3dcd35c7cae..00000000000 --- a/addons/resource/faces_new/plocale.py +++ /dev/null @@ -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., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -############################################################################ - -import gettext -import os.path -import locale - - -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() - diff --git a/addons/resource/faces_new/resource.py b/addons/resource/faces_new/resource.py deleted file mode 100644 index 7cc2741fc8e..00000000000 --- a/addons/resource/faces_new/resource.py +++ /dev/null @@ -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., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 "" % (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 "" % 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 "" % 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 diff --git a/addons/resource/faces_new/task.py b/addons/resource/faces_new/task.py deleted file mode 100755 index ed36965838a..00000000000 --- a/addons/resource/faces_new/task.py +++ /dev/null @@ -1,3855 +0,0 @@ -#@+leo-ver=4 -#@+node:@file task.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., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -############################################################################ - -#@-node:<< Copyright >> -#@nl -""" -This module contains all classes for project plan objects -""" -#@<< Imports >> -#@+node:<< Imports >> -import pcalendar -import resource -import types -import sys -import datetime -import operator as op -import warnings -import locale -import weakref -import opcode -import new -try: - set -except NameError: - from sets import Set as set -#@-node:<< Imports >> -#@nl - -_is_source = True - -STRICT = 3 -SLOPPY = 2 -SMART = 1 - -#@+others -#@+node:Exceptions -#@+node:class AttributeError -class AttributeError(AttributeError): - #@ << class AttributeError declarations >> - #@+node:<< class AttributeError declarations >> - is_frozen = False - - - #@-node:<< class AttributeError declarations >> - #@nl -#@-node:class AttributeError -#@+node:class RecursionError -class RecursionError(Exception): - """This exception is raised in cas of cirular dependencies - within an project""" - #@ << class RecursionError declarations >> - #@+node:<< class RecursionError declarations >> - pass - - - #@-node:<< class RecursionError declarations >> - #@nl -#@-node:class RecursionError -#@+node:class _IncompleteError -class _IncompleteError(Exception): - """This exception is raised, when there is not enough - data specified to calculate as task""" - #@ @+others - #@+node:__init__ - def __init__(self, *args): - if isinstance(args[0], (basestring)): - Exception.__init__(self, *args) - else: - Exception.__init__(self, - "not enough data for calculating task, "\ - "maybe you have a recursive reference", - *args) - #@-node:__init__ - #@-others -#@-node:class _IncompleteError -#@-node:Exceptions -#@+node:Proxies for self referencing -#@+node:class _MeProxy -class _MeProxy(object): - """ - A Proxy class for the me attribute of tasks in the compile case - """ - #@ << declarations >> - #@+node:<< declarations >> - __slots__ = "task" - - #@-node:<< declarations >> - #@nl - #@ @+others - #@+node:__init__ - def __init__(self, task): - object.__setattr__(self, "task", task) - #@-node:__init__ - #@+node:__getattr__ - def __getattr__(self, name): - if self.task._is_frozen: - return getattr(self.task, name) - - if name in ("name", "up", "root", "path", - "depth", "index", "calendar", - "children", "resource", "balance"): - return getattr(self.task, name) - - value = self.task.__dict__.get(name, _NEVER_USED_) - def make_val(default): - if value is _NEVER_USED_: return default - return value - - if name in ("start", "end"): - return self.task._to_start(make_val("1.1.2006")) - - if name in ("length", "effort", "duration", "todo", "done", - "buffer", "performed", "performed_effort", - "performed_end", "performed_start", - "performed_work_time" ): - return self.task._to_delta(make_val("0d")) - - if name in ("complete", "priority", "efficiency"): - return make_val(0) - - if value is _NEVER_USED_: - raise AttributeError("'%s' is not a valid attribute." % (name)) - - return value - #@-node:__getattr__ - #@+node:__setattr__ - def __setattr__(self, name, value): - self.task._set_attrib(name, value) - #@-node:__setattr__ - #@+node:__iter__ - def __iter__(self): - return iter(self.task) - #@nonl - #@-node:__iter__ - #@+node:add_attrib - def add_attrib(self, name_or_iter, val=None): - if not isinstance(name_or_iter, str): - for n, v in name_or_iter: - setattr(self, n, v) - else: - setattr(self, name_or_iter, val) - #@-node:add_attrib - #@-others -#@nonl -#@-node:class _MeProxy -#@+node:class _MeProxyRecalc -class _MeProxyRecalc(_MeProxy): - """ - A Proxy class for the me attribute of tasks in the recalc case - """ - #@ @+others - #@+node:__setattr__ - def __setattr__(self, name, value): - if self.task._properties.has_key(name): - self.task._set_attrib(name, value) - #@-node:__setattr__ - #@-others -#@-node:class _MeProxyRecalc -#@+node:class _MeProxyError -class _MeProxyError(_MeProxy): - #@ << declarations >> - #@+node:<< declarations >> - __slots__ = ("task", "attrib", "exc") - - #@-node:<< declarations >> - #@nl - #@ @+others - #@+node:__init__ - def __init__(self, task, attrib, exc): - _MeProxy.__init__(self, task) - object.__setattr__(self, "attrib", attrib) - object.__setattr__(self, "exc", exc) - #@-node:__init__ - #@+node:__setattr__ - def __setattr__(self, name, value): - if name == self.attrib or not self.attrib: - raise self.exc - #@-node:__setattr__ - #@-others -#@-node:class _MeProxyError -#@+node:class _MeProxyWarn -class _MeProxyWarn(_MeProxy): - #@ << declarations >> - #@+node:<< declarations >> - __slots__ = ("task", "attrib", "message") - - #@-node:<< declarations >> - #@nl - #@ @+others - #@+node:__init__ - def __init__(self, task, attrib, message): - _MeProxy.__init__(self, task) - object.__setattr__(self, "attrib", attrib) - object.__setattr__(self, "message", message) - #@-node:__init__ - #@+node:__setattr__ - def __setattr__(self, name, value): - if name == self.attrib or not self.attrib: - warnings.warn(self.message, RuntimeWarning, 2) - if not self.attrib: - #warn only one time! - object.__setattr__(self, "attrib", 1) - #@-node:__setattr__ - #@-others -#@-node:class _MeProxyWarn -#@-node:Proxies for self referencing -#@+node:Task instrumentation -#@+doc -# This section contains code for byte code instrumenting -# the task functions -#@-doc -#@nonl -#@+node:_int_to_arg -def _int_to_arg(value): - return value % 256, value / 256 -#@-node:_int_to_arg -#@+node:_correct_labels -def _correct_labels(old_code, new_code): - #@ << localize dot variables >> - #@+node:<< localize dot variables >> - hasjrel = opcode.hasjrel - hasjabs = opcode.hasjabs - HAVE_ARGUMENT = opcode.HAVE_ARGUMENT - #@nonl - #@-node:<< localize dot variables >> - #@nl - #@ << loop initialization >> - #@+node:<< loop initialization >> - labels = {} - old_new_map = {} # map old code offset to new code offset - n = len(old_code) - i = 0 - j = 0 - #@nonl - #@-node:<< loop initialization >> - #@nl - while i < n: - op = old_code[i] - nop = new_code[j] - old_new_map[i] = j - i = i + 1 - j = j + 1 - if op >= HAVE_ARGUMENT: - oparg = old_code[i] + old_code[i + 1] * 256 - i = i + 2 - j = j + 2 - if nop != op: - j += 3 # skip the 3 addition opcodes for attrib access - else: - #@ << add label if necessary >> - #@+node:<< add label if necessary >> - label = -1 - if op in hasjrel: - label = i + oparg - elif op in hasjabs: - label = oparg - if label >= 0: - labels[i] = label - #@nonl - #@-node:<< add label if necessary >> - #@nl - - for offset, label in labels.iteritems(): - new_offset = old_new_map[offset] - new_label = old_new_map[label] - op = new_code[new_offset - 3] - #change jump arguments - if op in hasjrel: - jump = _int_to_arg(new_label - new_offset) - new_code[new_offset - 2:new_offset] = jump - elif op in hasjabs: - new_code[new_offset - 2:new_offset] = _int_to_arg(new_label) -#@nonl -#@-node:_correct_labels -#@+node:_instrument -def _instrument(func): - #@ << localize dot variables >> - #@+node:<< localize dot variables >> - opname = opcode.opname - opmap = opcode.opmap - jumps = opcode.hasjrel + opcode.hasjabs - HAVE_ARGUMENT = opcode.HAVE_ARGUMENT - co = func.func_code - local_names = co.co_varnames - all_names = list(co.co_names) - global_names = set() - #@-node:<< localize dot variables >> - #@nl - #@ << define local functions list_to_dict and is_local >> - #@+node:<< define local functions list_to_dict and is_local >> - def list_to_dict(l): - return dict([(t[1], t[0]) for t in enumerate(l)]) - - def is_local(name): - return name[0] == "_" and name != "__constraint__" - #@nonl - #@-node:<< define local functions list_to_dict and is_local >> - #@nl - - #convert code - #@ << loop initialization >> - #@+node:<< loop initialization >> - # all_name_map maps names to the all_names index - # (same like all_names.index()) - all_name_map = list_to_dict(all_names) - if not all_name_map.has_key("me"): - all_name_map["me"] = len(all_names) - all_names.append("me") - - # - for ln in local_names: - if not all_name_map.has_key(ln): - all_name_map[ln] = len(all_names) - all_names.append(ln) - # - - new_local_names = filter(is_local, local_names) - new_local_name_map = list_to_dict(new_local_names) - - me_arg = _int_to_arg(all_name_map["me"]) - old_lnotab = map(ord, co.co_lnotab) - new_lnotab = [] - tab_pos = 0 - try: - next_tab_point = old_lnotab[0] - except IndexError: - next_tab_point = None - - last_tab_point = 0 - code = map(ord, co.co_code) - new_code = [] - has_labels = False - n = len(code) - i = 0 - #@nonl - #@-node:<< loop initialization >> - #@nl - while i < n: - if i == next_tab_point: - #@ << calculate new tab point >> - #@+node:<< calculate new tab point >> - increment = len(new_code) - last_tab_point - new_lnotab.extend((increment, old_lnotab[tab_pos + 1])) - tab_pos += 2 - try: - next_tab_point = i + old_lnotab[tab_pos] - last_tab_point = len(new_code) - except IndexError: - next_tab_point = -1 - #@nonl - #@-node:<< calculate new tab point >> - #@nl - - op = code[i] - i += 1 - if op >= HAVE_ARGUMENT: - #@ << calculate argument >> - #@+node:<< calculate argument >> - arg0 = code[i] - arg1 = code[i+1] - oparg = arg0 + arg1 * 256 - #@nonl - #@-node:<< calculate argument >> - #@nl - i += 2 - - if opname[op] == "LOAD_GLOBAL": - global_names.add(oparg) - - elif opname[op] == "STORE_FAST": - #@ << change "store fast" to "store attribute" >> - #@+node:<< change "store fast" to "store attribute" >> - name = local_names[oparg] - if not is_local(name): - new_code.append(opmap["LOAD_GLOBAL"]) - new_code.extend(me_arg) - op = opmap["STORE_ATTR"] - arg0, arg1 = _int_to_arg(all_name_map[name]) - else: - arg0, arg1 = _int_to_arg(new_local_name_map[name]) - #@nonl - #@-node:<< change "store fast" to "store attribute" >> - #@nl - - elif opname[op] == "LOAD_FAST": - #@ << change "load fast" to "load attribute" >> - #@+node:<< change "load fast" to "load attribute" >> - name = local_names[oparg] - if not is_local(name): - new_code.append(opmap["LOAD_GLOBAL"]) - new_code.extend(me_arg) - op = opmap["LOAD_ATTR"] - arg0, arg1 = _int_to_arg(all_name_map[name]) - else: - arg0, arg1 = _int_to_arg(new_local_name_map[name]) - #@nonl - #@-node:<< change "load fast" to "load attribute" >> - #@nl - - elif op in jumps: - has_labels = True - - new_code.extend((op, arg0, arg1)) - else: - new_code.append(op) - - if has_labels: - _correct_labels(code, new_code) - - #@ << create new code and function objects and return >> - #@+node:<< create new code and function objects and return >> - new_code = "".join(map(chr, new_code)) - new_lnotab = "".join(map(chr, new_lnotab)) - new_co = new.code(co.co_argcount, - len(new_local_names), - max(co.co_stacksize, 2), - co.co_flags, - new_code, - co.co_consts, - tuple(all_names), - tuple(new_local_names), - co.co_filename, - co.co_name, - co.co_firstlineno, - new_lnotab, - co.co_freevars, - co.co_cellvars) - - - func = new.function(new_co, - func.func_globals, - func.func_name, - func.func_defaults, - func.func_closure) - func.global_names = tuple([all_names[index] for index in global_names]) - return func - #@nonl - #@-node:<< create new code and function objects and return >> - #@nl -#@nonl -#@-node:_instrument -#@-node:Task instrumentation -#@+node:Wrappers -#@+node:class _Path -class _Path(object): - """ - This class represents an instrumented path, to - a task. If it points to an attribute of a task, it - not only returns the value of the attribute. You can also - find out the source attribute (task and attribute name) - of the value. - """ - #@ @+others - #@+node:__init__ - def __init__(self, task, path_str): - self._task = task - self._path_str = path_str - #@-node:__init__ - #@+node:__getattr__ - def __getattr__(self, name): - new = getattr(self._task, name) - if isinstance(new, Task): - return _Path(new, self._path_str + "." + name) - - return _ValueWrapper(new, [(self._task, name)]) - #@-node:__getattr__ - #@+node:__str__ - def __str__(self): - return self._path_str - #@-node:__str__ - #@+node:__iter__ - def __iter__(self): - return iter(self._task) - #@nonl - #@-node:__iter__ - #@-others - -#@-node:class _Path -#@+node:_val -#helper functions for _ValueWrapper -#---------------------------------- - -def _val(val): - if isinstance(val, _ValueWrapper): - return val._value - - return val -#@-node:_val -#@+node:_ref -def _ref(val): - if isinstance(val, _ValueWrapper): - return val._ref - - return [] -#@-node:_ref -#@+node:_sref -def _sref(val, ref): - if isinstance(val, _ValueWrapper): - val._ref = ref -#@nonl -#@-node:_sref -#@+node:_refsum -def _refsum(refs): - return reduce(lambda a, b: a + b, refs, []) -#@nonl -#@-node:_refsum -#@+node:class _ValueWrapper - - -class _ValueWrapper(object): - """ - This class represents a value, of a task attribute or - a return value of a task method. It contains the value, - and the supplier of that value - """ - #@ @+others - #@+node:__init__ - def __init__(self, value, ref): - self._value = value - self._ref = ref - #@-node:__init__ - #@+node:unicode - def unicode(self, *args): - if isinstance(self._value, str): - return unicode(self._value, *args) - - return unicode(self._value) - #@nonl - #@-node:unicode - #@+node:_vw - def _vw(self, operand, *args): - refs = _refsum(map(_ref, args)) - vals = map(_val, args) - result = operand(*vals) - return self.__class__(result, refs) - #@-node:_vw - #@+node:_cmp - def _cmp(self, operand, *args): - refs = _refsum(map(_ref, args)) - vals = map(_val, args) - result = operand(*vals) - map(lambda a: _sref(a, refs), args) - return result - #@-node:_cmp - #@+node:__getattr__ - def __getattr__(self, name): return getattr(self._value, name) - #@-node:__getattr__ - #@+node:__getitem__ - def __getitem__(self, slice): - return self.__class__(self._value[slice], self._ref) - #@nonl - #@-node:__getitem__ - #@+node:__str__ - def __str__(self): return str(self._value) - #@-node:__str__ - #@+node:__unicode__ - def __unicode__(self): return unicode(self._value) - #@nonl - #@-node:__unicode__ - #@+node:__repr__ - def __repr__(self): return repr(self._value) - #@-node:__repr__ - #@+node:__nonzero__ - def __nonzero__(self): return bool(self._value) - #@-node:__nonzero__ - #@+node:__lt__ - def __lt__(self, other): return self._cmp(op.lt, self, other) - #@-node:__lt__ - #@+node:__le__ - def __le__(self, other): return self._cmp(op.le, self, other) - #@-node:__le__ - #@+node:__eq__ - def __eq__(self, other): return self._cmp(op.eq, self, other) - #@-node:__eq__ - #@+node:__ne__ - def __ne__(self, other): return self._cmp(op.ne, self, other) - #@-node:__ne__ - #@+node:__gt__ - def __gt__(self, other): return self._cmp(op.gt, self, other) - #@-node:__gt__ - #@+node:__ge__ - def __ge__(self, other): return self._cmp(op.ge, self, other) - #@-node:__ge__ - #@+node:__add__ - def __add__(self, other): return self._vw(op.add, self, other) - #@nonl - #@-node:__add__ - #@+node:__sub__ - def __sub__(self, other): return self._vw(op.sub, self, other) - #@-node:__sub__ - #@+node:__mul__ - def __mul__(self, other): return self._vw(op.mul, self, other) - #@-node:__mul__ - #@+node:__floordiv__ - def __floordiv__(self, other): return self._vw(op.floordiv, self, other) - #@-node:__floordiv__ - #@+node:__mod__ - def __mod__(self, other): return self._vw(op.mod, self, other) - #@-node:__mod__ - #@+node:__divmod__ - def __divmod__(self, other): return self._vw(op.divmod, self, other) - #@-node:__divmod__ - #@+node:__pow__ - def __pow__(self, other): return self._vw(op.pow, self, other) - #@-node:__pow__ - #@+node:__lshift__ - def __lshift__(self, other): return self._vw(op.lshift, self, other) - #@-node:__lshift__ - #@+node:__rshift__ - def __rshift__(self, other): return self._vw(op.rshift, self, other) - #@-node:__rshift__ - #@+node:__and__ - def __and__(self, other): return self._vw(op.and_, self, other) - #@-node:__and__ - #@+node:__xor__ - def __xor__(self, other): return self._vw(op.xor, self, other) - #@-node:__xor__ - #@+node:__or__ - def __or__(self, other): return self._vw(op.or_, self, other) - #@-node:__or__ - #@+node:__div__ - def __div__(self, other): return self._vw(op.div, self, other) - #@-node:__div__ - #@+node:__radd__ - def __radd__(self, other): return self._vw(op.add, other, self) - #@-node:__radd__ - #@+node:__rsub__ - def __rsub__(self, other): return self._vw(op.sub, other, self) - #@-node:__rsub__ - #@+node:__rmul__ - def __rmul__(self, other): return self._vw(op.mul, other, self) - #@-node:__rmul__ - #@+node:__rdiv__ - def __rdiv__(self, other): return self._vw(op.div, other, self) - #@-node:__rdiv__ - #@+node:__rtruediv__ - def __rtruediv__(self, other): return self._vw(op.truediv, other, self) - #@-node:__rtruediv__ - #@+node:__rfloordiv__ - def __rfloordiv__(self, other): return self._vw(op.floordiv, other, self) - #@-node:__rfloordiv__ - #@+node:__rmod__ - def __rmod__(self, other): return self._vw(op.mod, other, self) - #@-node:__rmod__ - #@+node:__rdivmod__ - def __rdivmod__(self, other): return self._vw(op.divmod, other, self) - #@-node:__rdivmod__ - #@+node:__rpow__ - def __rpow__(self, other): return self._vw(op.pow, other, self) - #@-node:__rpow__ - #@+node:__rlshift__ - def __rlshift__(self, other): return self._vw(op.lshift, other, self) - #@-node:__rlshift__ - #@+node:__rrshift__ - def __rrshift__(self, other): return self._vw(op.rshift, other, self) - #@-node:__rrshift__ - #@+node:__rand__ - def __rand__(self, other): return self._vw(op.and_, other, self) - #@-node:__rand__ - #@+node:__rxor__ - def __rxor__(self, other): return self._vw(op.xor, other, self) - #@-node:__rxor__ - #@+node:__ror__ - def __ror__(self, other): return self._vw(op.or_, other, self) - #@-node:__ror__ - #@+node:__int__ - def __int__(self): return int(self._value) - #@-node:__int__ - #@+node:__long__ - def __long__(self): return long(self._value) - #@-node:__long__ - #@+node:__float__ - def __float__(self): return float(self._value) - #@-node:__float__ - #@+node:__len__ - def __len__(self): return len(self._value) - #@-node:__len__ - #@+node:__iter__ - def __iter__(self): return iter(self._value) - #@-node:__iter__ - #@+node:__hash__ - def __hash__(self): return hash(self._value) - #@-node:__hash__ - #@-others -#@-node:class _ValueWrapper -#@-node:Wrappers -#@+node:Utilities -#@+node:class _NEVER_USED_ -class _NEVER_USED_: - pass - -#@-node:class _NEVER_USED_ -#@+node:class _StringConverter -class _StringConverter(object): - """This class is a helper for the to_string mechanism - of tasks""" - #@ @+others - #@+node:__init__ - def __init__(self, source, format=None): - self.source = source - self.format = format - #@-node:__init__ - #@+node:__getitem__ - def __getitem__(self, format): - return _StringConverter(self.source, format) - #@-node:__getitem__ - #@+node:__getattr__ - def __getattr__(self, name): - class StrWrapper(object): - def __init__(self, value, name, source, format): - self._value = value - self.name = name - self.source = source - self.format = format - - def __call__(self, arg): - formatter = self.source.formatter(self.name, - arg, - self.format) - return formatter(self._value(arg)) - - value = getattr(self.source, name) - if callable(value): - #for methods the wrapper has to - return StrWrapper(value, name, self.source, self.format) - - formatter = self.source.formatter(name, format=self.format) - return formatter(value) - #@-node:__getattr__ - #@-others -#@-node:class _StringConverter -#@+node:Multi -def Multi(val, **kwargs): - """returns a directory for mutlivalued attributes""" - return dict(_default=val, **kwargs) -#@nonl -#@-node:Multi -#@+node:create_relative_path -def create_relative_path(from_, to_): - """ - creates a relative path from absolute path - from_ to absolute path to_ - """ - from_ = from_.split(".") - to_ = to_.split(".") - - for i, parts in enumerate(zip(from_, to_)): - from_part, to_part = parts - if from_part != to_part: - break - - from_ = from_[i:] - to_ = to_[i:] - return "up." * len(from_) + ".".join(to_) -#@nonl -#@-node:create_relative_path -#@+node:create_absolute_path -def create_absolute_path(from_, to_): - """ - creates a absolute path from absolute path - from_ to relative path to_ - """ - from_ = from_.split(".") - to_ = to_.split(".") - - for i, part in enumerate(to_): - if part != "up": - break - - from_ = from_[:-i] - to_ = to_[i:] - return "%s.%s" % (".".join(from_), ".".join(to_)) - - -#@-node:create_absolute_path -#@+node:_split_path -def _split_path(path): - try: - index = path.rindex(".") - return path[:index], path[index + 1:] - except: - return path -#@-node:_split_path -#@+node:_to_datetime -_to_datetime = pcalendar.to_datetime -#@nonl -#@-node:_to_datetime -#@+node:_get_tasks_of_sources -def _get_tasks_of_sources(task, attrib_filter="end,start,effort,length,duration"): - #return all source tasks, this task is dependend on - - dep_tasks = {} - - while task: - for dep in task._sources.values(): - for d in dep: - path, attrib = _split_path(d) - if attrib and attrib_filter.find(attrib) >= 0: - dep_tasks[path] = True - - task = task.up - - return dep_tasks.keys() -#@-node:_get_tasks_of_sources -#@+node:_build_balancing_list -def _build_balancing_list(tasks): - """ - Returns a specialy sorted list of tasks. - If the tasks will allocate resources in the sorting order of that list - correct balancing is ensured - """ - - # first sort the list for attributes - index = 0 - balancing_list = [(-t.priority, t.balance, index, t) for index, t in enumerate(tasks)] - balancing_list.sort() - - #print - #for p, b, i, t in balancing_list: - # print p, b, i, t.path - - balancing_list = [ t for p, b, i, t in balancing_list ] - - #now correct the presorted list: - #if task a is dependent on task b, b will be moved before a - - done_map = { } - count = len(balancing_list) - while len(done_map) < count: - for i in range(count): - to_inspect = balancing_list[i] - if done_map.has_key(to_inspect): - continue - - done_map[to_inspect] = True - break - else: - break - - #@ << define inspect_depends_on >> - #@+node:<< define inspect_depends_on >> - inspect_path = to_inspect.path + "." - sources = _get_tasks_of_sources(to_inspect) - sources = [ s + "." for s in sources - if not inspect_path.startswith(s) ] - - # the if in the later line ignores assignments like - # like start = up.start (i.e. references to parents) - # this will be handled in the second if of inspect_depends_on - # and can cause errors otherwise - - def inspect_depends_on(task): - cmp_path = task.path + "." - for src in sources: - if cmp_path.startswith(src): - #task is a source of to_inspect - return True - - if inspect_path.startswith(cmp_path): - #to_inspect is a child of task - return True - - return False - #@nonl - #@-node:<< define inspect_depends_on >> - #@nl - for j in range(i + 1, count): - check_task = balancing_list[j] - if done_map.has_key(check_task): - continue - - if inspect_depends_on(check_task): - del balancing_list[j] - balancing_list.insert(i, check_task) - i += 1 # to_inspect is now at i + 1 - - - return balancing_list -#@-node:_build_balancing_list -#@+node:_as_string -def _as_string(val): - if isinstance(val, basestring): - return '"""%s"""' % val.replace("\n", "\\n") - - if isinstance(val, pcalendar._WorkingDateBase): - return '"%s"' % val.strftime("%Y-%m-%d %H:%M") - - if isinstance(val, datetime.datetime): - return '"%s"' % val.strftime("%Y-%m-%d %H:%M") - - if isinstance(val, datetime.timedelta): - return '"%id %iM"' % (val.days, val.seconds / 60) - - if isinstance(val, tuple): - result = map(_as_string, val) - return "(%s)" % ", ".join(result) - - if isinstance(val, list): - result = map(_as_string, val) - return "[%s]" % ", ".join(result) - - if isinstance(val, resource.Resource): - return val._as_string() - - if isinstance(val, Task): - return val.path - - return str(val) -#@-node:_as_string -#@+node:_step_tasks -def _step_tasks(task): - if isinstance(task, Task): - yield task - - stack = [iter(task.children)] - while stack: - for task in stack[-1]: - yield task - - if task.children: - stack.append(iter(task.children)) - break - else: - stack.pop() -#@-node:_step_tasks -#@-node:Utilities -#@+node:Cache -instrumentation_cache = {} -balancing_cache = {} - -def clear_cache(): - instrumentation_cache.clear() - balancing_cache.clear() -#@nonl -#@-node:Cache -#@+node:Resource Allocators -#@+others -#@+node:VariableLoad -def VariableLoad(limit=0): - """ - Allocates the resource with maximal possible load. - If limit is given, a the load is at least limit or more. - """ - try: - balance = me.balance - except NameError: - balance = SLOPPY - - if balance != SLOPPY: - raise RuntimeError("You may specify variable_load only with balance=SLOPPY") - - return -limit -#@-node:VariableLoad -#@+node:_calc_load -def _calc_load(task, resource): - #changed at the resource instance - load = resource.__dict__.get("load") - if load is not None: return load - - load = task.__dict__.get("load") - if load is not None: return load - - #inherited by the task - return min(task.load, task.max_load, resource.max_load or 100.0) -#@-node:_calc_load -#@+node:_calc_maxload -def _calc_maxload(task, resource): - #changed at the resource instance - max_load = resource.__dict__.get("max_load") - if max_load: return max_load - - #an explicit load can overwrite max_load - load = max(resource.__dict__.get("load", 0), - task.__dict__.get("load"), 0) - - #change at the task - max_load = task.__dict__.get("max_load") - if max_load: return max(max_load, load) - - #inherited by the resource - max_load = resource.max_load - if max_load: return max(max_load, load) - - #inherited by the task - return max(task.max_load, load) -#@-node:_calc_maxload -#@+node:class AllocationAlgorithm -class AllocationAlgorithm(object): - """This class is a base for resource allocation algorithms""" - #@ @+others - #@+node:test_allocation - def test_allocation(self, task, resource): - """This method simulates the allocation of a specific resource. - It returns a list of values representing the state of the allocation. - The task allocator calls test_allocation for every alternative resource. - It compares the first items of all return lists, and allocates the - resource with the minum first item value""" - return (task.end, ) - #@-node:test_allocation - #@+node:allocate - def allocate(self, task, state): - """This method eventually allocates a specific resource. - State is the return list of test_allocation""" - pass - #@-node:allocate - #@-others -#@-node:class AllocationAlgorithm -#@+node:class StrictAllocator -class StrictAllocator(AllocationAlgorithm): - """This class implements the STRICT resource allocation""" - #@ @+others - #@+node:_distribute_len_loads - def _distribute_len_loads(self, task, resource, effort, length): - # A special load calculation, if effort and length are given. - # and the resources have a defined maxload, the load must be - # individually calculated for each resource. - - # Formulars: r=resources, t=task - # effort = length * efficiency(t) * sum[load(r) * effiency(r)] - # ==> sum_load = sum[load(r) * effiency(r)] - # = effort / (length * efficiency(t)) - # - - sum_load = float(effort) / (task.efficiency * length) - - # algorithm: - # The goal is to distribute the load (norm_load) equally - # to all resources. If a resource has a max_load(r) < norm_load - # the load of this resource will be max_load(r), and the other - # resources will have another (higher) norm_load - - max_loads = map(lambda r: (_calc_maxload(task, r), r), resource) - max_loads.sort() - - efficiency_sum = sum(map(lambda r: r.efficiency, resource)) - norm_load = sum_load / efficiency_sum - - loads = {} - for max_load, r in max_loads[:-1]: - if max_load < norm_load: - loads[r] = max_load - efficiency_sum -= r.efficiency - sum_load -= max_load * r.efficiency - norm_load = sum_load / efficiency_sum - else: - loads[r] = norm_load - - max_load, r = max_loads[-1] - loads[r] = norm_load - return loads - #@-node:_distribute_len_loads - #@+node:test_allocation - def test_allocation(self, task, resource): - effort = task.__dict__.get("effort") - to_start = task._to_start - to_end = task._to_end - to_delta = task._to_delta - - if task.performed_end: - start = to_start(max(task.performed_end, - task.root.calendar.now, - task.start)) - else: - start = task.start - if task.root.has_actual_data and task.complete == 0: - start = max(start, to_start(task.root.calendar.now)) - - base_start = to_start(task.performed_start or task.start) - calc_load = lambda r: _calc_load(task, r) - loads = map(lambda r: (r, calc_load(r)), resource) - - length = task.__dict__.get("length") - duration = task.__dict__.get("duration") - end = task.__dict__.get("end") - - #@ << correct length >> - #@+node:<< correct length >> - if length is not None: - length = to_delta(max(length - (task.start - base_start), 0)) - #@nonl - #@-node:<< correct length >> - #@nl - #@ << correct duration >> - #@+node:<< correct duration >> - if duration is not None: - delta = task.start.to_datetime() - base_start.to_datetime() - delta = to_delta(delta, True) - duration = to_delta(max(duration - delta, 0), True) - #@nonl - #@-node:<< correct duration >> - #@nl - #@ << check end >> - #@+node:<< check end >> - if end is not None: - length = end - start - if length <= 0: return False - #@nonl - #@-node:<< check end >> - #@nl - #@ << correct effort and (re)calculate length >> - #@+node:<< correct effort and (re)calculate length >> - if effort is not None: - effort -= task.performed_effort - effort = to_delta(max(effort, 0)) - if effort <= 0: return False - - if length is not None: - #if length and effort is set, the load will be calculated - length = length or task.calendar.minimum_time_unit - loads = self._distribute_len_loads(task, resource, - effort, length) - def calc_load(res): - return loads[res] - else: - #the length depends on the count of resources - factor = sum(map(lambda a: a[0].efficiency * a[1], - loads)) * task.efficiency - length = effort / factor - #@nonl - #@-node:<< correct effort and (re)calculate length >> - #@nl - #@ << set adjust_date and delta >> - #@+node:<< set adjust_date and delta >> - if length is not None: - adjust_date = lambda date: date - delta = to_delta(length).round() - else: - assert(duration is not None) - adjust_date = _to_datetime - delta = datetime.timedelta(minutes=duration) - #@nonl - #@-node:<< set adjust_date and delta >> - #@nl - - # find the earliest start date - start, book_load\ - = self.balance(task, start, delta, adjust_date, - calc_load, resource) - - end = to_end(start + delta) - start = to_start(start) - - if effort is None: - #length is frozen ==> a new effort will be calculated - factor = sum(map(lambda a: a[1], loads)) - length = end - start - - effort = to_delta(length * factor\ - + task.performed_effort).round() - - return (end, book_load), resource, calc_load, start, effort - #@-node:test_allocation - #@+node:allocate - def allocate(self, task, state): - # now really book the resource - end_bl, resource, calc_load, start, effort = state - end = end_bl[0] - cal = task.root.calendar - to_start = task._to_start - to_end = task._to_end - to_delta = task._to_delta - - task.start = task.performed_start \ - and to_start(task.performed_start) \ - or to_start(start) - - task.end = end - task._unfreeze("length") - task._unfreeze("duration") - length = end - start - - for r in resource: - book_load = calc_load(r) - work_time = to_delta(length * book_load).round() - r.book_task(task, start, end, book_load, work_time, False) - - #the following lines are important to be exactly at this - #positions in that order: - # done and todo are dependend on: - # - the existence of effort (if effort was set or not set) - # - book_task (they can only be calculated, if the task is booked) - # - booked_resource (to get the booked tasks) - task.booked_resource = resource - task.done = task.done - task.todo = task.todo - task.length = end - task.start - task.effort = to_delta(effort + task.performed_effort) - #@-node:allocate - #@+node:balance - #now effort exists always - - - def balance(self, task, start, delta, adjust_date, - calc_load, resource): - book_load = max(map(lambda r: r.get_load(task.start, task.scenario), resource)) - return start, book_load - #@-node:balance - #@-others -#@-node:class StrictAllocator -#@+node:class SmartAllocator - - -class SmartAllocator(StrictAllocator): - #@ @+others - #@+node:balance - def balance(self, task, start, delta, adjust_date, - calc_load, resource): - #find the earliest start date, at which all - #resources in the team are free - - cal = task.root.calendar - to_start = task._to_start - start = adjust_date(start) - scenario = task.scenario - - while True: - #we have finished, when all resources have the - #same next free start date - for r in resource: - max_load = _calc_maxload(task, r) - load = calc_load(r) - - #find the next free time of the resource - s = r.find_free_time(start, delta, load, max_load, scenario) - if s != start: - s = to_start(s) - start = adjust_date(s) - break - else: - #only one resource - break - - return start, 1.0 - #@-node:balance - #@-others -#@-node:class SmartAllocator -#@+node:class SloppyAllocator - - -class SloppyAllocator(AllocationAlgorithm): - #@ @+others - #@+node:test_allocation - def test_allocation(self, task, resource): - if task.__dict__.has_key("effort"): - return self.test_allocation_effort(task, resource) - - return self.test_allocation_length(task, resource) - #@-node:test_allocation - #@+node:test_allocation_length - def test_allocation_length(self, task, resource): - #length is frozen ==> effort will be calculated - to_start = task._to_start - to_end = task._to_end - to_delta = task._to_delta - - end = task.end - if task.performed_end: - start = to_start(max(task.performed_end, - task.root.calendar.now, - start)) - else: - start = task.start - - base_start = to_start(task.performed_start or task.start) - length = to_delta(max(task.length - (start - base_start), 0)) - sum_effort = 0 - intervals = [] - scenario = task.scenario - for r in resource: - date = start - max_load = _calc_maxload(task, r) - book_load = _calc_load(task, r) - - while date < end: - #find free time intervals and add them for booking - endi, load = r.end_of_booking_interval(date, task) - endi = min(endi, end) - endi = to_end(endi) - - if book_load <= 0: - #variable book_load ==> calc the maxmimal possible book_load >= (the given book_load) - used_book_load = - book_load - diff_load = max_load - load - if diff_load and diff_load >= book_load: - used_book_load = diff_load - else: - used_book_load = max_load - else: - used_book_load = book_load - - if max_load - load >= used_book_load: - intervals.append((r, used_book_load, date, endi)) - sum_effort = (endi - date) * used_book_load - - date = to_start(endi) - - return -sum_effort, end, resource, intervals - #@-node:test_allocation_length - #@+node:test_allocation_effort - def test_allocation_effort(self, task, resource): - #effort is frozen ==> length will be calculated - - to_start = task._to_start - to_end = task._to_end - to_delta = task._to_delta - - intervals = [] - effort = task.__dict__.get("effort") - - if task.performed_end: - next_date = to_start(max(task.performed_end, - task.root.calendar.now, - task.start)) - else: - next_date = task.start - if task.root.has_actual_data and task.complete == 0: - next_date = max(next_date, to_start(task.root.calendar.now)) - - #walks chronologicly through the booking - #intervals of each resource, and reduces - #the effort for each free interval - #until it becomes 0 - - alloc_effort = effort - effort -= task.performed_effort - while effort > 0: - date = next_date - - interval_resource = [] - interval_end = to_start(sys.maxint) - factor = 0 - - for r in resource: - max_load = _calc_maxload(task, r) - book_load = _calc_load(task, r) - end, load = r.end_of_booking_interval(date, task) - interval_end = to_start(min(end, interval_end)) - - if book_load <= 0: - #variable book_load ==> calc the maxmimal possible book_load >= (the given book_load) - book_load = - book_load - diff_load = max_load - load - if diff_load and diff_load >= book_load: - book_load = diff_load - else: - book_load = max_load - - if book_load + load <= max_load: - resource_factor = book_load * r.efficiency - interval_resource.append((r, book_load, resource_factor)) - factor += resource_factor - - - - next_date = interval_end - if factor: - factor *= task.efficiency - length = to_delta(effort / factor).round() - end = date + length - - if interval_end >= end: - next_date = interval_end = end - effort = 0 - book_end = end - else: - book_end = interval_end - length = book_end - date - minus_effort = length * factor - effort -= minus_effort - - book_end = to_end(book_end) - intervals.append((date, book_end, length, interval_resource)) - - return next_date, alloc_effort, resource, intervals - #@-node:test_allocation_effort - #@+node:allocate - def allocate(self, task, state): - if task.__dict__.has_key("effort"): self.allocate_effort(task, state) - else: self.allocate_length(task, state) - #@-node:allocate - #@+node:allocate_length - def allocate_length(self, task, state): - # now really book the resource - neg_sum_effort, end, resource, intervals = state - - cal = task.root.calendar - to_start = task._to_start - to_end = task._to_end - to_delta = task._to_delta - - task.start = to_start(task.performed_start or task.start) - task.end = to_end(end) - task._unfreeze("length") - task._unfreeze("duration") - - effort = 0 - for r, load, s, e in intervals: - work_time = to_delta((e - s) * load).round() - effort += work_time - r.book_task(task, s, e, load, work_time, False) - - #see comment at StrictAllocator.allocate - task.booked_resource = resource - task.done = task.done - task.todo = task.todo - task.effort = to_delta(effort + task.performed_effort).round() - #@-node:allocate_length - #@+node:allocate_effort - def allocate_effort(self, task, state): - # now really book the resource - end, effort, resource, intervals = state - to_start = task._to_start - to_end = task._to_end - to_delta = task._to_delta - - task.start = task.performed_start \ - and to_start(task.performed_start) \ - or to_start(intervals[0][0]) - task.end = to_end(end) - task._unfreeze("length") - task._unfreeze("duration") - - for start, end, length, resources in intervals: - for r, load, factor in resources: - work_time = to_delta(length * load) - r.book_task(task, start, end, load, work_time, False) - - task.booked_resource = resource - task.done = task.done - task.todo = task.todo - task.effort = to_delta(effort) - task.length = task.end - task.start - #@-node:allocate_effort - #@-others -#@-node:class SloppyAllocator -#@-others - -_smart_allocator = SmartAllocator() -_sloppy_allocator = SloppyAllocator() -_strict_allocator = StrictAllocator() -_allocators = { SMART: _smart_allocator, - SLOPPY: _sloppy_allocator, - STRICT: _strict_allocator } - -_allocator_strings = { SMART: "SMART", - SLOPPY: "SLOPPY", - STRICT: "STRICT" } -#@-node:Resource Allocators -#@+node:Load Calculators -#@+node:YearlyMax -def YearlyMax(value): - """ - Calculates a load parameter with a maximal yearly workload - """ - #@ << calculate calendar and time_diff >> - #@+node:<< calculate calendar and time_diff >> - try: - cal = me.calendar - except NameError: - cal = pcalendar._default_calendar - - time_diff = cal.Minutes(value) - #@nonl - #@-node:<< calculate calendar and time_diff >> - #@nl - return float(time_diff) / \ - (cal.working_days_per_year \ - * cal.working_hours_per_day \ - * 60) -#@nonl -#@-node:YearlyMax -#@+node:WeeklyMax -def WeeklyMax(value): - """ - Calculates a load parameter with a maximal weekly workload - """ - #@ << calculate calendar and time_diff >> - #@+node:<< calculate calendar and time_diff >> - try: - cal = me.calendar - except NameError: - cal = pcalendar._default_calendar - - time_diff = cal.Minutes(value) - #@nonl - #@-node:<< calculate calendar and time_diff >> - #@nl - return float(time_diff) / \ - (cal.working_days_per_week \ - * cal.working_hours_per_day \ - * 60) - -#@-node:WeeklyMax -#@+node:MonthlyMax -def MonthlyMax(value): - """ - Calculates a load parameter with a maximal monthly workload - """ - #@ << calculate calendar and time_diff >> - #@+node:<< calculate calendar and time_diff >> - try: - cal = me.calendar - except NameError: - cal = pcalendar._default_calendar - - time_diff = cal.Minutes(value) - #@nonl - #@-node:<< calculate calendar and time_diff >> - #@nl - return float(time_diff) / \ - (cal.working_days_per_month \ - * cal.working_hours_per_day \ - * 60) - -#@-node:MonthlyMax -#@+node:DailyMax -def DailyMax(value): - """ - Calculates a load parameter with a maximal daily workload - """ - #@ << calculate calendar and time_diff >> - #@+node:<< calculate calendar and time_diff >> - try: - cal = me.calendar - except NameError: - cal = pcalendar._default_calendar - - time_diff = cal.Minutes(value) - #@nonl - #@-node:<< calculate calendar and time_diff >> - #@nl - return float(time_diff) / (cal.working_hours_per_day * 60) -#@-node:DailyMax -#@-node:Load Calculators -#@+node:Task -#@+node:class _TaskProperty -class _TaskProperty(object): - #@ @+others - #@+node:__init__ - def __init__(self, method): - self.method = method - #@-node:__init__ - #@+node:__get__ - def __get__(self, instance, owner): - if not instance: - return None - - return instance._wrap_attrib(self.method) - #@-node:__get__ - #@-others -#@-node:class _TaskProperty -#@+node:class _RoundingTaskProperty -class _RoundingTaskProperty(object): - #@ @+others - #@+node:__init__ - def __init__(self, method, name): - self.method = method - self.name = name - #@-node:__init__ - #@+node:__get__ - def __get__(self, instance, owner): - if not instance: - return None - - result = instance._wrap_attrib(self.method).round() - if instance._is_frozen: - #correct the attrib to the rounded value - setattr(instance, self.name, result) - - return result - #@-node:__get__ - #@-others -#@-node:class _RoundingTaskProperty -#@+node:class Task -class Task(object): - #@ << description >> - #@+node:<< description >> - """ - This class represents a single task in the project tree. A task - can have other child tasks, or is a leaf of the tree. Resources - will be allocated only to leafes. You will never create task - objects by your self, they are created indirectly by Projects. - - @var root: - Returns the root project task. - - @var up: - Returns the parent task. - - @var title: - Specifies an alternative more descriptive name for the task. - - @var start: - The start date of the task. Valid values are expressions and - strings specifing a datatime - - @var end: - The end date of the task. Valid values are expressions and - strings. - - @var effort: - Specifies the effort needed to complete the task. Valid values - are expressions and strings. (Todo: What happens, in case of - specified performance data...) - - - @var length: - Specifies the time the task occupies the resources. This is - working time, not calendar time. 7d means 7 working days, not one - week. Whether a day is considered a working day or not depends on - the defined working hours and global vacations. - - @var duration: - Specifies the time the task occupies the resources. This is - calendar time, not working time. 7d means one week. - - @var buffer: - Specifies the time a task can be delayed, without moving dependend - milestones. A Task with a buffer S{<=} 0d is part of the critical - chain. This attribute is readonly. - - @var complete: - Specifies what percentage of the task is already completed. - - @var todo: - Specifies the effort, which needs to be done to complete a - task. This is another (indirect) way to specify the ME{complete} - attribute. - - @var done: - Specifies the work effort, which has been already done. This - attribute is readonly. - - @var estimated_effort: - Specifies the estimated_effort given by setting the effort property. - - @var performed: - Specifies a list of actual working times performed on the task. - The format is: C{[ (resource, from, to, time), ... ]} - - @var performed_work_time: - Specifies the sum of all working times. This attribute is - readonly. - - @var performed_effort: - Specifies the complete effort of all working times. This attribute is - readonly. - - @var performed_start: - The start date of the performed data. - - @var performed_end: - The end date of the performed data. - - @var performed_resource: - The resources who have already performed on the task. This attribute is readonly. - - - @var balance: - Specifies the resource allocation type. Possible values are - CO{STRICT}, CO{SLOPPY}, CO{SMART}. - - @var resource: - Specifies the possible resources, that may be allocated for the - task. - - @var booked_resource: - Specifies the allocated resources of a task. This attribute is - readonly. - - @var load: - Specifies the daily load of a resource for an allocation of the - specified task. A load of 1.0 (default) means the resource is - allocated for as many hours as specified by - ME{working_hours_per_day}. A load of 0.5 means half that many - hours. - - @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 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 milestone: - Specified if the task is a milestone. The possible values are - C{True} or "later". If the start date of the milestone is not - a valid working date, the milestone will appear at the previous - working date before the given start date. If "later" is specified - the milestone will appear at the next valid working date. - A milestone has always an effort of 0d. - - @var priority: - Specifies a priority between 1 and 1000. A task with higher - priority is more likely to get the requested resources. The - default priority is 500. - - @var children: - Specifies a list of all subtasks. A task without children is - called a leaf task index{leaf task} otherwise it is called a - parent task index{parent task}. This attribute is readonly. - - @var depth: - Specifies the depth of the task within the hierachy. This - attribute is readonly. - - @var index: - Specifies a structural index number. This attribute is readonly. - - @var path: - Specifies the path. - - @var copy_src: - Specifies the path to an other task. When you set this attribute, - all attributes (except of ME{start} and ME{end}) of copy_src will - be copied to the current task. This is usefull if you want to - define the same task, in diffent project definitions. It acts like - a task link. - - @var scenario: - The scenario which is currently evaluated. This attribute is readonly. - - @var dont_inherit: - A list of attribute names, which will be not inherited by - subtasks. - - @var calendar: - Specifies the task calendar. - - @var working_days_per_week: - Specifies the days within a working week. This value is used - internally to convert time differences from weeks to days. The - default value is 5 days. - - @var working_days_per_month: - Specifies the days within a working month. This value is used - internally to convert time differences from months to days. The - default value is 20 days. - - @var working_days_per_year: - Specifies the days within a working year. This value is used - internally to convert time differences from years to days The - default value is 200 days. - - @var working_hours_per_day: - Specifies the hours within a working day. This value is used - internally to convert time differences from are entered in days to - hours. The default value is 8 hours. - - @var minimum_time_unit: - Specifies the minimum resolution in minutes for the task - scheduling. The default value is 15 minutes. - - @var vacation: - Specifies a public vacation for the calendar. 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. - - @var extra_work: - Specifies additional worktime. 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. - - @var working_days: - Specifies the weekly working time within calendar. The format of - this attribute is: [ (day_range, time_range, ...), (day_range, time_range, ...), ... ]. - day_range is a comma sperated string of week days. Valid values - are mon, tue, wed, thu, fri, sat, sun. - time_range is string specifing a time interval like - 8:00-10:00. You can specified any number of time_ranges, following - the first. - - @var now: - Specifies the current daytime and is a date literal. ME{now} is - used to calculate several task attributes. - - """ - #@nonl - #@-node:<< description >> - #@nl - #@ << declarations >> - #@+node:<< declarations >> - # Variables for the gui interface - _date_completion = { "Date": 'Date("|")', - "max": "max(|)", - "min": "min(|)", - "Multi" : "Multi(|)" } - - - _delta_completion = { "Delta" : 'Delta("|")', - "Multi" : "Multi(|)" } - - - __attrib_completions__ = { \ - "def NewTask():" : "def |NewTask():\n", - "milestone": 'milestone = True', - "start": 'start = ', - "end": 'end = ', - "effort": 'effort = "|"', - "duration": 'duration = "|"', - "length": 'length = "|"', - "todo": 'todo = "|"', - "done": 'done = "|"', - "title": 'title = "|"', - "load": 'load = ', - "max_load": 'max_load = ', - "efficiency": 'efficiency = ', - "complete": 'complete = ', - "copy_src": 'copy_src =', - "__constraint__": '__constraint__():\n|"', - "priority": 'priority = ', - "balance" : 'balance = ', - "resource": 'resource = ', - "performed" : 'performed = [(|resource, "2002-02-01", "2002-02-05", "2H"),]', - "add_attrib": "add_attrib(|'name', None)", - "working_days_per_week": 'working_days_per_week = ', - "working_days_per_month": 'working_days_per_month = ', - "working_days_per_year": 'working_days_per_year = ', - "working_hours_per_day": 'working_hours_per_day = ', - "minimum_time_unit": 'minimum_time_unit = ', - "vacation": 'vacation = [("|2002-02-01", "2002-02-05")]', - "extra_work": 'extra_work = [("|2002-02-01", "2002-02-05")]', - "working_days" : 'working_days = ["|mon,tue,wed,thu,fri", "8:00-12:00", "13:00-17:00"]', - "now": 'now = "|"', - "calendar" : 'calendar = ', - "#load": { "YearlyMax": 'YearlyMax("|")', - "WeeklyMax": 'WeeklyMax("|")', - "MonthlyMax": 'MonthlyMax("|")', - "DailyMax": 'DailyMax("|")', - "VariableLoad" : "VariableLoad(|)"}, - "#max_load": { "YearlyMax": 'YearlyMax("|")', - "WeeklyMax": 'WeeklyMax("|")', - "MonthlyMax": 'MonthlyMax("|")', - "DailyMax": 'DailyMax("|")' }, - "#start": _date_completion, - "#end": _date_completion, - "#effort": _delta_completion, - "#duration": _delta_completion, - "#length": _delta_completion, - "#todo": _delta_completion, - "#done": _delta_completion, - "#resource" : "get_resource_completions", - "#calendar" : "get_calendar_completions", - "#balance": { "STRICT": "STRICT", - "SMART": "SMART", - "SLOPPY": "SLOPPY" } } - - - formats = { "start" : "%x %H:%M", - "end" : "%x %H:%M", - "performed_start" : "%x %H:%M", - "performed_end" : "%x %H:%M", - "load" : "%.2f", - "length" : "%dd{ %HH}{ %MM}", - "effort" : "%dd{ %HH}{ %MM}", - "estimated_effort" : "%dd{ %HH}{ %MM}", - "performed_effort" : "%dd{ %HH}{ %MM}", - "duration" : "%dd{ %HH}{ %MM}", - "complete" : "%i", - "priority" : "%i", - "todo" : "%dd{ %HH}{ %MM}", - "done" : "%dd{ %HH}{ %MM}", - "efficiency" : "%.2f", - "buffer" : "%dd{ %HH}{ %MM}", - "costs" : "%.2f", - "sum" : "%.2f", - "max" : "%.2f", - "min" : "%.2f", - "milestone" : "%s", - "resource" : "%s", - "booked_resource" : "%s", - "performed_resource" : "%s" } - - _constraint = None - _is_frozen = False - _is_compiled = False - _is_parent_referer = False - - scenario = None # only for autocompletion - milestone = False - performed = () - performed_resource = () - booked_resource = () - _performed_resource_length = () - _resource_length = () - dont_inherit = () - performed_start = None - performed_end = None - performed_work_time = pcalendar.Minutes(0) - - _setting_hooks = {} - #@nonl - #@-node:<< declarations >> - #@nl - #@ @+others - #@+node:__init__ - def __init__(self, func, name, parent=None, index=1): - - assert(type(func) == types.FunctionType) - - func_key = (func.func_code, func.func_closure and id(func.func_closure)) - - try: - instrumented = instrumentation_cache[func_key] - except KeyError: - instrumented = _instrument(func) - instrumented.org_code = func_key - instrumentation_cache[func_key] = instrumented - - func.task_func = instrumented # will be used in the gui - self._function = instrumented - self.name = name - self.up = parent - self.children = [] - self._sources = {} # all tasks, I am linked to - self._dependencies = {} # all tasks that link to me - self._original_values = {} - self._properties = {} # a registry of all non standard attributes - self.title = self.name - self.root = parent and parent.root or self - self.scenario = self.root.scenario - self.path = parent and parent.path + "." + name or name - self.depth = len(self.path.split(".")) - 1 - self.index = parent and ("%s.%i" % (parent.index, index)) \ - or str(index) - if self.formats.has_key(name): - raise AttributeError("Task name '%s' hides attribute of parent." \ - % name) - - cal = self.calendar - self._to_delta = cal.Minutes - self._to_start = cal.StartDate - self._to_end = cal.EndDate - - #@-node:__init__ - #@+node:__iter__ - def __iter__(self): - return _step_tasks(self) - #@-node:__iter__ - #@+node:__repr__ - def __repr__(self): - return "" % self.name - #@-node:__repr__ - #@+node:__cmp__ - def __cmp__(self, other): - try: - return cmp(self.path, other.path) - except Exception: - return cmp(self.path, other) - #@-node:__cmp__ - #@+node:__getattr__ - def __getattr__(self, name): - try: - if name[0] != "_": - parent = self.up - while parent: - if name not in parent.dont_inherit: - result = getattr(parent, name) - if not (isinstance(result, Task) and result.up == parent): - return result - - parent = parent.up - except AttributeError: - pass - except IndexError: - raise AttributeError() - - exception = AttributeError("'%s' is not a valid attribute of '%s'" - % (name, self.path)) - exception.is_frozen = self._is_frozen - raise exception - #@-node:__getattr__ - #@+node:_idendity_ - def _idendity_(self): return self.root.id + self.path[4:] - #@-node:_idendity_ - #@+node:_set_hook - def _set_hook(cls, attrib_name, function=None): - if function: - cls._setting_hooks[attrib_name] = function - else: - try: - del cls._setting_hooks[attrib_name] - except KeyError: pass - - - _set_hook = classmethod(_set_hook) - #@nonl - #@-node:_set_hook - #@+node:Public methods - #@+node:to_string - def to_string(self): return _StringConverter(self) - to_string = property(to_string) - #@nonl - #@-node:to_string - #@+node:indent_name - def indent_name(self, ident=" "): - """ - returns a indented name, according to its depth in the hierachy. - """ - - return ident * self.depth + self.name - - indent_name.attrib_method = True - indent_name.__call_completion__ = "indent_name()" - #@-node:indent_name - #@+node:costs - def costs(self, cost_name, mode="ep"): - """ - calculates the resource costs for the task. - cost_name is the name of a rate attribute of the reosurce - mode is character combination: - e calculates the estimated costs - p calculates the performed costs - ==> pe calculates all costs - """ - - if self.children: - return sum([ c.costs(cost_name, mode) for c in self.children]) - - costs = 0 - if 'e' in mode: - costs += sum(map(lambda rl: getattr(rl[0], cost_name) * rl[1], - self._resource_length)) - - if 'p' in mode: - costs += sum(map(lambda rl: getattr(rl[0], cost_name) * rl[1], - self._performed_resource_length)) - - costs /= (60.0 * self.root.calendar.working_hours_per_day) - return round(costs, 2) - - costs.attrib_method = True - costs.__call_completion__ = 'costs("|")' - #@-node:costs - #@+node:sum - def sum(self, attrib_name): - val = 0 - - if self.children: - val += sum(map(lambda c: c.sum(attrib_name), self.children)) - if self.is_inherited(attrib_name): - return val - - if attrib_name not in self.dont_inherit: - return val - - return val + getattr(self, attrib_name) - - sum.attrib_method = True - sum.__call_completion__ = 'sum("|")' - - #@-node:sum - #@+node:min - def min(self, attrib_name): - if self.children: - return min(map(lambda c: c.min(attrib_name), self.children)) - - return getattr(self, attrib_name) - - min.attrib_method = True - min.__call_completion__ = 'min("|")' - - #@-node:min - #@+node:max - def max(self, attrib_name): - if self.children: - return max(map(lambda c: c.max(attrib_name), self.children)) - - return getattr(self, attrib_name) - - max.attrib_method = True - max.__call_completion__ = 'max("|")' - - #@-node:max - #@+node:all_resources - def all_resources(self): - result = self._all_resources_as_dict() - result = result.keys() - result.sort() - return result - #@-node:all_resources - #@+node:get_task - def get_task(self, path=None): - """ - Returns a task with the given path. - """ - - if not path: - return self - - names = path.split(".") - rest = ".".join(names[1:]) - result = getattr(self, names[0], None) - return isinstance(result, Task) and result.get_task(rest) or None - #@-node:get_task - #@+node:snapshot - def snapshot(self, indent="", name=None): - text = indent + "def %s():\n" % (name or self.name) - indent += " " - for name in ("priority", "balance", "complete", - "milestone", "end", "start", "effort", "load"): - val = getattr(self, name, None) - if val is None: - continue - - if name[0] == "_": - name = name[1:] - - text += "%s%s = %s\n" % (indent, name, _as_string(val)) - - for name in self._properties: - if name.startswith("performed"): continue - val = getattr(self, name, None) - try: - if issubclass(val, resource.Resource): continue - except TypeError: - pass - text += "%s%s = %s\n" % (indent, name, _as_string(val)) - - resources = tuple(self._iter_booked_resources()) - if resources: - text += "%sresource = \\\n" % indent - def make_resource(res): - return "%s %s" \ - % (indent, res.snapshot()) - - text += "&\\\n".join(map(make_resource, resources)) + "\n" - - def make_resource_booking(res): - def make_booking(booking): - return '%s (%s, "%s", "%s", "%sM"),' \ - % (indent, res.name, - booking.book_start.strftime("%Y%m%d %H:%M"), - booking.book_end.strftime("%Y%m%d %H:%M"), - booking.work_time) - - return "\n".join(map(make_booking, res.get_bookings(self))) - - - text += "%sperformed = [\n" % indent - text += "\n".join(map(make_resource_booking, resources)) + "]" - - - child_text = map(lambda c: c.snapshot(indent), self.children) - text += "\n\n" - text += "".join(child_text) - - return text - #@-node:snapshot - #@+node:is_inherited - def is_inherited(self, attrib_name): - return not self.__dict__.has_key(attrib_name) - #@-node:is_inherited - #@+node:formatter - def formatter(self, attrib_name, arg=None, format=None): - """returns a function which is able - to convert the value of the given attrib_name to a string""" - - formats = self.formats - format = format or formats.get(attrib_name) - - if attrib_name in ("start", "end", "length", "effort", - "done", "todo", "buffer", "estimated_effort", - "performed_effort", "performed_start", "performed_end"): - def save_strftime(v): - try: - return v.strftime(format) - #except AttributeError: some bug avoid catching this exception - except Exception: - return str(v) - - return save_strftime - - if attrib_name == "duration": - def save_strftime(v): - try: - return v.strftime(format, True) - except AttributeError: - return str(v) - - return save_strftime - - if attrib_name in ("booked_resource", "performed_resource"): - def get_resource_name(v): - title = getattr(v, "title", None) - if title: return title - return ", ".join([r.title for r in v]) - return get_resource_name - - if arg and attrib_name in ("costs", "sum", "max", "min"): - format = formats.get("%s(%s)" % (attrib_name, arg), format) - - if format: - return lambda v: locale.format(format, v, True) - - return str - #@-node:formatter - #@-node:Public methods - #@+node:Resource allocation Methods - #@+node:_all_resources_as_dict - def _all_resources_as_dict(self): - if self.children: - result = {} - for c in self.children: - result.update(c._all_resources_as_dict()) - - return result - - if self.resource: - return dict(map(lambda r: (r, 1), self.resource.all_members())) - - return {} - #@-node:_all_resources_as_dict - #@+node:_test_allocation - def _test_allocation(self, resource_state, allocator): - resource = self.resource._get_resources(resource_state) - if not resource: - return False - - return allocator.test_allocation(self, resource) - #@-node:_test_allocation - #@+node:_allocate - def _allocate(self, state, allocator): - allocator.allocate(self, state) - #activate cache for done and todo - - if self.start.to_datetime() > self.end.to_datetime(): - #this can happen when performed effort are - #during non working time - tmp = self.start - self.start = self.end - self.end = tmp - - for r in self.performed_resource: - r.correct_bookings(self) - - self._resource_length = map(lambda r: (weakref.proxy(r), \ - r.length_of(self)), - self._iter_booked_resources()) - #@-node:_allocate - #@+node:_convert_performed - def _convert_performed(self, all_resources): - performed = self.performed - if not performed: return False - - if not isinstance(performed, (tuple, list)) \ - or not isinstance(performed[0], (tuple, list)) \ - or not len(performed[0]) >= 3: - self._raise(TypeError("""The format of the performed attribute must be: - [( res_name, start_literal, end_literal, working_time ), ... ]. - """), "performed") - - round_down_delta = self.root.calendar.minimum_time_unit / 2 - round_down_delta = datetime.timedelta(minutes=round_down_delta) - - def convert_item(index): - item = performed[index] - res, start, end = item[:3] - if isinstance(res, str): - found = filter(lambda r: r.name == res, all_resources) - if found: res = found[0] - - try: - if not isinstance(res, (resource.Resource, - resource._MetaResource)): - raise ValueError("the resource '%s' is unknown." % res) - - start = _to_datetime(start) - end = _to_datetime(end) - - if len(item) > 3: - working_time = self._to_delta(item[3]).round() - else: - working_time = self._to_delta(end - start, True) - - return ((res, start, end, working_time), index) - except Exception, exc: - self._raise(exc.__class__("Item %i: %s" \ - % (index + 1, str(exc))), - "performed") - - converted = dict(map(convert_item, range(len(performed)))) - converted = converted.items() - converted.sort() - - #check for overlapping items - last_res = None - for item, index in converted: - res, start, end, work_time = item - if last_res == res and start < last_end: - self._warn("Items %i, %i: %s and %s are overlapping." \ - % (last_index + 1, index + 1, - str(performed[last_index]), - str(performed[index])), - "performed") - - last_res = res - last_end = end - last_index = index - - self._performed = map(lambda x: x[0], converted) - return True - #@-node:_convert_performed - #@+node:_allocate_performed - def _allocate_performed(self, performed): - if not performed: return - - to_delta = self._to_delta - to_start = self._to_start - to_end = self._to_end - - last = datetime.datetime.min - first = datetime.datetime.max - effort = 0 - work_time_sum = 0 - zero_minutes = to_delta(0) - minimum_time_unit = to_delta(self.calendar.minimum_time_unit) - summary = {} - - for item in performed: - res, start, end, work_time = item - effort += work_time * self.efficiency * res.efficiency - work_time_sum += work_time - - res = res() - ss, es, wts = summary.get(res, (datetime.datetime.max, - datetime.datetime.min, - zero_minutes)) - summary[res] = (min(ss, start), max(es, end), wts + work_time) - - for r, v in summary.iteritems(): - start, end, work_time = v - assert(start.__class__ is datetime.datetime) - assert(end.__class__ is datetime.datetime) - - #the booking limits should be inside the workingtime - #to display them correct in resource charts - cstart = to_start(start).to_datetime() - if cstart > start: cstart = to_end(start).to_datetime() - - cend = to_end(end).to_datetime() - if cend < end: cend = to_start(end).to_datetime() - - if self.root.is_snapshot: - delta = to_end(cend) - to_start(cstart) - else: - delta = to_delta(cend - cstart).round() - - if not delta: - delta = minimum_time_unit - - book_load = float(work_time) / delta - r().book_task(self, cstart, cend, book_load, work_time, True) - last = max(end, last) - first = min(start, first) - - self._performed_resource_length = tuple([ (r, v[2]) for r, v in summary.iteritems() ]) - self.performed_resource = tuple(summary.keys()) - self.performed_end = last - self.performed_start = first - self.performed_effort = to_delta(effort) - self.performed_work_time = to_delta(work_time_sum) - self._check_completion() - #@-node:_allocate_performed - #@+node:_iter_booked_resources - def _iter_booked_resources(self): - result = dict(map(lambda r: (r, 1), self.performed_resource)) - result.update(dict(map(lambda r: (r, 1), self.booked_resource))) - return result.iterkeys() - #@-node:_iter_booked_resources - #@-node:Resource allocation Methods - #@+node:Compile Methods - #@+node:_generate - def _generate(self, deferred=None): - do_raise = False - deferred = deferred or [ self ] - while deferred: - new_deferred = [] - for task in deferred: - task._compile(new_deferred, do_raise) - - do_raise = deferred == new_deferred - deferred = new_deferred - #@-node:_generate - #@+node:_recalc_properties - def _recalc_properties(self): - if not self._properties: return - self.__compile_function([], False, _MeProxyRecalc(self)) - self._is_compiled = True - #@-node:_recalc_properties - #@+node:_compile - def _compile(self, deferred, do_raise): - self.dont_inherit = () - self._constraint = None - self._original_values.clear() - self._properties.clear() - - try: - self.__at_compile - #@ << raise child recursion error >> - #@+node:<< raise child recursion error >> - self._raise(RecursionError("A child defines a "\ - "recursive definition at %s" % self.path)) - #@-node:<< raise child recursion error >> - #@nl - except AttributeError: - self.__at_compile = self, "" - - try: - self.__compile_function(deferred, do_raise, _MeProxy(self)) - finally: - del self.__at_compile - - for c in self.children: - if not c._is_compiled: - c._compile(deferred, do_raise) - - if self._is_compiled: - self.__check_milestone() - self.__check_task() - self.root.has_actual_data |= self.__dict__.has_key("performed") - - #@-node:_compile - #@+node:__compile_function - def __compile_function(self, deferred, do_raise, me_instance): - self._is_compiled = self._is_frozen - - restore_globals = [] - globals_ = self._function.func_globals - - #@ << set function global values >> - #@+node:<< set function global values >> - def to_value_wrapper(a): - if isinstance(a, _ValueWrapper): - return a - - return _ValueWrapper(a, [(None, None)]) - - def my_max(*args): - return max(map(to_value_wrapper, args)) - - def my_min(*args): - return min(map(to_value_wrapper, args)) - - globals_["me"] = me_instance - - if self._is_compiled: - globals_["up"] = self.up - globals_["root"] = self.root - else: - globals_["up"] = _Path(self.up, "up") - globals_["root"] = _Path(self.root, "root") - - globals_["Delta"] = self._to_delta - globals_["Date"] = self._to_start - globals_["max"] = my_max - globals_["min"] = my_min - globals_["add_attrib"] = me_instance.add_attrib - #@nonl - #@-node:<< set function global values >> - #@nl - #@ << set me in global functions >> - #@+node:<< set me in global functions >> - #@+at - # Is used for functions like YearlyMax, MonthlyMax, .... - #@-at - #@@code - for name in self._function.global_names: - try: - obj = globals_[name] - if isinstance(obj, types.FunctionType): - fg = obj.func_globals - if not fg.has_key("me") and "me" in obj.func_code.co_names: - restore_globals.append(fg) - fg["me"] = me_instance - except KeyError: continue - #@nonl - #@-node:<< set me in global functions >> - #@nl - try: - #@ << eval function >> - #@+node:<< eval function >> - if do_raise: - try: - self._function() - self._is_compiled = True - except _IncompleteError, e: - src = e.args[1] - if src is not self: - self.__at_compile = e.args[1:] - src._compile([], True) - - raise - else: - try: - self._function() - self._is_compiled = True - except AttributeError, e: - #print "AttributeError:", e, self.name, e.is_frozen, do_raise - deferred.append(self) - except _IncompleteError: - #print "_IncompleteError:", id(self), self.name, do_raise - deferred.append(self) - except RecursionError: - self._is_parent_referer = True - deferred.append(self) - #@nonl - #@-node:<< eval function >> - #@nl - finally: - for fg in restore_globals: - del fg["me"] - - #@-node:__compile_function - #@-node:Compile Methods - #@+node:Setting methods - #@+node:_set_attrib - def _set_attrib(self, name, value): - - if value is _NEVER_USED_: return - - try: - value = self._setting_hooks[name](self, name, value) - except KeyError: pass - - if name == "__constraint__": - self._constraint = value - return - - if type(value) == types.FunctionType: - if value.func_code.co_argcount == 0: - #@ << add child task >> - #@+node:<< add child task >> - try: - task = self.__dict__[name] - except KeyError: - task = Task(value, name, self, len(self.children) + 1) - self.children.append(task) - setattr(self, task.name, task) - return - #@nonl - #@-node:<< add child task >> - #@nl - - if name[0] == "_": - #private vars will not be set - return - - if isinstance(value, _Path): - value = value._task - - set_method = getattr(self, "_set_" + name, None) - if set_method: - #@ << set standard attribute >> - #@+node:<< set standard attribute >> - if type(value) == types.DictionaryType: - self.root.all_scenarios.update(value.keys()) - value = value.get(self.scenario, value["_default"]) - - self.__set_sources(name, value) - self._original_values[name] = value - set_method(_val(value)) - #@nonl - #@-node:<< set standard attribute >> - #@nl - else: - #@ << set userdefined attribute >> - #@+node:<< set userdefined attribute >> - if callable( getattr(self.__class__, name, None)): - raise NameError('You may not use "%s" as attribute' % name) - - setattr(self, name, value) - self._properties[name] = True - self.__set_sources(name, value) - #@nonl - #@-node:<< set userdefined attribute >> - #@nl - #@-node:_set_attrib - #@+node:read only attributes - #@+node:_set_name - def _set_name(self, value): - raise AttributeError("The attribute 'name' is readonly.") - #@nonl - #@-node:_set_name - #@+node:_set_done - def _set_done(self, value): - raise AttributeError("The attribute 'done' is readonly.") - #@nonl - #@-node:_set_done - #@+node:_set_performed_work_time - def _set_performed_work_time(self, value): - raise AttributeError("The attribute 'performed_work_time' is readonly.") - #@nonl - #@-node:_set_performed_work_time - #@+node:_set_booked_resource - def _set_booked_resource(self, value): - raise AttributeError("The attribute 'booked_resource' is readonly.") - #@nonl - #@-node:_set_booked_resource - #@+node:_set_performed_effort - def _set_performed_effort(self, value): - raise AttributeError("The attribute 'performed_effort' is readonly.") - #@nonl - #@-node:_set_performed_effort - #@+node:_set_children - def _set_children(self, value): - raise AttributeError("The attribute 'children' is readonly.") - #@nonl - #@-node:_set_children - #@+node:_set_depth - def _set_depth(self, value): - raise AttributeError("The attribute 'depth' is readonly.") - #@nonl - #@-node:_set_depth - #@+node:_set_index - def _set_index(self, value): - raise AttributeError("The attribute 'index' is readonly.") - #@nonl - #@-node:_set_index - #@+node:_set_scenario - def _set_scenario(self, value): - raise AttributeError("The attribute 'scenario' is readonly.") - #@nonl - #@-node:_set_scenario - #@+node:_set_buffer - def _set_buffer(self, value): - raise AttributeError("The attribute 'buffer' is readonly.") - #@nonl - #@-node:_set_buffer - #@-node:read only attributes - #@+node:_set_start - def _set_start(self, value): - self.__start_class = value.__class__ - self.start = self._to_start(value).round() - #@-node:_set_start - #@+node:_set_end - def _set_end(self, value): - self.end = self._to_end(value) - #@-node:_set_end - #@+node:_set_max_load - def _set_max_load(self, max_load): - self.max_load = float(max_load) - #@-node:_set_max_load - #@+node:_set_load - def _set_load(self, load): - self.load = float(load) - #@-node:_set_load - #@+node:_set_length - def _set_length(self, value): - self.length = self._to_delta(value).round() - #@-node:_set_length - #@+node:_set_effort - def _set_effort(self, value): - self.effort = self._to_delta(value).round() - #@-node:_set_effort - #@+node:_set_duration - def _set_duration(self, value): - self.duration = self._to_delta(value, True).round() - #@-node:_set_duration - #@+node:_set_complete - def _set_complete(self, value): - self.complete = value - #@-node:_set_complete - #@+node:_set_done - def _set_done(self, value): - self.done = self._to_delta(value).round() - #@-node:_set_done - #@+node:_set_todo - def _set_todo(self, value): - self.todo = self._to_delta(value).round() - #@-node:_set_todo - #@+node:_set_milestone - def _set_milestone(self, value): - self.milestone = value - #@-node:_set_milestone - #@+node:_set_resource - def _set_resource(self, value): - if not value: - self.resource = None - return - - if isinstance(value, (tuple, list)): - value = reduce(lambda a, b: a & b, value) - print 'Value:::',value - self.resource = value() - - #@-node:_set_resource - #@+node:_set_copy_src - def _set_copy_src(self, value): - if isinstance(value, _MeProxy): - raise RuntimeError("Cannot copy me.") - - if not value._is_compiled: - raise _IncompleteError(value, "copy_src") - - if value.resource and not self.resource: - self.resource = value.resource - - if value.balance and not self.balance: - self.balance = value.balance - - copy_parms = ("priority", "todo", "complete", - "_constraint", "load", "length", - "effort", "duration") - - for p in copy_parms: - v = value.__dict__.get(p) - if v: setattr(self, p, v) - - self.copy_src = value - self._properties.update(value._properties) - for k in value._properties.iterkeys(): - setattr(self, k, getattr(value, k)) - #@-node:_set_copy_src - #@+node:__set_sources - def __set_sources(self, attrib_name, value): - #@ << find references >> - #@+node:<< find references >> - def make_ref(val): - if isinstance(val, _ValueWrapper): - return val._ref - - if isinstance(val, Task): - return [(val, "")] - - return [] - - if isinstance(value, (list, tuple)): - sources = _refsum(map(make_ref, value)) - else: - sources = make_ref(value) - #@nonl - #@-node:<< find references >> - #@nl - if not sources: return - - #track only dependcies within the same project - root = self.root - sources = [ task.path + "." + attrib - for task, attrib in sources - if task and task.root is root ] - self._sources[attrib_name] = tuple(sources) - attr_path = self.path + "." + attrib_name - - #set dependencies of my sources - for d in sources: - path, attrib = _split_path(d) - task = self.get_task(path) - r_d = task._dependencies - d_l = r_d.setdefault(attrib, {}) - d_l[attr_path] = True - #@-node:__set_sources - #@+node:Calendar Setters - #@+node:_set_calendar - def _set_calendar(self, value): - self.calendar = value - self._to_delta = value.Minutes - self._to_start = value.StartDate - self._to_end = value.EndDate - self.__renew_dates() - - #@-node:_set_calendar - #@+node:__renew_dates - def __renew_dates(self): - for attrib in ("effort", "start", "end", "length", "todo"): - try: - - self._set_attrib(attrib, self._original_values[attrib]) - except KeyError: - pass - - #@-node:__renew_dates - #@+node:__make_calendar - def __make_calendar(self): - if not "calendar" in self.__dict__: - cal = self.calendar = self.calendar.clone() - self._to_delta = cal.Minutes - self._to_start = cal.StartDate - self._to_end = cal.EndDate - #@nonl - #@-node:__make_calendar - #@+node:_set_vacation - def _set_vacation(self, value): - self.__make_calendar() - self.calendar.set_vacation(value) - self._properties["vacation"] = True - self.vacation = value - self.__renew_dates() - #@-node:_set_vacation - #@+node:_set_extra_work - def _set_extra_work(self, value): - self.__make_calendar() - self.calendar.set_extra_work(value) - self._properties["extra_work"] = True - self.extra_work = value - self.__renew_dates() - #@-node:_set_extra_work - #@+node:_set_working_days - def _set_working_days(self, value): - - if type(value[0]) is str: - value = (value, ) - - self.working_days = value - self._properties["working_days"] = True - self.__make_calendar() - - for v in value: - day_range = v[0] - tranges = tuple(v[1:]) - self.calendar.set_working_days(day_range, *tranges) - - self.__renew_dates() - #@nonl - #@-node:_set_working_days - #@+node:_set_minimum_time_unit - def _set_minimum_time_unit(self, value): - self.__make_calendar() - self.calendar.minimum_time_unit = value - self._properties["minimum_time_unit"] = True - #@-node:_set_minimum_time_unit - #@+node:_get_minimum_time_unit - def _get_minimum_time_unit(self): - return self.calendar.minimum_time_unit - - minimum_time_unit = property(_get_minimum_time_unit) - #@-node:_get_minimum_time_unit - #@+node:_set_working_days_per_week - def _set_working_days_per_week(self, value): - - self.__make_calendar() - self.calendar.working_days_per_week = value - self._properties["working_days_per_week"] = True - #@-node:_set_working_days_per_week - #@+node:_get_working_days_per_week - def _get_working_days_per_week(self): - return self.calendar.working_days_per_week - - working_days_per_week = property(_get_working_days_per_week) - #@-node:_get_working_days_per_week - #@+node:_set_working_days_per_month - def _set_working_days_per_month(self, value): - self.__make_calendar() - self.calendar.working_days_per_month = value - self._properties["working_days_per_month"] = True - #@-node:_set_working_days_per_month - #@+node:_get_working_days_per_month - def _get_working_days_per_month(self): - return self.calendar.working_days_per_month - - working_days_per_month = property(_get_working_days_per_month) - #@-node:_get_working_days_per_month - #@+node:_set_working_days_per_year - def _set_working_days_per_year(self, value): - self.__make_calendar() - self.calendar.working_days_per_year = value - self._properties["working_days_per_year"] = True - #@-node:_set_working_days_per_year - #@+node:_get_working_days_per_year - def _get_working_days_per_year(self): - return self.calendar.working_days_per_year - - working_days_per_year = property(_get_working_days_per_year) - #@-node:_get_working_days_per_year - #@+node:_set_working_hours_per_day - def _set_working_hours_per_day(self, value): - self.__make_calendar() - self.calendar.working_hours_per_day = value - self._properties["set_working_hours_per_day"] = True - #@-node:_set_working_hours_per_day - #@+node:_get_working_hours_per_day - def _get_working_hours_per_day(self): - return self.calendar.working_hours_per_day - - working_hours_per_day = property(_get_working_hours_per_day) - #@-node:_get_working_hours_per_day - #@+node:_set_now - def _set_now(self, value): - proxy = weakref.proxy - self.calendar.now = _to_datetime(value) - #@-node:_set_now - #@-node:Calendar Setters - #@-node:Setting methods - #@+node:Freezer Methods - #@+node:_unfreeze - def _unfreeze(self, attrib_name): - if self.__dict__.has_key(attrib_name): - del self.__dict__[attrib_name] - #@-node:_unfreeze - #@+node:_wrap_attrib - def _wrap_attrib(self, method): - attrib_name = method.__name__[7:] - recursion_attrib = "_rec" + attrib_name - - try: - dest, dattr = self.__at_compile - raise RecursionError("Recursive definition of %s(%s) and %s(%s)" \ - % (self.path, attrib_name, dest.path, dattr)) - except AttributeError: pass - - if not self._is_compiled: - raise _IncompleteError(self, attrib_name) - - try: - getattr(self, recursion_attrib) - raise RecursionError(self, attrib_name) - except AttributeError: pass - - setattr(self, recursion_attrib, True) - - try: - result = method(self) - - if self._is_frozen: - setattr(self, attrib_name, result) - - return result - finally: - delattr(self, recursion_attrib) - #@-node:_wrap_attrib - #@+node:_find_frozen - def _find_frozen(self, attrib_name, default=None): - value = self.__dict__.get(attrib_name) - if value is not None: - return value - - up = self.up - return up and up._find_frozen(attrib_name) or default - #@-node:_find_frozen - #@-node:Freezer Methods - #@+node:Calculation Methods - #@+node:__calc_performed_effort - def __calc_performed_effort(self): - if self.children: - return self._to_delta(sum([ t.performed_effort for t in self.children ])) - - return pcalendar.Minutes(0) - - performed_effort = _TaskProperty(__calc_performed_effort) - #@-node:__calc_performed_effort - #@+node:__calc_estimated_effort - def __calc_estimated_effort(self): - if self.children: - return self._to_delta(sum([ t.estimated_effort for t in self.children ])) - - return self.effort - - estimated_effort = _TaskProperty(__calc_estimated_effort) - #@-node:__calc_estimated_effort - #@+node:__calc_start - def __calc_start(self): - to_start = self._to_start - - if self.children: - try: - return min([ to_start(t.start) for t in self.children - if not t._is_parent_referer ]) - except ValueError: - #@ << raise child recursion error >> - #@+node:<< raise child recursion error >> - self._raise(RecursionError("A child defines a "\ - "recursive definition at %s" % self.path)) - #@-node:<< raise child recursion error >> - #@nl - - try: - end = self.end - duration = self.__dict__.get("duration") - if duration is not None: - start = end.to_datetime() - datetime.timedelta(minutes=duration) - else: - start = end - self.length - - return to_start(start) - - except RecursionError: - start = self._find_frozen("start") - if start: return to_start(start) - #@ << raise recursion error >> - #@+node:<< raise recursion error >> - raise RecursionError("you have to specify a "\ - "start or an end at %s" % self.path) - #@nonl - #@-node:<< raise recursion error >> - #@nl - - start = _TaskProperty(__calc_start) - - #@-node:__calc_start - #@+node:__calc_end - def __calc_end(self): - to_end = self._to_end - - if self.children: - try: - return max([ to_end(t.end) for t in self.children - if not t._is_parent_referer ]) - except ValueError: - #@ << raise child recursion error >> - #@+node:<< raise child recursion error >> - self._raise(RecursionError("A child defines a "\ - "recursive definition at %s" % self.path)) - #@-node:<< raise child recursion error >> - #@nl - - try: - start = self.start - duration = self.__dict__.get("duration") - if duration is not None: - end = start.to_datetime() + datetime.timedelta(minutes=duration) - else: - end = start + self.length - - return to_end(end) - - except RecursionError: - end = self._find_frozen("end") - if end: return to_end(end) - #@ << raise recursion error >> - #@+node:<< raise recursion error >> - raise RecursionError("you have to specify a "\ - "start or an end at %s" % self.path) - #@nonl - #@-node:<< raise recursion error >> - #@nl - - - end = _TaskProperty(__calc_end) - #@-node:__calc_end - #@+node:__calc_load - def __calc_load(self): - length = self.__dict__.get("length") - effort = self.__dict__.get("effort") - - if length is not None and effort is not None: - return float(effort) / (float(length) or 1.0) - - load = self._find_frozen("load") - if load is not None: return load - return 1.0 - - load = _TaskProperty(__calc_load) - #@-node:__calc_load - #@+node:__calc_length - def __calc_length(self): - effort = self.__dict__.get("effort") - if effort is None: - return self.end - self.start - - return self._to_delta(effort / self.load) - - length = _RoundingTaskProperty(__calc_length, "length") - #@-node:__calc_length - #@+node:__calc_duration - def __calc_duration(self): - return self._to_delta(self.end.to_datetime()\ - - self.start.to_datetime(), True) - - duration = _TaskProperty(__calc_duration) - #@-node:__calc_duration - #@+node:__calc_effort - def __calc_effort(self): - if self.children: - return self._to_delta(sum([ t.effort for t in self.children ])) - - return self._to_delta(self.length * self.load) - - effort = _RoundingTaskProperty(__calc_effort, "effort") - #@-node:__calc_effort - #@+node:__calc_done - def __calc_done(self): - if self.children: - dones = map(lambda t: t.done, self.children) - return self._to_delta(sum(dones)) - - res = self._iter_booked_resources() - done = sum(map(lambda r: r.done_of(self), res)) - - complete = self.__dict__.get("complete") - todo = self.__dict__.get("todo") - - if not done and complete == 100 or todo == 0: - #if now is not set - done = self.effort - - return self._to_delta(done) - - done = _TaskProperty(__calc_done) - #@-node:__calc_done - #@+node:__calc_buffer - def __calc_buffer(self): - if self.children: - return self._to_delta(min(map(lambda t: t.buffer, self.children))) - - scenario = self.scenario - end = self.end - old_end = self.__dict__.get("end") - - #@ << find all tasks, that depend on my end >> - #@+node:<< find all tasks, that depend on my end >> - deps = { } - task = self - while task: - deps.update(task._dependencies.get("end", {})) - task = task.up - #@nonl - #@-node:<< find all tasks, that depend on my end >> - #@nl - - #@ << define unfreeze_parents >> - #@+node:<< define unfreeze_parents >> - def unfreeze_parents(): - task = self.up - while task: - task._unfreeze("end") - task = task.up - #@nonl - #@-node:<< define unfreeze_parents >> - #@nl - - buffers = [ ] - for d in deps.keys(): - path, attrib = _split_path(d) - if attrib != "start": - continue - - #@ << calculate buffer to descendant 'd' >> - #@+node:<< calculate buffer to descendant 'd' >> - unfreeze_parents() - - # the following code considers a expressione like - # start = predecessor.end + Delta("1d") the buffer - # calculation must be aware of the 1d delay. - # (therefore a simple succ_start - end would be - # incorrect) - # Solution: Simluate a later end and calculate the - # real delay - - succ_task = self.get_task(path) - simulated_task = Task(succ_task._function, - succ_task.name, - succ_task.up, 1) - - current_start = succ_task.start - simulated_end = current_start - self.end = current_start - - simulated_task._generate() - simulated_start = simulated_task.start - - unfreeze_parents() - if old_end: self.end = old_end - else: self._unfreeze("end") - del simulated_task - - current_delay = current_start - end - simulated_delay = simulated_start - simulated_end - real_delay = current_delay - simulated_delay - try: - buffer_ = real_delay + succ_task.buffer - except RecursionError, err: - self._raise(err) - #@nonl - #@-node:<< calculate buffer to descendant 'd' >> - #@nl - - buffers.append(buffer_) - if not buffer_: - break - - if buffers: - return self._to_delta(min(buffers)) - - return not self.milestone \ - and self.root.end - end \ - or self._to_delta(0) - - buffer = _TaskProperty(__calc_buffer) - #@-node:__calc_buffer - #@+node:__calc_complete - def __calc_complete(self): - done = self.done - todo = self.todo - return int(100.0 * done / ((done + todo) or 1)) - - complete = _TaskProperty(__calc_complete) - #@-node:__calc_complete - #@+node:__calc_todo - def __calc_todo(self): - complete = self.__dict__.get("complete") - if complete: - # effort = done + todo - # done done - # complete = ------ ==> todo = -------- - done - # effort complete - complete = float(complete) - done = self.done - if done: - done = float(done) - return self._to_delta(done * 100.0 / complete - done) - return self._to_delta(self.effort * complete / 100.0) - - if self.children: - todos = map(lambda t: t.todo, self.children) - return self._to_delta(sum(todos)) - - todo = sum(map(lambda r: r.todo_of(self), self.booked_resource)) - return self._to_delta(max(todo, self.effort - self.done)) - - todo = _TaskProperty(__calc_todo) - #@-node:__calc_todo - #@-node:Calculation Methods - #@+node:Check Methods - #@+node:__check_task - def __check_task(self): - if self.children: return - - start = self._find_frozen("start") - end = self._find_frozen("end") - - if not (start or end): - self._raise(ValueError("You must specify either a"\ - " start or an end attribute")) - - if start and end: return - - length = self.__dict__.get("length") - duration = self.__dict__.get("duration") - effort = self.__dict__.get("effort") - if not (effort or length or duration): - #set a default value - self._set_effort("1d") - #self._raise(ValueError("You must specify either a"\ - # " length or a duration or "\ - # "an effort attribute")) - #@-node:__check_task - #@+node:__check_milestone - def __check_milestone(self): - if not self.milestone: return - - self.length = self._to_delta(0) - start = self.__dict__.get("start") - if not start: - self._raise(ValueError("Milestone must have start attribute"), - "milstone") - - if self.__start_class.__name__ == "edt": - #the milestone is probably dependent on the end date of - #an other task (see edt in pcalendar) ==> start at the end date - self.start = self.end = self._to_end(self.start) - else: - self.start = self.end = self._to_start(self.start) - - #@-node:__check_milestone - #@+node:_check_completion - def _check_completion(self): - if not self.performed_effort: return - if self.root.is_snapshot: return - - # allocation is not done yet ==> self.todo, self.done, - # self.complete cannot be calculated - if self._find_frozen("complete", 0) < 100 \ - and self.__dict__.get("todo", 1) > 0: - return - - start = self.performed_start - end = self.performed_end - #ensure that self.start.to_datetime() < self.end.to_datetime() - cstart = self._to_start(start) - if cstart.to_datetime() > start: cstart = self._to_end(start) - - cend = self._to_end(end) - if cend.to_datetime() < end: cend = self._to_start(end) - - self.start = cstart - self.end = cend - - if self.performed_effort != self.effort: - self.estimated_effort = self.effort - self.effort = self.performed_effort - #@-node:_check_completion - #@+node:check - def check(self): - if self._constraint and self._is_compiled: - globals_ = self._function.func_globals - globals_["me"] = self - globals_["up"] = self.up - globals_["root"] = self.root - globals_["assert_"] = self.__assert - self._constraint() - #@-node:check - #@-node:Check Methods - #@+node:Error Methods - #@+node:__assert - def __assert(self, value): - if not value: - warnings.warn('assertion in scenario: "%s"' % self.scenario, - RuntimeWarning, 2) - #@-node:__assert - #@+node:_warn - def _warn(self, message, attrib=None, level=2): - self.__compile_function([], True, _MeProxyWarn(self, attrib, message)) - #@-node:_warn - #@+node:_raise - def _raise(self, exc, attrib=None): - self.__compile_function([], True, _MeProxyError(self, attrib, exc)) - raise exc - #@-node:_raise - #@-node:Error Methods - #@-others -#@nonl -#@-node:class Task -#@-node:Task -#@+node:Projects -#@+node:class _ProjectBase -class _ProjectBase(Task): - """ - Base class for all projects. - """ - #@ << class _ProjectBase declarations >> - #@+node:<< class _ProjectBase declarations >> - __attrib_completions__ = { } - __attrib_completions__.update(Task.__attrib_completions__) - del __attrib_completions__["milestone"] #project cannot be milestones - - priority = 500 - efficiency = 1.0 - max_load = 1.0 - balance = 0 - resource = None - copy_src = None - has_actual_data = False - is_snapshot = False - - #@-node:<< class _ProjectBase declarations >> - #@nl - #@ @+others - #@+node:__init__ - def __init__(self, top_task, scenario="_default", id=""): - self.calendar = pcalendar.Calendar() - Task.__init__(self, top_task, top_task.func_name) - self.id = id or self.name - self.scenario = scenario - self.all_scenarios = set(("_default",)) - self.path = "root" - self._globals = top_task.func_globals.copy() - self._generate() - - #@-node:__init__ - #@+node:_idendity_ - def _idendity_(self): return self.id - #@-node:_idendity_ - #@+node:_restore_globals - def _restore_globals(self): - self._function.func_globals.clear() - self._function.func_globals.update(self._globals) - del self._globals - #@-node:_restore_globals - #@+node:free - def free(self): - all_resources = self.all_resources() - for r in all_resources: - r().unbook_tasks_of_project(self.id, self.scenario) - - for t in self: - t.booked_resource = () - - return all_resources - #@-node:free - #@+node:_get_balancing_list - def _get_balancing_list(self): - - try: - cached_list = balancing_cache[self._function.org_code] - if len(cached_list) != len(tuple(self)): - # different scenarios can have different tasks - raise KeyError() - - except KeyError: - cached_list = _build_balancing_list(self) - balancing_cache[self._function.org_code] = cached_list - else: - cached_list = [ self.get_task(t.path) for t in cached_list ] - - return cached_list - #@-node:_get_balancing_list - #@+node:snapshot - def snapshot(self, indent="", name=None): - text = Task.snapshot(self, indent, name) - - lines = text.splitlines(True) - indent += " " - - def make_resource(r): - return '%sclass %s(Resource): title = "%s"\n' \ - % (indent, r.name, r.title) - - now = datetime.datetime.now().strftime("%x %H:%M") - resource_text = map(lambda r: make_resource(r), self.all_resources()) - lines.insert(1, "%sfrom faces import Resource\n" % indent) - lines.insert(2, "".join(resource_text) + "\n") - lines.insert(3, '%snow = "%s"\n' % (indent, now)) - lines.insert(4, '%sis_snapshot = True\n' % indent) - return "".join(lines) - #@-node:snapshot - #@-others -#@-node:class _ProjectBase -#@+node:class Project -class Project(_ProjectBase): - """ - Generates a Project without allocating resources. - - @param top_task: Specifies the highest function of a project definiton. - - @param scenario: Specifies the name of the scenario which should be scheduled. - - @param id: Specifiess a unique idenfication name to distinguish the project from - other projects in the resource database. The default value for id - is the name of top_task. - """ - #@ << class Project declarations >> - #@+node:<< class Project declarations >> - __call_completion__ = 'Project(|top_task, scenario="_default", id=None)' - - #@-node:<< class Project declarations >> - #@nl - #@ @+others - #@+node:__init__ - def __init__(self, top_task, scenario="_default", id=None): - _ProjectBase.__init__(self, top_task, scenario, id) - no_snapshot = not self.is_snapshot - for t in self: - t._is_frozen = True - t._recalc_properties() - no_snapshot and t.check() - - self._restore_globals() - - #@-node:__init__ - #@-others - -#@-node:class Project -#@+node:class _AllocationPoject -class _AllocationPoject(_ProjectBase): - #@ @+others - #@+node:unfreeze_parents - def unfreeze_parents(self): - if self.has_actual_data: - for t in filter(lambda t: t.children, self): - if not t._original_values.has_key("start"): t._unfreeze("start") - if not t._original_values.has_key("end"): t._unfreeze("end") - #@-node:unfreeze_parents - #@-others -#@-node:class _AllocationPoject -#@+node:class BalancedProject -class BalancedProject(_AllocationPoject): - """ - Generates a project with allocated resources. The tasks are balanced - to fit the resources load conditions. - """ - #@ << class BalancedProject declarations >> - #@+node:<< class BalancedProject declarations >> - __call_completion__ = """BalancedProject(|top_task, scenario="_default", - id=None, balance=SMART, performed=None)""" - - #@-node:<< class BalancedProject declarations >> - #@nl - #@ @+others - #@+node:__init__ - def __init__(self, top_task, scenario="_default", - id=None, balance=SMART, performed=None): - _AllocationPoject.__init__(self, top_task, scenario, id) - self.balance = balance - if performed: - self._distribute_performed(performed) - self.has_actual_data = True - - no_snapshot = not self.is_snapshot - if no_snapshot: - self.allocate() - else: - self.allocate_snapshot() - - for t in self: - t._is_frozen = True - t._recalc_properties() - no_snapshot and t.check() - - self._restore_globals() - #@nonl - #@-node:__init__ - #@+node:allocate_snapshot - def allocate_snapshot(self): - all_resources = self.free() - scenario = self.scenario - has_actual_data = True - for t in self: - if not t.resource or t.milestone or t.children: - continue - - t._convert_performed(all_resources) - t._allocate_performed(t._performed) - #@-node:allocate_snapshot - #@+node:allocate - def allocate(self): - all_resources = self.free() - balancing_list = self._get_balancing_list() - scenario = self.scenario - - #for t in balancing_list: - # print t.path - - for t in balancing_list: - t._compile([], True) - - if not t.resource or t.milestone or t.children: - continue - - if t._convert_performed(all_resources): - has_actual_data = True - - try: - t._allocate_performed(t._performed) - except AttributeError: - pass - - allocator = _allocators[t.balance] - min_val = None - min_state = None - for p in range(t.resource._permutation_count()): - state = t._test_allocation(p, allocator) - - if not state: continue - - to_minimize = state[0] - if not min_val or min_val > to_minimize: - min_val = to_minimize - min_state = state - - if min_state: - t._allocate(min_state, allocator) - elif t.performed_start: - # t could not be allocated ==> - # performance data holds all information - t.start = t._to_start(t.performed_start) - t.end = t._to_end(t.performed_end) - - self.unfreeze_parents() - #@-node:allocate - #@+node:_distribute_performed - def _distribute_performed(self, performed): - project_id = self._idendity_() - plen = len(project_id) - - performed = filter(lambda item: item[0].startswith(project_id), - performed) - performed.sort() - - task = None - for item in performed: - path = item[0] - rpath = "root" + path[plen:] - task = self.get_task(rpath) - - if not task: - #@ << extract task in activity path >> - #@+node:<< extract task in activity path >> - #@+at - # A performed path can have sub activities appended to the - # task path. - # like: - # - # root.parent1.parent2.task.subactivity - # - # here rhe correct task path is: - # - # root.parent1.parent2.task - # - #@-at - #@@code - orpath = rpath - while not task: - #path can specify a sub module - #find the correct path to the module - try: - last_dot = rpath.rindex(".", 0, len(rpath)) - except ValueError: - break - - rpath = rpath[:last_dot] - task = self.get_task(rpath) - - item = list(item) - item.append(orpath[len(rpath):]) - #@nonl - #@-node:<< extract task in activity path >> - #@nl - - if not task or task.children: - self._warn("The performance data contain " - "a task with id '%s'. But such " - "a task does not exist in your " - "project." % path) - continue - - if not isinstance(task.performed, list): - task.performed = list(task.performed) - - task.performed.append(item[1:]) - #@nonl - #@-node:_distribute_performed - #@-others -#@-node:class BalancedProject -#@+node:class AdjustedProject -class AdjustedProject(_AllocationPoject): - """ - Generates a project with allocated resources. The tasks are - adjusted to the actual tracking data and balanced to fit the - resources load conditions. - """ - #@ << class AdjustedProject declarations >> - #@+node:<< class AdjustedProject declarations >> - __call_completion__ = 'AdjustedProject(|base_project)' - - #@-node:<< class AdjustedProject declarations >> - #@nl - #@ @+others - #@+node:__init__ - def __init__(self, base_project): - _AllocationPoject.__init__(self, base_project._function, - base_project.scenario, - base_project.id) - - self.balance = base_project.balance - self.has_actual_data = base_project.has_actual_data - self.allocate(base_project) - for t in self: - t._is_frozen = True - t._recalc_properties() - t.check() - - self._restore_globals() - - - #@-node:__init__ - #@+node:allocate - def allocate(self, base): - balancing_list = self._get_balancing_list() - scenario = self.scenario - cal = self.calendar - now = cal.now - - #for t in balancing_list: - # print t.path - - #@ << free the resources, we have to rebook >> - #@+node:<< free the resources, we have to rebook >> - for t in balancing_list: - src = base.get_task(t.path) - if src.end > now or src.complete < 100: - for r in src._iter_booked_resources(): - r.unbook_task(src) - #@nonl - #@-node:<< free the resources, we have to rebook >> - #@nl - - for t in balancing_list: - src = base.get_task(t.path) - - if src.end <= now and src.complete == 100: - #@ << copy the attribs of complete tasks >> - #@+node:<< copy the attribs of complete tasks >> - t.effort = src.effort - t.load = src.load - t.start = src.start - t.end = src.end - t.done = src.done - t.todo = src.todo - t.booked_resource = src.booked_resource - t.performed_resource = src.performed_resource - t._unfreeze("length") - t._unfreeze("duration") - #@nonl - #@-node:<< copy the attribs of complete tasks >> - #@nl - continue - - t._compile([], True) - if not t.resource or t.milestone or t.children: - continue - - # now allocate the uncomplete tasks - #@ << allocate performed data >> - #@+node:<< allocate performed data >> - try: - t._performed = src._performed - t._allocate_performed(t._performed) - except AttributeError: - pass - #@nonl - #@-node:<< allocate performed data >> - #@nl - allocator = _allocators[t.balance] - - if src.start >= now: - #@ << allocate tasks, that have not begun yet >> - #@+node:<< allocate tasks, that have not begun yet >> - min_val = None - min_state = None - for p in range(t.resource._permutation_count()): - state = t._test_allocation(p, allocator) - if not state: continue - - to_minimize = state[0] - if not min_val or min_val > to_minimize: - min_val = to_minimize - min_state = state - - if min_state: - t._allocate(min_state, allocator) - elif t.performed_start: - t.start = t._to_start(t.performed_start) - t.end = t._to_end(t.performed_end) - #@-node:<< allocate tasks, that have not begun yet >> - #@nl - else: - #@ << allocate tasks, that are allready at work >> - #@+node:<< allocate tasks, that are allready at work >> - if t.__dict__.has_key("effort"): - t.effort = t._to_delta(src.done + src.todo).round() - - resource = src.booked_resource or src.performed_resource - state = allocator.test_allocation(t, resource) - if state: - t._allocate(state, allocator) - #@nonl - #@-node:<< allocate tasks, that are allready at work >> - #@nl - - self.unfreeze_parents() - #@nonl - #@-node:allocate - #@-others -#@-node:class AdjustedProject -#@-node:Projects -#@-others - - -""" - Atttribute mit Bedeutung: - - calendar - -------- - minimum_time_unit |int in minutes| - working_days_per_week |int in days | - working_days_per_month|int in days | - working_days_per_year |int in days | - working_hours_per_day |int in hours | - vacation | [ one_day, (from, to), .. ] | - working_days - now - - - - Task - ----- - load - start - end - length - effort - duration - resource - booked_resource - - milestone - complete - done - todo - priority - efficiency - buffer - - children - depth - index - path - dont_inherit - - performed_effort - performed_end - performed_start - - sum() - min() - max() - costs() - indent_name() - max_load - - copy_src (set: copy all attributes of another task - get: reference of copy) - - balance - - for gantt - ----- - line - accumulate - - - - - Resource - ---------- - efficiency - load - vacation - max_load - -""" -#@-node:@file task.py -#@-leo diff --git a/addons/resource/faces_new/timescale.py b/addons/resource/faces_new/timescale.py deleted file mode 100644 index 215766eb958..00000000000 --- a/addons/resource/faces_new/timescale.py +++ /dev/null @@ -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., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -############################################################################ - -import faces.pcalendar as pcal -import matplotlib.cbook as cbook -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(cbook.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) diff --git a/addons/resource/faces_new/utils.py b/addons/resource/faces_new/utils.py deleted file mode 100644 index f7d61fb1dc9..00000000000 --- a/addons/resource/faces_new/utils.py +++ /dev/null @@ -1,124 +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., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 - - - diff --git a/addons/resource/resource_view.xml b/addons/resource/resource_view.xml index 2c53237a60a..7bb8358b104 100755 --- a/addons/resource/resource_view.xml +++ b/addons/resource/resource_view.xml @@ -1,18 +1,9 @@ - - - - - - - - - - - - + + + resource.calendar.form resource.calendar @@ -33,7 +24,7 @@ form - + resource.calendar.week.tree @@ -129,7 +120,7 @@ resource.resource tree,form - + @@ -164,12 +155,12 @@ - Leaves + Resource Leaves resource.calendar.leaves tree,form,calendar - + From 9955c50c144cd7ea193daf6404069cf5c83ad483 Mon Sep 17 00:00:00 2001 From: "Harry (Open ERP)" Date: Fri, 29 Jan 2010 16:23:56 +0530 Subject: [PATCH 16/30] [FIX] account: fixed bug in get sequence id bzr revid: hmo@tinyerp.com-20100129105356-nu8bybh9kcjovbx4 --- addons/account/sequence.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/account/sequence.py b/addons/account/sequence.py index 08e38dd14a4..498e997dc2b 100644 --- a/addons/account/sequence.py +++ b/addons/account/sequence.py @@ -37,13 +37,13 @@ class ir_sequence(osv.osv): _columns = { 'fiscal_ids' : fields.one2many('account.sequence.fiscalyear', 'sequence_main_id', 'Sequences') } - def get_id(self, cr, uid, sequence_id, test='id=%s', context={}): - cr.execute('select id from ir_sequence where '+test+' and active=%s', (sequence_id, True,)) + def get_id(self, cr, uid, sequence_id, test='id', context={}): + cr.execute('select id from ir_sequence where '+test+'=%s and active=%s', (sequence_id, True,)) res = cr.dictfetchone() if res: for line in self.browse(cr, uid, res['id'], context=context).fiscal_ids: if line.fiscalyear_id.id==context.get('fiscalyear_id', False): - return super(ir_sequence, self).get_id(cr, uid, line.sequence_id.id, test="id=%s", context=context) + return super(ir_sequence, self).get_id(cr, uid, line.sequence_id.id, test="id", context=context) return super(ir_sequence, self).get_id(cr, uid, sequence_id, test, context) ir_sequence() From 6632a6a64651d4a5893c77fc37725a05b3b1d26c Mon Sep 17 00:00:00 2001 From: "HDA (OpenERP)" Date: Fri, 29 Jan 2010 17:31:43 +0530 Subject: [PATCH 17/30] Improved task scheduling wizard bzr revid: hda@tinyerp.com-20100129120143-g8jtdojskvdpihsb --- addons/project/wizard/compute_tasks_date.py | 48 +++++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/addons/project/wizard/compute_tasks_date.py b/addons/project/wizard/compute_tasks_date.py index b7134544f69..40d238e10ba 100644 --- a/addons/project/wizard/compute_tasks_date.py +++ b/addons/project/wizard/compute_tasks_date.py @@ -27,9 +27,12 @@ from resource.faces import * compute_form = """ - +""" +success_msg = """ +
+
- + @@ -16,28 +16,28 @@ field - + field - + field - + field - + From 4083c9bf7bf582d88eeb785101ce6c582f2809e0 Mon Sep 17 00:00:00 2001 From: "rpa (Open ERP)" Date: Fri, 29 Jan 2010 17:44:29 +0530 Subject: [PATCH 19/30] [IMP]: base_calendar: Added duplicate fields checking in write, fixed minor problem in subscibe calendar wizard bzr revid: rpa@openerp.co.in-20100129121429-wqddwgfyg3m63gtx --- addons/base_calendar/base_calendar.py | 43 ++++++++++++++----- .../wizard/wizard_cal_subscribe.py | 5 ++- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index 71117227fa9..f4a276d0eda 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -422,23 +422,27 @@ class basic_calendar_fields(osv.osv): _defaults = { 'fn': lambda *a: 'field', } - + + def check_line(self, cr, uid, vals, name, context=None): + f_obj = self.pool.get('ir.model.fields') + field = f_obj.browse(cr, uid, vals['field_id'], context=context) + relation = field.relation + line_obj = self.pool.get('basic.calendar.lines') + l_id = line_obj.search(cr, uid, [('name', '=', name)]) + if l_id: + line = line_obj.browse(cr, uid, l_id, context=context)[0] + line_rel = line.object_id.model + if (relation != 'NULL') and (not relation == line_rel): + raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name))) + return True + def create(self, cr, uid, vals, context={}): cr.execute('select name from basic_calendar_attributes \ where id=%s' % (vals.get('name'))) name = cr.fetchone() name = name[0] if name in ('valarm', 'attendee'): - f_obj = self.pool.get('ir.model.fields') - field = f_obj.browse(cr, uid, vals['field_id'], context=context) - relation = field.relation - line_obj = self.pool.get('basic.calendar.lines') - l_id = line_obj.search(cr, uid, [('name', '=', name)]) - if l_id: - line = line_obj.browse(cr, uid, l_id, context=context)[0] - line_rel = line.object_id.model - if (relation != 'NULL') and (not relation == line_rel): - raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name))) + self.check_line(cr, uid, vals, name, context=context) cr.execute("Select count(id) from basic_calendar_fields \ where name=%s and type_id=%s" % (vals.get('name'), vals.get('type_id'))) res = cr.fetchone() @@ -446,6 +450,23 @@ class basic_calendar_fields(osv.osv): if res[0] > 0: raise osv.except_osv(_('Warning !'), _('Can not map same field more than once')) return super(basic_calendar_fields, self).create(cr, uid, vals, context=context) + + def write(self, cr, uid, ids, vals, context=None): + if not vals: + return + for id in ids: + field = self.browse(cr, uid, id, context=context) + name = field.name.name + if name in ('valarm', 'attendee'): + self.check_line(cr, uid, vals, name, context=context) + qry = "Select count(id) from basic_calendar_fields \ + where name=%s and type_id=%s" % (field.name.id, field.type_id.id) + cr.execute(qry) + res = cr.fetchone() + if res: + if res[0] > 1: + raise osv.except_osv(_('Warning !'), _('Can not map same field more than once')) + return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context) basic_calendar_fields() diff --git a/addons/base_calendar/wizard/wizard_cal_subscribe.py b/addons/base_calendar/wizard/wizard_cal_subscribe.py index 8146e7bb1fa..709c61d0b25 100644 --- a/addons/base_calendar/wizard/wizard_cal_subscribe.py +++ b/addons/base_calendar/wizard/wizard_cal_subscribe.py @@ -66,13 +66,14 @@ class cal_event_subscribe_wizard(wizard.interface): model_obj = pooler.get_pool(cr.dbname).get(model) context.update({'url': data['form']['url_path'], 'model': data.get('model')}) - vals = model_obj.import_cal(cr, uid, base64.encodestring(caldata), context) + vals = model_obj.import_cal(cr, uid, base64.encodestring(caldata), \ + data['id'], context) if vals: cnt = vals['count'] return {} def _result_set(self, cr, uid, data, context=None): - return {'msg': 'Subscribed %s Event(s)' % cnt} + return {'msg': 'Import Sucessful.'} states = { 'init': { From 9008ced144a09a5057ffa15d4fdc05a172ef430a Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Fri, 29 Jan 2010 13:22:20 +0100 Subject: [PATCH 20/30] [FIX] typo bzr revid: chs@tinyerp.com-20100129122220-yexokxsfbaulntm2 --- bin/netsvc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/netsvc.py b/bin/netsvc.py index b47faba823e..49d68c8479e 100644 --- a/bin/netsvc.py +++ b/bin/netsvc.py @@ -58,7 +58,7 @@ class Service(object): @classmethod def remove(cls, name): - if cls.exits(name): + if cls.exists(name): cls._services.pop(name) def exportMethod(self, method): From 9aa55b69a29ccf6d0c7f34376aced8f65306d482 Mon Sep 17 00:00:00 2001 From: "rpa (Open ERP)" Date: Fri, 29 Jan 2010 19:11:36 +0530 Subject: [PATCH 21/30] [FIX]: base_calendar: Fixed problem of exporting alarm bzr revid: rpa@openerp.co.in-20100129134136-zmc7cice7z6wpewm --- addons/base_calendar/base_calendar.py | 16 ++++++++++++---- addons/caldav/caldav_data.xml | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index f4a276d0eda..0fb5378d03e 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -184,7 +184,10 @@ class CalDAV(object): continue if cal_data.name.lower() == 'valarm': alarm = self.pool.get('basic.calendar.alarm') - vals = alarm.import_cal(cr, uid, cal_data) + ctx = context.copy() + if cal_children: + ctx.update({'model': cal_children[cal_data.name.lower()]}) + vals = alarm.import_cal(cr, uid, cal_data, context=ctx) self.ical_set(cal_data.name.lower(), vals, 'value') continue if cal_data.name.lower() in self.__attribute__: @@ -227,9 +230,11 @@ class CalDAV(object): data[map_field], vevent, context=context) elif field == 'valarm' and data[map_field]: model = self.__attribute__[field].get('object', False) + ctx = context.copy() + ctx.update({'model': model}) alarm_obj = self.pool.get('basic.calendar.alarm') vevent = alarm_obj.export_cal(cr, uid, model, \ - data[map_field][0], vevent, context=context) + data[map_field][0], vevent, context=ctx) elif data[map_field]: if map_type in ("char", "text"): vevent.add(field).value = tools.ustr(data[map_field]) @@ -628,11 +633,14 @@ class Alarm(CalDAV, osv.osv_memory): trigger.value = delta # Compute other details - valarm.add('DESCRIPTION').value = alarm_data['name'] + valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP' valarm.add('ACTION').value = alarm_data['action'] return vevent - def import_cal(self, cr, uid, ical_data): + def import_cal(self, cr, uid, ical_data, context=None): + ctx = context.copy() + ctx.update({'model': context.get('model', None)}) + self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx) for child in ical_data.getChildren(): if child.name.lower() == 'trigger': seconds = child.value.seconds diff --git a/addons/caldav/caldav_data.xml b/addons/caldav/caldav_data.xml index ae380e8f0d7..295b05ba540 100644 --- a/addons/caldav/caldav_data.xml +++ b/addons/caldav/caldav_data.xml @@ -366,7 +366,7 @@ - + field @@ -394,7 +394,7 @@ - + field From d5087fd9736baa7f0ff8e5421ddf76083f0cfde5 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Fri, 29 Jan 2010 17:16:08 +0100 Subject: [PATCH 22/30] [IMP] users_ldap: clean code + add some debug logging bzr revid: chs@tinyerp.com-20100129161608-awmum09g0bzzn8f1 --- addons/users_ldap/users_ldap.py | 36 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/addons/users_ldap/users_ldap.py b/addons/users_ldap/users_ldap.py index da4364d069c..df4528107cc 100644 --- a/addons/users_ldap/users_ldap.py +++ b/addons/users_ldap/users_ldap.py @@ -18,18 +18,12 @@ # ############################################################################## -from osv import fields,osv -from service import security +from osv import fields, osv import pooler import tools from service import security -try: - import ldap - from ldap.filter import filter_format -except ImportError: - import netsvc - logger = netsvc.Logger() - logger.notifyChannel("init", netsvc.LOG_WARNING, "could not import ldap!") +import ldap +from ldap.filter import filter_format class CompanyLDAP(osv.osv): @@ -71,18 +65,20 @@ res_company() class users(osv.osv): _inherit = "res.users" def login(self, db, login, password): - ret = super(users,self).login(db, login, password) + ret = super(users,self).login(db, login, password) + tools.debug(ret) if ret: return ret pool = pooler.get_pool(db) - cr = pooler.get_db(db).cursor() + cr = pooler.get_db(db).cursor() action_obj = pool.get('ir.actions.actions') cr.execute(""" - SELECT id, company, ldap_server, ldap_server_port, ldap_binddn, ldap_password, - ldap_filter, ldap_base, \"user\", create_user - FROM res_company_ldap + SELECT id, company, ldap_server, ldap_server_port, ldap_binddn, ldap_password, + ldap_filter, ldap_base, "user", create_user + FROM res_company_ldap WHERE ldap_server != '' and ldap_binddn != '' ORDER BY sequence""") for res_company_ldap in cr.dictfetchall(): + tools.debug(res_company_ldap) try: l = ldap.open(res_company_ldap['ldap_server'], res_company_ldap['ldap_server_port']) if l.simple_bind_s(res_company_ldap['ldap_binddn'], res_company_ldap['ldap_password']): @@ -97,16 +93,18 @@ class users(osv.osv): continue if result_type == ldap.RES_SEARCH_RESULT and len(result_data) == 1: dn = result_data[0][0] + tools.debug(dn) name = result_data[0][1]['cn'][0] - if l.bind_s(dn, passwd): + if l.bind_s(dn, password): l.unbind() cr.execute("SELECT id FROM res_users WHERE login=%s",(tools.ustr(login),)) res = cr.fetchone() + tools.debug(res) if res: cr.close() return res[0] if not res_company_ldap['create_user']: - continue + continue action_id = action_obj.search(cr, 1, [('usage', '=', 'menu')])[0] if res_company_ldap['user']: res = self.copy(cr, 1, res_company_ldap['user'], @@ -129,6 +127,7 @@ class users(osv.osv): return res l.unbind() except Exception, e: + tools.debug(e) continue cr.close() return False @@ -138,8 +137,7 @@ class users(osv.osv): return super(users,self).check(db, uid, passwd) except: # AccessDenied pass - pool = pooler.get_pool(db) - cr = pooler.get_db(db).cursor() + cr = pooler.get_db(db).cursor() user = self.browse(cr, 1, uid) if user and user.company_id.ldaps: for res_company_ldap in user.company_id.ldaps: @@ -156,7 +154,6 @@ class users(osv.osv): result_type, result_data = l.result(result_id, timeout) if result_data and result_type == ldap.RES_SEARCH_RESULT and len(result_data) == 1: dn = result_data[0][0] - name = result_data[0][1]['cn'] if l.bind_s(dn, passwd): l.unbind() self._uid_cache.setdefault(db, {})[uid] = passwd @@ -164,6 +161,7 @@ class users(osv.osv): return True l.unbind() except Exception, e: + tools.debug(e) pass cr.close() raise security.ExceptionNoTb('AccessDenied') From a62d77fa115760328a1fb133b2ddc3f2846849b9 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 29 Jan 2010 17:23:32 +0100 Subject: [PATCH 23/30] [fix] exception thrown by browse_record.__getitem__ when field not found bzr revid: xmo@tinyerp.com-20100129162332-a7nvb3w153iv2e7y --- bin/osv/orm.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/bin/osv/orm.py b/bin/osv/orm.py index 512ab0606d8..18e816a7e59 100644 --- a/bin/osv/orm.py +++ b/bin/osv/orm.py @@ -45,6 +45,7 @@ import types import string import netsvc import re +import traceback import pickle @@ -126,6 +127,8 @@ class browse_record_list(list): class browse_record(object): + logger = netsvc.Logger() + def __init__(self, cr, uid, id, table, cache, context=None, list_class = None, fields_process={}): ''' table : the object (inherited from orm) @@ -172,9 +175,11 @@ class browse_record(object): else: return getattr(self._table, name) else: - logger = netsvc.Logger() - logger.notifyChannel('orm', netsvc.LOG_ERROR, "Programming error: field '%s' does not exist in object '%s' !" % (name, self._table._name)) - return None + self.logger.notifyChannel("browse_record", netsvc.LOG_WARNING, + "Field '%s' does not exist in object '%s': \n%s" % ( + name, self, ''.join(traceback.format_trace()))) + raise KeyError("Field '%s' does not exist in object '%s'" % ( + name, self)) # if the field is a classic one or a many2one, we'll fetch all classic and many2one fields if col._prefetch: @@ -207,8 +212,11 @@ class browse_record(object): if not datas: - # Where did those ids come from? Perhaps old entries in ir_model_data? - raise except_orm('NoDataError', 'Field %s in %s%s'%(name,self._table_name,str(ids))) + # Where did those ids come from? Perhaps old entries in ir_model_dat? + self.logger.notifyChannel("browse_record", netsvc.LOG_WARNING, + "No datas found for ids %s in %s \n%s" % ( + ids, self, ''.join(traceback.format_trace()))) + raise KeyError('Field %s not found in %s'%(name,self)) # create browse records for 'remote' objects for data in datas: if len(str(data['id']).split('-')) > 1: @@ -233,15 +241,18 @@ class browse_record(object): self._data[data['id']].update(data) if not name in self._data[self._id]: #how did this happen? - logger = netsvc.Logger() - logger.notifyChannel("browse_record", netsvc.LOG_ERROR,"Ffields: %s, datas: %s"%(str(fffields),str(datas))) - logger.notifyChannel("browse_record", netsvc.LOG_ERROR,"Data: %s, Table: %s"%(str(self._data[self._id]),str(self._table))) - raise AttributeError(_('Unknown attribute %s in %s ') % (str(name),self._table_name)) + self.logger.notifyChannel("browse_record", netsvc.LOG_ERROR, + "Ffields: %s, datas: %s"%(fffields, datas)) + self.logger.notifyChannel("browse_record", netsvc.LOG_ERROR, + "Data: %s, Table: %s"%(self._data[self._id], self._table)) + raise KeyError(_('Unknown attribute %s in %s ') % (name, self)) return self._data[self._id][name] def __getattr__(self, name): -# raise an AttributeError exception. - return self[name] + try: + return self[name] + except KeyError, e: + raise AttributeError(e) def __contains__(self, name): return (name in self._table._columns) or (name in self._table._inherit_fields) or hasattr(self._table, name) From 77eed56346ce06b4d745389365150ee96e9798df Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 29 Jan 2010 17:26:16 +0100 Subject: [PATCH 24/30] [imp] fix imports bzr revid: xmo@tinyerp.com-20100129162616-6zq38zjn5c981avx --- bin/osv/orm.py | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/bin/osv/orm.py b/bin/osv/orm.py index 18e816a7e59..223c2875f03 100644 --- a/bin/osv/orm.py +++ b/bin/osv/orm.py @@ -38,40 +38,28 @@ # # -import time -import datetime import calendar -import types -import string -import netsvc -import re -import traceback - +import copy +import datetime import pickle +import random +import re +import string +import sys +import time +import traceback +import types +from lxml import etree import fields +import netsvc import tools -from tools.translate import _ - -import copy -import sys -import copy - -try: - from lxml import etree -except ImportError: - sys.stderr.write("ERROR: Import lxml module\n") - sys.stderr.write("ERROR: Try to install the python-lxml package\n") - sys.exit(2) - - from tools.config import config +from tools.translate import _ regex_order = re.compile('^([a-z0-9_]+( *desc| *asc)?( *, *|))+$', re.I) def last_day_of_current_month(): - import datetime - import calendar today = datetime.date.today() last_day = str(calendar.monthrange(today.year, today.month)[1]) return time.strftime('%Y-%m-' + last_day) @@ -2298,7 +2286,6 @@ class orm(orm_template): elif field['ttype'] == 'one2many': self._columns[field['name']] = getattr(fields, field['ttype'])(field['relation'], field['relation_field'], **attrs) elif field['ttype'] == 'many2many': - import random _rel1 = field['relation'].replace('.', '_') _rel2 = field['model'].replace('.', '_') _rel_name = 'x_%s_%s_%s_rel' %(_rel1, _rel2, field['name']) From 6054dec0afef236c172c1dfbb2d706a7a7c09d72 Mon Sep 17 00:00:00 2001 From: Launchpad Code Hosting Date: Sun, 31 Jan 2010 04:51:43 +0000 Subject: [PATCH 25/30] Launchpad automatic translations update. bzr revid: codehost@crowberry-20100130044003-jcmu84vilfyyaaqj bzr revid: codehost@crowberry-20100131045143-nspay62olcx1olij --- addons/account/i18n/hu.po | 74 ++++++++++++++++----------------- addons/account/i18n/it.po | 35 +++++++++------- addons/account_chart/i18n/it.po | 8 ++-- addons/base_setup/i18n/it.po | 11 +++-- addons/board/i18n/it.po | 12 +++--- addons/hr/i18n/lt.po | 12 +++--- addons/report_crm/i18n/it.po | 28 ++++++------- addons/sale/i18n/lt.po | 8 ++-- 8 files changed, 98 insertions(+), 90 deletions(-) diff --git a/addons/account/i18n/hu.po b/addons/account/i18n/hu.po index 629d46d10c3..21fbe4cf06d 100644 --- a/addons/account/i18n/hu.po +++ b/addons/account/i18n/hu.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2010-01-22 23:52+0000\n" -"Last-Translator: Balint (eSolve) \n" +"PO-Revision-Date: 2010-01-30 16:38+0000\n" +"Last-Translator: Dukai Gábor \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-01-24 04:50+0000\n" +"X-Launchpad-Export-Date: 2010-01-31 04:51+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: account @@ -910,7 +910,7 @@ msgstr "" #. module: account #: wizard_field:account.open_closed_fiscalyear,init,fyear_id:0 msgid "Fiscal Year to Open" -msgstr "" +msgstr "Visszanyitandó üzleti év" #. module: account #: view:account.config.wizard:0 @@ -981,7 +981,7 @@ msgstr "" #: wizard_field:account.print.journal.report,init,period_id:0 #: field:account.subscription,period_nbr:0 msgid "Period" -msgstr "Időszak" +msgstr "Periódus" #. module: account #: rml:account.partner.balance:0 @@ -1172,7 +1172,7 @@ msgstr "" #. module: account #: wizard_view:account.fiscalyear.close.state,init:0 msgid "Are you sure you want to close the fiscal year ?" -msgstr "" +msgstr "Biztos benne, hogy le akarja zárni az üzleti évet?" #. module: account #: selection:account.move,type:0 @@ -1212,7 +1212,7 @@ msgstr "" #. module: account #: field:account.account,company_currency_id:0 msgid "Company Currency" -msgstr "" +msgstr "Cég pénzneme" #. module: account #: model:ir.model,name:account.model_account_fiscal_position_account_template @@ -1332,7 +1332,7 @@ msgstr "" #: selection:account.analytic.account,type:0 #: field:account.journal,view_id:0 msgid "View" -msgstr "" +msgstr "nézet" #. module: account #: selection:account.account.balance.report,checktype,display_account:0 @@ -1471,7 +1471,7 @@ msgstr "Kötelezettség összesen" #. module: account #: wizard_button:account.fiscalyear.close.state,init,close:0 msgid "Close states" -msgstr "" +msgstr "Állapotok zárása" #. module: account #: model:ir.model,name:account.model_wizard_company_setup @@ -1707,7 +1707,7 @@ msgstr "Visszanyitás" #. module: account #: wizard_view:account.fiscalyear.close,init:0 msgid "Are you sure you want to create entries?" -msgstr "" +msgstr "Biztos hogy létre akarja hozni ezeket a könyvelési tételeket?" #. module: account #: field:account.tax,include_base_amount:0 @@ -2071,7 +2071,7 @@ msgstr "" #. module: account #: wizard_field:account.fiscalyear.close,init,period_id:0 msgid "Opening Entries Period" -msgstr "" +msgstr "Nyitóegyenleg periódusa" #. module: account #: model:ir.actions.wizard,name:account.wizard_validate_account_moves @@ -2088,7 +2088,7 @@ msgstr "" #. module: account #: selection:account.aged.trial.balance,init,direction_selection:0 msgid "Past" -msgstr "" +msgstr "múlt" #. module: account #: field:account.analytic.account,company_currency_id:0 @@ -2193,7 +2193,7 @@ msgstr "Tételes" #: selection:account.account,type:0 #: selection:account.account.template,type:0 msgid "Consolidation" -msgstr "Konszolidáció" +msgstr "konszolidáció" #. module: account #: field:account.chart.template,account_root_id:0 @@ -2236,7 +2236,7 @@ msgstr "" #. module: account #: wizard_field:account.fiscalyear.close,init,journal_id:0 msgid "Opening Entries Journal" -msgstr "" +msgstr "Nyitóegyenleg naplója" #. module: account #: view:account.config.wizard:0 @@ -2711,7 +2711,7 @@ msgstr "Számla azonosító" #. module: account #: field:account.period,date_stop:0 msgid "End of Period" -msgstr "Időszak vége" +msgstr "Periódus vége" #. module: account #: wizard_button:populate_statement_from_inv,go,finish:0 @@ -2765,7 +2765,7 @@ msgstr "" #. module: account #: wizard_field:account.aged.trial.balance,init,direction_selection:0 msgid "Analysis Direction" -msgstr "" +msgstr "Elemzés iránya" #. module: account #: wizard_button:populate_statement_from_inv,init,go:0 @@ -3093,7 +3093,7 @@ msgstr "" #: selection:account.account.template,type:0 #: selection:account.aged.trial.balance,init,result_selection:0 msgid "Payable" -msgstr "" +msgstr "kötelezettségek" #. module: account #: rml:account.invoice:0 @@ -3110,7 +3110,7 @@ msgstr "" #: selection:account.account,type:0 #: selection:account.account.template,type:0 msgid "Others" -msgstr "" +msgstr "egyéb" #. module: account #: selection:account.automatic.reconcile,init,power:0 @@ -3250,7 +3250,7 @@ msgstr "" #: model:ir.actions.wizard,name:account.wizard_fiscalyear_close #: model:ir.ui.menu,name:account.menu_wizard_fy_close msgid "Generate Fiscal Year Opening Entries" -msgstr "" +msgstr "Nyitóegyenlegek készítése az üzleti évhez" #. module: account #: model:ir.actions.wizard,name:account.wizard_reconcile @@ -3277,7 +3277,7 @@ msgstr "Adónév" #. module: account #: wizard_view:account.fiscalyear.close.state,init:0 msgid " Close states of Fiscal year and periods" -msgstr "" +msgstr " Az üzleti év és a periódusok állapotainak zárása" #. module: account #: model:account.payment.term,name:account.account_payment_term @@ -3565,7 +3565,7 @@ msgstr "Naplókód" #: wizard_button:account.fiscalyear.close,init,close:0 #: view:account.model:0 msgid "Create entries" -msgstr "" +msgstr "Tételek létrehozása" #. module: account #: view:account.analytic.line:0 @@ -3780,7 +3780,7 @@ msgstr "" #: selection:account.account,type:0 #: selection:account.account.template,type:0 msgid "Closed" -msgstr "" +msgstr "lezárt" #. module: account #: model:process.node,name:account.process_node_paymententries0 @@ -3878,7 +3878,7 @@ msgstr "Maximális mennyiség" #. module: account #: field:account.period,name:0 msgid "Period Name" -msgstr "Időszak" +msgstr "Periódus" #. module: account #: help:account.analytic.journal,type:0 @@ -4294,7 +4294,7 @@ msgstr "" #: field:account.account,type:0 #: field:account.account.template,type:0 msgid "Internal Type" -msgstr "" +msgstr "Belső típus" #. module: account #: selection:account.automatic.reconcile,init,power:0 @@ -4333,7 +4333,7 @@ msgstr "Záró dátum" #: model:ir.actions.wizard,name:account.wizard_open_closed_fiscalyear #: model:ir.ui.menu,name:account.menu_wizard_open_closed_fy msgid "Cancel Opening Entries" -msgstr "" +msgstr "Nyitóegyenleg visszavonása" #. module: account #: model:process.transition,name:account.process_transition_invoicemanually0 @@ -4435,7 +4435,7 @@ msgstr "" #: view:account.fiscalyear:0 #: view:account.period:0 msgid "States" -msgstr "" +msgstr "Állapotok" #. module: account #: view:account.move:0 @@ -4560,7 +4560,7 @@ msgstr "" #. module: account #: wizard_field:account.aged.trial.balance,init,result_selection:0 msgid "Filter on Partners" -msgstr "" +msgstr "Partnerek szűrése" #. module: account #: field:account.tax,price_include:0 @@ -4772,7 +4772,7 @@ msgstr "" #: selection:account.account.template,type:0 #: selection:account.aged.trial.balance,init,result_selection:0 msgid "Receivable" -msgstr "" +msgstr "követelések" #. module: account #: model:ir.actions.report.xml,name:account.account_account_balance @@ -4909,12 +4909,12 @@ msgstr "Tételek" #. module: account #: field:account.period,date_start:0 msgid "Start of Period" -msgstr "" +msgstr "Periódus kezdete" #. module: account #: wizard_field:account.fiscalyear.close,init,report_name:0 msgid "Name of new entries" -msgstr "" +msgstr "Új tételek neve" #. module: account #: wizard_button:account_use_models,init_form,create:0 @@ -5252,7 +5252,7 @@ msgstr "" #. module: account #: wizard_view:account.period.close,init:0 msgid "Are you sure ?" -msgstr "" +msgstr "Biztos benne?" #. module: account #: rml:account.invoice:0 @@ -5320,7 +5320,7 @@ msgstr "" #. module: account #: selection:account.aged.trial.balance,init,result_selection:0 msgid "Receivable and Payable" -msgstr "" +msgstr "követelések és kötelezettségek" #. module: account #: rml:account.account.balance:0 @@ -5468,7 +5468,7 @@ msgstr "" #: view:account.move:0 #: view:account.move.line:0 msgid "General Information" -msgstr "" +msgstr "Általános adatok" #. module: account #: help:populate_statement_from_inv,init,journal_id:0 @@ -5566,7 +5566,7 @@ msgstr "" #: wizard_field:account.fiscalyear.close,init,fy_id:0 #: wizard_field:account.fiscalyear.close.state,init,fy_id:0 msgid "Fiscal Year to close" -msgstr "" +msgstr "Lezárandó üzleti év" #. module: account #: wizard_field:account.aged.trial.balance,init,date1:0 @@ -5670,7 +5670,7 @@ msgstr "Árbevétel számla" #. module: account #: field:account.period,special:0 msgid "Opening/Closing Period" -msgstr "" +msgstr "Nyitó/záró periódus" #. module: account #: rml:account.analytic.account.balance:0 @@ -5763,7 +5763,7 @@ msgstr "Pénzügyi év" #. module: account #: selection:account.aged.trial.balance,init,direction_selection:0 msgid "Future" -msgstr "" +msgstr "jövő" #. module: account #: help:account.account.balance.report,checktype,fiscalyear:0 @@ -5893,7 +5893,7 @@ msgstr "" #: wizard_field:account.fiscalyear.close.state,init,sure:0 #: wizard_field:account.period.close,init,sure:0 msgid "Check this box" -msgstr "" +msgstr "Jelölje meg ezt a négyzetet" #. module: account #: help:account.tax,price_include:0 diff --git a/addons/account/i18n/it.po b/addons/account/i18n/it.po index a677f708ff2..58ad4f5346d 100644 --- a/addons/account/i18n/it.po +++ b/addons/account/i18n/it.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2010-01-17 16:07+0000\n" -"Last-Translator: Davide Corio \n" +"PO-Revision-Date: 2010-01-29 14:02+0000\n" +"Last-Translator: Carlo \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-01-18 04:44+0000\n" +"X-Launchpad-Export-Date: 2010-01-30 04:39+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: account @@ -299,7 +299,7 @@ msgstr "Fisso" #: model:ir.actions.report.xml,name:account.account_overdue #: view:res.company:0 msgid "Overdue Payments" -msgstr "" +msgstr "Ritardo Pagamenti" #. module: account #: wizard_view:account.account.balance.report,checktype:0 @@ -336,7 +336,7 @@ msgstr "Computazione del canone" #. module: account #: rml:account.central.journal:0 msgid "Account Num." -msgstr "" +msgstr "Conto n." #. module: account #: rml:account.analytic.account.analytic.check:0 @@ -475,7 +475,7 @@ msgstr "Riconciliazione bancaria" #. module: account #: rml:account.invoice:0 msgid "Disc.(%)" -msgstr "" +msgstr "Sconto (%)" #. module: account #: rml:account.general.ledger:0 @@ -497,6 +497,8 @@ msgid "" "Set if the amount of tax must be included in the base amount before " "computing the next taxes." msgstr "" +"Selezionare se l'importo della tassa debba essere incluso nell'importo base " +"prima di calcolare le prossime tasse" #. module: account #: model:ir.ui.menu,name:account.menu_finance_periodical_processing @@ -506,7 +508,7 @@ msgstr "Elaborazioni periodiche" #. module: account #: view:report.hr.timesheet.invoice.journal:0 msgid "Analytic Entries Stats" -msgstr "" +msgstr "Statistiche delle registrazioni analitiche" #. module: account #: model:ir.actions.act_window,name:account.action_account_tax_code_template_form @@ -528,7 +530,7 @@ msgstr "Pagamento Riconciliato" #. module: account #: wizard_field:account.chart,init,target_move:0 msgid "Target Moves" -msgstr "" +msgstr "Registrazioni:" #. module: account #: model:ir.actions.act_window,name:account.action_account_tax_template_form @@ -549,7 +551,7 @@ msgstr "Metodo riapertura conti" #. module: account #: field:account.tax.template,include_base_amount:0 msgid "Include in Base Amount" -msgstr "" +msgstr "Incluso nell'imponibile" #. module: account #: field:account.tax,ref_base_code_id:0 @@ -579,6 +581,9 @@ msgid "" "Number of days to add before computation of the day of month.If Date=15/01, " "Number of Days=22, Day of Month=-1, then the due date is 28/02." msgstr "" +"Numero di giorni da aggiungere prima della computazione del giorno del mese. " +"Se Data=15/01, Numero di giorni=22, Giorno del mese=-1, allora la scadenza " +"sarà 28/02." #. module: account #: model:ir.model,name:account.model_account_tax @@ -588,12 +593,12 @@ msgstr "account.tax" #. module: account #: rml:account.central.journal:0 msgid "Printing Date" -msgstr "" +msgstr "Stampa Data" #. module: account #: rml:account.general.ledger:0 msgid "Mvt" -msgstr "" +msgstr "Reg. n." #. module: account #: model:ir.actions.wizard,name:account.wizard_aged_trial_balance @@ -659,7 +664,7 @@ msgstr "" #. module: account #: help:account.fiscalyear,company_id:0 msgid "Keep empty if the fiscal year belongs to several companies." -msgstr "" +msgstr "Lasciare vuoto se l'anno fiscale appartiene a più aziende" #. module: account #: model:ir.ui.menu,name:account.menu_analytic_accounting @@ -669,7 +674,7 @@ msgstr "Contabilità Analitica" #. module: account #: rml:account.overdue:0 msgid "Sub-Total :" -msgstr "" +msgstr "Sub-totale" #. module: account #: field:account.analytic.account,line_ids:0 @@ -687,12 +692,12 @@ msgstr "Mese" #. module: account #: field:account.analytic.account,partner_id:0 msgid "Associated Partner" -msgstr "" +msgstr "Partner associato" #. module: account #: field:account.invoice,comment:0 msgid "Additional Information" -msgstr "" +msgstr "Informazioni Aggiuntive" #. module: account #: selection:account.invoice,type:0 diff --git a/addons/account_chart/i18n/it.po b/addons/account_chart/i18n/it.po index 0a18bba9493..265ed870a9b 100644 --- a/addons/account_chart/i18n/it.po +++ b/addons/account_chart/i18n/it.po @@ -7,16 +7,16 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-09-08 13:46+0000\n" -"Last-Translator: Fabien (Open ERP) \n" +"PO-Revision-Date: 2010-01-29 07:09+0000\n" +"Last-Translator: Stefano Prenna \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:27+0000\n" +"X-Launchpad-Export-Date: 2010-01-30 04:40+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: account_chart #: model:ir.module.module,description:account_chart.module_meta_information msgid "Remove minimal account chart" -msgstr "" +msgstr "Rimuovi il grafico del conto minimo" diff --git a/addons/base_setup/i18n/it.po b/addons/base_setup/i18n/it.po index 262a33abdf7..0f78558505a 100644 --- a/addons/base_setup/i18n/it.po +++ b/addons/base_setup/i18n/it.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-09-08 14:11+0000\n" -"Last-Translator: Fabien (Open ERP) \n" +"PO-Revision-Date: 2010-01-29 14:07+0000\n" +"Last-Translator: Carlo \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:10+0000\n" +"X-Launchpad-Export-Date: 2010-01-30 04:39+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base_setup @@ -165,6 +165,9 @@ msgid "" "We suggest you to put bank information here:\n" "IBAN: BE74 1262 0121 6907 - SWIFT: CPDF BE71 - VAT: BE0477.472.701" msgstr "" +"Questa frase apparirà al fondo dei report.\n" +"Si possono inserire le coordinate bancarie qui:\n" +"IBAN: BE74 1262 0121 6907 - SWIFT: CPDF BE71 - VAT: BE0477.472.701" #. module: base_setup #: wizard_field:base_setup.base_setup,company,country_id:0 @@ -270,7 +273,7 @@ msgstr "Logo" #. module: base_setup #: model:ir.module.module,shortdesc:base_setup.module_meta_information msgid "Base Setup" -msgstr "" +msgstr "Configurazione di base" #. module: base_setup #: wizard_button:base_setup.base_setup,company,init:0 diff --git a/addons/board/i18n/it.po b/addons/board/i18n/it.po index a1bf5c5ecfc..80b1555039f 100644 --- a/addons/board/i18n/it.po +++ b/addons/board/i18n/it.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-09-08 12:29+0000\n" -"Last-Translator: Fabien (Open ERP) \n" +"PO-Revision-Date: 2010-01-29 14:08+0000\n" +"Last-Translator: Carlo \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:20+0000\n" +"X-Launchpad-Export-Date: 2010-01-30 04:40+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: board @@ -37,7 +37,7 @@ msgstr "Autore" #. module: board #: model:ir.module.module,shortdesc:board.module_meta_information msgid "Dashboard main module" -msgstr "" +msgstr "Modulo principale cruscotti" #. module: board #: view:board.note:0 @@ -53,7 +53,7 @@ msgstr "Larghezza" #. module: board #: constraint:ir.actions.act_window:0 msgid "Invalid model name in the action definition." -msgstr "" +msgstr "Nome del modulo non valido nella definizione dell'azione." #. module: board #: field:board.board.line,name:0 @@ -137,7 +137,7 @@ msgstr "Dashboard" #. module: board #: model:ir.module.module,description:board.module_meta_information msgid "Base module for all dashboards." -msgstr "" +msgstr "Modulo base per tutti i cruscotti." #. module: board #: field:board.board.line,position:0 diff --git a/addons/hr/i18n/lt.po b/addons/hr/i18n/lt.po index 85bec0daa25..4663d01ac37 100644 --- a/addons/hr/i18n/lt.po +++ b/addons/hr/i18n/lt.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-11-18 06:22+0000\n" -"Last-Translator: Fabien (Open ERP) \n" +"PO-Revision-Date: 2010-01-30 21:47+0000\n" +"Last-Translator: Domas Jokubauskis \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:21+0000\n" +"X-Launchpad-Export-Date: 2010-01-31 04:51+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: hr @@ -42,7 +42,7 @@ msgstr "Soc.draud.nr" #: model:ir.module.module,shortdesc:hr.module_meta_information #: model:ir.ui.menu,name:hr.menu_hr_root msgid "Human Resources" -msgstr "" +msgstr "Personalas" #. module: hr #: selection:hr.timesheet,dayofweek:0 @@ -52,7 +52,7 @@ msgstr "Sekmadienis" #. module: hr #: view:res.users:0 msgid "Parents" -msgstr "" +msgstr "Tėvai" #. module: hr #: constraint:ir.actions.act_window:0 @@ -431,7 +431,7 @@ msgstr "Tautybė" #. module: hr #: field:hr.employee,otherid:0 msgid "Other ID" -msgstr "" +msgstr "Kitas ID" #. module: hr #: field:hr.timesheet,name:0 diff --git a/addons/report_crm/i18n/it.po b/addons/report_crm/i18n/it.po index db59bbbb1a6..a6f7877f814 100644 --- a/addons/report_crm/i18n/it.po +++ b/addons/report_crm/i18n/it.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-02-09 21:36+0000\n" -"Last-Translator: LucaSub \n" +"PO-Revision-Date: 2010-01-29 14:12+0000\n" +"Last-Translator: Carlo \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:09+0000\n" +"X-Launchpad-Export-Date: 2010-01-30 04:39+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: report_crm @@ -30,12 +30,12 @@ msgstr "Probabilità Media" #. module: report_crm #: field:report.crm.case.service.dashboard,planned_revenue:0 msgid "Planned Revenue" -msgstr "" +msgstr "Entrata Pianificata" #. module: report_crm #: constraint:ir.actions.act_window:0 msgid "Invalid model name in the action definition." -msgstr "" +msgstr "Nome del modulo non valido nella definizione dell'azione." #. module: report_crm #: model:ir.model,name:report_crm.model_report_crm_case_categ @@ -52,7 +52,7 @@ msgstr "Sezione" #. module: report_crm #: field:report.crm.case.service.dashboard,user_id:0 msgid "Responsible" -msgstr "" +msgstr "Responsabile" #. module: report_crm #: field:report.crm.case.section,perc_done:0 @@ -62,7 +62,7 @@ msgstr "" #. module: report_crm #: field:report.crm.case.service.dashboard,planned_cost:0 msgid "Planned Costs" -msgstr "" +msgstr "Costi Pianificati" #. module: report_crm #: field:report.crm.case.section,nbr_cases:0 @@ -148,7 +148,7 @@ msgstr "Stato" #. module: report_crm #: field:report.crm.case.service.dashboard,name:0 msgid "Description" -msgstr "" +msgstr "Descrizione" #. module: report_crm #: model:ir.actions.act_window,name:report_crm.action_report_crm_case_user_tree_month @@ -169,7 +169,7 @@ msgstr "Reportistica" #. module: report_crm #: view:report.crm.case.service.dashboard:0 msgid "Planned costs" -msgstr "" +msgstr "Costi Pianificati" #. module: report_crm #: model:ir.actions.act_window,name:report_crm.action_view_closed_crm_case_dashboard @@ -195,7 +195,7 @@ msgstr "Cai Mensili per Sezione" #. module: report_crm #: field:report.crm.case.service.dashboard,date:0 msgid "Date" -msgstr "" +msgstr "Data" #. module: report_crm #: model:ir.actions.act_window,name:report_crm.action_report_crm_case_user_tree_month_my @@ -251,7 +251,7 @@ msgstr "" #. module: report_crm #: field:report.crm.case.service.dashboard,priority:0 msgid "Priority" -msgstr "" +msgstr "Priorità" #. module: report_crm #: field:report.crm.case.section,perc_cancel:0 @@ -298,12 +298,12 @@ msgstr "Annullato" #. module: report_crm #: field:report.crm.case.service.dashboard,create_date:0 msgid "Create Date" -msgstr "" +msgstr "Data Creazione" #. module: report_crm #: field:report.crm.case.service.dashboard,date_deadline:0 msgid "Deadline" -msgstr "" +msgstr "Scadenza" #. module: report_crm #: model:ir.model,name:report_crm.model_report_crm_case_user @@ -313,7 +313,7 @@ msgstr "Casi per Utente e Sezione" #. module: report_crm #: field:report.crm.case.service.dashboard,partner_id:0 msgid "Partner" -msgstr "" +msgstr "Partner" #. module: report_crm #: model:ir.module.module,description:report_crm.module_meta_information diff --git a/addons/sale/i18n/lt.po b/addons/sale/i18n/lt.po index 72d4f6e6915..b0b069652b9 100644 --- a/addons/sale/i18n/lt.po +++ b/addons/sale/i18n/lt.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0_rc3\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-09-08 16:15+0000\n" -"Last-Translator: Andrius Vilciauskas \n" +"PO-Revision-Date: 2010-01-30 22:13+0000\n" +"Last-Translator: Domas Jokubauskis \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:14+0000\n" +"X-Launchpad-Export-Date: 2010-01-31 04:51+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: sale @@ -72,7 +72,7 @@ msgstr "" #: wizard_field:sale.advance_payment_inv,init,qtty:0 #: rml:sale.order:0 msgid "Quantity" -msgstr "" +msgstr "Kiekis" #. module: sale #: wizard_view:sale.advance_payment_inv,create:0 From b94785776462b318dc5f638cc2ecda1b670422ad Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Sun, 31 Jan 2010 22:29:06 +0100 Subject: [PATCH 26/30] [IMP] Project Management - better useability bzr revid: fp@tinyerp.com-20100131212906-ja5a1pat6g56cef6 --- addons/project/project.py | 85 +++----------------- addons/project/project_view.xml | 54 ++++++------- addons/project/security/project_security.xml | 42 ---------- addons/project_gtd/__terp__.py | 2 +- addons/project_gtd/project_gtd.py | 2 +- addons/project_gtd/project_gtd_view.xml | 62 +++----------- 6 files changed, 54 insertions(+), 193 deletions(-) diff --git a/addons/project/project.py b/addons/project/project.py index b5865f8bd94..36abedd66ba 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -29,9 +29,9 @@ from tools.translate import _ class project_task_type(osv.osv): _name = 'project.task.type' - _description = 'Project task type' + _description = 'Task Stage' _columns = { - 'name': fields.char('Type', required=True, size=64, translate=True), + 'name': fields.char('Stage Name', required=True, size=64, translate=True), 'description': fields.text('Description'), 'sequence': fields.integer('Sequence'), } @@ -52,7 +52,6 @@ class project(osv.osv): res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name return res - def check_recursion(self, cursor, user, ids, parent=None): return super(project, self).check_recursion(cursor, user, ids, parent=parent) @@ -104,43 +103,30 @@ class project(osv.osv): raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.')) return super(project, self).unlink(cr, uid, ids, *args, **kwargs) _columns = { -# 'name': fields.char("Project Name", size=128, required=True), -# 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=128), + 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250), 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."), 'category_id': fields.many2one('account.analytic.account','Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc."), 'priority': fields.integer('Sequence'), -# 'manager': fields.many2one('res.users', 'Project Manager'), 'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive a request each time a task is completed by his team."), 'resource_ids': fields.many2many('resource.resource', 'project_resource_rel', 'project_id', 'resource_id', 'Project Members', help="Project's member. Not used in any computation, just for information purpose."), 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"), -# 'parent_id': fields.many2one('project.project', 'Parent Project',\ -# help="If you have [?] in the name, it means there are no analytic account linked to project."), -# 'child_id': fields.one2many('project.project', 'parent_id', 'Subproject'), 'planned_hours': fields.function(_progress_rate, multi="progress", method=True, string='Planned Time', help="Sum of planned hours of all tasks related to this project."), 'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project."), 'total_hours': fields.function(_progress_rate, multi="progress", method=True, string='Total Time', help="Sum of total hours of all tasks related to this project."), 'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', help="Percent of tasks closed according to the total of tasks todo."), -# 'partner_id': fields.many2one('res.partner', 'Partner'), -# 'contact_id': fields.many2one('res.partner.address', 'Contact'), 'warn_customer': fields.boolean('Warn Partner', help="If you check this, the user will have a popup when closing a task that propose a message to send by email to the customer."), 'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed."), 'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed."), -# 'notes': fields.text('Notes', help="Internal description of the project."), 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"), # 'state': fields.selection([('template', 'Template'), ('open', 'Running'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', required=True, readonly=True, # help='The project can be in either if the states \'Template\' and \'Running\'.\n If it is template then we can make projects based on the template projects. If its in \'Running\' state it is a normal project.\ # \n If it is to be reviewed then the state is \'Pending\'.\n When the project is completed the state is set to \'Done\'.'), -# 'company_id': fields.many2one('res.company', 'Company'), -# 'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"), - 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Allowed Task Types'), + 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages'), } _defaults = { 'active': lambda *a: True, -# 'manager': lambda object,cr,uid,context: uid, 'priority': lambda *a: 1, -# 'state': lambda *a: 'open', -# 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.project', context=c) } def _check_dates(self, cr, uid, ids): leave = self.read(cr, uid, ids[0],['date_start','date']) @@ -153,12 +139,6 @@ class project(osv.osv): (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date']) ] -# _order = "parent_id,priority,name" -# _constraints = [ -# (check_recursion, 'Error ! You can not create recursive projects.', ['parent_id']) -# ] - - # toggle activity of projects, their sub projects and their tasks def set_template(self, cr, uid, ids, context={}): res = self.setActive(cr, uid, ids, value=False, context=context) return res @@ -235,51 +215,12 @@ class task(osv.osv): _description = "Tasks" _date_name = "date_start" -# def compute_date(self,cr,uid): -# project_id = self.pool.get('project.project').search(cr,uid,[]) -# for i in range(len(project_id)): -# task_ids = self.pool.get('project.task').search(cr,uid,[('project_id','=',project_id[i])]) -# if task_ids: -# task_obj = self.pool.get('project.task').browse(cr,uid,task_ids) -# task_1 = task_obj[0] -# task_1.date_start = self.pool.get('project.project').browse(cr,uid,project_id[i]).date_start -# dt = mx.DateTime.strptime(task_1.date_start,"%Y-%m-%d").strftime("%Y-%m-%d") -# def Project_1(): -# title = "New Project" -# start = dt -# -# def task1(): -# start = dt -# effort = task_1.planned_hours -# title = "Task 1" -## project_1 = BalancedProject(Project_1) -## for t in project_1: -## print 'details:::',t.indent_name(), t.start, t.end, t.effort - - def _str_get(self, task, level=0, border='***', context={}): return border+' '+(task.user_id and task.user_id.name.upper() or '')+(level and (': L'+str(level)) or '')+(' - %.1fh / %.1fh'%(task.effective_hours or 0.0,task.planned_hours))+' '+border+'\n'+ \ border[0]+' '+(task.name or '')+'\n'+ \ (task.description or '')+'\n\n' - def _history_get(self, cr, uid, ids, name, args, context={}): - result = {} - for task in self.browse(cr, uid, ids, context=context): - result[task.id] = self._str_get(task, border='===') - t2 = task.parent_ids - level = 0 - while t2: - level -= 1 - result[task.id] = self._str_get(t2, level) + result[task.id] - t2 = t2.parent_ids - t3 = map(lambda x: (x,1), task.child_ids) - while t3: - t2 = t3.pop(0) - result[task.id] = result[task.id] + self._str_get(t2[0], t2[1]) - t3 += map(lambda x: (x,t2[1]+1), t2[0].child_ids) - return result - -# Compute: effective_hours, total_hours, progress + # Compute: effective_hours, total_hours, progress def _hours_get(self, cr, uid, ids, field_names, args, context): task_set = ','.join(map(str, ids)) cr.execute(("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id in (%s) GROUP BY task_id") % (task_set,)) @@ -293,6 +234,8 @@ class task(osv.osv): res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 100),2) else: res[task.id]['progress'] = 0.0 + if task.state in ('done','cancel'): + res[task.id]['progress'] = 100.0 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours return res @@ -337,19 +280,17 @@ class task(osv.osv): 'description': fields.text('Description'), 'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'), 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."), - 'type': fields.many2one('project.task.type', 'Type', readonly=True), + 'type': fields.many2one('project.task.type', 'Stage', readonly=True), 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True, help='If the task is created the state \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\ \n If the task is over, the states is set to \'Done\'.'), 'date_start': fields.datetime('Starting Date'), 'date_end': fields.datetime('Ending Date'), - 'date_deadline': fields.datetime('Deadline'), - 'date_close': fields.datetime('Date Closed', readonly=True), + 'date_deadline': fields.date('Deadline'), 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade', help="If you have [?] in the project name, it means there are no analytic account linked to this project."), 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'), 'child_ids': fields.many2many('project.task', 'project_task_child_rel', 'task_id', 'child_id', 'Delegated Tasks'), - 'history': fields.function(_history_get, method=True, string="Task Details", type="text"), 'notes': fields.text('Notes'), 'occupation_rate': fields.float('Occupation Rate', help='The occupation rate fields indicates how much of his time a user is working on a task. A 100% occupation rate means the user works full time on the tasks. The ending date of a task is computed like this: Starting Date + Duration / Occupation Rate.'), 'planned_hours': fields.float('Planned Hours', required=True, help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'), @@ -378,7 +319,7 @@ class task(osv.osv): 'occupation_rate':lambda *a: '1', 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c) } - _order = "sequence, priority, date_deadline, id" + _order = "sequence, priority, date_start, id" _constraints = [ (_check_dates, 'Error! task start-date must be lower then task end-date.', ['date_start', 'date_end']) @@ -430,7 +371,7 @@ class task(osv.osv): 'ref_doc1': 'project.task,%d'% (task.id,), 'ref_doc2': 'project.project,%d'% (project.id,), }) - self.write(cr, uid, [task.id], {'state': 'done', 'date_close':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0}) + self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0}) if task.parent_ids and task.parent_ids.state in ('pending','draft'): reopen = True for child in task.parent_ids.child_ids: @@ -509,9 +450,9 @@ class task(osv.osv): typeid = typ.type.id types = map(lambda x:x.id, typ.project_id.type_ids) if types: - if typeid and typeid in types and types.index(typeid) != 0 : + if typeid and typeid in types: index = types.index(typeid) - self.write(cr, uid, typ.id, {'type': types[index-1]}) + self.write(cr, uid, typ.id, {'type': index and types[index-1] or False}) return True task() diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index 55819fb911b..9181b6dfd88 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -3,7 +3,6 @@ - @@ -13,7 +12,7 @@ action="account_budget.act_crossovered_budget_view" /> - @@ -83,8 +82,7 @@ - + project.task.work.form @@ -268,10 +266,7 @@ - - - @@ -313,7 +308,7 @@ - + @@ -327,7 +322,6 @@ - @@ -338,16 +332,17 @@ - + + -