diff options
| author | RomainLibert <rli@odoo.com> | 2019-04-08 11:41:13 +0200 |
|---|---|---|
| committer | Yannick Tivisse <yti@odoo.com> | 2019-05-31 10:21:20 +0200 |
| commit | c9ca3761464413327d2beb697553a3ccd7eef4d1 (patch) | |
| tree | 74b3bf1db6dcb5eb4c75ce0378a08d08075920ef | |
| parent | b2b26f1ff7e4dcf7d0c0ccc36b249045372329c7 (diff) | |
[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.
| -rw-r--r-- | addons/hr/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/hr/models/__init__.py | 2 | ||||
| -rw-r--r-- | addons/hr/models/hr_employee.py | 90 | ||||
| -rw-r--r-- | addons/hr/models/hr_employee_base.py | 25 | ||||
| -rw-r--r-- | addons/hr/models/hr_employee_public.py | 59 | ||||
| -rw-r--r-- | addons/hr/security/hr_security.xml | 7 | ||||
| -rw-r--r-- | addons/hr/security/ir.model.access.csv | 3 | ||||
| -rw-r--r-- | addons/hr/tests/test_self_user_access.py | 25 | ||||
| -rw-r--r-- | addons/hr/views/hr_employee_public_views.xml | 151 | ||||
| -rw-r--r-- | addons/hr/views/hr_views.xml | 46 |
10 files changed, 360 insertions, 49 deletions
diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py index c38700c0404..bcbd2fafdf9 100644 --- a/addons/hr/__manifest__.py +++ b/addons/hr/__manifest__.py @@ -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', diff --git a/addons/hr/models/__init__.py b/addons/hr/models/__init__.py index dc940941bd1..efe3c1d702c 100644 --- a/addons/hr/models/__init__.py +++ b/addons/hr/models/__init__.py @@ -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 diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index 8f9078b7292..1764b1e0550 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -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: diff --git a/addons/hr/models/hr_employee_base.py b/addons/hr/models/hr_employee_base.py new file mode 100644 index 00000000000..fec003ccfc6 --- /dev/null +++ b/addons/hr/models/hr_employee_base.py @@ -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') diff --git a/addons/hr/models/hr_employee_public.py b/addons/hr/models/hr_employee_public.py new file mode 100644 index 00000000000..82799661d0e --- /dev/null +++ b/addons/hr/models/hr_employee_public.py @@ -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())) diff --git a/addons/hr/security/hr_security.xml b/addons/hr/security/hr_security.xml index c735d05b1ce..bbafaf7fb38 100644 --- a/addons/hr/security/hr_security.xml +++ b/addons/hr/security/hr_security.xml @@ -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"/> diff --git a/addons/hr/security/ir.model.access.csv b/addons/hr/security/ir.model.access.csv index 345374b31fe..e28b4065ee3 100644 --- a/addons/hr/security/ir.model.access.csv +++ b/addons/hr/security/ir.model.access.csv @@ -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
diff --git a/addons/hr/tests/test_self_user_access.py b/addons/hr/tests/test_self_user_access.py index 360cf4f5a1e..ab732f9f9e6 100644 --- a/addons/hr/tests/test_self_user_access.py +++ b/addons/hr/tests/test_self_user_access.py @@ -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'}) diff --git a/addons/hr/views/hr_employee_public_views.xml b/addons/hr/views/hr_employee_public_views.xml new file mode 100644 index 00000000000..6b7430c21f0 --- /dev/null +++ b/addons/hr/views/hr_employee_public_views.xml @@ -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> diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml index da423b9adb7..3a5c490ec70 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -16,31 +16,38 @@ sequence="0"/> <menuitem - id="hr_menu_hr_reports" - name="Reporting" - parent="menu_hr_root" - sequence="95"/> - - <menuitem - id="menu_open_view_employee_list_my" - action="open_view_employee_list_my" + 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_department_tree" - action="open_module_tree_department" + id="menu_hr_employee" + name="Company Address" + action="hr_employee_public_action" parent="menu_hr_root" - sequence="90" - groups="group_hr_user"/> + sequence="4"/> - <!-- RLI TODO Rename this, it has nothing to do with timesheet --> <menuitem - id="menu_hr_reporting_timesheet" + id="hr_menu_hr_reports" name="Reporting" parent="menu_hr_root" - groups="group_hr_manager,group_hr_user" - sequence="99"/> + sequence="95"/> + + <menuitem + 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" @@ -65,6 +72,13 @@ 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" parent="menu_human_resources_configuration" |
