[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,

closes odoo/odoo#28307
This commit is contained in:
Robot Odoo
2018-11-06 17:14:35 +01:00
committed by GitHub
18 changed files with 393 additions and 6 deletions
+4
View File
@@ -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')
+10 -2
View File
@@ -346,12 +346,20 @@
</record>
<menuitem
id="menu_view_employee_category_form"
action="open_view_categ_form"
id="menu_human_resources_configuration_employee"
name="Employee"
parent="menu_human_resources_configuration"
groups="base.group_no_one"
sequence="1"/>
<menuitem
id="menu_view_employee_category_form"
name="Tags"
action="open_view_categ_form"
parent="menu_human_resources_configuration_employee"
groups="base.group_no_one"
sequence="1"/>
<!-- hr.job -->
<record id="view_hr_job_form" model="ir.ui.view">
<field name="name">hr.job.form</field>
+10 -2
View File
@@ -260,12 +260,20 @@
</record>
<menuitem
id="hr_menu_contract_type"
action="action_hr_contract_type"
id="menu_human_resources_configuration_contract"
name="Contracts"
parent="hr.menu_human_resources_configuration"
sequence="3"
groups="base.group_no_one"/>
<menuitem
id="hr_menu_contract_type"
name="Types"
action="action_hr_contract_type"
parent="hr_contract.menu_human_resources_configuration_contract"
sequence="3"
groups="base.group_no_one"/>
<menuitem
id="hr_menu_contract"
name="Contracts"
@@ -61,7 +61,7 @@
<field name="view_id" ref="gamification.challenge_form_view"/>
</record>
<menuitem id="menu_hr_gamification" parent="hr.menu_hr_root" name="Challenges" sequence="40"/>
<menuitem id="menu_hr_gamification" parent="hr.menu_human_resources_configuration" name="Challenges" sequence="100"/>
<menuitem id="gamification_badge_menu_hr" parent="menu_hr_gamification" action="gamification.badge_list_action" />
<menuitem id="gamification_challenge_menu_hr" parent="menu_hr_gamification" action="challenge_list_action2" groups="hr.group_hr_user"/>
+5
View File
@@ -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
+29
View File
@@ -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,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import bus_controller
@@ -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)
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_actions_server_action_open_presence_view" model="ir.actions.server">
<field name="name">Compute presence and open presence view</field>
<field name="model_id" ref="hr.model_hr_employee" />
<field name="state">code</field>
<field name="code">
action = env['hr.employee']._action_open_presence_view()
</field>
</record>
<menuitem id="menu_hr_presence_reports" name="Reports" parent="hr.menu_hr_root" sequence="95" groups="hr.group_hr_manager"/>
<menuitem id="menu_hr_presence_view" name="Employee Presence" action="ir_actions_server_action_open_presence_view" parent="menu_hr_presence_reports" groups="hr.group_hr_manager"/>
</odoo>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_template_presence" model="mail.template">
<field name="name">Presence: Contact Employee</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="subject">Unexpected Absence</field>
<field name="email_from">${ctx['email_from'] or ''}</field>
<field name="email_to">${ctx['email_to'] or ''}</field>
<field name="auto_delete" eval="False"/>
<field name="body_html" type="xml">
<div>
Dear ${object.name},<br/><br/>
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.<br/>
Please, take appropriate measures in order to carry out this work absence.<br/>
Do not hesitate to contact your manager or the human resource department.
<br/>Best Regards,<br/><br/>
</div>
</field>
</record>
</data>
</odoo>
+7
View File
@@ -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
+147
View File
@@ -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,
}
+13
View File
@@ -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()
@@ -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)
@@ -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")
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_employee_view_search" model="ir.ui.view">
<field name="name">hr.employee.view.search</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_filter"/>
<field name="groups_id" eval="[(4, ref('hr.group_hr_manager'))]"/>
<field name="arch" type="xml">
<filter name="group_manager" position="after">
<filter name="group_hr_presence_state" string="Presence" domain="[]" context="{'group_by':'hr_presence_state'}"/>
</filter>
</field>
</record>
<record id="hr_employee_view_kanban" model="ir.ui.view">
<field name="name">hr.employee.view.kanban</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.hr_kanban_view_employees"/>
<field name="groups_id" eval="[(4, ref('hr.group_hr_manager'))]"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_kanban_details')]" position="inside">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<button name="%(hr_holidays.hr_leave_action_all)d" type="action" class="btn btn-primary btn-sm mt8" context="{'default_employee_id': active_id}">Leaves</button>
<button name="action_send_sms" type="object" class="btn btn-primary btn-sm mt8" context="{'active_id': active_id}">SMS</button>
<button name="action_send_mail" type="object" class="btn btn-primary btn-sm mt8" context="{'active_id': active_id}">Mail</button>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.hr</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="70"/>
<field name="inherit_id" ref="hr.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@data-key='hr']//div[hasclass('o_settings_container')]" position="inside">
<div class="col-12 col-lg-6 o_setting_box" title="Control presence of employees">
<div class="o_setting_left_pane">
<field name="module_hr_presence"/>
</div>
<div class="o_setting_right_pane">
<label for="module_hr_presence"/>
<div class="content-group">
<div class="row">
<field name="hr_presence_control_login" class="col-lg-1 ml16"/>
<label for="hr_presence_control_login" class="col-lg-10 o_light_label"/>
</div>
<div class="row">
<field name="hr_presence_control_email" class="col-lg-1 ml16"/>
<label for="hr_presence_control_email" class="col-lg-10 o_light_label"/>
</div>
<div attrs="{'invisible': [('hr_presence_control_email', '=', False)]}">
<span>At least </span>
<field name="hr_presence_control_email_amount"/>
<span> sent emails</span>
</div>
<div class="row">
<field name="hr_presence_control_ip" class="col-lg-1 ml16"/>
<label for="hr_presence_control_ip" class="col-lg-10 o_light_label"/>
</div>
<div attrs="{'invisible': [('hr_presence_control_ip', '=', False)]}">
<span>IP List (comma separated):</span>
<field name="hr_presence_control_ip_list"/>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
+1 -1
View File
@@ -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 = []