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} - + + + + {'search_default_sheet_id': [active_id]} + Attendances + hr.attendance + hr_timesheet_sheet.sheet + + + + hr.timesheet.sheet.tree + hr_timesheet_sheet.sheet + + + + + + + + + + + + {'search_default_sheet_id': [active_id]} + Timesheet Activities + account.analytic.line + hr_timesheet_sheet.sheet + + + + + hr.timesheet.sheet.form + hr_timesheet_sheet.sheet + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + hr_attendance_view_filter + hr.attendance + + + + + + + + + + hr.attendance.form + hr.attendance + + + + + + + + + hr.attendance.tree + hr.attendance + + + + + + + + + + + + + + hr.timesheet.day.tree + hr_timesheet_sheet.sheet.day + + + + + + + + + + + + + + res.company.sheet + res.company + + +
+
+
+
+
+
+ +
diff --git a/addons/hr_timesheet_sheet/__init__.py b/addons/hr_timesheet_sheet/__init__.py index 7c5a17f1dfd..1331bcbef92 100644 --- a/addons/hr_timesheet_sheet/__init__.py +++ b/addons/hr_timesheet_sheet/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -# import models -# import wizard +import models +import wizard \ No newline at end of file diff --git a/addons/hr_timesheet_sheet/__openerp__.py b/addons/hr_timesheet_sheet/__openerp__.py index 5b88a0a5446..550bea2c0e6 100644 --- a/addons/hr_timesheet_sheet/__openerp__.py +++ b/addons/hr_timesheet_sheet/__openerp__.py @@ -4,15 +4,15 @@ { 'name': 'Timesheets', - 'version': '1.0', + 'version': '1.1', 'category': 'Human Resources', 'sequence': 80, - 'summary': 'Timesheets, Attendances, Activities', + 'summary': 'Timesheets, Activities', 'description': """ Record and validate timesheets and attendances easily ===================================================== -This application supplies a new screen enabling you to manage both attendances (Sign in/Sign out) and your work encoding (timesheet) by period. Timesheet entries are made by employees each day. At the end of the defined period, employees validate their sheet and the manager must then approve his team's entries. Periods are defined in the company forms and you can set them to run monthly or weekly. +This application supplies a new screen enabling you to manage your work encoding (timesheet) by period. Timesheet entries are made by employees each day. At the end of the defined period, employees validate their sheet and the manager must then approve his team's entries. Periods are defined in the company forms and you can set them to run monthly or weekly. The complete timesheet validation process is: --------------------------------------------- @@ -26,19 +26,16 @@ The validation can be configured in the company: * Maximal difference between timesheet and attendances """, 'website': 'https://www.odoo.com/page/employees', - 'depends': ['hr_timesheet', 'hr_attendance'], + 'depends': ['hr_timesheet'], 'data': [ - # 'security/ir.model.access.csv', - # 'security/hr_timesheet_sheet_security.xml', - # 'views/hr_timesheet_workflow.xml', - # 'data/hr_timesheet_sheet_data.xml', - # 'views/hr_timesheet_sheet_template.xml', - # 'views/hr_timesheet_sheet_view.xml', - # 'views/hr_department_views.xml', - ], - 'test': [ - # '../account/test/account_minimal_test.xml', - # 'test/test_hr_timesheet_sheet.yml' + 'security/ir.model.access.csv', + 'security/hr_timesheet_sheet_security.xml', + 'data/hr_timesheet_workflow.xml', + 'data/hr_timesheet_sheet_data.xml', + 'views/hr_timesheet_sheet_templates.xml', + 'views/hr_timesheet_sheet_views.xml', + 'views/hr_department_views.xml', + 'views/hr_timesheet_sheet_config_settings_views.xml', ], 'installable': True, 'auto_install': False, diff --git a/addons/hr_timesheet_sheet/data/hr_timesheet_sheet_data.xml b/addons/hr_timesheet_sheet/data/hr_timesheet_sheet_data.xml index c987acaf52d..1d85cf211ee 100644 --- a/addons/hr_timesheet_sheet/data/hr_timesheet_sheet_data.xml +++ b/addons/hr_timesheet_sheet/data/hr_timesheet_sheet_data.xml @@ -11,7 +11,7 @@ From the top menu "Human Resources", enter and validate timesheets and attendances.

]]>
- + code ir.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 @@
@@ -27,7 +27,7 @@
@@ -37,4 +37,4 @@ - \ No newline at end of file + diff --git a/addons/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml b/addons/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml index 6ea6393b89e..3c67c4113b3 100644 --- a/addons/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml +++ b/addons/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml @@ -36,25 +36,6 @@ - - - {'search_default_sheet_id': [active_id]} - Timesheet Activities - account.analytic.line - hr_timesheet_sheet.sheet - - - - - hr.timesheet.sheet.tree hr_timesheet_sheet.sheet @@ -64,9 +45,6 @@ - @@ -86,22 +64,6 @@ -