summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorRobot Odoo <xmo+roboto@odoo.com>2019-05-31 11:36:31 +0200
committerGitHub <noreply@github.com>2019-05-31 11:36:31 +0200
commit2d4b026625b162f4767d1b8152fd87fd34adf18c (patch)
tree6c235d36ea76f40c7066dabf953b5d7562ed68b7 /addons
parentaae6b3c337bdab3d62dd517b416a0a7b080ed248 (diff)
parentf755e57fb8520c099f4712f0092c9b5975d88fc4 (diff)
[MERGE] hr*: Introduce the public employee profile
Purpose ======= 1/ Robustness & security: right now it is not easy to understand and do something clean in term of security (hr people vs employees, private info vs public). A HR officer doesn't know if he can write something on the chatter. Currently, a note will be visible for all the employees who have access to the employee form view for example. 2/ In term of business, it makes sense to let a hr manages payroll stuff (contract, employees private information, ... and other employee see public information (résumé and work information) Specification ============= Introduce 2 new models: - hr.employee.base (AbstractModel): This represents the basic skeleton model on which the shared fields and methods between the public and the private employees models. - hr.employee.public (_auto=False): This is a sql view based on the employee values, readable for an internal user (i.e. an employee). The model hr.employee is not readable anymore for an employee. There are now 3 ways to access the employee data: 1/ From the hr.employee views. HR officer access rights are required 2/ From the public profile. The public data for an employee are accessible but can't be modified. 3/ From the 'My Profile' menu. A classic employee can access its own data from there, and can modify them. TaskID: 1961151 closes odoo/odoo#33725 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/hr/__manifest__.py6
-rw-r--r--addons/hr/models/__init__.py11
-rw-r--r--addons/hr/models/hr_department.py84
-rw-r--r--addons/hr/models/hr_employee.py (renamed from addons/hr/models/hr.py)276
-rw-r--r--addons/hr/models/hr_employee_base.py25
-rw-r--r--addons/hr/models/hr_employee_category.py18
-rw-r--r--addons/hr/models/hr_employee_public.py59
-rw-r--r--addons/hr/models/hr_job.py71
-rw-r--r--addons/hr/security/hr_security.xml7
-rw-r--r--addons/hr/security/ir.model.access.csv3
-rw-r--r--addons/hr/tests/test_self_user_access.py25
-rw-r--r--addons/hr/views/hr_department_views.xml139
-rw-r--r--addons/hr/views/hr_employee_category_views.xml38
-rw-r--r--addons/hr/views/hr_employee_public_views.xml151
-rw-r--r--addons/hr/views/hr_employee_views.xml360
-rw-r--r--addons/hr/views/hr_job_views.xml121
-rw-r--r--addons/hr/views/hr_plan_views.xml103
-rw-r--r--addons/hr/views/hr_views.xml882
-rw-r--r--addons/hr/views/res_users.xml11
-rw-r--r--addons/hr_attendance/models/hr_employee.py51
-rw-r--r--addons/hr_attendance/models/res_users.py5
-rw-r--r--addons/hr_attendance/tests/test_hr_attendance_constraints.py21
-rw-r--r--addons/hr_attendance_presence/models/hr_employee.py5
-rw-r--r--addons/hr_contract/models/__init__.py1
-rw-r--r--addons/hr_contract/models/hr_contract.py38
-rw-r--r--addons/hr_contract/models/hr_employee.py41
-rw-r--r--addons/hr_contract/views/hr_contract_views.xml2
-rw-r--r--addons/hr_expense/models/hr_employee.py6
-rw-r--r--addons/hr_fleet/controllers/main.py6
-rw-r--r--addons/hr_fleet/models/employee.py4
-rw-r--r--addons/hr_fleet/views/employee_views.xml2
-rw-r--r--addons/hr_gamification/models/__init__.py1
-rw-r--r--addons/hr_gamification/models/gamification.py2
-rw-r--r--addons/hr_gamification/models/hr_employee.py11
-rw-r--r--addons/hr_gamification/models/res_users.py11
-rw-r--r--addons/hr_gamification/views/hr_employee_views.xml24
-rw-r--r--addons/hr_holidays/models/__init__.py3
-rw-r--r--addons/hr_holidays/models/hr_department.py58
-rw-r--r--addons/hr_holidays/models/hr_employee.py (renamed from addons/hr_holidays/models/hr.py)66
-rw-r--r--addons/hr_holidays/views/hr_leave_views.xml2
-rw-r--r--addons/hr_holidays/views/hr_views.xml46
-rw-r--r--addons/hr_org_chart/controllers/hr_org_chart.py20
-rw-r--r--addons/hr_org_chart/models/__init__.py1
-rw-r--r--addons/hr_org_chart/models/hr_employee.py41
-rw-r--r--addons/hr_org_chart/models/hr_org_chart_mixin.py39
-rw-r--r--addons/hr_org_chart/static/src/js/hr_org_chart.js57
-rw-r--r--addons/hr_org_chart/static/src/xml/hr_org_chart.xml6
-rw-r--r--addons/hr_org_chart/static/tests/hr_org_chart_tests.js8
-rw-r--r--addons/hr_org_chart/views/hr_views.xml16
-rw-r--r--addons/hr_presence/models/hr_employee.py2
-rw-r--r--addons/hr_skills/models/hr_resume.py7
-rw-r--r--addons/hr_skills/views/hr_views.xml43
-rw-r--r--addons/hr_timesheet/models/__init__.py2
-rw-r--r--addons/hr_timesheet/models/hr_employee.py (renamed from addons/hr_timesheet/models/hr.py)3
-rw-r--r--addons/website_hr/models/hr_employee.py7
55 files changed, 1787 insertions, 1261 deletions
diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py
index d3ed7d4b2fc..bcbd2fafdf9 100644
--- a/addons/hr/__manifest__.py
+++ b/addons/hr/__manifest__.py
@@ -26,6 +26,12 @@
'security/ir.model.access.csv',
'wizard/hr_plan_wizard_views.xml',
'wizard/hr_departure_wizard_views.xml',
+ 'views/hr_job_views.xml',
+ 'views/hr_plan_views.xml',
+ 'views/hr_employee_category_views.xml',
+ 'views/hr_employee_public_views.xml',
+ 'views/hr_employee_views.xml',
+ 'views/hr_department_views.xml',
'views/hr_views.xml',
'views/hr_templates.xml',
'views/res_config_settings_views.xml',
diff --git a/addons/hr/models/__init__.py b/addons/hr/models/__init__.py
index fb22e57dcf7..efe3c1d702c 100644
--- a/addons/hr/models/__init__.py
+++ b/addons/hr/models/__init__.py
@@ -1,10 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import hr
-from . import res_config_settings
+from . import hr_employee_base
+from . import hr_employee
+from . import hr_employee_category
+from . import hr_employee_public
+from . import hr_department
+from . import hr_job
+from . import hr_plan
from . import mail_alias
from . import mail_channel
+from . import res_config_settings
from . import res_partner
from . import res_users
-from . import hr_plan
diff --git a/addons/hr/models/hr_department.py b/addons/hr/models/hr_department.py
new file mode 100644
index 00000000000..180b3372e46
--- /dev/null
+++ b/addons/hr/models/hr_department.py
@@ -0,0 +1,84 @@
+# -*- 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 Department(models.Model):
+ _name = "hr.department"
+ _description = "HR Department"
+ _inherit = ['mail.thread']
+ _order = "name"
+ _rec_name = 'complete_name'
+
+ name = fields.Char('Department Name', required=True)
+ complete_name = fields.Char('Complete Name', compute='_compute_complete_name', store=True)
+ active = fields.Boolean('Active', default=True)
+ company_id = fields.Many2one('res.company', string='Company', index=True, default=lambda self: self.env.company)
+ parent_id = fields.Many2one('hr.department', string='Parent Department', index=True)
+ child_ids = fields.One2many('hr.department', 'parent_id', string='Child Departments')
+ manager_id = fields.Many2one('hr.employee', string='Manager', tracking=True)
+ member_ids = fields.One2many('hr.employee', 'department_id', string='Members', readonly=True)
+ jobs_ids = fields.One2many('hr.job', 'department_id', string='Jobs')
+ note = fields.Text('Note')
+ color = fields.Integer('Color Index')
+
+ @api.multi
+ def name_get(self):
+ if not self.env.context.get('hierarchical_naming', True):
+ return [(record.id, record.name) for record in self]
+ return super(Department, self).name_get()
+
+ @api.depends('name', 'parent_id.complete_name')
+ def _compute_complete_name(self):
+ for department in self:
+ if department.parent_id:
+ department.complete_name = '%s / %s' % (department.parent_id.complete_name, department.name)
+ else:
+ department.complete_name = department.name
+
+ @api.constrains('parent_id')
+ def _check_parent_id(self):
+ if not self._check_recursion():
+ raise ValidationError(_('You cannot create recursive departments.'))
+
+ @api.model
+ def create(self, vals):
+ # TDE note: auto-subscription of manager done by hand, because currently
+ # the tracking allows to track+subscribe fields linked to a res.user record
+ # An update of the limited behavior should come, but not currently done.
+ department = super(Department, self.with_context(mail_create_nosubscribe=True)).create(vals)
+ manager = self.env['hr.employee'].browse(vals.get("manager_id"))
+ if manager.user_id:
+ department.message_subscribe(partner_ids=manager.user_id.partner_id.ids)
+ return department
+
+ @api.multi
+ def write(self, vals):
+ """ If updating manager of a department, we need to update all the employees
+ of department hierarchy, and subscribe the new manager.
+ """
+ # TDE note: auto-subscription of manager done by hand, because currently
+ # the tracking allows to track+subscribe fields linked to a res.user record
+ # An update of the limited behavior should come, but not currently done.
+ if 'manager_id' in vals:
+ manager_id = vals.get("manager_id")
+ if manager_id:
+ manager = self.env['hr.employee'].browse(manager_id)
+ # subscribe the manager user
+ if manager.user_id:
+ self.message_subscribe(partner_ids=manager.user_id.partner_id.ids)
+ # set the employees's parent to the new manager
+ self._update_employee_manager(manager_id)
+ return super(Department, self).write(vals)
+
+ def _update_employee_manager(self, manager_id):
+ employees = self.env['hr.employee']
+ for department in self:
+ employees = employees | self.env['hr.employee'].search([
+ ('id', '!=', manager_id),
+ ('department_id', '=', department.id),
+ ('parent_id', '=', department.manager_id.id)
+ ])
+ employees.write({'parent_id': manager_id})
diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr_employee.py
index 971c5d9a561..1764b1e0550 100644
--- a/addons/hr/models/hr.py
+++ b/addons/hr/models/hr_employee.py
@@ -1,106 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
from random import choice
from string import digits
-import base64
-import logging
from werkzeug import url_encode
-from odoo import api, fields, models, SUPERUSER_ID
-from odoo import tools, _
+from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import ValidationError, AccessError
from odoo.modules.module import get_module_resource
-_logger = logging.getLogger(__name__)
-
-
-class EmployeeCategory(models.Model):
-
- _name = "hr.employee.category"
- _description = "Employee Category"
-
- name = fields.Char(string="Employee Tag", required=True)
- color = fields.Integer(string='Color Index')
- employee_ids = fields.Many2many('hr.employee', 'employee_category_rel', 'category_id', 'emp_id', string='Employees')
- _sql_constraints = [
- ('name_uniq', 'unique (name)', "Tag name already exists !"),
- ]
-
-
-class Job(models.Model):
-
- _name = "hr.job"
- _description = "Job Position"
- _inherit = ['mail.thread']
-
- name = fields.Char(string='Job Position', required=True, index=True, translate=True)
- expected_employees = fields.Integer(compute='_compute_employees', string='Total Forecasted Employees', store=True,
- help='Expected number of employees for this job position after new recruitment.')
- no_of_employee = fields.Integer(compute='_compute_employees', string="Current Number of Employees", store=True,
- help='Number of employees currently occupying this job position.')
- no_of_recruitment = fields.Integer(string='Expected New Employees', copy=False,
- help='Number of new employees you expect to recruit.', default=1)
- no_of_hired_employee = fields.Integer(string='Hired Employees', copy=False,
- help='Number of hired employees for this job position during recruitment phase.')
- employee_ids = fields.One2many('hr.employee', 'job_id', string='Employees', groups='base.group_user')
- description = fields.Text(string='Job Description')
- requirements = fields.Text('Requirements')
- department_id = fields.Many2one('hr.department', string='Department')
- company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
- state = fields.Selection([
- ('recruit', 'Recruitment in Progress'),
- ('open', 'Not Recruiting')
- ], string='Status', readonly=True, required=True, tracking=True, copy=False, default='recruit', help="Set whether the recruitment process is open or closed for this job position.")
-
- _sql_constraints = [
- ('name_company_uniq', 'unique(name, company_id, department_id)', 'The name of the job position must be unique per department in company!'),
- ]
-
- @api.depends('no_of_recruitment', 'employee_ids.job_id', 'employee_ids.active')
- def _compute_employees(self):
- employee_data = self.env['hr.employee'].read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id'])
- result = dict((data['job_id'][0], data['job_id_count']) for data in employee_data)
- for job in self:
- job.no_of_employee = result.get(job.id, 0)
- job.expected_employees = result.get(job.id, 0) + job.no_of_recruitment
-
- @api.model
- def create(self, values):
- """ We don't want the current user to be follower of all created job """
- return super(Job, self.with_context(mail_create_nosubscribe=True)).create(values)
-
- @api.multi
- @api.returns('self', lambda value: value.id)
- def copy(self, default=None):
- self.ensure_one()
- default = dict(default or {})
- if 'name' not in default:
- default['name'] = _("%s (copy)") % (self.name)
- return super(Job, self).copy(default=default)
-
- @api.multi
- def set_recruit(self):
- for record in self:
- no_of_recruitment = 1 if record.no_of_recruitment == 0 else record.no_of_recruitment
- record.write({'state': 'recruit', 'no_of_recruitment': no_of_recruitment})
- return True
-
- @api.multi
- def set_open(self):
- return self.write({
- 'state': 'open',
- 'no_of_recruitment': 0,
- 'no_of_hired_employee': 0
- })
-
-
-class Employee(models.Model):
+class HrEmployeePrivate(models.Model):
+ """
+ NB: Any field only available on the model hr.employee (i.e. not on the
+ hr.employee.public model) should have `groups="hr.group_hr_user"` on its
+ definition to avoid being prefetched when the user hasn't access to the
+ hr.employee model. Indeed, the prefetch loads the data for all the fields
+ that are available according to the group defined on them.
+ """
_name = "hr.employee"
_description = "Employee"
_order = 'name'
- _inherit = ['mail.thread', 'mail.activity.mixin', 'resource.mixin']
-
+ _inherit = ['hr.employee.base', 'mail.thread', 'mail.activity.mixin', 'resource.mixin']
_mail_post_access = 'read'
@api.model
@@ -167,7 +89,6 @@ class Employee(models.Model):
emergency_phone = fields.Char("Emergency Phone", groups="hr.group_hr_user", tracking=True)
km_home_work = fields.Integer(string="Km home-work", groups="hr.group_hr_user", tracking=True)
google_drive_link = fields.Char(string="Employee Documents", groups="hr.group_hr_user", tracking=True)
- job_title = fields.Char("Job Title")
# image: all image fields are base64 encoded and PIL-supported
image = fields.Binary(
@@ -183,17 +104,8 @@ class Employee(models.Model):
help="Small-sized photo of the employee. It is automatically "
"resized as a 64x64px image, with aspect ratio preserved. "
"Use this field anywhere a small image is required.")
- # work
- address_id = fields.Many2one(
- 'res.partner', 'Work Address')
- work_phone = fields.Char('Work Phone')
- mobile_phone = fields.Char('Work Mobile')
phone = fields.Char(related='address_home_id.phone', related_sudo=False, string="Private Phone", groups="hr.group_hr_user")
- work_email = fields.Char('Work Email')
- work_location = fields.Char('Work Location')
# employee in company
- job_id = fields.Many2one('hr.job', 'Job Position')
- department_id = fields.Many2one('hr.department', 'Department')
parent_id = fields.Many2one('hr.employee', 'Manager')
child_ids = fields.One2many('hr.employee', 'parent_id', string='Direct subordinates')
coach_id = fields.Many2one('hr.employee', 'Coach')
@@ -202,22 +114,80 @@ class Employee(models.Model):
'emp_id', 'category_id',
string='Tags')
# misc
- notes = fields.Text('Notes')
- color = fields.Integer('Color Index', default=0)
- barcode = fields.Char(string="Badge ID", help="ID used for employee identification.", copy=False)
- pin = fields.Char(string="PIN", help="PIN used to Check In/Out in Kiosk Mode (if enabled in Configuration).", copy=False)
+ notes = fields.Text('Notes', groups="hr.group_hr_user")
+ color = fields.Integer('Color Index', default=0, groups="hr.group_hr_user")
+ barcode = fields.Char(string="Badge ID", help="ID used for employee identification.", groups="hr.group_hr_user", copy=False)
+ pin = fields.Char(string="PIN", groups="hr.group_hr_user", copy=False,
+ help="PIN used to Check In/Out in Kiosk Mode (if enabled in Configuration).")
departure_reason = fields.Selection([
('fired', 'Fired'),
('resigned', 'Resigned'),
('retired', 'Retired')
- ], string="Departure Reason", copy=False, tracking=True)
- departure_description = fields.Text(string="Additional Information", copy=False, tracking=True)
+ ], string="Departure Reason", groups="hr.group_hr_user", copy=False, tracking=True)
+ departure_description = fields.Text(string="Additional Information", groups="hr.group_hr_user", copy=False, tracking=True)
+ message_main_attachment_id = fields.Many2one(groups="hr.group_hr_user")
_sql_constraints = [
('barcode_uniq', 'unique (barcode)', "The Badge ID must be unique, this one is already assigned to another employee."),
('user_uniq', 'unique (user_id, company_id)', "A user cannot be linked to multiple employees in the same company.")
]
+ @api.multi
+ def name_get(self):
+ if self.check_access_rights('read', raise_exception=False):
+ return super(HrEmployeePrivate, self).name_get()
+ return self.env['hr.employee.public'].browse(self.ids).name_get()
+
+ @api.multi
+ def read(self, fields, load='_classic_read'):
+ if self.check_access_rights('read', raise_exception=False):
+ return super(HrEmployeePrivate, self).read(fields, load=load)
+ private_fields = set(fields).difference(self.env['hr.employee.public']._fields.keys())
+ if private_fields:
+ raise AccessError(_('The fields "%s" you try to read is not available on the public employee profile.') % (','.join(private_fields)))
+ return self.env['hr.employee.public'].browse(self.ids).read(fields, load=load)
+
+ @api.model
+ def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
+ """
+ We override the _search because it is the method that checks the access rights
+ This is correct to override the _search. That way we enforce the fact that calling
+ search on an hr.employee returns a hr.employee recordset, even if you don't have access
+ to this model, as the result of _search (the ids of the public employees) is to be
+ browsed on the hr.employee model. This can be trusted as the ids of the public
+ employees exactly match the ids of the related hr.employee.
+ """
+ if self.check_access_rights('read', raise_exception=False):
+ return super(HrEmployeePrivate, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
+ return self.env['hr.employee.public']._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
+
+ @api.multi
+ def get_formview_id(self, access_uid=None):
+ """ Override this method in order to redirect many2one towards the right model depending on access_uid """
+ if access_uid:
+ self_sudo = self.sudo(access_uid)
+ else:
+ self_sudo = self
+
+ if self_sudo.check_access_rights('read', raise_exception=False):
+ return super(HrEmployeePrivate, self).get_formview_id(access_uid=access_uid)
+ # Hardcode the form view for public employee
+ return self.env.ref('hr.hr_employee_public_view_form').id
+
+ @api.multi
+ def get_formview_action(self, access_uid=None):
+ """ Override this method in order to redirect many2one towards the right model depending on access_uid """
+ res = super(HrEmployeePrivate, self).get_formview_action(access_uid=access_uid)
+ if access_uid:
+ self_sudo = self.sudo(access_uid)
+ else:
+ self_sudo = self
+
+ if not self_sudo.check_access_rights('read', raise_exception=False):
+ res['res_model'] = 'hr.employee.public'
+
+ return res
+
@api.constrains('pin')
def _verify_pin(self):
for employee in self:
@@ -278,7 +248,7 @@ class Employee(models.Model):
vals.update(self._sync_user(user))
vals['name'] = vals.get('name', user.name)
tools.image_resize_images(vals)
- employee = super(Employee, self).create(vals)
+ employee = super(HrEmployeePrivate, self).create(vals)
url = '/web#%s' % url_encode({'action': 'hr.plan_wizard_action', 'active_id': employee.id, 'active_model': 'hr.employee'})
employee._message_log(body=_('<b>Congratulations !</b> May I recommand you to setup an <a href="%s">onboarding plan ?</a>') % (url))
if employee.department_id:
@@ -296,7 +266,7 @@ class Employee(models.Model):
if vals.get('user_id'):
vals.update(self._sync_user(self.env['res.users'].browse(vals['user_id'])))
tools.image_resize_images(vals)
- res = super(Employee, self).write(vals)
+ res = super(HrEmployeePrivate, self).write(vals)
if vals.get('department_id') or vals.get('user_id'):
department_id = vals['department_id'] if vals.get('department_id') else self[:1].department_id.id
# When added to a department or changing user, subscribe to the channels auto-subscribed by department
@@ -308,11 +278,11 @@ class Employee(models.Model):
@api.multi
def unlink(self):
resources = self.mapped('resource_id')
- super(Employee, self).unlink()
+ super(HrEmployeePrivate, self).unlink()
return resources.unlink()
def toggle_active(self):
- res = super(Employee, self).toggle_active()
+ res = super(HrEmployeePrivate, self).toggle_active()
self.filtered(lambda employee: employee.active).write({
'departure_reason': False,
'departure_description': False,
@@ -331,7 +301,8 @@ class Employee(models.Model):
@api.multi
def generate_random_barcode(self):
- for i in self: i.barcode = "".join(choice(digits) for i in range(8))
+ for employee in self:
+ employee.barcode = "".join(choice(digits) for i in range(8))
@api.depends('address_home_id.parent_id')
def _compute_is_address_home_a_company(self):
@@ -364,88 +335,9 @@ class Employee(models.Model):
return self
def _message_log(self, **kwargs):
- return super(Employee, self._post_author())._message_log(**kwargs)
+ return super(HrEmployeePrivate, self._post_author())._message_log(**kwargs)
@api.multi
@api.returns('mail.message', lambda value: value.id)
def message_post(self, *args, **kwargs):
- return super(Employee, self._post_author()).message_post(*args, **kwargs)
-
-
-class Department(models.Model):
- _name = "hr.department"
- _description = "HR Department"
- _inherit = ['mail.thread']
- _order = "name"
- _rec_name = 'complete_name'
-
- name = fields.Char('Department Name', required=True)
- complete_name = fields.Char('Complete Name', compute='_compute_complete_name', store=True)
- active = fields.Boolean('Active', default=True)
- company_id = fields.Many2one('res.company', string='Company', index=True, default=lambda self: self.env.company)
- parent_id = fields.Many2one('hr.department', string='Parent Department', index=True)
- child_ids = fields.One2many('hr.department', 'parent_id', string='Child Departments')
- manager_id = fields.Many2one('hr.employee', string='Manager', tracking=True)
- member_ids = fields.One2many('hr.employee', 'department_id', string='Members', readonly=True)
- jobs_ids = fields.One2many('hr.job', 'department_id', string='Jobs')
- note = fields.Text('Note')
- color = fields.Integer('Color Index')
-
- @api.multi
- def name_get(self):
- if not self.env.context.get('hierarchical_naming', True):
- return [(record.id, record.name) for record in self]
- return super(Department, self).name_get()
-
- @api.depends('name', 'parent_id.complete_name')
- def _compute_complete_name(self):
- for department in self:
- if department.parent_id:
- department.complete_name = '%s / %s' % (department.parent_id.complete_name, department.name)
- else:
- department.complete_name = department.name
-
- @api.constrains('parent_id')
- def _check_parent_id(self):
- if not self._check_recursion():
- raise ValidationError(_('You cannot create recursive departments.'))
-
- @api.model
- def create(self, vals):
- # TDE note: auto-subscription of manager done by hand, because currently
- # the tracking allows to track+subscribe fields linked to a res.user record
- # An update of the limited behavior should come, but not currently done.
- department = super(Department, self.with_context(mail_create_nosubscribe=True)).create(vals)
- manager = self.env['hr.employee'].browse(vals.get("manager_id"))
- if manager.user_id:
- department.message_subscribe(partner_ids=manager.user_id.partner_id.ids)
- return department
-
- @api.multi
- def write(self, vals):
- """ If updating manager of a department, we need to update all the employees
- of department hierarchy, and subscribe the new manager.
- """
- # TDE note: auto-subscription of manager done by hand, because currently
- # the tracking allows to track+subscribe fields linked to a res.user record
- # An update of the limited behavior should come, but not currently done.
- if 'manager_id' in vals:
- manager_id = vals.get("manager_id")
- if manager_id:
- manager = self.env['hr.employee'].browse(manager_id)
- # subscribe the manager user
- if manager.user_id:
- self.message_subscribe(partner_ids=manager.user_id.partner_id.ids)
- # set the employees's parent to the new manager
- self._update_employee_manager(manager_id)
- return super(Department, self).write(vals)
-
- def _update_employee_manager(self, manager_id):
- employees = self.env['hr.employee']
- for department in self:
- employees = employees | self.env['hr.employee'].search([
- ('id', '!=', manager_id),
- ('department_id', '=', department.id),
- ('parent_id', '=', department.manager_id.id)
- ])
- employees.write({'parent_id': manager_id})
+ return super(HrEmployeePrivate, self._post_author()).message_post(*args, **kwargs)
diff --git a/addons/hr/models/hr_employee_base.py b/addons/hr/models/hr_employee_base.py
new file mode 100644
index 00000000000..fec003ccfc6
--- /dev/null
+++ b/addons/hr/models/hr_employee_base.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class HrEmployeeBase(models.AbstractModel):
+ _name = "hr.employee.base"
+ _description = "Basic Employee"
+ _order = 'name'
+
+ name = fields.Char()
+ active = fields.Boolean("Active")
+ department_id = fields.Many2one('hr.department', 'Department')
+ job_id = fields.Many2one('hr.job', 'Job Position')
+ job_title = fields.Char("Job Title")
+ company_id = fields.Many2one('res.company', 'Company')
+ address_id = fields.Many2one('res.partner', 'Work Address')
+ work_phone = fields.Char('Work Phone')
+ mobile_phone = fields.Char('Work Mobile')
+ work_email = fields.Char('Work Email')
+ work_location = fields.Char('Work Location')
+ user_id = fields.Many2one('res.users')
+ resource_id = fields.Many2one('resource.resource')
+ resource_calendar_id = fields.Many2one('resource.calendar')
diff --git a/addons/hr/models/hr_employee_category.py b/addons/hr/models/hr_employee_category.py
new file mode 100644
index 00000000000..a088b9af893
--- /dev/null
+++ b/addons/hr/models/hr_employee_category.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class EmployeeCategory(models.Model):
+
+ _name = "hr.employee.category"
+ _description = "Employee Category"
+
+ name = fields.Char(string="Employee Tag", required=True)
+ color = fields.Integer(string='Color Index')
+ employee_ids = fields.Many2many('hr.employee', 'employee_category_rel', 'category_id', 'emp_id', string='Employees')
+
+ _sql_constraints = [
+ ('name_uniq', 'unique (name)', "Tag name already exists !"),
+ ]
diff --git a/addons/hr/models/hr_employee_public.py b/addons/hr/models/hr_employee_public.py
new file mode 100644
index 00000000000..82799661d0e
--- /dev/null
+++ b/addons/hr/models/hr_employee_public.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, tools
+
+
+class HrEmployeePublic(models.Model):
+ _name = "hr.employee.public"
+ _inherit = ["hr.employee.base"]
+ _description = 'Public Employee'
+ _order = 'name'
+ _auto = False
+ _log_access = True # Include magic fields
+
+ # Fields coming from hr.employee.base
+ create_date = fields.Datetime(readonly=True)
+ name = fields.Char(readonly=True)
+ active = fields.Boolean(readonly=True)
+ department_id = fields.Many2one(readonly=True)
+ job_id = fields.Many2one(readonly=True)
+ job_title = fields.Char(readonly=True)
+ company_id = fields.Many2one(readonly=True)
+ address_id = fields.Many2one(readonly=True)
+ mobile_phone = fields.Char(readonly=True)
+ work_phone = fields.Char(readonly=True)
+ work_email = fields.Char(readonly=True)
+ work_location = fields.Char(readonly=True)
+ user_id = fields.Many2one(readonly=True)
+ resource_id = fields.Many2one(readonly=True)
+ resource_calendar_id = fields.Many2one(readonly=True)
+
+ # hr.employee.public specific fields
+ child_ids = fields.One2many('hr.employee.public', 'parent_id', string='Direct subordinates', readonly=True)
+ image = fields.Binary("Photo", compute='_compute_image', readonly=True, compute_sudo=True)
+ image_medium = fields.Binary("Medium-sized photo", compute='_compute_image', readonly=True, compute_sudo=True)
+ image_small = fields.Binary("Small-sized photo", compute='_compute_image', readonly=True, compute_sudo=True)
+ parent_id = fields.Many2one('hr.employee.public', 'Manager', readonly=True)
+ coach_id = fields.Many2one('hr.employee.public', 'Coach', readonly=True)
+
+ def _compute_image(self):
+ for employee in self:
+ # We have to be in sudo to have access to the images
+ employee_id = self.sudo().env['hr.employee'].browse(employee.id)
+ employee.image = employee_id.image
+ employee.image_medium = employee_id.image_medium
+ employee.image_small = employee_id.image_small
+
+ @api.model
+ def _get_fields(self):
+ return ','.join('emp.%s' % name for name, field in self._fields.items() if field.store and field.type not in ['many2many', 'one2many'])
+
+ @api.model_cr
+ def init(self):
+ tools.drop_view_if_exists(self.env.cr, self._table)
+ self.env.cr.execute("""CREATE or REPLACE VIEW %s as (
+ SELECT
+ %s
+ FROM hr_employee emp
+ )""" % (self._table, self._get_fields()))
diff --git a/addons/hr/models/hr_job.py b/addons/hr/models/hr_job.py
new file mode 100644
index 00000000000..d181a0b18f0
--- /dev/null
+++ b/addons/hr/models/hr_job.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+
+
+class Job(models.Model):
+
+ _name = "hr.job"
+ _description = "Job Position"
+ _inherit = ['mail.thread']
+
+ name = fields.Char(string='Job Position', required=True, index=True, translate=True)
+ expected_employees = fields.Integer(compute='_compute_employees', string='Total Forecasted Employees', store=True,
+ help='Expected number of employees for this job position after new recruitment.')
+ no_of_employee = fields.Integer(compute='_compute_employees', string="Current Number of Employees", store=True,
+ help='Number of employees currently occupying this job position.')
+ no_of_recruitment = fields.Integer(string='Expected New Employees', copy=False,
+ help='Number of new employees you expect to recruit.', default=1)
+ no_of_hired_employee = fields.Integer(string='Hired Employees', copy=False,
+ help='Number of hired employees for this job position during recruitment phase.')
+ employee_ids = fields.One2many('hr.employee', 'job_id', string='Employees', groups='base.group_user')
+ description = fields.Text(string='Job Description')
+ requirements = fields.Text('Requirements')
+ department_id = fields.Many2one('hr.department', string='Department')
+ company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
+ state = fields.Selection([
+ ('recruit', 'Recruitment in Progress'),
+ ('open', 'Not Recruiting')
+ ], string='Status', readonly=True, required=True, tracking=True, copy=False, default='recruit', help="Set whether the recruitment process is open or closed for this job position.")
+
+ _sql_constraints = [
+ ('name_company_uniq', 'unique(name, company_id, department_id)', 'The name of the job position must be unique per department in company!'),
+ ]
+
+ @api.depends('no_of_recruitment', 'employee_ids.job_id', 'employee_ids.active')
+ def _compute_employees(self):
+ employee_data = self.env['hr.employee'].read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id'])
+ result = dict((data['job_id'][0], data['job_id_count']) for data in employee_data)
+ for job in self:
+ job.no_of_employee = result.get(job.id, 0)
+ job.expected_employees = result.get(job.id, 0) + job.no_of_recruitment
+
+ @api.model
+ def create(self, values):
+ """ We don't want the current user to be follower of all created job """
+ return super(Job, self.with_context(mail_create_nosubscribe=True)).create(values)
+
+ @api.multi
+ @api.returns('self', lambda value: value.id)
+ def copy(self, default=None):
+ self.ensure_one()
+ default = dict(default or {})
+ if 'name' not in default:
+ default['name'] = _("%s (copy)") % (self.name)
+ return super(Job, self).copy(default=default)
+
+ @api.multi
+ def set_recruit(self):
+ for record in self:
+ no_of_recruitment = 1 if record.no_of_recruitment == 0 else record.no_of_recruitment
+ record.write({'state': 'recruit', 'no_of_recruitment': no_of_recruitment})
+ return True
+
+ @api.multi
+ def set_open(self):
+ return self.write({
+ 'state': 'open',
+ 'no_of_recruitment': 0,
+ 'no_of_hired_employee': 0
+ })
diff --git a/addons/hr/security/hr_security.xml b/addons/hr/security/hr_security.xml
index c735d05b1ce..bbafaf7fb38 100644
--- a/addons/hr/security/hr_security.xml
+++ b/addons/hr/security/hr_security.xml
@@ -40,6 +40,13 @@
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
+ <record id="hr_employee_public_comp_rule" model="ir.rule">
+ <field name="name">Employee multi company rule</field>
+ <field name="model_id" ref="model_hr_employee_public"/>
+ <field name="global" eval="True"/>
+ <field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
+ </record>
+
<record id="hr_job_comp_rule" model="ir.rule">
<field name="name">Job multi company rule</field>
<field name="model_id" ref="model_hr_job"/>
diff --git a/addons/hr/security/ir.model.access.csv b/addons/hr/security/ir.model.access.csv
index 345374b31fe..e28b4065ee3 100644
--- a/addons/hr/security/ir.model.access.csv
+++ b/addons/hr/security/ir.model.access.csv
@@ -2,7 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_employee_category_user,hr.employee.category.user,model_hr_employee_category,group_hr_user,1,1,1,1
access_hr_employee_category_emp,hr.employee.category.emp,model_hr_employee_category,base.group_user,1,0,0,0
access_hr_employee_user,hr.employee user,model_hr_employee,group_hr_user,1,1,1,1
-access_hr_employee_system_user,hr.employee system user,model_hr_employee,base.group_user,1,0,0,0
+access_hr_employee_system_user,hr.employee system user,model_hr_employee,base.group_user,0,0,0,0
+access_hr_employee_public_user,hr.employee_public,model_hr_employee_public,base.group_user,1,0,0,0
access_hr_employee_resource_user,resource.resource.user,resource.model_resource_resource,group_hr_user,1,1,1,1
access_hr_department_user,hr.department.user,model_hr_department,group_hr_user,1,1,1,1
access_hr_department_employee,hr.department.employee,model_hr_department,base.group_user,1,0,0,0
diff --git a/addons/hr/tests/test_self_user_access.py b/addons/hr/tests/test_self_user_access.py
index 360cf4f5a1e..ab732f9f9e6 100644
--- a/addons/hr/tests/test_self_user_access.py
+++ b/addons/hr/tests/test_self_user_access.py
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from collections import OrderedDict
+
from odoo.addons.hr.tests.common import TestHrCommon
from odoo.tests import new_test_user
from odoo.exceptions import AccessError
-from datetime import date
-from collections import OrderedDict
class TestSelfAccessRights(TestHrCommon):
@@ -26,6 +26,8 @@ class TestSelfAccessRights(TestHrCommon):
})
self.protected_fields_emp = OrderedDict([(k, v) for k, v in self.env['hr.employee']._fields.items() if v.groups == 'hr.group_hr_user'])
+ # Compute fields and id field are always readable by everyone
+ self.read_protected_fields_emp = OrderedDict([(k, v) for k, v in self.env['hr.employee']._fields.items() if not v.compute and k != 'id'])
self.self_protected_fields_user = OrderedDict([
(k, v)
for k, v in self.env['res.users']._fields.items()
@@ -34,14 +36,12 @@ class TestSelfAccessRights(TestHrCommon):
# Read hr.employee #
def testReadSelfEmployee(self):
- for f in self.protected_fields_emp:
- with self.assertRaises(AccessError):
- self.richard_emp.sudo(self.richard)[f]
+ with self.assertRaises(AccessError):
+ self.hubert_emp.sudo(self.richard).read(self.protected_fields_emp.keys())
def testReadOtherEmployee(self):
- for f in self.protected_fields_emp:
- with self.assertRaises(AccessError):
- self.hubert_emp.sudo(self.richard)[f]
+ with self.assertRaises(AccessError):
+ self.hubert_emp.sudo(self.richard).read(self.protected_fields_emp.keys())
# Write hr.employee #
def testWriteSelfEmployee(self):
@@ -60,10 +60,8 @@ class TestSelfAccessRights(TestHrCommon):
self.richard.sudo(self.richard).read([f]) # should not raise
def testReadOtherUserEmployee(self):
-
- for f in self.self_protected_fields_user:
- with self.assertRaises(AccessError, msg="Field %s should not be readable by other usrs" % f):
- self.hubert.sudo(self.richard)[f]
+ with self.assertRaises(AccessError):
+ self.hubert.sudo(self.richard).read(self.self_protected_fields_user)
# Write res.users #
def testWriteSelfUserEmployeeSettingFalse(self):
@@ -76,7 +74,7 @@ class TestSelfAccessRights(TestHrCommon):
for f, v in self.self_protected_fields_user.items():
val = None
if v.type == 'char' or v.type == 'text':
- val = 'dummy'
+ val = '0000' if f == 'pin' else 'dummy'
if val is not None:
self.richard.sudo(self.richard).write({f: val})
@@ -116,7 +114,6 @@ class TestSelfAccessRights(TestHrCommon):
self.richard.sudo(self.richard).write({'phone': '2154545'})
def testWriteOtherUserEmployee(self):
-
for f in self.self_protected_fields_user:
with self.assertRaises(AccessError):
self.hubert.sudo(self.richard).write({f: 'dummy'})
diff --git a/addons/hr/views/hr_department_views.xml b/addons/hr/views/hr_department_views.xml
new file mode 100644
index 00000000000..a0ebffc6c27
--- /dev/null
+++ b/addons/hr/views/hr_department_views.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <record id="view_department_form" model="ir.ui.view">
+ <field name="name">hr.department.form</field>
+ <field name="model">hr.department</field>
+ <field name="arch" type="xml">
+ <form string="department">
+ <sheet>
+ <div class="oe_button_box" name="button_box">
+ <button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
+ <field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
+ </button>
+ </div>
+ <group col="4">
+ <field name="name"/>
+ <field name="manager_id"/>
+ <field name="parent_id"/>
+ <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
+ </group>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_department_tree" model="ir.ui.view">
+ <field name="name">hr.department.tree</field>
+ <field name="model">hr.department</field>
+ <field name="arch" type="xml">
+ <tree string="Companies">
+ <field name="display_name"/>
+ <field name="company_id" groups="base.group_multi_company"/>
+ <field name="manager_id"/>
+ <field name="parent_id"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_department_filter" model="ir.ui.view">
+ <field name="name">hr.department.search</field>
+ <field name="model">hr.department</field>
+ <field name="arch" type="xml">
+ <search string="Departments">
+ <field name="name" string="Department"/>
+ <field name="manager_id" />
+ <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
+ <separator/>
+ <filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="hr_department_view_kanban" model="ir.ui.view" >
+ <field name="name">hr.department.kanban</field>
+ <field name="model">hr.department</field>
+ <field name="arch" type="xml">
+ <kanban class="oe_background_grey o_kanban_dashboard o_hr_kanban">
+ <field name="name"/>
+ <field name="company_id"/>
+ <field name="manager_id"/>
+ <field name="color"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div t-attf-class="#{kanban_color(record.color.raw_value)}">
+ <div t-attf-class="o_kanban_card_header">
+ <div class="o_kanban_card_header_title">
+ <div class="o_primary"><field name="name"/></div>
+ <div class="o_secondary"><field name="company_id" groups="base.group_multi_company"/></div>
+ </div>
+ <div class="o_kanban_manage_button_section">
+ <a class="o_kanban_manage_toggle_button" href="#"><i class="fa fa-ellipsis-v" role="img" aria-label="Manage" title="Manage"/></a>
+ </div>
+ </div>
+ <div class="container o_kanban_card_content">
+ <div class="row">
+ <div class="col-6 o_kanban_primary_left">
+ <button class="btn btn-primary" name="%(act_employee_from_department)d" type="action">Employees</button>
+ </div>
+ <div class="col-6 o_kanban_primary_right">
+ </div>
+ </div>
+ </div><div class="container o_kanban_card_manage_pane dropdown-menu" role="menu">
+ <div class="row">
+ <div role="menuitem" class="col-4 o_kanban_card_manage_section o_kanban_manage_to_do">
+ <div class="o_kanban_card_manage_title">
+ <span>To Do</span>
+ </div>
+ </div>
+ <div role="menuitem" class="col-4 o_kanban_card_manage_section o_kanban_manage_to_approve">
+ <div class="o_kanban_card_manage_title">
+ <span>To Approve</span>
+ </div>
+ </div>
+ <div role="menuitem" class="col-4 o_kanban_card_manage_section o_kanban_manage_reports">
+ <div class="o_kanban_card_manage_title">
+ <span>Reporting</span>
+ </div>
+ </div>
+ </div>
+
+ <div t-if="widget.editable" class="o_kanban_card_manage_settings row">
+ <div role="menuitem" aria-haspopup="true" class="col-8">
+ <ul class="oe_kanban_colorpicker" data-field="color" role="menu"/>
+ </div>
+ <div class="col-4 text-right">
+ <a role="menuitem" type="edit">Settings</a>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
+ <record id="open_module_tree_department" model="ir.actions.act_window">
+ <field name="name">Departments</field>
+ <field name="res_model">hr.department</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">kanban,tree,form</field>
+ <field name="search_view_id" ref="view_department_filter"/>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Create a new department
+ </p><p>
+ Odoo's department structure is used to manage all documents
+ related to employees by departments: expenses, timesheets,
+ leaves, recruitments, etc.
+ </p>
+ </field>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/hr/views/hr_employee_category_views.xml b/addons/hr/views/hr_employee_category_views.xml
new file mode 100644
index 00000000000..56dea45d32d
--- /dev/null
+++ b/addons/hr/views/hr_employee_category_views.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <record id="view_employee_category_form" model="ir.ui.view">
+ <field name="name">hr.employee.category.form</field>
+ <field name="model">hr.employee.category</field>
+ <field name="arch" type="xml">
+ <form string="Employee Tags">
+ <sheet>
+ <group>
+ <field name="name"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_employee_category_list" model="ir.ui.view">
+ <field name="name">hr.employee.category.list</field>
+ <field name="model">hr.employee.category</field>
+ <field eval="8" name="priority"/>
+ <field name="arch" type="xml">
+ <tree string="Employees Tags">
+ <field name="name"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="open_view_categ_form" model="ir.actions.act_window">
+ <field name="name">Employee Tags</field>
+ <field name="res_model">hr.employee.category</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ </data>
+ </odoo>
diff --git a/addons/hr/views/hr_employee_public_views.xml b/addons/hr/views/hr_employee_public_views.xml
new file mode 100644
index 00000000000..6b7430c21f0
--- /dev/null
+++ b/addons/hr/views/hr_employee_public_views.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <record id="hr_employee_public_view_search" model="ir.ui.view">
+ <field name="name">hr.employee.search</field>
+ <field name="model">hr.employee.public</field>
+ <field name="arch" type="xml">
+ <search string="Employees">
+ <field name="name" string="Employees" filter_domain="['|',('work_email','ilike',self),('name','ilike',self)]"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="hr_employee_public_view_form" model="ir.ui.view">
+ <field name="name">hr.employee.public.form</field>
+ <field name="model">hr.employee.public</field>
+ <field name="arch" type="xml">
+ <form string="Employee" create="0" write="0">
+ <sheet>
+ <field name="user_id" invisible="1"/>
+ <field name="active" invisible="1"/>
+ <div class="oe_button_box" name="button_box">
+ <!-- Used by other modules-->
+ </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>
+ <h2>
+ <field name="job_title" placeholder="Job Title" />
+ </h2>
+ </div>
+ <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>
+ <group string="Schedule" groups="base.group_no_one">
+ <field name="resource_calendar_id"/>
+ </group>
+ </div>
+ </div>
+ </page>
+ </notebook>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="hr_employee_public_view_tree" model="ir.ui.view">
+ <field name="name">hr.employee.tree</field>
+ <field name="model">hr.employee.public</field>
+ <field name="arch" type="xml">
+ <tree string="Employees">
+ <field name="name"/>
+ <field name="work_phone"/>
+ <field name="work_email"/>
+ <field name="company_id" groups="base.group_multi_company"/>
+ <field name="department_id"/>
+ <field name="job_id"/>
+ <field name="parent_id"/>
+ <field name="coach_id" invisible="1"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="hr_employee_public_view_kanban" model="ir.ui.view">
+ <field name="name">hr.employee.kanban</field>
+ <field name="model">hr.employee.public</field>
+ <field name="priority">10</field>
+ <field name="arch" type="xml">
+ <kanban class="o_hr_employee_kanban">
+ <field name="id"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
+ <!-- Medium employee's picture -->
+ <div class="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image: url(#{kanban_image('hr.employee.public', 'image_medium', record.id.raw_value)})" role="img"/>
+ <!-- Employee's picture optimized for mobile -->
+ <div class="o_kanban_image d-md-none rounded-circle" t-attf-style="background-image: url(#{kanban_image('hr.employee.public', 'image_small', record.id.raw_value)})" role="img"/>
+
+ <div class="oe_kanban_details">
+ <div class="o_kanban_record_top">
+ <div class="o_kanban_record_headings">
+ <strong class="o_kanban_record_title">
+ <field name="name"/>
+ </strong>
+ <span t-if="record.job_id.raw_value" class="o_kanban_record_subtitle"><field name="job_id"/></span>
+ </div>
+ </div>
+ <ul>
+ <li id="last_login"/>
+ <li t-if="record.work_location.raw_value"><field name="work_location"/></li>
+ </ul>
+ </div>
+ </div>
+ </t>
+ </templates>
+ <searchpanel>
+ <field name="company_id" groups="base.group_multi_company" icon="fa-building"/>
+ <field name="department_id" icon="fa-users"/>
+ </searchpanel>
+ </kanban>
+ </field>
+ </record>
+
+ <record id="hr_employee_public_action" model="ir.actions.act_window">
+ <field name="name">Employees</field>
+ <field name="res_model">hr.employee.public</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">kanban,tree,form</field>
+ <field name="domain">[]</field>
+ <field name="context">{}</field>
+ <field name="view_id" eval="False"/>
+ <field name="search_view_id" ref="hr_employee_public_view_search"/>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Add a new employee
+ </p><p>
+ With just a quick glance on the Odoo employee screen, you
+ can easily find all the information you need for each person;
+ contact data, job position, availability, etc.
+ </p>
+ </field>
+ </record>
+
+ </data>
+</odoo>
diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml
new file mode 100644
index 00000000000..b4a9b6bfcc1
--- /dev/null
+++ b/addons/hr/views/hr_employee_views.xml
@@ -0,0 +1,360 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <record id="view_employee_filter" model="ir.ui.view">
+ <field name="name">hr.employee.search</field>
+ <field name="model">hr.employee</field>
+ <field name="arch" type="xml">
+ <search string="Employees">
+ <field name="name" string="Employees" filter_domain="['|',('work_email','ilike',self),('name','ilike',self)]"/>
+ <field name="department_id" operator="child_of"/>
+ <field name="category_ids" groups="hr.group_hr_user"/>
+ <field name="job_id" />
+ <field name="company_id" groups="base.group_multi_company"/>
+ <filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
+ <separator/>
+ <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
+ <separator/>
+ <filter string="Late Activities" name="activities_overdue"
+ domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"/>
+ <filter string="Today Activities" name="activities_today"
+ domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ <filter string="Future Activities" name="activities_upcoming_all"
+ domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
+ <group expand="0" string="Group By">
+ <filter name="group_manager" string="Manager" domain="[]" context="{'group_by':'parent_id'}"/>
+ <filter name="group_coach" string="Coach" domain="[]" context="{'group_by':'coach_id'}"/>
+ <filter name="group_department" string="Department" domain="[]" context="{'group_by':'department_id'}"/>
+ <filter name="group_job" string="Job" domain="[]" context="{'group_by':'job_id'}"/>
+ <filter name="group_company" string="Company" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record id="view_employee_form" model="ir.ui.view">
+ <field name="name">hr.employee.form</field>
+ <field name="model">hr.employee</field>
+ <field name="arch" type="xml">
+ <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">
+ <!-- Used by other modules-->
+ </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>
+ <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>
+ <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>
+ <group name="departure" 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="Contact Information">
+ <field name="address_home_id"
+ context="{
+ 'show_address': 1,
+ '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" groups="hr.group_hr_user"
+ 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" 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="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="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="Education">
+ <field name="certificate"/>
+ <field name="study_field"/>
+ <field name="study_school"/>
+ </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="hr_settings" string="HR Settings" groups="hr.group_hr_user">
+ <group>
+ <group string='Status' name="active_group">
+ <field name="user_id" string="Related User"/>
+ </group>
+ <group string="Login Information" name="identification_group">
+ <field name="pin"/>
+ <field name="barcode"/>
+ <button string="Generate Badge ID" class="btn btn-primary" type="object" name="generate_random_barcode" attrs="{'invisible': [('barcode', '!=', False)]}"/>
+ </group>
+ </group>
+ </page>
+ </notebook>
+
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
+ <field name="activity_ids" widget="mail_activity"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </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>
+ <field name="arch" type="xml">
+ <tree string="Employees">
+ <field name="name"/>
+ <field name="work_phone"/>
+ <field name="work_email"/>
+ <field name="company_id" groups="base.group_multi_company"/>
+ <field name="department_id"/>
+ <field name="job_id"/>
+ <field name="parent_id"/>
+ <field name="coach_id" invisible="1"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="hr_kanban_view_employees" model="ir.ui.view">
+ <field name="name">hr.employee.kanban</field>
+ <field name="model">hr.employee</field>
+ <field name="priority">10</field>
+ <field name="arch" type="xml">
+ <kanban class="o_hr_employee_kanban">
+ <field name="id"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
+ <!-- Medium employee's picture -->
+ <div class="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image: url(#{kanban_image('hr.employee', 'image_medium', record.id.raw_value)})" role="img"/>
+ <!-- Employee's picture optimized for mobile -->
+ <div class="o_kanban_image d-md-none rounded-circle" t-attf-style="background-image: url(#{kanban_image('hr.employee', 'image_small', record.id.raw_value)})" role="img"/>
+
+ <div class="oe_kanban_details">
+ <div class="o_kanban_record_top">
+ <div class="o_kanban_record_headings">
+ <strong class="o_kanban_record_title">
+ <field name="name"/>
+ </strong>
+ <span t-if="record.job_id.raw_value" class="o_kanban_record_subtitle"><field name="job_id"/></span>
+ </div>
+ </div>
+ <field name="category_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ <ul>
+ <li id="last_login"/>
+ <li t-if="record.work_location.raw_value"><field name="work_location"/></li>
+ </ul>
+ </div>
+ </div>
+ </t>
+ </templates>
+ <searchpanel>
+ <field name="company_id" groups="base.group_multi_company" icon="fa-building"/>
+ <field name="department_id" icon="fa-users"/>
+ <field name="category_ids" select="multi" icon="fa-tags"/>
+ </searchpanel>
+ </kanban>
+
+ </field>
+ </record>
+
+ <record id="hr_employee_view_activity" model="ir.ui.view">
+ <field name="name">hr.employee.activity</field>
+ <field name="model">hr.employee</field>
+ <field name="arch" type="xml">
+ <activity string="Employees">
+ <field name="id"/>
+ <templates>
+ <div t-name="activity-box">
+ <img t-att-src="activity_image('hr.employee', 'image_small', record.id.raw_value)" role="img" t-att-title="record.id.value" t-att-alt="record.id.value"/>
+ <div>
+ <field name="name" display="full"/>
+ <field name="job_id" muted="1" display="full"/>
+ </div>
+ </div>
+ </templates>
+ </activity>
+ </field>
+ </record>
+
+ <record id="open_view_employee_list_my" model="ir.actions.act_window">
+ <field name="name">Employees</field>
+ <field name="res_model">hr.employee</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">kanban,tree,form,activity</field>
+ <field name="domain">[]</field>
+ <field name="context">{}</field>
+ <field name="view_id" eval="False"/>
+ <field name="search_view_id" ref="view_employee_filter"/>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Add a new employee
+ </p><p>
+ With just a quick glance on the Odoo employee screen, you
+ can easily find all the information you need for each person;
+ contact data, job position, availability, etc.
+ </p>
+ </field>
+ </record>
+
+ <record id="open_view_employee_tree" model="ir.actions.act_window">
+ <field name="name">Employees Structure</field>
+ <field name="res_model">hr.employee</field>
+ <field name="view_type">tree</field>
+ <field name="view_mode">tree,form</field>
+ <field name="view_id" ref="view_employee_tree"/>
+ <field name="domain">[('parent_id','=',False)]</field>
+ <field name="search_view_id" ref="view_employee_filter"/>
+ </record>
+
+ <record id="open_view_employee_list" model="ir.actions.act_window">
+ <field name="name">Employees</field>
+ <field name="res_model">hr.employee</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">form,tree</field>
+ <field name="view_id" eval="False"/>
+ <field name="search_view_id" ref="view_employee_filter"/>
+ </record>
+
+ <!-- Employee tree by manager -->
+ <record id="view_partner_tree2" model="ir.ui.view">
+ <field name="name">hr.employee.tree</field>
+ <field name="model">hr.employee</field>
+ <field name="priority" eval="20"/>
+ <field name="arch" type="xml">
+ <tree string="Employees">
+ <field name="name"/>
+ <field name="work_phone"/>
+ <field name="work_email"/>
+ <field name="company_id" groups="base.group_multi_company"/>
+ <field name="department_id"/>
+ <field name="job_id"/>
+ <field name="coach_id" invisible="1"/>
+ <field name="parent_id" invisible="1"/>
+ </tree>
+ </field>
+ </record>
+
+ <!-- TODO remove me in master -->
+ <record id="hr_employee_action_subordinate_hierachy" model="ir.actions.act_window">
+ <field name="name">Subordinate Hierarchy</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">hr.employee</field>
+ <field name="domain">['|', ('id','in',active_ids), ('parent_id', 'in', active_ids)]</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="view_id" ref="view_partner_tree2"/>
+ <field name="binding_model_id" ref="hr.model_hr_employee"/>
+ </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>
+ <field name="view_type">form</field>
+ <field name="view_mode">kanban,form,tree</field>
+ <field name="search_view_id" ref="view_employee_filter"/>
+ <field name="context">{
+ "search_default_department_id": [active_id],
+ "default_department_id": active_id}
+ </field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Add a new employee
+ </p><p>
+ With just a quick glance on the Odoo employee screen, you
+ can easily find all the information you need for each person;
+ contact data, job position, availability, etc.
+ </p>
+ </field>
+ </record>
+
+ </data>
+</odoo>
diff --git a/addons/hr/views/hr_job_views.xml b/addons/hr/views/hr_job_views.xml
new file mode 100644
index 00000000000..9d054d77834
--- /dev/null
+++ b/addons/hr/views/hr_job_views.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <record id="view_hr_job_form" model="ir.ui.view">
+ <field name="name">hr.job.form</field>
+ <field name="model">hr.job</field>
+ <field name="arch" type="xml">
+ <form string="Job">
+ <header>
+ <button name="set_recruit" string="Start Recruitment" states="open" type="object" class="oe_highlight" groups="base.group_user"/>
+ <button name="set_open" string="Stop Recruitment" states="recruit" type="object" groups="base.group_user"/>
+ <field name="state" widget="statusbar" statusbar_visible="recruit,open"/>
+ </header>
+ <sheet>
+ <div class="oe_button_box" name="button_box"/>
+ <div class="oe_title">
+ <label for="name" class="oe_edit_only"/>
+ <h1><field name="name" placeholder="e.g. Sales Manager"/></h1>
+ </div>
+ <group>
+ <group name="recruitment">
+ <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
+ <field name="department_id"/>
+ <field name="no_of_recruitment"/>
+ </group>
+ </group>
+ <div attrs="{'invisible': [('state', '!=', 'recruit')]}">
+ <label for="description"/>
+ <field name="description"/>
+ </div>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_hr_job_tree" model="ir.ui.view">
+ <field name="name">hr.job.tree</field>
+ <field name="model">hr.job</field>
+ <field name="arch" type="xml">
+ <tree string="Job" decoration-bf="message_needaction==True">
+ <field name="name"/>
+ <field name="department_id"/>
+ <field name="no_of_employee"/>
+ <field name="no_of_recruitment"/>
+ <field name="expected_employees"/>
+ <field name="no_of_hired_employee"/>
+ <field name="state"/>
+ <field name="message_needaction" invisible="1"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="hr_job_view_kanban" model="ir.ui.view">
+ <field name="name">hr.job.kanban</field>
+ <field name="model">hr.job</field>
+ <field name="arch" type="xml">
+ <kanban class="o_kanban_mobile">
+ <templates>
+ <t t-name="kanban-box">
+ <div class="oe_kanban_global_click">
+ <div>
+ <strong><field name="name"/></strong>
+ </div>
+ <div>
+ <span><field name="department_id"/>&amp;nbsp;</span>
+ </div>
+ <div>
+ <span>Vacancies : <field name="expected_employees"/></span>
+ <span t-att-class="record.state.raw_value == 'recruit' and 'float-right badge badge-success' or 'float-right badge badge-danger'">
+ <field name="state"/>
+ </span>
+ </div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
+ <record id="view_job_filter" model="ir.ui.view">
+ <field name="name">hr.job.search</field>
+ <field name="model">hr.job</field>
+ <field name="arch" type="xml">
+ <search string="Jobs">
+ <field name="name" string="Job"/>
+ <filter domain="[('state','=','open')]" name="in_position" string="In Position"/>
+ <filter domain="[('state','=','recruit')]" string="In Recruitment" name="in_recruitment"/>
+ <field name="department_id" operator="child_of"/>
+ <separator/>
+ <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
+ <group expand="0" string="Group By">
+ <filter string="Department" name="department" domain="[]" context="{'group_by':'department_id'}"/>
+ <filter string="Status" name="status" domain="[]" context="{'group_by':'state'}"/>
+ <filter string="Company" name="company" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record id="action_hr_job" model="ir.actions.act_window">
+ <field name="name">Job Positions</field>
+ <field name="res_model">hr.job</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="view_job_filter"/>
+ <field name="context">{"search_default_Current":1}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Ready to recruit more efficiently?
+ </p><p>
+ Let's create a job position.
+ </p>
+ </field>
+ </record>
+
+ </data>
+</odoo>
diff --git a/addons/hr/views/hr_plan_views.xml b/addons/hr/views/hr_plan_views.xml
new file mode 100644
index 00000000000..dd8d30e7584
--- /dev/null
+++ b/addons/hr/views/hr_plan_views.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <record id="hr_plan_view_search" model="ir.ui.view">
+ <field name="name">hr.plan.view.search</field>
+ <field name="model">hr.plan</field>
+ <field name="arch" type="xml">
+ <search string="Plan">
+ <field name="name"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="hr_plan_view_tree" model="ir.ui.view">
+ <field name="name">hr.plan.view.tree</field>
+ <field name="model">hr.plan</field>
+ <field name="arch" type="xml">
+ <tree string="Planning">
+ <field name="name"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="hr_plan_view_form" model="ir.ui.view">
+ <field name="name">hr.plan.view.form</field>
+ <field name="model">hr.plan</field>
+ <field name="arch" type="xml">
+ <form string="Planning">
+ <sheet>
+ <div class="oe_button_box" name="button_box">
+ <button name="toggle_active" type="object"
+ class="oe_stat_button" icon="fa-archive">
+ <field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
+ </button>
+ </div>
+ <div class="oe_title">
+ <label for="name" class="oe_edit_only"/>
+ <h1>
+ <field name="name"/>
+ </h1>
+ </div>
+ <group string="Activities">
+ <field name="plan_activity_type_ids" nolabel="1">
+ <tree editable="bottom">
+ <field name="activity_type_id"/>
+ <field name="summary"/>
+ <field name="responsible"/>
+ <field name="responsible_id" attrs="{'readonly': [('responsible', '!=', 'other')]}"/>
+ </tree>
+ </field>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="hr_plan_activity_type_view_tree" model="ir.ui.view">
+ <field name="name">hr.plan.activity.type.view.tree</field>
+ <field name="model">hr.plan.activity.type</field>
+ <field name="arch" type="xml">
+ <tree string="Activities">
+ <field name="activity_type_id"/>
+ <field name="summary"/>
+ <field name="responsible"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="hr_plan_activity_type_view_form" model="ir.ui.view">
+ <field name="name">hr.plan.activity.type.view.form</field>
+ <field name="model">hr.plan.activity.type</field>
+ <field name="arch" type="xml">
+ <form string="Activity">
+ <sheet>
+ <group>
+ <field name="activity_type_id"/>
+ <field name="summary"/>
+ <field name="responsible"/>
+ <field name="responsible_id" attrs="{'invisible': [('responsible', '!=', 'other')]}"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="hr_plan_action" model="ir.actions.act_window">
+ <field name="name">Planning</field>
+ <field name="res_model">hr.plan</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="hr_plan_view_search"/>
+ </record>
+
+ <record id="hr_plan_activity_type_action" model="ir.actions.act_window">
+ <field name="name">Planning Types</field>
+ <field name="res_model">hr.plan.activity.type</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ </data>
+</odoo>
diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml
index 81609ef89a5..3a5c490ec70 100644
--- a/addons/hr/views/hr_views.xml
+++ b/addons/hr/views/hr_views.xml
@@ -2,7 +2,6 @@
<odoo>
<data>
- <!-- Top menu item -->
<menuitem
id="menu_hr_root"
name="Employees"
@@ -17,830 +16,89 @@
sequence="0"/>
<menuitem
- id="hr_menu_hr_reports"
- name="Reporting"
+ id="menu_hr_employee_payroll"
+ name="Payroll"
parent="menu_hr_root"
- sequence="95"/>
+ groups="group_hr_user"
+ sequence="3"/>
- <menuitem
- id="menu_hr_reporting_timesheet"
- name="Reporting"
- parent="menu_hr_root"
- groups="group_hr_manager,group_hr_user"
- sequence="99"/>
+ <menuitem
+ id="menu_hr_employee_user"
+ name="Employees"
+ action="open_view_employee_list_my"
+ parent="menu_hr_employee_payroll"
+ sequence="1"/>
<menuitem
- id="menu_human_resources_configuration"
- name="Configuration"
+ id="menu_hr_employee"
+ name="Company Address"
+ action="hr_employee_public_action"
parent="menu_hr_root"
- groups="group_hr_manager"
- sequence="100"/>
-
- <record id="hr_plan_view_search" model="ir.ui.view">
- <field name="name">hr.plan.view.search</field>
- <field name="model">hr.plan</field>
- <field name="arch" type="xml">
- <search string="Plan">
- <field name="name"/>
- </search>
- </field>
- </record>
-
- <record id="hr_plan_view_tree" model="ir.ui.view">
- <field name="name">hr.plan.view.tree</field>
- <field name="model">hr.plan</field>
- <field name="arch" type="xml">
- <tree string="Planning">
- <field name="name"/>
- </tree>
- </field>
- </record>
-
- <record id="hr_plan_view_form" model="ir.ui.view">
- <field name="name">hr.plan.view.form</field>
- <field name="model">hr.plan</field>
- <field name="arch" type="xml">
- <form string="Planning">
- <sheet>
- <div class="oe_button_box" name="button_box">
- <button name="toggle_active" type="object"
- class="oe_stat_button" icon="fa-archive">
- <field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
- </button>
- </div>
- <div class="oe_title">
- <label for="name" class="oe_edit_only"/>
- <h1>
- <field name="name"/>
- </h1>
- </div>
- <group string="Activities">
- <field name="plan_activity_type_ids" nolabel="1">
- <tree editable="bottom">
- <field name="activity_type_id"/>
- <field name="summary"/>
- <field name="responsible"/>
- <field name="responsible_id" attrs="{'readonly': [('responsible', '!=', 'other')]}"/>
- </tree>
- </field>
- </group>
- </sheet>
- </form>
- </field>
- </record>
-
- <record id="hr_plan_activity_type_view_tree" model="ir.ui.view">
- <field name="name">hr.plan.activity.type.view.tree</field>
- <field name="model">hr.plan.activity.type</field>
- <field name="arch" type="xml">
- <tree string="Activities">
- <field name="activity_type_id"/>
- <field name="summary"/>
- <field name="responsible"/>
- </tree>
- </field>
- </record>
-
- <record id="hr_plan_activity_type_view_form" model="ir.ui.view">
- <field name="name">hr.plan.activity.type.view.form</field>
- <field name="model">hr.plan.activity.type</field>
- <field name="arch" type="xml">
- <form string="Activity">
- <sheet>
- <group>
- <field name="activity_type_id"/>
- <field name="summary"/>
- <field name="responsible"/>
- <field name="responsible_id" attrs="{'invisible': [('responsible', '!=', 'other')]}"/>
- </group>
- </sheet>
- </form>
- </field>
- </record>
-
- <record id="hr_plan_action" model="ir.actions.act_window">
- <field name="name">Planning</field>
- <field name="res_model">hr.plan</field>
- <field name="view_type">form</field>
- <field name="view_mode">tree,form</field>
- <field name="search_view_id" ref="hr_plan_view_search"/>
- </record>
-
- <record id="hr_plan_activity_type_action" model="ir.actions.act_window">
- <field name="name">Planning Types</field>
- <field name="res_model">hr.plan.activity.type</field>
- <field name="view_type">form</field>
- <field name="view_mode">tree,form</field>
- </record>
-
- <menuitem
- id="menu_config_plan"
- name="Activity Planning"
- parent="menu_human_resources_configuration"
- groups="group_hr_manager"
- sequence="100"/>
-
- <menuitem
- id="menu_config_plan_types"
- name="Planning Types"
- action="hr_plan_activity_type_action"
- parent="menu_config_plan"
- groups="base.group_no_one"
- sequence="99"/>
-
- <menuitem
- id="menu_config_plan_plan"
- name="Plans"
- action="hr_plan_action"
- parent="menu_config_plan"
- groups="group_hr_manager"
- sequence="100"/>
-
- <!-- Employee -->
- <record id="view_employee_form" model="ir.ui.view">
- <field name="name">hr.employee.form</field>
- <field name="model">hr.employee</field>
- <field name="arch" type="xml">
- <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">
- <!-- Used by other modules-->
- </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>
- <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>
- <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>
- <group name="departure" 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="Contact Information">
- <field name="address_home_id"
- context="{
- 'show_address': 1,
- '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" groups="hr.group_hr_user"
- 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" 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="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="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="Education">
- <field name="certificate"/>
- <field name="study_field"/>
- <field name="study_school"/>
- </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="hr_settings" string="HR Settings" groups="hr.group_hr_user">
- <group>
- <group string='Status' name="active_group">
- <field name="user_id" string="Related User"/>
- </group>
- <group string="Login Information" name="identification_group">
- <field name="pin"/>
- <field name="barcode"/>
- <button string="Generate Badge ID" class="btn btn-primary" type="object" name="generate_random_barcode" attrs="{'invisible': [('barcode', '!=', False)]}"/>
- </group>
- </group>
- </page>
- </notebook>
-
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
- <field name="activity_ids" widget="mail_activity"/>
- <field name="message_ids" widget="mail_thread"/>
- </div>
- </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>
- <field name="arch" type="xml">
- <tree string="Employees" decoration-bf="message_needaction==True">
- <field name="name"/>
- <field name="work_phone"/>
- <field name="work_email"/>
- <field name="company_id" groups="base.group_multi_company"/>
- <field name="department_id"/>
- <field name="job_id"/>
- <field name="parent_id"/>
- <field name="coach_id" invisible="1"/>
- <field name="message_needaction" invisible="1"/>
- </tree>
- </field>
- </record>
-
- <record id="view_employee_filter" model="ir.ui.view">
- <field name="name">hr.employee.search</field>
- <field name="model">hr.employee</field>
- <field name="arch" type="xml">
- <search string="Employees">
- <field name="name" string="Employees" filter_domain="['|',('work_email','ilike',self),('name','ilike',self)]"/>
- <field name="department_id" operator="child_of"/>
- <field name="category_ids" groups="hr.group_hr_user"/>
- <field name="job_id" />
- <field name="company_id" groups="base.group_multi_company"/>
- <filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
- <separator/>
- <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
- <separator/>
- <filter string="Late Activities" name="activities_overdue"
- domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"/>
- <filter string="Today Activities" name="activities_today"
- domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
- <filter string="Future Activities" name="activities_upcoming_all"
- domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
- <group expand="0" string="Group By">
- <filter name="group_manager" string="Manager" domain="[]" context="{'group_by':'parent_id'}"/>
- <filter name="group_coach" string="Coach" domain="[]" context="{'group_by':'coach_id'}"/>
- <filter name="group_department" string="Department" domain="[]" context="{'group_by':'department_id'}"/>
- <filter name="group_job" string="Job" domain="[]" context="{'group_by':'job_id'}"/>
- <filter name="group_company" string="Company" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
- </group>
- </search>
- </field>
- </record>
-
- <record id="hr_kanban_view_employees" model="ir.ui.view">
- <field name="name">hr.employee.kanban</field>
- <field name="model">hr.employee</field>
- <field name="priority">10</field>
- <field name="arch" type="xml">
- <kanban class="o_hr_employee_kanban">
- <field name="id"/>
- <field name="message_follower_ids"/>
- <field name="message_ids"/>
- <field name="message_needaction_counter"/>
- <field name="activity_state"/>
- <progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}'/>
- <templates>
- <t t-name="kanban-box">
- <div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
- <!-- Medium employee's picture -->
- <div class="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image: url(#{kanban_image('hr.employee', 'image_medium', record.id.raw_value)})" role="img"/>
- <!-- Employee's picture optimized for mobile -->
- <div class="o_kanban_image d-md-none rounded-circle" t-attf-style="background-image: url(#{kanban_image('hr.employee', 'image_small', record.id.raw_value)})" role="img"/>
-
- <div class="oe_kanban_details">
- <div class="o_kanban_record_top">
- <div class="o_kanban_record_headings">
- <strong class="o_kanban_record_title">
- <field name="name"/>
- </strong>
- <span t-if="record.job_id.raw_value" class="o_kanban_record_subtitle"><field name="job_id"/></span>
- </div>
- </div>
- <field name="category_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
- <ul>
- <li id="last_login"/>
- <li t-if="record.work_location.raw_value"><field name="work_location"/></li>
- </ul>
- </div>
- </div>
- </t>
- </templates>
- <searchpanel>
- <field name="company_id" groups="base.group_multi_company" icon="fa-building"/>
- <field name="department_id" icon="fa-users"/>
- <field name="category_ids" select="multi" icon="fa-tags"/>
- </searchpanel>
- </kanban>
-
- </field>
- </record>
-
- <record id="hr_employee_view_activity" model="ir.ui.view">
- <field name="name">hr.employee.activity</field>
- <field name="model">hr.employee</field>
- <field name="arch" type="xml">
- <activity string="Employees">
- <field name="id"/>
- <templates>
- <div t-name="activity-box">
- <img t-att-src="activity_image('hr.employee', 'image_small', record.id.raw_value)" role="img" t-att-title="record.id.value" t-att-alt="record.id.value"/>
- <div>
- <field name="name" display="full"/>
- <field name="job_id" muted="1" display="full"/>
- </div>
- </div>
- </templates>
- </activity>
- </field>
- </record>
-
- <record id="open_view_employee_tree" model="ir.actions.act_window">
- <field name="name">Employees Structure</field>
- <field name="res_model">hr.employee</field>
- <field name="view_type">tree</field>
- <field name="view_mode">tree,form</field>
- <field name="view_id" ref="view_employee_tree"/>
- <field name="domain">[('parent_id','=',False)]</field>
- <field name="search_view_id" ref="view_employee_filter"/>
- </record>
-
- <record id="open_view_employee_list" model="ir.actions.act_window">
- <field name="name">Employees</field>
- <field name="res_model">hr.employee</field>
- <field name="view_type">form</field>
- <field name="view_mode">form,tree</field>
- <field name="view_id" eval="False"/>
- <field name="search_view_id" ref="view_employee_filter"/>
- </record>
-
- <record id="open_view_employee_list_my" model="ir.actions.act_window">
- <field name="name">Employees</field>
- <field name="res_model">hr.employee</field>
- <field name="view_type">form</field>
- <field name="view_mode">kanban,tree,form,activity</field>
- <field name="domain">[]</field>
- <field name="context">{}</field>
- <field name="view_id" eval="False"/>
- <field name="search_view_id" ref="view_employee_filter"/>
- <field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
- Add a new employee
- </p><p>
- With just a quick glance on the Odoo employee screen, you
- can easily find all the information you need for each person;
- contact data, job position, availability, etc.
- </p>
- </field>
- </record>
+ sequence="4"/>
<menuitem
- id="menu_open_view_employee_list_my"
- action="open_view_employee_list_my"
+ id="hr_menu_hr_reports"
+ name="Reporting"
parent="menu_hr_root"
- sequence="3"/>
-
- <!-- Employee tree by manager -->
- <record id="view_partner_tree2" model="ir.ui.view">
- <field name="name">hr.employee.tree</field>
- <field name="model">hr.employee</field>
- <field name="field_parent">child_ids</field>
- <field name="priority" eval="20"/>
- <field name="arch" type="xml">
- <tree string="Employees">
- <field name="name"/>
- <field name="work_phone"/>
- <field name="work_email"/>
- <field name="company_id" groups="base.group_multi_company"/>
- <field name="department_id"/>
- <field name="job_id"/>
- <field name="coach_id" invisible="1"/>
- <field name="parent_id" invisible="1"/>
- </tree>
- </field>
- </record>
-
- <!-- TODO remove me in master -->
- <record id="hr_employee_action_subordinate_hierachy" model="ir.actions.act_window">
- <field name="name">Subordinate Hierarchy</field>
- <field name="type">ir.actions.act_window</field>
- <field name="res_model">hr.employee</field>
- <field name="domain">['|', ('id','in',active_ids), ('parent_id', 'in', active_ids)]</field>
- <field name="view_type">form</field>
- <field name="view_mode">tree,form</field>
- <field name="view_id" ref="view_partner_tree2"/>
- <field name="binding_model_id" ref="hr.model_hr_employee"/>
- </record>
-
- <!-- Employee Category -->
- <record id="view_employee_category_form" model="ir.ui.view">
- <field name="name">hr.employee.category.form</field>
- <field name="model">hr.employee.category</field>
- <field name="arch" type="xml">
- <form string="Employee Tags">
- <sheet>
- <group>
- <field name="name"/>
- </group>
- </sheet>
- </form>
- </field>
- </record>
-
- <record id="view_employee_category_list" model="ir.ui.view">
- <field name="name">hr.employee.category.list</field>
- <field name="model">hr.employee.category</field>
- <field eval="8" name="priority"/>
- <field name="arch" type="xml">
- <tree string="Employees Tags">
- <field name="name"/>
- </tree>
- </field>
- </record>
-
- <record id="open_view_categ_form" model="ir.actions.act_window">
- <field name="name">Employee Tags</field>
- <field name="res_model">hr.employee.category</field>
- <field name="view_type">form</field>
- <field name="view_mode">tree,form</field>
- </record>
-
- <menuitem
- id="menu_human_resources_configuration_employee"
- name="Employee"
- parent="menu_human_resources_configuration"
- groups="base.group_no_one"
- sequence="1"/>
+ sequence="95"/>
<menuitem
- id="menu_view_employee_category_form"
- name="Tags"
- action="open_view_categ_form"
- parent="menu_human_resources_configuration_employee"
- groups="base.group_no_one"
- sequence="1"/>
-
- <!-- hr.job -->
- <record id="view_hr_job_form" model="ir.ui.view">
- <field name="name">hr.job.form</field>
- <field name="model">hr.job</field>
- <field name="arch" type="xml">
- <form string="Job">
- <header>
- <button name="set_recruit" string="Start Recruitment" states="open" type="object" class="oe_highlight" groups="base.group_user"/>
- <button name="set_open" string="Stop Recruitment" states="recruit" type="object" groups="base.group_user"/>
- <field name="state" widget="statusbar" statusbar_visible="recruit,open"/>
- </header>
- <sheet>
- <div class="oe_button_box" name="button_box"/>
- <div class="oe_title">
- <label for="name" class="oe_edit_only"/>
- <h1><field name="name" placeholder="e.g. Sales Manager"/></h1>
- </div>
- <group>
- <group name="recruitment">
- <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
- <field name="department_id"/>
- <field name="no_of_recruitment"/>
- </group>
- </group>
- <div attrs="{'invisible': [('state', '!=', 'recruit')]}">
- <label for="description"/>
- <field name="description"/>
- </div>
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids" widget="mail_followers"/>
- </div>
- </form>
- </field>
- </record>
-
- <record id="view_hr_job_tree" model="ir.ui.view">
- <field name="name">hr.job.tree</field>
- <field name="model">hr.job</field>
- <field name="arch" type="xml">
- <tree string="Job" decoration-bf="message_needaction==True">
- <field name="name"/>
- <field name="department_id"/>
- <field name="no_of_employee"/>
- <field name="no_of_recruitment"/>
- <field name="expected_employees"/>
- <field name="no_of_hired_employee"/>
- <field name="state"/>
- <field name="message_needaction" invisible="1"/>
- </tree>
- </field>
- </record>
-
- <record id="hr_job_view_kanban" model="ir.ui.view">
- <field name="name">hr.job.kanban</field>
- <field name="model">hr.job</field>
- <field name="arch" type="xml">
- <kanban class="o_kanban_mobile">
- <templates>
- <t t-name="kanban-box">
- <div class="oe_kanban_global_click">
- <div>
- <strong><field name="name"/></strong>
- </div>
- <div>
- <span><field name="department_id"/>&amp;nbsp;</span>
- </div>
- <div>
- <span>Vacancies : <field name="expected_employees"/></span>
- <span t-att-class="record.state.raw_value == 'recruit' and 'float-right badge badge-success' or 'float-right badge badge-danger'">
- <field name="state"/>
- </span>
- </div>
- </div>
- </t>
- </templates>
- </kanban>
- </field>
- </record>
-
- <record id="view_job_filter" model="ir.ui.view">
- <field name="name">hr.job.search</field>
- <field name="model">hr.job</field>
- <field name="arch" type="xml">
- <search string="Jobs">
- <field name="name" string="Job"/>
- <filter domain="[('state','=','open')]" name="in_position" string="In Position"/>
- <filter domain="[('state','=','recruit')]" string="In Recruitment" name="in_recruitment"/>
- <field name="department_id" operator="child_of"/>
- <separator/>
- <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
- <group expand="0" string="Group By">
- <filter string="Department" name="department" domain="[]" context="{'group_by':'department_id'}"/>
- <filter string="Status" name="status" domain="[]" context="{'group_by':'state'}"/>
- <filter string="Company" name="company" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
- </group>
- </search>
- </field>
- </record>
-
- <record id="action_hr_job" model="ir.actions.act_window">
- <field name="name">Job Positions</field>
- <field name="res_model">hr.job</field>
- <field name="view_type">form</field>
- <field name="view_mode">tree,form</field>
- <field name="search_view_id" ref="view_job_filter"/>
- <field name="context">{"search_default_Current":1}</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
- Ready to recruit more efficiently?
- </p><p>
- Let's create a job position.
- </p>
- </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>
- <field name="view_type">form</field>
- <field name="view_mode">kanban,form,tree</field>
- <field name="search_view_id" ref="view_employee_filter"/>
- <field name="context">{
- "search_default_department_id": [active_id],
- "default_department_id": active_id}
- </field>
- <field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
- Add a new employee
- </p><p>
- With just a quick glance on the Odoo employee screen, you
- can easily find all the information you need for each person;
- contact data, job position, availability, etc.
- </p>
- </field>
- </record>
-
- <!-- hr.department -->
- <record id="view_department_form" model="ir.ui.view">
- <field name="name">hr.department.form</field>
- <field name="model">hr.department</field>
- <field name="arch" type="xml">
- <form string="department">
- <sheet>
- <div class="oe_button_box" name="button_box">
- <button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
- <field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
- </button>
- </div>
- <group col="4">
- <field name="name"/>
- <field name="manager_id"/>
- <field name="parent_id"/>
- <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
- </group>
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
- <field name="message_ids" widget="mail_thread"/>
- </div>
- </form>
- </field>
- </record>
-
- <record id="view_department_tree" model="ir.ui.view">
- <field name="name">hr.department.tree</field>
- <field name="model">hr.department</field>
- <field name="field_parent">child_ids</field>
- <field name="arch" type="xml">
- <tree string="Companies">
- <field name="display_name"/>
- <field name="company_id" groups="base.group_multi_company"/>
- <field name="manager_id"/>
- <field name="parent_id"/>
- </tree>
- </field>
- </record>
-
- <record id="view_department_filter" model="ir.ui.view">
- <field name="name">hr.department.search</field>
- <field name="model">hr.department</field>
- <field name="arch" type="xml">
- <search string="Departments">
- <field name="name" string="Department"/>
- <field name="manager_id" />
- <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
- <separator/>
- <filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
- </search>
- </field>
- </record>
-
- <record id="hr_department_view_kanban" model="ir.ui.view" >
- <field name="name">hr.department.kanban</field>
- <field name="model">hr.department</field>
- <field name="arch" type="xml">
- <kanban class="oe_background_grey o_kanban_dashboard o_hr_kanban">
- <field name="name"/>
- <field name="company_id"/>
- <field name="manager_id"/>
- <field name="color"/>
- <templates>
- <t t-name="kanban-box">
- <div t-attf-class="#{kanban_color(record.color.raw_value)}">
- <div t-attf-class="o_kanban_card_header">
- <div class="o_kanban_card_header_title">
- <div class="o_primary"><field name="name"/></div>
- <div class="o_secondary"><field name="company_id" groups="base.group_multi_company"/></div>
- </div>
- <div class="o_kanban_manage_button_section">
- <a class="o_kanban_manage_toggle_button" href="#"><i class="fa fa-ellipsis-v" role="img" aria-label="Manage" title="Manage"/></a>
- </div>
- </div>
- <div class="container o_kanban_card_content">
- <div class="row">
- <div class="col-6 o_kanban_primary_left">
- <button class="btn btn-primary" name="%(act_employee_from_department)d" type="action">Employees</button>
- </div>
- <div class="col-6 o_kanban_primary_right">
- </div>
- </div>
- </div><div class="container o_kanban_card_manage_pane dropdown-menu" role="menu">
- <div class="row">
- <div role="menuitem" class="col-4 o_kanban_card_manage_section o_kanban_manage_to_do">
- <div class="o_kanban_card_manage_title">
- <span>To Do</span>
- </div>
- </div>
- <div role="menuitem" class="col-4 o_kanban_card_manage_section o_kanban_manage_to_approve">
- <div class="o_kanban_card_manage_title">
- <span>To Approve</span>
- </div>
- </div>
- <div role="menuitem" class="col-4 o_kanban_card_manage_section o_kanban_manage_reports">
- <div class="o_kanban_card_manage_title">
- <span>Reporting</span>
- </div>
- </div>
- </div>
-
- <div t-if="widget.editable" class="o_kanban_card_manage_settings row">
- <div role="menuitem" aria-haspopup="true" class="col-8">
- <ul class="oe_kanban_colorpicker" data-field="color" role="menu"/>
- </div>
- <div class="col-4 text-right">
- <a role="menuitem" type="edit">Settings</a>
- </div>
- </div>
- </div>
-
- </div>
- </t>
- </templates>
- </kanban>
- </field>
- </record>
-
- <record id="open_module_tree_department" model="ir.actions.act_window">
- <field name="name">Departments</field>
- <field name="res_model">hr.department</field>
- <field name="view_type">form</field>
- <field name="view_mode">kanban,tree,form</field>
- <field name="search_view_id" ref="view_department_filter"/>
- <field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
- Create a new department
- </p><p>
- Odoo's department structure is used to manage all documents
- related to employees by departments: expenses, timesheets,
- leaves, recruitments, etc.
- </p>
- </field>
- </record>
-
- <record id="view_users_form_simple_modif_resource" model="ir.ui.view">
- <field name="name">res.users.preferences.form.resource</field>
- <field name="model">res.users</field>
- <field name="inherit_id" ref="base.view_users_form_simple_modif" />
- <field name="arch" type="xml">
- <field name="tz" position="attributes">
- <attribute name="required">1</attribute>
- </field>
- </field>
- </record>
+ id="menu_hr_reporting_timesheet"
+ name="Reporting"
+ parent="menu_hr_root"
+ groups="group_hr_manager,group_hr_user"
+ sequence="99"/>
<menuitem
- action="open_module_tree_department"
- id="menu_hr_department_tree"
+ id="menu_human_resources_configuration"
+ name="Configuration"
parent="menu_hr_root"
- sequence="90"
- groups="group_hr_user"/>
+ groups="group_hr_manager"
+ sequence="100"/>
+ <menuitem
+ id="menu_human_resources_configuration_employee"
+ name="Employee"
+ parent="menu_human_resources_configuration"
+ groups="base.group_no_one"
+ sequence="1"/>
+
+ <menuitem
+ id="menu_view_employee_category_form"
+ name="Tags"
+ action="open_view_categ_form"
+ parent="menu_human_resources_configuration_employee"
+ groups="base.group_no_one"
+ sequence="1"/>
+
+ <menuitem
+ id="menu_hr_department_tree"
+ action="open_module_tree_department"
+ parent="menu_human_resources_configuration"
+ sequence="2"
+ groups="group_hr_user"/>
+
+ <menuitem
+ id="menu_config_plan"
+ name="Activity Planning"
+ parent="menu_human_resources_configuration"
+ groups="group_hr_manager"
+ sequence="100"/>
+
+ <menuitem
+ id="menu_config_plan_types"
+ name="Planning Types"
+ action="hr_plan_activity_type_action"
+ parent="menu_config_plan"
+ groups="base.group_no_one"
+ sequence="99"/>
+
+ <menuitem
+ id="menu_config_plan_plan"
+ name="Plans"
+ action="hr_plan_action"
+ parent="menu_config_plan"
+ groups="group_hr_manager"
+ sequence="100"/>
</data>
</odoo>
diff --git a/addons/hr/views/res_users.xml b/addons/hr/views/res_users.xml
index 7a93c6745a7..675d495869b 100644
--- a/addons/hr/views/res_users.xml
+++ b/addons/hr/views/res_users.xml
@@ -18,6 +18,17 @@
</field>
</record>
+ <record id="view_users_form_simple_modif_resource" model="ir.ui.view">
+ <field name="name">res.users.preferences.form.resource</field>
+ <field name="model">res.users</field>
+ <field name="inherit_id" ref="base.view_users_form_simple_modif" />
+ <field name="arch" type="xml">
+ <field name="tz" position="attributes">
+ <attribute name="required">1</attribute>
+ </field>
+ </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>
diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py
index ac2f23b1eb8..48a1561e4a5 100644
--- a/addons/hr_attendance/models/hr_employee.py
+++ b/addons/hr_attendance/models/hr_employee.py
@@ -1,13 +1,14 @@
# -*- 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
+from odoo import models, fields, api, exceptions, _, SUPERUSER_ID
-class HrEmployee(models.Model):
- _inherit = "hr.employee"
- _description = "Employee"
+
+class HrEmployeeBase(models.AbstractModel):
+ _inherit = "hr.employee.base"
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)
@@ -44,35 +45,37 @@ class HrEmployee(models.Model):
""" Receive a barcode scanned from the Kiosk Mode and change the attendances of corresponding employee.
Returns either an action or a warning.
"""
- employee = self.search([('barcode', '=', barcode)], limit=1)
- return employee and employee.attendance_action('hr_attendance.hr_attendance_action_kiosk_mode') or \
- {'warning': _('No employee corresponding to barcode %(barcode)s') % {'barcode': barcode}}
+ employee = self.sudo().search([('barcode', '=', barcode)], limit=1)
+ if employee:
+ return employee._attendance_action('hr_attendance.hr_attendance_action_kiosk_mode')
+ return {'warning': _('No employee corresponding to barcode %(barcode)s') % {'barcode': barcode}}
@api.multi
def attendance_manual(self, next_action, entered_pin=None):
self.ensure_one()
- if not (entered_pin is None) or self.env['res.users'].browse(SUPERUSER_ID).has_group('hr_attendance.group_hr_attendance_use_pin') and (self.user_id and self.user_id.id != self._uid or not self.user_id):
+ if not (entered_pin is None) or self.env.user.has_group('hr_attendance.group_hr_attendance_use_pin') and (self.user_id and self.user_id == self.env.user):
if entered_pin != self.pin:
return {'warning': _('Wrong PIN')}
- return self.attendance_action(next_action)
+ return self._attendance_action(next_action)
@api.multi
- def attendance_action(self, next_action):
+ def _attendance_action(self, next_action):
""" Changes the attendance of the employee.
Returns an action to the check in/out message,
next_action defines which menu the check in/out message should return to. ("My Attendances" or "Kiosk Mode")
"""
self.ensure_one()
+ employee = self.sudo()
action_message = self.env.ref('hr_attendance.hr_attendance_action_greeting_message').read()[0]
- action_message['previous_attendance_change_date'] = self.last_attendance_id and (self.last_attendance_id.check_out or self.last_attendance_id.check_in) or False
- action_message['employee_name'] = self.name
- action_message['barcode'] = self.barcode
+ action_message['previous_attendance_change_date'] = employee.last_attendance_id and (employee.last_attendance_id.check_out or employee.last_attendance_id.check_in) or False
+ action_message['employee_name'] = employee.name
+ action_message['barcode'] = employee.barcode
action_message['next_action'] = next_action
- if self.user_id:
- modified_attendance = self.sudo(self.user_id.id).attendance_action_change()
+ if employee.user_id:
+ modified_attendance = employee.sudo(employee.user_id.id).attendance_action_change()
else:
- modified_attendance = self.sudo().attendance_action_change()
+ modified_attendance = employee.attendance_action_change()
action_message['attendance'] = modified_attendance.read()[0]
return {'action': action_message}
@@ -82,8 +85,7 @@ class HrEmployee(models.Model):
Check In: create a new attendance record
Check Out: modify check_out field of appropriate attendance record
"""
- if len(self) > 1:
- raise exceptions.UserError(_('Cannot perform check in or check out on multiple employees.'))
+ self.ensure_one()
action_date = fields.Datetime.now()
if self.attendance_state != 'checked_in':
@@ -92,11 +94,10 @@ class HrEmployee(models.Model):
'check_in': action_date,
}
return self.env['hr.attendance'].create(vals)
+ attendance = self.env['hr.attendance'].search([('employee_id', '=', self.id), ('check_out', '=', False)], limit=1)
+ if attendance:
+ attendance.check_out = action_date
else:
- attendance = self.env['hr.attendance'].search([('employee_id', '=', self.id), ('check_out', '=', False)], limit=1)
- if attendance:
- attendance.check_out = action_date
- else:
- raise exceptions.UserError(_('Cannot perform check out on %(empl_name)s, could not find corresponding check in. '
- 'Your attendances have probably been modified manually by human resources.') % {'empl_name': self.name, })
- return attendance
+ raise exceptions.UserError(_('Cannot perform check out on %(empl_name)s, could not find corresponding check in. '
+ 'Your attendances have probably been modified manually by human resources.') % {'empl_name': self.sudo().name, })
+ return attendance
diff --git a/addons/hr_attendance/models/res_users.py b/addons/hr_attendance/models/res_users.py
index cf0a329a028..8b2bc7c3a3e 100644
--- a/addons/hr_attendance/models/res_users.py
+++ b/addons/hr_attendance/models/res_users.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import models, fields, api, _
+from odoo import models, fields
class User(models.Model):
@@ -19,7 +19,6 @@ class User(models.Model):
'hours_last_month',
'attendance_state',
]
- init_res = super(User, self).__init__(pool, cr)
+ super(User, self).__init__(pool, cr)
# duplicate list to avoid modifying the original reference
type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + attendance_readable_fields
- return init_res
diff --git a/addons/hr_attendance/tests/test_hr_attendance_constraints.py b/addons/hr_attendance/tests/test_hr_attendance_constraints.py
index d02acad4bdb..8b00197cd60 100644
--- a/addons/hr_attendance/tests/test_hr_attendance_constraints.py
+++ b/addons/hr_attendance/tests/test_hr_attendance_constraints.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
-from odoo.tests.common import TransactionCase
import time
+from odoo.tests.common import TransactionCase
+
class TestHrAttendance(TransactionCase):
"""Tests for attendance date ranges validity"""
@@ -12,6 +13,10 @@ class TestHrAttendance(TransactionCase):
self.attendance = self.env['hr.attendance']
self.test_employee = self.browse_ref('hr.employee_qdp')
# demo data contains set up for self.test_employee
+ self.open_attendance = self.attendance.create({
+ 'employee_id': self.test_employee.id,
+ 'check_in': time.strftime('%Y-%m-10 10:00'),
+ })
def test_attendance_in_before_out(self):
# Make sure check_out is before check_in
@@ -24,10 +29,6 @@ class TestHrAttendance(TransactionCase):
def test_attendance_no_check_out(self):
# Make sure no second attandance without check_out can be created
- self.attendance.create({
- 'employee_id': self.test_employee.id,
- 'check_in': time.strftime('%Y-%m-10 10:00'),
- })
with self.assertRaises(Exception):
self.attendance.create({
'employee_id': self.test_employee.id,
@@ -68,10 +69,6 @@ class TestHrAttendance(TransactionCase):
})
def test_attendance_5(self):
- self.attendance.create({
- 'employee_id': self.test_employee.id,
- 'check_in': time.strftime('%Y-%m-10 10:00'),
- })
with self.assertRaises(Exception):
self.attendance.create({
'employee_id': self.test_employee.id,
@@ -86,11 +83,7 @@ class TestHrAttendance(TransactionCase):
'check_in': time.strftime('%Y-%m-10 11:00'),
'check_out': time.strftime('%Y-%m-10 12:00'),
})
- open_attendance = self.attendance.create({
- 'employee_id': self.test_employee.id,
- 'check_in': time.strftime('%Y-%m-10 10:00'),
- })
with self.assertRaises(Exception):
- open_attendance.write({
+ self.open_attendance.write({
'check_out': time.strftime('%Y-%m-10 11:30'),
})
diff --git a/addons/hr_attendance_presence/models/hr_employee.py b/addons/hr_attendance_presence/models/hr_employee.py
index 3f99535da87..c1dc644cac7 100644
--- a/addons/hr_attendance_presence/models/hr_employee.py
+++ b/addons/hr_attendance_presence/models/hr_employee.py
@@ -2,9 +2,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
+
+from odoo import models
_logger = logging.getLogger(__name__)
diff --git a/addons/hr_contract/models/__init__.py b/addons/hr_contract/models/__init__.py
index 7e9d4a5f5eb..6541bf23a31 100644
--- a/addons/hr_contract/models/__init__.py
+++ b/addons/hr_contract/models/__init__.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import hr_employee
from . import hr_contract
from . import res_users
diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py
index 1fbecc1b114..97df03d45a2 100644
--- a/addons/hr_contract/models/hr_contract.py
+++ b/addons/hr_contract/models/hr_contract.py
@@ -9,42 +9,6 @@ from odoo.exceptions import ValidationError
from odoo.osv import expression
-class Employee(models.Model):
- _inherit = "hr.employee"
-
- 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')
- contract_id = fields.Many2one('hr.contract', string='Current Contract', help='Current contract of the employee')
- contracts_count = fields.Integer(compute='_compute_contracts_count', string='Contract Count')
-
- def _compute_contracts_count(self):
- # read_group as sudo, since contract count is displayed on form view
- contract_data = self.env['hr.contract'].sudo().read_group([('employee_id', 'in', self.ids)], ['employee_id'], ['employee_id'])
- result = dict((data['employee_id'][0], data['employee_id_count']) for data in contract_data)
- for employee in self:
- employee.contracts_count = result.get(employee.id, 0)
-
- def _get_contracts(self, date_from, date_to, states=['open', 'pending']):
- """
- Returns the contracts of the employee between date_from and date_to
- """
- return self.env['hr.contract'].search([
- '&', '&', '&',
- ('employee_id', 'in', self.ids),
- ('state', 'in', states),
- ('date_start', '<=', date_to),
- '|', ('date_end', '=', False), ('date_end', '>=', date_from)
- ])
-
- @api.model
- def _get_all_contracts(self, date_from, date_to, states=['open', 'pending']):
- """
- Returns the contracts of all employees between date_from and date_to
- """
- return self.search([])._get_contracts(date_from, date_to, states=states)
-
-
class Contract(models.Model):
_name = 'hr.contract'
_description = 'Contract'
@@ -155,7 +119,7 @@ class Contract(models.Model):
def write(self, vals):
if vals.get('state') == 'open':
for contract in self:
- contract.employee_id.contract_id = contract
+ contract.employee_id.sudo().write({'contract_id': contract.id})
return super(Contract, self).write(vals)
@api.multi
diff --git a/addons/hr_contract/models/hr_employee.py b/addons/hr_contract/models/hr_employee.py
new file mode 100644
index 00000000000..02531df04b8
--- /dev/null
+++ b/addons/hr_contract/models/hr_employee.py
@@ -0,0 +1,41 @@
+# -*- 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"
+
+ 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')
+ contract_id = fields.Many2one('hr.contract', string='Current Contract',
+ groups="hr.group_hr_user", help='Current contract of the employee')
+ contracts_count = fields.Integer(compute='_compute_contracts_count', string='Contract Count')
+
+ def _compute_contracts_count(self):
+ # read_group as sudo, since contract count is displayed on form view
+ contract_data = self.env['hr.contract'].sudo().read_group([('employee_id', 'in', self.ids)], ['employee_id'], ['employee_id'])
+ result = dict((data['employee_id'][0], data['employee_id_count']) for data in contract_data)
+ for employee in self:
+ employee.contracts_count = result.get(employee.id, 0)
+
+ def _get_contracts(self, date_from, date_to, states=['open', 'pending']):
+ """
+ Returns the contracts of the employee between date_from and date_to
+ """
+ return self.env['hr.contract'].search([
+ '&', '&', '&',
+ ('employee_id', 'in', self.ids),
+ ('state', 'in', states),
+ ('date_start', '<=', date_to),
+ '|', ('date_end', '=', False), ('date_end', '>=', date_from)
+ ])
+
+ @api.model
+ def _get_all_contracts(self, date_from, date_to, states=['open', 'pending']):
+ """
+ Returns the contracts of all employees between date_from and date_to
+ """
+ return self.search([])._get_contracts(date_from, date_to, states=states)
diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml
index 4c8077269a5..2a58aa89861 100644
--- a/addons/hr_contract/views/hr_contract_views.xml
+++ b/addons/hr_contract/views/hr_contract_views.xml
@@ -266,7 +266,7 @@
id="hr_menu_contract"
name="Contracts"
action="action_hr_contract"
- parent="hr.menu_hr_root"
+ parent="hr.menu_hr_employee_payroll"
sequence="4"
groups="hr_contract.group_hr_contract_manager"/>
diff --git a/addons/hr_expense/models/hr_employee.py b/addons/hr_expense/models/hr_employee.py
index 0e760a6cca4..4820587a267 100644
--- a/addons/hr_expense/models/hr_employee.py
+++ b/addons/hr_expense/models/hr_employee.py
@@ -31,6 +31,12 @@ class Employee(models.Model):
self.expense_manager_id = manager
+class EmployeePublic(models.Model):
+ _inherit = 'hr.employee.public'
+
+ expense_manager_id = fields.Many2one('res.users', readonly=True)
+
+
class User(models.Model):
_inherit = ['res.users']
diff --git a/addons/hr_fleet/controllers/main.py b/addons/hr_fleet/controllers/main.py
index a943fdac284..175139e04da 100644
--- a/addons/hr_fleet/controllers/main.py
+++ b/addons/hr_fleet/controllers/main.py
@@ -4,15 +4,15 @@
import base64
import io
+from PyPDF2 import PdfFileReader, PdfFileWriter
+from reportlab.pdfgen import canvas
+
from odoo import _
from odoo.http import request, route, Controller
-from PyPDF2 import PdfFileReader, PdfFileWriter
-from reportlab.pdfgen import canvas
class HrFleet(Controller):
-
@route(["/fleet/print_claim_report/<int:employee_id>"], type='http', auth='user')
def get_claim_report_user(self, employee_id, **post):
if not request.env.user.has_group('fleet.fleet_group_manager'):
diff --git a/addons/hr_fleet/models/employee.py b/addons/hr_fleet/models/employee.py
index dc0d6c214d1..1e80a6569f1 100644
--- a/addons/hr_fleet/models/employee.py
+++ b/addons/hr_fleet/models/employee.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
from odoo import fields, models
+
class Employee(models.Model):
_inherit = 'hr.employee'
@@ -26,7 +26,7 @@ class Employee(models.Model):
driver_ids = (self.mapped('user_id.partner_id') | self.sudo().mapped('address_home_id')).ids
fleet_data = self.env['fleet.vehicle.assignation.log'].read_group(
domain=[('driver_id', 'in', driver_ids)], fields=['driver_id'], groupby=['driver_id'])
- mapped_data = dict([(m['driver_id'][0], m['driver_id_count']) for m in fleet_data])
+ mapped_data = {m['driver_id'][0]: m['driver_id_count'] for m in fleet_data}
for employee in self:
drivers = employee.user_id.partner_id | employee.sudo().address_home_id
employee.employee_cars_count = sum(mapped_data.get(pid, 0) for pid in drivers.ids)
diff --git a/addons/hr_fleet/views/employee_views.xml b/addons/hr_fleet/views/employee_views.xml
index 010775dce22..7f9b7fc87a1 100644
--- a/addons/hr_fleet/views/employee_views.xml
+++ b/addons/hr_fleet/views/employee_views.xml
@@ -36,7 +36,7 @@
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_open_employee_cars" type="object"
- class="oe_stat_button" icon="fa-car"
+ class="oe_stat_button" icon="fa-car" groups="fleet.fleet_group_manager"
attrs="{'invisible': [('employee_cars_count','=', 0)]}">
<field name="employee_cars_count" widget="statinfo" />
</button>
diff --git a/addons/hr_gamification/models/__init__.py b/addons/hr_gamification/models/__init__.py
index 6088b354662..002facd9929 100644
--- a/addons/hr_gamification/models/__init__.py
+++ b/addons/hr_gamification/models/__init__.py
@@ -3,3 +3,4 @@
from . import gamification
from . import hr_employee
+from . import res_users
diff --git a/addons/hr_gamification/models/gamification.py b/addons/hr_gamification/models/gamification.py
index 1107f0e0012..3e6b8af3886 100644
--- a/addons/hr_gamification/models/gamification.py
+++ b/addons/hr_gamification/models/gamification.py
@@ -39,6 +39,6 @@ class GamificationBadge(models.Model):
'name': 'Granted Employees',
'view_mode': 'kanban,tree,form',
'view_type': 'form',
- 'res_model': 'hr.employee',
+ 'res_model': 'hr.employee.public',
'domain': [('id', 'in', employee_ids)]
}
diff --git a/addons/hr_gamification/models/hr_employee.py b/addons/hr_gamification/models/hr_employee.py
index 36171bea1aa..d36edd79d49 100644
--- a/addons/hr_gamification/models/hr_employee.py
+++ b/addons/hr_gamification/models/hr_employee.py
@@ -4,8 +4,8 @@
from odoo import api, fields, models
-class HrEmployee(models.Model):
- _inherit = "hr.employee"
+class HrEmployeeBase(models.AbstractModel):
+ _inherit = "hr.employee.base"
goal_ids = fields.One2many('gamification.goal', string='Employee HR Goals', compute='_compute_employee_goals')
badge_ids = fields.One2many(
@@ -36,10 +36,3 @@ class HrEmployee(models.Model):
])
employee.has_badges = bool(badge_ids)
employee.badge_ids = badge_ids
-
-
-class ResUsers(models.Model):
- _inherit = 'res.users'
-
- goal_ids = fields.One2many('gamification.goal', 'user_id')
- badge_ids = fields.One2many('gamification.badge.user', 'user_id')
diff --git a/addons/hr_gamification/models/res_users.py b/addons/hr_gamification/models/res_users.py
new file mode 100644
index 00000000000..f5a301f7b15
--- /dev/null
+++ b/addons/hr_gamification/models/res_users.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = 'res.users'
+
+ goal_ids = fields.One2many('gamification.goal', 'user_id')
+ badge_ids = fields.One2many('gamification.badge.user', 'user_id')
diff --git a/addons/hr_gamification/views/hr_employee_views.xml b/addons/hr_gamification/views/hr_employee_views.xml
index 4390f50327b..285d9a44136 100644
--- a/addons/hr_gamification/views/hr_employee_views.xml
+++ b/addons/hr_gamification/views/hr_employee_views.xml
@@ -25,4 +25,28 @@
</field>
</record>
+ <record id="hr_employee_public_view_form" model="ir.ui.view">
+ <field name="name">hr.employee.public.view.form.inherit</field>
+ <field name="model">hr.employee.public</field>
+ <field name="inherit_id" ref="hr.hr_employee_public_view_form"/>
+ <field name="arch" type="xml">
+
+ <xpath expr="//page[@name='public']" position="after">
+ <page string="Received Badges" attrs="{'invisible': [('user_id', '=', False)]}">
+ <field name="has_badges" invisible="1"/>
+ <button string="Grant a Badge" type="action" name="%(action_reward_wizard)d"/> to reward this employee for a good action
+ <div class="o_field_nocontent" attrs="{'invisible': [('has_badges', '=', True)]}">
+ <p>
+ Grant this employee his first badge
+ </p><p class="oe_grey">
+ Badges are rewards of good work. Give them to people you believe deserve it.
+ </p>
+ </div>
+ <field name="badge_ids" mode="kanban" widget="many2many"/>
+ </page>
+ </xpath>
+
+ </field>
+ </record>
+
</odoo>
diff --git a/addons/hr_holidays/models/__init__.py b/addons/hr_holidays/models/__init__.py
index 2da28fe24fa..0697c64a04e 100644
--- a/addons/hr_holidays/models/__init__.py
+++ b/addons/hr_holidays/models/__init__.py
@@ -2,7 +2,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import resource
-from . import hr
+from . import hr_employee
+from . import hr_department
from . import hr_leave
from . import hr_leave_allocation
from . import hr_leave_type
diff --git a/addons/hr_holidays/models/hr_department.py b/addons/hr_holidays/models/hr_department.py
new file mode 100644
index 00000000000..44675d836ba
--- /dev/null
+++ b/addons/hr_holidays/models/hr_department.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import datetime
+from dateutil.relativedelta import relativedelta
+
+from odoo import api, fields, models
+
+
+class Department(models.Model):
+
+ _inherit = 'hr.department'
+
+ absence_of_today = fields.Integer(
+ compute='_compute_leave_count', string='Absence by Today')
+ leave_to_approve_count = fields.Integer(
+ compute='_compute_leave_count', string='Time Off to Approve')
+ allocation_to_approve_count = fields.Integer(
+ compute='_compute_leave_count', string='Allocation to Approve')
+ total_employee = fields.Integer(
+ compute='_compute_total_employee', string='Total Employee')
+
+ @api.multi
+ def _compute_leave_count(self):
+ Requests = self.env['hr.leave']
+ Allocations = self.env['hr.leave.allocation']
+ 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))
+
+ leave_data = Requests.read_group(
+ [('department_id', 'in', self.ids),
+ ('state', '=', 'confirm')],
+ ['department_id'], ['department_id'])
+ allocation_data = Allocations.read_group(
+ [('department_id', 'in', self.ids),
+ ('state', '=', 'confirm')],
+ ['department_id'], ['department_id'])
+ absence_data = Requests.read_group(
+ [('department_id', 'in', self.ids), ('state', 'not in', ['cancel', 'refuse']),
+ ('date_from', '<=', today_end), ('date_to', '>=', today_start)],
+ ['department_id'], ['department_id'])
+
+ res_leave = dict((data['department_id'][0], data['department_id_count']) for data in leave_data)
+ res_allocation = dict((data['department_id'][0], data['department_id_count']) for data in allocation_data)
+ res_absence = dict((data['department_id'][0], data['department_id_count']) for data in absence_data)
+
+ for department in self:
+ department.leave_to_approve_count = res_leave.get(department.id, 0)
+ department.allocation_to_approve_count = res_allocation.get(department.id, 0)
+ department.absence_of_today = res_absence.get(department.id, 0)
+
+ @api.multi
+ def _compute_total_employee(self):
+ emp_data = self.env['hr.employee'].read_group([('department_id', 'in', self.ids)], ['department_id'], ['department_id'])
+ result = dict((data['department_id'][0], data['department_id_count']) for data in emp_data)
+ for department in self:
+ department.total_employee = result.get(department.id, 0)
diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr_employee.py
index f870d61892a..459cf0a642d 100644
--- a/addons/hr_holidays/models/hr.py
+++ b/addons/hr_holidays/models/hr_employee.py
@@ -2,67 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
-from dateutil.relativedelta import relativedelta
-from odoo import api, fields, models, _
-from odoo.exceptions import UserError
+from odoo import api, fields, models
from odoo.tools.float_utils import float_round
-# YTI TODO: Split into 2 files
-class Department(models.Model):
-
- _inherit = 'hr.department'
-
- absence_of_today = fields.Integer(
- compute='_compute_leave_count', string='Absence by Today')
- leave_to_approve_count = fields.Integer(
- compute='_compute_leave_count', string='Time Off to Approve')
- allocation_to_approve_count = fields.Integer(
- compute='_compute_leave_count', string='Allocation to Approve')
- total_employee = fields.Integer(
- compute='_compute_total_employee', string='Total Employee')
-
- @api.multi
- def _compute_leave_count(self):
- Requests = self.env['hr.leave']
- Allocations = self.env['hr.leave.allocation']
- 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))
-
- leave_data = Requests.read_group(
- [('department_id', 'in', self.ids),
- ('state', '=', 'confirm')],
- ['department_id'], ['department_id'])
- allocation_data = Allocations.read_group(
- [('department_id', 'in', self.ids),
- ('state', '=', 'confirm')],
- ['department_id'], ['department_id'])
- absence_data = Requests.read_group(
- [('department_id', 'in', self.ids), ('state', 'not in', ['cancel', 'refuse']),
- ('date_from', '<=', today_end), ('date_to', '>=', today_start)],
- ['department_id'], ['department_id'])
-
- res_leave = dict((data['department_id'][0], data['department_id_count']) for data in leave_data)
- res_allocation = dict((data['department_id'][0], data['department_id_count']) for data in allocation_data)
- res_absence = dict((data['department_id'][0], data['department_id_count']) for data in absence_data)
-
- for department in self:
- department.leave_to_approve_count = res_leave.get(department.id, 0)
- department.allocation_to_approve_count = res_allocation.get(department.id, 0)
- department.absence_of_today = res_absence.get(department.id, 0)
-
- @api.multi
- def _compute_total_employee(self):
- emp_data = self.env['hr.employee'].read_group([('department_id', 'in', self.ids)], ['department_id'], ['department_id'])
- result = dict((data['department_id'][0], data['department_id_count']) for data in emp_data)
- for department in self:
- department.total_employee = result.get(department.id, 0)
-
-
-class Employee(models.Model):
- _inherit = "hr.employee"
+class HrEmployeeBase(models.AbstractModel):
+ _inherit = "hr.employee.base"
def _group_hr_user_domain(self):
group = self.env.ref('hr_holidays.group_hr_holidays_team_leader', raise_if_not_found=False)
@@ -173,7 +119,7 @@ class Employee(models.Model):
@api.onchange('parent_id')
def _onchange_parent_id(self):
- super(Employee, self)._onchange_parent_id()
+ super(HrEmployeeBase, 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):
@@ -199,7 +145,7 @@ class Employee(models.Model):
return [('id', 'in', holidays.mapped('employee_id').ids)]
def write(self, values):
- res = super(Employee, self).write(values)
+ res = super(HrEmployeeBase, self).write(values)
today_date = fields.Datetime.now()
if 'parent_id' in values or 'department_id' in values:
hr_vals = {}
@@ -207,7 +153,7 @@ class Employee(models.Model):
hr_vals['manager_id'] = values['parent_id']
if values.get('department_id') is not None:
hr_vals['department_id'] = values['department_id']
- holidays = self.env['hr.leave'].search(['|',('state', 'in', ['draft', 'confirm']),('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
+ holidays = self.env['hr.leave'].search(['|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
holidays.write(hr_vals)
allocations = self.env['hr.leave.allocation'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
allocations.write(hr_vals)
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml
index 81012520af8..1c083ba8b10 100644
--- a/addons/hr_holidays/views/hr_leave_views.xml
+++ b/addons/hr_holidays/views/hr_leave_views.xml
@@ -592,7 +592,7 @@
<field name="name">Time Off</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">hr.leave</field>
- <field name="binding_model_id" ref="model_hr_employee"/>
+ <field name="binding_model_id" ref="hr.model_hr_employee"/>
<field name="view_mode">tree,kanban,form,calendar,activity</field>
<field name="context">{}</field>
<field name="domain">[]</field>
diff --git a/addons/hr_holidays/views/hr_views.xml b/addons/hr_holidays/views/hr_views.xml
index 6c42da51f71..14cc0e16002 100644
--- a/addons/hr_holidays/views/hr_views.xml
+++ b/addons/hr_holidays/views/hr_views.xml
@@ -177,6 +177,52 @@
</field>
</record>
+ <record id="hr_employee_public_form_view_inherit" model="ir.ui.view">
+ <field name="name">hr.employee.public.leave.form.inherit</field>
+ <field name="model">hr.employee.public</field>
+ <field name="inherit_id" ref="hr.hr_employee_public_view_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='coach_id']" position="after">
+ <field name="leave_manager_id"/>
+ </xpath>
+ <xpath expr="//div[@name='button_box']" position="inside">
+ <field name="current_leave_id" invisible="1"/>
+ <field name="show_leaves" 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>
+
<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>
diff --git a/addons/hr_org_chart/controllers/hr_org_chart.py b/addons/hr_org_chart/controllers/hr_org_chart.py
index 4a8fb029dda..d9aeb0ce5fa 100644
--- a/addons/hr_org_chart/controllers/hr_org_chart.py
+++ b/addons/hr_org_chart/controllers/hr_org_chart.py
@@ -19,7 +19,7 @@ class HrOrgChartController(http.Controller):
else:
cids = [request.env.company.id]
- Employee = request.env['hr.employee'].with_context(allowed_company_ids=cids)
+ Employee = request.env['hr.employee.public'].with_context(allowed_company_ids=cids)
# check and raise
if not Employee.check_access_rights('read', raise_exception=False):
return None
@@ -35,13 +35,19 @@ class HrOrgChartController(http.Controller):
return dict(
id=employee.id,
name=employee.name,
- link='/mail/view?model=hr.employee&res_id=%s' % employee.id,
+ link='/mail/view?model=%s&res_id=%s' % ('hr.employee.public', employee.id,),
job_id=job.id,
job_name=job.name or '',
direct_sub_count=len(employee.child_ids),
indirect_sub_count=employee.child_all_count,
)
+ @http.route('/hr/get_redirect_model', type='json', auth='user')
+ def get_redirect_model(self):
+ if request.env['hr.employee'].check_access_rights('read', raise_exception=False):
+ return 'hr.employee'
+ return 'hr.employee.public'
+
@http.route('/hr/get_org_chart', type='json', auth='user')
def get_org_chart(self, employee_id, **kw):
@@ -50,7 +56,7 @@ class HrOrgChartController(http.Controller):
return {}
# compute employee data for org chart
- ancestors, current = request.env['hr.employee'], employee
+ ancestors, current = request.env['hr.employee.public'], employee
while current.parent_id and len(ancestors) < self._managers_level+1:
ancestors += current.parent_id
current = current.parent_id
@@ -81,8 +87,10 @@ class HrOrgChartController(http.Controller):
return {}
if subordinates_type == 'direct':
- return employee.child_ids.ids
+ res = employee.child_ids.ids
elif subordinates_type == 'indirect':
- return (employee.subordinate_ids - employee.child_ids).ids
+ res = (employee.subordinate_ids - employee.child_ids).ids
else:
- return employee.subordinate_ids.ids
+ res = employee.subordinate_ids.ids
+
+ return res
diff --git a/addons/hr_org_chart/models/__init__.py b/addons/hr_org_chart/models/__init__.py
index 71d1e5c7c23..27f11d1b306 100644
--- a/addons/hr_org_chart/models/__init__.py
+++ b/addons/hr_org_chart/models/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import hr_org_chart_mixin
from . import hr_employee
diff --git a/addons/hr_org_chart/models/hr_employee.py b/addons/hr_org_chart/models/hr_employee.py
index 15fc50645d2..8d04dd989ec 100644
--- a/addons/hr_org_chart/models/hr_employee.py
+++ b/addons/hr_org_chart/models/hr_employee.py
@@ -1,43 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
+from odoo import fields, models
-class Employee(models.Model):
- _name = "hr.employee"
- _inherit = "hr.employee"
-
- child_all_count = fields.Integer(
- 'Indirect Surbordinates Count',
- compute='_compute_subordinates', store=False)
-
- subordinate_ids = fields.One2many('hr.employee', string='Subordinates', compute='_compute_subordinates', help="Direct and indirect subordinates", groups='base.group_user')
+class Employee(models.Model):
+ _inherit = ["hr.employee"]
- def _get_subordinates(self, parents=None):
- """
- Helper function to compute subordinates_ids.
- Get all subordinates (direct and indirect) of an employee.
- An employee can be a manager of his own manager (recursive hierarchy; e.g. the CEO is manager of everyone but is also
- member of the RD department, managed by the CTO itself managed by the CEO).
- In that case, the manager in not counted as a subordinate if it's in the 'parents' set.
- """
-
- if not parents:
- parents = self.env['hr.employee']
+ subordinate_ids = fields.One2many('hr.employee', string='Subordinates', compute='_compute_subordinates', help="Direct and indirect subordinates")
- indirect_subordinates = self.env['hr.employee']
- parents |= self
- direct_subordinates = self.child_ids - parents
- for child in direct_subordinates:
- child_subordinate = child._get_subordinates(parents=parents)
- child.subordinate_ids = child_subordinate
- indirect_subordinates |= child_subordinate
- return indirect_subordinates | direct_subordinates
+class HrEmployeePublic(models.Model):
+ _inherit = ["hr.employee.public"]
- @api.depends('child_ids', 'child_ids.child_all_count')
- def _compute_subordinates(self):
- for employee in self:
- employee.subordinate_ids = employee._get_subordinates()
- employee.child_all_count = len(employee.subordinate_ids)
+ subordinate_ids = fields.One2many('hr.employee.public', string='Subordinates', compute='_compute_subordinates', help="Direct and indirect subordinates")
diff --git a/addons/hr_org_chart/models/hr_org_chart_mixin.py b/addons/hr_org_chart/models/hr_org_chart_mixin.py
new file mode 100644
index 00000000000..7633f2325ab
--- /dev/null
+++ b/addons/hr_org_chart/models/hr_org_chart_mixin.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class HrEmployeeBase(models.AbstractModel):
+ _inherit = "hr.employee.base"
+
+ child_all_count = fields.Integer(
+ 'Indirect Surbordinates Count',
+ compute='_compute_subordinates', store=False)
+
+ def _get_subordinates(self, parents=None):
+ """
+ Helper function to compute subordinates_ids.
+ Get all subordinates (direct and indirect) of an employee.
+ An employee can be a manager of his own manager (recursive hierarchy; e.g. the CEO is manager of everyone but is also
+ member of the RD department, managed by the CTO itself managed by the CEO).
+ In that case, the manager in not counted as a subordinate if it's in the 'parents' set.
+ """
+ if not parents:
+ parents = self.env[self._name]
+
+ indirect_subordinates = self.env[self._name]
+ parents |= self
+ direct_subordinates = self.child_ids - parents
+ for child in direct_subordinates:
+ child_subordinate = child._get_subordinates(parents=parents)
+ child.subordinate_ids = child_subordinate
+ indirect_subordinates |= child_subordinate
+ return indirect_subordinates | direct_subordinates
+
+
+ @api.depends('child_ids', 'child_ids.child_all_count')
+ def _compute_subordinates(self):
+ for employee in self:
+ employee.subordinate_ids = employee._get_subordinates()
+ employee.child_all_count = len(employee.subordinate_ids)
diff --git a/addons/hr_org_chart/static/src/js/hr_org_chart.js b/addons/hr_org_chart/static/src/js/hr_org_chart.js
index 5e2ba861058..d1831b53c8b 100644
--- a/addons/hr_org_chart/static/src/js/hr_org_chart.js
+++ b/addons/hr_org_chart/static/src/js/hr_org_chart.js
@@ -24,7 +24,7 @@ var FieldOrgChart = AbstractField.extend({
init: function (parent, options) {
this._super.apply(this, arguments);
this.dm = new concurrency.DropMisordered();
- this.employee;
+ this.employee = null;
},
//--------------------------------------------------------------------------
@@ -37,16 +37,16 @@ var FieldOrgChart = AbstractField.extend({
* @param {integer} employee_id
* @returns {Promise}
*/
- _getOrgData: function (employee_id) {
+ _getOrgData: function () {
var self = this;
return this.dm.add(this._rpc({
route: '/hr/get_org_chart',
params: {
- employee_id: employee_id,
+ employee_id: this.employee,
context: session.user_context,
},
})).then(function (data) {
- self.orgData = data;
+ return data;
});
},
/**
@@ -83,10 +83,9 @@ var FieldOrgChart = AbstractField.extend({
}
var self = this;
- return this._getOrgData(this.employee).then(function () {
-
- self.orgData['view_employee_id'] = self.recordData.id;
- self.$el.html(QWeb.render("hr_org_chart", self.orgData));
+ return this._getOrgData().then(function (orgData) {
+ orgData.view_employee_id = self.recordData.id;
+ self.$el.html(QWeb.render("hr_org_chart", orgData));
self.$('[data-toggle="popover"]').each(function () {
$(this).popover({
html: true,
@@ -130,7 +129,7 @@ var FieldOrgChart = AbstractField.extend({
_onEmployeeMoreManager: function(event) {
event.preventDefault();
this.employee = parseInt($(event.currentTarget).data('employee-id'));
- this._render()
+ this._render();
},
/**
* Redirect to the employee form view.
@@ -140,16 +139,15 @@ var FieldOrgChart = AbstractField.extend({
* @returns {Promise} action loaded
*/
_onEmployeeRedirect: function (event) {
+ var self = this;
event.preventDefault();
var employee_id = parseInt($(event.currentTarget).data('employee-id'));
- return this.do_action({
- type: 'ir.actions.act_window',
- view_type: 'form',
- view_mode: 'form',
- views: [[false, 'form']],
- target: 'current',
- res_model: 'hr.employee',
- res_id: employee_id,
+ return this._rpc({
+ model: 'hr.employee',
+ method: 'get_formview_action',
+ args: [employee_id],
+ }).then(function(action) {
+ return self.do_action(action);
});
},
/**
@@ -164,22 +162,23 @@ var FieldOrgChart = AbstractField.extend({
var employee_id = parseInt($(event.currentTarget).data('employee-id'));
var employee_name = $(event.currentTarget).data('employee-name');
var type = $(event.currentTarget).data('type') || 'direct';
- var self = this
+ var self = this;
if (employee_id) {
this._getSubordinatesData(employee_id, type).then(function(data) {
var domain = [['id', 'in', data]];
-
- return self.do_action({
- name: employee_name,
- type: 'ir.actions.act_window',
- view_mode: 'kanban,list,form',
- views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
- target: 'current',
- res_model: 'hr.employee',
- domain: domain,
+ return self._rpc({
+ model: 'hr.employee',
+ method: 'get_formview_action',
+ args: [employee_id],
+ }).then(function(action) {
+ action = _.extend(action, {
+ 'view_mode': 'kanban,list,form',
+ 'views': [[false, 'kanban'], [false, 'list'], [false, 'form']],
+ 'domain': domain,
+ });
+ return self.do_action(action);
});
- })
-
+ });
}
},
});
diff --git a/addons/hr_org_chart/static/src/xml/hr_org_chart.xml b/addons/hr_org_chart/static/src/xml/hr_org_chart.xml
index 893e0394f90..2bb9db5d4ca 100644
--- a/addons/hr_org_chart/static/src/xml/hr_org_chart.xml
+++ b/addons/hr_org_chart/static/src/xml/hr_org_chart.xml
@@ -10,13 +10,13 @@
use bg-images to get a clean and centred images -->
<a t-if="! is_self"
class="o_media_object rounded-circle o_employee_redirect"
- t-att-style="'background-image:url(\'/web/image/hr.employee/' + employee.id + '/image/\')'"
+ t-att-style="'background-image:url(\'/web/image/hr.employee.public/' + employee.id + '/image/\')'"
t-att-alt="employee.name"
t-att-data-employee-id="employee.id"
t-att-href="employee.link"/>
<div t-if="is_self"
class="o_media_object rounded-circle"
- t-att-style="'background-image:url(\'/web/image/hr.employee/' + employee.id + '/image/\')'"/>
+ t-att-style="'background-image:url(\'/web/image/hr.employee.public/' + employee.id + '/image/\')'"/>
</div>
<div class="media-body">
@@ -151,7 +151,7 @@
<t t-name="hr_orgchart_emp_popover_title">
<div>
- <span t-att-style='"background-image:url(\"/web/image/hr.employee/" + employee.id + "/image/\")"'/>
+ <span t-att-style='"background-image:url(\"/web/image/hr.employee.public/" + employee.id + "/image/\")"'/>
<a href="#" class="float-right o_employee_redirect" t-att-data-employee-id="employee.id"><i class="fa fa-external-link" role="img" aria-label='Redirect' title="Redirect"></i></a>
<b><t t-esc="employee.name"/></b>
</div>
diff --git a/addons/hr_org_chart/static/tests/hr_org_chart_tests.js b/addons/hr_org_chart/static/tests/hr_org_chart_tests.js
index e536f0b587b..98104311f55 100644
--- a/addons/hr_org_chart/static/tests/hr_org_chart_tests.js
+++ b/addons/hr_org_chart/static/tests/hr_org_chart_tests.js
@@ -41,6 +41,8 @@ QUnit.module('hr_org_chart', {
managers: [],
managers_more: false,
});
+ } else if (route === '/hr/get_redirect_model') {
+ return Promise.resolve('hr.employee');
}
return this._super(route, args);
}
@@ -92,6 +94,8 @@ QUnit.module('hr_org_chart', {
name: 'Antoine Langlais',
}
});
+ } else if (route === '/hr/get_redirect_model') {
+ return Promise.resolve('hr.employee');
}
return this._super(route, args);
}
@@ -153,6 +157,8 @@ QUnit.module('hr_org_chart', {
name: 'John Smith',
}
});
+ } else if (route === '/hr/get_redirect_model') {
+ return Promise.resolve('hr.employee');
}
return this._super(route, args);
}
@@ -164,4 +170,4 @@ QUnit.module('hr_org_chart', {
});
});
-}); \ No newline at end of file
+});
diff --git a/addons/hr_org_chart/views/hr_views.xml b/addons/hr_org_chart/views/hr_views.xml
index 2e9e090887b..a8e7a6c70bd 100644
--- a/addons/hr_org_chart/views/hr_views.xml
+++ b/addons/hr_org_chart/views/hr_views.xml
@@ -14,6 +14,22 @@
</field>
</record>
+ <record id="hr_employee_public_view_form_inherit_org_chart" model="ir.ui.view">
+ <field name="name">hr.employee.public.view.form.inherit.org_chart</field>
+ <field name="model">hr.employee.public</field>
+ <field name="inherit_id" ref="hr.hr_employee_public_view_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[@id='o_work_employee_main']" position="after">
+ <group>
+ <div id="o_employee_right">
+ <h4 class="o_org_chart_title mb16 mt0">Organization Chart</h4>
+ <field name="child_ids" widget="hr_org_chart"/>
+ </div>
+ </group>
+ </xpath>
+ </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>
diff --git a/addons/hr_presence/models/hr_employee.py b/addons/hr_presence/models/hr_employee.py
index 9e6815f1461..e79d8a325ca 100644
--- a/addons/hr_presence/models/hr_employee.py
+++ b/addons/hr_presence/models/hr_employee.py
@@ -16,7 +16,7 @@ class ResCompany(models.Model):
hr_presence_state = fields.Selection([
('present', 'Present'),
('absent', 'Absent'),
- ('to_define', 'To Define')], default='to_define')
+ ('to_define', 'To Define')], groups="hr.group_hr_user", default='to_define')
last_activity = fields.Date(compute="_compute_last_activity")
def _compute_last_activity(self):
diff --git a/addons/hr_skills/models/hr_resume.py b/addons/hr_skills/models/hr_resume.py
index 7725a5c05ac..79e26312f05 100644
--- a/addons/hr_skills/models/hr_resume.py
+++ b/addons/hr_skills/models/hr_resume.py
@@ -27,6 +27,13 @@ class Employee(models.Model):
return res
+class EmployeePublic(models.Model):
+ _inherit = 'hr.employee.public'
+
+ 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")
+
+
class ResumeLine(models.Model):
_name = 'hr.resume.line'
_description = "Resumé line of an employee"
diff --git a/addons/hr_skills/views/hr_views.xml b/addons/hr_skills/views/hr_views.xml
index 98f2a73e59e..2af8dccab00 100644
--- a/addons/hr_skills/views/hr_views.xml
+++ b/addons/hr_skills/views/hr_views.xml
@@ -89,6 +89,47 @@
</field>
</record>
+
+ <record id="hr_employee_public_view_form_inherit" model="ir.ui.view">
+ <field name="name">hr.employee.public.view.form.inherit.resume</field>
+ <field name="model">hr.employee.public</field>
+ <field name="inherit_id" ref="hr.hr_employee_public_view_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 mode="tree" nolabel="1" name="resume_line_ids" widget="hr_resume">
+ <tree>
+ <field name="line_type_id"/>
+ <field name="name"/>
+ <field name="description"/>
+ <field name="date_start"/>
+ <field name="date_end"/>
+ <field name="display_type" invisible="1"/>
+ </tree>
+ </field>
+ </div>
+ <div class="col-lg-4 o_group_skills">
+ <separator string="Skills"/>
+ <field mode="tree" 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>
@@ -272,5 +313,3 @@
sequence="3"
groups="base.group_no_one"/>
</odoo>
-
-
diff --git a/addons/hr_timesheet/models/__init__.py b/addons/hr_timesheet/models/__init__.py
index 5e18c694d32..ee021539a0f 100644
--- a/addons/hr_timesheet/models/__init__.py
+++ b/addons/hr_timesheet/models/__init__.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import hr
+from . import hr_employee
from . import hr_timesheet
from . import ir_http
from . import res_company
diff --git a/addons/hr_timesheet/models/hr.py b/addons/hr_timesheet/models/hr_employee.py
index afedd48561e..5e4a2abb2ca 100644
--- a/addons/hr_timesheet/models/hr.py
+++ b/addons/hr_timesheet/models/hr_employee.py
@@ -7,5 +7,6 @@ from odoo import fields, models
class HrEmployee(models.Model):
_inherit = 'hr.employee'
- timesheet_cost = fields.Monetary('Timesheet Cost', currency_field='currency_id', default=0.0)
+ timesheet_cost = fields.Monetary('Timesheet Cost', currency_field='currency_id',
+ groups="hr.group_hr_user", default=0.0)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True)
diff --git a/addons/website_hr/models/hr_employee.py b/addons/website_hr/models/hr_employee.py
index 0787163acbe..c301078883a 100644
--- a/addons/website_hr/models/hr_employee.py
+++ b/addons/website_hr/models/hr_employee.py
@@ -14,3 +14,10 @@ class HrEmployee(models.Model):
super(HrEmployee, self)._compute_website_url()
for employee in self:
employee.website_url = '/aboutus#team'
+
+class HrEmployeePublic(models.Model):
+ _name = 'hr.employee.public'
+ _inherit = ['hr.employee.public', 'website.published.multi.mixin']
+
+ public_info = fields.Char(string='Public Info')
+