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 @@
['|',('company_id','=',False),('company_id', 'in', company_ids)]
+
+ Employee multi company rule
+
+
+ ['|',('company_id','=',False),('company_id', 'in', company_ids)]
+
+
Job multi company rule
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 @@
+
+
+
+
+
+ hr.employee.search
+ hr.employee.public
+
+
+
+
+
+
+
+
+ hr.employee.public.form
+ hr.employee.public
+
+
+
+
+
+
+ hr.employee.tree
+ hr.employee.public
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.employee.kanban
+ hr.employee.public
+ 10
+
+
+
+
+
+
+ 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.
+