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 + + + + + +
+ +