[IMP] Add employee profile

[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. The Profile will show the employee of the current company.

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.

## Changed modules

### hr_attendance

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 hour.
-> The status is accurate on the report view (status is still updated
   when loading the view)
-> The status in accurate at 1 hour on the kanban view

### [ADD] hr_attendance_presence

Bridge module between `hr_attendance` and `hr_presence`.

This PR 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

[ADD] hr_skills: Introduce a new module for employee resumé and skills
=======================================================

Purpose
-----------

Consultancy companies need resumé and skills of their consultants.
For big projects, they often need to send them to their customers.
These information are also useful to statistics.

Specification
-----------------

### New models

####  `hr.resume.line.type`
Types of resumé lines. e.g. *Experience*, *Education*, *Hobbies*

#### `hr.resume.line`
It is a line in the resumé of an employee.

#### `hr.skill`
Name of a skill. e.g *French*, *Python*, *Piano*

#### `hr.skill.type`
Skills can belongs to a particular type. A skill type has skill levels associated.
e.g. *Languages*, *Dev*, *Music*

#### `hr.skill.level`
Levels available for a particular skill type. Each level has a label
and a progress (between 0 and 100) associated.
e.g. *Intermediary (20%)*, *Advanced (85%)*, *Expert (100%)*

#### `hr.employee.skill`
These are skills which employees have. It links an employee with a particular skill
and level.
e.g. Mitchell has an *Intermediary* level in *Python*

### Access Rights

Only a `hr_user` can create/edit `hr.resume.line.type`, `hr.skill`, `hr.skill.level`, `hr.skill.type`.
If employees are allowed to edit their infos (setting), they can also create/edit `hr.resume.line`,
`hr.employee.skill` for themselves.

### UI

Resumé lines are displayed, grouped by type, in a new 'Resumé' tab in the employee
form. Resumé lines can be reordered (handle widget)

Emloyee skills are displayed in the Resumé tab, grouped by skill type.

[IMP] hr, hr_holidays, hr_expense: Change onchange parent_id behaviour
=========================================================

Purpose
-----------

When the manager (parent_id) of an employee changes, it doesn't always mean that other responsibles (Leave responsible, expense responsible, coach) should also change.

Specification
-----------------

When changing the employee's manager, the leave responsible, the expense responsible and the coach should be changed only if the field was not set or if the responsible is the previous manager. In that case, they should be set to the new manager. Otherwise, it means the field was most probably set manually and it should be left unchanged.

Task 1913089

--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr

closes odoo/odoo#30502
This commit is contained in:
Robot Odoo
2019-02-14 19:09:34 +01:00
committed by GitHub
97 changed files with 3071 additions and 271 deletions
+2 -1
View File
@@ -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'],
}
+62 -28
View File
@@ -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):
@@ -218,6 +224,13 @@ class Employee(models.Model):
if employee.pin and not employee.pin.isdigit():
raise ValidationError(_("The PIN must be a sequence of digits."))
@api.onchange('parent_id')
def _onchange_parent_id(self):
manager = self.parent_id
previous_manager = self._origin.parent_id
if manager and (self.coach_id == previous_manager or not self.coach_id):
self.coach_id = manager
@api.onchange('job_id')
def _onchange_job_id(self):
if self.job_id:
@@ -334,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"
+2
View File
@@ -11,6 +11,8 @@ class ResConfigSettings(models.TransientModel):
related='company_id.resource_calendar_id', readonly=False)
module_hr_org_chart = fields.Boolean(string="Show Organizational Chart")
module_hr_presence = fields.Boolean(string="Control presence of employees")
module_hr_skills = fields.Boolean(string="Employee Skills and Resumé")
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')
+14
View File
@@ -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
+142 -3
View File
@@ -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)
+71
View File
@@ -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>
+1
View File
@@ -4,3 +4,4 @@
from . import test_hr_employee
from . import test_channel
from . import test_hr_flow
from . import test_self_user_access
+122
View File
@@ -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'})
+1
View File
@@ -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>
+72 -59
View File
@@ -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 &amp; 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>
@@ -600,6 +602,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>
@@ -8,6 +8,19 @@
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Employees" string="Employees" data-key="hr" groups="hr.group_hr_manager">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_hr_skills"/>
</div>
<div class="o_setting_right_pane">
<label for="module_hr_skills"/>
<div class="text-muted">
Manage skills, knowledge and resumé of your employees
</div>
</div>
</div>
</div>
<h2>Work Organization</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
@@ -42,6 +55,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>
+152
View File
@@ -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>
+1
View File
@@ -3,3 +3,4 @@
from . import res_config_settings
from . import hr_attendance
from . import hr_employee
from . import res_users
+13 -15
View File
@@ -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):
+25
View File
@@ -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>
+1
View File
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_contract
from . import res_users
-1
View File
@@ -12,7 +12,6 @@ from odoo.osv import expression
class Employee(models.Model):
_inherit = "hr.employee"
manager = fields.Boolean(string='Is a Manager')
medic_exam = fields.Date(string='Medical Examination Date', groups="hr.group_hr_user")
vehicle = fields.Char(string='Company Vehicle', groups="hr.group_hr_user")
contract_ids = fields.One2many('hr.contract', 'employee_id', string='Employee Contracts')
+27
View File
@@ -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
+18 -4
View File
@@ -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"
@@ -28,9 +28,6 @@
<field name="contracts_count" widget="statinfo" string="Contracts"/>
</button>
</xpath>
<xpath expr="//field[@name='coach_id']" position="after">
<field name="manager" groups="base.group_no_one"/>
</xpath>
<xpath expr="//group[@name='active_group']" position="after">
<group string="Current Contract">
<field name="medic_exam" string="Medical Exam"/>
@@ -44,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>
+37 -2
View File
@@ -1,12 +1,47 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
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')
def _onchange_parent_id(self):
super(Employee, self)._onchange_parent_id()
previous_manager = self._origin.parent_id.user_id
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
+13 -4
View File
@@ -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>
+1
View File
@@ -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
+26
View File
@@ -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()
+29 -8
View File
@@ -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>
+36 -26
View File
@@ -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,15 @@ 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):
super(Employee, self)._onchange_parent_id()
previous_manager = self._origin.parent_id.user_id
manager = self.parent_id.user_id
if manager and manager.has_group('hr.group_hr_user') and (self.leave_manager_id == previous_manager or not self.leave_manager_id):
self.leave_manager_id = manager
@api.multi
def _compute_show_leaves(self):
@@ -153,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)]
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+28 -2
View File
@@ -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>
@@ -169,17 +169,18 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) {
* @private
*/
_onNewAllocation: function () {
var self = this;
this.do_action({
type: 'ir.actions.act_window',
res_model: 'hr.leave.allocation',
view_type: 'form',
views: [[false,'form']],
target: 'new',
}), {
}, {
on_close: function () {
self.reload();
}
};
});
},
});
var TimeOffCalendarView = CalendarView.extend({
+90 -15
View File
@@ -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>
+1
View File
@@ -11,6 +11,7 @@
'data': [
'security/equipment.xml',
'views/maintenance_views.xml',
'views/hr_views.xml',
],
'demo': [],
'installable': True,
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import equipment
from . import res_users
+29
View File
@@ -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')
+38
View File
@@ -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>
+1
View File
@@ -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 {
+16 -1
View File
@@ -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>
@@ -101,7 +101,6 @@
<!-- Employee -->
<record id="hr_employee_payroll" model="hr.employee">
<field eval="0" name="manager"/>
<field name="company_id" ref="base.main_company"/>
<field eval="1" name="active"/>
<field name="name">Roger Scott</field>
@@ -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"
+1 -1
View File
@@ -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': [],
+16
View File
@@ -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>
+20 -8
View File
@@ -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"/>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+31
View File
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Skills Management',
'category': 'Hidden',
'version': '1.0',
'summary': 'Manage skills, knowledge and resumé of your employees',
'description':
"""
Skills and Resumé for HR
========================
This module introduces skills and resumé management for employees.
""",
'depends': ['hr'],
'data': [
'security/ir.model.access.csv',
'security/hr_skills_security.xml',
'views/hr_views.xml',
'views/hr_templates.xml',
'data/hr_resume_data.xml',
],
'demo': ['data/hr_resume_demo.xml'],
'qweb': [
'static/src/xml/resume_templates.xml',
'static/src/xml/skills_templates.xml',
],
'installable': True,
'application': True,
}
+13
View File
@@ -0,0 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data>
<record id="resume_type_experience" model="hr.resume.line.type">
<field name="name">Experience</field>
</record>
<record id="resume_type_education" model="hr.resume.line.type">
<field name="name">Education</field>
</record>
</data>
</odoo>
+317
View File
@@ -0,0 +1,317 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!--Skill Types-->
<record id="hr_skill_type_lang" model="hr.skill.type">
<field name="name">Languages</field>
</record>
<record id="hr_skill_type_dev" model="hr.skill.type">
<field name="name">Dev</field>
</record>
<record id="hr_skill_type_music" model="hr.skill.type">
<field name="name">Music</field>
</record>
<!--Skill Levels-->
<record id="hr_skill_level_a1" model="hr.skill.level">
<field name="name">A1</field>
<field name="level_progress">10</field>
<field name="sequence">5</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_level_a2" model="hr.skill.level">
<field name="name">A2</field>
<field name="level_progress">40</field>
<field name="sequence">10</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_level_b1" model="hr.skill.level">
<field name="name">B1</field>
<field name="level_progress">60</field>
<field name="sequence">15</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_level_b2" model="hr.skill.level">
<field name="name">B2</field>
<field name="level_progress">75</field>
<field name="sequence">20</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_level_c1" model="hr.skill.level">
<field name="name">C1</field>
<field name="level_progress">85</field>
<field name="sequence">25</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_level_c2" model="hr.skill.level">
<field name="name">C2</field>
<field name="level_progress">100</field>
<field name="sequence">35</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_level_beginner" model="hr.skill.level">
<field name="name">Beginner</field>
<field name="level_progress">15</field>
<field name="sequence">1</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_level_elementary" model="hr.skill.level">
<field name="name">Elementary</field>
<field name="level_progress">25</field>
<field name="sequence">5</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_level_intermediate" model="hr.skill.level">
<field name="name">Intermediate</field>
<field name="level_progress">50</field>
<field name="sequence">15</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_level_advanced" model="hr.skill.level">
<field name="name">Advanced</field>
<field name="level_progress">80</field>
<field name="sequence">25</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_level_expert" model="hr.skill.level">
<field name="name">Expert</field>
<field name="level_progress">100</field>
<field name="sequence">35</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_level_l1" model="hr.skill.level">
<field name="name">L1</field>
<field name="level_progress">25</field>
<field name="sequence">1</field>
<field name="skill_type_id" ref="hr_skill_type_music"/>
</record>
<record id="hr_skill_level_l2" model="hr.skill.level">
<field name="name">L2</field>
<field name="level_progress">50</field>
<field name="sequence">5</field>
<field name="skill_type_id" ref="hr_skill_type_music"/>
</record>
<record id="hr_skill_level_l3" model="hr.skill.level">
<field name="name">L3</field>
<field name="level_progress">75</field>
<field name="sequence">15</field>
<field name="skill_type_id" ref="hr_skill_type_music"/>
</record>
<record id="hr_skill_level_l4" model="hr.skill.level">
<field name="name">L4</field>
<field name="level_progress">100</field>
<field name="sequence">25</field>
<field name="skill_type_id" ref="hr_skill_type_music"/>
</record>
<!-- **** Skills **** -->
<!-- Languages -->
<record id="hr_skill_french" model="hr.skill">
<field name="name">French</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_spanish" model="hr.skill">
<field name="name">Spanish</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_english" model="hr.skill">
<field name="name">English</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_german" model="hr.skill">
<field name="name">German</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<record id="hr_skill_filipino" model="hr.skill">
<field name="name">Filipino</field>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
</record>
<!-- Dev -->
<record id="hr_skill_js" model="hr.skill">
<field name="name">Javascript</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_python" model="hr.skill">
<field name="name">Python</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_c" model="hr.skill">
<field name="name">C/C++</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_android" model="hr.skill">
<field name="name">Android</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_hadoop" model="hr.skill">
<field name="name">Hadoop</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<record id="hr_skill_spark" model="hr.skill">
<field name="name">Spark</field>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
</record>
<!-- Music -->
<record id="hr_skill_piano" model="hr.skill">
<field name="name">Piano</field>
<field name="skill_type_id" ref="hr_skill_type_music"/>
</record>
<record id="hr_skill_violin" model="hr.skill">
<field name="name">Violin</field>
<field name="skill_type_id" ref="hr_skill_type_music"/>
</record>
<record id="hr_skill_singing" model="hr.skill">
<field name="name">Singing</field>
<field name="skill_type_id" ref="hr_skill_type_music"/>
</record>
<record id="hr_skill_flute" model="hr.skill">
<field name="name">Flute</field>
<field name="skill_type_id" ref="hr_skill_type_music"/>
</record>
<!-- **** Employee Skills **** -->
<!-- Abigael -->
<record id="employee_level_1" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_hne"/>
<field name="skill_id" ref="hr_skill_french"/>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
<field name="skill_level_id" ref="hr_skill_level_b2"/>
</record>
<record id="employee_level_2" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_hne"/>
<field name="skill_id" ref="hr_skill_filipino"/>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
<field name="skill_level_id" ref="hr_skill_level_a1"/>
</record>
<record id="employee_level_3" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_hne"/>
<field name="skill_id" ref="hr_skill_english"/>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
<field name="skill_level_id" ref="hr_skill_level_c2"/>
</record>
<!-- Admin -->
<record id="employee_level_4" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="skill_id" ref="hr_skill_spanish"/>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
<field name="skill_level_id" ref="hr_skill_level_c2"/>
</record>
<record id="employee_level_5" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="skill_id" ref="hr_skill_filipino"/>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
<field name="skill_level_id" ref="hr_skill_level_a2"/>
</record>
<record id="employee_level_6" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="skill_id" ref="hr_skill_js"/>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
<field name="skill_level_id" ref="hr_skill_level_intermediate"/>
</record>
<record id="employee_level_7" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="skill_id" ref="hr_skill_c"/>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
<field name="skill_level_id" ref="hr_skill_level_advanced"/>
</record>
<record id="employee_level_8" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="skill_id" ref="hr_skill_spark"/>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
<field name="skill_level_id" ref="hr_skill_level_expert"/>
</record>
<!-- Demo -->
<record id="employee_level_9" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="skill_id" ref="hr_skill_french"/>
<field name="skill_type_id" ref="hr_skill_type_lang"/>
<field name="skill_level_id" ref="hr_skill_level_c2"/>
</record>
<record id="employee_level_10" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="skill_id" ref="hr_skill_js"/>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
<field name="skill_level_id" ref="hr_skill_level_elementary"/>
</record>
<record id="employee_level_11" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="skill_id" ref="hr_skill_python"/>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
<field name="skill_level_id" ref="hr_skill_level_expert"/>
</record>
<record id="employee_level_12" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="skill_id" ref="hr_skill_spark"/>
<field name="skill_type_id" ref="hr_skill_type_dev"/>
<field name="skill_level_id" ref="hr_skill_level_expert"/>
</record>
<record id="employee_level_13" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="skill_id" ref="hr_skill_piano"/>
<field name="skill_type_id" ref="hr_skill_type_music"/>
<field name="skill_level_id" ref="hr_skill_level_l1"/>
</record>
<record id="employee_level_14" model="hr.employee.skill">
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="skill_id" ref="hr_skill_singing"/>
<field name="skill_type_id" ref="hr_skill_type_music"/>
<field name="skill_level_id" ref="hr_skill_level_l3"/>
</record>
<!-- Resumé -->
<record id="employee_resume_line_admin_1" model="hr.resume.line">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="name">Université Libre de Bruxelles - Polytechnique</field>
<field name="date_start" eval="(datetime.now()+relativedelta(years=-12)).strftime('%Y-09-17')"/>
<field name="date_end" eval="(datetime.now()+relativedelta(years=-7)).strftime('%Y-09-10')"/>
<field name="line_type_id" ref="resume_type_education"/>
<field name="description">
Master in Electrical engineering
Master thesis: Better grid management and control through machine learning
</field>
</record>
<record id="employee_resume_line_admin_2" model="hr.resume.line">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="name">Saint-Joseph School</field>
<field name="date_start" eval="(datetime.now()+relativedelta(years=-18)).strftime('%Y-09-01')"/>
<field name="date_end" eval="(datetime.now()+relativedelta(years=-12)).strftime('%Y-06-30')"/>
<field name="line_type_id" ref="resume_type_education"/>
<field name="description">
Science &amp; math
</field>
</record>
<record id="employee_resume_line_admin_4" model="hr.resume.line">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="name">Odoo SA</field>
<field name="sequence">5</field>
<field name="date_start" eval="(datetime.now()+relativedelta(years=-3)).strftime('%Y-11-01')"/>
<field name="line_type_id" ref="resume_type_experience"/>
<field name="description">
Job position: Development team leader
Core Python Framework
</field>
</record>
<record id="employee_resume_line_admin_3" model="hr.resume.line">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="name">Burtho Inc.</field>
<field name="sequence">15</field>
<field name="date_start" eval="(datetime.now()+relativedelta(years=-7)).strftime('%Y-09-10')"/>
<field name="date_end" eval="(datetime.now()+relativedelta(years=-3)).strftime('%Y-09-10')"/>
<field name="line_type_id" ref="resume_type_experience"/>
<field name="description">
Job position: Product manager
Business strategy, functional requirements, resource planning, product lifecycle management, etc.
</field>
</record>
</data>
</odoo>
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_resume
from . import hr_skills
from . import res_users
+51
View File
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class Employee(models.Model):
_inherit = 'hr.employee'
resume_line_ids = fields.One2many('hr.resume.line', 'employee_id', string="Resumé lines")
employee_skill_ids = fields.One2many('hr.employee.skill', 'employee_id', string="Skills")
@api.model_create_multi
def create(self, vals_list):
res = super(Employee, self).create(vals_list)
resume_lines_values = []
for employee in res:
line_type = self.env.ref('hr_skills.resume_type_experience', raise_if_not_found=False)
resume_lines_values.append({
'employee_id': employee.id,
'name': employee.company_id.name,
'date_start': employee.create_date.date(),
'description': employee.job_title,
'line_type_id': line_type and line_type.id,
})
self.env['hr.resume.line'].create(resume_lines_values)
return res
class ResumeLine(models.Model):
_name = 'hr.resume.line'
_description = "Resumé line of an employee"
employee_id = fields.Many2one('hr.employee', required=True, ondelete='cascade')
name = fields.Char(required=True)
date_start = fields.Date(required=True)
date_end = fields.Date()
description = fields.Text(string="Description")
line_type_id = fields.Many2one('hr.resume.line.type', string="Type")
sequence = fields.Integer(default=100)
_sql_constraints = [
('date_check', "CHECK ((date_start <= date_end OR date_end = NULL))", "The start date must be anterior to the end date."),
]
class ResumeLineType(models.Model):
_name = 'hr.resume.line.type'
_description = "Type of a resumé line"
name = fields.Char(required=True)
+59
View File
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class Skill(models.Model):
_name = 'hr.skill'
_description = "Skill"
name = fields.Char(required=True)
skill_type_id = fields.Many2one('hr.skill.type')
class EmployeeSkill(models.Model):
_name = 'hr.employee.skill'
_description = "Skill level for an employee"
employee_id = fields.Many2one('hr.employee', required=True, ondelete='cascade')
skill_id = fields.Many2one('hr.skill', required=True)
skill_level_id = fields.Many2one('hr.skill.level', required=True)
skill_type_id = fields.Many2one('hr.skill.type', required=True)
level_progress = fields.Integer(related='skill_level_id.level_progress')
_sql_constraints = [
('_unique_skill', 'unique (employee_id, skill_id)', _("Two levels for the same skill is not allowed")),
]
@api.constrains('skill_id', 'skill_type_id')
def _check_skill_type(self):
for record in self:
if record.skill_id not in record.skill_type_id.skill_ids:
raise ValidationError(_("The skill %s and skill type %s doesn't match") % (record.skill_id.name, record.skill_type_id.name))
@api.constrains('skill_type_id', 'skill_level_id')
def _check_skill_level(self):
for record in self:
if record.skill_level_id not in record.skill_type_id.skill_level_ids:
raise ValidationError(_("The skill level %s is not valid for skill type: %s ") % (record.skill_level_id.name, record.skill_type_id.name))
class SkillLevel(models.Model):
_name = 'hr.skill.level'
_description = "Skill Level"
skill_type_id = fields.Many2one('hr.skill.type')
name = fields.Char(required=True)
level_progress = fields.Integer(string="Progress", help="Progress from zero knowledge (0%) to fully mastered (100%).")
sequence = fields.Integer(default=100)
class SkillType(models.Model):
_name = 'hr.skill.type'
_description = "Skill Type"
name = fields.Char(required=True)
skill_ids = fields.One2many('hr.skill', 'skill_type_id', string="Skills", ondelete='cascade')
skill_level_ids = fields.One2many('hr.skill.level', 'skill_type_id', string="Levels", ondelete='cascade')
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class User(models.Model):
_inherit = ['res.users']
resume_line_ids = fields.One2many(related='employee_id.resume_line_ids', readonly=False)
employee_skill_ids = fields.One2many(related='employee_id.employee_skill_ids', 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.
"""
hr_skills_fields = [
'resume_line_ids',
'employee_skill_ids',
]
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_skills_fields
type(self).SELF_WRITEABLE_FIELDS = type(self).SELF_WRITEABLE_FIELDS + hr_skills_fields
return init_res
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_resume_rule_employee" model="ir.rule">
<field name="name">Resumé: employee: read all</field>
<field name="model_id" ref="model_hr_resume_line"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_create" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<record id="hr_resume_rule_employee_hr_user" model="ir.rule">
<field name="name">Resumé: HR user: all</field>
<field name="model_id" ref="model_hr_resume_line"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4,ref('hr.group_hr_user'))]"/>
</record>
<record id="hr_skills_rule_employee_update" model="ir.rule">
<field name="name">Resumé: employee: create/write/unlink own</field>
<field name="model_id" ref="model_hr_resume_line"/>
<field name="domain_force">[('employee_id.user_id','=',user.id)]</field>
<field name="perm_read" eval="False"/>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<record id="hr_skill_rule_employee" model="ir.rule">
<field name="name">Employee skill: employee: read all</field>
<field name="model_id" ref="model_hr_employee_skill"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_create" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<record id="hr_skill_rule_hr_user" model="ir.rule">
<field name="name">Employee skill: HR user: read all</field>
<field name="model_id" ref="model_hr_employee_skill"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4,ref('hr.group_hr_user'))]"/>
</record>
<record id="hr_skill_rule_employee_update" model="ir.rule">
<field name="name">Employee skill: employee: create/write/unlink own</field>
<field name="model_id" ref="model_hr_employee_skill"/>
<field name="domain_force">[('employee_id.user_id','=',user.id)]</field>
<field name="perm_read" eval="False"/>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
</odoo>
@@ -0,0 +1,13 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_resume_line,hr.resume.line,model_hr_resume_line,hr.group_hr_user,1,1,1,1
access_hr_resume_line_employee,hr.resume.line.employee,model_hr_resume_line,base.group_user,1,1,1,1
access_hr_resume_line_type,hr.resume.line.type,model_hr_resume_line_type,hr.group_hr_user,1,1,1,1
access_hr_resume_line_type_employee,hr.resume.line.type.employee,model_hr_resume_line_type,base.group_user,1,0,0,0
access_hr_skill_type,hr.skill.type,model_hr_skill_type,hr.group_hr_user,1,1,1,1
access_hr_skill_type_employee,hr.skill.type.employee,model_hr_skill_type,base.group_user,1,0,0,0
access_hr_skill_level,hr.skill.level,model_hr_skill_level,hr.group_hr_user,1,1,1,1
access_hr_skill_level_employee,hr.skill.level.employee,model_hr_skill_level,base.group_user,1,0,0,0
access_hr_skill,hr.skill,model_hr_skill,hr.group_hr_user,1,1,1,1
access_hr_skill_employee,hr.skill.employee,model_hr_skill,base.group_user,1,0,0,0
access_hr_employee_skill,hr.employee.skill,model_hr_employee_skill,hr.group_hr_user,1,1,1,1
access_hr_employee_skill_employee,hr.employee.skill,model_hr_employee_skill,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_resume_line hr.resume.line model_hr_resume_line hr.group_hr_user 1 1 1 1
3 access_hr_resume_line_employee hr.resume.line.employee model_hr_resume_line base.group_user 1 1 1 1
4 access_hr_resume_line_type hr.resume.line.type model_hr_resume_line_type hr.group_hr_user 1 1 1 1
5 access_hr_resume_line_type_employee hr.resume.line.type.employee model_hr_resume_line_type base.group_user 1 0 0 0
6 access_hr_skill_type hr.skill.type model_hr_skill_type hr.group_hr_user 1 1 1 1
7 access_hr_skill_type_employee hr.skill.type.employee model_hr_skill_type base.group_user 1 0 0 0
8 access_hr_skill_level hr.skill.level model_hr_skill_level hr.group_hr_user 1 1 1 1
9 access_hr_skill_level_employee hr.skill.level.employee model_hr_skill_level base.group_user 1 0 0 0
10 access_hr_skill hr.skill model_hr_skill hr.group_hr_user 1 1 1 1
11 access_hr_skill_employee hr.skill.employee model_hr_skill base.group_user 1 0 0 0
12 access_hr_employee_skill hr.employee.skill model_hr_employee_skill hr.group_hr_user 1 1 1 1
13 access_hr_employee_skill_employee hr.employee.skill model_hr_employee_skill base.group_user 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
<defs>
<path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#269396"/>
<stop offset="100%" stop-color="#218689"/>
</linearGradient>
<path id="icon-d" d="M35.5,22.75 C39.6529297,22.75 43.0195312,26.1166016 43.0195312,30.2695312 C43.0195312,34.4224609 39.6529297,37.7890625 35.5,37.7890625 C31.3470703,37.7890625 27.9804687,34.4224609 27.9804687,30.2695312 C27.9804687,26.1166016 31.3470703,22.75 35.5,22.75 Z M43.6256055,38.3165755 L40.7623112,37.6007161 C37.2411654,40.1333659 32.9730794,39.5681836 30.2377604,37.6007161 L27.3744661,38.3165755 C25.0790039,38.8904232 23.46875,40.9527799 23.46875,43.3188542 L23.46875,47.671875 C23.46875,49.0957161 24.6230339,50.25 26.046875,50.25 L44.953125,50.25 C46.3769661,50.25 47.53125,49.0957161 47.53125,47.671875 L47.53125,43.3188542 C47.53125,40.9527799 45.9209961,38.8904232 43.6256055,38.3165755 Z M50.3958333,39.6510417 C53.1644531,39.6510417 55.4088542,37.4066406 55.4088542,34.6380208 C55.4088542,31.869401 53.1644531,29.625 50.3958333,29.625 C47.6272135,29.625 45.3828125,31.869401 45.3828125,34.6380208 C45.3828125,37.4066406 47.6272135,39.6510417 50.3958333,39.6510417 Z M20.6041667,39.6510417 C23.3727865,39.6510417 25.6171875,37.4066406 25.6171875,34.6380208 C25.6171875,31.869401 23.3727865,29.625 20.6041667,29.625 C17.8355469,29.625 15.5911458,31.869401 15.5911458,34.6380208 C15.5911458,37.4066406 17.8355469,39.6510417 20.6041667,39.6510417 Z M22.3229167,47.671875 L22.3229167,43.3188542 C22.3229167,42.1335612 22.6518424,41.0125781 23.2326367,40.0533008 C21.0850586,41.1074674 18.6968555,40.6769206 17.0959831,39.5255013 L15.1870964,40.0027409 C13.6568359,40.3852344 12.5833333,41.7602344 12.5833333,43.3375456 L12.5833333,46.2395833 C12.5833333,47.1888346 13.352832,47.9583333 14.3020833,47.9583333 L22.3350195,47.9583333 C22.3273388,47.8630362 22.3233016,47.7674804 22.3229167,47.671875 Z M55.8129036,40.0026693 L53.9040169,39.5254297 C51.9041797,40.9638802 49.5434049,40.902793 47.7604883,40.0423437 C48.3454362,41.004056 48.6770833,42.1289779 48.6770833,43.3188542 L48.6770833,47.671875 C48.6770833,47.7683398 48.6722135,47.8636589 48.6649805,47.9583333 L56.6979167,47.9583333 C57.647168,47.9583333 58.4166667,47.1888346 58.4166667,46.2395833 L58.4166667,43.3375456 C58.4166667,41.7602344 57.3431641,40.3852344 55.8129036,40.0026693 Z"/>
<path id="icon-e" d="M35.5,20.75 C39.6529297,20.75 43.0195312,24.1166016 43.0195312,28.2695312 C43.0195312,32.4224609 39.6529297,35.7890625 35.5,35.7890625 C31.3470703,35.7890625 27.9804687,32.4224609 27.9804687,28.2695312 C27.9804687,24.1166016 31.3470703,20.75 35.5,20.75 Z M43.6256055,36.3165755 L40.7623112,35.6007161 C37.2411654,38.1333659 32.9730794,37.5681836 30.2377604,35.6007161 L27.3744661,36.3165755 C25.0790039,36.8904232 23.46875,38.9527799 23.46875,41.3188542 L23.46875,45.671875 C23.46875,47.0957161 24.6230339,48.25 26.046875,48.25 L44.953125,48.25 C46.3769661,48.25 47.53125,47.0957161 47.53125,45.671875 L47.53125,41.3188542 C47.53125,38.9527799 45.9209961,36.8904232 43.6256055,36.3165755 Z M50.3958333,37.6510417 C53.1644531,37.6510417 55.4088542,35.4066406 55.4088542,32.6380208 C55.4088542,29.869401 53.1644531,27.625 50.3958333,27.625 C47.6272135,27.625 45.3828125,29.869401 45.3828125,32.6380208 C45.3828125,35.4066406 47.6272135,37.6510417 50.3958333,37.6510417 Z M20.6041667,37.6510417 C23.3727865,37.6510417 25.6171875,35.4066406 25.6171875,32.6380208 C25.6171875,29.869401 23.3727865,27.625 20.6041667,27.625 C17.8355469,27.625 15.5911458,29.869401 15.5911458,32.6380208 C15.5911458,35.4066406 17.8355469,37.6510417 20.6041667,37.6510417 Z M22.3229167,45.671875 L22.3229167,41.3188542 C22.3229167,40.1335612 22.6518424,39.0125781 23.2326367,38.0533008 C21.0850586,39.1074674 18.6968555,38.6769206 17.0959831,37.5255013 L15.1870964,38.0027409 C13.6568359,38.3852344 12.5833333,39.7602344 12.5833333,41.3375456 L12.5833333,44.2395833 C12.5833333,45.1888346 13.352832,45.9583333 14.3020833,45.9583333 L22.3350195,45.9583333 C22.3273388,45.8630362 22.3233016,45.7674804 22.3229167,45.671875 Z M55.8129036,38.0026693 L53.9040169,37.5254297 C51.9041797,38.9638802 49.5434049,38.902793 47.7604883,38.0423437 C48.3454362,39.004056 48.6770833,40.1289779 48.6770833,41.3188542 L48.6770833,45.671875 C48.6770833,45.7683398 48.6722135,45.8636589 48.6649805,45.9583333 L56.6979167,45.9583333 C57.647168,45.9583333 58.4166667,45.1888346 58.4166667,44.2395833 L58.4166667,41.3375456 C58.4166667,39.7602344 57.3431641,38.3852344 55.8129036,38.0026693 Z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<mask id="icon-b" fill="#fff">
<use xlink:href="#icon-a"/>
</mask>
<g mask="url(#icon-b)">
<rect width="70" height="70" fill="url(#icon-c)"/>
<path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
<path fill="#393939" d="M44,47 L4,47 C2,47 -7.10542736e-15,46.8509317 0,42.826087 L1.81527147e-16,22.6291049 L17.2090667,6.04664397 L19.583071,9.5209307 L30.2767729,0.11143939 L40.9146315,10.270152 L46.6446282,6.41116033 L55.3045682,10.7749724 L52.3812234,16.1277957 L58.2417324,21.9036543 L44,47 Z" opacity=".324" transform="translate(0 23)"/>
<path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
<use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#icon-d"/>
<use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-e"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

@@ -0,0 +1,17 @@
.o_form_sheet {
.o_resume_line {
width: 100%;
}
.o_list_view tbody:first-of-type > tr.o_resume_group_header:first-child {
box-shadow: none;
}
.o_group_skills th.o_group_name {
background-color: #e9ecef;
}
.o_group_resume tbody tr.o_resume_group_header th {
font-weight: normal;
}
}
@@ -0,0 +1,222 @@
odoo.define('web.FieldResume', function (require) {
"use strict";
var time = require('web.time');
var FieldOne2Many = require('web.relational_fields').FieldOne2Many;
var FieldProgressBar = require('web.basic_fields').FieldProgressBar;
var ListRenderer = require('web.ListRenderer');
var field_registry = require('web.field_registry');
var core = require('web.core');
var qweb = core.qweb;
var _t = core._t;
var AbstractGroupedOne2ManyRenderer = ListRenderer.extend({
/**
* This abstract renderer is use to render a one2many field in a form view.
* The records in the one2many field are displayed grouped by a specific field.
*
* A concrete renderer can/should set:
* - groupBy: field to group records
* - dataRowTemplate: template to render a record's data
* - groupTitleTemplate (optional): template to render the header row of a group
* - addLineButtonTemplate (optional): template to render the 'Add a line' button at the end of each group (edit mode only)
**/
groupBy: '', // Field: records are grouped based on this field
groupTitleTemplate: 'hr_default_group_row', // Template used to render the title row of a group
dataRowTemplate: '', // Template used to render a record
addLineButtonTemplate: 'group_add_item',
/**
* @override
* @private
* Don't render any header
*/
_renderHeader: function () {
return $('<div>');
},
/**
* @override
* @private
* Don't render any footer
*/
_renderFooter: function () {
return $('<div>');
},
/**
* @override
* @private
*/
_renderGroupRow: function (display_name) {
return qweb.render(this.groupTitleTemplate, {display_name: display_name});
},
/**
* This method is meant to be overriten by concrete renderers and
* is called each time a row is rendered.
* It is a hook to format record's data before it's given to the qweb template.
*
* @private
*/
_formatData: function (data) {
return data;
},
_renderRow: function (record) {
return $(qweb.render(this.dataRowTemplate, {
id: record.id,
data: this._formatData(record.data),
}));
},
/**
* This method is meant to be overridden by concrete renderers.
* Returns a context used for the 'Add a line' button.
* It's useful to set default values.
* An 'Add a line' button is added after each group of records.
* The group passed as parameters allow to set a different context based on the group.
* If no records exist, group is undefined.
*
* @private
*/
_getCreateLineContext: function (group) {
return {};
},
_renderTrashIcon: function() {
return qweb.render('hr_trash_button');
},
_renderAddItemButton(group) {
return qweb.render(this.addLineButtonTemplate, {
context: JSON.stringify(this._getCreateLineContext(group)),
});
},
_renderBody: function () {
var self = this;
var grouped_by = _.groupBy(this.state.data, function (record) {
return record.data[self.groupBy].res_id;
});
var $body = $('<tbody>');
for (var key in grouped_by) {
var group = grouped_by[key];
var groupTitle = group[0].data[self.groupBy].data.display_name;
var $title_row = $(self._renderGroupRow(groupTitle));
$body.append($title_row);
// Render each rows
group.forEach(function (record) {
var $row = self._renderRow(record);
if (self.addTrashIcon) $row.append(self._renderTrashIcon());
$body.append($row);
});
if (self.addCreateLine) {
$body.append(self._renderAddItemButton(group));
}
}
if ($body.is(':empty') && self.addCreateLine) {
$body.append(this._renderAddItemButton());
}
return $body;
},
});
var ResumeLineRenderer = AbstractGroupedOne2ManyRenderer.extend({
groupBy: 'line_type_id',
groupTitleTemplate: 'hr_resume_group_row',
dataRowTemplate: 'hr_resume_data_row',
_formatData: function (data) {
var dateFormat = time.getLangDateFormat();
var date_start = data.date_start && data.date_start.format(dateFormat) || "";
var date_end = data.date_end && data.date_end.format(dateFormat) || _t("Current");
return _.extend(data, {
date_start,
date_end,
});
},
_getCreateLineContext: function (group) {
var ctx = this._super(group);
return group ? _.extend({ default_line_type_id: group[0].data[this.groupBy].data.id }, ctx) : ctx;
},
_render: function () {
var self = this;
this._super().then(function () {
// Allow to sort records
self.$el.find('.o_list_view').sortable({
axis: 'y',
items: '.o_data_row',
helper: 'clone',
handle: '.o_row_handle',
stop: self._resequence.bind(self),
});
self.$el.find('table').removeClass('table table-striped o_list_view_ungrouped');
});
},
});
var SkillsRenderer = AbstractGroupedOne2ManyRenderer.extend({
groupBy: 'skill_type_id',
dataRowTemplate: 'hr_skill_data_row',
_renderRow: function (record) {
var $row = this._super(record);
// Add progress bar widget at the end of rows
var $td = $('<td/>', {class: 'o_data_cell'});
var progress = new FieldProgressBar(this, 'level_progress', record, {
current_value: record.data.level_progress,
attrs: this.arch.attrs,
});
progress.appendTo($td);
return $row.append($td);
},
_getCreateLineContext: function (group) {
var ctx = this._super(group);
return group ? _.extend({ default_skill_type_id: group[0].data[this.groupBy].data.id }, ctx) : ctx;
},
});
var FieldResume = FieldOne2Many.extend({
/**
* @override
* @private
*/
_getRenderer: function () {
return ResumeLineRenderer;
},
});
var FieldSkills = FieldOne2Many.extend({
/**
* @override
* @private
*/
_getRenderer: function () {
return SkillsRenderer;
},
});
field_registry.add('hr_resume', FieldResume);
field_registry.add('hr_skills', FieldSkills);
return FieldResume;
});
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr_resume_data_row">
<tr class="o_data_row" t-att-data-id="id">
<td class="o_data_cell o_list_number o_handle_cell">
<span class="o_row_handle fa fa-arrows o_field_widget ui-sortable-handle" t-att-data-sequence="id"/>
</td>
<td class="o_data_cell container">
<div class="o_resume_line row" t-att-data-id="id">
<div class="o_resume_line_dates col-lg-3">
<div class="row">
<span class="col-6 col-lg-8"><t t-esc="data.date_start"/>-</span>
<span class="col-6 col-lg-6"><t t-esc="data.date_end"/></span>
</div>
</div>
<div class="o_resume_line_desc col-lg-9">
<h3><t t-esc="data.name"/></h3>
<t t-esc="data.description"/>
</div>
</div>
</td>
</tr>
</t>
<t t-name="hr_trash_button">
<td class="o_list_record_remove" style="text-align: right;">
<button class="fa fa-trash-o"
name="delete"
arial-label="Delete row"/>
</td>
</t>
<t t-name="hr_resume_group_row">
<tr class="o_resume_group_header">
<th colspan="100%"><div class="o_horizontal_separator"><t t-esc="display_name"/></div></th>
</tr>
</t>
<t t-name="group_add_item">
<div class="o_field_x2many_list_row_add">
<a href="#"
role="button"
class="btn btn-secondary o-kanban-button-new "
t-attf-data-context="{{ context }}">ADD</a>
</div>
</t>
</templates>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr_skill_data_row">
<tr class="o_data_row" t-att-data-id="id">
<td class="o_data_cell">
<t t-esc="data.skill_id.data.display_name"/>
</td>
<td class="o_data_cell">
<t t-esc="data.skill_level_id.data.display_name"/>
</td>
</tr>
</t>
<t t-name="hr_default_group_row">
<tr class="o_group_header o_group_has_content">
<th class="o_group_name" colspan="99"><t t-esc="display_name"/></th>
</tr>
</t>
</templates>
@@ -0,0 +1,329 @@
odoo.define('hr_skills.field_one_to_many_group_tests', function (require) {
"use strict";
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
QUnit.module('skills_widgets', {
beforeEach: function () {
this.data = {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
line_ids: { string: "one2many field", type: "one2many", relation: 'line', relation_field: 'trululu' },
skill_ids: { string: "one2many field", type: "one2many", relation: 'partner_skill', relation_field: 'partner_id' },
},
records: [{
id: 1,
display_name: "first record",
line_ids: [37, 38, 39],
skill_ids: [75, 76, 77]
}],
onchanges: {},
},
partner_skill: {
fields: {
skill_id: { string: "Name", type: "many2one", relation: 'skill'},
skill_type_id: { string: "Type", type: "many2one", relation: 'skill_type' },
skill_level_id: { string: "Level", type: "many2one", relation: 'skill_level' },
level_progress: { string: "Progress", type: "int" },
},
records: [{
id: 75,
skill_id: 444,
skill_type_id: 221,
skill_level_id: 112,
level_progress: 50,
}, {
id: 76,
skill_id: 445,
skill_type_id: 222,
skill_level_id: 111,
level_progress: 50,
}, {
id: 77,
skill_id: 446,
skill_type_id: 222,
skill_level_id: 111,
level_progress: 70,
}],
},
skill: {
fields: {
name: { string: "Name", type: "char" },
},
records: [
{ id: 444, name: 'Python' },
{ id: 445, name: 'Piano' },
{ id: 446, name: 'Flute' },
]
},
skill_level: {
fields: {
name: { string: "Name", type: "char" },
},
records: [
{ id: 111, name: 'L1' },
{ id: 112, name: 'Intermediate' }
]
},
skill_type: {
fields: {
name: { string: "Name", type: "char" },
},
records: [
{ id: 221, name: 'Dev' },
{ id: 222, name: 'Music' },
]
},
line: {
fields: {
name: { string: "Name", type: "char" },
line_type_id: { string: "Type", relation: 'line_type', type: "many2one" },
sequence: { string: "Sequence", type: "int", default:10 },
description: { string: "Description", type: "text" },
date_start: { string: "Date start", type: "date" },
date_end: { string: "Date end", type: "date" },
trululu: { string: "Trululu", type: "many2one", relation: 'partner' },
},
records: [{
id: 37,
name: "ULB",
line_type_id: 50,
sequence: 5,
date_start: "2017-01-25",
date_end: "2019-01-25",
description: 'Hello',
trululu: 1,
}, {
id: 38,
name: "UCL",
line_type_id: 50,
date_start: "2013-01-25",
date_end: "2014-01-25",
description: 'World',
trululu: 1,
}, {
id: 39,
name: "KUL",
line_type_id: 51,
sequence: 15,
date_start: "2008-01-25",
description: 'Hi',
trululu: 1,
}],
onchanges: {},
},
line_type: {
fields: {
name: { string: "Name", type: "char" },
},
records: [{
id: 50,
name: 'AAA',
}, {
id: 51,
name: 'BBB'
}],
}
};
}
}, function () {
QUnit.test('resumé one2many field group by field render', function (assert) {
assert.expect(16);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="line_ids" widget="hr_resume">' +
'<tree>' +
'<field name="name"/>' +
'<field name="line_type_id"/>' +
'<field name="description"/>' +
'<field name="date_start"/>' +
'<field name="date_end"/>' +
'<field name="sequence"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
});
var $headers = form.$('.o_resume_group_header');
assert.strictEqual($headers.length, 2, 'There should be 2 headers');
assert.strictEqual($headers.find('th:contains(AAA)').length, 1, "it should have line type AAA");
assert.strictEqual($headers.find('th:contains(BBB)').length, 1, "it should have line type BBB");
var dataRows = form.$('.o_data_row');
assert.strictEqual(dataRows.length, 3, 'There should be 3 data rows');
var $row = $(dataRows[0]);
assert.strictEqual($row.find('td:contains(01/25/2017)').length, 1, "it should have start date 01/25/2017");
assert.strictEqual($row.find('td:contains(01/25/2019)').length, 1, "it should have end date 01/25/2019");
assert.strictEqual($row.find('td:contains(ULB)').length, 1, "it should have line name ULB");
assert.strictEqual($row.find('td:contains(Hello)').length, 1, "it should have line description Hello");
$row = $(dataRows[1]);
assert.strictEqual($row.find('td:contains(01/25/2013)').length, 1, "it should have start date 01/25/2013");
assert.strictEqual($row.find('td:contains(01/25/2014)').length, 1, "it should have end date 01/25/2014");
assert.strictEqual($row.find('td:contains(UCL)').length, 1, "it should have line name UCL");
assert.strictEqual($row.find('td:contains(World)').length, 1, "it should have line description World");
$row = $(dataRows[2]);
assert.strictEqual($row.find('td:contains(01/25/2008)').length, 1, "it should have start date 01/25/2008");
assert.strictEqual($row.find('td:contains(Current)').length, 1, "it should have end date Current");
assert.strictEqual($row.find('td:contains(KUL)').length, 1, "it should have line name KUL");
assert.strictEqual($row.find('td:contains(Hi)').length, 1, "it should have line description Hi");
form.destroy();
});
QUnit.test('resumé one2many field group by field create', function (assert) {
assert.expect(5);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="line_ids" widget="hr_resume">' +
'<tree>' +
'<field name="name"/>' +
'<field name="line_type_id"/>' +
'<field name="description"/>' +
'<field name="date_start"/>' +
'<field name="date_end"/>' +
'<field name="sequence"/>' +
'</tree>' +
'</field>' +
'</form>',
archs: {
'line,false,form': '<form>' +
'<field name="name"/>' +
'<field name="line_type_id"/>' +
'<field name="description"/>' +
'<field name="date_start"/>' +
'<field name="date_end"/>' +
'</form>',
},
res_id: 1,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'write') {
var new_line_data = args.args[1].line_ids[3][2];
assert.strictEqual(new_line_data.date_end, '2030-01-01');
assert.strictEqual(new_line_data.date_start, '2025-01-01');
assert.strictEqual(new_line_data.line_type_id, 50, "it should have the line type from context");
assert.strictEqual(new_line_data.name, 'new line');
assert.strictEqual(new_line_data.description, 'new description');
}
return result;
},
});
testUtils.form.clickEdit(form);
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')[0]);
// Fill line form (type should be set from the add button context)
testUtils.fields.editInput($('input[name="name"]'), 'new line');
testUtils.fields.editInput($('textarea[name="description"]'), 'new description');
testUtils.fields.editSelect($('input[name="date_start"]'), '2025-01-01');
testUtils.fields.editSelect($('input[name="date_end"]'), '2030-01-01');
testUtils.modal.clickButton('Save & Close');
testUtils.form.clickSave(form);
form.destroy();
});
QUnit.test('resumé one2many field group by field delete', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="line_ids" widget="hr_resume">' +
'<tree>' +
'<field name="name"/>' +
'<field name="line_type_id"/>' +
'<field name="description"/>' +
'<field name="date_start"/>' +
'<field name="date_end"/>' +
'<field name="sequence"/>' +
'</tree>' +
'</field>' +
'</form>',
archs: {
'line,false,form': '<form>' +
'<field name="name"/>' +
'<field name="line_type_id"/>' +
'<field name="description"/>' +
'<field name="date_start"/>' +
'<field name="date_end"/>' +
'</form>',
},
res_id: 1,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'write') {
var orm_cmd = args.args[1].line_ids[2][0];
var id = args.args[1].line_ids[2][1];
assert.strictEqual(orm_cmd, 2, "it should delete resume line");
assert.strictEqual(id, 37, "it should delete resume line #37");
}
return result;
},
});
testUtils.form.clickEdit(form);
testUtils.dom.click(form.$('.o_list_record_remove')[0]);
testUtils.form.clickSave(form);
form.destroy();
});
QUnit.test('skills one2many field group by field render', function (assert) {
assert.expect(13);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="skill_ids" widget="hr_skills">' +
'<tree>' +
'<field name="skill_id"/>' +
'<field name="skill_type_id"/>' +
'<field name="skill_level_id"/>' +
'<field name="level_progress"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
});
var $headers = form.$('.o_group_header');
assert.strictEqual($headers.length, 2, 'There should be 2 headers');
assert.strictEqual($headers.find('th:contains(Dev)').length, 1, "it should have skill type Dev");
assert.strictEqual($headers.find('th:contains(Music)').length, 1, "it should have skill type Music");
var dataRows = form.$('.o_data_row');
assert.strictEqual(dataRows.length, 3, 'There should be 3 data rows');
var $row = $(dataRows[0]);
assert.strictEqual($row.find('td:contains(Python)').length, 1, "it should have skill name Python");
assert.strictEqual($row.find('td:contains(Intermediate)').length, 1, "it should have skill name Intermediate");
assert.strictEqual($row.find('td:contains(50)').length, 1, "it should have skill progress 50");
$row = $(dataRows[1]);
assert.strictEqual($row.find('td:contains(Piano)').length, 1, "it should have skill name Piano");
assert.strictEqual($row.find('td:contains(L1)').length, 1, "it should have skill level L1");
assert.strictEqual($row.find('td:contains(50)').length, 1, "it should have skill progress 50");
$row = $(dataRows[2]);
assert.strictEqual($row.find('td:contains(Flute)').length, 1, "it should have skill name Flute");
assert.strictEqual($row.find('td:contains(L1)').length, 1, "it should have skill level L1");
assert.strictEqual($row.find('td:contains(70)').length, 1, "it should have skill progress 70");
form.destroy();
});
});
});
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend" name="hr_skills_assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/hr_skills/static/src/css/hr_skills.scss"/>
<script type="text/javascript" src="/hr_skills/static/src/js/resume_widget.js"></script>
</xpath>
</template>
<template id="qunit_suite" name="hr_skills_qunit_suite" inherit_id="web.qunit_suite">
<xpath expr="." position="inside">
<script type="text/javascript" src="/hr_skills/static/tests/widget_tests.js"></script>
</xpath>
</template>
</odoo>
+265
View File
@@ -0,0 +1,265 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="resume_line_view_search" model="ir.ui.view">
<field name="name">hr.resume.search</field>
<field name="model">hr.resume.line</field>
<field name="arch" type="xml">
<search string="Résume">
<filter string="Resumé line type" name="group_by_resume_line_type" context="{'group_by':'line_type'}"/>
</search>
</field>
</record>
<record id="resume_line_view_form" model="ir.ui.view">
<field name="name">hr.resume.line.form</field>
<field name="model">hr.resume.line</field>
<field name="arch" type="xml">
<form string="Resumé">
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="Title" required="True"/>
</h1>
</div>
<group>
<group>
<field name="line_type_id"/>
<field name="sequence" groups="base.group_no_one"/>
</group>
<group>
<field name="date_start" required="True"/>
<field name="date_end"/>
</group>
</group>
<field name="description" placeholder="Description"/>
</form>
</field>
</record>
<record id="hr_employee_view_form" model="ir.ui.view">
<field name="name">hr.employee.view.form.inherit.resume</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='public']" position="before">
<page name="public" string="Resumé">
<div class="row">
<div class="col-lg-8 o_group_resume">
<!-- This field uses a custom tree view rendered by the 'hr_resume' widget.
Adding fields in the tree arch below makes them accessible to the widget
-->
<field class="" nolabel="1" name="resume_line_ids" widget="hr_resume">
<tree>
<field name="line_type_id"/>
<field name="name"/>
<field name="sequence" widget="handle"/>
<field name="description"/>
<field name="date_start"/>
<field name="date_end"/>
</tree>
</field>
</div>
<div class="col-lg-4 o_group_skills">
<separator string="Skills"/>
<field nolabel="1" name="employee_skill_ids" widget="hr_skills">
<tree>
<field name="skill_type_id" invisible="1"/>
<field name="skill_id"/>
<field name="skill_level_id"/>
<field name="level_progress" widget="progressbar"/>
</tree>
</field>
</div>
</div>
</page>
</xpath>
</field>
</record>
<record id="res_users_view_form" model="ir.ui.view">
<field name="name">hr.user.preferences.form.inherit.hr.skills</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="//page[@name='public']" position="before">
<page name="public" string="Resumé">
<div class="row">
<div class="col-lg-8 o_group_resume">
<!-- This field uses a custom tree view rendered by the 'hr_resume' widget.
Adding fields in the tree arch below makes them accessible to the widget
-->
<field class="" nolabel="1" name="resume_line_ids" widget="hr_resume">
<tree>
<field name="line_type_id"/>
<field name="name"/>
<field name="sequence" widget="handle"/>
<field name="description"/>
<field name="date_start"/>
<field name="date_end"/>
</tree>
</field>
</div>
<div class="col-lg-4 o_group_skills">
<separator string="Skills"/>
<field nolabel="1" name="employee_skill_ids" widget="hr_skills">
<tree>
<field name="skill_type_id" invisible="1"/>
<field name="skill_id"/>
<field name="skill_level_id"/>
<field name="level_progress" widget="progressbar"/>
</tree>
</field>
</div>
</div>
</page>
</xpath>
</field>
</record>
<record id="hr_resume_type_action" model="ir.actions.act_window">
<field name="name">Resumé Line Types</field>
<field name="res_model">hr.resume.line.type</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="menu_human_resources_configuration_resume"
name="Resumé"
parent="hr.menu_human_resources_configuration"
sequence="4"
groups="base.group_no_one"/>
<menuitem
id="hr_resume_line_type_menu"
name="Types"
action="hr_resume_type_action"
parent="hr_skills.menu_human_resources_configuration_resume"
sequence="3"
groups="base.group_no_one"/>
<!-- Skills -->
<record id="employee_skill_level_view_tree" model="ir.ui.view">
<field name="name">hr.skill.level.tree</field>
<field name="model">hr.skill.level</field>
<field name="arch" type="xml">
<tree string="Skill Levels">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="level_progress" widget="progressbar"/>
</tree>
</field>
</record>
<record id="employee_skill_view_tree" model="ir.ui.view">
<field name="name">hr.skill.tree</field>
<field name="model">hr.skill</field>
<field name="arch" type="xml">
<tree string="Skill Levels">
<field name="name"/>
</tree>
</field>
</record>
<record id="employee_skill_level_view_form" model="ir.ui.view">
<field name="name">hr.skill.level.form</field>
<field name="model">hr.skill.level</field>
<field name="arch" type="xml">
<form string="Skill Level">
<sheet>
<group>
<field name="name"/>
<field name="level_progress" string="Progress (%)"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="employee_skill_view_form" model="ir.ui.view">
<field name="name">hr.employees.skill.form</field>
<field name="model">hr.employee.skill</field>
<field name="arch" type="xml">
<form string="Skills">
<sheet>
<group>
<group>
<field name="skill_type_id"/>
<field name="skill_id" domain="[('skill_type_id', '=', skill_type_id)]"/>
</group>
<group>
<field name="skill_level_id" domain="[('skill_type_id', '=', skill_type_id)]"/>
<field name="level_progress" widget="progressbar"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="hr_skill_view_form" model="ir.ui.view">
<field name="name">hr.skill.form</field>
<field name="model">hr.skill</field>
<field name="arch" type="xml">
<form string="Skills">
<sheet>
<field name="name"/>
</sheet>
</form>
</field>
</record>
<record id="hr_skill_type_view_tree" model="ir.ui.view">
<field name="name">hr.skill.type.tree</field>
<field name="model">hr.skill.type</field>
<field name="arch" type="xml">
<tree string="Skill Types">
<field name="name"/>
<field name="skill_ids" widget="many2many_tags"/>
</tree>
</field>
</record>
<record id="hr_employee_skill_type_view_form" model="ir.ui.view">
<field name="name">hr.skill.type.form</field>
<field name="model">hr.skill.type</field>
<field name="arch" type="xml">
<form string="Skill Type">
<field name="id" invisible="1"/>
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="Skill Type" required="True"/>
</h1>
</div>
<group string="Skills">
<field name="skill_ids" nolabel="1" context="{'default_skill_type_id': id}"/>
</group>
<group string="Levels">
<field name="skill_level_ids" nolabel="1" context="{'default_skill_type_id': id}"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="hr_skill_type_action" model="ir.actions.act_window">
<field name="name">Skill Types</field>
<field name="res_model">hr.skill.type</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="hr_skill_type_menu"
name="Skills"
action="hr_skill_type_action"
parent="hr.menu_human_resources_configuration_employee"
sequence="3"
groups="base.group_no_one"/>
</odoo>
+2 -2
View File
@@ -32,7 +32,7 @@
<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">
<xpath expr="//page[@name='hr_settings']" position="inside">
<group string="Timesheets" name="timesheet" groups="hr_timesheet.group_timesheet_manager">
<label for="timesheet_cost"/>
<div>
@@ -41,7 +41,7 @@
</div>
</group>
</xpath>
<xpath expr="//button[@name='toggle_active']" position="before">
<xpath expr="//div[@name='button_box']" position="inside">
<button class="oe_stat_button" type="action" name="%(timesheet_action_from_employee)d" icon="fa-calendar">
<div class="o_stat_info">
<span class="o_stat_text">Timesheets</span>
+1 -1
View File
@@ -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>
+10 -8
View File
@@ -350,14 +350,16 @@ class Message(models.Model):
message_to_tracking = dict()
tracking_tree = dict.fromkeys(tracking_values.ids, False)
for tracking in tracking_values:
message_to_tracking.setdefault(tracking.mail_message_id.id, list()).append(tracking.id)
tracking_tree[tracking.id] = {
'id': tracking.id,
'changed_field': tracking.field_desc,
'old_value': tracking.get_old_display_value()[0],
'new_value': tracking.get_new_display_value()[0],
'field_type': tracking.field_type,
}
groups = tracking.groups
if not groups or self.user_has_groups(groups):
message_to_tracking.setdefault(tracking.mail_message_id.id, list()).append(tracking.id)
tracking_tree[tracking.id] = {
'id': tracking.id,
'changed_field': tracking.field_desc,
'old_value': tracking.get_old_display_value()[0],
'new_value': tracking.get_new_display_value()[0],
'field_type': tracking.field_type,
}
# 4. Update message dictionaries
for message_dict in messages:
+1 -1
View File
@@ -601,7 +601,7 @@ class MailThread(models.AbstractModel):
and initial values, return a structure that is a tuple containing :
- a set of updated column names
- a list of changes (initial value, new value, column name, column info) """
- a list of ORM (0, 0, values) commands to create 'mail.tracking.value' """
self.ensure_one()
changes = set() # contains onchange tracked fields that changed
tracking_value_ids = []
@@ -35,6 +35,13 @@ class MailTracking(models.Model):
tracking_sequence = fields.Integer('Tracking field sequence', readonly=1, default=100, oldname='track_sequence')
groups = fields.Char(compute='_compute_groups')
def _compute_groups(self):
for tracking in self:
model = self.env[tracking.mail_message_id.model]
tracking.groups = model._fields[tracking.field].groups
@api.model
def create_tracking_values(self, initial_value, new_value, col_name, col_info, tracking_sequence):
tracked = True
+5 -3
View File
@@ -69,9 +69,11 @@ class Partner(models.Model):
tracking = []
for tracking_value in self.env['mail.tracking.value'].sudo().search([('mail_message_id', '=', message.id)]):
tracking.append((tracking_value.field_desc,
tracking_value.get_old_display_value()[0],
tracking_value.get_new_display_value()[0]))
groups = tracking_value.groups
if not groups or self.user_has_groups(groups):
tracking.append((tracking_value.field_desc,
tracking_value.get_old_display_value()[0],
tracking_value.get_new_display_value()[0]))
is_discussion = message.subtype_id.id == self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')
@@ -248,6 +248,17 @@ var AbstractMessage = Class.extend({
hasSubject: function () {
return false;
},
/**
* State whether this message is empty
*
* @return {boolean}
*/
isEmpty: function () {
return !this.hasTrackingValues() &&
!this.hasSubtypeDescription() &&
!this.hasAttachments() &&
!this.getBody();
},
/**
* By default, messages do not have any subtype description
*
+1 -1
View File
@@ -255,7 +255,7 @@
@param {integer} [options.selectedMessageID]
-->
<t t-name="mail.widget.Thread.Message">
<div t-att-class="'o_thread_message ' + (message.getID() === options.selectedMessageID ? 'o_thread_selected_message ' : ' ') + (message.isDiscussion() or message.isNotification() ? ' o_mail_discussion ' : ' o_mail_not_discussion ')" t-att-data-message-id="message.getID()">
<div t-if="!message.isEmpty()" t-att-class="'o_thread_message ' + (message.getID() === options.selectedMessageID ? 'o_thread_selected_message ' : ' ') + (message.isDiscussion() or message.isNotification() ? ' o_mail_discussion ' : ' o_mail_not_discussion ')" t-att-data-message-id="message.getID()">
<div t-if="options.displayAvatars" class="o_thread_message_sidebar">
<t t-if="message.hasAuthor()">
<div t-if="displayAuthorMessages[message.getID()]" class="o_thread_message_sidebar_image">
+10
View File
@@ -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
@@ -131,3 +131,21 @@ class TestTracking(common.BaseFunctionalTest, common.MockEmails):
self.assertEqual(tracking_values[0].tracking_sequence, 1)
self.assertEqual(tracking_values[1].tracking_sequence, 2)
self.assertEqual(tracking_values[2].tracking_sequence, 100)
def test_track_groups(self):
self.record._fields['email_from'].groups = 'base.group_erp_manager' # patch the group attribute
self.record.sudo().write({'email_from': 'X'})
msg_emp = self.record.message_ids.message_format()
msg_admin = self.record.message_ids.sudo(self.user_admin).message_format()
self.assertFalse(msg_emp[0].get('tracking_value_ids'), "should not have protected tracking values")
self.assertTrue(msg_admin[0].get('tracking_value_ids'), "should have protected tracking values")
def test_notify_track_groups(self):
self.record._fields['email_from'].groups = 'base.group_erp_manager' # patch the group attribute
self.record.sudo().write({'email_from': 'X'})
msg_emp = self.partner_employee.sudo(self.user_employee)._notify_prepare_template_context(self.record.message_ids, self.record)
msg_admin = self.partner_admin.sudo(self.user_admin)._notify_prepare_template_context(self.record.message_ids, self.record)
self.assertFalse(msg_emp.get('tracking_values'), "should not have protected tracking values")
self.assertTrue(msg_admin.get('tracking_values'), "should have protected tracking values")
+3 -3
View File
@@ -511,7 +511,7 @@ class TestHeavyMailPerformance(TransactionCase):
customer_id = self.customer.id
user_id = self.user_portal.id
with self.assertQueryCount(__system__=161, emp=198): # com runbot: 161 - 198 // test_mail only: 161 - 192
with self.assertQueryCount(__system__=162, emp=199): # com runbot: 161 - 198 // test_mail only: 161 - 192
rec = self.env['mail.test.full'].create({
'name': 'Test',
'umbrella_id': umbrella_id,
@@ -540,7 +540,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
with self.assertQueryCount(__system__=99, emp=120): # com runbot: 99 - 120 // test_mail only: 98 - 116
with self.assertQueryCount(__system__=100, emp=121): # com runbot: 99 - 120 // test_mail only: 98 - 116
rec.write({
'name': 'Test2',
'umbrella_id': self.umbrella.id,
@@ -578,7 +578,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
with self.assertQueryCount(__system__=105, emp=126): # test_mail only: 105 - 122
with self.assertQueryCount(__system__=106, emp=127): # test_mail only: 105 - 122
rec.write({
'name': 'Test2',
'umbrella_id': umbrella_id,
+2 -4
View File
@@ -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;
+2 -3
View File
@@ -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">
+1
View File
@@ -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>
+1 -1
View File
@@ -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")
+1 -2
View File
@@ -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"/>
+11 -11
View File
@@ -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"/>