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