From c0c72f027d33ac44c1cd65781e683af7e8736311 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 30 Oct 2018 12:22:25 +0000 Subject: [PATCH 1/2] [ADD] hr_presence: New module to control employees presences Purpose ======= A lot of companies don't use the module hr.attendance but are faced to employees distraction, they forget to record leaves. In big companies, it is not easy to controle presence and check if everyone is present in the office. Specification ============= 1/ Review Human Resources menuitems - Employees - Contract - Department - Reports - Employee Presence - Configuration - Settings - Employee - Tag - Contract - Types - Templates - Challenges - Challenges - Badges - Goal History 2/ Add option in hr settings to decide the company policy [ ] Controle Presence of Employees [ ] According the system login --> Based on the bus presence (Present if 'away' or 'online') [ ] Following number of sent emails --> Based on the sent mail.message during the day [ ] According to the IP address --> Based on the ip address when requesting a long polling, stored on a res.users.log 3/ Add an employee presence view This view is only for the hr manager and the data is computed when the hr manager goes to it Clicking on this menu item will generate all kanban of employees grouped by state (present, absent, to define) On each employee cart, an action can be requested as: - Send a sms to the employee - Send an email to the employee - Create a leave for the employee 4/ Use this email template to contact the employee Dear %(employee_name)s, Exception made if there was a mistake of ours, it seems that you are not at your office and there is not request of leaves from you. Please, take appropriate measures in order to carry out this work absence. Do not hesitate to contact your manager or the human resource department. Best Regards, --- addons/hr/models/res_config_settings.py | 4 + addons/hr/views/hr_views.xml | 12 +- .../hr_contract/views/hr_contract_views.xml | 12 +- .../views/gamification_views.xml | 2 +- addons/hr_presence/__init__.py | 5 + addons/hr_presence/__manifest__.py | 29 ++++ addons/hr_presence/controllers/__init__.py | 4 + .../hr_presence/controllers/bus_controller.py | 24 +++ addons/hr_presence/data/ir_actions_server.xml | 16 ++ addons/hr_presence/data/mail_data.xml | 22 +++ addons/hr_presence/models/__init__.py | 7 + addons/hr_presence/models/hr_employee.py | 147 ++++++++++++++++++ addons/hr_presence/models/res_company.py | 13 ++ .../hr_presence/models/res_config_settings.py | 13 ++ addons/hr_presence/models/res_users_log.py | 10 ++ .../hr_presence/views/hr_employee_views.xml | 33 ++++ .../views/res_config_settings_views.xml | 44 ++++++ 17 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 addons/hr_presence/__init__.py create mode 100644 addons/hr_presence/__manifest__.py create mode 100644 addons/hr_presence/controllers/__init__.py create mode 100644 addons/hr_presence/controllers/bus_controller.py create mode 100644 addons/hr_presence/data/ir_actions_server.xml create mode 100644 addons/hr_presence/data/mail_data.xml create mode 100644 addons/hr_presence/models/__init__.py create mode 100644 addons/hr_presence/models/hr_employee.py create mode 100644 addons/hr_presence/models/res_company.py create mode 100644 addons/hr_presence/models/res_config_settings.py create mode 100644 addons/hr_presence/models/res_users_log.py create mode 100644 addons/hr_presence/views/hr_employee_views.xml create mode 100644 addons/hr_presence/views/res_config_settings_views.xml diff --git a/addons/hr/models/res_config_settings.py b/addons/hr/models/res_config_settings.py index 4512d6e04f3..ea96590434c 100644 --- a/addons/hr/models/res_config_settings.py +++ b/addons/hr/models/res_config_settings.py @@ -10,3 +10,7 @@ class ResConfigSettings(models.TransientModel): 'resource.calendar', 'Company Working Hours', related='company_id.resource_calendar_id', readonly=False) module_hr_org_chart = fields.Boolean(string="Show Organizational Chart") + module_hr_presence = fields.Boolean(string="Control presence of employees") + hr_presence_control_login = fields.Boolean(string="According to the system login (User status on chat)", config_parameter='hr.hr_presence_control_login') + hr_presence_control_email = fields.Boolean(string="According to the amount of sent emails", config_parameter='hr.hr_presence_control_email') + hr_presence_control_ip = fields.Boolean(string="According to the IP address", config_parameter='hr.hr_presence_control_ip') diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml index d02240f639d..62722d90330 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -346,12 +346,20 @@ + + hr.job.form diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml index af8e47cebce..8365c57c2eb 100644 --- a/addons/hr_contract/views/hr_contract_views.xml +++ b/addons/hr_contract/views/hr_contract_views.xml @@ -260,12 +260,20 @@ + + - + diff --git a/addons/hr_presence/__init__.py b/addons/hr_presence/__init__.py new file mode 100644 index 00000000000..7d34c7c054a --- /dev/null +++ b/addons/hr_presence/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/hr_presence/__manifest__.py b/addons/hr_presence/__manifest__.py new file mode 100644 index 00000000000..f4439dd7ac9 --- /dev/null +++ b/addons/hr_presence/__manifest__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Employee Presence Control', + 'version': '1.0', + 'category': 'Human Resources', + 'description': """ +Control Employees Presence +========================== + +Based on: + * The IP Address + * The User's Session + * The Sent Emails + +Allows to contact directly the employee in case of unjustified absence. + """, + 'depends': ['hr', 'hr_holidays', 'sms'], + 'data': [ + 'views/res_config_settings_views.xml', + 'views/hr_employee_views.xml', + 'data/ir_actions_server.xml', + 'data/mail_data.xml', + ], + 'demo': [], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/hr_presence/controllers/__init__.py b/addons/hr_presence/controllers/__init__.py new file mode 100644 index 00000000000..9304f48d4e5 --- /dev/null +++ b/addons/hr_presence/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import bus_controller \ No newline at end of file diff --git a/addons/hr_presence/controllers/bus_controller.py b/addons/hr_presence/controllers/bus_controller.py new file mode 100644 index 00000000000..6ee91242988 --- /dev/null +++ b/addons/hr_presence/controllers/bus_controller.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +from odoo import registry, SUPERUSER_ID +from odoo.api import Environment +from odoo.addons.bus.controllers.main import BusController +from odoo.fields import Datetime +from odoo.http import Controller, request, route + + +class BusController(BusController): + + @route('/longpolling/poll', type="json", auth="public") + def poll(self, channels, last, options=None): + if request.env.user.has_group('base.group_user'): + ip_address = request.httprequest.remote_addr + users_log = request.env['res.users.log'].search_count([ + ('create_uid', '=', request.env.user.id), + ('ip', '=', ip_address), + ('create_date', '>=', Datetime.to_string(Datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)))]) + if not users_log: + with registry(request.env.cr.dbname).cursor() as cr: + env = Environment(cr, request.env.user.id, {}) + env['res.users.log'].create({'ip': ip_address}) + return super(BusController, self).poll(channels, last, options=None) diff --git a/addons/hr_presence/data/ir_actions_server.xml b/addons/hr_presence/data/ir_actions_server.xml new file mode 100644 index 00000000000..7c18b7f7798 --- /dev/null +++ b/addons/hr_presence/data/ir_actions_server.xml @@ -0,0 +1,16 @@ + + + + Compute presence and open presence view + + code + +action = env['hr.employee']._action_open_presence_view() + + + + + + + + diff --git a/addons/hr_presence/data/mail_data.xml b/addons/hr_presence/data/mail_data.xml new file mode 100644 index 00000000000..d6c1fb3539f --- /dev/null +++ b/addons/hr_presence/data/mail_data.xml @@ -0,0 +1,22 @@ + + + + + Presence: Contact Employee + + Unexpected Absence + ${ctx['email_from'] or ''} + ${ctx['email_to'] or ''} + + +
+ Dear ${object.name},

