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 @@
+
+
+
+
+ My Profile
+
+
+
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.formhr.employee
-
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
+
+
+
+
+
+
+
+
+ res.users.preferences.form.inherit
+ res.users
+
+
+
+
+
+
+
+
+ Change my Preferences
+ res.users
+ form
+ form
+
+
+
+
diff --git a/addons/hr_attendance/models/__init__.py b/addons/hr_attendance/models/__init__.py
index fd62623b92b..724d7ee23aa 100644
--- a/addons/hr_attendance/models/__init__.py
+++ b/addons/hr_attendance/models/__init__.py
@@ -3,3 +3,4 @@
from . import res_config_settings
from . import hr_attendance
from . import hr_employee
+from . import res_users
diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py
index 79c22e280a4..ac2f23b1eb8 100644
--- a/addons/hr_attendance/models/hr_employee.py
+++ b/addons/hr_attendance/models/hr_employee.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, exceptions, _, SUPERUSER_ID
+from datetime import datetime
+from dateutil.relativedelta import relativedelta
class HrEmployee(models.Model):
@@ -10,23 +12,19 @@ class HrEmployee(models.Model):
attendance_ids = fields.One2many('hr.attendance', 'employee_id', help='list of attendances for the employee')
last_attendance_id = fields.Many2one('hr.attendance', compute='_compute_last_attendance_id', store=True)
attendance_state = fields.Selection(string="Attendance Status", compute='_compute_attendance_state', selection=[('checked_out', "Checked out"), ('checked_in', "Checked in")])
- manual_attendance = fields.Boolean(string='Manual Attendance', compute='_compute_manual_attendance', inverse='_inverse_manual_attendance',
- help='The employee will have access to the "My Attendances" menu to check in and out from his session')
+ hours_last_month = fields.Float(compute='_compute_hours_last_month')
- @api.multi
- def _compute_manual_attendance(self):
+ def _compute_hours_last_month(self):
for employee in self:
- employee.manual_attendance = employee.user_id.has_group('hr_attendance.group_hr_attendance') if employee.user_id else False
-
- @api.multi
- def _inverse_manual_attendance(self):
- manual_attendance_group = self.env.ref('hr_attendance.group_hr_attendance')
- for employee in self:
- if employee.user_id:
- if employee.manual_attendance:
- manual_attendance_group.users = [(4, employee.user_id.id, 0)]
- else:
- manual_attendance_group.users = [(3, employee.user_id.id, 0)]
+ now = datetime.now()
+ start = now + relativedelta(months=-1, day=1)
+ end = now + relativedelta(days=-1, day=1)
+ attendances = self.env['hr.attendance'].search([
+ ('employee_id', '=', employee.id),
+ ('check_in', '>=', start),
+ ('check_out', '<=', end),
+ ])
+ employee.hours_last_month = sum(attendances.mapped('worked_hours'))
@api.depends('attendance_ids')
def _compute_last_attendance_id(self):
diff --git a/addons/hr_attendance/models/res_users.py b/addons/hr_attendance/models/res_users.py
new file mode 100644
index 00000000000..cf0a329a028
--- /dev/null
+++ b/addons/hr_attendance/models/res_users.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, fields, api, _
+
+
+class User(models.Model):
+ _inherit = ['res.users']
+
+ hours_last_month = fields.Float(related='employee_id.hours_last_month')
+ attendance_state = fields.Selection(related='employee_id.attendance_state')
+
+ 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.
+ """
+ attendance_readable_fields = [
+ 'hours_last_month',
+ 'attendance_state',
+ ]
+ 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 + attendance_readable_fields
+ return init_res
diff --git a/addons/hr_attendance/views/hr_attendance_view.xml b/addons/hr_attendance/views/hr_attendance_view.xml
index a3b7ca51c42..d6e8f394aa9 100644
--- a/addons/hr_attendance/views/hr_attendance_view.xml
+++ b/addons/hr_attendance/views/hr_attendance_view.xml
@@ -70,7 +70,7 @@
-
+
diff --git a/addons/hr_attendance/views/hr_employee_view.xml b/addons/hr_attendance/views/hr_employee_view.xml
index f78b916c9a8..f9403ff847a 100644
--- a/addons/hr_attendance/views/hr_employee_view.xml
+++ b/addons/hr_attendance/views/hr_employee_view.xml
@@ -7,7 +7,7 @@
10
-
+
-
-
+
+
+
+
+
+
+
+ hr.user.preferences.view.form.attendance.inherit
+ res.users
+
+
+
+
+
+
+
+
+
diff --git a/addons/hr_attendance_presence/__init__.py b/addons/hr_attendance_presence/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/addons/hr_attendance_presence/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/addons/hr_attendance_presence/__manifest__.py b/addons/hr_attendance_presence/__manifest__.py
new file mode 100644
index 00000000000..da31006ddb7
--- /dev/null
+++ b/addons/hr_attendance_presence/__manifest__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Attendance Presence',
+ 'version': '1.0',
+ 'category': 'Human Resources',
+ 'sequence': 85,
+ 'summary': 'Bridge Attendance module and Presence module',
+ 'description': "",
+ 'website': 'https://www.odoo.com/page/employees',
+ 'depends': ['hr_attendance', 'hr_presence'],
+ 'installable': True,
+ 'auto_install': True,
+ 'data': [
+ 'views/res_config_settings_views.xml',
+ 'views/hr_employee.xml',
+ ],
+}
diff --git a/addons/hr_attendance_presence/models/__init__.py b/addons/hr_attendance_presence/models/__init__.py
new file mode 100644
index 00000000000..5d68338dfc7
--- /dev/null
+++ b/addons/hr_attendance_presence/models/__init__.py
@@ -0,0 +1,2 @@
+from . import hr_employee
+from . import res_config_settings
diff --git a/addons/hr_attendance_presence/models/hr_employee.py b/addons/hr_attendance_presence/models/hr_employee.py
new file mode 100644
index 00000000000..3fac86ce6c4
--- /dev/null
+++ b/addons/hr_attendance_presence/models/hr_employee.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+from odoo import fields, models, _
+from odoo.exceptions import UserError
+from odoo.fields import Datetime
+
+_logger = logging.getLogger(__name__)
+
+
+class Employee(models.Model):
+ _inherit = 'hr.employee'
+
+ def _action_open_presence_view(self):
+ action = super(Employee, self)._action_open_presence_view()
+
+ if self.env['ir.config_parameter'].sudo().get_param('hr_presence.hr_presence_control_attendance'):
+ company = self.env.user.company_id
+ employees = self.env['hr.employee'].search([
+ ('department_id.company_id', '=', company.id),
+ ('user_id', '!=', False),
+ ])
+
+ employees.filtered(
+ lambda e: e.attendance_state == 'checked_in'
+ ).write({'hr_presence_state': 'present'})
+
+ employees.filtered(
+ lambda e: e.attendance_state == 'checked_out'
+ ).write({'hr_presence_state': 'absent'})
+
+ return action
diff --git a/addons/hr_attendance_presence/models/res_config_settings.py b/addons/hr_attendance_presence/models/res_config_settings.py
new file mode 100644
index 00000000000..a9bbd8f64b3
--- /dev/null
+++ b/addons/hr_attendance_presence/models/res_config_settings.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ hr_presence_control_attendance = fields.Boolean(string="According to the Attendance module", config_parameter='hr_presence.hr_presence_control_attendance')
diff --git a/addons/hr_attendance_presence/views/hr_employee.xml b/addons/hr_attendance_presence/views/hr_employee.xml
new file mode 100644
index 00000000000..c49871522a2
--- /dev/null
+++ b/addons/hr_attendance_presence/views/hr_employee.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ hr.employee
+ hr.employee
+
+ 20
+
+
+ 1
+
+
+ 1
+
+
+ 1
+
+
+
+
diff --git a/addons/hr_attendance_presence/views/res_config_settings_views.xml b/addons/hr_attendance_presence/views/res_config_settings_views.xml
new file mode 100644
index 00000000000..ef6ab9142e6
--- /dev/null
+++ b/addons/hr_attendance_presence/views/res_config_settings_views.xml
@@ -0,0 +1,17 @@
+
+
+
+ res.config.settings.view.form.inherit.presence.hr
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/hr_contract/models/__init__.py b/addons/hr_contract/models/__init__.py
index 855ff940d55..7e9d4a5f5eb 100644
--- a/addons/hr_contract/models/__init__.py
+++ b/addons/hr_contract/models/__init__.py
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_contract
+from . import res_users
diff --git a/addons/hr_contract/models/res_users.py b/addons/hr_contract/models/res_users.py
new file mode 100644
index 00000000000..2bf71f17f60
--- /dev/null
+++ b/addons/hr_contract/models/res_users.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, fields, api, _
+
+
+class User(models.Model):
+ _inherit = ['res.users']
+
+ medic_exam = fields.Date(related="employee_id.medic_exam")
+ vehicle = fields.Char(related="employee_id.vehicle")
+ bank_account_id = fields.Many2one(related="employee_id.bank_account_id")
+
+ 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.
+ """
+ contract_readable_fields = [
+ 'medic_exam',
+ 'vehicle',
+ 'bank_account_id',
+ ]
+ 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 + contract_readable_fields
+ return init_res
diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml
index 0e2576f2ab9..468983a9d0b 100644
--- a/addons/hr_contract/views/hr_contract_views.xml
+++ b/addons/hr_contract/views/hr_contract_views.xml
@@ -19,7 +19,7 @@
-
+