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 similarity index 58% rename from addons/hr/models/hr.py rename to 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=_('Congratulations ! May I recommand you to setup an onboarding plan ?') % (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 @@ ['|',('company_id','=',False),('company_id', 'in', company_ids)] + + Employee multi company rule + + + ['|',('company_id','=',False),('company_id', 'in', company_ids)] + + Job multi company rule 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 @@ + + + + + hr.department.form + hr.department + +
+ +
+ +
+ + + + + + +
+
+ + +
+
+
+
+ + + hr.department.tree + hr.department + + + + + + + + + + + + hr.department.search + hr.department + + + + + + + + + + + + + hr.department.kanban + hr.department + + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ + + Departments + hr.department + form + kanban,tree,form + + +

+ Create a new department +

+ Odoo's department structure is used to manage all documents + related to employees by departments: expenses, timesheets, + leaves, recruitments, etc. +

+
+
+
+
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 @@ + + + + + + hr.employee.category.form + hr.employee.category + +
+ + + + + +
+
+
+ + + hr.employee.category.list + hr.employee.category + + + + + + + + + + Employee Tags + hr.employee.category + form + tree,form + + +
+
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 @@ + + + + + + hr.employee.search + hr.employee.public + + + + + + + + + hr.employee.public.form + hr.employee.public + +
+ + + +
+ +
+ +
+
+ + + + + + + + + + + + + +
+
+ + + + + + + + + + + +
+
+
+
+
+
+
+
+ + + hr.employee.tree + hr.employee.public + + + + + + + + + + + + + + + + hr.employee.kanban + hr.employee.public + 10 + + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + hr.plan.activity.type.view.tree + hr.plan.activity.type + + + + + + + + + + + hr.plan.activity.type.view.form + hr.plan.activity.type + +
+ + + + + + + + +
+
+
+ + + Planning + hr.plan + form + tree,form + + + + + Planning Types + hr.plan.activity.type + form + tree,form + + + + 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 @@ - + + + + + + + id="menu_hr_reporting_timesheet" + name="Reporting" + parent="menu_hr_root" + groups="group_hr_manager,group_hr_user" + sequence="99"/> - - hr.plan.view.search - hr.plan - - - - - - + - - hr.plan.view.tree - hr.plan - - - - - - + - - hr.plan.view.form - hr.plan - -
- -
- -
-
-
- - - - - - - - - - -
-
-
-
+ - - hr.plan.activity.type.view.tree - hr.plan.activity.type - - - - - - - - + - - hr.plan.activity.type.view.form - hr.plan.activity.type - -
- - - - - - - - -
-
-
- - - Planning - hr.plan - form - tree,form - - - - - Planning Types - hr.plan.activity.type - form - tree,form - - - - - - - - - - - hr.employee.form - hr.employee - -
- - -
-
- -
- -
- -
-
- - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - -
-
-
- - - - - -
- 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. -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - - - - - -
- - - - - - - -
- - -
- -
-
- - - hr.department.tree - hr.department - child_ids - - - - - - - - - - - - hr.department.search - hr.department - - - - - - - - - - - - - hr.department.kanban - hr.department - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
- - - Departments - hr.department - form - kanban,tree,form - - -

- Create a new department -

- Odoo's department structure is used to manage all documents - related to employees by departments: expenses, timesheets, - leaves, recruitments, etc. -

-
-
- - - res.users.preferences.form.resource - res.users - - - - 1 - - - - - + +
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 @@
+ + res.users.preferences.form.resource + res.users + + + + 1 + + + + res.users.preferences.form.inherit res.users 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 odoo import _ -from odoo.http import request, route, Controller - from PyPDF2 import PdfFileReader, PdfFileWriter from reportlab.pdfgen import canvas +from odoo import _ +from odoo.http import request, route, Controller + + class HrFleet(Controller): - @route(["/fleet/print_claim_report/"], 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 @@ 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 @@ + + hr.employee.public.view.form.inherit + hr.employee.public + + + + + + + + + + + + hr.user.preferences.view.form.leave.inherit res.users 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" + _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') + subordinate_ids = fields.One2many('hr.employee', string='Subordinates', compute='_compute_subordinates', help="Direct and indirect subordinates") - 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. - """ +class HrEmployeePublic(models.Model): + _inherit = ["hr.employee.public"] - if not parents: - parents = self.env['hr.employee'] - - 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 - - - @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 -->
+ t-att-style="'background-image:url(\'/web/image/hr.employee.public/' + employee.id + '/image/\')'"/>
@@ -151,7 +151,7 @@
- +
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 @@ + + hr.employee.public.view.form.inherit.org_chart + hr.employee.public + + + + +
+

Organization Chart

+ +
+
+
+
+
+ res.users.preferences.view.form.inherit.org_chart res.users 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 @@ + + + hr.employee.public.view.form.inherit.resume + hr.employee.public + + + + +
+
+ + + + + + + + + + + +
+
+ + + + + + + + + +
+
+
+
+
+
+ hr.user.preferences.form.inherit.hr.skills res.users @@ -272,5 +313,3 @@ sequence="3" groups="base.group_no_one"/> - - 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 similarity index 84% rename from addons/hr_timesheet/models/hr.py rename to 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') +