181 lines
6.1 KiB
Python
181 lines
6.1 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
|
|
import datetime
|
|
|
|
from sql import Literal
|
|
from sql.aggregate import Sum
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import ModelSQL, ModelStorage, ModelView, Unique, fields
|
|
from trytond.pool import Pool
|
|
from trytond.pyson import Bool, Eval, If
|
|
from trytond.tools import grouped_slice, reduce_ids
|
|
from trytond.transaction import Transaction
|
|
|
|
from .exceptions import CompanyValidationError
|
|
|
|
|
|
class Work(ModelSQL, ModelView):
|
|
__name__ = 'timesheet.work'
|
|
name = fields.Char('Name',
|
|
states={
|
|
'invisible': Bool(Eval('origin')),
|
|
'required': ~Eval('origin'),
|
|
},
|
|
help="The main identifier of the work.")
|
|
origin = fields.Reference('Origin', selection='get_origin',
|
|
states={
|
|
'invisible': Bool(Eval('name')),
|
|
'required': ~Eval('name'),
|
|
},
|
|
help="Use to relate the time spent to other records.")
|
|
duration = fields.Function(fields.TimeDelta('Timesheet Duration',
|
|
'company_work_time', help="Total time spent on this work."),
|
|
'get_duration')
|
|
timesheet_start_date = fields.Date('Timesheet Start',
|
|
domain=[
|
|
If(Eval('timesheet_start_date') & Eval('timesheet_end_date'),
|
|
('timesheet_start_date', '<=', Eval('timesheet_end_date')),
|
|
()),
|
|
],
|
|
help="Restrict adding lines before the date.")
|
|
timesheet_end_date = fields.Date('Timesheet End',
|
|
domain=[
|
|
If(Eval('timesheet_start_date') & Eval('timesheet_end_date'),
|
|
('timesheet_end_date', '>=', Eval('timesheet_start_date')),
|
|
()),
|
|
],
|
|
help="Restrict adding lines after the date.")
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
help="Make the work belong to the company.")
|
|
timesheet_lines = fields.One2Many('timesheet.line', 'work',
|
|
'Timesheet Lines',
|
|
help="Spend time on this work.")
|
|
# Self referring field to use for aggregation in graph view
|
|
work = fields.Function(fields.Many2One('timesheet.work', 'Work'),
|
|
'get_work')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_constraints += [
|
|
('origin_unique', Unique(t, t.origin, t.company),
|
|
'timesheet.msg_work_origin_unique_company'),
|
|
]
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@classmethod
|
|
def _get_origin(cls):
|
|
'Return list of Model names for origin Reference'
|
|
return []
|
|
|
|
@classmethod
|
|
def get_origin(cls):
|
|
Model = Pool().get('ir.model')
|
|
get_name = Model.get_name
|
|
models = cls._get_origin()
|
|
return [('', '')] + [(m, get_name(m)) for m in models]
|
|
|
|
@classmethod
|
|
def get_duration(cls, works, name):
|
|
pool = Pool()
|
|
Line = pool.get('timesheet.line')
|
|
transaction = Transaction()
|
|
cursor = transaction.connection.cursor()
|
|
context = transaction.context
|
|
|
|
table_w = cls.__table__()
|
|
line = Line.__table__()
|
|
ids = [w.id for w in works]
|
|
durations = dict.fromkeys(ids, None)
|
|
where = Literal(True)
|
|
if context.get('from_date'):
|
|
where &= line.date >= context['from_date']
|
|
if context.get('to_date'):
|
|
where &= line.date <= context['to_date']
|
|
if context.get('employees'):
|
|
where &= line.employee.in_(context['employees'])
|
|
|
|
query_table = table_w.join(line, 'LEFT',
|
|
condition=line.work == table_w.id)
|
|
|
|
for sub_ids in grouped_slice(ids):
|
|
red_sql = reduce_ids(table_w.id, sub_ids)
|
|
cursor.execute(*query_table.select(table_w.id, Sum(line.duration),
|
|
where=red_sql & where,
|
|
group_by=table_w.id))
|
|
for work_id, duration in cursor:
|
|
# SQLite uses float for SUM
|
|
if duration and not isinstance(duration, datetime.timedelta):
|
|
duration = datetime.timedelta(seconds=duration)
|
|
durations[work_id] = duration
|
|
return durations
|
|
|
|
def get_work(self, name):
|
|
return self.id
|
|
|
|
def get_rec_name(self, name):
|
|
if isinstance(self.origin, ModelStorage):
|
|
return self.origin.rec_name
|
|
else:
|
|
return self.name
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
if clause[1].startswith('!') or clause[1].startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
return [bool_op,
|
|
('name',) + tuple(clause[1:]),
|
|
] + [
|
|
('origin.rec_name',) + tuple(clause[1:]) + (origin,)
|
|
for origin in cls._get_origin()]
|
|
|
|
@classmethod
|
|
def copy(cls, works, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('timesheet_lines', None)
|
|
return super().copy(works, default=default)
|
|
|
|
@classmethod
|
|
def validate(cls, works):
|
|
super().validate(works)
|
|
for work in works:
|
|
if work.origin and not work._validate_company():
|
|
raise CompanyValidationError(
|
|
gettext('timesheet.msg_work_company_different_origin',
|
|
work=work.rec_name))
|
|
|
|
def _validate_company(self):
|
|
return True
|
|
|
|
@classmethod
|
|
def search_global(cls, text):
|
|
for record, rec_name, icon in super().search_global(text):
|
|
icon = icon or 'tryton-clock'
|
|
yield record, rec_name, icon
|
|
|
|
@property
|
|
def hours(self):
|
|
if not self.duration:
|
|
return 0
|
|
return self.duration.total_seconds() / 60 / 60
|
|
|
|
|
|
class WorkContext(ModelView):
|
|
__name__ = 'timesheet.work.context'
|
|
from_date = fields.Date('From Date',
|
|
help="Do not take into account lines before the date.")
|
|
to_date = fields.Date('To Date',
|
|
help="Do not take into account lines after the date.")
|