[IMP] hr_*: introduce the employee profile
General Purpose =============== We want an 'Employee profile' gathering every data about an employee. The main form view is modified to become this employee profile. A user can also see his own profile through the Preferences menu. The new profile replaces the current Preferences view if the hr module is installed and the current user is linked to an employee. A user should be able to see and edit his own profile. *Problem*: Many fields on hr.employee are protected by groups="hr.group_hr_user". Therefore, a regular user cannot see or edit those fields. This protection must be bypassed to allow read/write access to the regular user's own data. A similar mechanism already exists for res.users (for Preferences) The better (least worst) solution found is to reuse this mechanism by adding related fields on res.users. Pros: - Don't change security access on hr.employee - Don't implement yet another custom security layer, risking to add new security breaches - A lot of fields are added by other modules on hr.employee. It would have required to integrate them with the custom security layer. - Fields added by other modules on the user's preferences view (normal view, not the profile) are automatically included in the employee's profile view. - Allow the hr.employee form view to be different than the user profile accessible through the Preferences menu. E.g. add custom buttons only relevant to the logged in user such as "Request a leave". Cons: - Each field from hr.employee that you want to appear on its profile must be added as a related field on res.users - Those related fields must be added to user's preferences view (duplicate views) - They also must be added to SELF_[READABLE | WRITABLE]_FIELDS Note: 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 (groups are kept on the related fields on res.users). The front-end need to be made aware of those fields by sending all field definitions. hr_attendance ============= This commit integrate attendance in the new employee profile. It also adds a stat button to this employee profile showing the number of hours worked last month. Remove the boolean computed field 'manual_attendance'. This field is just a shortcut to add/remove the employee's user in the "Manual Attendance" group. The checkbox is confusing on the employee's form and this should be done through the normal group management screens. hr_presence =========== Display the presence status on the employee kanban template. The status is a colored chip which can be green (present), orange (to define) or red (absent). Currently, the presence status is only computed when accessing the report view. As this commits displays it on the employee kanban, it should be updated more frequently. The state should not be updated every time the kanban view is loaded since the computation is a bit heavy. Instead: add a cron to update status every 15 minutes. -> The status is accurate on the report view (status is still updated when loading the view) -> The status in accurate at 15 minutes on the kanban view [ADD] hr_attendance_presence ============================ Bridge module between hr_attendance and hr_presence. This commit integrates hr_presence module in the employee profile and adds the presence status on the employee kanban view. But hr_attendance adds at the same place a similar status icon for checkin/checkout. This bridge module makes the status from hr_presence invisible as hr_attendance should be the main presence control mechanism. Also, this commit adds the ability (through a new setting option) for hr_presence to take into account checkin/checkout to determine the presence status. l10n_be_hr_payroll ================== integration with employee profile
This commit is contained in:
@@ -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'],
|
||||
}
|
||||
|
||||
+55
-28
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<template xml:space="preserve">
|
||||
<t t-extend="UserMenu.Actions">
|
||||
<t t-jquery="a[data-menu='settings']" t-operation='inner'>
|
||||
My Profile
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import test_hr_employee
|
||||
from . import test_channel
|
||||
from . import test_hr_flow
|
||||
from . import test_self_user_access
|
||||
|
||||
@@ -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': "<p>I'm Richard!</p>"},
|
||||
{'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': "<p>I'm Richard!</p>"},
|
||||
{'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'})
|
||||
@@ -4,6 +4,7 @@
|
||||
<template id="assets_backend" name="hr assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" type="text/scss" href="/hr/static/src/scss/hr.scss"/>
|
||||
<script type="text/javascript" src="/hr/static/src/js/chat.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -141,63 +141,66 @@
|
||||
<field name="name">hr.employee.form</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Employee">
|
||||
<form string="Employee" js_class="hr_employee_form">
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="user_partner_id" invisible="1"/>
|
||||
<header>
|
||||
<button string="Chat" class="btn btn-primary o_employee_chat_btn" attrs="{'invisible': [('user_id','=', False)]}"/>
|
||||
<button name="%(plan_wizard_action)d" string="Launch Plan" type="action" groups="hr.group_hr_manager"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object" groups="hr.group_hr_user"
|
||||
class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button"
|
||||
options='{"terminology": "archive"}'/>
|
||||
</button>
|
||||
<!-- Used by other modules-->
|
||||
</div>
|
||||
<div id="o_employee_container"><div id="o_employee_main">
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"preview_image":"image_medium"}'/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Employee's Name" required="True"/>
|
||||
</h1>
|
||||
<h2>
|
||||
<field name="job_title" placeholder="Job Title" />
|
||||
</h2>
|
||||
<field name="category_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}" placeholder="Tags" groups="hr.group_hr_manager"/>
|
||||
</div>
|
||||
<field name="category_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}" placeholder="e.g. Part Time" groups="hr.group_hr_user"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="mobile_phone" widget="phone"/>
|
||||
<field name="work_phone" widget="phone"/>
|
||||
<field name="work_email" widget="email"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="department_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="public" string="Work Information">
|
||||
<group>
|
||||
<group string="Contact Information">
|
||||
<field name="address_id"
|
||||
context="{'show_address': 1}"
|
||||
options='{"always_reload": True, "highlight_first_line": True}'/>
|
||||
<field name="work_location"/>
|
||||
<field name="work_email" widget="email"/>
|
||||
<field name="mobile_phone" widget="phone"/>
|
||||
<field name="work_phone" widget="phone"/>
|
||||
</group>
|
||||
<group string="Position">
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="job_title"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="coach_id"/>
|
||||
<field name="resource_calendar_id" required="1"/>
|
||||
<field name="tz"/>
|
||||
</group>
|
||||
<group string="Departure" attrs="{'invisible': [('active', '=', True)]}">
|
||||
<field name="departure_reason"/>
|
||||
<field name="departure_description"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="notes" placeholder="Other Information ..." colspan="4"/>
|
||||
<div id="o_work_employee_container"> <!-- These two div are used to position org_chart -->
|
||||
<div id="o_work_employee_main">
|
||||
<group string="Location">
|
||||
<field name="address_id"
|
||||
context="{'show_address': 1}"
|
||||
options='{"always_reload": True, "highlight_first_line": True}'/>
|
||||
<field name="work_location"/>
|
||||
</group>
|
||||
<group name="managers" string="Managers">
|
||||
<field name="parent_id"/>
|
||||
<field name="coach_id"/>
|
||||
</group>
|
||||
<group string="Departure" attrs="{'invisible': [('active', '=', True)]}">
|
||||
<field name="departure_reason"/>
|
||||
<field name="departure_description"/>
|
||||
</group>
|
||||
<group string="Schedule" groups="base.group_no_one">
|
||||
<field name="resource_calendar_id" required="1"/>
|
||||
<field name="tz"/>
|
||||
</group>
|
||||
</div>
|
||||
</div>
|
||||
</page>
|
||||
<page name="personal_information" string="Private Information" groups="hr.group_hr_user">
|
||||
<group>
|
||||
<group string="Citizenship & Other Information">
|
||||
<field name="country_id" options='{"no_open": True, "no_create": True}'/>
|
||||
<field name="identification_id" groups="hr.group_hr_user"/>
|
||||
<field name="passport_id" groups="hr.group_hr_user"/>
|
||||
<field name="bank_account_id" context="{'default_partner_id': address_home_id}"/>
|
||||
</group>
|
||||
<group string="Contact Information">
|
||||
<field name="address_home_id"
|
||||
context="{
|
||||
@@ -212,44 +215,44 @@
|
||||
This private address is used in the expense report reimbursement document.
|
||||
It should be different from the work address.
|
||||
</div>
|
||||
<field name="emergency_contact"/>
|
||||
<field name="emergency_phone"/>
|
||||
<field name="km_home_work" groups="hr.group_hr_user"/>
|
||||
<field name="phone" groups="hr.group_hr_user"/>
|
||||
<field name="bank_account_id" context="{'default_partner_id': address_home_id}"/>
|
||||
</group>
|
||||
<group string="Status">
|
||||
<group string="Citizenship">
|
||||
<field name="country_id" options='{"no_open": True, "no_create": True}'/>
|
||||
<field name="identification_id" groups="hr.group_hr_user"/>
|
||||
<field name="passport_id" groups="hr.group_hr_user"/>
|
||||
<field name="gender"/>
|
||||
<field name="marital"/>
|
||||
<field name="spouse_complete_name" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}" groups="hr.group_hr_user"/>
|
||||
<field name="spouse_birthdate" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}" groups="hr.group_hr_user"/>
|
||||
<field name="children"/>
|
||||
</group>
|
||||
<group string="Birth">
|
||||
<field name="birthday"/>
|
||||
<field name="place_of_birth" groups="hr.group_hr_user"/>
|
||||
<field name="country_of_birth" groups="hr.group_hr_user"/>
|
||||
</group>
|
||||
<group string="Marital Status">
|
||||
<field name="marital"/>
|
||||
<field name="spouse_complete_name" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}" groups="hr.group_hr_user"/>
|
||||
<field name="spouse_birthdate" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}" groups="hr.group_hr_user"/>
|
||||
</group>
|
||||
<group string="Dependant">
|
||||
<field name="children"/>
|
||||
</group>
|
||||
<group string="Emergency">
|
||||
<field name="emergency_contact"/>
|
||||
<field name="emergency_phone"/>
|
||||
</group>
|
||||
<group string="Work Permit" name="work_permit">
|
||||
<field name="visa_no"/>
|
||||
<field name="permit_no"/>
|
||||
<field name="visa_expire"/>
|
||||
</group>
|
||||
<group string="Education">
|
||||
<field name="certificate"/>
|
||||
<field name="study_field"/>
|
||||
<field name="study_school"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="google_drive_link" widget="url"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="additional_note"/>
|
||||
</group>
|
||||
</page>
|
||||
<page name="hr_settings" string="HR Settings" groups="hr.group_hr_user">
|
||||
<group>
|
||||
<group string='Status' name="active_group">
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="user_id" string="Related User"/>
|
||||
</group>
|
||||
<group string="Login Information" name="identification_group">
|
||||
@@ -260,7 +263,7 @@
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</div></div>
|
||||
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
|
||||
@@ -270,7 +273,6 @@
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_employee_tree" model="ir.ui.view">
|
||||
<field name="name">hr.employee.tree</field>
|
||||
<field name="model">hr.employee</field>
|
||||
@@ -595,6 +597,17 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.server" id="hr_employee_action_toggle_active">
|
||||
<field name="name">Archive/Restore</field>
|
||||
<field name="model_id" ref="hr.model_hr_employee"/>
|
||||
<field name="binding_model_id" ref="hr.model_hr_employee"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
records.toggle_active()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="act_employee_from_department" model="ir.actions.act_window">
|
||||
<field name="name">Employees</field>
|
||||
<field name="res_model">hr.employee</field>
|
||||
|
||||
@@ -42,6 +42,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Employee Update Rights</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box" title="Allow employees to update their own data.">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="hr_employee_self_edit"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="hr_employee_self_edit"/>
|
||||
<div class="text-muted">
|
||||
Allow employees to update their own data.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Inherit the preference view to remove title, image and footer -->
|
||||
<!-- This view is meant to be included in the employee profile view -->
|
||||
<!-- It ensures that if the 'normal' Preferences view is changed, it's
|
||||
also reflected in the employee's profile -->
|
||||
<record id="res_users_view_form_simple_modif" model="ir.ui.view">
|
||||
<field name="name">res.users.preferences.form.simplified.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<footer position="replace"/>
|
||||
<h1 position="replace"/>
|
||||
<xpath expr="//field[@name='image']" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_users_view_form_profile" model="ir.ui.view">
|
||||
<field name="name">res.users.preferences.form.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="res_users_view_form_simple_modif"/>
|
||||
<field name="arch" type="xml">
|
||||
<form position="replace">
|
||||
<form>
|
||||
<header></header> <!-- Used by other modules to add buttons -->
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
</div>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"preview_image":"image_medium"}'/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Employee's Name" required="True"/>
|
||||
</h1>
|
||||
<field name="category_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}" placeholder="Tags" groups="hr.group_hr_user"/>
|
||||
</div>
|
||||
<h2>
|
||||
<field name="job_title"/>
|
||||
</h2>
|
||||
<group>
|
||||
<group>
|
||||
<field name="mobile_phone" widget="phone"/>
|
||||
<field name="work_phone" widget="phone"/>
|
||||
<field name="work_email" widget="email"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="department_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="public" string="Work Information">
|
||||
<div id="o_work_employee_container"> <!-- These two div are used to position org_chart -->
|
||||
<div id="o_work_employee_main">
|
||||
<group string="Location">
|
||||
<field name="address_id"
|
||||
context="{'show_address': 1}"
|
||||
options='{"always_reload": True, "highlight_first_line": True}'/>
|
||||
<field name="work_location"/>
|
||||
</group>
|
||||
<group name="managers" string="Managers">
|
||||
<field name="parent_id"/>
|
||||
<field name="coach_id"/>
|
||||
</group>
|
||||
</div>
|
||||
</div>
|
||||
</page>
|
||||
<page name="personal_information" string="Private Information">
|
||||
<group>
|
||||
<group string="Contact Information">
|
||||
<field name="employee_ids" invisible="1"/>
|
||||
<field name="address_home_id"
|
||||
context="{
|
||||
'show_address': 1,
|
||||
'default_employee_ids': employee_ids,
|
||||
'default_type': 'private',
|
||||
'form_view_ref': 'base.res_partner_view_form_private'}"
|
||||
options='{"always_reload": True, "highlight_first_line": True}'/>
|
||||
<field name="is_address_home_a_company" invisible="1"/>
|
||||
<div class="text-warning"
|
||||
attrs="{'invisible': [('is_address_home_a_company','=', False)]}">
|
||||
Use here the home address of the employee.
|
||||
This private address is used in the expense report reimbursement document.
|
||||
It should be different from the work address.
|
||||
</div>
|
||||
<field name="km_home_work"/>
|
||||
<field name="phone"/>
|
||||
<field name="employee_bank_account_id"/>
|
||||
</group>
|
||||
<group string="Citizenship">
|
||||
<field name="employee_country_id" options='{"no_open": True, "no_create": True}'/>
|
||||
<field name="identification_id"/>
|
||||
<field name="passport_id"/>
|
||||
<field name="gender"/>
|
||||
<field name="birthday"/>
|
||||
<field name="place_of_birth"/>
|
||||
<field name="country_of_birth"/>
|
||||
</group>
|
||||
<group string="Marital Status">
|
||||
<field name="marital"/>
|
||||
<field name="spouse_complete_name" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}"/>
|
||||
<field name="spouse_birthdate" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}"/>
|
||||
</group>
|
||||
<group string="Dependant">
|
||||
<field name="children"/>
|
||||
</group>
|
||||
<group string="Emergency">
|
||||
<field name="emergency_contact"/>
|
||||
<field name="emergency_phone"/>
|
||||
</group>
|
||||
<group string="Work Permit" name="work_permit">
|
||||
<field name="visa_no"/>
|
||||
<field name="permit_no"/>
|
||||
<field name="visa_expire"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="google_drive_link" widget="url"/>
|
||||
</group>
|
||||
</page>
|
||||
<page name="preferences" string="Preferences">
|
||||
<div>$0</div>
|
||||
</page>
|
||||
<page name="hr_settings" string="HR Settings">
|
||||
<group>
|
||||
<group string='Status' name="active_group">
|
||||
</group>
|
||||
<group string="Login Information" name="identification_group">
|
||||
<field name="pin"/>
|
||||
<field name="barcode"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_users_action_my" model="ir.actions.act_window">
|
||||
<field name="name">Change my Preferences</field>
|
||||
<field name="res_model">res.users</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="hr.res_users_view_form_profile"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -3,3 +3,4 @@
|
||||
from . import res_config_settings
|
||||
from . import hr_attendance
|
||||
from . import hr_employee
|
||||
from . import res_users
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -70,7 +70,7 @@
|
||||
<field name="department_id" operator="child_of"/>
|
||||
<filter string="My Attendances" name="myattendances" domain="[('employee_id.user_id.id', '=', uid)]" />
|
||||
<separator/>
|
||||
<filter name="check_in_filter" string="Check In Date" date="check_in"/>
|
||||
<filter name="check_in_filter" string="Check In Date" date="check_in" default_period="last_month"/>
|
||||
<filter string="No Check Out" name="nocheckout" domain="[('check_out', '=', False)]" />
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="employee" string="Employee" context="{'group_by':'employee_id'}"/>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="priority">10</field>
|
||||
<field name="groups_id" eval="[(4,ref('hr_attendance.group_hr_attendance_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field name="attendance_state" invisible="1"/>
|
||||
<button name="%(hr_attendance_action_employee)d"
|
||||
class="oe_stat_button"
|
||||
@@ -19,8 +19,62 @@
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='identification_group']" position="inside">
|
||||
<field name="manual_attendance" attrs="{'invisible': [('user_id', '=', False)]}"/>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="%(hr_attendance_action)d"
|
||||
class="oe_stat_button"
|
||||
icon="fa-calendar"
|
||||
type="action"
|
||||
context="{'search_default_employee_id': active_id, 'search_default_check_in_filter': '1'}"
|
||||
groups="base.group_user"
|
||||
help="Worked hours last month">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="hours_last_month"/> Hours
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Last Month
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_user_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.user.preferences.view.form.attendance.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="hr.res_users_view_form_profile"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field name="attendance_state" invisible="1"/>
|
||||
<button name="%(hr_attendance_action_my_attendances)d"
|
||||
class="oe_stat_button"
|
||||
type="action" attrs="{'invisible': [('attendance_state', '=', False)]}">
|
||||
<div role="img" id="oe_hr_attendance_status" class="fa fa-fw fa-circle o_button_icon oe_hr_attendance_status_green" attrs="{'invisible': [('attendance_state', '=', 'checked_out')]}" aria-label="Available" title="Available"/>
|
||||
<div role="img" id="oe_hr_attendance_status" class="fa fa-fw fa-circle o_button_icon oe_hr_attendance_status_red" attrs="{'invisible': [('attendance_state', '=', 'checked_in')]}" aria-label="Not available" title="Not available"/>
|
||||
<div class="o_stat_info">
|
||||
<span class="o_stat_text">Attendance</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field name="employee_ids" invisible="1"/>
|
||||
<button name="%(hr_attendance_action)d"
|
||||
class="oe_stat_button"
|
||||
icon="fa-calendar"
|
||||
type="action"
|
||||
context="{'search_default_employee_id': employee_ids, 'search_default_check_in_filter': '1'}"
|
||||
groups="base.group_user"
|
||||
help="Worked hours last month">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="hours_last_month"/> Hours
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Last Month
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -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',
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import hr_employee
|
||||
from . import res_config_settings
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_employee_view_kanban" model="ir.ui.view">
|
||||
<field name="name">hr.employee</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr_presence.hr_employee_view_kanban"/>
|
||||
<field name="priority">20</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//span[@name='presence_absent']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//span[@name='presence_present']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//span[@name='presence_to_define']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.presence.hr</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="70"/>
|
||||
<field name="inherit_id" ref="hr.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='hr_presence_options']" position="inside">
|
||||
<div class="row">
|
||||
<field name="hr_presence_control_attendance" class="col-lg-1 ml16"/>
|
||||
<label for="hr_presence_control_attendance" class="col-lg-10 o_light_label"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_contract
|
||||
from . import res_users
|
||||
|
||||
@@ -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
|
||||
@@ -19,7 +19,7 @@
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="%(act_hr_employee_2_hr_contract)d"
|
||||
class="oe_stat_button"
|
||||
icon="fa-book"
|
||||
@@ -41,6 +41,23 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_user_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.user.preferences.view.form.contract.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="hr.res_users_view_form_profile"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='active_group']" position="after">
|
||||
<group string="Current Contract">
|
||||
<field name="medic_exam" string="Medical Exam"/>
|
||||
<field name="vehicle"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='employee_bank_account_id']" position="replace">
|
||||
<field name="employee_bank_account_id" context="{'display_partner':True}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract_view_search" model="ir.ui.view">
|
||||
<field name="name">hr.contract.search</field>
|
||||
<field name="model">hr.contract</field>
|
||||
|
||||
@@ -3,12 +3,23 @@
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class Employee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
def _group_hr_expense_user_domain(self):
|
||||
# We return the domain only if the group exists for the following reason:
|
||||
# When a group is created (at module installation), the `res.users` form view is
|
||||
# automatically modifiedto add application accesses. When modifiying the view, it
|
||||
# reads the related field `expense_manager_id` of `res.users` and retrieve its domain.
|
||||
# This is a problem because the `group_hr_expense_user` record has already been created but
|
||||
# not its associated `ir.model.data` which makes `self.env.ref(...)` fail.
|
||||
group = self.env.ref('hr_expense.group_hr_expense_user', raise_if_not_found=False)
|
||||
return [('groups_id', 'in', group.ids)] if group else []
|
||||
|
||||
expense_manager_id = fields.Many2one(
|
||||
'res.users', string='Expense Responsible',
|
||||
domain=lambda self: [('groups_id', 'in', self.env.ref('hr_expense.group_hr_expense_user').id)],
|
||||
domain=_group_hr_expense_user_domain,
|
||||
help="User responsible of expense approval. Should be Expense Manager.")
|
||||
|
||||
@api.onchange('parent_id')
|
||||
@@ -18,3 +29,19 @@ class Employee(models.Model):
|
||||
manager = self.parent_id.user_id
|
||||
if manager and manager.has_group('hr_expense.group_hr_expense_user') and (self.expense_manager_id == previous_manager or not self.expense_manager_id):
|
||||
self.expense_manager_id = manager
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
_inherit = ['res.users']
|
||||
|
||||
expense_manager_id = fields.Many2one(related='employee_id.expense_manager_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.
|
||||
"""
|
||||
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 + ['expense_manager_id']
|
||||
return init_res
|
||||
|
||||
@@ -6,10 +6,19 @@
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='active_group']" position="before">
|
||||
<group string="Expenses" name="timesheet">
|
||||
<field name="expense_manager_id"/>
|
||||
</group>
|
||||
<xpath expr="//group[@name='managers']" position="inside">
|
||||
<field name="expense_manager_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_users_view_form_preferences" model="ir.ui.view">
|
||||
<field name="name">hr.user.preferences.form.inherit.hr.expense</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="hr.res_users_view_form_profile" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='managers']" position="inside">
|
||||
<field name="expense_manager_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import employee
|
||||
from . import res_users
|
||||
from . import fleet_vehicle_assignation_log
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
_inherit = ['res.users']
|
||||
|
||||
employee_cars_count = fields.Integer(related='employee_id.employee_cars_count')
|
||||
|
||||
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.
|
||||
"""
|
||||
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 + ['employee_cars_count']
|
||||
return init_res
|
||||
|
||||
def action_get_claim_report(self):
|
||||
return self.employee_id.action_get_claim_report()
|
||||
|
||||
def action_open_employee_cars(self):
|
||||
return self.employee_id.action_open_employee_cars()
|
||||
@@ -5,15 +5,14 @@
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet" position="before">
|
||||
<header>
|
||||
<button name="action_get_claim_report" string="Claim Report"
|
||||
type="object" groups="fleet.fleet_group_manager"
|
||||
attrs="{'invisible': [('employee_cars_count','=', 0)]}"
|
||||
confirm="This report will contain only PDF files. If you want all documents, please go on vehicle page. Do you want to proceed?"/>
|
||||
</header>
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_get_claim_report" string="Claim Car Report"
|
||||
class="btn btn-secondary"
|
||||
type="object" groups="fleet.fleet_group_manager"
|
||||
attrs="{'invisible': [('employee_cars_count','=', 0)]}"
|
||||
confirm="This report will contain only PDF files. If you want all documents, please go on vehicle page. Do you want to proceed?"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_open_employee_cars" type="object"
|
||||
class="oe_stat_button" icon="fa-car" groups="fleet.fleet_group_manager"
|
||||
attrs="{'invisible': [('employee_cars_count','=', 0)]}">
|
||||
@@ -22,4 +21,26 @@
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_users_view_form_preferences" model="ir.ui.view">
|
||||
<field name="name">hr.user.preferences.form.inherit.hr.fleet</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="hr.res_users_view_form_profile" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_get_claim_report" string="Claim Car Report"
|
||||
class="btn btn-primary"
|
||||
type="object" groups="fleet.fleet_group_manager"
|
||||
attrs="{'invisible': [('employee_cars_count','=', 0)]}"
|
||||
confirm="This report will contain only PDF files. If you want all documents, please go on vehicle page. Do you want to proceed?"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_open_employee_cars" type="object"
|
||||
class="oe_stat_button" icon="fa-car"
|
||||
attrs="{'invisible': [('employee_cars_count','=', 0)]}">
|
||||
<field name="employee_cars_count" widget="statinfo" />
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -63,10 +63,14 @@ class Department(models.Model):
|
||||
class Employee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
def _group_hr_user_domain(self):
|
||||
group = self.env.ref('hr_holidays.group_hr_holidays_team_leader', raise_if_not_found=False)
|
||||
return [('groups_id', 'in', group.ids)] if group else []
|
||||
|
||||
leave_manager_id = fields.Many2one(
|
||||
'res.users', string='Time Off Responsible',
|
||||
domain=lambda self: [('groups_id', 'in', self.env.ref('hr_holidays.group_hr_holidays_team_leader').id)],
|
||||
help="User responsible of time off approval. Should be Team Leader or Department Manager.")
|
||||
domain=_group_hr_user_domain,
|
||||
help="User responsible of leaves approval. Should be Team Leader or Department Manager.")
|
||||
remaining_leaves = fields.Float(
|
||||
compute='_compute_remaining_leaves', string='Remaining Paid Time Off',
|
||||
help='Total number of paid time off allocated to this employee, change this value to create allocation/time off request. '
|
||||
@@ -84,8 +88,10 @@ class Employee(models.Model):
|
||||
leave_date_from = fields.Date('From Date', compute='_compute_leave_status')
|
||||
leave_date_to = fields.Date('To Date', compute='_compute_leave_status')
|
||||
leaves_count = fields.Float('Number of Time Off', compute='_compute_remaining_leaves')
|
||||
allocation_count = fields.Float('Total number of days allocated.', compute='_compute_allocation_count')
|
||||
allocation_used_count = fields.Float('Total number of days off used', compute='_compute_total_allocation_used')
|
||||
show_leaves = fields.Boolean('Able to see Remaining Time Off', compute='_compute_show_leaves')
|
||||
is_absent_totay = fields.Boolean('Absent Today', compute='_compute_absent_employee', search='_search_absent_employee')
|
||||
is_absent = fields.Boolean('Absent Today', compute='_compute_leave_status', search='_search_absent_employee')
|
||||
|
||||
def _get_remaining_leaves(self):
|
||||
""" Helper to compute the remaining leaves for the current employees
|
||||
@@ -121,6 +127,22 @@ class Employee(models.Model):
|
||||
employee.leaves_count = value
|
||||
employee.remaining_leaves = value
|
||||
|
||||
@api.multi
|
||||
def _compute_allocation_count(self):
|
||||
for employee in self:
|
||||
allocations = self.env['hr.leave.allocation'].search([
|
||||
('employee_id', '=', employee.id),
|
||||
('state', '=', 'validate'),
|
||||
'|',
|
||||
('date_to', '=', False),
|
||||
('date_to', '>=', datetime.date.today()),
|
||||
])
|
||||
employee.allocation_count = sum(allocations.mapped('number_of_days'))
|
||||
|
||||
def _compute_total_allocation_used(self):
|
||||
for employee in self:
|
||||
employee.allocation_used_count = employee.allocation_count - employee.remaining_leaves
|
||||
|
||||
@api.multi
|
||||
def _compute_leave_status(self):
|
||||
# Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule
|
||||
@@ -143,6 +165,7 @@ class Employee(models.Model):
|
||||
employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to')
|
||||
employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
|
||||
employee.current_leave_id = leave_data.get(employee.id, {}).get('current_leave_id')
|
||||
employee.is_absent = leave_data and leave_data.get(employee.id, {}).get('current_leave_state') not in ['cancel', 'refuse', 'draft']
|
||||
|
||||
@api.onchange('parent_id')
|
||||
def _onchange_parent_id(self):
|
||||
@@ -161,34 +184,13 @@ class Employee(models.Model):
|
||||
else:
|
||||
employee.show_leaves = False
|
||||
|
||||
@api.multi
|
||||
def _compute_absent_employee(self):
|
||||
today_date = datetime.datetime.utcnow().date()
|
||||
today_start = fields.Datetime.to_string(today_date) # get the midnight of the current utc day
|
||||
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
|
||||
data = self.env['hr.leave'].read_group([
|
||||
('employee_id', 'in', self.ids),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
('date_from', '<=', today_end),
|
||||
('date_to', '>=', today_start)
|
||||
], ['employee_id'], ['employee_id'])
|
||||
result = dict.fromkeys(self.ids, False)
|
||||
for item in data:
|
||||
if item['employee_id_count'] >= 1:
|
||||
result[item['employee_id'][0]] = True
|
||||
for employee in self:
|
||||
employee.is_absent_totay = result[employee.id]
|
||||
|
||||
@api.multi
|
||||
def _search_absent_employee(self, operator, value):
|
||||
today_date = datetime.datetime.utcnow().date()
|
||||
today_start = fields.Datetime.to_string(today_date) # get the midnight of the current utc day
|
||||
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
|
||||
holidays = self.env['hr.leave'].sudo().search([
|
||||
('employee_id', '!=', False),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
('date_from', '<=', today_end),
|
||||
('date_to', '>=', today_start)
|
||||
('date_from', '<=', datetime.datetime.utcnow()),
|
||||
('date_to', '>=', datetime.datetime.utcnow())
|
||||
])
|
||||
return [('id', 'in', holidays.mapped('employee_id').ids)]
|
||||
|
||||
|
||||
@@ -778,9 +778,9 @@ class HolidaysRequest(models.Model):
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _get_responsible_for_approval(self):
|
||||
if self.state == 'confirm' and self.employee_id.leave_manager_id:
|
||||
if self.state == 'confirm' and self.employee_id.leave_manager_id and self.employee_id.leave_manager_id.has_group('hr_holidays.group_hr_holidays_team_leader'):
|
||||
return self.employee_id.leave_manager_id
|
||||
elif self.state == 'confirm' and self.employee_id.parent_id.user_id:
|
||||
elif self.state == 'confirm' and self.employee_id.parent_id.user_id and self.employee_id.parent_id.user_id.has_group('hr_holidays.group_hr_holidays_team_leader'):
|
||||
return self.employee_id.parent_id.user_id
|
||||
elif self.department_id.manager_id.user_id:
|
||||
return self.department_id.manager_id.user_id
|
||||
|
||||
@@ -232,7 +232,7 @@ class HolidaysType(models.Model):
|
||||
elif 'default_employee_id' in self._context:
|
||||
employee_id = self._context['default_employee_id']
|
||||
else:
|
||||
employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.user.id)], limit=1).id
|
||||
employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.user.id), ('company_id', '=', self.env.user.company_id.id)], limit=1).id
|
||||
return employee_id
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -4,11 +4,37 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
class User(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
leave_manager_id = fields.Many2one(related='employee_id.leave_manager_id')
|
||||
show_leaves = fields.Boolean(related='employee_id.show_leaves')
|
||||
allocation_used_count = fields.Float(related='employee_id.allocation_used_count')
|
||||
allocation_count = fields.Float(related='employee_id.allocation_count')
|
||||
leave_date_to = fields.Date(related='employee_id.leave_date_to')
|
||||
is_absent = fields.Boolean(related='employee_id.is_absent')
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
readable_fields = [
|
||||
'leave_manager_id',
|
||||
'show_leaves',
|
||||
'allocation_used_count',
|
||||
'allocation_count',
|
||||
'leave_date_to',
|
||||
'is_absent',
|
||||
]
|
||||
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 + readable_fields
|
||||
return init_res
|
||||
|
||||
def _compute_im_status(self):
|
||||
super(ResUsers, self)._compute_im_status()
|
||||
super(User, self)._compute_im_status()
|
||||
on_leave_user_ids = self._get_on_leave_ids()
|
||||
for user in self:
|
||||
if user.id in on_leave_user_ids:
|
||||
|
||||
@@ -118,8 +118,6 @@
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{
|
||||
'search_default_employee_id': [active_id],
|
||||
'default_employee_id': active_id,
|
||||
'search_default_group_type': 1,
|
||||
'search_default_year': 1}</field>
|
||||
<field name="domain">[('holiday_type','=','employee')]</field>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="context">{
|
||||
'search_default_is_absent_totay': 1,
|
||||
'search_default_is_absent': 1,
|
||||
'search_default_department_id': [active_id],
|
||||
'default_department_id': active_id}
|
||||
</field>
|
||||
@@ -104,7 +104,7 @@
|
||||
<field name="inherit_id" ref="hr.view_employee_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='department_id']" position="after">
|
||||
<filter name="is_absent_totay" string="Absent Today" domain="[('is_absent_totay', '=', True)]"/>
|
||||
<filter name="is_absent" string="Absent Today" domain="[('is_absent', '=', True)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -136,27 +136,102 @@
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='active_group']" position="after">
|
||||
<group string="Time Off">
|
||||
<label for="remaining_leaves"/>
|
||||
<div>
|
||||
<field name="remaining_leaves" class="oe_inline"/> days
|
||||
</div>
|
||||
<field name="leave_manager_id"/>
|
||||
</group>
|
||||
<xpath expr="//group[@name='managers']" position="inside">
|
||||
<field name="leave_manager_id"/>
|
||||
</xpath>
|
||||
<button name="toggle_active" position="before">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field name="current_leave_id" invisible="1"/>
|
||||
<field name="show_leaves" invisible="1"/>
|
||||
<button name="%(act_hr_employee_holiday_request)d"
|
||||
<field name="is_absent" invisible="1"/>
|
||||
<button name="%(hr_leave_action_new_request)d" type="action"
|
||||
class="oe_stat_button"
|
||||
attrs="{'invisible': [('is_absent', '=', False)]}">
|
||||
<div role="img" class="fa fa-fw fa-circle o_button_icon text-danger" aria-label="Absent Until" title="Absent Until"/>
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="leave_date_to"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Absent Until
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="%(hr_leave_action_new_request)d"
|
||||
type="action"
|
||||
class="oe_stat_button"
|
||||
icon="fa-calendar"
|
||||
attrs="{'invisible': [('show_leaves','=', False)]}"
|
||||
groups="base.group_user"
|
||||
help="Remaining time off">
|
||||
<field string="Time Off Left" name="leaves_count" widget="statinfo"/>
|
||||
help="Remaining leaves">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="allocation_used_count" digits="[42,1]"/>/
|
||||
<field name="allocation_count" digits="[42,1]"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Time Off
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_users_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.user.preferences.view.form.leave.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="hr.res_users_view_form_profile"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="%(hr_leave_action_new_request)d"
|
||||
string="Request Time off"
|
||||
type="action"
|
||||
class="btn btn-primary"/>
|
||||
</xpath>
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="%(hr_leave_allocation_action_my)d"
|
||||
string="Request Allocation"
|
||||
type="action"
|
||||
class="btn btn-primary"/>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='managers']" position="inside">
|
||||
<field name="leave_manager_id"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field name="show_leaves" invisible="1"/>
|
||||
<field name="employee_ids" invisible="1"/>
|
||||
<field name="is_absent" invisible="1"/>
|
||||
<button name="%(hr_leave_action_new_request)d" type="action"
|
||||
class="oe_stat_button"
|
||||
attrs="{'invisible': [('is_absent', '=', False)]}">
|
||||
<div role="img" class="fa fa-fw fa-circle o_button_icon text-danger" aria-label="Absent Until" title="Absent Until"/>
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="leave_date_to"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Absent Until
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="%(hr_leave_action_new_request)d"
|
||||
type="action"
|
||||
class="oe_stat_button"
|
||||
icon="fa-calendar"
|
||||
attrs="{'invisible': [('show_leaves','=', False)]}"
|
||||
groups="base.group_user"
|
||||
help="Remaining leaves">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="allocation_used_count" digits="[42,1]"/>/
|
||||
<field name="allocation_count" digits="[42,1]"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Time Off
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
'data': [
|
||||
'security/equipment.xml',
|
||||
'views/maintenance_views.xml',
|
||||
'views/hr_views.xml',
|
||||
],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import equipment
|
||||
from . import res_users
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class Users(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
equipment_ids = fields.One2many('maintenance.equipment', 'owner_user_id')
|
||||
equipment_count = fields.Integer('Equipments', compute='_compute_equipment_count')
|
||||
|
||||
@api.depends('equipment_ids')
|
||||
def _compute_equipment_count(self):
|
||||
for user in self:
|
||||
user.equipment_count = len(user.equipment_ids)
|
||||
|
||||
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.
|
||||
"""
|
||||
init_res = super(Users, self).__init__(pool, cr)
|
||||
# duplicate list to avoid modifying the original reference
|
||||
type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + ['equipment_count']
|
||||
return init_res
|
||||
|
||||
|
||||
class Employee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
equipment_count = fields.Integer(related='user_id.equipment_count')
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_employee_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.employee.view.form.inherit.maintenance</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="%(maintenance.hr_equipment_action)d"
|
||||
context="{'search_default_my':1}"
|
||||
groups="maintenance.group_equipment_manager"
|
||||
class="o_stat_button"
|
||||
icon="fa-cubes"
|
||||
type="action">
|
||||
<field name="equipment_count" widget="statinfo"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- user preferences -->
|
||||
<record id="res_users_view_form_preference" model="ir.ui.view">
|
||||
<field name="name">res.users.view.form.inherit.maintenance</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="hr.res_users_view_form_profile"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="%(maintenance.hr_equipment_action)d"
|
||||
context="{'search_default_my':1}"
|
||||
class="o_stat_button"
|
||||
icon="fa-cubes"
|
||||
type="action">
|
||||
<field name="equipment_count" widget="statinfo"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -14,6 +14,7 @@ This module extend the employee form with a organizational chart.
|
||||
(N+1, N+2, direct subordinates)
|
||||
""",
|
||||
'depends': ['hr'],
|
||||
'auto_install': True,
|
||||
'data': [
|
||||
'views/hr_templates.xml',
|
||||
'views/hr_views.xml'
|
||||
|
||||
@@ -58,10 +58,10 @@ var FieldOrgChart = AbstractField.extend({
|
||||
return this.dm.add(this._rpc({
|
||||
route: '/hr/get_subordinates',
|
||||
params: {
|
||||
employee_id: employee_id,
|
||||
employee_id: employee_id,
|
||||
subordinates_type: type
|
||||
},
|
||||
}))
|
||||
}));
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -75,7 +75,8 @@ var FieldOrgChart = AbstractField.extend({
|
||||
}));
|
||||
}
|
||||
else if (!this.employee) {
|
||||
this.employee = this.recordData.id
|
||||
// the widget is either dispayed in the context of a hr.employee form or a res.users form
|
||||
this.employee = this.recordData.employee_ids !== undefined ? this.recordData.employee_ids.res_ids[0] : this.recordData.id;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
|
||||
// SMALL DESKTOP LAYOUT
|
||||
@include media-breakpoint-up(md) {
|
||||
#o_employee_container {
|
||||
#o_work_employee_container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
#o_employee_main {
|
||||
#o_work_employee_main {
|
||||
flex: 1 1 60%;
|
||||
}
|
||||
#o_employee_right {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<div id="o_employee_main" position="after">
|
||||
<div id="o_work_employee_main" position="after">
|
||||
<div id="o_employee_right">
|
||||
<h4 class="o_org_chart_title mb16 mt0">Organization Chart</h4>
|
||||
<field name="child_ids" widget="hr_org_chart"/>
|
||||
@@ -13,4 +13,19 @@
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_users_view_form" model="ir.ui.view">
|
||||
<field name="name">res.users.preferences.view.form.inherit.org_chart</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="hr.res_users_view_form_profile"/>
|
||||
<field name="arch" type="xml">
|
||||
<div id="o_work_employee_main" position="after">
|
||||
<div id="o_employee_right">
|
||||
<field name="employee_ids" invisible="1"/>
|
||||
<h4 class="o_org_chart_title mb16 mt0">Organization Chart</h4>
|
||||
<field name="child_ids" widget="hr_org_chart"/>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="%(act_hr_employee_payslip_list)d"
|
||||
class="oe_stat_button"
|
||||
icon="fa-money"
|
||||
|
||||
@@ -19,8 +19,8 @@ Allows to contact directly the employee in case of unjustified absence.
|
||||
'depends': ['hr', 'hr_holidays', 'sms'],
|
||||
'data': [
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/hr_employee_views.xml',
|
||||
'data/ir_actions_server.xml',
|
||||
'views/hr_employee_views.xml',
|
||||
'data/mail_data.xml',
|
||||
],
|
||||
'demo': [],
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record forcecreate="True" id="ir_cron_presence_control" model="ir.cron">
|
||||
<field name="name">HR Presence: cron</field>
|
||||
<field name="model_id" ref="hr.model_hr_employee"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._check_presence()</field>
|
||||
<field eval="True" name="active" />
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo import fields, models, _, api
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.fields import Datetime
|
||||
|
||||
@@ -16,16 +16,18 @@ class ResCompany(models.Model):
|
||||
hr_presence_state = fields.Selection([
|
||||
('present', 'Present'),
|
||||
('absent', 'Absent'),
|
||||
('to_define', 'To Define')], default='to_define', groups='hr.group_hr_manager')
|
||||
('to_define', 'To Define')], default='to_define')
|
||||
last_activity = fields.Date(compute="_compute_last_activity")
|
||||
|
||||
def _compute_last_activity(self):
|
||||
employees = self.filtered(lambda e: e.user_id)
|
||||
presences = self.env['bus.presence'].search([('user_id', 'in', employees.mapped('user_id.id'))])
|
||||
|
||||
def _action_open_presence_view(self):
|
||||
# Compute the presence/absence for the employees on the same
|
||||
# company than the HR/manager. Then opens the kanban view
|
||||
# of the employees with an undefined presence/absence
|
||||
|
||||
_logger.info("Employees presence checked by: %s" % self.env.user.name)
|
||||
for presence in presences:
|
||||
presence.user_id.employee_ids.last_activity = presence.last_presence.date()
|
||||
|
||||
@api.model
|
||||
def _check_presence(self):
|
||||
company = self.env.user.company_id
|
||||
if not company.hr_presence_last_compute_date or \
|
||||
company.hr_presence_last_compute_date.day != Datetime.now().day:
|
||||
@@ -85,6 +87,16 @@ class ResCompany(models.Model):
|
||||
|
||||
company.hr_presence_last_compute_date = Datetime.now()
|
||||
|
||||
@api.model
|
||||
def _action_open_presence_view(self):
|
||||
# Compute the presence/absence for the employees on the same
|
||||
# company than the HR/manager. Then opens the kanban view
|
||||
# of the employees with an undefined presence/absence
|
||||
|
||||
_logger.info("Employees presence checked by: %s" % self.env.user.name)
|
||||
|
||||
self._check_presence()
|
||||
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.employee",
|
||||
|
||||
@@ -12,6 +12,37 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.employee.presence.form.inherit</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="groups_id" eval="[(4, ref('hr.group_hr_manager'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field name="hr_presence_state" invisible="1"/>
|
||||
<button name="%(ir_actions_server_action_open_presence_view)d"
|
||||
class="oe_stat_button"
|
||||
type="action"
|
||||
attrs="{'invisible': ['|', ('hr_presence_state', '=', 'absent'), ('user_id', '=', False)]}">
|
||||
<div role="img" class="fa fa-fw fa-circle text-success o_button_icon" attrs="{'invisible': [('hr_presence_state', '!=', 'present')]}" aria-label="Available" title="Available"/>
|
||||
<div role="img" class="fa fa-fw fa-circle text-warning o_button_icon" attrs="{'invisible': [('hr_presence_state', '!=', 'to_define')]}" aria-label="Not available" title="Not available"/>
|
||||
|
||||
<div class="o_stat_info" attrs="{'invisible': [('hr_presence_state', '!=', 'to_define')]}">
|
||||
<span class="o_stat_value">
|
||||
<field name="last_activity"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Last Activity
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_stat_info" attrs="{'invisible': [('hr_presence_state', '!=', 'present')]}">
|
||||
<span class="o_stat_text">Connected</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_view_kanban" model="ir.ui.view">
|
||||
<field name="name">hr.employee.view.kanban</field>
|
||||
<field name="model">hr.employee</field>
|
||||
@@ -32,4 +63,32 @@
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_view_kanban" model="ir.ui.view">
|
||||
<field name="name">hr.employee</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.hr_kanban_view_employees"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="arch" type="xml">
|
||||
<templates position="before">
|
||||
<field name="hr_presence_state"/>
|
||||
</templates>
|
||||
<field name="name" position="replace">
|
||||
<div>
|
||||
<div class="float-right" t-if="record.hr_presence_state.raw_value == 'present'">
|
||||
<span class="fa fa-circle text-success" role="img" aria-label="Present" title="Present" name="presence_present"></span>
|
||||
</div>
|
||||
<div class="float-right" t-if="record.hr_presence_state.raw_value == 'absent'">
|
||||
<span class="fa fa-circle text-danger" role="img" aria-label="Absent" title="Absent" name="presence_absent"></span>
|
||||
</div>
|
||||
<div class="float-right" t-if="record.hr_presence_state.raw_value == 'to_define'">
|
||||
<span class="fa fa-circle text-warning" role="img" aria-label="To define" title="To define" name="presence_to_define"></span>
|
||||
</div>
|
||||
<strong>
|
||||
<field name="name" placeholder="Employee's Name"/>
|
||||
</strong>
|
||||
</div>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_hr_presence"/>
|
||||
<div class="content-group">
|
||||
<div class="content-group" name="hr_presence_options">
|
||||
<div class="row">
|
||||
<field name="hr_presence_control_login" class="col-lg-1 ml16"/>
|
||||
<label for="hr_presence_control_login" class="col-lg-10 o_light_label"/>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='preferences']" position="after">
|
||||
<xpath expr="//footer" position="before">
|
||||
<group name="livechat" string="Livechat Preferences">
|
||||
<group>
|
||||
<field name="livechat_username" readonly="0"/>
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import l10n_be_hr_payroll
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- 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']
|
||||
|
||||
spouse_fiscal_status = fields.Selection(related='employee_ids.spouse_fiscal_status', readonly=False)
|
||||
disabled = fields.Boolean(related='employee_ids.disabled', readonly=False)
|
||||
disabled_spouse_bool = fields.Boolean(related='employee_ids.disabled_spouse_bool', readonly=False)
|
||||
disabled_children_bool = fields.Boolean(related='employee_ids.disabled_children_bool', readonly=False)
|
||||
resident_bool = fields.Boolean(related='employee_ids.resident_bool', readonly=False)
|
||||
disabled_children_number = fields.Integer(related='employee_ids.disabled_children_number', readonly=False)
|
||||
dependent_children = fields.Integer(related='employee_ids.dependent_children', readonly=False)
|
||||
other_dependent_people = fields.Boolean(related='employee_ids.other_dependent_people', readonly=False)
|
||||
other_senior_dependent = fields.Integer(related='employee_ids.other_senior_dependent', readonly=False)
|
||||
other_disabled_senior_dependent = fields.Integer(related='employee_ids.other_disabled_senior_dependent', readonly=False)
|
||||
other_juniors_dependent = fields.Integer(related='employee_ids.other_juniors_dependent', readonly=False)
|
||||
other_disabled_juniors_dependent = fields.Integer(related='employee_ids.other_disabled_juniors_dependent', readonly=False)
|
||||
dependent_seniors = fields.Integer(related='employee_ids.dependent_seniors', readonly=False)
|
||||
dependent_juniors = fields.Integer(related='employee_ids.dependent_juniors', readonly=False)
|
||||
spouse_net_revenue = fields.Float(related='employee_ids.spouse_net_revenue', readonly=False)
|
||||
spouse_other_net_revenue = fields.Float(related='employee_ids.spouse_other_net_revenue', readonly=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.
|
||||
"""
|
||||
l10n_be_payroll_readable_fields = [
|
||||
'spouse_fiscal_status',
|
||||
'disabled',
|
||||
'disabled_spouse_bool',
|
||||
'disabled_children_bool',
|
||||
'resident_bool',
|
||||
'disabled_children_number',
|
||||
'dependent_children',
|
||||
'other_dependent_people',
|
||||
'other_senior_dependent',
|
||||
'other_disabled_senior_dependent',
|
||||
'other_juniors_dependent',
|
||||
'other_disabled_juniors_dependent',
|
||||
'dependent_seniors',
|
||||
'dependent_juniors',
|
||||
'spouse_net_revenue',
|
||||
'spouse_other_net_revenue',
|
||||
]
|
||||
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 + l10n_be_payroll_readable_fields
|
||||
type(self).SELF_WRITEABLE_FIELDS = type(self).SELF_WRITEABLE_FIELDS + l10n_be_payroll_readable_fields
|
||||
return init_res
|
||||
@@ -148,4 +148,36 @@
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_users_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.user.preferences.view.form.l10n.be.payroll.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="hr.res_users_view_form_profile"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='vehicle']" position="after">
|
||||
<field name="resident_bool" eval="False"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='marital']" position="after">
|
||||
<field name="spouse_fiscal_status" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])], 'required': [('marital', 'in', ['married', 'cohabitant'])]}" colspan="1" help="if spouse has professionnel income or not"/>
|
||||
<field name="disabled_spouse_bool" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}" colspan="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='gender']" position="after">
|
||||
<field name="disabled"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='children']" position="after">
|
||||
<field name="disabled_children_bool"/>
|
||||
<field name="disabled_children_number" attrs="{'invisible':[('disabled_children_bool','!=',True)]}" colspan="1" help="number of dependent children declared as disabled"/>
|
||||
<field name="dependent_children" invisible="1"/>
|
||||
<field name="other_dependent_people" groups="hr.group_hr_user"/>
|
||||
<field name="other_senior_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
|
||||
<field name="other_disabled_senior_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
|
||||
<field name="other_juniors_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
|
||||
<field name="other_disabled_juniors_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='spouse_fiscal_status']" position="after">
|
||||
<field name="spouse_net_revenue" attrs="{'invisible': ['|', ('marital', 'not in', ['married', 'cohabitant']), ('spouse_fiscal_status', '!=', 'with income')]}"/>
|
||||
<field name="spouse_other_net_revenue" attrs="{'invisible': ['|', ('marital', 'not in', ['married', 'cohabitant']), ('spouse_fiscal_status', '!=', 'with income')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -15,3 +15,13 @@ class Users(models.Model):
|
||||
('idle', 'Idle'),
|
||||
('disabled', 'Disabled'),
|
||||
], string="OdooBot Status", readonly=True, required=True, default="not_initialized") # keep track of the state: correspond to the code of the last message sent
|
||||
|
||||
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.
|
||||
"""
|
||||
init_res = super(Users, self).__init__(pool, cr)
|
||||
# duplicate list to avoid modifying the original reference
|
||||
type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + ['odoobot_state']
|
||||
return init_res
|
||||
|
||||
@@ -97,10 +97,8 @@ var UserMenu = Widget.extend({
|
||||
this.trigger_up('clear_uncommitted_changes', {
|
||||
callback: function () {
|
||||
self._rpc({
|
||||
route: "/web/action/load",
|
||||
params: {
|
||||
action_id: "base.action_res_users_my",
|
||||
},
|
||||
model: "res.users",
|
||||
method: "action_get"
|
||||
})
|
||||
.done(function (result) {
|
||||
result.res_id = session.uid;
|
||||
|
||||
@@ -174,10 +174,9 @@
|
||||
<td width="100%"><input type="password" class="o_field_widget o_input" name="confirm_pwd" minlength="1" autocomplete="new-password"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<button type="button" class="btn btn-primary oe_form_button">Change Password</button>
|
||||
<button type="button" class="btn btn-secondary oe_form_button oe_form_button_cancel" href="javascript:void(0)">Cancel</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary oe_form_button">Change Password</button>
|
||||
<button type="button" class="btn btn-secondary oe_form_button oe_form_button_cancel" href="javascript:void(0)">Cancel</button>
|
||||
</form>
|
||||
|
||||
<div t-name="Action" class="o_action">
|
||||
|
||||
@@ -19,6 +19,7 @@ class Users(models.Model):
|
||||
set(
|
||||
self.SELF_WRITEABLE_FIELDS +
|
||||
['country_id', 'city', 'website', 'website_description', 'website_published']))
|
||||
type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + ['karma']
|
||||
return init_res
|
||||
|
||||
create_date = fields.Datetime('Create Date', readonly=True, index=True)
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="toggle_active" position="before">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button class="oe_stat_button" name="website_publish_button"
|
||||
type="object" icon="fa-globe" groups="hr.group_hr_user">
|
||||
<field name="is_published" widget="website_button"/>
|
||||
</button>
|
||||
</button>
|
||||
<xpath expr="//page[@name='public']/group" position="inside">
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='managers']" position="after">
|
||||
<group>
|
||||
<field name="public_info"/>
|
||||
</group>
|
||||
|
||||
@@ -353,7 +353,7 @@ class IrModelFields(models.Model):
|
||||
domain = fields.Char(default="[]", help="The optional domain to restrict possible values for relationship fields, "
|
||||
"specified as a Python expression defining a list of triplets. "
|
||||
"For example: [('color','=','red')]")
|
||||
groups = fields.Many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id')
|
||||
groups = fields.Many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id') # CLEANME unimplemented field (empty table)
|
||||
selectable = fields.Boolean(default=True)
|
||||
modules = fields.Char(compute='_in_modules', string='In Apps', help='List of modules in which the field is defined')
|
||||
relation_table = fields.Char(help="Used for custom many2many fields to define a custom relation table name")
|
||||
|
||||
@@ -543,9 +543,8 @@ class Users(models.Model):
|
||||
}
|
||||
|
||||
@api.model
|
||||
@api.returns('ir.actions.act_window', lambda record: record.id)
|
||||
def action_get(self):
|
||||
return self.sudo().env.ref('base.action_res_users_my')
|
||||
return self.sudo().env.ref('base.action_res_users_my').read()[0]
|
||||
|
||||
def check_super(self, passwd):
|
||||
return check_super(passwd)
|
||||
|
||||
@@ -34,37 +34,6 @@
|
||||
<field name="perm_write" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Security restriction for private addresses -->
|
||||
<record id="res_partner_rule_private_employee" model="ir.rule">
|
||||
<field name="name">res.partner.rule.private.employee</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="domain_force">
|
||||
[('type', '!=', 'private')]
|
||||
</field>
|
||||
<field name="groups" eval="[
|
||||
(4, ref('base.group_user')),
|
||||
]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
<!-- Relex previous rule for group_private_addresses -->
|
||||
<record id="res_partner_rule_private_group" model="ir.rule">
|
||||
<field name="name">res.partner.rule.private.group</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="domain_force">
|
||||
[('type', '=', 'private')]
|
||||
</field>
|
||||
<field name="groups" eval="[
|
||||
(4, ref('base.group_private_addresses'))
|
||||
]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="ir_default_user_rule">
|
||||
<field name="name">Defaults: alter personal defaults</field>
|
||||
<field name="model_id" ref="model_ir_default"/>
|
||||
|
||||
@@ -340,23 +340,23 @@
|
||||
<h1>
|
||||
<field name="name" readonly="1" class="oe_inline"/>
|
||||
</h1>
|
||||
<button name="preference_change_password" type="object" string="Change password" class="oe_link"/>
|
||||
<group name="preferences" col="4">
|
||||
<field name="lang" readonly="0" placeholder="System (English)"/>
|
||||
<field name="tz" widget="timezone_mismatch" options="{'tz_offset_field': 'tz_offset'}" readonly="0"/>
|
||||
<field name="tz_offset" invisible="1"/>
|
||||
<field name="company_id" options="{'no_create': True}" readonly="0"
|
||||
groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group string="Email Preferences">
|
||||
<group name="preference_email">
|
||||
<group name="preferences">
|
||||
<group>
|
||||
<field name="email" widget="email" readonly="0"/>
|
||||
<field name="company_id" options="{'no_create': True}" readonly="0"
|
||||
groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="lang" readonly="0" placeholder="System (English)"/>
|
||||
<field name="tz" widget="timezone_mismatch" options="{'tz_offset_field': 'tz_offset'}" readonly="0"/>
|
||||
<field name="tz_offset" invisible="1"/>
|
||||
</group>
|
||||
<group name="preference_contact"></group>
|
||||
</group>
|
||||
<group name="preference_contact"></group>
|
||||
<group name="signature">
|
||||
<field name="signature" readonly="0"/>
|
||||
</group>
|
||||
<button name="preference_change_password" type="object" string="Change password" class="oe_link"/>
|
||||
<footer>
|
||||
<button name="preference_save" type="object" string="Save" class="btn-primary"/>
|
||||
<button name="preference_cancel" string="Cancel" special="cancel" class="btn-secondary"/>
|
||||
|
||||
Reference in New Issue
Block a user