318 lines
11 KiB
Python
318 lines
11 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 Max, Sum
|
|
from sql.functions import Extract
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import Index, ModelSQL, ModelView, Unique, fields
|
|
from trytond.pool import Pool
|
|
from trytond.pyson import Date, Eval, PYSONEncoder, TimeDelta
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Button, StateAction, StateView, Wizard
|
|
|
|
from .exceptions import DurationValidationError
|
|
|
|
|
|
class Line(ModelSQL, ModelView):
|
|
__name__ = 'timesheet.line'
|
|
company = fields.Many2One('company.company', 'Company', required=True,
|
|
help="The company on which the time is spent.")
|
|
employee = fields.Many2One(
|
|
'company.employee', "Employee", required=True,
|
|
search_context={
|
|
'active_test': False,
|
|
},
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
['OR',
|
|
('start_date', '=', None),
|
|
('start_date', '<=', Eval('date', None)),
|
|
],
|
|
['OR',
|
|
('end_date', '=', None),
|
|
('end_date', '>=', Eval('date', None)),
|
|
],
|
|
],
|
|
help="The employee who spends the time.")
|
|
date = fields.Date(
|
|
"Date", required=True,
|
|
help="When the time is spent.")
|
|
duration = fields.TimeDelta(
|
|
'Duration', 'company_work_time', required=True,
|
|
domain=[
|
|
('duration', '>=', TimeDelta()),
|
|
])
|
|
work = fields.Many2One(
|
|
'timesheet.work', "Work", required=True,
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
['OR',
|
|
('timesheet_start_date', '=', None),
|
|
('timesheet_start_date', '<=', Eval('date', None)),
|
|
],
|
|
['OR',
|
|
('timesheet_end_date', '=', None),
|
|
('timesheet_end_date', '>=', Eval('date', None)),
|
|
],
|
|
],
|
|
help="The work on which the time is spent.")
|
|
description = fields.Char('Description',
|
|
help="Additional description of the work done.")
|
|
uuid = fields.Char("UUID", readonly=True, strip=False)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order = [
|
|
('date', 'DESC'),
|
|
('id', 'DESC'),
|
|
]
|
|
t = cls.__table__()
|
|
cls._sql_constraints = [
|
|
('uuid_unique', Unique(t, t.uuid),
|
|
'timesheet.msg_line_uuid_unique'),
|
|
]
|
|
cls._sql_indexes.update({
|
|
Index(
|
|
t,
|
|
(t.date, Index.Range()),
|
|
(t.company, Index.Range()),
|
|
(t.employee, Index.Range())),
|
|
Index(
|
|
t,
|
|
(t.date, Index.Range()),
|
|
(t.employee, Index.Range())),
|
|
Index(
|
|
t,
|
|
(Extract('YEAR', t.date), Index.Range()),
|
|
(Extract('WEEK', t.date), Index.Range()),
|
|
(t.employee, Index.Range())),
|
|
Index(
|
|
t,
|
|
(Extract('YEAR', t.date), Index.Range()),
|
|
(Extract('MONTH', t.date), Index.Range()),
|
|
(t.employee, Index.Range())),
|
|
})
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@staticmethod
|
|
def default_employee():
|
|
User = Pool().get('res.user')
|
|
employee_id = None
|
|
if Transaction().context.get('employee'):
|
|
employee_id = Transaction().context['employee']
|
|
else:
|
|
user = User(Transaction().user)
|
|
if user.employee:
|
|
employee_id = user.employee.id
|
|
if employee_id:
|
|
return employee_id
|
|
|
|
@staticmethod
|
|
def default_date():
|
|
Date_ = Pool().get('ir.date')
|
|
return Transaction().context.get('date') or Date_.today()
|
|
|
|
@classmethod
|
|
def validate_fields(cls, lines, field_names):
|
|
super().validate_fields(lines, field_names)
|
|
cls.check_duration(lines, field_names)
|
|
|
|
@classmethod
|
|
def check_duration(cls, lines, field_names=None):
|
|
if field_names and 'duration' not in field_names:
|
|
return
|
|
for line in lines:
|
|
if line.duration < datetime.timedelta():
|
|
raise DurationValidationError(
|
|
gettext('timesheet.msg_line_duration_positive',
|
|
line=line.rec_name))
|
|
|
|
@classmethod
|
|
def copy(cls, lines, default=None):
|
|
if default is not None:
|
|
default = default.copy()
|
|
else:
|
|
default = {}
|
|
default.setdefault('uuid')
|
|
return super().copy(lines, default=default)
|
|
|
|
@property
|
|
def hours(self):
|
|
return self.duration.total_seconds() / 60 / 60
|
|
|
|
def to_json(self):
|
|
return {
|
|
'id': self.id,
|
|
'work': self.work.id,
|
|
'work.name': self.work.rec_name,
|
|
'duration': self.duration.total_seconds(),
|
|
'description': self.description,
|
|
'uuid': self.uuid,
|
|
}
|
|
|
|
|
|
class EnterLinesStart(ModelView):
|
|
__name__ = 'timesheet.line.enter.start'
|
|
employee = fields.Many2One('company.employee', 'Employee', required=True,
|
|
domain=[
|
|
('company', '=', Eval('context', {}).get('company', -1)),
|
|
['OR',
|
|
('start_date', '=', None),
|
|
('start_date', '<=', Eval('date', None)),
|
|
],
|
|
['OR',
|
|
('end_date', '=', None),
|
|
('end_date', '>=', Eval('date', None)),
|
|
],
|
|
],
|
|
help="The employee who spends the time.")
|
|
date = fields.Date('Date', required=True,
|
|
help="When the time is spent.")
|
|
|
|
@staticmethod
|
|
def default_employee():
|
|
Line = Pool().get('timesheet.line')
|
|
return Line.default_employee()
|
|
|
|
@staticmethod
|
|
def default_date():
|
|
Line = Pool().get('timesheet.line')
|
|
return Line.default_date()
|
|
|
|
|
|
class EnterLines(Wizard):
|
|
__name__ = 'timesheet.line.enter'
|
|
start = StateView('timesheet.line.enter.start',
|
|
'timesheet.line_enter_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Enter', 'enter', 'tryton-ok', default=True),
|
|
])
|
|
enter = StateAction('timesheet.act_line_form')
|
|
|
|
def do_enter(self, action):
|
|
pool = Pool()
|
|
Lang = pool.get('ir.lang')
|
|
date = self.start.date
|
|
date = Date(date.year, date.month, date.day)
|
|
action['pyson_domain'] = PYSONEncoder().encode([
|
|
('employee', '=', self.start.employee.id),
|
|
('company', '=', self.start.employee.company.id),
|
|
('date', '=', date),
|
|
])
|
|
action['pyson_context'] = PYSONEncoder().encode({
|
|
'employee': self.start.employee.id,
|
|
'company': self.start.employee.company.id,
|
|
'date': date,
|
|
})
|
|
action['name'] += ' @ %(date)s - %(employee)s' % {
|
|
'date': Lang.get().strftime(self.start.date),
|
|
'employee': self.start.employee.rec_name,
|
|
}
|
|
return action, {}
|
|
|
|
def transition_enter(self):
|
|
return 'end'
|
|
|
|
|
|
class HoursEmployee(ModelSQL, ModelView):
|
|
__name__ = 'timesheet.hours_employee'
|
|
employee = fields.Many2One('company.employee', 'Employee')
|
|
duration = fields.TimeDelta('Duration', 'company_work_time')
|
|
|
|
@staticmethod
|
|
def table_query():
|
|
pool = Pool()
|
|
Line = pool.get('timesheet.line')
|
|
line = Line.__table__()
|
|
where = Literal(True)
|
|
if Transaction().context.get('start_date'):
|
|
where &= line.date >= Transaction().context['start_date']
|
|
if Transaction().context.get('end_date'):
|
|
where &= line.date <= Transaction().context['end_date']
|
|
return line.select(
|
|
line.employee.as_('id'),
|
|
line.employee,
|
|
Sum(line.duration).as_('duration'),
|
|
where=where,
|
|
group_by=line.employee)
|
|
|
|
|
|
class HoursEmployeeContext(ModelView):
|
|
__name__ = 'timesheet.hours_employee.context'
|
|
start_date = fields.Date('Start Date')
|
|
end_date = fields.Date('End Date')
|
|
|
|
|
|
class HoursEmployeeWeekly(ModelSQL, ModelView):
|
|
__name__ = 'timesheet.hours_employee_weekly'
|
|
year = fields.Integer("Year")
|
|
week = fields.Integer("Week")
|
|
employee = fields.Many2One('company.employee', 'Employee')
|
|
duration = fields.TimeDelta('Duration', 'company_work_time')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('year', 'DESC'))
|
|
cls._order.insert(1, ('week', 'DESC'))
|
|
cls._order.insert(2, ('employee', 'ASC'))
|
|
|
|
@classmethod
|
|
def table_query(cls):
|
|
pool = Pool()
|
|
Line = pool.get('timesheet.line')
|
|
line = Line.__table__()
|
|
year_column = Extract('YEAR', line.date).as_('year')
|
|
week_column = Extract('WEEK', line.date).as_('week')
|
|
return line.select(
|
|
Max(Extract('WEEK', line.date)
|
|
+ Extract('YEAR', line.date) * 100
|
|
+ line.employee * 1000000).as_('id'),
|
|
year_column,
|
|
week_column,
|
|
line.employee,
|
|
Sum(line.duration).as_('duration'),
|
|
group_by=(year_column, week_column, line.employee))
|
|
|
|
|
|
class HoursEmployeeMonthly(ModelSQL, ModelView):
|
|
__name__ = 'timesheet.hours_employee_monthly'
|
|
year = fields.Integer("Year")
|
|
month = fields.Many2One('ir.calendar.month', "Month")
|
|
employee = fields.Many2One('company.employee', 'Employee')
|
|
duration = fields.TimeDelta('Duration', 'company_work_time')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('year', 'DESC'))
|
|
cls._order.insert(1, ('month.index', 'DESC'))
|
|
cls._order.insert(2, ('employee', 'ASC'))
|
|
|
|
@classmethod
|
|
def table_query(cls):
|
|
pool = Pool()
|
|
Line = pool.get('timesheet.line')
|
|
Month = pool.get('ir.calendar.month')
|
|
line = Line.__table__()
|
|
month = Month.__table__()
|
|
year_column = Extract('YEAR', line.date).as_('year')
|
|
month_index = Extract('MONTH', line.date)
|
|
return line.join(month, condition=month_index == month.id).select(
|
|
Max(Extract('MONTH', line.date)
|
|
+ Extract('YEAR', line.date) * 100
|
|
+ line.employee * 1000000).as_('id'),
|
|
year_column,
|
|
month.id.as_('month'),
|
|
line.employee,
|
|
Sum(line.duration).as_('duration'),
|
|
group_by=(year_column, month.id, line.employee))
|