Skip to content
Snippets Groups Projects
Commit f6fdde96 authored by Lucas Lefèvre's avatar Lucas Lefèvre Committed by Yannick Tivisse
Browse files

[IMP] hr_*: Improve employee presence state


Purpose
=======
There are currently multiple ways to determine if an employee is presence
or absent:
- login state (user status on chat)
- checkin/checkout from the Attendance app
- leave
- hr_presence (email, ip)
Those states are displayed in several different places and can be inconsitent.
e.g. Logged in -> green in the chat
Not checked in -> red on the employee kanban view.

The multiple ways to determine employee presence described above
should be aggregated together to have a better consistency.

Specification
=============
There are 3 presence states (previously defined in module `hr_presence`),
namely `present`, `absent`, `to_define`. They are now defined as soon as
`hr` module is installed.

The state computation can have several behaviors according to which apps
are installed:

1. `hr` is installed
Check employee presence based on login by default (only for employees with
a user).
Kanban state (private and public): should be green when logged in, red when logged out and
orange when the user is away.
Private employee form: Display a stat button "Connected" on the form view
when the user is logged in or "Last Activity xx/xx/xxx" otherwise.

2. `hr_attendance` is installed:
when the user is logged out, the state
is determined from the checked in/out state. But when the user is logged in,
consider the user as present (even if he checked out).
Kanban state: green for checked in, red for checked out
Private form view: attendance stat button.

3. `hr_holidays` is installed:
Has the highest priority if the employee is on leave.
Kanban state: red if employee is on leave.
Private form view: stat button "Absent Until xx/xx/xxxx" if employee
is on leave.

4. `hr_presence` is installed:
Two additionnal presence checking option: emails sent and IP address connected.
Once a user sent an email or the IP address was connected, he is considered
present for the entire day.

There should be at most one stat button on the employee form view, with the
relevent information.

Task id: 2024482

closes odoo/odoo#34933

