diff --git a/CHANGELOG b/CHANGELOG
index ef139cd85e..0dc73cece3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,5 @@
+* Use TimeDelta field
+
Version 3.4.0 - 2014-10-20
* Bug fixes (see mercurial logs for details)
diff --git a/line.py b/line.py
index bb4cb69c17..f9a0e151c1 100644
--- a/line.py
+++ b/line.py
@@ -1,8 +1,10 @@
# 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 Max, Sum
-from sql.conditionals import Coalesce
from sql.functions import Extract
from trytond.model import ModelView, ModelSQL, fields
@@ -28,7 +30,7 @@ class Line(ModelSQL, ModelView):
],
depends=['company'])
date = fields.Date('Date', required=True, select=True)
- hours = fields.Float('Hours', digits=(16, 2), required=True)
+ duration = fields.TimeDelta('Duration', 'company_work_time', required=True)
work = fields.Many2One('timesheet.work', 'Work',
required=True, select=True, domain=[
('company', '=', Eval('company', -1)),
@@ -49,10 +51,10 @@ class Line(ModelSQL, ModelView):
def __setup__(cls):
super(Line, cls).__setup__()
cls._order.insert(0, ('date', 'DESC'))
- cls._sql_constraints += [
- ('check_move_hours_pos',
- 'CHECK(hours >= 0.0)', 'Hours field must be positive'),
- ]
+ cls._error_messages.update({
+ 'duration_positive': (
+ 'Duration of line "%(line)s" must be positive.'),
+ })
@classmethod
def __register__(cls, module_name):
@@ -68,12 +70,26 @@ class Line(ModelSQL, ModelView):
super(Line, cls).__register__(module_name)
+ table = TableHandler(cursor, cls, module_name)
+
# Migration from 3.4: new company field
if created_company:
# Don't use FROM because SQLite nor MySQL support it.
cursor.execute(*sql_table.update(
[sql_table.company], [work.select(work.company,
where=work.id == sql_table.work)]))
+ # Migration from 3.4: change hours into timedelta duration
+ if table.column_exist('hours'):
+ table.drop_constraint('check_move_hours_pos')
+ cursor.execute(*sql_table.select(
+ sql_table.id, sql_table.hours))
+ for id_, hours in cursor.fetchall():
+ duration = datetime.timedelta(hours=hours)
+ cursor.execute(*sql_table.update(
+ [sql_table.duration],
+ [duration],
+ where=sql_table.id == id_))
+ table.drop_column('hours')
@staticmethod
def default_company():
@@ -105,6 +121,22 @@ class Line(ModelSQL, ModelView):
employee = Employee(Transaction().context['employee'])
return value + " (" + employee.rec_name + ")"
+ @classmethod
+ def validate(cls, lines):
+ super(Line, cls).validate(lines)
+ for line in lines:
+ line.check_duration()
+
+ def check_duration(self):
+ if self.duration < datetime.timedelta():
+ self.raise_user_error('duration_positive', {
+ 'line': self.rec_name,
+ })
+
+ @property
+ def hours(self):
+ return self.duration.total_seconds() / 60 / 60
+
class EnterLinesStart(ModelView):
'Enter Lines'
@@ -156,7 +188,7 @@ class HoursEmployee(ModelSQL, ModelView):
'Hours per Employee'
__name__ = 'timesheet.hours_employee'
employee = fields.Many2One('company.employee', 'Employee', select=True)
- hours = fields.Float('Hours', digits=(16, 2))
+ duration = fields.TimeDelta('Duration', 'company_work_time')
@staticmethod
def table_query():
@@ -175,7 +207,7 @@ class HoursEmployee(ModelSQL, ModelView):
Max(line.write_uid).as_('write_uid'),
Max(line.write_date).as_('write_date'),
line.employee,
- Sum(Coalesce(line.hours, 0)).as_('hours'),
+ Sum(line.duration).as_('duration'),
where=where,
group_by=line.employee)
@@ -214,7 +246,7 @@ class HoursEmployeeWeekly(ModelSQL, ModelView):
year = fields.Char('Year', select=True)
week = fields.Integer('Week', select=True)
employee = fields.Many2One('company.employee', 'Employee', select=True)
- hours = fields.Float('Hours', digits=(16, 2), select=True)
+ duration = fields.TimeDelta('Duration', 'company_work_time')
@classmethod
def __setup__(cls):
@@ -242,7 +274,7 @@ class HoursEmployeeWeekly(ModelSQL, ModelView):
year_column,
week_column,
line.employee,
- Sum(Coalesce(line.hours, 0)).as_('hours'),
+ Sum(line.duration).as_('duration'),
group_by=(year_column, week_column, line.employee))
@@ -252,7 +284,7 @@ class HoursEmployeeMonthly(ModelSQL, ModelView):
year = fields.Char('Year', select=True)
month = fields.Integer('Month', select=True)
employee = fields.Many2One('company.employee', 'Employee', select=True)
- hours = fields.Float('Hours', digits=(16, 2), select=True)
+ duration = fields.TimeDelta('Duration', 'company_work_time')
@classmethod
def __setup__(cls):
@@ -280,5 +312,5 @@ class HoursEmployeeMonthly(ModelSQL, ModelView):
year_column,
month_column,
line.employee,
- Sum(Coalesce(line.hours, 0)).as_('hours'),
+ Sum(line.duration).as_('duration'),
group_by=(year_column, month_column, line.employee))
diff --git a/view/hours_employee_graph.xml b/view/hours_employee_graph.xml
index c9ca574c79..324a228e35 100644
--- a/view/hours_employee_graph.xml
+++ b/view/hours_employee_graph.xml
@@ -6,7 +6,6 @@ this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/view/hours_employee_monthly_tree.xml b/view/hours_employee_monthly_tree.xml
index b2350ed9db..a881e1acc3 100644
--- a/view/hours_employee_monthly_tree.xml
+++ b/view/hours_employee_monthly_tree.xml
@@ -5,5 +5,5 @@ this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/view/hours_employee_tree.xml b/view/hours_employee_tree.xml
index 6df2f8ccdc..30701ccb26 100644
--- a/view/hours_employee_tree.xml
+++ b/view/hours_employee_tree.xml
@@ -3,6 +3,5 @@
this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/view/hours_employee_weekly_tree.xml b/view/hours_employee_weekly_tree.xml
index d0335f2d01..c8f235669e 100644
--- a/view/hours_employee_weekly_tree.xml
+++ b/view/hours_employee_weekly_tree.xml
@@ -5,5 +5,5 @@ this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/view/line_form.xml b/view/line_form.xml
index 36aa660d39..3e0df19bb6 100644
--- a/view/line_form.xml
+++ b/view/line_form.xml
@@ -8,8 +8,8 @@ this repository contains the full copyright notices and license terms. -->
-
-
+
+
diff --git a/view/line_tree.xml b/view/line_tree.xml
index 4ef1e44d50..e9f6b61697 100644
--- a/view/line_tree.xml
+++ b/view/line_tree.xml
@@ -5,8 +5,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/view/work_graph.xml b/view/work_graph.xml
index c3531d0282..0e598ba90c 100644
--- a/view/work_graph.xml
+++ b/view/work_graph.xml
@@ -6,7 +6,6 @@ this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/view/work_tree2.xml b/view/work_tree2.xml
index 41ff7eaf1a..81e9d02e2f 100644
--- a/view/work_tree2.xml
+++ b/view/work_tree2.xml
@@ -3,5 +3,5 @@
this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/work.py b/work.py
index 5fdfc10207..a1109c9025 100644
--- a/work.py
+++ b/work.py
@@ -1,5 +1,9 @@
# 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
@@ -31,8 +35,9 @@ class Work(ModelSQL, ModelView):
('company', '=', Eval('company', -1)),
],
depends=['company'])
- hours = fields.Function(fields.Float('Timesheet Hours', digits=(16, 2),
- help="Total time spent on this work"), 'get_hours')
+ duration = fields.Function(fields.TimeDelta('Timesheet Duration',
+ 'company_work_time', help="Total time spent on this work"),
+ 'get_duration')
timesheet_available = fields.Boolean('Available on timesheets',
help="Allow to fill in timesheets with this work")
timesheet_start_date = fields.Date('Timesheet Start',
@@ -90,7 +95,7 @@ class Work(ModelSQL, ModelView):
cls.check_recursion(works, rec_name='name')
@classmethod
- def get_hours(cls, works, name):
+ def get_duration(cls, works, name):
pool = Pool()
Line = pool.get('timesheet.line')
transaction = Transaction()
@@ -101,7 +106,7 @@ class Work(ModelSQL, ModelView):
table_c = cls.__table__()
line = Line.__table__()
ids = [w.id for w in works]
- hours = dict.fromkeys(ids, 0)
+ durations = dict.fromkeys(ids, None)
where = Literal(True)
if context.get('from_date'):
where &= line.date >= context['from_date']
@@ -115,11 +120,15 @@ class Work(ModelSQL, ModelView):
condition=(table_c.left >= table_w.left)
& (table_c.right <= table_w.right)
).join(line, 'LEFT', condition=line.work == table_c.id
- ).select(table_w.id, Sum(line.hours),
+ ).select(table_w.id, Sum(line.duration),
where=red_sql & where,
group_by=table_w.id))
- hours.update(dict(cursor.fetchall()))
- return hours
+ 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_rec_name(self, name):
if self.parent:
@@ -185,6 +194,12 @@ class Work(ModelSQL, ModelView):
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 OpenWorkStart(ModelView):
'Open Work'