[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:
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user