Signed-off-by: default avatarYannick Tivisse (yti) <yti@odoo.com>
parent 00863473
No related branches found
No related tags found
No related merge requests found
Showing
with 208 additions and 138 deletions
......@@ -99,5 +99,10 @@
<field name="user_signature" eval="False"/>
</record>
<record model="ir.config_parameter" id="hr_presence_control_login" forcecreate="False">
<field name="key">hr.hr_presence_control_login</field>
<field name="value">True</field>
</record>
</data>
</odoo>
......@@ -13,3 +13,4 @@ from . import mail_channel
from . import res_config_settings
from . import res_partner
from . import res_users
from . import res_company
......@@ -23,3 +23,31 @@ class HrEmployeeBase(models.AbstractModel):
user_id = fields.Many2one('res.users')
resource_id = fields.Many2one('resource.resource')
resource_calendar_id = fields.Many2one('resource.calendar')
hr_presence_state = fields.Selection([
('present', 'Present'),
('absent', 'Absent'),
('to_define', 'To Define')], compute='_compute_presence_state', default='to_define')
last_activity = fields.Date(compute="_compute_last_activity")
def _compute_presence_state(self):
"""
This method is overritten in several other modules which add additional
presence criterions. e.g. hr_attendance, hr_holidays
"""
# Check on login
check_login = self.env['ir.config_parameter'].sudo().get_param('hr.hr_presence_control_login')
for employee in self:
state = 'to_define'
if check_login:
if employee.user_id.im_status == 'online':
state = 'present'
elif employee.user_id.im_status == 'offline':
state = 'absent'
employee.hr_presence_state = state
def _compute_last_activity(self):
employees = self.filtered(lambda e: e.user_id)
presences = self.env['bus.presence'].search([('user_id', 'in', employees.mapped('user_id.id'))])
for presence in presences:
presence.user_id.employee_ids.last_activity = presence.last_presence.date()
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class Company(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")
......@@ -10,9 +10,12 @@ 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")
module_hr_presence = fields.Boolean(string="Advanced control presence of employees")
module_hr_skills = fields.Boolean(string="Employee Skills and Resumé")
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')
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')
module_hr_attendance = fields.Boolean(string="According to the Attendance module.")
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)
hr_employee_self_edit = fields.Boolean(string="Employee Edition", config_parameter='hr.hr_employee_self_edit')
......@@ -52,6 +52,8 @@ class User(models.Model):
study_field = fields.Char(related='employee_id.study_field', readonly=False, related_sudo=False)
study_school = fields.Char(related='employee_id.study_school', readonly=False, related_sudo=False)
employee_count = fields.Integer(compute='_compute_employee_count')
hr_presence_state = fields.Selection(related='employee_id.hr_presence_state')
last_activity = fields.Date(related='employee_id.last_activity')
@api.depends('employee_ids')
def _compute_employee_count(self):
......@@ -69,6 +71,8 @@ class User(models.Model):
'employee_id',
'employee_ids',
'employee_parent_id',
'hr_presence_state',
'last_activity',
]
hr_writable_fields = [
......
......@@ -95,6 +95,7 @@
<field name="arch" type="xml">
<kanban class="o_hr_employee_kanban">
<field name="id"/>
<field name="hr_presence_state"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
......@@ -107,6 +108,15 @@
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<div class="float-right" t-if="record.hr_presence_state.raw_value == 'present'">
<span class="fa fa-circle text-success" role="img" aria-label="Present" title="Present" name="presence_present"></span>
</div>
<div class="float-right" t-if="record.hr_presence_state.raw_value == 'absent'">
<span class="fa fa-circle text-danger" role="img" aria-label="Absent" title="Absent" name="presence_absent"></span>
</div>
<div class="float-right" t-if="record.hr_presence_state.raw_value == 'to_define'">
<span class="fa fa-circle text-warning" role="img" aria-label="To define" title="To define" name="presence_to_define"></span>
</div>
<field name="name"/>
</strong>
<span t-if="record.job_id.raw_value" class="o_kanban_record_subtitle"><field name="job_id"/></span>
......
......@@ -40,14 +40,37 @@
<form string="Employee" js_class="hr_employee_form">
<field name="active" invisible="1"/>
<field name="user_partner_id" invisible="1"/>
<field name="hr_presence_state" invisible="1"/>
<header>
<button string="Chat" class="btn btn-primary o_employee_chat_btn" attrs="{'invisible': [('user_id','=', False)]}"/>
<button name="%(plan_wizard_action)d" string="Launch Plan" type="action" groups="hr.group_hr_manager"/>
</header>
<sheet>
<widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div name="button_box" class="oe_button_box"/>
<field name="image" widget='image' class="oe_avatar" options='{"zoom": true, "preview_image":"image_medium"}'/>
<widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div name="button_box" class="oe_button_box">
<button
id="hr_presence_button"
class="oe_stat_button"
disabled="1"
attrs="{'invisible': ['|', ('last_activity', '=', False), ('user_id', '=', False)]}">
<div role="img" class="fa fa-fw fa-circle text-success o_button_icon" attrs="{'invisible': [('hr_presence_state', '!=', 'present')]}" aria-label="Available" title="Available"/>
<div role="img" class="fa fa-fw fa-circle text-warning o_button_icon" attrs="{'invisible': [('hr_presence_state', '!=', 'to_define')]}" aria-label="Away" title="Away"/>
<div role="img" class="fa fa-fw fa-circle text-danger o_button_icon" attrs="{'invisible': [('hr_presence_state', '!=', 'absent')]}" aria-label="Not available" title="Not available"/>
<div class="o_stat_info" attrs="{'invisible': [('hr_presence_state', '=', 'present')]}">
<span class="o_stat_value">
<field name="last_activity"/>
</span>
<span class="o_stat_text">
Last Activity
</span>
</div>
<div class="o_stat_info" attrs="{'invisible': [('hr_presence_state', '!=', 'present')]}">
<span class="o_stat_text">Connected</span>
</div>
</button>
</div>
<field name="image" widget='image' class="oe_avatar" options='{"zoom": true, "preview_image":"image_medium"}'/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
......@@ -204,6 +227,7 @@
<field name="arch" type="xml">
<kanban class="o_hr_employee_kanban">
<field name="id"/>
<field name="hr_presence_state"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
......@@ -216,7 +240,16 @@
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<field name="name"/>
<div class="float-right" t-if="record.hr_presence_state.raw_value == 'present'">
<span class="fa fa-circle text-success" role="img" aria-label="Present" title="Present" name="presence_present"></span>
</div>
<div class="float-right" t-if="record.hr_presence_state.raw_value == 'absent'">
<span class="fa fa-circle text-danger" role="img" aria-label="Absent" title="Absent" name="presence_absent"></span>
</div>
<div class="float-right" t-if="record.hr_presence_state.raw_value == 'to_define'">
<span class="fa fa-circle text-warning" role="img" aria-label="To define" title="To define" name="presence_to_define"></span>
</div>
<field name="name" placeholder="Employee's Name"/>
</strong>
<span t-if="record.job_id.raw_value" class="o_kanban_record_subtitle"><field name="job_id"/></span>
</div>
......
......@@ -20,6 +20,50 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" title="Presence of employees">
<div class="o_setting_right_pane">
<span class="o_form_label">Control presence of employees</span>
<div class="content-group" name="hr_presence_options">
<div class="row">
<field name="module_hr_attendance" class="col-lg-1 ml16"/>
<label for="module_hr_attendance" class="col-lg-10 o_light_label"/>
</div>
<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>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" title="Advanced 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="text-muted" name="hr_presence_options_advanced">
Presence reporting screen, email and IP address control.
</div>
<div class="row mt-1" attrs="{'invisible': [('module_hr_presence', '=', False)]}">
<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 class="row ml-4" attrs="{'invisible': ['|', ('module_hr_presence', '=', False), ('hr_presence_control_email', '=', False)]}">
<span class="mr-2">At least </span>
<field name="hr_presence_control_email_amount"/>
<span> sent emails</span>
</div>
<div class="row" attrs="{'invisible': [('module_hr_presence', '=', False)]}">
<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 class="row ml-4" attrs="{'invisible': ['|', ('module_hr_presence', '=', False), ('hr_presence_control_ip', '=', False)]}">
<span class="mr-2">IP List (comma separated):</span>
<field name="hr_presence_control_ip_list"/>
</div>
</div>
</div>
</div>
<h2>Work Organization</h2>
<div class="row mt16 o_settings_container">
......
......@@ -36,9 +36,31 @@
<field name="arch" type="xml">
<form position="replace">
<form>
<field name="hr_presence_state" invisible="1"/>
<header></header> <!-- Used by other modules to add buttons -->
<sheet>
<div class="oe_button_box" name="button_box">
<button
id="hr_presence_button"
class="oe_stat_button"
disabled="1"
attrs="{'invisible': [('hr_presence_state', '=', 'absent')]}">
<div role="img" class="fa fa-fw fa-circle text-success o_button_icon" attrs="{'invisible': [('hr_presence_state', '!=', 'present')]}" aria-label="Available" title="Available"/>
<div role="img" class="fa fa-fw fa-circle text-warning o_button_icon" attrs="{'invisible': [('hr_presence_state', '!=', 'to_define')]}" aria-label="Away" title="Away"/>
<div role="img" class="fa fa-fw fa-circle text-danger o_button_icon" attrs="{'invisible': [('hr_presence_state', '!=', 'absent')]}" aria-label="Not available" title="Not available"/>
<div class="o_stat_info" attrs="{'invisible': [('hr_presence_state', '=', 'present')]}">
<span class="o_stat_value">
<field name="last_activity"/>
</span>
<span class="o_stat_text">
Last Activity
</span>
</div>
<div class="o_stat_info" attrs="{'invisible': [('hr_presence_state', '!=', 'present')]}">
<span class="o_stat_text">Connected</span>
</div>
</button>
</div>
<field name="image" widget='image' class="oe_avatar" options='{"zoom": true, "preview_image":"image_medium"}'/>
<div class="oe_title">
......
......@@ -17,6 +17,20 @@ class HrEmployeeBase(models.AbstractModel):
hours_last_month = fields.Float(compute='_compute_hours_last_month')
hours_today = fields.Float(compute='_compute_hours_today')
def _compute_presence_state(self):
"""
Override to include checkin/checkout in the presence state
Attendance has the second highest priority after login
"""
super()._compute_presence_state()
employees = self.filtered(lambda employee: employee.hr_presence_state != 'present')
for employee in employees:
if employee.attendance_state == 'checked_out' and employee.hr_presence_state == 'to_define':
employee.hr_presence_state = 'absent'
for employee in employees:
if employee.attendance_state == 'checked_in':
employee.hr_presence_state = 'present'
def _compute_hours_last_month(self):
for employee in self:
now = datetime.now()
......
......@@ -4,14 +4,24 @@
<field name="name">hr.employee</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="priority">10</field>
<field name="priority">20</field>
<field name="groups_id" eval="[(4,ref('hr_attendance.group_hr_attendance_user'))]"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<xpath expr="//button[@id='hr_presence_button']" position="attributes">
<attribute name="attrs">
{'invisible': ['|', '|', ('user_id', '=', False), ('hr_presence_state', '=', 'absent'), ('attendance_state', '=', 'checked_in')]}
</attribute>
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<field name="attendance_state" invisible="1"/>
<button name="%(hr_attendance_action_employee)d"
id="hr_attendance_button"
class="oe_stat_button"
type="action" attrs="{'invisible': [('attendance_state', '=', False)]}">
type="action" attrs="{'invisible': [
'|', ('attendance_state', '=', False),
'&amp;',
('hr_presence_state', '=', 'present'),
('attendance_state', '=', 'checked_out')]}">
<div role="img" id="oe_hr_attendance_status" class="fa fa-fw fa-circle o_button_icon oe_hr_attendance_status_green" attrs="{'invisible': [('attendance_state', '=', 'checked_out')]}" aria-label="Available" title="Available"/>
<div role="img" id="oe_hr_attendance_status" class="fa fa-fw fa-circle o_button_icon oe_hr_attendance_status_red" attrs="{'invisible': [('attendance_state', '=', 'checked_in')]}" aria-label="Not available" title="Not available"/>
<div class="o_stat_info">
......@@ -34,7 +44,7 @@
</span>
</div>
</button>
</div>
</xpath>
</field>
</record>
......@@ -43,11 +53,17 @@
<field name="model">res.users</field>
<field name="inherit_id" ref="hr.res_users_view_form_profile"/>
<field name="arch" type="xml">
<xpath expr="//button[@id='hr_presence_button']" position="attributes">
<attribute name="attrs">
{'invisible': ['|', '|', ('hr_presence_state', '=', 'absent'), ('attendance_state', '=', 'checked_in')]}
</attribute>
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<field name="attendance_state" invisible="1"/>
<button name="%(hr_attendance_action_my_attendances)d"
id="hr_attendance_button"
class="oe_stat_button"
type="action" attrs="{'invisible': [('attendance_state', '=', False)]}">
type="action" attrs="{'invisible': ['|', ('attendance_state', '=', False), '&amp;', ('hr_presence_state', '=', 'present'), ('attendance_state', '=', 'checked_out')]}">
<div role="img" id="oe_hr_attendance_status" class="fa fa-fw fa-circle o_button_icon oe_hr_attendance_status_green" attrs="{'invisible': [('attendance_state', '=', 'checked_out')]}" aria-label="Available" title="Available"/>
<div role="img" id="oe_hr_attendance_status" class="fa fa-fw fa-circle o_button_icon oe_hr_attendance_status_red" attrs="{'invisible': [('attendance_state', '=', 'checked_in')]}" aria-label="Not available" title="Not available"/>
<div class="o_stat_info">
......@@ -131,31 +147,6 @@
</field>
</record>
<record id="view_employee_kanban_inherit_hr_attendance" model="ir.ui.view">
<field name="name">hr.employee</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.hr_kanban_view_employees"/>
<field name="priority">1</field>
<field name="arch" type="xml">
<templates position="before">
<field name="attendance_state"/>
</templates>
<field name="name" position="replace">
<div>
<div class="float-right" t-if="record.attendance_state.raw_value == 'checked_in'">
<span id="oe_hr_attendance_status" class="fa fa-circle oe_hr_attendance_status_green" role="img" aria-label="Available" title="Available"></span>
</div>
<div class="float-right" t-if="record.attendance_state.raw_value == 'checked_out'">
<span id="oe_hr_attendance_status" class="fa fa-circle oe_hr_attendance_status_red" role="img" aria-label="Not available" title="Not available"></span>
</div>
<strong>
<field name="name" placeholder="Employee's Name"/>
</strong>
</div>
</field>
</field>
</record>
<record id="hr_employee_attendance_action_kanban" model="ir.actions.act_window">
<field name="name">Employees</field>
<field name="res_model">hr.employee</field>
......
from . import models
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Attendance Presence',
'version': '1.0',
'category': 'Human Resources',
'sequence': 85,
'summary': 'Bridge Attendance module and Presence module',
'description': "",
'website': 'https://www.odoo.com/page/employees',
'depends': ['hr_attendance', 'hr_presence'],
'installable': True,
'auto_install': True,
'data': [
'views/res_config_settings_views.xml',
'views/hr_employee.xml',
],
}
from . import hr_employee
from . import res_config_settings
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class Employee(models.Model):
_inherit = 'hr.employee'
def _action_open_presence_view(self):
action = super(Employee, self)._action_open_presence_view()
if self.env['ir.config_parameter'].sudo().get_param('hr_presence.hr_presence_control_attendance'):
company = self.env.company
employees = self.env['hr.employee'].search([
('department_id.company_id', '=', company.id),
('user_id', '!=', False),
])
employees.filtered(
lambda e: e.attendance_state == 'checked_in'
).write({'hr_presence_state': 'present'})
employees.filtered(
lambda e: e.attendance_state == 'checked_out'
).write({'hr_presence_state': 'absent'})
return action
# -*- coding: utf-8 -*-
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
hr_presence_control_attendance = fields.Boolean(string="According to the Attendance module", config_parameter='hr_presence.hr_presence_control_attendance')
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_employee_view_kanban" model="ir.ui.view">
<field name="name">hr.employee</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr_presence.hr_employee_view_kanban_inherit"/>
<field name="priority">20</field>
<field name="arch" type="xml">
<xpath expr="//span[@name='presence_absent']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//span[@name='presence_present']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//span[@name='presence_to_define']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
</odoo>
<?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.presence.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[@name='hr_presence_options']" position="inside">
<div class="row">
<field name="hr_presence_control_attendance" class="col-lg-1 ml16"/>
<label for="hr_presence_control_attendance" class="col-lg-10 o_light_label"/>
</div>
</xpath>
</field>
</record>
</odoo>
......@@ -93,6 +93,11 @@ class HrEmployeeBase(models.AbstractModel):
for employee in self:
employee.allocation_used_count = employee.allocation_count - employee.remaining_leaves
def _compute_presence_state(self):
super()._compute_presence_state()
employees = self.filtered(lambda employee: employee.hr_presence_state != 'present' and employee.is_absent)
employees.update({'hr_presence_state': 'absent'})
@api.multi
def _compute_leave_status(self):
# Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment