# 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 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']) origin = fields.Reference('Origin', selection='get_origin', states={ 'invisible': Bool(Eval('name')), 'required': ~Eval('name'), }, depends=['name']) active = fields.Boolean('Active') 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') timesheet_end_date = fields.Date('Timesheet End') company = fields.Many2One('company.company', 'Company', required=True, select=True) timesheet_lines = fields.One2Many('timesheet.line', 'work', 'Timesheet Lines', depends=['active'], states={ 'readonly': Not(Bool(Eval('active'))), }) # 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 then 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') to_date = fields.Date('To Date')