diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py index 80c31263acd..edac09aa6f5 100644 --- a/addons/hr/__manifest__.py +++ b/addons/hr/__manifest__.py @@ -30,6 +30,7 @@ 'views/hr_templates.xml', 'views/res_config_settings_views.xml', 'views/mail_channel_views.xml', + 'views/res_users.xml', 'data/hr_data.xml', 'report/hr_employee_badge.xml' ], @@ -39,5 +40,5 @@ 'installable': True, 'application': True, 'auto_install': False, - 'qweb': [], + 'qweb': ['static/src/xml/hr_templates.xml'], } diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index 665893c76a4..209235ff42b 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -6,7 +6,7 @@ import base64 import logging from werkzeug import url_encode -from odoo import api, fields, models +from odoo import api, fields, models, SUPERUSER_ID from odoo import tools, _ from odoo.exceptions import ValidationError, AccessError from odoo.modules.module import get_module_resource @@ -110,61 +110,63 @@ class Employee(models.Model): # resource and user # required on the resource, make sure required="True" set in the view - name = fields.Char(related='resource_id.name', store=True, oldname='name_related', readonly=False) + name = fields.Char(related='resource_id.name', store=True, oldname='name_related', readonly=False, tracking=True) user_id = fields.Many2one('res.users', 'User', related='resource_id.user_id', store=True, readonly=False) + user_partner_id = fields.Many2one(related='user_id.partner_id', related_sudo=False, string="User's partner") active = fields.Boolean('Active', related='resource_id.active', default=True, store=True, readonly=False) # private partner address_home_id = fields.Many2one( 'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.', - groups="hr.group_hr_user") + groups="hr.group_hr_user", tracking=True) is_address_home_a_company = fields.Boolean( 'The employee adress has a company linked', compute='_compute_is_address_home_a_company', ) country_id = fields.Many2one( - 'res.country', 'Nationality (Country)', groups="hr.group_hr_user") + 'res.country', 'Nationality (Country)', groups="hr.group_hr_user", tracking=True) gender = fields.Selection([ ('male', 'Male'), ('female', 'Female'), ('other', 'Other') - ], groups="hr.group_hr_user", default="male") + ], groups="hr.group_hr_user", default="male", tracking=True) marital = fields.Selection([ ('single', 'Single'), ('married', 'Married'), ('cohabitant', 'Legal Cohabitant'), ('widower', 'Widower'), ('divorced', 'Divorced') - ], string='Marital Status', groups="hr.group_hr_user", default='single') - spouse_complete_name = fields.Char(string="Spouse Complete Name", groups="hr.group_hr_user") - spouse_birthdate = fields.Date(string="Spouse Birthdate", groups="hr.group_hr_user") - children = fields.Integer(string='Number of Children', groups="hr.group_hr_user") - place_of_birth = fields.Char('Place of Birth', groups="hr.group_hr_user") - country_of_birth = fields.Many2one('res.country', string="Country of Birth", groups="hr.group_hr_user") - birthday = fields.Date('Date of Birth', groups="hr.group_hr_user") - ssnid = fields.Char('SSN No', help='Social Security Number', groups="hr.group_hr_user") - sinid = fields.Char('SIN No', help='Social Insurance Number', groups="hr.group_hr_user") - identification_id = fields.Char(string='Identification No', groups="hr.group_hr_user") - passport_id = fields.Char('Passport No', groups="hr.group_hr_user") + ], string='Marital Status', groups="hr.group_hr_user", default='single', tracking=True) + spouse_complete_name = fields.Char(string="Spouse Complete Name", groups="hr.group_hr_user", tracking=True) + spouse_birthdate = fields.Date(string="Spouse Birthdate", groups="hr.group_hr_user", tracking=True) + children = fields.Integer(string='Number of Children', groups="hr.group_hr_user", tracking=True) + place_of_birth = fields.Char('Place of Birth', groups="hr.group_hr_user", tracking=True) + country_of_birth = fields.Many2one('res.country', string="Country of Birth", groups="hr.group_hr_user", tracking=True) + birthday = fields.Date('Date of Birth', groups="hr.group_hr_user", tracking=True) + ssnid = fields.Char('SSN No', help='Social Security Number', groups="hr.group_hr_user", tracking=True) + sinid = fields.Char('SIN No', help='Social Insurance Number', groups="hr.group_hr_user", tracking=True) + identification_id = fields.Char(string='Identification No', groups="hr.group_hr_user", tracking=True) + passport_id = fields.Char('Passport No', groups="hr.group_hr_user", tracking=True) bank_account_id = fields.Many2one( 'res.partner.bank', 'Bank Account Number', domain="[('partner_id', '=', address_home_id)]", groups="hr.group_hr_user", + tracking=True, help='Employee bank salary account') - permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user") - visa_no = fields.Char('Visa No', groups="hr.group_hr_user") - visa_expire = fields.Date('Visa Expire Date', groups="hr.group_hr_user") - additional_note = fields.Text(string='Additional Note', groups="hr.group_hr_user") + permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user", tracking=True) + visa_no = fields.Char('Visa No', groups="hr.group_hr_user", tracking=True) + visa_expire = fields.Date('Visa Expire Date', groups="hr.group_hr_user", tracking=True) + additional_note = fields.Text(string='Additional Note', groups="hr.group_hr_user", tracking=True) certificate = fields.Selection([ ('bachelor', 'Bachelor'), ('master', 'Master'), ('other', 'Other'), - ], 'Certificate Level', default='master', groups="hr.group_hr_user") - study_field = fields.Char("Field of Study", placeholder='Computer Science', groups="hr.group_hr_user") - study_school = fields.Char("School", groups="hr.group_hr_user") - emergency_contact = fields.Char("Emergency Contact", groups="hr.group_hr_user") - emergency_phone = fields.Char("Emergency Phone", groups="hr.group_hr_user") - km_home_work = fields.Integer(string="Km home-work", groups="hr.group_hr_user") - google_drive_link = fields.Char(string="Employee Documents", groups="hr.group_hr_user") + ], 'Certificate Level', default='master', groups="hr.group_hr_user", tracking=True) + study_field = fields.Char("Field of Study", placeholder='Computer Science', groups="hr.group_hr_user", tracking=True) + study_school = fields.Char("School", groups="hr.group_hr_user", tracking=True) + emergency_contact = fields.Char("Emergency Contact", groups="hr.group_hr_user", tracking=True) + 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 @@ -186,6 +188,7 @@ class Employee(models.Model): '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 @@ -210,7 +213,10 @@ class Employee(models.Model): ], string="Departure Reason") departure_description = fields.Text(string="Additional Information") - _sql_constraints = [('barcode_uniq', 'unique (barcode)', "The Badge ID must be unique, this one is already assigned to another employee.")] + _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.constrains('pin') def _verify_pin(self): @@ -341,6 +347,27 @@ class Employee(models.Model): 'template': '/hr/static/xls/hr_employee.xls' }] + def _post_author(self): + """ + When a user updates his own employee's data, all operations are performed + by super user. However, tracking messages should not be posted as OdooBot + but as the actual user. + This method is used in the overrides of `_message_log` and `message_post` + to post messages as the correct user. + """ + real_user = self.env.context.get('binary_field_real_user') + if self.env.user.id == SUPERUSER_ID and real_user: + self = self.sudo(real_user) + return self + + def _message_log(self, body='', subject=False, message_type='notification', **kwargs): + return super(Employee, self._post_author()).message_post(body=body, subject=subject, message_type=message_type, **kwargs) + + @api.multi + @api.returns('mail.message', lambda value: value.id) + def message_post(self, **kwargs): + return super(Employee, self._post_author()).message_post(**kwargs) + class Department(models.Model): _name = "hr.department" diff --git a/addons/hr/models/res_config_settings.py b/addons/hr/models/res_config_settings.py index ea96590434c..2b7f75b855a 100644 --- a/addons/hr/models/res_config_settings.py +++ b/addons/hr/models/res_config_settings.py @@ -14,3 +14,4 @@ class ResConfigSettings(models.TransientModel): 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_employee_self_edit = fields.Boolean(string="Employee Edition", config_parameter='hr.hr_employee_self_edit') diff --git a/addons/hr/models/res_partner.py b/addons/hr/models/res_partner.py index 6b77f3f3371..9258c1ac5da 100644 --- a/addons/hr/models/res_partner.py +++ b/addons/hr/models/res_partner.py @@ -22,3 +22,17 @@ class Partner(models.Model): return suggestions except AccessError: return suggestions + + @api.multi + def name_get(self): + """ Override to allow an employee to see its private address in his profile. + This avoids to relax access rules on `res.parter` and to add an `ir.rule`. + (advantage in both security and performance). + Use a try/except instead of systematically checking to minimize the impact on performance. + """ + try: + return super(Partner, self).name_get() + except AccessError as e: + if len(self) == 1 and self in self.env.user.employee_ids.mapped('address_home_id'): + return super(Partner, self.sudo()).name_get() + raise e diff --git a/addons/hr/models/res_users.py b/addons/hr/models/res_users.py index be6b5148eba..8398b543776 100644 --- a/addons/hr/models/res_users.py +++ b/addons/hr/models/res_users.py @@ -1,21 +1,160 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models, fields +from odoo import api, models, fields, _ +from odoo.exceptions import AccessError class User(models.Model): _inherit = ['res.users'] - employee_ids = fields.One2many('hr.employee', 'user_id', string='Related employees') + # note: a user can only be linked to one employee per company (see sql constraint in ´hr.employee´) + employee_ids = fields.One2many('hr.employee', 'user_id', string='Related employee') + employee_id = fields.Many2one('hr.employee', string="Company employee", compute='_compute_company_employee', store=True) + + job_title = fields.Char(related='employee_id.job_title', readonly=False) + work_phone = fields.Char(related='employee_id.work_phone', readonly=False) + mobile_phone = fields.Char(related='employee_id.mobile_phone', readonly=False) + phone = fields.Char(related='employee_id.phone', readonly=False, related_sudo=False) + work_email = fields.Char(related='employee_id.work_email', readonly=False, related_sudo=False) + category_ids = fields.Many2many(related='employee_id.category_ids', string="Employee Tags", readonly=False, related_sudo=False) + department_id = fields.Many2one(related='employee_id.department_id', readonly=False, related_sudo=False) + address_id = fields.Many2one(related='employee_id.address_id', readonly=False, related_sudo=False) + work_location = fields.Char(related='employee_id.work_location', readonly=False, related_sudo=False) + parent_id = fields.Many2one(related='employee_id.parent_id', related_sudo=False) + coach_id = fields.Many2one(related='employee_id.coach_id', readonly=False, related_sudo=False) + address_home_id = fields.Many2one(related='employee_id.address_home_id', readonly=False, related_sudo=False) + is_address_home_a_company = fields.Boolean(related='employee_id.is_address_home_a_company', readonly=False, related_sudo=False) + km_home_work = fields.Integer(related='employee_id.km_home_work', readonly=False, related_sudo=False) + # res.users already have a field bank_account_id and country_id from the res.partner inheritance: don't redefine them + employee_bank_account_id = fields.Many2one(related='employee_id.bank_account_id', string="Employee's Bank Account Number", related_sudo=False, readonly=False) + employee_country_id = fields.Many2one(related='employee_id.country_id', string="Employee's Country", readonly=False, related_sudo=False) + identification_id = fields.Char(related='employee_id.identification_id', readonly=False, related_sudo=False) + passport_id = fields.Char(related='employee_id.passport_id', readonly=False, related_sudo=False) + gender = fields.Selection(related='employee_id.gender', readonly=False, related_sudo=False) + birthday = fields.Date(related='employee_id.birthday', readonly=False, related_sudo=False) + place_of_birth = fields.Char(related='employee_id.place_of_birth', readonly=False, related_sudo=False) + country_of_birth = fields.Many2one(related='employee_id.country_of_birth', readonly=False, related_sudo=False) + marital = fields.Selection(related='employee_id.marital', readonly=False, related_sudo=False) + spouse_complete_name = fields.Char(related='employee_id.spouse_complete_name', readonly=False, related_sudo=False) + spouse_birthdate = fields.Date(related='employee_id.spouse_birthdate', readonly=False, related_sudo=False) + children = fields.Integer(related='employee_id.children', readonly=False, related_sudo=False) + emergency_contact = fields.Char(related='employee_id.emergency_contact', readonly=False, related_sudo=False) + emergency_phone = fields.Char(related='employee_id.emergency_phone', readonly=False, related_sudo=False) + visa_no = fields.Char(related='employee_id.visa_no', readonly=False, related_sudo=False) + permit_no = fields.Char(related='employee_id.permit_no', readonly=False, related_sudo=False) + visa_expire = fields.Date(related='employee_id.visa_expire', readonly=False, related_sudo=False) + google_drive_link = fields.Char(related='employee_id.google_drive_link', readonly=False, related_sudo=False) + additional_note = fields.Text(related='employee_id.additional_note', readonly=False, related_sudo=False) + barcode = fields.Char(related='employee_id.barcode', readonly=False, related_sudo=False) + pin = fields.Char(related='employee_id.pin', readonly=False, related_sudo=False) + + def __init__(self, pool, cr): + """ Override of __init__ to add access rights. + Access rights are disabled by default, but allowed + on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS. + """ + hr_readable_fields = [ + 'active', + 'child_ids', + 'employee_id', + 'employee_ids', + 'parent_id', + ] + + hr_writable_fields = [ + 'additional_note', + 'address_home_id', + 'address_id', + 'barcode', + 'birthday', + 'category_ids', + 'children', + 'coach_id', + 'country_of_birth', + 'department_id', + 'display_name', + 'emergency_contact', + 'emergency_phone', + 'employee_bank_account_id', + 'employee_country_id', + 'gender', + 'google_drive_link', + 'identification_id', + 'is_address_home_a_company', + 'job_title', + 'km_home_work', + 'marital', + 'mobile_phone', + 'notes', + 'parent_id', + 'passport_id', + 'permit_no', + 'phone', + 'pin', + 'place_of_birth', + 'spouse_birthdate', + 'spouse_complete_name', + 'visa_expire', + 'visa_no', + 'work_email', + 'work_location', + 'work_phone' + ] + + init_res = super(User, self).__init__(pool, cr) + # duplicate list to avoid modifying the original reference + type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + hr_readable_fields + hr_writable_fields + type(self).SELF_WRITEABLE_FIELDS = type(self).SELF_WRITEABLE_FIELDS + hr_writable_fields + return init_res + + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + # When the front-end loads the views it gets the list of available fields + # for the user (according to its access rights). Later, when the front-end wants to + # populate the view with data, it only asks to read those available fields. + # However, in this case, we want the user to be able to read/write its own data, + # even if they are protected by groups. + # We make the front-end aware of those fields by sending all field definitions. + if not self.env.user.share: + self = self.sudo() + return super(User, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) @api.multi def write(self, vals): - """ Synchronize user and its related employee """ + """ + Synchronize user and its related employee + and check access rights if employees are not allowed to update + their own data (otherwise sudo is applied for self data). + """ + hr_fields = { + field + for field_name, field in self._fields.items() + if field.related_field and field.related_field.model_name == 'hr.employee' and field_name in vals + } + can_edit_self = self.env['ir.config_parameter'].sudo().get_param('hr.hr_employee_self_edit') or self.env.user.has_group('hr.group_hr_user') + if hr_fields and not can_edit_self: + # Raise meaningful error message + raise AccessError(_("You are only allowed to update your preferences. Please contact a HR officer to update other informations.")) + result = super(User, self).write(vals) + employee_values = {} for fname in [f for f in ['name', 'email', 'image', 'tz'] if f in vals]: employee_values[fname] = vals[fname] if employee_values: + if 'email' in employee_values: + employee_values['work_email'] = employee_values.pop('email') self.env['hr.employee'].sudo().search([('user_id', 'in', self.ids)]).write(employee_values) return result + + @api.model + def action_get(self): + if self.env.user.employee_id: + return self.sudo().env.ref('hr.res_users_action_my').read()[0] + return super(User, self).action_get() + + @api.depends('company_id', 'employee_ids') + def _compute_company_employee(self): + for user in self: + user.employee_id = self.env['hr.employee'].search([('id', 'in', user.employee_ids.ids), ('company_id', '=', user.company_id.id)], limit=1) diff --git a/addons/hr/static/src/js/chat.js b/addons/hr/static/src/js/chat.js new file mode 100644 index 00000000000..2f39851de67 --- /dev/null +++ b/addons/hr/static/src/js/chat.js @@ -0,0 +1,71 @@ +odoo.define('hr.employee_chat', function (require) { +'use strict'; + + var FormController = require('web.FormController'); + var FormView = require('web.FormView'); + var FormRenderer = require('web.FormRenderer'); + var viewRegistry = require('web.view_registry'); + + var EmployeeFormRenderer = FormRenderer.extend({ + + /** + * @override + */ + _render: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + var $chat_button = self.$el.find('.o_employee_chat_btn'); + if (self.state.context.uid === self.state.data.user_id.res_id) { // Hide the button for yourself + $chat_button.hide(); + } + else { + $chat_button.off('click').on('click', self._onOpenChat.bind(self)); + } + }); + }, + + destroy: function () { + this.$el.find('.o_employee_chat_btn').off('click'); + return this._super(); + }, + + _onOpenChat: function(ev) { + ev.preventDefault(); + ev.stopImmediatePropagation(); + this.trigger_up('open_chat', { + partner_id: this.state.data.user_partner_id.res_id + }); + return true; + }, + }); + + var EmployeeFormController = FormController.extend({ + custom_events: _.extend({}, FormController.prototype.custom_events, { + open_chat: '_onOpenChat' + }), + + _onOpenChat: function(ev) { + var self = this; + var dmChat = this.call('mail_service', 'getDMChatFromPartnerID', ev.data.partner_id); + if (dmChat) { + dmChat.detach(); + } else { + var def = this.call('mail_service', 'createChannel', ev.data.partner_id, 'dm_chat').then(function (dmChatId) { + dmChat = self.call('mail_service', 'getChannel', dmChatId); + dmChat.detach(); + }); + $.when(def); + } + }, + }); + + var EmployeeFormView = FormView.extend({ + config: _.extend({}, FormView.prototype.config, { + Controller: EmployeeFormController, + Renderer: EmployeeFormRenderer + }), + }); + + viewRegistry.add('hr_employee_form', EmployeeFormView); + return EmployeeFormView; +}); diff --git a/addons/hr/static/src/xml/hr_templates.xml b/addons/hr/static/src/xml/hr_templates.xml new file mode 100644 index 00000000000..c805e5921f0 --- /dev/null +++ b/addons/hr/static/src/xml/hr_templates.xml @@ -0,0 +1,8 @@ + + diff --git a/addons/hr/tests/__init__.py b/addons/hr/tests/__init__.py index 9e189d217c7..27d9c2fbb81 100644 --- a/addons/hr/tests/__init__.py +++ b/addons/hr/tests/__init__.py @@ -4,3 +4,4 @@ from . import test_hr_employee from . import test_channel from . import test_hr_flow +from . import test_self_user_access diff --git a/addons/hr/tests/test_self_user_access.py b/addons/hr/tests/test_self_user_access.py new file mode 100644 index 00000000000..360cf4f5a1e --- /dev/null +++ b/addons/hr/tests/test_self_user_access.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +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): + + def setUp(self): + super(TestSelfAccessRights, self).setUp() + self.richard = new_test_user(self.env, login='ric', groups='base.group_user', name='Simple employee', email='ric@example.com') + self.richard_emp = self.env['hr.employee'].create({ + 'name': 'Richard', + 'user_id': self.richard.id, + 'address_home_id': self.env['res.partner'].create({'name': 'Richard', 'phone': '21454', 'type': 'private'}).id, + }) + self.hubert = new_test_user(self.env, login='hub', groups='base.group_user', name='Simple employee', email='hub@example.com') + self.hubert_emp = self.env['hr.employee'].create({ + 'name': 'Hubert', + 'user_id': self.hubert.id, + 'address_home_id': self.env['res.partner'].create({'name': 'Hubert', 'type': 'private'}).id, + }) + + self.protected_fields_emp = OrderedDict([(k, v) for k, v in self.env['hr.employee']._fields.items() if v.groups == 'hr.group_hr_user']) + self.self_protected_fields_user = OrderedDict([ + (k, v) + for k, v in self.env['res.users']._fields.items() + if v.groups == 'hr.group_hr_user' and k in self.env['res.users'].SELF_READABLE_FIELDS + ]) + + # Read hr.employee # + def testReadSelfEmployee(self): + for f in self.protected_fields_emp: + with self.assertRaises(AccessError): + self.richard_emp.sudo(self.richard)[f] + + def testReadOtherEmployee(self): + for f in self.protected_fields_emp: + with self.assertRaises(AccessError): + self.hubert_emp.sudo(self.richard)[f] + + # Write hr.employee # + def testWriteSelfEmployee(self): + for f in self.protected_fields_emp: + with self.assertRaises(AccessError): + self.richard_emp.sudo(self.richard).write({f: 'dummy'}) + + def testWriteOtherEmployee(self): + for f in self.protected_fields_emp: + with self.assertRaises(AccessError): + self.hubert_emp.sudo(self.richard).write({f: 'dummy'}) + + # Read res.users # + def testReadSelfUserEmployee(self): + for f in self.self_protected_fields_user: + 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] + + # Write res.users # + def testWriteSelfUserEmployeeSettingFalse(self): + for f, v in self.self_protected_fields_user.items(): + with self.assertRaises(AccessError): + self.richard.sudo(self.richard).write({f: 'dummy'}) + + def testWriteSelfUserEmployee(self): + self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', True) + for f, v in self.self_protected_fields_user.items(): + val = None + if v.type == 'char' or v.type == 'text': + val = 'dummy' + if val is not None: + self.richard.sudo(self.richard).write({f: val}) + + def testWriteSelfUserPreferencesEmployee(self): + # self should always be able to update non hr.employee fields if + # they are in SELF_READABLE_FIELDS + self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', False) + # should not raise + vals = [ + {'tz': "Australia/ACT"}, + {'email': "new@example.com"}, + {'signature': "

