first commit
This commit is contained in:
382
modules/attendance/attendance.py
Normal file
382
modules/attendance/attendance.py
Normal file
@@ -0,0 +1,382 @@
|
||||
# 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 as dt
|
||||
|
||||
from sql import Column, Window
|
||||
from sql.aggregate import Min, Sum
|
||||
from sql.conditionals import Coalesce
|
||||
from sql.functions import Function, NthValue
|
||||
|
||||
from trytond import backend
|
||||
from trytond.cache import Cache
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import Index, ModelSQL, ModelView, Workflow, fields
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval
|
||||
from trytond.tools import timezone as tz
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from .exceptions import PeriodClosedError, PeriodTransitionError
|
||||
|
||||
|
||||
class SQLiteStrftime(Function):
|
||||
__slots__ = ()
|
||||
_function = 'STRFTIME'
|
||||
|
||||
|
||||
class Line(ModelSQL, ModelView):
|
||||
__name__ = 'attendance.line'
|
||||
|
||||
company = fields.Many2One('company.company', "Company", required=True,
|
||||
help="The company which the employee attended.")
|
||||
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)),
|
||||
],
|
||||
])
|
||||
at = fields.DateTime("At", required=True)
|
||||
date = fields.Date("Date", required=True)
|
||||
type = fields.Selection([
|
||||
('in', 'In'),
|
||||
('out', 'Out'),
|
||||
], "Type", required=True)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._order.insert(0, ('at', 'DESC'))
|
||||
# Do not cache default_at
|
||||
cls.__rpc__['default_get'].cache = None
|
||||
|
||||
@classmethod
|
||||
def default_at(cls):
|
||||
return dt.datetime.now()
|
||||
|
||||
@classmethod
|
||||
def default_company(cls):
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@classmethod
|
||||
def default_employee(cls):
|
||||
return Transaction().context.get('employee')
|
||||
|
||||
@fields.depends('at', 'company')
|
||||
def on_change_with_date(self):
|
||||
if not self.at:
|
||||
return
|
||||
at = self.at
|
||||
if self.company and self.company.timezone:
|
||||
timezone = tz.ZoneInfo(self.company.timezone)
|
||||
at = self.at.replace(tzinfo=tz.UTC) .astimezone(timezone)
|
||||
return at.date()
|
||||
|
||||
def get_rec_name(self, name):
|
||||
pool = Pool()
|
||||
Lang = pool.get('ir.lang')
|
||||
lang = Lang.get()
|
||||
return '%s@%s' % (self.employee.rec_name, lang.strftime(self.at))
|
||||
|
||||
def compute_fields(self, field_names=None):
|
||||
cls = self.__class__
|
||||
values = super().compute_fields(field_names=field_names)
|
||||
if (field_names is None
|
||||
or (cls.date.on_change_with & field_names)):
|
||||
date = self.on_change_with_date()
|
||||
if getattr(self, 'date', None) != date:
|
||||
values['date'] = date
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def check_modification(cls, mode, lines, values=None, external=False):
|
||||
super().check_modification(
|
||||
mode, lines, values=values, external=external)
|
||||
if mode == 'delete':
|
||||
cls.check_closed_period(lines, msg='delete')
|
||||
|
||||
@classmethod
|
||||
def validate_fields(cls, records, field_names):
|
||||
super().validate_fields(records, field_names)
|
||||
cls.check_closed_period(records, field_names=field_names)
|
||||
|
||||
@classmethod
|
||||
def check_closed_period(cls, records, msg='modify', field_names=None):
|
||||
pool = Pool()
|
||||
Period = pool.get('attendance.period')
|
||||
if field_names and not (field_names & {'company', 'at'}):
|
||||
return
|
||||
for record in records:
|
||||
period_date = Period.get_last_period_date(record.company)
|
||||
if period_date and period_date > record.at:
|
||||
raise PeriodClosedError(
|
||||
gettext('attendance.msg_%s_period_close' % msg,
|
||||
attendance=record.rec_name,
|
||||
period=period_date))
|
||||
|
||||
@fields.depends('employee', 'at', 'id')
|
||||
def on_change_with_type(self):
|
||||
records = self.search([
|
||||
('employee', '=', self.employee),
|
||||
('at', '<', self.at),
|
||||
('id', '!=', self.id),
|
||||
],
|
||||
order=[('at', 'desc')],
|
||||
limit=1)
|
||||
if records:
|
||||
record, = records
|
||||
return {'in': 'out', 'out': 'in'}.get(record.type)
|
||||
else:
|
||||
return 'in'
|
||||
|
||||
|
||||
class Period(Workflow, ModelSQL, ModelView):
|
||||
__name__ = 'attendance.period'
|
||||
_states = {
|
||||
'readonly': Eval('state') == 'closed',
|
||||
}
|
||||
|
||||
_last_period_cache = Cache('attendance.period', context=False)
|
||||
|
||||
ends_at = fields.DateTime("Ends at", required=True, states=_states)
|
||||
company = fields.Many2One('company.company', "Company", required=True,
|
||||
states=_states, help="The company the period is associated with.")
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('closed', 'Closed'),
|
||||
], "State", readonly=True, sort=False,
|
||||
help="The current state of the attendance period.")
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_indexes.add(
|
||||
Index(
|
||||
t,
|
||||
(t.company, Index.Equality()),
|
||||
(t.state, Index.Equality(cardinality='low')),
|
||||
(t.ends_at, Index.Range(order='DESC'))))
|
||||
cls._transitions |= set((
|
||||
('draft', 'closed'),
|
||||
('closed', 'draft'),
|
||||
))
|
||||
cls._buttons.update({
|
||||
'draft': {
|
||||
'invisible': Eval('state') == 'draft',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'close': {
|
||||
'invisible': Eval('state') == 'closed',
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def default_company(cls):
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@classmethod
|
||||
def default_state(cls):
|
||||
return 'draft'
|
||||
|
||||
def get_rec_name(self, name):
|
||||
pool = Pool()
|
||||
Lang = pool.get('ir.lang')
|
||||
lang = Lang.get()
|
||||
return lang.strftime(self.ends_at)
|
||||
|
||||
@classmethod
|
||||
def get_last_period_date(cls, company):
|
||||
key = int(company)
|
||||
result = cls._last_period_cache.get(key, -1)
|
||||
if result == -1:
|
||||
records = cls.search([
|
||||
('company', '=', company),
|
||||
('state', '=', 'closed'),
|
||||
],
|
||||
order=[('ends_at', 'DESC')],
|
||||
limit=1)
|
||||
if records:
|
||||
record, = records
|
||||
result = record.ends_at
|
||||
else:
|
||||
result = None
|
||||
cls._last_period_cache.set(key, result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('draft')
|
||||
def draft(cls, periods):
|
||||
for period in periods:
|
||||
records = cls.search([
|
||||
('company', '=', period.company),
|
||||
('state', '=', 'closed'),
|
||||
('ends_at', '>', period.ends_at),
|
||||
],
|
||||
order=[('ends_at', 'ASC')],
|
||||
limit=1)
|
||||
if records:
|
||||
record, = records
|
||||
raise PeriodTransitionError(
|
||||
gettext('attendance.msg_draft_period_previous_closed',
|
||||
period=period.rec_name,
|
||||
other_period=record.rec_name))
|
||||
cls._last_period_cache.clear()
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('closed')
|
||||
def close(cls, periods):
|
||||
for period in periods:
|
||||
records = cls.search([
|
||||
('company', '=', period.company),
|
||||
('state', '=', 'draft'),
|
||||
('ends_at', '<', period.ends_at),
|
||||
],
|
||||
order=[('ends_at', 'ASC')],
|
||||
limit=1)
|
||||
if records:
|
||||
record, = records
|
||||
raise PeriodTransitionError(
|
||||
gettext('attendance.msg_close_period_previous_open',
|
||||
period=period.rec_name,
|
||||
other_period=record.rec_name))
|
||||
cls._last_period_cache.clear()
|
||||
|
||||
|
||||
class SheetLine(ModelSQL, ModelView):
|
||||
__name__ = 'attendance.sheet.line'
|
||||
|
||||
company = fields.Many2One('company.company', "Company")
|
||||
employee = fields.Many2One('company.employee', "Employee")
|
||||
from_ = fields.DateTime("From")
|
||||
to = fields.DateTime("To")
|
||||
duration = fields.TimeDelta("Duration")
|
||||
date = fields.Date("Date")
|
||||
sheet = fields.Many2One('attendance.sheet', "Sheet")
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._order.insert(0, ('from_', 'DESC'))
|
||||
|
||||
@classmethod
|
||||
def table_query(cls):
|
||||
pool = Pool()
|
||||
Attendance = pool.get('attendance.line')
|
||||
|
||||
attendance = Attendance.__table__()
|
||||
|
||||
window = Window(
|
||||
[attendance.employee],
|
||||
order_by=[attendance.at.asc],
|
||||
frame='ROWS', start=0, end=1)
|
||||
type = NthValue(attendance.type, 1, window=window)
|
||||
from_ = NthValue(attendance.at, 1, window=window)
|
||||
to = NthValue(attendance.at, 2, window=window)
|
||||
date = NthValue(attendance.date, 1, window=window)
|
||||
query = attendance.select(
|
||||
attendance.id.as_('id'),
|
||||
attendance.company.as_('company'),
|
||||
attendance.employee.as_('employee'),
|
||||
type.as_('type'),
|
||||
from_.as_('from_'),
|
||||
to.as_('to'),
|
||||
date.as_('date'))
|
||||
|
||||
sheet = (
|
||||
Min(query.id * 2, window=Window([query.employee, query.date])))
|
||||
|
||||
from_ = Column(query, 'from_')
|
||||
if backend.name == 'sqlite':
|
||||
# As SQLite does not support operation on datetime
|
||||
# we convert datetime into seconds
|
||||
duration = (
|
||||
SQLiteStrftime('%s', query.to) - SQLiteStrftime('%s', from_))
|
||||
else:
|
||||
duration = query.to - from_
|
||||
return query.select(
|
||||
query.id.as_('id'),
|
||||
query.company.as_('company'),
|
||||
query.employee.as_('employee'),
|
||||
from_.as_('from_'),
|
||||
query.to.as_('to'),
|
||||
query.date.as_('date'),
|
||||
duration.as_('duration'),
|
||||
sheet.as_('sheet'),
|
||||
where=query.type == 'in')
|
||||
|
||||
|
||||
class Sheet(ModelSQL, ModelView):
|
||||
__name__ = 'attendance.sheet'
|
||||
|
||||
company = fields.Many2One('company.company', "Company")
|
||||
employee = fields.Many2One('company.employee', "Employee")
|
||||
duration = fields.TimeDelta("Duration")
|
||||
date = fields.Date("Date")
|
||||
lines = fields.One2Many('attendance.sheet.line', 'sheet', "Lines")
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._order.insert(0, ('date', 'DESC'))
|
||||
|
||||
@classmethod
|
||||
def table_query(cls):
|
||||
pool = Pool()
|
||||
Line = pool.get('attendance.sheet.line')
|
||||
|
||||
line = Line.__table__()
|
||||
|
||||
return line.select(
|
||||
(Min(line.id * 2)).as_('id'),
|
||||
line.company.as_('company'),
|
||||
line.employee.as_('employee'),
|
||||
Sum(line.duration).as_('duration'),
|
||||
line.date.as_('date'),
|
||||
group_by=[line.company, line.employee, line.date])
|
||||
|
||||
|
||||
class Sheet_Timesheet(metaclass=PoolMeta):
|
||||
__name__ = 'attendance.sheet'
|
||||
|
||||
timesheet_duration = fields.TimeDelta("Timesheet Duration")
|
||||
|
||||
@classmethod
|
||||
def table_query(cls):
|
||||
pool = Pool()
|
||||
Timesheet = pool.get('timesheet.line')
|
||||
line = Timesheet.__table__()
|
||||
timesheet = line.select(
|
||||
Min(line.id * 2 + 1).as_('id'),
|
||||
line.company.as_('company'),
|
||||
line.employee.as_('employee'),
|
||||
Sum(line.duration).as_('duration'),
|
||||
line.date.as_('date'),
|
||||
group_by=[line.company, line.employee, line.date])
|
||||
attendance = super().table_query()
|
||||
return (attendance
|
||||
.join(
|
||||
timesheet, 'FULL' if backend.name != 'sqlite' else 'LEFT',
|
||||
condition=(attendance.company == timesheet.company)
|
||||
& (attendance.employee == timesheet.employee)
|
||||
& (attendance.date == timesheet.date))
|
||||
.select(
|
||||
Coalesce(attendance.id, timesheet.id).as_('id'),
|
||||
Coalesce(attendance.company, timesheet.company).as_('company'),
|
||||
Coalesce(
|
||||
attendance.employee, timesheet.employee).as_('employee'),
|
||||
attendance.duration.as_('duration'),
|
||||
timesheet.duration.as_('timesheet_duration'),
|
||||
Coalesce(attendance.date, timesheet.date).as_('date'),
|
||||
))
|
||||
Reference in New Issue
Block a user