diff options
| author | Robot Odoo <xmo+roboto@odoo.com> | 2019-05-31 11:36:31 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-05-31 11:36:31 +0200 |
| commit | 2d4b026625b162f4767d1b8152fd87fd34adf18c (patch) | |
| tree | 6c235d36ea76f40c7066dabf953b5d7562ed68b7 /addons | |
| parent | aae6b3c337bdab3d62dd517b416a0a7b080ed248 (diff) | |
| parent | f755e57fb8520c099f4712f0092c9b5975d88fc4 (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')
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', '<', 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', '>', 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"/>&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', '<', 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', '>', 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"/>&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') + |
