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 @@