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 + + + + +
+
+ +
+
+
+
+
+
+
+
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 = []