+ Exception made if there was a mistake of ours, it seems that you are not at your office and there is not request of leaves from you.
+ Please, take appropriate measures in order to carry out this work absence.
+ Do not hesitate to contact your manager or the human resource department. +
Best Regards,

+
+
+
+
+
diff --git a/addons/hr_presence/models/__init__.py b/addons/hr_presence/models/__init__.py new file mode 100644 index 00000000000..996b19c0a45 --- /dev/null +++ b/addons/hr_presence/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import hr_employee +from . import res_company +from . import res_config_settings +from . import res_users_log diff --git a/addons/hr_presence/models/hr_employee.py b/addons/hr_presence/models/hr_employee.py new file mode 100644 index 00000000000..4a5dd0b6292 --- /dev/null +++ b/addons/hr_presence/models/hr_employee.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging + +from odoo import fields, models, _ +from odoo.exceptions import UserError +from odoo.fields import Datetime + +_logger = logging.getLogger(__name__) + + +class ResCompany(models.Model): + _inherit = 'hr.employee' + + hr_presence_state = fields.Selection([ + ('present', 'Present'), + ('absent', 'Absent'), + ('to_define', 'To Define')], default='to_define', groups='hr.group_hr_manager') + + + def _action_open_presence_view(self): + # Compute the presence/absence for the employees on the same + # company than the HR/manager. Then opens the kanban view + # of the employees with an undefined presence/absence + + _logger.info("Employees presence checked by: %s" % self.env.user.name) + + company = self.env.user.company_id + if not company.hr_presence_last_compute_date or \ + company.hr_presence_last_compute_date.day != Datetime.now().day: + self.env['hr.employee'].search([ + ('department_id.company_id', '=', company.id) + ]).write({'hr_presence_state': 'to_define'}) + + employees = self.env['hr.employee'].search([ + ('department_id.company_id', '=', company.id), + ('user_id', '!=', False), + ('hr_presence_state', '=', 'to_define')]) + + # Remove employees on holidays + leaves = self.env['hr.leave'].search([ + ('state', '=', 'validate'), + ('date_from', '<=', Datetime.to_string(Datetime.now())), + ('date_to', '>=', Datetime.to_string(Datetime.now()))]) + employees_on_holiday = leaves.mapped('employee_id') + employees_on_holiday.write({'hr_presence_state': 'absent'}) + employees = employees - employees_on_holiday + + # Check on system login + if self.env['ir.config_parameter'].sudo().get_param('hr_presence.hr_presence_control_login'): + online_employees = employees.filtered(lambda employee: employee.user_id.im_status in ['away', 'online']) + online_employees.write({'hr_presence_state': 'present'}) + employees = employees - online_employees + + # Check on IP + if self.env['ir.config_parameter'].sudo().get_param('hr_presence.hr_presence_control_ip'): + ip_list = company.hr_presence_control_ip_list + ip_list = ip_list.split(',') if ip_list else [] + ip_employees = self.env['hr.employee'] + for employee in employees: + employee_ips = self.env['res.users.log'].search([ + ('create_uid', '=', employee.user_id.id), + ('ip', '!=', False), + ('create_date', '>=', Datetime.to_string(Datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)))] + ).mapped('ip') + if any([ip in ip_list for ip in employee_ips]): + ip_employees |= employee + ip_employees.write({'hr_presence_state': 'present'}) + employees = employees - ip_employees + + # Check on sent emails + if self.env['ir.config_parameter'].sudo().get_param('hr_presence.hr_presence_control_email'): + email_employees = self.env['hr.employee'] + threshold = company.hr_presence_control_email_amount + for employee in employees: + sent_emails = self.env['mail.message'].search_count([ + ('author_id', '=', employee.user_id.partner_id.id), + ('date', '>=', Datetime.to_string(Datetime.now().replace(hour=0, minute=0, second=0, microsecond=0))), + ('date', '<=', Datetime.to_string(Datetime.now()))]) + if sent_emails >= threshold: + email_employees |= employee + email_employees.write({'hr_presence_state': 'present'}) + employees = employees - email_employees + + company.hr_presence_last_compute_date = Datetime.now() + + return { + "type": "ir.actions.act_window", + "res_model": "hr.employee", + "views": [[self.env.ref('hr_presence.hr_employee_view_kanban').id, "kanban"], [False, "tree"], [False, "form"]], + 'view_mode': 'kanban,tree,form', + "domain": [], + "name": "Employee's Presence to Define", + "context": {'search_default_group_hr_presence_state': 1}, + } + + def action_send_sms(self): + if not self.env.user.has_group('hr.group_hr_manager'): + raise UserError(_("You don't have the right to do this. Please contact an Administrator.")) + if not self.mobile_phone: + raise UserError(_("There is no professional phone for this employee.")) + body = _("""Exception made if there was a mistake of ours, it seems that you are not at your office and there is not request of leaves from you. +Please, take appropriate measures in order to carry out this work absence. +Do not hesitate to contact your manager or the human resource department.""") + return { + "type": "ir.actions.act_window", + "res_model": "sms.send_sms", + "view_mode": 'form', + "context": { + 'active_id': self.env.context.get('active_id'), + 'default_message': body, + 'default_recipients': self.mobile_phone, + }, + "name": "Send SMS", + "target": "new", + } + + def action_send_mail(self): + if not self.env.user.has_group('hr.group_hr_manager'): + raise UserError(_("You don't have the right to do this. Please contact an Administrator.")) + employee = self.env['hr.employee'].sudo().browse(self.env.context.get('active_id')) + if not employee.work_email: + raise UserError(_("There is no professional email address for this employee.")) + template = self.env.ref('hr_presence.mail_template_presence', False) + compose_form = self.env.ref('mail.email_compose_message_wizard_form', False) + ctx = dict( + default_model='hr.employee', + default_res_id=employee.id, + default_use_template=bool(template), + default_template_id=template.id, + default_composition_mode='comment', + custom_layout='mail.mail_notification_light', + email_from=self.env.user.email, + email_to=employee.work_email, + ) + return { + 'name': _('Compose Email'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'mail.compose.message', + 'views': [(compose_form.id, 'form')], + 'view_id': compose_form.id, + 'target': 'new', + 'context': ctx, + } diff --git a/addons/hr_presence/models/res_company.py b/addons/hr_presence/models/res_company.py new file mode 100644 index 00000000000..7de70604a92 --- /dev/null +++ b/addons/hr_presence/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' + + + hr_presence_control_email_amount = fields.Integer(string="# emails to send") + hr_presence_control_ip_list = fields.Char(string="Valid IP addresses") + hr_presence_last_compute_date = fields.Datetime() diff --git a/addons/hr_presence/models/res_config_settings.py b/addons/hr_presence/models/res_config_settings.py new file mode 100644 index 00000000000..e831d7506bb --- /dev/null +++ b/addons/hr_presence/models/res_config_settings.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + hr_presence_control_login = fields.Boolean(string="According to the system login (User status on chat)", config_parameter='hr_presence.hr_presence_control_login') + hr_presence_control_email = fields.Boolean(string="According to the amount of sent emails", config_parameter='hr_presence.hr_presence_control_email') + hr_presence_control_ip = fields.Boolean(string="According to the IP address", config_parameter='hr_presence.hr_presence_control_ip') + hr_presence_control_email_amount = fields.Integer(related="company_id.hr_presence_control_email_amount", readonly=False) + hr_presence_control_ip_list = fields.Char(related="company_id.hr_presence_control_ip_list", readonly=False) diff --git a/addons/hr_presence/models/res_users_log.py b/addons/hr_presence/models/res_users_log.py new file mode 100644 index 00000000000..9ad3c97df6e --- /dev/null +++ b/addons/hr_presence/models/res_users_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class ResUsersLog(models.Model): + _inherit = 'res.users.log' + + create_uid = fields.Integer(index=True) + ip = fields.Char(string="IP Address") diff --git a/addons/hr_presence/views/hr_employee_views.xml b/addons/hr_presence/views/hr_employee_views.xml new file mode 100644 index 00000000000..3cf722f0841 --- /dev/null +++ b/addons/hr_presence/views/hr_employee_views.xml @@ -0,0 +1,33 @@ + + + + hr.employee.view.search + hr.employee + + + + + + + + + + + hr.employee.view.kanban + hr.employee + + + primary + + +
+
+ + + +
+
+
+
+
+
diff --git a/addons/hr_presence/views/res_config_settings_views.xml b/addons/hr_presence/views/res_config_settings_views.xml new file mode 100644 index 00000000000..b645cf0f7aa --- /dev/null +++ b/addons/hr_presence/views/res_config_settings_views.xml @@ -0,0 +1,44 @@ + + + + res.config.settings.view.form.inherit.hr + res.config.settings + + + + +
+
+ +
+
+
+
+
+
+
+
From 3c91ac425f74755fa0e79aa620b601d29a4be65b Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 6 Nov 2018 15:02:02 +0000 Subject: [PATCH 2/2] [IMP] sms: Don't overwrite default recipients on default_get If the recipients are passed by setting 'default_recipients' on the context, then don't overwrite the content with the method '_get_default_sms_recipients'. For example, as an employee is a mail.thread, then the method '_get_default_sms_recipients' exists on it. Calling this method will erase the content of the field 'recipients' that comes from the default_recipients key in the context. If we want to use this wizard on something else than a partner, we should avoid this kind of overwrite. --- addons/sms/wizard/send_sms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py index 448d9fcc626..786a5e2af76 100644 --- a/addons/sms/wizard/send_sms.py +++ b/addons/sms/wizard/send_sms.py @@ -67,7 +67,7 @@ class SendSMS(models.TransientModel): model = self.env[active_model] records = self._get_records(model) - if getattr(records, '_get_default_sms_recipients'): + if getattr(records, '_get_default_sms_recipients') and not self.env.context.get('default_recipients'): partners = records._get_default_sms_recipients() phone_numbers = [] no_phone_partners = []