diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py index d3ed7d4b2fc..c38700c0404 100644 --- a/addons/hr/__manifest__.py +++ b/addons/hr/__manifest__.py @@ -26,6 +26,11 @@ '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_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..dc940941bd1 100644 --- a/addons/hr/models/__init__.py +++ b/addons/hr/models/__init__.py @@ -1,10 +1,13 @@ # -*- 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 +from . import hr_employee_category +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 61% rename from addons/hr/models/hr.py rename to addons/hr/models/hr_employee.py index 971c5d9a561..8f9078b7292 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr_employee.py @@ -1,101 +1,17 @@ # -*- 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): _name = "hr.employee" _description = "Employee" _order = 'name' @@ -278,7 +194,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 +212,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 +224,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 +247,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 +281,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_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_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/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_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 @@ + + + + + + hr.employee.search + hr.employee + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.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..da423b9adb7 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -2,7 +2,6 @@ - + + + + + - - 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