From 510d260434447b839a24b95822f7df85286f3041 Mon Sep 17 00:00:00 2001
From: "Denis Vermylen (dve)"
Date: Wed, 29 Jun 2016 14:53:47 +0200
Subject: [PATCH] [IMP] hr_timesheet_sheet, hr_timesheet_attendance: split
hr_attendance dependency
* split hr_timesheet_sheet into: - hr_timesheet_sheet (fully independent from hr_attendance)
- hr_timesheet_attendance (containing the dependencies to the revamped hr_attendance module)
* adapt timesheet_attendance to the revamped hr_attendance module, remove all commented dependencies
* few fixes to hr_timesheet_sheet, but some remain as it is deprecated.
---
addons/hr_timesheet_attendance/__init__.py | 4 +-
addons/hr_timesheet_attendance/__openerp__.py | 13 +-
.../models/__init__.py | 6 +
.../models/hr_attendance.py | 76 +++---
.../models/hr_timesheet_sheet.py | 177 +++++++++++++
.../models/res_company.py | 13 +
.../report/__init__.py | 4 +-
.../report/hr_timesheet_attendance_report.py | 80 +++---
.../hr_timesheet_attendance_report_view.xml | 6 +-
.../security/ir.model.access.csv | 3 +
.../hr_timesheet_attendance/tests/__init__.py | 3 +
.../tests/test_hr_timesheet_sheet.py | 79 ++++++
.../views/hr_timesheet_sheet_views.xml | 161 ++++++++++++
addons/hr_timesheet_sheet/__init__.py | 4 +-
addons/hr_timesheet_sheet/__openerp__.py | 27 +-
.../data/hr_timesheet_sheet_data.xml | 2 +-
addons/hr_timesheet_sheet/models/__init__.py | 2 +-
.../models/account_analytic_line.py | 19 +-
.../models/hr_department.py | 8 +-
.../models/hr_timesheet_sheet.py | 243 +++---------------
.../hr_timesheet_sheet_config_settings.py | 1 +
.../test/test_hr_timesheet_sheet.yml | 106 --------
.../views/hr_department_views.xml | 6 +-
.../views/hr_timesheet_sheet_views.xml | 150 +----------
24 files changed, 619 insertions(+), 574 deletions(-)
create mode 100644 addons/hr_timesheet_attendance/models/__init__.py
rename addons/{hr_timesheet_sheet => hr_timesheet_attendance}/models/hr_attendance.py (58%)
create mode 100644 addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py
create mode 100644 addons/hr_timesheet_attendance/models/res_company.py
create mode 100644 addons/hr_timesheet_attendance/security/ir.model.access.csv
create mode 100644 addons/hr_timesheet_attendance/tests/__init__.py
create mode 100644 addons/hr_timesheet_attendance/tests/test_hr_timesheet_sheet.py
create mode 100644 addons/hr_timesheet_attendance/views/hr_timesheet_sheet_views.xml
delete mode 100644 addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml
diff --git a/addons/hr_timesheet_attendance/__init__.py b/addons/hr_timesheet_attendance/__init__.py
index 006edd3fd74..c2abfbdfea8 100644
--- a/addons/hr_timesheet_attendance/__init__.py
+++ b/addons/hr_timesheet_attendance/__init__.py
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import report
+import models
+import report
\ No newline at end of file
diff --git a/addons/hr_timesheet_attendance/__openerp__.py b/addons/hr_timesheet_attendance/__openerp__.py
index d02ba711157..5524e569ba2 100644
--- a/addons/hr_timesheet_attendance/__openerp__.py
+++ b/addons/hr_timesheet_attendance/__openerp__.py
@@ -2,12 +2,15 @@
{
'name': "Timesheets/attendances reporting",
'description': """
+ Module linking the attendance module to the timesheet app.
""",
'category': 'Hidden',
- 'version': '0.1',
+ 'version': '1.0',
- # any module necessary for this one to work correctly
- 'depends': ['hr_timesheet', 'hr_attendance'],
- 'data': [],#'report/hr_timesheet_attendance_report_view.xml'],
- #'auto_install': True,
+ 'depends': ['hr_timesheet_sheet', 'hr_attendance'],
+ 'data': [
+ 'report/hr_timesheet_attendance_report_view.xml',
+ 'views/hr_timesheet_sheet_views.xml',
+ ],
+ 'auto_install': True,
}
diff --git a/addons/hr_timesheet_attendance/models/__init__.py b/addons/hr_timesheet_attendance/models/__init__.py
new file mode 100644
index 00000000000..e9110f22b7c
--- /dev/null
+++ b/addons/hr_timesheet_attendance/models/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import hr_attendance
+import hr_timesheet_sheet
+import res_company
diff --git a/addons/hr_timesheet_sheet/models/hr_attendance.py b/addons/hr_timesheet_attendance/models/hr_attendance.py
similarity index 58%
rename from addons/hr_timesheet_sheet/models/hr_attendance.py
rename to addons/hr_timesheet_attendance/models/hr_attendance.py
index a3658ca2762..ecc0ba4b01f 100644
--- a/addons/hr_timesheet_sheet/models/hr_attendance.py
+++ b/addons/hr_timesheet_attendance/models/hr_attendance.py
@@ -14,33 +14,38 @@ from odoo.exceptions import UserError
class HrAttendance(models.Model):
_inherit = "hr.attendance"
+ sheet_id_computed = fields.Many2one('hr_timesheet_sheet.sheet', string='Sheet', compute='_compute_sheet', index=True, ondelete='cascade',
+ search='_search_sheet')
sheet_id = fields.Many2one('hr_timesheet_sheet.sheet', compute='_compute_sheet', string='Sheet', store=True)
- # Same problem as in account.analytic.line for dependance, need to define a search function.
- # No need to migrate correctly, hr.attendance will be removed from this module anyways.
+ @api.depends('employee_id', 'check_in', 'check_out', 'sheet_id_computed.date_to', 'sheet_id_computed.date_from', 'sheet_id_computed.employee_id')
+ def _compute_sheet(self):
+ """Links the attendance to the corresponding sheet
+ """
+ for attendance in self:
+ corresponding_sheet = self.env['hr_timesheet_sheet.sheet'].search(
+ [('date_to', '>=', attendance.check_in), ('date_from', '<=', attendance.check_in),
+ ('employee_id', '=', attendance.employee_id.id),
+ ('state', 'in', ['draft', 'new'])], limit=1)
+ if corresponding_sheet:
+ attendance.sheet_id_computed = corresponding_sheet[0]
+ attendance.sheet_id = corresponding_sheet[0]
- # def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
- # attendance_ids = []
- # for ts in self.browse(cr, uid, ids, context=context):
- # cr.execute("""
- # SELECT a.id
- # FROM hr_attendance a
- # INNER JOIN hr_employee e
- # INNER JOIN resource_resource r
- # ON (e.resource_id = r.id)
- # ON (a.employee_id = e.id)
- # LEFT JOIN res_users u
- # ON r.user_id = u.id
- # LEFT JOIN res_partner p
- # ON u.partner_id = p.id
- # WHERE %(date_to)s >= date_trunc('day', a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))
- # AND %(date_from)s <= date_trunc('day', a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))
- # AND %(user_id)s = r.user_id
- # GROUP BY a.id""", {'date_from': ts.date_from,
- # 'date_to': ts.date_to,
- # 'user_id': ts.employee_id.user_id.id,})
- # attendance_ids.extend([row[0] for row in cr.fetchall()])
- # return attendance_ids
+ def _search_sheet(self, operator, value):
+ assert operator == 'in'
+ ids = []
+ for ts in self.env['hr_timesheet_sheet.sheet'].browse(value):
+ self._cr.execute("""
+ SELECT a.id
+ FROM hr_attendance a
+ WHERE %(date_to)s >= a.check_in
+ AND %(date_from)s <= a.check_in
+ AND %(employee_id)s = a.employee_id
+ GROUP BY a.id""", {'date_from': ts.date_from,
+ 'date_to': ts.date_to,
+ 'employee_id': ts.employee_id.id, })
+ ids.extend([row[0] for row in self._cr.fetchall()])
+ return [('id', 'in', ids)]
def _get_attendance_employee_tz(self, employee_id, date):
""" Simulate timesheet in employee timezone
@@ -49,11 +54,9 @@ class HrAttendance(models.Model):
tz converted from utc timezone as we consider date of employee
timesheet is in employee timezone
"""
- employee_obj = self.env['hr.employee']
-
tz = False
if employee_id:
- employee = employee_obj.browse(employee_id)
+ employee = self.env['hr.employee'].browse(employee_id)
tz = employee.user_id.partner_id.tz
if not date:
@@ -81,20 +84,17 @@ class HrAttendance(models.Model):
('employee_id', '=', employee_id)], limit=1)
return sheet or False
- @api.depends('employee_id', 'name', 'day', 'sheet_id.employee_id', 'sheet_id.date_from', 'sheet_id.date_to')
- def _compute_sheet(self, name, args):
- for attendance in self:
- attendance.sheet_id = self._get_current_sheet(attendance.employee_id.id, attendance.name)
-
@api.model
def create(self, vals):
- sheet_id = self.env.context.get('sheet_id') or self._get_current_sheet(vals.get('employee_id'), vals.get('name'))
- if sheet_id:
- att_tz_date_str = self._get_attendance_employee_tz(vals.get('employee_id'), date=vals.get('name'))
- ts = self.env['hr_timesheet_sheet.sheet'].browse(sheet_id)
- if ts.state not in ('draft', 'new'):
+ if self.env.context.get('sheet_id'):
+ sheet = self.env['hr_timesheet_sheet.sheet'].browse(self.env.context.get('sheet_id'))
+ else:
+ sheet = self._get_current_sheet(vals.get('employee_id'), vals.get('check_in'))
+ if sheet:
+ att_tz_date_str = self._get_attendance_employee_tz(vals.get('employee_id'), date=vals.get('check_in'))
+ if sheet.state not in ('draft', 'new'):
raise UserError(_('You can not enter an attendance in a submitted timesheet. Ask your manager to reset it before adding attendance.'))
- elif ts.date_from > att_tz_date_str or ts.date_to < att_tz_date_str:
+ elif sheet.date_from > att_tz_date_str or sheet.date_to < att_tz_date_str:
raise UserError(_('You can not enter an attendance date outside the current timesheet dates.'))
return super(HrAttendance, self).create(vals)
diff --git a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py
new file mode 100644
index 00000000000..7a9da24eb10
--- /dev/null
+++ b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.tools.translate import _
+from odoo.exceptions import UserError
+
+
+class HrTimesheetSheet(models.Model):
+ _inherit = "hr_timesheet_sheet.sheet"
+
+ attendances_ids = fields.One2many('hr.attendance', 'sheet_id', 'Attendances')
+ total_attendance = fields.Integer(string='Total Attendance', compute='_compute_total')
+ total_timesheet = fields.Float(string='Total Timesheet', compute="_compute_total")
+ total_difference = fields.Float(string='Difference', compute="_compute_total")
+ period_ids = fields.One2many('hr_timesheet_sheet.sheet.day', 'sheet_id', string='Period', readonly=True)
+ attendance_count = fields.Integer(compute='_compute_attendances', string="Attendances")
+
+ @api.depends('period_ids.total_attendance', 'period_ids.total_timesheet', 'period_ids.total_difference')
+ def _compute_total(self):
+ """ Compute the attendances, analytic lines timesheets and differences
+ between them for all the days of a timesheet and the current day
+ """
+ if len(self.ids) == 0:
+ return
+
+ self.env.cr.execute("""
+ SELECT sheet_id as id,
+ sum(total_attendance) as total_attendance,
+ sum(total_timesheet) as total_timesheet,
+ sum(total_difference) as total_difference
+ FROM hr_timesheet_sheet_sheet_day
+ WHERE sheet_id IN %s
+ GROUP BY sheet_id
+ """, (tuple(self.ids), ))
+
+ for x in self.env.cr.dictfetchall():
+ sheet = self.browse(x.pop('id'))
+ sheet.total_attendance = x.pop('total_attendance')
+ sheet.total_timesheet = x.pop('total_timesheet')
+ sheet.total_difference = x.pop('total_difference')
+
+ @api.depends('attendances_ids')
+ def _compute_attendances(self):
+ for sheet in self:
+ sheet.attendance_count = len(sheet.attendances_ids)
+
+ @api.multi
+ def unlink(self):
+ sheets = self.read(['total_attendance'])
+ for sheet in sheets:
+ if sheet['total_attendance'] > 0.00:
+ raise UserError(_('You cannot delete a timesheet that has attendance entries.'))
+ return super(HrTimesheetSheet, self).unlink()
+
+ @api.multi
+ def action_sheet_report(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'HR Timesheet/Attendance Report',
+ 'res_model': 'hr.timesheet.attendance.report',
+ 'domain': [('date', '>=', self.date_from), ('date', '<=', self.date_to)],
+ 'view_mode': 'pivot',
+ 'context': {'search_default_user_id': self.user_id.id, }
+ }
+
+ @api.multi
+ def button_confirm(self):
+ for sheet in self:
+ sheet.check_employee_attendance_state()
+ di = sheet.user_id.company_id.timesheet_max_difference
+ if (abs(sheet.total_difference) <= di) or not di:
+ return super(HrTimesheetSheet, self).button_confirm()
+ else:
+ raise UserError(_('Please verify that the total difference of the sheet is lower than %.2f.') % (di,))
+
+ @api.multi
+ def check_employee_attendance_state(self):
+ """ Checks the attendance records of the timesheet, make sure they are all closed
+ (by making sure they have a check_out time)
+ """
+ self.ensure_one()
+ if any(self.attendances_ids.filtered(lambda r: not r.check_out)):
+ raise UserError(_("The timesheet cannot be validated as it contains an attendance record with no Check Out)."))
+ return True
+
+
+class hr_timesheet_sheet_sheet_day(models.Model):
+ _name = "hr_timesheet_sheet.sheet.day"
+ _description = "Timesheets by Period"
+ _auto = False
+ _order = 'name'
+
+ name = fields.Date('Date', readonly=True)
+ sheet_id = fields.Many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True, index=True)
+ total_timesheet = fields.Float('Total Timesheet', readonly=True)
+ total_attendance = fields.Float('Attendance', readonly=True)
+ total_difference = fields.Float('Difference', readonly=True)
+
+ _depends = {
+ 'account.analytic.line': ['date', 'unit_amount'],
+ 'hr.attendance': ['check_in', 'check_out', 'sheet_id'],
+ 'hr_timesheet_sheet.sheet': ['attendances_ids', 'timesheet_ids'],
+ }
+
+ def init(self, cr):
+ cr.execute("""create or replace view %s as
+ SELECT
+ id,
+ name,
+ sheet_id,
+ total_timesheet,
+ total_attendance,
+ cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
+ FROM
+ ((
+ SELECT
+ MAX(id) as id,
+ name,
+ sheet_id,
+ timezone,
+ SUM(total_timesheet) as total_timesheet,
+ CASE WHEN SUM(orphan_attendances) != 0
+ THEN (SUM(total_attendance) +
+ CASE WHEN current_date <> name
+ THEN 1440
+ ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC' AT TIME ZONE coalesce(timezone, 'UTC')) * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC' AT TIME ZONE coalesce(timezone, 'UTC'))
+ END
+ )
+ ELSE SUM(total_attendance)
+ END /60 as total_attendance
+ FROM
+ ((
+ select
+ min(l.id) as id,
+ p.tz as timezone,
+ l.date::date as name,
+ s.id as sheet_id,
+ sum(l.unit_amount) as total_timesheet,
+ 0 as orphan_attendances,
+ 0.0 as total_attendance
+ from
+ account_analytic_line l
+ LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = l.sheet_id
+ JOIN hr_employee e ON s.employee_id = e.id
+ JOIN resource_resource r ON e.resource_id = r.id
+ LEFT JOIN res_users u ON r.user_id = u.id
+ LEFT JOIN res_partner p ON u.partner_id = p.id
+ group by l.date::date, s.id, timezone
+ ) union (
+ select
+ -min(a.id) as id,
+ p.tz as timezone,
+ (a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date as name,
+ s.id as sheet_id,
+ 0.0 as total_timesheet,
+ 0 as orphan_attendances,
+ SUM((EXTRACT(hour FROM (a.check_out AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))) * 60 + EXTRACT(minute FROM (a.check_out AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))))
+ - (EXTRACT(hour FROM (a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))) * 60 + EXTRACT(minute FROM (a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))))) as total_attendance
+ from
+ hr_attendance a
+ LEFT JOIN hr_timesheet_sheet_sheet s
+ ON s.id = a.sheet_id
+ JOIN hr_employee e
+ ON a.employee_id = e.id
+ JOIN resource_resource r
+ ON e.resource_id = r.id
+ LEFT JOIN res_users u
+ ON r.user_id = u.id
+ LEFT JOIN res_partner p
+ ON u.partner_id = p.id
+ WHERE check_out IS NOT NULL
+ group by (a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date, s.id, timezone
+ )) AS foo
+ GROUP BY name, sheet_id, timezone
+ )) AS bar""" % self._table)
diff --git a/addons/hr_timesheet_attendance/models/res_company.py b/addons/hr_timesheet_attendance/models/res_company.py
new file mode 100644
index 00000000000..972ce91f58b
--- /dev/null
+++ b/addons/hr_timesheet_attendance/models/res_company.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResCompany(models.Model):
+ _inherit = 'res.company'
+
+ timesheet_max_difference = fields.Float(string='Timesheet allowed difference(Hours)',
+ help="Allowed difference in hours between the sign in/out and the timesheet " \
+ "computation for one sheet. Set this to 0 if you do not want any control.",
+ default=0.0)
diff --git a/addons/hr_timesheet_attendance/report/__init__.py b/addons/hr_timesheet_attendance/report/__init__.py
index a32b4f5eea7..18d5feae891 100644
--- a/addons/hr_timesheet_attendance/report/__init__.py
+++ b/addons/hr_timesheet_attendance/report/__init__.py
@@ -1,2 +1,4 @@
# -*- coding: utf-8 -*-
-from . import hr_timesheet_attendance_report
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import hr_timesheet_attendance_report
diff --git a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py
index 8cd405f7ca5..5ab1b632ec1 100644
--- a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py
+++ b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py
@@ -1,47 +1,47 @@
# -*- coding: utf-8 -*-
-from openerp import api, fields, models, tools
+from openerp import fields, models
+
class TimesheetAttendance(models.Model):
_name = 'hr.timesheet.attendance.report'
- # _auto = False
+ _auto = False
- # user_id = fields.Many2one('res.users')
- # date = fields.Date()
- # total_timesheet = fields.Float()
- # total_attendance = fields.Float()
- # total_difference = fields.Float()
+ user_id = fields.Many2one('res.users')
+ date = fields.Date()
+ total_timesheet = fields.Float()
+ total_attendance = fields.Float()
+ total_difference = fields.Float()
- # def init(self, cr):
- # cr.execute("""CREATE OR REPLACE VIEW %s AS (
- # SELECT
- # max(id) AS id,
- # t.user_id,
- # t.date,
- # coalesce(sum(t.attendance), 0) AS total_attendance,
- # coalesce(sum(t.timesheet), 0) AS total_timesheet,
- # coalesce(sum(t.attendance), 0) - coalesce(sum(t.timesheet), 0) as total_difference
- # FROM (
- # SELECT
- # -hr_attendance.id AS id,
- # resource_resource.user_id AS user_id,
- # hr_attendance.worked_hours AS attendance,
- # NULL AS timesheet,
- # date_trunc('day', hr_attendance.check_in) AS date --TO DO FIX !
- # FROM hr_attendance
- # LEFT JOIN hr_employee ON hr_employee.id = hr_attendance.employee_id
- # LEFT JOIN resource_resource on resource_resource.id = hr_employee.resource_id
- # WHERE hr_attendance.action = 'sign_out'
- # UNION ALL
- # SELECT
- # ts.id AS id,
- # ts.user_id AS user_id,
- # NULL AS attendance,
- # ts.unit_amount AS timesheet,
- # date_trunc('day', ts.date) AS date
- # FROM account_analytic_line AS ts
- # ) AS t
- # GROUP BY t.user_id, t.date
- # ORDER BY t.date
- # )
- # """ % self._table)
+ def init(self, cr):
+ cr.execute("""CREATE OR REPLACE VIEW %s AS (
+ SELECT
+ max(id) AS id,
+ t.user_id,
+ t.date,
+ coalesce(sum(t.attendance), 0) AS total_attendance,
+ coalesce(sum(t.timesheet), 0) AS total_timesheet,
+ coalesce(sum(t.attendance), 0) - coalesce(sum(t.timesheet), 0) as total_difference
+ FROM (
+ SELECT
+ -hr_attendance.id AS id,
+ resource_resource.user_id AS user_id,
+ hr_attendance.worked_hours AS attendance,
+ NULL AS timesheet,
+ date_trunc('day', hr_attendance.check_in) AS date
+ FROM hr_attendance
+ LEFT JOIN hr_employee ON hr_employee.id = hr_attendance.employee_id
+ LEFT JOIN resource_resource on resource_resource.id = hr_employee.resource_id
+ UNION ALL
+ SELECT
+ ts.id AS id,
+ ts.user_id AS user_id,
+ NULL AS attendance,
+ ts.unit_amount AS timesheet,
+ date_trunc('day', ts.date) AS date
+ FROM account_analytic_line AS ts
+ ) AS t
+ GROUP BY t.user_id, t.date
+ ORDER BY t.date
+ )
+ """ % self._table)
diff --git a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml
index 34164187167..ba48dceb8f2 100644
--- a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml
+++ b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml
@@ -5,11 +5,12 @@
hr.timesheet.attendance.report
+ {'search_default_week': True}
-
]]>
-
+ codeir.actions.server
diff --git a/addons/hr_timesheet_sheet/models/__init__.py b/addons/hr_timesheet_sheet/models/__init__.py
index e3e37e107ff..06f8b1acfb2 100644
--- a/addons/hr_timesheet_sheet/models/__init__.py
+++ b/addons/hr_timesheet_sheet/models/__init__.py
@@ -2,8 +2,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import account_analytic_line
-import hr_attendance
import hr_department
import hr_employee
import hr_timesheet_sheet
+import hr_timesheet_sheet_config_settings
import res_company
diff --git a/addons/hr_timesheet_sheet/models/account_analytic_line.py b/addons/hr_timesheet_sheet/models/account_analytic_line.py
index 9493a1728b2..2cf7fdb7838 100644
--- a/addons/hr_timesheet_sheet/models/account_analytic_line.py
+++ b/addons/hr_timesheet_sheet/models/account_analytic_line.py
@@ -1,20 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp import api, fields, models, _
-from openerp.exceptions import UserError
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
class AccountAnalyticLine(models.Model):
_inherit = "account.analytic.line"
-
- sheet_id = fields.Many2one('hr_timesheet_sheet.sheet', string='Sheet', compute='_compute_sheet', index=True, ondelete='cascade',
- store=True, search='_search_sheet')
- # dependency TO TEST
- # shouldn't work ... the search won't be called since store=True, so it'll look up in the column
- # will probably need to create a 2nd non-stored related variable.
- @api.depends('date', 'user_id', 'project_id', 'sheet_id.date_to', 'sheet_id.date_from', 'sheet_id.employee_id')
+ sheet_id_computed = fields.Many2one('hr_timesheet_sheet.sheet', string='Sheet', compute='_compute_sheet', index=True, ondelete='cascade',
+ search='_search_sheet')
+ sheet_id = fields.Many2one('hr_timesheet_sheet.sheet', compute='_compute_sheet', string='Sheet', store=True)
+
+ @api.depends('date', 'user_id', 'project_id', 'sheet_id_computed.date_to', 'sheet_id_computed.date_from', 'sheet_id_computed.employee_id')
def _compute_sheet(self):
"""Links the timesheet line to the corresponding sheet
"""
@@ -23,10 +21,11 @@ class AccountAnalyticLine(models.Model):
continue
sheets = self.env['hr_timesheet_sheet.sheet'].search(
[('date_to', '>=', ts_line.date), ('date_from', '<=', ts_line.date),
- ('employee_id.user_id', '=', ts_line.user_id.id),
+ ('employee_id.user_id.id', '=', ts_line.user_id.id),
('state', 'in', ['draft', 'new'])])
if sheets:
# [0] because only one sheet possible for an employee between 2 dates
+ ts_line.sheet_id_computed = sheets[0]
ts_line.sheet_id = sheets[0]
def _search_sheet(self, operator, value):
diff --git a/addons/hr_timesheet_sheet/models/hr_department.py b/addons/hr_timesheet_sheet/models/hr_department.py
index 9942c1c116e..d91cce1fd11 100644
--- a/addons/hr_timesheet_sheet/models/hr_department.py
+++ b/addons/hr_timesheet_sheet/models/hr_department.py
@@ -1,10 +1,15 @@
# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from odoo import api, fields, models
class HrDepartment(models.Model):
_inherit = 'hr.department'
+ timesheet_to_approve_count = fields.Integer(
+ compute='_compute_timesheet_to_approve', string='Timesheet to Approve')
+
@api.multi
def _compute_timesheet_to_approve(self):
timesheet_data = self.env['hr_timesheet_sheet.sheet'].read_group(
@@ -12,6 +17,3 @@ class HrDepartment(models.Model):
result = dict((data['department_id'][0], data['department_id_count']) for data in timesheet_data)
for department in self:
department.timesheet_to_approve_count = result.get(department.id, 0)
-
- timesheet_to_approve_count = fields.Integer(
- compute='_compute_timesheet_to_approve', string='Timesheet to Approve')
diff --git a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py
index a90dc349c36..cc681abb2f7 100644
--- a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py
+++ b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py
@@ -17,88 +17,6 @@ class HrTimesheetSheet(models.Model):
_order = "id desc"
_description = "Timesheet"
- # @api.depends('period_ids.total_attendance', 'period_ids.total_timesheet', 'period_ids.total_difference')
- # def _total(self):
- # """ Compute the attendances, analytic lines timesheets and differences
- # between them for all the days of a timesheet and the current day
- # """
-
- # self.env.cr.execute("""
- # SELECT sheet_id as id,
- # sum(total_attendance) as total_attendance,
- # sum(total_timesheet) as total_timesheet,
- # sum(total_difference) as total_difference
- # FROM hr_timesheet_sheet_sheet_day
- # WHERE sheet_id IN %s
- # GROUP BY sheet_id
- # """, (tuple(self.ids),))
-
- # for x in self.env.cr.dictfetchall():
- # sheet = self.browse(x.pop('id'))
- # sheet.total_attendance = x.pop('total_attendance')
- # sheet.total_timesheet = x.pop('total_timesheet')
- # sheet.total_difference = x.pop('total_difference')
-
- # def check_employee_attendance_state(self):
- # """ Checks the attendance records of the timesheet, make sure they are all closed
- # (by making sure they have a check_out time)
- # """
- # if any(self.attendances_ids.filtered(lambda r: not r.check_out)):
- # raise UserError(_("The timesheet cannot be validated as it contains an attendance record with no Check Out)."))
- # return True
-
- def copy(self, *args, **argv):
- raise UserError(_('You cannot duplicate a timesheet.'))
-
- @api.model
- def create(self, vals):
- if 'employee_id' in vals:
- if not self.env['hr.employee'].browse(vals['employee_id']).user_id:
- raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
- # No more need to sort the attendances, except for user lookupability maybe ...
- return super(HrTimesheetSheet, self).create(vals)
-
- @api.multi
- def write(self, vals):
- if 'employee_id' in vals:
- new_user_id = self.env['hr.employee'].browse(vals['employee_id']).user_id.id
- if not new_user_id:
- raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
- if not self._sheet_date(forced_user_id=new_user_id):
- raise UserError(_('You cannot have 2 timesheets that overlap!\nYou should use the menu \'My Timesheet\' to avoid this problem.'))
- # ??? I don't see a product_id field defined to hr.employee anywhere. (and why would it be used for?)
- # if not self.env['hr.employee'].browse(vals['employee_id']).product_id:
- # raise UserError(_('In order to create a timesheet for this employee, you must link the employee to a product.'))
- # No more need to sort.
- return super(HrTimesheetSheet, self).write(vals)
-
- def button_confirm(self):
- for sheet in self:
- if sheet.employee_id and sheet.employee_id.parent_id and sheet.employee_id.parent_id.user_id:
- self.message_subscribe_users(user_ids=[sheet.employee_id.parent_id.user_id.id])
- # self.check_employee_attendance_state()
- # di = sheet.user_id.company_id.timesheet_max_difference
- # if (abs(sheet.total_difference) < di) or not di:
- sheet.signal_workflow('confirm')
- # else:
- # raise UserError(_('Please verify that the total difference of the sheet is lower than %.2f.') % (di,))
- return True
-
- # @api.multi # multi no need ? called from xml
- # def attendance_action_change(self):
- # employee_ids = []
- # for sheet in self:
- # if sheet.employee_id.id not in employee_ids:
- # employee_ids.append(sheet.employee_id.id)
- # return self.browse(employee_ids).attendance_action_change()
-
- # @api.depends('attendances_ids')
- # def _count_attendances(self):
- # res = {}
- # for sheet in self:
- # res[sheet.id] = len(sheet.attendances_ids)
- # return res
-
def _default_date_from(self):
user = self.env['res.users'].browse(self.env.uid)
r = user.company_id and user.company_id.timesheet_range or 'month'
@@ -125,10 +43,9 @@ class HrTimesheetSheet(models.Model):
emp_ids = self.env['hr.employee'].search([('user_id', '=', self.env.uid)])
return emp_ids and emp_ids[0] or False
- # name had select=1 (= index=True, ) why, no use, is there ???
name = fields.Char(string="Note", states={'confirm': [('readonly', True)], 'done': [('readonly', True)]})
employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee, required=True)
- user_id = fields.Many2one('res.users', related='employee_id.user_id', string='User', store=True, readonly=True) # required=False, not necessary, right ?
+ user_id = fields.Many2one('res.users', related='employee_id.user_id', string='User', store=True, readonly=True)
date_from = fields.Date(string='Date From', default=_default_date_from, required=True,
index=True, readonly=True, states={'new': [('readonly', False)]})
date_to = fields.Date(string='Date To', default=_default_date_to, required=True,
@@ -138,7 +55,6 @@ class HrTimesheetSheet(models.Model):
readonly=True, states={
'draft': [('readonly', False)],
'new': [('readonly', False)]})
- # attendances_ids = fields.One2many('hr.attendance', 'sheet_id', 'Attendances')
state = fields.Selection([
('new', 'New'),
('draft', 'Open'),
@@ -148,31 +64,60 @@ class HrTimesheetSheet(models.Model):
help=' * The \'Open\' status is used when a user is encoding a new and unconfirmed timesheet. \
\n* The \'Waiting Approval\' status is used to confirm the timesheet by user. \
\n* The \'Approved\' status is used when the users timesheet is accepted by his/her senior.')
- # state_attendance = fields.Selection(string='Current Status', related='employee_id.state',
- # selection=[('absent', 'Absent'), ('present', 'Present')], readonly=True)
- # total_attendance = fields.Integer(string='Total Attendance', compute='_total')
- # total_timesheet = fields.Float(string='Total Timesheet', compute="_total")
- # total_difference = fields.Float(string='Difference', compute="_total")
- # period_ids = fields.One2many('hr_timesheet_sheet.sheet.day', 'sheet_id', string='Period', readonly=True)
account_ids = fields.One2many('hr_timesheet_sheet.sheet.account', 'sheet_id', string='Analytic accounts', readonly=True)
company_id = fields.Many2one('res.company', string='Company')
department_id = fields.Many2one('hr.department', string='Department',
default=lambda self: self.env['res.company']._company_default_get())
- # attendance_count = fields.Integer(compute='_count_attendances', string="Attendances")
@api.constrains('date_to', 'date_from', 'employee_id')
- def _sheet_date(self):
+ def _check_sheet_date(self, forced_user_id=False):
for sheet in self:
- new_user_id = sheet.user_id and sheet.user_id.id
+ new_user_id = forced_user_id or sheet.user_id and sheet.user_id.id
if new_user_id:
self.env.cr.execute('SELECT id \
FROM hr_timesheet_sheet_sheet \
WHERE (date_from <= %s and %s <= date_to) \
AND user_id=%s \
AND id <> %s', (sheet.date_to, sheet.date_from, new_user_id, sheet.id))
- if not self.env.cr.fetchall():
+ if any(self.env.cr.fetchall()):
raise ValidationError('You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.')
+ @api.multi
+ @api.onchange('employee_id')
+ def onchange_employee_id(self, employee_id):
+ employee = self.env['hr.employee'].browse(employee_id)
+ for sheet in self:
+ if employee_id:
+ sheet.department_id = employee.department_id
+ sheet.user_id = employee.user_id
+
+ def copy(self, *args, **argv):
+ raise UserError(_('You cannot duplicate a timesheet.'))
+
+ @api.model
+ def create(self, vals):
+ if 'employee_id' in vals:
+ if not self.env['hr.employee'].browse(vals['employee_id']).user_id:
+ raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
+ return super(HrTimesheetSheet, self).create(vals)
+
+ @api.multi
+ def write(self, vals):
+ if 'employee_id' in vals:
+ new_user_id = self.env['hr.employee'].browse(vals['employee_id']).user_id.id
+ if not new_user_id:
+ raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
+ self._check_sheet_date(forced_user_id=new_user_id)
+ return super(HrTimesheetSheet, self).write(vals)
+
+ @api.multi
+ def button_confirm(self):
+ for sheet in self:
+ if sheet.employee_id and sheet.employee_id.parent_id and sheet.employee_id.parent_id.user_id:
+ self.message_subscribe_users(user_ids=[sheet.employee_id.parent_id.user_id.id])
+ sheet.signal_workflow('confirm')
+ return True
+
@api.multi
def action_set_to_draft(self):
self.write({'state': 'draft'})
@@ -182,18 +127,15 @@ class HrTimesheetSheet(models.Model):
@api.multi
def name_get(self):
# week number according to ISO 8601 Calendar
- return [(r['id'], _('Week ') + str(datetime.strptime(r['date_from'], '%Y-%m-%d').isocalendar()[1])) \
+ return [(r['id'], _('Week ') + str(datetime.strptime(r['date_from'], '%Y-%m-%d').isocalendar()[1]))
for r in self.read(['date_from'], load='_classic_write')]
@api.multi
def unlink(self):
- # sheets = self.read(cr, uid, ids, ['state','total_attendance'], context=context)
sheets = self.read(['state'])
for sheet in sheets:
if sheet['state'] in ('confirm', 'done'):
raise UserError(_('You cannot delete a timesheet which is already confirmed.'))
- # elif sheet['total_attendance'] <> 0.00:
- # raise UserError(_('You cannot delete a timesheet which have attendance entries.'))
analytic_timesheet_toremove = self.env['account.analytic.line']
for sheet in self:
@@ -202,17 +144,6 @@ class HrTimesheetSheet(models.Model):
return super(HrTimesheetSheet, self).unlink()
- @api.multi
- @api.onchange('employee_id')
- def onchange_employee_id(self, employee_id):
- print " ok %d" %employee_id + "\n"
- print self
- employee = self.env['hr.employee'].browse(employee_id)
- for sheet in self:
- if employee_id:
- sheet.department_id = employee.department_id
- sheet.user_id = employee.user_id
-
# ------------------------------------------------
# OpenChatter methods and notifications
# ------------------------------------------------
@@ -232,97 +163,7 @@ class HrTimesheetSheet(models.Model):
empids = self.env['hr.employee'].search([('parent_id.user_id', '=', self.env.uid)])
if not empids:
return False
- return ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids)]
-
-# hr_timesheet_sheet_sheet_day Won't be needed as dependancy towards hr_attendance will be removed
-
-# class hr_timesheet_sheet_sheet_day(osv.osv):
-# _name = "hr_timesheet_sheet.sheet.day"
-# _description = "Timesheets by Period"
-# _auto = False
-# _order='name'
-# _columns = {
-# 'name': fields.date('Date', readonly=True),
-# 'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True, select="1"),
-# 'total_timesheet': fields.float('Total Timesheet', readonly=True),
-# 'total_attendance': fields.float('Attendance', readonly=True),
-# 'total_difference': fields.float('Difference', readonly=True),
-# }
-# _depends = {
-# 'account.analytic.line': ['date', 'unit_amount'],
-# 'hr.attendance': ['action', 'name', 'sheet_id'],
-# }
-
-# def init(self, cr):
-# cr.execute("""create or replace view hr_timesheet_sheet_sheet_day as
-# SELECT
-# id,
-# name,
-# sheet_id,
-# total_timesheet,
-# total_attendance,
-# cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
-# FROM
-# ((
-# SELECT
-# MAX(id) as id,
-# name,
-# sheet_id,
-# timezone,
-# SUM(total_timesheet) as total_timesheet,
-# CASE WHEN SUM(orphan_attendances) != 0
-# THEN (SUM(total_attendance) +
-# CASE WHEN current_date <> name
-# THEN 1440
-# ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC' AT TIME ZONE coalesce(timezone, 'UTC')) * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC' AT TIME ZONE coalesce(timezone, 'UTC'))
-# END
-# )
-# ELSE SUM(total_attendance)
-# END /60 as total_attendance
-# FROM
-# ((
-# select
-# min(l.id) as id,
-# p.tz as timezone,
-# l.date::date as name,
-# s.id as sheet_id,
-# sum(l.unit_amount) as total_timesheet,
-# 0 as orphan_attendances,
-# 0.0 as total_attendance
-# from
-# account_analytic_line l
-# LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = l.sheet_id
-# JOIN hr_employee e ON s.employee_id = e.id
-# JOIN resource_resource r ON e.resource_id = r.id
-# LEFT JOIN res_users u ON r.user_id = u.id
-# LEFT JOIN res_partner p ON u.partner_id = p.id
-# group by l.date::date, s.id, timezone
-# ) union (
-# select
-# -min(a.id) as id,
-# p.tz as timezone,
-# (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date as name,
-# s.id as sheet_id,
-# 0.0 as total_timesheet,
-# SUM(CASE WHEN a.action = 'sign_in' THEN -1 ELSE 1 END) as orphan_attendances,
-# SUM(((EXTRACT(hour FROM (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))) * 60) + EXTRACT(minute FROM (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC')))) * (CASE WHEN a.action = 'sign_in' THEN -1 ELSE 1 END)) as total_attendance
-# from
-# hr_attendance a
-# LEFT JOIN hr_timesheet_sheet_sheet s
-# ON s.id = a.sheet_id
-# JOIN hr_employee e
-# ON a.employee_id = e.id
-# JOIN resource_resource r
-# ON e.resource_id = r.id
-# LEFT JOIN res_users u
-# ON r.user_id = u.id
-# LEFT JOIN res_partner p
-# ON u.partner_id = p.id
-# WHERE action in ('sign_in', 'sign_out')
-# group by (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date, s.id, timezone
-# )) AS foo
-# GROUP BY name, sheet_id, timezone
-# )) AS bar""")
+ return ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids.ids)]
class HrTimesheetSheetSheetAccount(models.Model):
diff --git a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet_config_settings.py b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet_config_settings.py
index 355b49a1180..f26fcfb2561 100644
--- a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet_config_settings.py
+++ b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet_config_settings.py
@@ -3,6 +3,7 @@
from odoo import fields, models
+
class HrTimesheetConfiguration(models.TransientModel):
_inherit = 'project.config.settings'
diff --git a/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml b/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml
deleted file mode 100644
index e53a8e44549..00000000000
--- a/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml
+++ /dev/null
@@ -1,106 +0,0 @@
--
- In order to test hr_timesheet_sheet module in OpenERP,
- I create Timesheet and check validation process done by manager.
--
- I set TimeSheet allowed difference for company
--
- !record {model: res.company, id: base.main_company}:
- timesheet_max_difference: 1.00
--
- I create a timesheet for employee "Gilles Gravie".
--
- !record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
- date_from: !eval time.strftime('%Y-%m-01')
- name: Gilles Gravie
- state: new
- user_id: base.user_demo
- employee_id: 'hr.employee_qdp'
--
- Gilles "Sign In" at around 9am.
--
- !record {model: hr.attendance, id: hr_attendance_1}:
- action: sign_in
- employee_id: 'hr.employee_qdp'
- name: !eval datetime.now().strftime('%Y-%m-%d 09:12:37')
--
- I test that Gilles in signed in
--
- !assert {model: hr.employee, id: hr.employee_qdp}:
- - state == 'present'
--
- At the time of logout, I create attendance and perform "Sign Out".
--
- !record {model: hr.attendance, id: hr_attendance_2}:
- action: sign_out
- employee_id: 'hr.employee_qdp'
- name: !eval datetime.now().strftime('%Y-%m-%d 17:30:00')
--
- I create Timesheet Entry for time spend on today work.
--
- !record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
- timesheet_ids:
- - project_id: project.project_project_2
- date: !eval time.strftime('%Y-%m-%d')
- name: 'Develop yaml for hr module'
- user_id: base.user_demo
- unit_amount: 6.00
- amount: -90.00
- product_id: product.product_product_1
- general_account_id: a_expense
--
- I confirm my Timesheet at end of period by click on "Confirm" button,
- if the difference between Timesheet hour and attendance hour is more than 1 hour it will give message.
--
- !python {model: hr_timesheet_sheet.sheet}: |
- uid = ref('base.user_root')
- from openerp import netsvc
- from openerp.osv import osv
- try:
- self.button_confirm(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], {"active_ids":
- [ref("hr_timesheet_sheet.menu_act_hr_timesheet_sheet_form")],"active_id": ref("hr_timesheet_sheet.menu_act_hr_timesheet_sheet_form"),
- })
- assert False, "The validation of the timesheet was unexpectedly accepted despite the 2:30 hours of difference"
- except osv.except_osv:
- pass
--
- I Modified the timesheet record and make the difference less than 1 hour.
--
- !record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
- timesheet_ids:
- - project_id: project.project_project_2
- date: !eval time.strftime('%Y-%m-%d')
- name: 'Develop yaml for hr module'
- unit_amount: 2.00
- user_id: base.user_demo
- amount: -90.00
- product_id: product.product_product_1
- general_account_id: a_expense
--
- I set an allowed difference of 1 hour
--
- !record {model: res.company, id: base.main_company}:
- timesheet_max_difference: 1.00
--
- I try again to confirm the timesheet after modification.
--
- !python {model: hr_timesheet_sheet.sheet}: |
- uid = ref('base.user_root')
- self.button_confirm(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], {"active_ids":
- [ref("hr_timesheet_sheet.menu_act_hr_timesheet_sheet_form")],"active_id": ref("hr_timesheet_sheet.menu_act_hr_timesheet_sheet_form"),
- })
--
- This time timesheet confirmed successfully.
- I check that state is "Confirmed".
--
- !assert {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
- - state == 'confirm'
--
- The manager will accept This request by click on "Accept" button.
--
- !python {model: hr_timesheet_sheet.sheet}: |
- self.write(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], {'state': 'done'})
--
- I check that state is "Done".
--
- !assert {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
- - state == 'done'
diff --git a/addons/hr_timesheet_sheet/views/hr_department_views.xml b/addons/hr_timesheet_sheet/views/hr_department_views.xml
index c91f0ccc197..138564d2d42 100644
--- a/addons/hr_timesheet_sheet/views/hr_department_views.xml
+++ b/addons/hr_timesheet_sheet/views/hr_department_views.xml
@@ -14,7 +14,7 @@