350 lines
12 KiB
Python
350 lines
12 KiB
Python
# The COPYRIGHT file at the top level of this repository contains the full
|
|
# copyright notices and license terms.
|
|
import datetime
|
|
from dateutil.relativedelta import relativedelta
|
|
from trytond.model import ModelSQL, ModelView, fields
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval, PYSONDecoder, PYSONEncoder
|
|
from trytond.wizard import Wizard, StateAction, StateView, Button
|
|
from trytond.rpc import RPC
|
|
|
|
__all__ = ['Work', 'PredecessorSuccessor',
|
|
'ProjectResourcePlanStart', 'ProjectResourcePlanTasks',
|
|
'ProjectResourcePlan']
|
|
|
|
__metaclass__ = PoolMeta
|
|
|
|
|
|
class Work:
|
|
__name__ = 'project.work'
|
|
|
|
predecessors = fields.Many2Many('project.predecessor_successor',
|
|
'successor', 'predecessor', 'Predecessors',
|
|
domain=[
|
|
('id', '!=', Eval('id')),
|
|
],
|
|
states={
|
|
'invisible': Eval('type') == 'project',
|
|
}, depends=['type', 'id'])
|
|
successors = fields.Many2Many('project.predecessor_successor',
|
|
'predecessor', 'successor', 'Successors',
|
|
domain=[
|
|
('id', '!=', Eval('id')),
|
|
],
|
|
states={
|
|
'invisible': Eval('type') == 'project',
|
|
}, depends=['type', 'id'])
|
|
bookings = fields.One2Many('resource.booking', 'document', 'Bookings',
|
|
states={
|
|
'invisible': Eval('type') != 'task',
|
|
}, depends=['type'])
|
|
expected_end_date = fields.DateTime('Expected End Date',
|
|
states={
|
|
'invisible': Eval('type') == 'project',
|
|
},
|
|
depends=['type'])
|
|
planned_start_date = fields.DateTime('Planned Start Date',
|
|
states={
|
|
'invisible': Eval('type') != 'task',
|
|
},
|
|
depends=['type'])
|
|
planned_end_date = fields.DateTime('Planned End Date',
|
|
states={
|
|
'invisible': Eval('type') != 'task',
|
|
},
|
|
depends=['type'])
|
|
planned_start_date_project = fields.Function(fields.DateTime(
|
|
'Planned Start Date',
|
|
states={
|
|
'invisible': Eval('type') == 'task',
|
|
},
|
|
depends=['type']),
|
|
'get_project_dates')
|
|
planned_end_date_project = fields.Function(fields.DateTime(
|
|
'Planned End Date',
|
|
states={
|
|
'invisible': Eval('type') == 'task',
|
|
},
|
|
depends=['type']),
|
|
'get_project_dates')
|
|
expected_end_date_project = fields.Function(fields.DateTime(
|
|
'Expected End Date',
|
|
states={
|
|
'invisible': Eval('type') == 'task',
|
|
},
|
|
depends=['type']),
|
|
'get_project_dates', setter='set_expected_end_date_project')
|
|
|
|
planned_employee = fields.Many2One('company.employee', 'Planned')
|
|
assigned_employee = fields.Many2One('company.employee','Assigned')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Work, cls).__setup__()
|
|
cls._error_messages.update({
|
|
'no_resource_found': 'No resource found for the employee "%s"',
|
|
})
|
|
cls._buttons.update({
|
|
'schedule': {
|
|
'invisible': (Eval('type') != 'task')
|
|
},
|
|
})
|
|
|
|
@property
|
|
def scheduled(self):
|
|
return any(b.state == 'confirmed' for b in self.bookings)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def schedule(cls, works, planning_days=None, done_works=None):
|
|
pool = Pool()
|
|
Resource = pool.get('resource.resource')
|
|
today = datetime.datetime.now()
|
|
Booking = pool.get('resource.booking')
|
|
|
|
def get_planned_start(predecessors):
|
|
planned_start = None
|
|
for task in predecessors:
|
|
if not planned_start:
|
|
planned_start = task.planned_end_date
|
|
if task.planned_end_date:
|
|
planned_start = max(planned_start, task.planned_end_date)
|
|
if planned_start and planned_start.time() >= task.company.day_ends:
|
|
tomorrow = planned_start + relativedelta(days=1)
|
|
#Skip saturdays and sundays
|
|
while tomorrow.weekday() > 4:
|
|
tomorrow += relativedelta(days=1)
|
|
planned_start = datetime.datetime.combine(tomorrow,
|
|
task.company.day_starts)
|
|
return planned_start
|
|
|
|
if done_works is None:
|
|
done_works = set()
|
|
to_allocate = []
|
|
for work in works:
|
|
if work.id in done_works or work.scheduled:
|
|
continue
|
|
|
|
Booking.delete(work.bookings)
|
|
planned_end = None
|
|
predecessors = list(work.predecessors)
|
|
resource = None
|
|
if predecessors:
|
|
cls.schedule(predecessors, planning_days, done_works)
|
|
if not work.assigned_employee:
|
|
start = get_planned_start(list(work.predecessors)) or today
|
|
# Find the employee that can start first the task
|
|
resource = Resource.get_free_resource(start, work.effort,
|
|
domain=work.get_free_resource_domain())
|
|
if not resource:
|
|
continue
|
|
if not work.effort:
|
|
continue
|
|
|
|
planned_start = get_planned_start(predecessors)
|
|
start = planned_start or today
|
|
|
|
effort = work.effort
|
|
if not resource:
|
|
resources = Resource.search([
|
|
('employee', '=', work.assigned_employee.id),
|
|
], limit=1)
|
|
if not resources:
|
|
cls.raise_user_error('no_resource_found',
|
|
assigned_employee.rec_name)
|
|
resource, = resources
|
|
bookings = resource.book_hours(start, effort, planning_days)
|
|
s, e = resource.book_interval(bookings)
|
|
if not s:
|
|
continue
|
|
planned_start = planned_start and min(planned_start, s) or s
|
|
planned_end = planned_end and max(planned_end, e) or e
|
|
resource.book(bookings, 'project.work,%s' % work.id)
|
|
|
|
work.planned_end_date = planned_end
|
|
work.planned_start_date = planned_start
|
|
work.planned_employee = resource.employee.id
|
|
work.save()
|
|
done_works.add(work.id)
|
|
|
|
|
|
def get_free_resource_domain(self):
|
|
return [('employee', '!=', None)]
|
|
|
|
@classmethod
|
|
def get_project_dates(cls, works, names):
|
|
result = {}
|
|
for name in names:
|
|
result[name] = {}.fromkeys([w.id for w in works], None)
|
|
|
|
for work in works:
|
|
for child in cls.search([('parent', 'child_of', [w.id])]):
|
|
for name in names:
|
|
func = min if 'start' in name else max
|
|
fname = name[:-8]
|
|
current = result[name][work.id]
|
|
value = getattr(child, fname)
|
|
if not current:
|
|
current = value
|
|
if value:
|
|
result[name][work.id] = func(current, value)
|
|
return result
|
|
|
|
@classmethod
|
|
def set_expected_end_date_project(cls, works, name, value):
|
|
childs = cls.search([
|
|
('parent', 'child_of', [w.id for w in works]),
|
|
])
|
|
cls.write(childs, {
|
|
'expected_end_date': value,
|
|
})
|
|
|
|
def get_assigned_employee(self, name):
|
|
if self.assigned_employee:
|
|
return self.assigned_employee
|
|
|
|
@classmethod
|
|
def set_assigned_employee(cls, works, name, value):
|
|
Allocation = Pool().get('project.allocation')
|
|
Allocation.delete([allocation for work in works
|
|
for allocation in work.allocations])
|
|
if value:
|
|
to_create = []
|
|
for work in works:
|
|
to_create.append({
|
|
'employee': value,
|
|
'work': work.id,
|
|
'percentage': 100.0,
|
|
})
|
|
Allocation.create(to_create)
|
|
|
|
@classmethod
|
|
def search_assigned_employee(cls, name, clause):
|
|
if clause[2] is None:
|
|
return [('allocations',) + tuple(clause[1:])]
|
|
return [('allocations.employee',) + tuple(clause[1:])]
|
|
|
|
|
|
class PredecessorSuccessor(ModelSQL):
|
|
'Predecessor - Successor'
|
|
__name__ = 'project.predecessor_successor'
|
|
predecessor = fields.Many2One('project.work', 'Predecessor',
|
|
ondelete='CASCADE', required=True, select=True)
|
|
successor = fields.Many2One('project.work', 'Successor',
|
|
ondelete='CASCADE', required=True, select=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(PredecessorSuccessor, cls).__setup__()
|
|
cls.__rpc__.update({
|
|
'read': RPC(True),
|
|
'search': RPC(True),
|
|
'search_read': RPC(True),
|
|
})
|
|
|
|
class ProjectResourcePlanStart(ModelView):
|
|
'Project Resource Plan Start'
|
|
__name__ = 'project.resource.plan.start'
|
|
|
|
view_search = fields.Many2One('ir.ui.view_search', 'Search',
|
|
domain=[
|
|
('model', '=', 'project.work'),
|
|
])
|
|
domain = fields.Char('Domain')
|
|
order = fields.Char('Order')
|
|
planning_days = fields.Integer('Planning Days')
|
|
delete_drafts = fields.Boolean('Delete drafts', help='If marked all the '
|
|
'draft bookings will be deleted.')
|
|
confirm_bookings = fields.Boolean('Confirm Bookings', help='If marked the '
|
|
'generated bookings will be confirmed.')
|
|
|
|
@staticmethod
|
|
def default_delete_drafts():
|
|
return True
|
|
|
|
@staticmethod
|
|
def default_planning_days():
|
|
return 90
|
|
|
|
@fields.depends('view_search')
|
|
def on_change_with_domain(self):
|
|
return self.view_search.domain if self.view_search else None
|
|
|
|
|
|
class ProjectResourcePlanTasks(ModelView):
|
|
'Project Resource Plan Tasks'
|
|
__name__ = 'project.resource.plan.tasks'
|
|
|
|
tasks = fields.Many2Many('project.work', None, None, 'Tasks To Schedule',
|
|
domain=[
|
|
('type', '=', 'task'),
|
|
])
|
|
|
|
|
|
class ProjectResourcePlan(Wizard):
|
|
'Project Resource Plan'
|
|
__name__ = 'project.resource.plan'
|
|
|
|
start = StateView('project.resource.plan.start',
|
|
'project_resource_plan.resource_plan_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Ok', 'tasks', 'tryton-ok', default=True),
|
|
])
|
|
tasks = StateView('project.resource.plan.tasks',
|
|
'project_resource_plan.resource_plan_tasks_view_form', [
|
|
Button('Back', 'start', 'tryton-go-previous'),
|
|
Button('Ok', 'plan', 'tryton-ok', default=True),
|
|
])
|
|
plan = StateAction('project_resource_plan.act_project_allocation_tree')
|
|
|
|
def _execute(self, state_name):
|
|
result = super(ProjectResourcePlan, self)._execute(state_name)
|
|
if state_name == 'tasks' and self.start.domain:
|
|
#Ensure that the view domain respects the start domain
|
|
domain = result['view']['fields_view']['fields']['tasks']['domain']
|
|
decoder = PYSONDecoder()
|
|
domain = decoder.decode(domain)
|
|
view_domain = decoder.decode(self.start.domain)
|
|
domain.extend(view_domain)
|
|
domain = PYSONEncoder().encode(domain)
|
|
result['view']['fields_view']['fields']['tasks']['domain'] = domain
|
|
return result
|
|
|
|
def default_tasks(self, fields):
|
|
pool = Pool()
|
|
Work = pool.get('project.work')
|
|
order = None
|
|
domain = []
|
|
if self.start.domain:
|
|
domain = PYSONDecoder().decode(self.start.domain)
|
|
if self.start.order:
|
|
order = PYSONDecoder().decode(self.start.order)
|
|
domain.append(('type', '=', 'task'))
|
|
tasks = Work.search(domain, order=order)
|
|
return {'tasks': [t.id for t in tasks]}
|
|
|
|
def do_plan(self, action):
|
|
pool = Pool()
|
|
Work = pool.get('project.work')
|
|
Booking = pool.get('resource.booking')
|
|
|
|
if self.start.delete_drafts:
|
|
to_delete = Booking.search([
|
|
('state', '=', 'draft'),
|
|
])
|
|
|
|
if to_delete:
|
|
Booking.cancel(to_delete)
|
|
Booking.delete(to_delete)
|
|
|
|
tasks = list(self.tasks.tasks)
|
|
planning_days = relativedelta(days=self.start.planning_days)
|
|
Work.schedule(tasks, planning_days)
|
|
if self.start.confirm_bookings:
|
|
to_confirm = []
|
|
for task in tasks:
|
|
to_confirm.extend(list(task.bookings))
|
|
if to_confirm:
|
|
Booking.confirm(to_confirm)
|
|
return action, {'res_id': [x.id for x in tasks]}
|