[IMP] hr: Introduce the public employee profile
Purpose ======= 1/ Robustness & security: right now it is not easy to understand and do something clean in term of security (hr people vs employees, private info vs public). A HR officer doesn't know if he can write something on the chatter. Currently, a note will be visible for all the employees who have access to the employee form view for example. 2/ In term of business, it makes sense to let a hr manages payroll stuff (contract, employees private information, ... and other employee see public information (résumé and work information) Specification ============= Introduce 2 new models: - hr.employee.base (AbstractModel): This represents the basic skeleton model on which the shared fields and methods between the public and the private employees models. - hr.employee.public (_auto=False): This is a sql view based on the employee values, readable for an internal user (i.e. an employee). The model hr.employee is not readable anymore for an employee. There are now 3 ways to access the employee data: 1/ From the hr.employee views. HR officer access rights are required 2/ From the public profile. The public data for an employee are accessible but can't be modified. 3/ From the 'My Profile' menu. A classic employee can access its own data from there, and can modify them.
This commit is contained in:
committed by
Yannick Tivisse
parent
b2b26f1ff7
commit
c9ca376146
@@ -29,6 +29,7 @@
|
||||
'views/hr_job_views.xml',
|
||||
'views/hr_plan_views.xml',
|
||||
'views/hr_employee_category_views.xml',
|
||||
'views/hr_employee_public_views.xml',
|
||||
'views/hr_employee_views.xml',
|
||||
'views/hr_department_views.xml',
|
||||
'views/hr_views.xml',
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_employee_base
|
||||
from . import hr_employee
|
||||
from . import hr_employee_category
|
||||
from . import hr_employee_public
|
||||
from . import hr_department
|
||||
from . import hr_job
|
||||
from . import hr_plan
|
||||
|
||||
@@ -12,11 +12,17 @@ from odoo.modules.module import get_module_resource
|
||||
|
||||
|
||||
class HrEmployeePrivate(models.Model):
|
||||
"""
|
||||
NB: Any field only available on the model hr.employee (i.e. not on the
|
||||
hr.employee.public model) should have `groups="hr.group_hr_user"` on its
|
||||
definition to avoid being prefetched when the user hasn't access to the
|
||||
hr.employee model. Indeed, the prefetch loads the data for all the fields
|
||||
that are available according to the group defined on them.
|
||||
"""
|
||||
_name = "hr.employee"
|
||||
_description = "Employee"
|
||||
_order = 'name'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin', 'resource.mixin']
|
||||
|
||||
_inherit = ['hr.employee.base', 'mail.thread', 'mail.activity.mixin', 'resource.mixin']
|
||||
_mail_post_access = 'read'
|
||||
|
||||
@api.model
|
||||
@@ -83,7 +89,6 @@ class HrEmployeePrivate(models.Model):
|
||||
emergency_phone = fields.Char("Emergency Phone", groups="hr.group_hr_user", tracking=True)
|
||||
km_home_work = fields.Integer(string="Km home-work", groups="hr.group_hr_user", tracking=True)
|
||||
google_drive_link = fields.Char(string="Employee Documents", groups="hr.group_hr_user", tracking=True)
|
||||
job_title = fields.Char("Job Title")
|
||||
|
||||
# image: all image fields are base64 encoded and PIL-supported
|
||||
image = fields.Binary(
|
||||
@@ -99,17 +104,8 @@ class HrEmployeePrivate(models.Model):
|
||||
help="Small-sized photo of the employee. It is automatically "
|
||||
"resized as a 64x64px image, with aspect ratio preserved. "
|
||||
"Use this field anywhere a small image is required.")
|
||||
# work
|
||||
address_id = fields.Many2one(
|
||||
'res.partner', 'Work Address')
|
||||
work_phone = fields.Char('Work Phone')
|
||||
mobile_phone = fields.Char('Work Mobile')
|
||||
phone = fields.Char(related='address_home_id.phone', related_sudo=False, string="Private Phone", groups="hr.group_hr_user")
|
||||
work_email = fields.Char('Work Email')
|
||||
work_location = fields.Char('Work Location')
|
||||
# employee in company
|
||||
job_id = fields.Many2one('hr.job', 'Job Position')
|
||||
department_id = fields.Many2one('hr.department', 'Department')
|
||||
parent_id = fields.Many2one('hr.employee', 'Manager')
|
||||
child_ids = fields.One2many('hr.employee', 'parent_id', string='Direct subordinates')
|
||||
coach_id = fields.Many2one('hr.employee', 'Coach')
|
||||
@@ -118,22 +114,80 @@ class HrEmployeePrivate(models.Model):
|
||||
'emp_id', 'category_id',
|
||||
string='Tags')
|
||||
# misc
|
||||
notes = fields.Text('Notes')
|
||||
color = fields.Integer('Color Index', default=0)
|
||||
barcode = fields.Char(string="Badge ID", help="ID used for employee identification.", copy=False)
|
||||
pin = fields.Char(string="PIN", help="PIN used to Check In/Out in Kiosk Mode (if enabled in Configuration).", copy=False)
|
||||
notes = fields.Text('Notes', groups="hr.group_hr_user")
|
||||
color = fields.Integer('Color Index', default=0, groups="hr.group_hr_user")
|
||||
barcode = fields.Char(string="Badge ID", help="ID used for employee identification.", groups="hr.group_hr_user", copy=False)
|
||||
pin = fields.Char(string="PIN", groups="hr.group_hr_user", copy=False,
|
||||
help="PIN used to Check In/Out in Kiosk Mode (if enabled in Configuration).")
|
||||
departure_reason = fields.Selection([
|
||||
('fired', 'Fired'),
|
||||
('resigned', 'Resigned'),
|
||||
('retired', 'Retired')
|
||||
], string="Departure Reason", copy=False, tracking=True)
|
||||
departure_description = fields.Text(string="Additional Information", copy=False, tracking=True)
|
||||
], string="Departure Reason", groups="hr.group_hr_user", copy=False, tracking=True)
|
||||
departure_description = fields.Text(string="Additional Information", groups="hr.group_hr_user", copy=False, tracking=True)
|
||||
message_main_attachment_id = fields.Many2one(groups="hr.group_hr_user")
|
||||
|
||||
_sql_constraints = [
|
||||
('barcode_uniq', 'unique (barcode)', "The Badge ID must be unique, this one is already assigned to another employee."),
|
||||
('user_uniq', 'unique (user_id, company_id)', "A user cannot be linked to multiple employees in the same company.")
|
||||
]
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
if self.check_access_rights('read', raise_exception=False):
|
||||
return super(HrEmployeePrivate, self).name_get()
|
||||
return self.env['hr.employee.public'].browse(self.ids).name_get()
|
||||
|
||||
@api.multi
|
||||
def read(self, fields, load='_classic_read'):
|
||||
if self.check_access_rights('read', raise_exception=False):
|
||||
return super(HrEmployeePrivate, self).read(fields, load=load)
|
||||
private_fields = set(fields).difference(self.env['hr.employee.public']._fields.keys())
|
||||
if private_fields:
|
||||
raise AccessError(_('The fields "%s" you try to read is not available on the public employee profile.') % (','.join(private_fields)))
|
||||
return self.env['hr.employee.public'].browse(self.ids).read(fields, load=load)
|
||||
|
||||
@api.model
|
||||
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
|
||||
"""
|
||||
We override the _search because it is the method that checks the access rights
|
||||
This is correct to override the _search. That way we enforce the fact that calling
|
||||
search on an hr.employee returns a hr.employee recordset, even if you don't have access
|
||||
to this model, as the result of _search (the ids of the public employees) is to be
|
||||
browsed on the hr.employee model. This can be trusted as the ids of the public
|
||||
employees exactly match the ids of the related hr.employee.
|
||||
"""
|
||||
if self.check_access_rights('read', raise_exception=False):
|
||||
return super(HrEmployeePrivate, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
|
||||
return self.env['hr.employee.public']._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
|
||||
|
||||
@api.multi
|
||||
def get_formview_id(self, access_uid=None):
|
||||
""" Override this method in order to redirect many2one towards the right model depending on access_uid """
|
||||
if access_uid:
|
||||
self_sudo = self.sudo(access_uid)
|
||||
else:
|
||||
self_sudo = self
|
||||
|
||||
if self_sudo.check_access_rights('read', raise_exception=False):
|
||||
return super(HrEmployeePrivate, self).get_formview_id(access_uid=access_uid)
|
||||
# Hardcode the form view for public employee
|
||||
return self.env.ref('hr.hr_employee_public_view_form').id
|
||||
|
||||
@api.multi
|
||||
def get_formview_action(self, access_uid=None):
|
||||
""" Override this method in order to redirect many2one towards the right model depending on access_uid """
|
||||
res = super(HrEmployeePrivate, self).get_formview_action(access_uid=access_uid)
|
||||
if access_uid:
|
||||
self_sudo = self.sudo(access_uid)
|
||||
else:
|
||||
self_sudo = self
|
||||
|
||||
if not self_sudo.check_access_rights('read', raise_exception=False):
|
||||
res['res_model'] = 'hr.employee.public'
|
||||
|
||||
return res
|
||||
|
||||
@api.constrains('pin')
|
||||
def _verify_pin(self):
|
||||
for employee in self:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrEmployeeBase(models.AbstractModel):
|
||||
_name = "hr.employee.base"
|
||||
_description = "Basic Employee"
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char()
|
||||
active = fields.Boolean("Active")
|
||||
department_id = fields.Many2one('hr.department', 'Department')
|
||||
job_id = fields.Many2one('hr.job', 'Job Position')
|
||||
job_title = fields.Char("Job Title")
|
||||
company_id = fields.Many2one('res.company', 'Company')
|
||||
address_id = fields.Many2one('res.partner', 'Work Address')
|
||||
work_phone = fields.Char('Work Phone')
|
||||
mobile_phone = fields.Char('Work Mobile')
|
||||
work_email = fields.Char('Work Email')
|
||||
work_location = fields.Char('Work Location')
|
||||
user_id = fields.Many2one('res.users')
|
||||
resource_id = fields.Many2one('resource.resource')
|
||||
resource_calendar_id = fields.Many2one('resource.calendar')
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, tools
|
||||
|
||||
|
||||
class HrEmployeePublic(models.Model):
|
||||
_name = "hr.employee.public"
|
||||
_inherit = ["hr.employee.base"]
|
||||
_description = 'Public Employee'
|
||||
_order = 'name'
|
||||
_auto = False
|
||||
_log_access = True # Include magic fields
|
||||
|
||||
# Fields coming from hr.employee.base
|
||||
create_date = fields.Datetime(readonly=True)
|
||||
name = fields.Char(readonly=True)
|
||||
active = fields.Boolean(readonly=True)
|
||||
department_id = fields.Many2one(readonly=True)
|
||||
job_id = fields.Many2one(readonly=True)
|
||||
job_title = fields.Char(readonly=True)
|
||||
company_id = fields.Many2one(readonly=True)
|
||||
address_id = fields.Many2one(readonly=True)
|
||||
mobile_phone = fields.Char(readonly=True)
|
||||
work_phone = fields.Char(readonly=True)
|
||||
work_email = fields.Char(readonly=True)
|
||||
work_location = fields.Char(readonly=True)
|
||||
user_id = fields.Many2one(readonly=True)
|
||||
resource_id = fields.Many2one(readonly=True)
|
||||
resource_calendar_id = fields.Many2one(readonly=True)
|
||||
|
||||
# hr.employee.public specific fields
|
||||
child_ids = fields.One2many('hr.employee.public', 'parent_id', string='Direct subordinates', readonly=True)
|
||||
image = fields.Binary("Photo", compute='_compute_image', readonly=True, compute_sudo=True)
|
||||
image_medium = fields.Binary("Medium-sized photo", compute='_compute_image', readonly=True, compute_sudo=True)
|
||||
image_small = fields.Binary("Small-sized photo", compute='_compute_image', readonly=True, compute_sudo=True)
|
||||
parent_id = fields.Many2one('hr.employee.public', 'Manager', readonly=True)
|
||||
coach_id = fields.Many2one('hr.employee.public', 'Coach', readonly=True)
|
||||
|
||||
def _compute_image(self):
|
||||
for employee in self:
|
||||
# We have to be in sudo to have access to the images
|
||||
employee_id = self.sudo().env['hr.employee'].browse(employee.id)
|
||||
employee.image = employee_id.image
|
||||
employee.image_medium = employee_id.image_medium
|
||||
employee.image_small = employee_id.image_small
|
||||
|
||||
@api.model
|
||||
def _get_fields(self):
|
||||
return ','.join('emp.%s' % name for name, field in self._fields.items() if field.store and field.type not in ['many2many', 'one2many'])
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self.env.cr, self._table)
|
||||
self.env.cr.execute("""CREATE or REPLACE VIEW %s as (
|
||||
SELECT
|
||||
%s
|
||||
FROM hr_employee emp
|
||||
)""" % (self._table, self._get_fields()))
|
||||
@@ -40,6 +40,13 @@
|
||||
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_public_comp_rule" model="ir.rule">
|
||||
<field name="name">Employee multi company rule</field>
|
||||
<field name="model_id" ref="model_hr_employee_public"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_job_comp_rule" model="ir.rule">
|
||||
<field name="name">Job multi company rule</field>
|
||||
<field name="model_id" ref="model_hr_job"/>
|
||||
|
||||
@@ -2,7 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_employee_category_user,hr.employee.category.user,model_hr_employee_category,group_hr_user,1,1,1,1
|
||||
access_hr_employee_category_emp,hr.employee.category.emp,model_hr_employee_category,base.group_user,1,0,0,0
|
||||
access_hr_employee_user,hr.employee user,model_hr_employee,group_hr_user,1,1,1,1
|
||||
access_hr_employee_system_user,hr.employee system user,model_hr_employee,base.group_user,1,0,0,0
|
||||
access_hr_employee_system_user,hr.employee system user,model_hr_employee,base.group_user,0,0,0,0
|
||||
access_hr_employee_public_user,hr.employee_public,model_hr_employee_public,base.group_user,1,0,0,0
|
||||
access_hr_employee_resource_user,resource.resource.user,resource.model_resource_resource,group_hr_user,1,1,1,1
|
||||
access_hr_department_user,hr.department.user,model_hr_department,group_hr_user,1,1,1,1
|
||||
access_hr_department_employee,hr.department.employee,model_hr_department,base.group_user,1,0,0,0
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from odoo.addons.hr.tests.common import TestHrCommon
|
||||
from odoo.tests import new_test_user
|
||||
from odoo.exceptions import AccessError
|
||||
from datetime import date
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
class TestSelfAccessRights(TestHrCommon):
|
||||
@@ -26,6 +26,8 @@ class TestSelfAccessRights(TestHrCommon):
|
||||
})
|
||||
|
||||
self.protected_fields_emp = OrderedDict([(k, v) for k, v in self.env['hr.employee']._fields.items() if v.groups == 'hr.group_hr_user'])
|
||||
# Compute fields and id field are always readable by everyone
|
||||
self.read_protected_fields_emp = OrderedDict([(k, v) for k, v in self.env['hr.employee']._fields.items() if not v.compute and k != 'id'])
|
||||
self.self_protected_fields_user = OrderedDict([
|
||||
(k, v)
|
||||
for k, v in self.env['res.users']._fields.items()
|
||||
@@ -34,14 +36,12 @@ class TestSelfAccessRights(TestHrCommon):
|
||||
|
||||
# Read hr.employee #
|
||||
def testReadSelfEmployee(self):
|
||||
for f in self.protected_fields_emp:
|
||||
with self.assertRaises(AccessError):
|
||||
self.richard_emp.sudo(self.richard)[f]
|
||||
with self.assertRaises(AccessError):
|
||||
self.hubert_emp.sudo(self.richard).read(self.protected_fields_emp.keys())
|
||||
|
||||
def testReadOtherEmployee(self):
|
||||
for f in self.protected_fields_emp:
|
||||
with self.assertRaises(AccessError):
|
||||
self.hubert_emp.sudo(self.richard)[f]
|
||||
with self.assertRaises(AccessError):
|
||||
self.hubert_emp.sudo(self.richard).read(self.protected_fields_emp.keys())
|
||||
|
||||
# Write hr.employee #
|
||||
def testWriteSelfEmployee(self):
|
||||
@@ -60,10 +60,8 @@ class TestSelfAccessRights(TestHrCommon):
|
||||
self.richard.sudo(self.richard).read([f]) # should not raise
|
||||
|
||||
def testReadOtherUserEmployee(self):
|
||||
|
||||
for f in self.self_protected_fields_user:
|
||||
with self.assertRaises(AccessError, msg="Field %s should not be readable by other usrs" % f):
|
||||
self.hubert.sudo(self.richard)[f]
|
||||
with self.assertRaises(AccessError):
|
||||
self.hubert.sudo(self.richard).read(self.self_protected_fields_user)
|
||||
|
||||
# Write res.users #
|
||||
def testWriteSelfUserEmployeeSettingFalse(self):
|
||||
@@ -76,7 +74,7 @@ class TestSelfAccessRights(TestHrCommon):
|
||||
for f, v in self.self_protected_fields_user.items():
|
||||
val = None
|
||||
if v.type == 'char' or v.type == 'text':
|
||||
val = 'dummy'
|
||||
val = '0000' if f == 'pin' else 'dummy'
|
||||
if val is not None:
|
||||
self.richard.sudo(self.richard).write({f: val})
|
||||
|
||||
@@ -116,7 +114,6 @@ class TestSelfAccessRights(TestHrCommon):
|
||||
self.richard.sudo(self.richard).write({'phone': '2154545'})
|
||||
|
||||
def testWriteOtherUserEmployee(self):
|
||||
|
||||
for f in self.self_protected_fields_user:
|
||||
with self.assertRaises(AccessError):
|
||||
self.hubert.sudo(self.richard).write({f: 'dummy'})
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="hr_employee_public_view_search" model="ir.ui.view">
|
||||
<field name="name">hr.employee.search</field>
|
||||
<field name="model">hr.employee.public</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Employees">
|
||||
<field name="name" string="Employees" filter_domain="['|',('work_email','ilike',self),('name','ilike',self)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_public_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.employee.public.form</field>
|
||||
<field name="model">hr.employee.public</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Employee" create="0" write="0">
|
||||
<sheet>
|
||||
<field name="user_id" invisible="1"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<!-- Used by other modules-->
|
||||
</div>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"preview_image":"image_medium"}'/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Employee's Name" required="True"/>
|
||||
</h1>
|
||||
<h2>
|
||||
<field name="job_title" placeholder="Job Title" />
|
||||
</h2>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="mobile_phone" widget="phone"/>
|
||||
<field name="work_phone" widget="phone"/>
|
||||
<field name="work_email" widget="email"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="department_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="public" string="Work Information">
|
||||
<div id="o_work_employee_container"> <!-- These two div are used to position org_chart -->
|
||||
<div id="o_work_employee_main">
|
||||
<group string="Location">
|
||||
<field name="address_id"
|
||||
context="{'show_address': 1}"
|
||||
options='{"always_reload": True, "highlight_first_line": True}'/>
|
||||
<field name="work_location"/>
|
||||
</group>
|
||||
<group name="managers" string="Managers">
|
||||
<field name="parent_id"/>
|
||||
<field name="coach_id"/>
|
||||
</group>
|
||||
<group string="Schedule" groups="base.group_no_one">
|
||||
<field name="resource_calendar_id"/>
|
||||
</group>
|
||||
</div>
|
||||
</div>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_public_view_tree" model="ir.ui.view">
|
||||
<field name="name">hr.employee.tree</field>
|
||||
<field name="model">hr.employee.public</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Employees">
|
||||
<field name="name"/>
|
||||
<field name="work_phone"/>
|
||||
<field name="work_email"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="coach_id" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_public_view_kanban" model="ir.ui.view">
|
||||
<field name="name">hr.employee.kanban</field>
|
||||
<field name="model">hr.employee.public</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_hr_employee_kanban">
|
||||
<field name="id"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
|
||||
<!-- Medium employee's picture -->
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image: url(#{kanban_image('hr.employee.public', 'image_medium', record.id.raw_value)})" role="img"/>
|
||||
<!-- Employee's picture optimized for mobile -->
|
||||
<div class="o_kanban_image d-md-none rounded-circle" t-attf-style="background-image: url(#{kanban_image('hr.employee.public', 'image_small', record.id.raw_value)})" role="img"/>
|
||||
|
||||
<div class="oe_kanban_details">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<strong class="o_kanban_record_title">
|
||||
<field name="name"/>
|
||||
</strong>
|
||||
<span t-if="record.job_id.raw_value" class="o_kanban_record_subtitle"><field name="job_id"/></span>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li id="last_login"/>
|
||||
<li t-if="record.work_location.raw_value"><field name="work_location"/></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
<searchpanel>
|
||||
<field name="company_id" groups="base.group_multi_company" icon="fa-building"/>
|
||||
<field name="department_id" icon="fa-users"/>
|
||||
</searchpanel>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_public_action" model="ir.actions.act_window">
|
||||
<field name="name">Employees</field>
|
||||
<field name="res_model">hr.employee.public</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="view_id" eval="False"/>
|
||||
<field name="search_view_id" ref="hr_employee_public_view_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Add a new employee
|
||||
</p><p>
|
||||
With just a quick glance on the Odoo employee screen, you
|
||||
can easily find all the information you need for each person;
|
||||
contact data, job position, availability, etc.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -15,6 +15,27 @@
|
||||
parent="menu_hr_root"
|
||||
sequence="0"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_hr_employee_payroll"
|
||||
name="Payroll"
|
||||
parent="menu_hr_root"
|
||||
groups="group_hr_user"
|
||||
sequence="3"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_hr_employee_user"
|
||||
name="Employees"
|
||||
action="open_view_employee_list_my"
|
||||
parent="menu_hr_employee_payroll"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_hr_employee"
|
||||
name="Company Address"
|
||||
action="hr_employee_public_action"
|
||||
parent="menu_hr_root"
|
||||
sequence="4"/>
|
||||
|
||||
<menuitem
|
||||
id="hr_menu_hr_reports"
|
||||
name="Reporting"
|
||||
@@ -22,25 +43,11 @@
|
||||
sequence="95"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_open_view_employee_list_my"
|
||||
action="open_view_employee_list_my"
|
||||
parent="menu_hr_root"
|
||||
sequence="3"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_hr_department_tree"
|
||||
action="open_module_tree_department"
|
||||
parent="menu_hr_root"
|
||||
sequence="90"
|
||||
groups="group_hr_user"/>
|
||||
|
||||
<!-- RLI TODO Rename this, it has nothing to do with timesheet -->
|
||||
<menuitem
|
||||
id="menu_hr_reporting_timesheet"
|
||||
name="Reporting"
|
||||
parent="menu_hr_root"
|
||||
groups="group_hr_manager,group_hr_user"
|
||||
sequence="99"/>
|
||||
id="menu_hr_reporting_timesheet"
|
||||
name="Reporting"
|
||||
parent="menu_hr_root"
|
||||
groups="group_hr_manager,group_hr_user"
|
||||
sequence="99"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_human_resources_configuration"
|
||||
@@ -64,6 +71,13 @@
|
||||
groups="base.group_no_one"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_hr_department_tree"
|
||||
action="open_module_tree_department"
|
||||
parent="menu_human_resources_configuration"
|
||||
sequence="2"
|
||||
groups="group_hr_user"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_config_plan"
|
||||
name="Activity Planning"
|
||||
|
||||
Reference in New Issue
Block a user