From bf67f1e20e0bc0cfc48f32114383f36fa0b09dfe Mon Sep 17 00:00:00 2001 From: "Jurgen (jugj)" Date: Fri, 14 Jul 2023 11:32:27 +0200 Subject: [PATCH] [IMP] hr_(holidays)_attendance : Revamp Attendance V17 Part-of: odoo/odoo#135439 --- addons/hr_attendance/__init__.py | 1 - addons/hr_attendance/__manifest__.py | 47 +++- addons/hr_attendance/controllers/main.py | 143 ++++++++++- .../hr_attendance/data/hr_attendance_demo.xml | 3 +- addons/hr_attendance/models/__init__.py | 1 - addons/hr_attendance/models/hr_attendance.py | 127 +++++++++- addons/hr_attendance/models/hr_employee.py | 195 +++++++++------ .../models/hr_employee_public.py | 26 +- addons/hr_attendance/models/ir_ui_menu.py | 14 -- addons/hr_attendance/models/res_company.py | 28 ++- .../models/res_config_settings.py | 11 +- addons/hr_attendance/models/res_users.py | 43 +++- addons/hr_attendance/report/__init__.py | 4 - .../report/hr_attendance_report.py | 73 ------ .../report/hr_attendance_report_views.xml | 71 ------ .../security/hr_attendance_security.xml | 131 +++++----- .../security/ir.model.access.csv | 12 +- .../static/img/background-light.svg | 35 +++ .../greeting_message/greeting_message.js | 224 ----------------- .../greeting_message/greeting_message.xml | 63 ----- .../kiosk_confirm/kiosk_confirm.js | 109 -------- .../kiosk_confirm/kiosk_confirm.xml | 63 ----- .../client_action/kiosk_mode/kiosk_mode.js | 93 ------- .../client_action/kiosk_mode/kiosk_mode.xml | 31 --- .../my_attendances/my_attendances.js | 40 --- .../my_attendances/my_attendances.xml | 26 -- .../attendance_menu/attendance_menu.js | 79 ++++++ .../attendance_menu/attendance_menu.scss | 32 +++ .../attendance_menu/attendance_menu.xml | 61 +++++ .../components/card_layout/card_layout.xml | 2 +- .../components/check_in_out/check_in_out.js | 17 +- .../src/components/greetings/greetings.js | 32 +++ .../src/components/greetings/greetings.xml | 63 +++++ .../components/kiosk_barcode/kiosk_barcode.js | 16 ++ .../manual_selection/manual_selection.js | 43 ++++ .../manual_selection/manual_selection.xml | 87 +++++++ .../src/components/pin_code/pin_code.js | 73 ++++++ .../src/components/pin_code/pin_code.xml | 49 ++++ .../{client_action => }/hr_attendance.scss | 24 ++ .../static/src/js/attendance_report_views.js | 54 ---- .../src/public_kiosk/public_kiosk_app.js | 146 +++++++++++ .../src/public_kiosk/public_kiosk_app.xml | 46 ++++ .../static/tests/client_action_tests.js | 229 ----------------- .../static/tests/hr_attendance_mock_server.js | 34 +++ .../tests/test_hr_attendance_overtime.py | 74 +++++- .../tests/test_hr_attendance_process.py | 53 +--- .../views/hr_attendance_kiosk_templates.xml | 32 +++ .../views/hr_attendance_view.xml | 236 ++++++++++++++---- .../views/hr_department_view.xml | 2 +- .../hr_attendance/views/hr_employee_view.xml | 72 ++++-- .../views/res_config_settings_views.xml | 31 ++- addons/hr_holidays_attendance/__manifest__.py | 1 - .../hr_holidays_attendance/models/hr_leave.py | 4 +- .../models/hr_leave_allocation.py | 2 +- .../hr_holidays_attendance_security.xml | 6 - .../tests/test_holidays_overtime.py | 6 +- .../views/hr_employee_views.xml | 1 + .../models/ir_ui_menu.py | 2 +- 58 files changed, 1793 insertions(+), 1430 deletions(-) delete mode 100644 addons/hr_attendance/models/ir_ui_menu.py delete mode 100644 addons/hr_attendance/report/__init__.py delete mode 100644 addons/hr_attendance/report/hr_attendance_report.py delete mode 100644 addons/hr_attendance/report/hr_attendance_report_views.xml create mode 100644 addons/hr_attendance/static/img/background-light.svg delete mode 100644 addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.js delete mode 100644 addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.xml delete mode 100644 addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.js delete mode 100644 addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.xml delete mode 100644 addons/hr_attendance/static/src/client_action/kiosk_mode/kiosk_mode.js delete mode 100644 addons/hr_attendance/static/src/client_action/kiosk_mode/kiosk_mode.xml delete mode 100644 addons/hr_attendance/static/src/client_action/my_attendances/my_attendances.js delete mode 100644 addons/hr_attendance/static/src/client_action/my_attendances/my_attendances.xml create mode 100644 addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.js create mode 100644 addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.scss create mode 100644 addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.xml create mode 100644 addons/hr_attendance/static/src/components/greetings/greetings.js create mode 100644 addons/hr_attendance/static/src/components/greetings/greetings.xml create mode 100644 addons/hr_attendance/static/src/components/kiosk_barcode/kiosk_barcode.js create mode 100644 addons/hr_attendance/static/src/components/manual_selection/manual_selection.js create mode 100644 addons/hr_attendance/static/src/components/manual_selection/manual_selection.xml create mode 100644 addons/hr_attendance/static/src/components/pin_code/pin_code.js create mode 100644 addons/hr_attendance/static/src/components/pin_code/pin_code.xml rename addons/hr_attendance/static/src/{client_action => }/hr_attendance.scss (56%) delete mode 100644 addons/hr_attendance/static/src/js/attendance_report_views.js create mode 100644 addons/hr_attendance/static/src/public_kiosk/public_kiosk_app.js create mode 100644 addons/hr_attendance/static/src/public_kiosk/public_kiosk_app.xml delete mode 100644 addons/hr_attendance/static/tests/client_action_tests.js create mode 100644 addons/hr_attendance/static/tests/hr_attendance_mock_server.js create mode 100644 addons/hr_attendance/views/hr_attendance_kiosk_templates.xml delete mode 100644 addons/hr_holidays_attendance/security/hr_holidays_attendance_security.xml diff --git a/addons/hr_attendance/__init__.py b/addons/hr_attendance/__init__.py index 5905766d867..7d34c7c054a 100644 --- a/addons/hr_attendance/__init__.py +++ b/addons/hr_attendance/__init__.py @@ -3,4 +3,3 @@ from . import controllers from . import models -from . import report diff --git a/addons/hr_attendance/__manifest__.py b/addons/hr_attendance/__manifest__.py index 18a0319e941..1bcf281f07a 100644 --- a/addons/hr_attendance/__manifest__.py +++ b/addons/hr_attendance/__manifest__.py @@ -22,10 +22,10 @@ actions(Check in/Check out) performed by them. 'security/ir.model.access.csv', 'views/hr_attendance_view.xml', 'views/hr_attendance_overtime_view.xml', - 'report/hr_attendance_report_views.xml', 'views/hr_department_view.xml', 'views/hr_employee_view.xml', 'views/res_config_settings_views.xml', + 'views/hr_attendance_kiosk_templates.xml' ], 'demo': [ 'data/hr_attendance_demo.xml' @@ -37,8 +37,51 @@ actions(Check in/Check out) performed by them. 'hr_attendance/static/src/**/*', ], 'web.qunit_suite_tests': [ - 'hr_attendance/static/tests/client_action_tests.js', + 'hr_attendance/static/tests/hr_attendance_mock_server.js', ], + 'web.qunit_mobile_suite_tests': [ + 'hr_attendance/static/tests/hr_attendance_mock_server.js', + ], + 'hr_attendance.assets_public_attendance': [ + # Front-end libraries + ('include', 'web._assets_helpers'), + ('include', 'web._assets_frontend_helpers'), + 'web/static/src/scss/pre_variables.scss', + 'web/static/lib/bootstrap/scss/_variables.scss', + ('include', 'web._assets_bootstrap_frontend'), + ('include', 'web._assets_bootstrap_backend'), + '/web/static/lib/odoo_ui_icons/*', + '/web/static/lib/bootstrap/scss/_functions.scss', + '/web/static/lib/bootstrap/scss/_mixins.scss', + '/web/static/lib/bootstrap/scss/utilities/_api.scss', + 'web/static/src/libs/fontawesome/css/font-awesome.css', + ('include', 'web._assets_core'), + + # Public Kiosk app and its components + "hr_attendance/static/src/public_kiosk/**/*", + "hr_attendance/static/src/hr_attendance.scss", + 'hr_attendance/static/src/components/**/*', + "web/static/src/views/fields/formatters.js", + + # Barcode reader utils + "web/static/src/webclient/barcode/barcode_scanner.js", + "web/static/src/webclient/barcode/barcode_scanner.xml", + "web/static/src/webclient/barcode/barcode_scanner.scss", + "web/static/src/webclient/barcode/crop_overlay.js", + "web/static/src/webclient/webclient_layout.scss", + "web/static/src/webclient/barcode/crop_overlay.xml", + "web/static/src/webclient/barcode/crop_overlay.scss", + "web/static/src/webclient/barcode/ZXingBarcodeDetector.js", + "barcodes/static/src/components/barcode_scanner.js", + "barcodes/static/src/components/barcode_scanner.xml", + "barcodes/static/src/components/barcode_scanner.scss", + "barcodes/static/src/barcode_service.js", + + # Kanban view mock + "web/static/src/views/kanban/kanban_controller.scss", + "web/static/src/search/search_panel/search_panel.scss", + "web/static/src/search/control_panel/control_panel.scss", + ] }, 'license': 'LGPL-3', } diff --git a/addons/hr_attendance/controllers/main.py b/addons/hr_attendance/controllers/main.py index aacd4049b37..54f825605b9 100644 --- a/addons/hr_attendance/controllers/main.py +++ b/addons/hr_attendance/controllers/main.py @@ -1,12 +1,151 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import http +from odoo import http, _ from odoo.http import request - +import datetime class HrAttendance(http.Controller): + @staticmethod + def _get_company(token): + company = request.env['res.company'].sudo().search([('attendance_kiosk_key', '=', token)]) + return company + + @staticmethod + def _get_employee_info_response(employee): + response = {} + if employee: + response = { + 'id': employee.id, + 'employee_name': employee.name, + 'employee_avatar': employee.image_1920, + 'hours_today': employee.hours_today, + 'total_overtime': employee.total_overtime, + 'last_attendance_worked_hours': employee.last_attendance_worked_hours, + 'last_check_in': employee.last_check_in, + 'attendance_state': employee.attendance_state, + 'hours_previously_today': employee.hours_previously_today, + 'kiosk_delay': employee.company_id.attendance_kiosk_delay * 1000, + 'attendance': {'check_in': employee.last_attendance_id.check_in, + 'check_out': employee.last_attendance_id.check_out}, + 'overtime_today': request.env['hr.attendance.overtime'].sudo().search([ + ('employee_id', '=', employee.id), ('date', '=', datetime.date.today()), + ('adjustment', '=', False)]).duration or 0, + 'use_pin': employee.company_id.attendance_kiosk_use_pin, + 'display_systray': employee.company_id.attendance_from_systray, + 'display_overtime': employee.company_id.hr_attendance_display_overtime + } + return response + + @staticmethod + def _get_geoip_response(mode, latitude=False, longitude=False): + return { + 'city': request.geoip.city.name or _('Unknown'), + 'country_name': request.geoip.country.name or request.geoip.continent.name or _('Unknown'), + 'latitude': latitude or request.geoip.location.latitude or False, + 'longitude': longitude or request.geoip.location.longitude or False, + 'ip_address': request.geoip.ip, + 'browser': request.httprequest.user_agent.browser, + 'mode': mode + } + + @http.route('/hr_attendance/kiosk_mode_menu', auth='user', type='http') + def kiosk_menu_item_action(self): + if request.env.user.user_has_groups("hr_attendance.group_hr_attendance_manager"): + # Auto log out will prevent users from forgetting to log out of their session + # before leaving the kiosk mode open to the public. This is a prevention security + # measure. + request.session.logout(keep_db=True) + return request.redirect(request.env.user.company_id.attendance_kiosk_url) + else: + return request.not_found() + @http.route('/hr_attendance/kiosk_keepalive', auth='user', type='json') def kiosk_keepalive(self): request.session.touch() return {} + + @http.route(["/hr_attendance/"], type='http', auth='public', website=True, sitemap=True) + def open_kiosk_mode(self, token): + company = self._get_company(token) + if not company: + return request.not_found() + else: + employee_list = [{"id": e["id"], + "name": e["name"], + "avatar": e["avatar_1024"].decode(), + "job": e["job_id"][1] if e["job_id"] else False, + "department": {"id": e["department_id"][0] if e["department_id"] else False, + "name": e["department_id"][1] if e["department_id"] else False + } + } for e in request.env['hr.employee'].sudo().search_read(domain=[('company_id', '=', company.id)], + fields=["id", + "name", + "avatar_1024", + "job_id", + "department_id"])] + departement_list = [{'id': dep["id"], + 'name': dep["name"], + 'count': dep["total_employee"] + } for dep in request.env['hr.department'].sudo().search_read(domain=[('company_id', '=', company.id)], + fields=["id", + "name", + "total_employee"])] + request.session.logout(keep_db=True) + return request.render( + 'hr_attendance.public_kiosk_mode', + { + 'kiosk_backend_info': { + 'token': token, + 'company_id': company.id, + 'company_name': company.name, + 'employees': employee_list, + 'departments': departement_list, + 'kiosk_mode': company.attendance_kiosk_mode, + 'barcode_source': company.attendance_barcode_source + } + } + ) + + @http.route('/hr_attendance/attendance_employee_data', type="json", auth="public") + def employee_attendance_data(self, token, employee_id): + company = self._get_company(token) + if company: + employee = request.env['hr.employee'].sudo().browse(employee_id) + if employee.company_id == company: + return self._get_employee_info_response(employee) + return {} + + @http.route('/hr_attendance/attendance_barcode_scanned', type="json", auth="public") + def scan_barcode(self, token, barcode): + company = self._get_company(token) + if company: + employee = request.env['hr.employee'].sudo().search([('barcode', '=', barcode), ('company_id', '=', company.id)], limit=1) + if employee: + employee._attendance_action_change(self._get_geoip_response('kiosk')) + return self._get_employee_info_response(employee) + return {} + + @http.route('/hr_attendance/manual_selection', type="json", auth="public") + def manual_selection(self, token, employee_id, pin_code): + company = self._get_company(token) + if company: + employee = request.env['hr.employee'].sudo().browse(employee_id) + if employee.company_id == company and ((not company.attendance_kiosk_use_pin) or (employee.pin == pin_code)): + employee.sudo()._attendance_action_change(self._get_geoip_response('kiosk')) + return self._get_employee_info_response(employee) + return {} + + @http.route('/hr_attendance/systray_check_in_out', type="json", auth="user") + def systray_attendance(self, latitude=False, longitude=False): + employee = request.env.user.employee_id + geo_ip_response = self._get_geoip_response(mode='systray', + latitude=latitude, + longitude=longitude) + employee._attendance_action_change(geo_ip_response) + return self._get_employee_info_response(employee) + + @http.route('/hr_attendance/attendance_user_data', type="json", auth="user") + def user_attendance_data(self): + employee = request.env.user.employee_id + return self._get_employee_info_response(employee) diff --git a/addons/hr_attendance/data/hr_attendance_demo.xml b/addons/hr_attendance/data/hr_attendance_demo.xml index 309aa5c90fa..24b382050b6 100644 --- a/addons/hr_attendance/data/hr_attendance_demo.xml +++ b/addons/hr_attendance/data/hr_attendance_demo.xml @@ -2,7 +2,7 @@ - + @@ -11,6 +11,7 @@ + 0000 456 diff --git a/addons/hr_attendance/models/__init__.py b/addons/hr_attendance/models/__init__.py index b2c32bd2d2e..d2cba8cea8e 100644 --- a/addons/hr_attendance/models/__init__.py +++ b/addons/hr_attendance/models/__init__.py @@ -6,6 +6,5 @@ from . import hr_attendance_overtime from . import hr_employee_base from . import hr_employee from . import hr_employee_public -from . import ir_ui_menu from . import res_company from . import res_users diff --git a/addons/hr_attendance/models/hr_attendance.py b/addons/hr_attendance/models/hr_attendance.py index 1fa22aca68e..bdd9f5ebf83 100644 --- a/addons/hr_attendance/models/hr_attendance.py +++ b/addons/hr_attendance/models/hr_attendance.py @@ -14,12 +14,17 @@ from odoo.tools import format_datetime from odoo.osv.expression import AND, OR from odoo.tools.float_utils import float_is_zero from odoo.exceptions import AccessError +from odoo.tools import format_duration + +def get_google_maps_url(latitude, longitude): + return "https://maps.google.com?q=%s,%s" % (latitude, longitude) class HrAttendance(models.Model): _name = "hr.attendance" _description = "Attendance" _order = "check_in desc" + _inherit = "mail.thread" def _default_employee(self): return self.env.user.employee_id @@ -27,25 +32,101 @@ class HrAttendance(models.Model): employee_id = fields.Many2one('hr.employee', string="Employee", default=_default_employee, required=True, ondelete='cascade', index=True) department_id = fields.Many2one('hr.department', string="Department", related="employee_id.department_id", readonly=True) - check_in = fields.Datetime(string="Check In", default=fields.Datetime.now, required=True) - check_out = fields.Datetime(string="Check Out") + check_in = fields.Datetime(string="Check In", default=fields.Datetime.now, required=True, tracking=True) + check_out = fields.Datetime(string="Check Out", tracking=True) worked_hours = fields.Float(string='Worked Hours', compute='_compute_worked_hours', store=True, readonly=True) + color = fields.Integer(compute='_compute_color') + overtime_hours = fields.Float(string="Over Time", compute='_compute_overtime_hours', store=True) + in_latitude = fields.Float(string="Latitude", digits=(10, 7), readonly=True) + in_longitude = fields.Float(string="Longitude", digits=(10, 7), readonly=True) + in_country_name = fields.Char(string="Country", help="Based on IP Address", readonly=True) + in_city = fields.Char(string="City", readonly=True) + in_ip_address = fields.Char(string="IP Address", readonly=True) + in_browser = fields.Char(string="Browser", readonly=True) + in_mode = fields.Selection(string="Mode", + selection=[('kiosk', "Kiosk"), + ('systray', "Systray"), + ('manual', "Manual")], + readonly=True, + default='manual') + out_latitude = fields.Float(digits=(10, 7), readonly=True) + out_longitude = fields.Float(digits=(10, 7), readonly=True) + out_country_name = fields.Char(help="Based on IP Address", readonly=True) + out_city = fields.Char(readonly=True) + out_ip_address = fields.Char(readonly=True) + out_browser = fields.Char(readonly=True) + out_mode = fields.Selection(selection=[('kiosk', "Kiosk"), + ('systray', "Systray"), + ('manual', "Manual")], + readonly=True, + default='manual') + + def _compute_color(self): + for attendance in self: + if attendance.check_out: + attendance.color = 1 if attendance.worked_hours > 16 else 0 + else: + attendance.color = 1 if attendance.check_in < (datetime.today() - timedelta(days=1)) else 10 + + @api.depends('worked_hours') + def _compute_overtime_hours(self): + self.env['hr.attendance'].flush_model(['worked_hours']) + self.env['hr.attendance.overtime'].flush_model(['duration']) + self.env.cr.execute(''' + SELECT att.id as att_id, + att.worked_hours as att_wh, + ot.id as ot_id, + ot.duration as ot_d, + ot.date as od, + att.check_in as ad + FROM hr_attendance att + INNER JOIN hr_attendance_overtime ot + ON date_trunc('day',att.check_in) = date_trunc('day', ot.date) + AND date_trunc('day',att.check_out) = date_trunc('day', ot.date) + AND att.employee_id IN %s + AND att.employee_id = ot.employee_id + ORDER BY att.check_in DESC + ''', (tuple(self.employee_id.ids),)) + a = self.env.cr.dictfetchall() + grouped_dict = dict() + for row in a: + if row['ot_id'] and row['att_wh']: + if row['ot_id'] not in grouped_dict: + grouped_dict[row['ot_id']] = {'attendances': [(row['att_id'], row['att_wh'])], 'overtime_duration': row['ot_d']} + else: + grouped_dict[row['ot_id']]['attendances'].append((row['att_id'], row['att_wh'])) + + att_progress_values = dict() + for ot in grouped_dict: + ot_bucket = grouped_dict[ot]['overtime_duration'] + for att in grouped_dict[ot]['attendances']: + if ot_bucket > 0: + sub_time = att[1] - ot_bucket + if sub_time < 0: + att_progress_values[att[0]] = 0 + ot_bucket -= att[1] + else: + att_progress_values[att[0]] = float(((att[1] - ot_bucket) / att[1])*100) + ot_bucket = 0 + else: + att_progress_values[att[0]] = 100 + for attendance in self: + attendance.overtime_hours = attendance.worked_hours * ((100 - att_progress_values.get(attendance.id, 100))/100) @api.depends('employee_id', 'check_in', 'check_out') def _compute_display_name(self): for attendance in self: if not attendance.check_out: attendance.display_name = _( - "%s from %s", - attendance.employee_id.name, - format_datetime(self.env, attendance.check_in, dt_format=False) + "From %s", + format_datetime(self.env, attendance.check_in, dt_format="HH:mm"), ) else: attendance.display_name = _( - "%s from %s to %s", - attendance.employee_id.name, - format_datetime(self.env, attendance.check_in, dt_format=False), - format_datetime(self.env, attendance.check_out, dt_format=False), + "%s : (%s-%s)", + format_duration(attendance.worked_hours), + format_datetime(self.env, attendance.check_in, dt_format="HH:mm"), + format_datetime(self.env, attendance.check_out, dt_format="HH:mm"), ) def _get_employee_calendar(self): @@ -156,7 +237,7 @@ class HrAttendance(models.Model): overtime_to_unlink = self.env['hr.attendance.overtime'] overtime_vals_list = [] - + affected_employees = self.env['hr.employee'] for emp, attendance_dates in employee_attendance_dates.items(): # get_attendances_dates returns the date translated from the local timezone without tzinfo, # and contains all the date which we need to check for overtime @@ -295,10 +376,16 @@ class HrAttendance(models.Model): 'duration': overtime_duration, 'duration_real': overtime_duration }) + affected_employees |= overtime.employee_id elif overtime: overtime_to_unlink |= overtime - self.env['hr.attendance.overtime'].sudo().create(overtime_vals_list) + created_overtimes = self.env['hr.attendance.overtime'].sudo().create(overtime_vals_list) + employees_worked_hours_to_compute = (affected_employees.ids + + created_overtimes.employee_id.ids + + overtime_to_unlink.employee_id.ids) overtime_to_unlink.sudo().unlink() + self.env.add_to_compute(self._fields['overtime_hours'], + self.search([('employee_id', 'in', employees_worked_hours_to_compute)])) @api.model_create_multi def create(self, vals_list): @@ -309,7 +396,7 @@ class HrAttendance(models.Model): def write(self, vals): if vals.get('employee_id') and \ vals['employee_id'] not in self.env.user.employee_ids.ids and \ - not self.env.user.has_group('hr_attendance.group_hr_attendance_user'): + not self.env.user.has_group('hr_attendance.group_hr_attendance_officer'): raise AccessError(_("Do not have access, user cannot edit the attendances that are not his own.")) attendances_dates = self._get_attendances_dates() result = super(HrAttendance, self).write(vals) @@ -330,3 +417,19 @@ class HrAttendance(models.Model): @api.returns('self', lambda value: value.id) def copy(self, default=None): raise exceptions.UserError(_('You cannot duplicate an attendance.')) + + def action_in_attendance_maps(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_url', + 'url': get_google_maps_url(self.in_latitude, self.in_longitude), + 'target': 'new' + } + + def action_out_attendance_maps(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_url', + 'url': get_google_maps_url(self.out_latitude, self.out_longitude), + 'target': 'new' + } diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py index f495b178f34..4ea2d709f06 100644 --- a/addons/hr_attendance/models/hr_employee.py +++ b/addons/hr_attendance/models/hr_employee.py @@ -11,33 +11,70 @@ from odoo.tools import float_round class HrEmployee(models.Model): _inherit = "hr.employee" + attendance_manager_id = fields.Many2one( + 'res.users', store=True, readonly=False, + domain="[('share', '=', False), ('company_ids', 'in', company_id)]", + groups="hr_attendance.group_hr_attendance_manager", + help="The user set in Attendance will access the attendance of the employee through the dedicated app and will be able to edit them.") attendance_ids = fields.One2many( - 'hr.attendance', 'employee_id', groups="hr_attendance.group_hr_attendance_user,hr.group_hr_user") + 'hr.attendance', 'employee_id', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") last_attendance_id = fields.Many2one( 'hr.attendance', compute='_compute_last_attendance_id', store=True, - groups="hr_attendance.group_hr_attendance_kiosk,hr_attendance.group_hr_attendance,hr.group_hr_user") + groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") last_check_in = fields.Datetime( related='last_attendance_id.check_in', store=True, - groups="hr_attendance.group_hr_attendance_user,hr.group_hr_user") + groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user", tracking=False) last_check_out = fields.Datetime( related='last_attendance_id.check_out', store=True, - groups="hr_attendance.group_hr_attendance_user,hr.group_hr_user") + groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user", tracking=False) attendance_state = fields.Selection( string="Attendance Status", compute='_compute_attendance_state', selection=[('checked_out', "Checked out"), ('checked_in', "Checked in")], - groups="hr_attendance.group_hr_attendance_kiosk,hr_attendance.group_hr_attendance,hr.group_hr_user") + groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") hours_last_month = fields.Float( - compute='_compute_hours_last_month', groups="hr_attendance.group_hr_attendance_user,hr.group_hr_user") + compute='_compute_hours_last_month', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") hours_today = fields.Float( compute='_compute_hours_today', - groups="hr_attendance.group_hr_attendance_kiosk,hr_attendance.group_hr_attendance,hr.group_hr_user") + groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") + hours_previously_today = fields.Float( + compute='_compute_hours_today', + groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") + last_attendance_worked_hours = fields.Float( + compute='_compute_hours_today', + groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") hours_last_month_display = fields.Char( - compute='_compute_hours_last_month', groups="hr_attendance.group_hr_attendance_user,hr.group_hr_user") + compute='_compute_hours_last_month') overtime_ids = fields.One2many( - 'hr.attendance.overtime', 'employee_id', groups="hr_attendance.group_hr_attendance_user,hr.group_hr_user") + 'hr.attendance.overtime', 'employee_id', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user") total_overtime = fields.Float( - compute='_compute_total_overtime', compute_sudo=True, - groups="hr_attendance.group_hr_attendance_kiosk,hr_attendance.group_hr_attendance,hr.group_hr_user") + compute='_compute_total_overtime', compute_sudo=True) + + @api.model_create_multi + def create(self, vals_list): + officer_group = self.env.ref('hr_attendance.group_hr_attendance_officer', raise_if_not_found=False) + group_updates = [] + for vals in vals_list: + if officer_group and vals.get('attendance_manager_id'): + group_updates.append((4, vals['attendance_manager_id'])) + if group_updates: + officer_group.sudo().write({'users': group_updates}) + return super().create(vals_list) + + def write(self, values): + old_officers = self.env['res.users'] + if 'attendance_manager_id' in values: + old_officers = self.attendance_manager_id + # Officer was added + if values['attendance_manager_id']: + officer = self.env['res.users'].browse(values['attendance_manager_id']) + officers_group = self.env.ref('hr_attendance.group_hr_attendance_officer', raise_if_not_found=False) + if officers_group and not officer.has_group('hr_attendance.group_hr_attendance_officer'): + officer.sudo().write({'groups_id': [(4, officers_group.id)]}) + + res = super(HrEmployee, self).write(values) + old_officers.sudo()._clean_attendance_officers() + + return res @api.depends('overtime_ids.duration', 'attendance_ids') def _compute_total_overtime(self): @@ -48,30 +85,26 @@ class HrEmployee(models.Model): employee.total_overtime = 0 def _compute_hours_last_month(self): + """ + Compute hours in the current month, if we are the 15th of october, will compute hours from 1 oct to 15 oct + """ now = fields.Datetime.now() now_utc = pytz.utc.localize(now) for employee in self: tz = pytz.timezone(employee.tz or 'UTC') now_tz = now_utc.astimezone(tz) - start_tz = now_tz + relativedelta(months=-1, day=1, hour=0, minute=0, second=0, microsecond=0) + start_tz = now_tz.replace(day=1, hour=0, minute=0, second=0, microsecond=0) start_naive = start_tz.astimezone(pytz.utc).replace(tzinfo=None) - end_tz = now_tz + relativedelta(day=1, hour=0, minute=0, second=0, microsecond=0) + end_tz = now_tz end_naive = end_tz.astimezone(pytz.utc).replace(tzinfo=None) - attendances = self.env['hr.attendance'].search([ ('employee_id', '=', employee.id), '&', - ('check_in', '<=', end_naive), - ('check_out', '>=', start_naive), + ('check_in', '>=', start_naive), + ('check_out', '<=', end_naive), ]) - hours = 0 - for attendance in attendances: - check_in = max(attendance.check_in, start_naive) - check_out = min(attendance.check_out, end_naive) - hours += (check_out - check_in).total_seconds() / 3600.0 - - employee.hours_last_month = round(hours, 2) + employee.hours_last_month = round(sum([att.worked_hours for att in attendances]), 2) employee.hours_last_month_display = "%g" % employee.hours_last_month def _compute_hours_today(self): @@ -88,12 +121,18 @@ class HrEmployee(models.Model): ('employee_id', '=', employee.id), ('check_in', '<=', now), '|', ('check_out', '>=', start_naive), ('check_out', '=', False), - ]) - + ], order='check_in asc') + hours_previously_today = 0 worked_hours = 0 + attendance_worked_hours = 0 for attendance in attendances: delta = (attendance.check_out or now) - max(attendance.check_in, start_naive) - worked_hours += delta.total_seconds() / 3600.0 + attendance_worked_hours = delta.total_seconds() / 3600.0 + worked_hours += attendance_worked_hours + hours_previously_today += attendance_worked_hours + employee.last_attendance_worked_hours = attendance_worked_hours + hours_previously_today -= attendance_worked_hours + employee.hours_previously_today = hours_previously_today employee.hours_today = worked_hours @api.depends('attendance_ids') @@ -101,7 +140,7 @@ class HrEmployee(models.Model): for employee in self: employee.last_attendance_id = self.env['hr.attendance'].search([ ('employee_id', '=', employee.id), - ], limit=1) + ], order="check_in desc", limit=1) @api.depends('last_attendance_id.check_in', 'last_attendance_id.check_out', 'last_attendance_id') def _compute_attendance_state(self): @@ -109,55 +148,7 @@ class HrEmployee(models.Model): att = employee.last_attendance_id.sudo() employee.attendance_state = att and not att.check_out and 'checked_in' or 'checked_out' - @api.model - def attendance_scan(self, barcode): - """ Receive a barcode scanned from the Kiosk Mode and change the attendances of corresponding employee. - Returns either an action or a warning. - """ - employee = self.sudo().search([('barcode', '=', barcode)], limit=1) - if employee: - return employee._attendance_action('hr_attendance.hr_attendance_action_kiosk_mode') - return {'warning': _("No employee corresponding to Badge ID '%(barcode)s.'") % {'barcode': barcode}} - - def attendance_manual(self, next_action, entered_pin=None): - self.ensure_one() - attendance_user_and_no_pin = self.user_has_groups( - 'hr_attendance.group_hr_attendance_user,' - '!hr_attendance.group_hr_attendance_use_pin') - can_check_without_pin = attendance_user_and_no_pin or (self.user_id == self.env.user and entered_pin is None) - if can_check_without_pin or entered_pin is not None and entered_pin == self.sudo().pin: - return self._attendance_action(next_action) - if not self.user_has_groups('hr_attendance.group_hr_attendance_user'): - return {'warning': _('To activate Kiosk mode without pin code, you must have access right as an Officer or above in the Attendance app. Please contact your administrator.')} - return {'warning': _('Wrong PIN')} - - def _attendance_action(self, next_action): - """ Changes the attendance of the employee. - Returns an action to the check in/out message, - next_action defines which menu the check in/out message should return to. ("My Attendances" or "Kiosk Mode") - """ - self.ensure_one() - employee = self.sudo() - action_message = self.env["ir.actions.actions"]._for_xml_id("hr_attendance.hr_attendance_action_greeting_message") - action_message['previous_attendance_change_date'] = employee.last_attendance_id and (employee.last_attendance_id.check_out or employee.last_attendance_id.check_in) or False - action_message['employee_name'] = employee.name - action_message['barcode'] = employee.barcode - action_message['next_action'] = next_action - action_message['hours_today'] = employee.hours_today - action_message['kiosk_delay'] = employee.company_id.attendance_kiosk_delay * 1000 - - if employee.user_id: - modified_attendance = employee.with_user(employee.user_id).sudo()._attendance_action_change() - else: - modified_attendance = employee._attendance_action_change() - action_message['attendance'] = modified_attendance.read()[0] - action_message['total_overtime'] = employee.total_overtime - # Overtime have an unique constraint on the day, no need for limit=1 - action_message['overtime_today'] = self.env['hr.attendance.overtime'].sudo().search([ - ('employee_id', '=', employee.id), ('date', '=', fields.Date.context_today(self)), ('adjustment', '=', False)]).duration or 0 - return {'action': action_message} - - def _attendance_action_change(self): + def _attendance_action_change(self, geo_information=None): """ Check In/Check Out action Check In: create a new attendance record Check Out: modify check_out field of appropriate attendance record @@ -166,15 +157,57 @@ class HrEmployee(models.Model): action_date = fields.Datetime.now() if self.attendance_state != 'checked_in': - vals = { - 'employee_id': self.id, - 'check_in': action_date, - } + if geo_information: + vals = { + 'employee_id': self.id, + 'check_in': action_date, + **{'in_%s' % key: geo_information[key] for key in geo_information} + } + else: + vals = { + 'employee_id': self.id, + 'check_in': action_date, + } return self.env['hr.attendance'].create(vals) attendance = self.env['hr.attendance'].search([('employee_id', '=', self.id), ('check_out', '=', False)], limit=1) if attendance: - attendance.check_out = action_date + if geo_information: + attendance.write({ + 'check_out': action_date, + **{'out_%s' % key: geo_information[key] for key in geo_information} + }) + else: + attendance.write({ + 'check_out': action_date + }) else: raise exceptions.UserError(_('Cannot perform check out on %(empl_name)s, could not find corresponding check in. ' 'Your attendances have probably been modified manually by human resources.') % {'empl_name': self.sudo().name, }) return attendance + + def action_open_last_month_attendances(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Attendances This Month"), + "res_model": "hr.attendance", + "views": [[self.env.ref('hr_attendance.hr_attendance_employee_simple_tree_view').id, "tree"]], + "context": { + "create": 0 + }, + "domain": [('employee_id', '=', self.id), + ('check_in', ">=", fields.datetime.today().replace(day=1, hour=0, minute=0))] + } + + def action_open_last_month_overtime(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Overtime"), + "res_model": "hr.attendance.overtime", + "views": [[False, "tree"]], + "context": { + "create": 0 + }, + "domain": [('employee_id', '=', self.id)] + } diff --git a/addons/hr_attendance/models/hr_employee_public.py b/addons/hr_attendance/models/hr_employee_public.py index 826122fef89..7b4d6f90ced 100644 --- a/addons/hr_attendance/models/hr_employee_public.py +++ b/addons/hr_attendance/models/hr_employee_public.py @@ -8,22 +8,16 @@ class HrEmployeePublic(models.Model): # These are required for manual attendance attendance_state = fields.Selection(related='employee_id.attendance_state', readonly=True, - groups="hr_attendance.group_hr_attendance_kiosk,hr_attendance.group_hr_attendance") + groups="hr_attendance.group_hr_attendance_officer") hours_today = fields.Float(related='employee_id.hours_today', readonly=True, - groups="hr_attendance.group_hr_attendance_kiosk,hr_attendance.group_hr_attendance") + groups="hr_attendance.group_hr_attendance_officer") last_attendance_id = fields.Many2one(related='employee_id.last_attendance_id', readonly=True, - groups="hr_attendance.group_hr_attendance_kiosk,hr_attendance.group_hr_attendance") + groups="hr_attendance.group_hr_attendance_officer") total_overtime = fields.Float(related='employee_id.total_overtime', readonly=True, - groups="hr_attendance.group_hr_attendance_kiosk,hr_attendance.group_hr_attendance") - - def action_employee_kiosk_confirm(self): - self.ensure_one() - return { - 'type': 'ir.actions.client', - 'name': 'Confirm', - 'tag': 'hr_attendance_kiosk_confirm', - 'employee_id': self.id, - 'employee_name': self.name, - 'employee_state': self.attendance_state, - 'employee_hours_today': self.hours_today, - } + groups="hr_attendance.group_hr_attendance_officer") + attendance_manager_id = fields.Many2one(related='employee_id.attendance_manager_id', + groups="hr_attendance.group_hr_attendance_officer") + last_check_in = fields.Datetime(related='employee_id.last_check_in', + groups="hr_attendance.group_hr_attendance_officer") + last_check_out = fields.Datetime(related='employee_id.last_check_out', + groups="hr_attendance.group_hr_attendance_officer") diff --git a/addons/hr_attendance/models/ir_ui_menu.py b/addons/hr_attendance/models/ir_ui_menu.py deleted file mode 100644 index d7deaf45203..00000000000 --- a/addons/hr_attendance/models/ir_ui_menu.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models - - -class IrUiMenu(models.Model): - _inherit = 'ir.ui.menu' - - def _load_menus_blacklist(self): - res = super()._load_menus_blacklist() - if self.env.user.has_group('hr_attendance.group_hr_attendance_user'): - res.append(self.env.ref('hr_attendance.menu_hr_attendance_attendances_overview').id) - return res diff --git a/addons/hr_attendance/models/res_company.py b/addons/hr_attendance/models/res_company.py index 9b3a39c4982..6e0290010c4 100644 --- a/addons/hr_attendance/models/res_company.py +++ b/addons/hr_attendance/models/res_company.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import fields, models, api from odoo.osv.expression import OR +import uuid +from werkzeug.urls import url_join class ResCompany(models.Model): @@ -12,7 +14,7 @@ class ResCompany(models.Model): overtime_start_date = fields.Date(string="Extra Hours Starting Date") overtime_company_threshold = fields.Integer(string="Tolerance Time In Favor Of Company", default=0) overtime_employee_threshold = fields.Integer(string="Tolerance Time In Favor Of Employee", default=0) - + hr_attendance_display_overtime = fields.Boolean(string="Display Extra Hours") attendance_kiosk_mode = fields.Selection([ ('barcode', 'Barcode / RFID'), ('barcode_manual', 'Barcode / RFID and Manual Selection'), @@ -24,6 +26,15 @@ class ResCompany(models.Model): ('back', 'Back Camera'), ], string='Barcode Source', default='front') attendance_kiosk_delay = fields.Integer(default=10) + attendance_kiosk_key = fields.Char(default=lambda s: uuid.uuid4().hex, copy=False, groups='hr_attendance.group_hr_attendance_manager') + attendance_kiosk_url = fields.Char(compute="_compute_attendance_kiosk_url") + attendance_kiosk_use_pin = fields.Boolean(string='Employee PIN Identification') + attendance_from_systray = fields.Boolean(string='Attendance From Systray', default=True) + + @api.depends("attendance_kiosk_key") + def _compute_attendance_kiosk_url(self): + for company in self: + company.attendance_kiosk_url = url_join(company.get_base_url(), '/hr_attendance/%s' % company.attendance_kiosk_key) def write(self, vals): search_domain = False # Overtime to generate @@ -72,3 +83,16 @@ class ResCompany(models.Model): self.env['hr.attendance'].search(search_domain)._update_overtime() return res + + def _regenerate_attendance_kiosk_key(self): + self.ensure_one() + self.write({ + 'attendance_kiosk_key': uuid.uuid4().hex + }) + + def _action_open_kiosk_mode(self): + return { + 'type': 'ir.actions.act_url', + 'target': 'self', + 'url': '/hr_attendance/kiosk_mode_menu' + } diff --git a/addons/hr_attendance/models/res_config_settings.py b/addons/hr_attendance/models/res_config_settings.py index b9d6e66bf57..ae820ead525 100644 --- a/addons/hr_attendance/models/res_config_settings.py +++ b/addons/hr_attendance/models/res_config_settings.py @@ -7,9 +7,6 @@ from odoo import api, fields, models class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' - group_attendance_use_pin = fields.Boolean( - string='Employee PIN', - implied_group="hr_attendance.group_hr_attendance_use_pin") hr_attendance_overtime = fields.Boolean( string="Count Extra Hours", readonly=False) overtime_start_date = fields.Date(string="Extra Hours Starting Date", readonly=False) @@ -17,9 +14,13 @@ class ResConfigSettings(models.TransientModel): string="Tolerance Time In Favor Of Company", readonly=False) overtime_employee_threshold = fields.Integer( string="Tolerance Time In Favor Of Employee", readonly=False) + hr_attendance_display_overtime = fields.Boolean(related='company_id.hr_attendance_display_overtime', readonly=False) attendance_kiosk_mode = fields.Selection(related='company_id.attendance_kiosk_mode', readonly=False) attendance_barcode_source = fields.Selection(related='company_id.attendance_barcode_source', readonly=False) attendance_kiosk_delay = fields.Integer(related='company_id.attendance_kiosk_delay', readonly=False) + attendance_kiosk_url = fields.Char(related='company_id.attendance_kiosk_url') + attendance_kiosk_use_pin = fields.Boolean(related='company_id.attendance_kiosk_use_pin', readonly=False) + attendance_from_systray = fields.Boolean(related="company_id.attendance_from_systray", readonly=False) @api.model def get_values(self): @@ -47,3 +48,7 @@ class ResConfigSettings(models.TransientModel): ] if any(self[field] != company[field] for field in fields_to_check): company.write({field: self[field] for field in fields_to_check}) + + def regenerate_kiosk_key(self): + if self.user_has_groups("hr_attendance.group_hr_attendance_manager"): + self.company_id._regenerate_attendance_kiosk_key() diff --git a/addons/hr_attendance/models/res_users.py b/addons/hr_attendance/models/res_users.py index 69a9e63054b..3de356ef5c1 100644 --- a/addons/hr_attendance/models/res_users.py +++ b/addons/hr_attendance/models/res_users.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import models, fields - +from odoo import models, fields, _ class User(models.Model): _inherit = ['res.users'] @@ -13,6 +12,8 @@ class User(models.Model): last_check_in = fields.Datetime(related='employee_id.last_attendance_id.check_in') last_check_out = fields.Datetime(related='employee_id.last_attendance_id.check_out') total_overtime = fields.Float(related='employee_id.total_overtime') + attendance_manager_id = fields.Many2one(related='employee_id.attendance_manager_id', readonly=False) + display_extra_hours = fields.Boolean(related='company_id.hr_attendance_display_overtime') @property def SELF_READABLE_FIELDS(self): @@ -22,5 +23,41 @@ class User(models.Model): 'attendance_state', 'last_check_in', 'last_check_out', - 'total_overtime' + 'total_overtime', + 'attendance_manager_id', + 'display_extra_hours', ] + + def _clean_attendance_officers(self): + attendance_officers = self.env['hr.employee'].search( + [('attendance_manager_id', 'in', self.ids)]).attendance_manager_id + officers_to_remove_ids = self - attendance_officers + if officers_to_remove_ids: + self.env.ref('hr_attendance.group_hr_attendance_officer').users = [(3, user.id) for user in + officers_to_remove_ids] + def action_open_last_month_attendances(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Attendances This Month"), + "res_model": "hr.attendance", + "views": [[self.env.ref('hr_attendance.hr_attendance_employee_simple_tree_view').id, "tree"]], + "context": { + "create": 0 + }, + "domain": [('employee_id', '=', self.employee_id.id), + ('check_in', ">=", fields.datetime.today().replace(day=1, hour=0, minute=0))] + } + + def action_open_last_month_overtime(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Overtime"), + "res_model": "hr.attendance.overtime", + "views": [[False, "tree"]], + "context": { + "create": 0 + }, + "domain": [('employee_id', '=', self.employee_id.id)] + } diff --git a/addons/hr_attendance/report/__init__.py b/addons/hr_attendance/report/__init__.py deleted file mode 100644 index 3588798d571..00000000000 --- a/addons/hr_attendance/report/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import hr_attendance_report diff --git a/addons/hr_attendance/report/hr_attendance_report.py b/addons/hr_attendance/report/hr_attendance_report.py deleted file mode 100644 index 82f36c328e9..00000000000 --- a/addons/hr_attendance/report/hr_attendance_report.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models, tools - - -class HRAttendanceReport(models.Model): - _name = "hr.attendance.report" - _description = "Attendance Statistics" - _auto = False - - department_id = fields.Many2one('hr.department', string="Department", readonly=True) - employee_id = fields.Many2one('hr.employee', string="Employee", readonly=True) - company_id = fields.Many2one('res.company', string="Company", readonly=True) - check_in = fields.Date("Check In", readonly=True) - worked_hours = fields.Float("Hours Worked", readonly=True) - overtime_hours = fields.Float("Extra Hours", readonly=True) - - @api.model - def _select(self): - return """ - SELECT - hra.id, - hr_employee.department_id, - hra.employee_id, - hr_employee.company_id, - hra.check_in, - hra.worked_hours, - coalesce(ot.duration, 0) as overtime_hours - """ - - @api.model - def _from(self): - return """ - FROM ( - SELECT - id, - row_number() over (partition by employee_id, CAST(check_in AS DATE)) as ot_check, - employee_id, - CAST(check_in - at time zone 'utc' - at time zone - (SELECT calendar.tz FROM resource_calendar as calendar - INNER JOIN hr_employee as employee ON employee.id = hr_attendance.employee_id - WHERE calendar.id = employee.resource_calendar_id) - as DATE) as check_in, - worked_hours - FROM - hr_attendance - ) as hra - """ - - def _join(self): - return """ - LEFT JOIN hr_employee ON hr_employee.id = hra.employee_id - LEFT JOIN hr_attendance_overtime ot - ON hra.ot_check = 1 - AND ot.employee_id = hra.employee_id - AND ot.date = hra.check_in - AND ot.adjustment = FALSE - """ - - def init(self): - tools.drop_view_if_exists(self.env.cr, self._table) - - self.env.cr.execute(""" - CREATE OR REPLACE VIEW %s AS ( - %s - %s - %s - ) - """ % (self._table, self._select(), self._from(), self._join()) - ) diff --git a/addons/hr_attendance/report/hr_attendance_report_views.xml b/addons/hr_attendance/report/hr_attendance_report_views.xml deleted file mode 100644 index a292d193452..00000000000 --- a/addons/hr_attendance/report/hr_attendance_report_views.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - hr.attendance.report.view.search - hr.attendance.report - - - - - - - - - - - - - - - hr.attendance.report.view.pivot - hr.attendance.report - - - - - - - - - - - - hr.attendance.report.view.graph - hr.attendance.report - - - - - - - - - - - - Attendance Analysis - hr.attendance.report - graph,pivot - - {'group_by': ['check_in:day', 'employee_id'], 'search_default_check_in': '1'} - - - - Attendance Analysis - hr.attendance.report - graph,pivot - - { - 'group_by': ['check_in:day', 'employee_id'], - 'search_default_department_id': [active_id]} - - - - - diff --git a/addons/hr_attendance/security/hr_attendance_security.xml b/addons/hr_attendance/security/hr_attendance_security.xml index 7cad995c86f..d286eac1a76 100644 --- a/addons/hr_attendance/security/hr_attendance_security.xml +++ b/addons/hr_attendance/security/hr_attendance_security.xml @@ -4,48 +4,36 @@ 14 - - User: Only kiosk mode - - The user will be able to open the kiosk mode and validate the employee PIN. - + + User: Read his own attendances + + The user will have access to his own attendances on his user / employee profile - - + + - - User - - The user will gain access to the human resources attendance menu, enabling him to manage his own attendance. - - - - Officer: Manage all attendances - - + + Officer: Manage attendances + + The user will have access to the attendance records and reporting of employees where he's set as an attendance manager Administrator - + - - - - - Enable PIN use - - The user will have to enter his PIN to check in and out manually at the company screen. + + Employee multi company rule @@ -53,33 +41,43 @@ ['|',('employee_id.company_id','=',False),('employee_id.company_id', 'in', company_ids)] - - attendance officer: full access + + Attendance Administrator: Full access [(1,'=',1)] - + - - Manual Attendance: own attendances + + Attendance Officer: Restrict Attendances to managed employees + + + [ + '|', + '&', + ('employee_id.attendance_manager_id', '=', user.id), + ('employee_id.user_id', '=', user.id), + '&', + ('employee_id.user_id', '!=', user.id), + ('employee_id.attendance_manager_id', '=', user.id) + ] + + + + + + + + + + Attendance base user: Read his own attendances in other apps [('employee_id.user_id', '=', user.id)] - - - - - - - - - user: read own attendance only - - [('employee_id.user_id','=',user.id)] - - + + - + @@ -90,29 +88,44 @@ ['|',('employee_id.company_id','=',False),('employee_id.company_id', 'in', company_ids)] - - attendance officer: full access + + Attendance Officer: Restrict Overtime to managed employees + + + [ + '|', + '&', + ('employee_id.attendance_manager_id', '=', user.id), + ('employee_id.user_id', '=', user.id), + '&', + ('employee_id.user_id', '!=', user.id), + ('employee_id.attendance_manager_id', '=', user.id) + ] + + + + + + + + + + Attendance Admin: full access [(1,'=',1)] - + + - - user: read and modify own attendance only + + Attendance base user: Read his own overtime [('employee_id.user_id', '=', user.id)] - - + + - - - - - - Attendance report multi company rule - - [('company_id', 'in', company_ids)] + diff --git a/addons/hr_attendance/security/ir.model.access.csv b/addons/hr_attendance/security/ir.model.access.csv index 55e17240e12..a54e89b2353 100644 --- a/addons/hr_attendance/security/ir.model.access.csv +++ b/addons/hr_attendance/security/ir.model.access.csv @@ -1,7 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_hr_attendance_officer,hr.attendance.user,model_hr_attendance,group_hr_attendance_user,1,1,1,1 -access_hr_attendance_user,hr.attendance.system.user,model_hr_attendance,group_hr_attendance,1,1,1,0 -access_hr_attendance_system_user,hr.attendance.system.user,model_hr_attendance,base.group_user,1,0,0,0 -access_hr_attendance_overtime_user,hr.attendance.overtime.user,model_hr_attendance_overtime,hr_attendance.group_hr_attendance_user,1,1,1,1 -access_hr_attendance_overtime_system_user,hr.attendance.overtime.system.user,model_hr_attendance_overtime,base.group_user,1,0,0,0 -access_hr_attendance_report_user,hr.attendance.report.user,model_hr_attendance_report,hr_attendance.group_hr_attendance_user,1,0,0,0 +access_hr_attendance_admin,hr.attendance.admin,model_hr_attendance,group_hr_attendance_manager,1,1,1,1 +access_hr_attendance_admin_overtime,hr.attendance.admin.overtime,model_hr_attendance_overtime,group_hr_attendance_manager,1,1,1,1 +access_hr_attendance_officer,hr.attendance.officer,model_hr_attendance,group_hr_attendance_officer,1,1,1,1 +access_hr_attendance_officer_overtime,hr.attendance.officer.overtime,model_hr_attendance_overtime,group_hr_attendance_officer,1,1,1,1 +access_hr_attendance_user,hr.attendance.user,model_hr_attendance,group_hr_attendance_own_reader,1,0,0,0 +access_hr_attendance_overtime_user,hr.attendance.overtime.user,model_hr_attendance_overtime,group_hr_attendance_own_reader,1,0,0,0 diff --git a/addons/hr_attendance/static/img/background-light.svg b/addons/hr_attendance/static/img/background-light.svg new file mode 100644 index 00000000000..aa437a117fa --- /dev/null +++ b/addons/hr_attendance/static/img/background-light.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.js b/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.js deleted file mode 100644 index 30f321719c5..00000000000 --- a/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.js +++ /dev/null @@ -1,224 +0,0 @@ -/** @odoo-module **/ - -import { _t } from "@web/core/l10n/translation"; -import { CardLayout } from "@hr_attendance/components/card_layout/card_layout"; -import { Component, onWillStart, onWillUnmount, useState } from "@odoo/owl"; -import { deserializeDateTime } from "@web/core/l10n/dates"; -import { registry } from "@web/core/registry"; -import { useBus, useService } from "@web/core/utils/hooks"; - -const { Duration } = luxon; - -export class GreetingMessage extends Component { - setup() { - const { action } = this.props; - - this.actionService = useService("action"); - this.notificationService = useService("notification"); - this.orm = useService("orm"); - this.user = useService("user"); - - this.kioskDelay = action.kiosk_delay; - this.state = useState({ - message: "", - randomMessage: "", - showWarningMessage: false, - }); - - onWillStart(this.onWillStart); - onWillUnmount(() => this.return_to_main_menu && clearTimeout(this.return_to_main_menu)); - - let activeBarcode = true; - if (!this.props.action.attendance) { - activeBarcode = false; - return; - } - - this.next_action = - action.next_action || "hr_attendance.hr_attendance_action_my_attendances"; - // no listening to barcode scans if we aren't coming from the kiosk mode (and thus not going back to it with next_action) - if ( - this.next_action !== "hr_attendance.hr_attendance_action_kiosk_mode" && - this.next_action.tag !== "hr_attendance_kiosk_mode" - ) { - activeBarcode = false; - } - - this.attendance = action.attendance; - // We receive the check in/out times in UTC - // This widget only deals with display, which should be in browser's TimeZone - this.attendance.check_in = - this.attendance.check_in && deserializeDateTime(this.attendance.check_in); - this.attendance.check_out = - this.attendance.check_out && deserializeDateTime(this.attendance.check_out); - this.previous_attendance_change_date = - action.previous_attendance_change_date && - deserializeDateTime(action.previous_attendance_change_date); - - const formatDateTime = registry.category("formatters").get("datetime"); - this.attendance.check_in_time = - this.attendance.check_in && formatDateTime(this.attendance.check_in); - this.attendance.check_out_time = - this.attendance.check_out && formatDateTime(this.attendance.check_out); - - const formatFloatTime = registry.category("formatters").get("float_time"); - // extra hours amount displayed in the greeting message template. - this.total_overtime_float = action.total_overtime; // Used for comparison in template - this.total_overtime = formatFloatTime(this.total_overtime_float); - this.today_overtime_float = action.overtime_today; - this.today_overtime = formatFloatTime(this.today_overtime_float); - - if (action.hours_today) { - const duration = Duration.fromObject({ hours: action.hours_today }) - .toFormat("hh-mm") - .split("-"); - - this.hours_today = _t("%(hours)s hours, %(minutes)s minutes", { - hours: duration[0], - minutes: duration[1], - }); - } - - this.employee_name = action.employee_name; - this.attendanceBarcode = action.barcode; - - const barcode = useService("barcode"); - this.lockScanner = false; - if (activeBarcode) { - useBus(barcode.bus, "barcode_scanned", this.onBarcodeScanned.bind(this)); - } - } - - async onBarcodeScanned(ev) { - if (this.lockScanner) { - return; - } - const { barcode } = ev.detail; - if (this.attendanceBarcode !== barcode) { - this.lockScanner = true; - if (this.return_to_main_menu) { - // in case of multiple scans in the greeting message view, delete the timer, a new one will be created. - clearTimeout(this.return_to_main_menu); - } - - const result = await this.orm.call("hr.employee", "attendance_scan", [barcode]); - if (result.action) { - this.actionService.doAction(result.action); - } else if (result.warning) { - this.notificationService.add(result.warning, { type: "danger" }); - this.setTimeoutNextAction(); - this.lockScanner = false; - } - } - } - - onClickDismiss() { - this.actionService.doAction(this.next_action, { clearBreadcrumbs: true }); - } - - async onWillStart() { - // if no correct action given (due to an erroneous back or refresh from the browser), we set the dismiss button to return - // to the (likely) appropriate menu, according to the user access rights - if (!this.attendance) { - const hasGroup = await this.user.hasGroup("hr_attendance.group_hr_attendance_user"); - if (hasGroup) { - this.next_action = "hr_attendance.hr_attendance_action_kiosk_mode"; - } else { - this.next_action = "hr_attendance.hr_attendance_action_my_attendances"; - } - return; - } - - this.setTimeoutNextAction(); - this.attendance.check_out ? this.setFarewellMessage() : this.setWelcomeMessage(); - } - - setWelcomeMessage() { - const now = this.attendance.check_in; - if (now.hour < 5) { - this.state.message = _t("Good night"); - } else if (now.hour < 12) { - if (now.hour < 8 && Math.random() < 0.3) { - if (Math.random() < 0.75) { - this.state.message = _t("The early bird catches the worm"); - } else { - this.state.message = _t("First come, first served"); - } - } else { - this.state.message = _t("Good morning"); - } - } else if (now.hour < 17) { - this.state.message = _t("Good afternoon"); - } else if (now.hour < 23) { - this.state.message = _t("Good evening"); - } else { - this.state.message = _t("Good night"); - } - if (this.previous_attendance_change_date) { - const last_check_out_date = this.previous_attendance_change_date; - if (now - last_check_out_date > 24 * 7 * 60 * 60 * 1000) { - this.state.randomMessage = _t("Glad to have you back, it's been a while!"); - } else { - if (Math.random() < 0.02) { - this.state.randomMessage = _t( - "If a job is worth doing, it is worth doing well!" - ); - } - } - } - } - - setFarewellMessage() { - const now = this.attendance.check_out; - - if (this.previous_attendance_change_date) { - const last_check_in_date = this.previous_attendance_change_date; - if (now - last_check_in_date > 1000 * 60 * 60 * 12) { - this.state.showWarningMessage = true; - if (this.return_to_main_menu) { - clearTimeout(this.return_to_main_menu); - } - this.lockScanner = true; - } else if (now - last_check_in_date > 1000 * 60 * 60 * 8) { - this.state.randomMessage = _t("Another good day's work! See you soon!"); - } - } - - if (now.hour < 12) { - this.state.message = _t("Have a good day!"); - } else if (now.hour < 14) { - this.state.message = _t("Have a nice lunch!"); - if (Math.random() < 0.05) { - this.state.randomMessage = _t( - "Eat breakfast as a king, lunch as a merchant and supper as a beggar" - ); - } else if (Math.random() < 0.06) { - this.state.randomMessage = _t("An apple a day keeps the doctor away"); - } - } else if (now.hour < 17) { - this.state.message = _t("Have a good afternoon"); - } else { - if (now.hour < 18 && Math.random() < 0.2) { - this.state.message = _t( - "Early to bed and early to rise, makes a man healthy, wealthy and wise" - ); - } else { - this.state.message = _t("Have a good evening"); - } - } - } - - setTimeoutNextAction() { - if (this.kioskDelay > 0) { - this.return_to_main_menu = setTimeout(() => { - this.actionService.doAction(this.next_action, { clearBreadcrumbs: true }); - }, this.kioskDelay); - } - } - -} - -GreetingMessage.template = "hr_attendance.GreetingMessage"; -GreetingMessage.components = { CardLayout }; - -registry.category("actions").add("hr_attendance_greeting_message", GreetingMessage); diff --git a/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.xml b/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.xml deleted file mode 100644 index 10ea270349d..00000000000 --- a/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - -
-

Goodbye !

-

-
- Checked out at -
-
-
- Extra hours today: - -
- - Total extra hours: - -

- -

-
-

Welcome !

-

-
- Checked in at -
-

- -
- -
- - -
- -
-
- -
-
- - - diff --git a/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.js b/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.js deleted file mode 100644 index 8a51f8f8e01..00000000000 --- a/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.js +++ /dev/null @@ -1,109 +0,0 @@ -/** @odoo-module **/ - -import { CardLayout } from "@hr_attendance/components/card_layout/card_layout"; -import { CheckInOut } from "@hr_attendance/components/check_in_out/check_in_out"; -import { Component, onWillStart, useState } from "@odoo/owl"; -import { browser } from "@web/core/browser/browser"; -import { registry } from "@web/core/registry"; -import { useService } from "@web/core/utils/hooks"; - -export class KioskConfirm extends Component { - setup() { - this.actionService = useService("action"); - this.notification = useService("notification"); - this.orm = useService("orm"); - this.user = useService("user"); - - this.padButtons = [ - ...Array.from({ length: 9 }, (_, i) => [i + 1]), // [[1], ..., [9]] - ["C", "btn-warning"], - [0], - ["OK", "btn-primary"], - ]; - this.state = useState({ - codePin: "", - }); - this.lockPad = false; - - this.nextAction = "hr_attendance.hr_attendance_action_kiosk_mode"; - const { employee_id, employee_name, employee_state, employee_hours_today } = - this.props.action; - this.employeeId = employee_id; - this.employeeName = employee_name; - this.checkedIn = employee_state === "checked_in"; - this.employeeHoursToday = registry.category("formatters").get("float_time")( - employee_hours_today - ); - - onWillStart(this.onWillStart); - } - - async onWillStart() { - this.use_pin = await this.user.hasGroup("hr_attendance.group_hr_attendance_use_pin"); - - if (this.use_pin) { - browser.addEventListener('keydown', async (ev) => { - const allowedKeys = [...Array(10).keys()].reduce((acc, value) => { // { from '0': '0' ... to '9': '9' } - acc[value] = value; - return acc; - }, { - 'Delete': 'C', - 'Enter': 'OK', - 'Backspace': null, - }); - const key = ev.key; - - if (!Object.keys(allowedKeys).includes(key)) { - return; - } - - ev.preventDefault(); - ev.stopPropagation(); - - if (allowedKeys[key]) { - await this.onClickPadButton(allowedKeys[key]); - } - else { - this.state.codePin = this.state.codePin.substring(0, this.state.codePin.length - 1); - } - }); - } - } - - onClickBack() { - this.actionService.doAction(this.nextAction, { clearBreadcrumbs: true }); - } - - async onClickPadButton(value) { - if (this.lockPad) { - return; - } - - if (value === "C") { - this.state.codePin = ""; - } else if (value === "OK") { - this.lockPad = true; - const result = await this.orm.call("hr.employee", "attendance_manual", [ - [this.employeeId], - this.nextAction, - this.state.codePin, - ]); - if (result.action) { - this.actionService.doAction(result.action); - } else if (result.warning) { - this.notification.add(result.warning, { type: "danger" }); - this.state.codePin = ""; - browser.setTimeout(() => { - this.lockPad = false; - }, 500); - } - } else { - this.state.codePin += value; - } - } -} - -KioskConfirm.template = "hr_attendance.KioskConfirm"; -KioskConfirm.components = { CardLayout, CheckInOut }; - -registry.category("actions").add("hr_attendance_kiosk_confirm", KioskConfirm); diff --git a/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.xml b/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.xml deleted file mode 100644 index f02566a8fa2..00000000000 --- a/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - -
- -
-
- - - - - - - - - - - - - -
-

-

Welcome!Want to check out?

-

Today's work hours:

- - - - -

Please enter your PIN to check outcheck in

-
-
-
-
- -
-
-
- -
- - - -
-
-
-
-
-
-
- -
+ Attendances diff --git a/addons/hr_attendance/views/hr_employee_view.xml b/addons/hr_attendance/views/hr_employee_view.xml index 587a55ef66f..92c4acdf7d8 100644 --- a/addons/hr_attendance/views/hr_employee_view.xml +++ b/addons/hr_attendance/views/hr_employee_view.xml @@ -8,31 +8,29 @@ - - - + + + + + 0 + @@ -54,30 +58,29 @@ - - + + + + + 0 + @@ -153,4 +162,27 @@ + + hr.employee.tree.leave + hr.employee + + + + + + + + + + hr.attendance.tree + hr.attendance + + + + + + + + + diff --git a/addons/hr_attendance/views/res_config_settings_views.xml b/addons/hr_attendance/views/res_config_settings_views.xml index ceddf03a219..b3a38a5ff04 100644 --- a/addons/hr_attendance/views/res_config_settings_views.xml +++ b/addons/hr_attendance/views/res_config_settings_views.xml @@ -8,20 +8,32 @@ - - + + + + + + + + + + seconds - - - - - - + + + + +
+ + If your address is compromised, you can refresh it to generate a new one. + +
+