# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from __future__ import division import datetime from sql import Literal from sql.aggregate import Sum from trytond.model import ModelView, ModelSQL, fields, Unique from trytond.pyson import Not, Bool, Eval, If from trytond.transaction import Transaction from trytond.pool import Pool from trytond.tools import reduce_ids, grouped_slice from trytond import backend __all__ = ['Work', 'WorkContext'] class Work(ModelSQL, ModelView): 'Work' __name__ = 'timesheet.work' name = fields.Char('Name', states={ 'invisible': Bool(Eval('origin')), 'required': ~Eval('origin'), }, depends=['origin'], help="The main identifier of the work.") origin = fields.Reference('Origin', selection='get_origin', states={ 'invisible': Bool(Eval('name')), 'required': ~Eval('name'), }, depends=['name'], help="Use to relate the time spent to other records.") active = fields.Boolean('Active', help="Uncheck to exclude the work from future use.") 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')), ()), ], depends=['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')), ()), ], depends=['timesheet_start_date'], help="Restrict adding lines after the date.") company = fields.Many2One('company.company', 'Company', required=True, select=True, help="Make the work belong to the company.") timesheet_lines = fields.One2Many('timesheet.line', 'work', 'Timesheet Lines', depends=['active'], states={ 'readonly': Not(Bool(Eval('active'))), }, 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(Work, cls).__setup__() t = cls.__table__() cls._sql_constraints += [ ('origin_unique', Unique(t, t.origin, t.company), 'The origin must be unique per company.'), ] cls._error_messages.update({ 'mismatch_company': ('The company of the work "%(work)s" ' 'is different than the origin\'s company'), }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') table_h = TableHandler(cls, module_name) table = cls.__table__() cursor = Transaction().connection.cursor() super(Work, cls).__register__(module_name) # Migration from 4.0: remove required on name table_h.not_null_action('name', 'remove') # Migration from 4.0: remove parent, left and right if table_h.column_exist('parent'): id2name = {} id2parent = {} cursor.execute(*table.select( table.id, table.parent, table.name)) for id_, parent, name in cursor: id2name[id_] = name id2parent[id_] = parent for id_, name in id2name.iteritems(): parent = id2parent[id_] while parent: name = '%s\\%s' % (id2name[parent], name) parent = id2parent[parent] cursor.execute(*table.update( [table.name], [name], where=table.id == id_)) table_h.drop_column('parent') table_h.drop_column('left') table_h.drop_column('right') # Migration from 4.0: remove timesheet_available if table_h.column_exist('timesheet_available'): cursor.execute(*table.delete( where=table.timesheet_available == False)) table_h.drop_column('timesheet_available') @staticmethod def default_active(): return True @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') models = cls._get_origin() models = Model.search([ ('model', 'in', models), ]) return [('', '')] + [(m.model, m.name) 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.fetchall(): # 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 self.origin: 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 = {} default = default.copy() if 'timesheet_lines' not in default: default['timesheet_lines'] = None return super(Work, cls).copy(works, default=default) @classmethod def validate(cls, works): super(Work, cls).validate(works) for work in works: if work.origin and not work._validate_company(): cls.raise_user_error('mismatch_company', { 'work': work.rec_name, }) def _validate_company(self): return True @classmethod def search_global(cls, text): for record, rec_name, icon in super(Work, cls).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): 'Work Context' __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.")