I'm Richard!

"}, + {'notification_type': "email"}, + ] + for v in vals: + # should not raise + self.richard.sudo(self.richard).write(v) + + def testWriteOtherUserPreferencesEmployee(self): + # self should always be able to update non hr.employee fields if + # they are in SELF_READABLE_FIELDS + self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', False) + vals = [ + {'tz': "Australia/ACT"}, + {'email': "new@example.com"}, + {'signature': "

I'm Richard!

"}, + {'notification_type': "email"}, + ] + for v in vals: + with self.assertRaises(AccessError): + self.hubert.sudo(self.richard).write(v) + + def testWriteSelfPhoneEmployee(self): + # phone is a related from res.partner (from base) but added in SELF_READABLE_FIELDS + self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', False) + with self.assertRaises(AccessError): + 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_templates.xml b/addons/hr/views/hr_templates.xml index dd7d5acb9e0..465c98d39e0 100644 --- a/addons/hr/views/hr_templates.xml +++ b/addons/hr/views/hr_templates.xml @@ -4,6 +4,7 @@ diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml index 9bf5f22c7b8..2ddfbb34e18 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -141,63 +141,66 @@ hr.employee.form hr.employee -
+ + +
+
- +
-
- + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - +
+
+ + + + + + + + + + + + + + + + +
+
- - - - - - - + + - + + + + - - - - - - + + + + + + + + + + + + - - - - - - - - - @@ -260,7 +263,7 @@
-
+
@@ -270,7 +273,6 @@ - hr.employee.tree hr.employee @@ -595,6 +597,17 @@ + + Archive/Restore + + + code + + if records: + records.toggle_active() + + + Employees hr.employee diff --git a/addons/hr/views/res_config_settings_views.xml b/addons/hr/views/res_config_settings_views.xml index 1e4550f3cb9..97ff8116337 100644 --- a/addons/hr/views/res_config_settings_views.xml +++ b/addons/hr/views/res_config_settings_views.xml @@ -42,6 +42,20 @@
+

Employee Update Rights

+
+
+
+ +
+
+
+
+
diff --git a/addons/hr/views/res_users.xml b/addons/hr/views/res_users.xml new file mode 100644 index 00000000000..b2e64082bd3 --- /dev/null +++ b/addons/hr/views/res_users.xml @@ -0,0 +1,152 @@ + + + + + + + + + res.users.preferences.form.simplified.inherit + res.users + + primary + +