diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/hr/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/hr/models')
| -rw-r--r-- | addons/hr/models/__init__.py | 18 | ||||
| -rw-r--r-- | addons/hr/models/hr_department.py | 86 | ||||
| -rw-r--r-- | addons/hr/models/hr_employee.py | 343 | ||||
| -rw-r--r-- | addons/hr/models/hr_employee_base.py | 179 | ||||
| -rw-r--r-- | addons/hr/models/hr_employee_category.py | 23 | ||||
| -rw-r--r-- | addons/hr/models/hr_employee_public.py | 65 | ||||
| -rw-r--r-- | addons/hr/models/hr_job.py | 68 | ||||
| -rw-r--r-- | addons/hr/models/hr_plan.py | 64 | ||||
| -rw-r--r-- | addons/hr/models/mail_alias.py | 19 | ||||
| -rw-r--r-- | addons/hr/models/mail_channel.py | 27 | ||||
| -rw-r--r-- | addons/hr/models/models.py | 20 | ||||
| -rw-r--r-- | addons/hr/models/res_company.py | 11 | ||||
| -rw-r--r-- | addons/hr/models/res_config_settings.py | 20 | ||||
| -rw-r--r-- | addons/hr/models/res_partner.py | 23 | ||||
| -rw-r--r-- | addons/hr/models/res_users.py | 209 | ||||
| -rw-r--r-- | addons/hr/models/resource.py | 10 |
16 files changed, 1185 insertions, 0 deletions
diff --git a/addons/hr/models/__init__.py b/addons/hr/models/__init__.py new file mode 100644 index 00000000..f9e64bc7 --- /dev/null +++ b/addons/hr/models/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +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 models +from . import res_config_settings +from . import res_partner +from . import res_users +from . import res_company +from . import resource diff --git a/addons/hr/models/hr_department.py b/addons/hr/models/hr_department.py new file mode 100644 index 00000000..d65b8cb9 --- /dev/null +++ b/addons/hr/models/hr_department.py @@ -0,0 +1,86 @@ +# -*- 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 = "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, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + child_ids = fields.One2many('hr.department', 'parent_id', string='Child Departments') + manager_id = fields.Many2one('hr.employee', string='Manager', tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + 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') + + 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.model + def name_create(self, name): + return self.create({'name': name}).name_get()[0] + + @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 + + 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_employee.py b/addons/hr/models/hr_employee.py new file mode 100644 index 00000000..88272514 --- /dev/null +++ b/addons/hr/models/hr_employee.py @@ -0,0 +1,343 @@ +# -*- 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 +from werkzeug.urls import url_encode + +from odoo import api, fields, models, _ +from odoo.osv.query import Query +from odoo.exceptions import ValidationError, AccessError +from odoo.modules.module import get_module_resource + + +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 = ['hr.employee.base', 'mail.thread', 'mail.activity.mixin', 'resource.mixin', 'image.mixin'] + _mail_post_access = 'read' + + @api.model + def _default_image(self): + image_path = get_module_resource('hr', 'static/src/img', 'default_image.png') + return base64.b64encode(open(image_path, 'rb').read()) + + # resource and user + # required on the resource, make sure required="True" set in the view + name = fields.Char(string="Employee Name", related='resource_id.name', store=True, readonly=False, tracking=True) + user_id = fields.Many2one('res.users', 'User', related='resource_id.user_id', store=True, readonly=False) + user_partner_id = fields.Many2one(related='user_id.partner_id', related_sudo=False, string="User's partner") + active = fields.Boolean('Active', related='resource_id.active', default=True, store=True, readonly=False) + company_id = fields.Many2one('res.company',required=True) + # private partner + address_home_id = fields.Many2one( + 'res.partner', 'Address', help='Enter here the private address of the employee, not the one linked to your company.', + groups="hr.group_hr_user", tracking=True, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + is_address_home_a_company = fields.Boolean( + 'The employee address has a company linked', + compute='_compute_is_address_home_a_company', + ) + private_email = fields.Char(related='address_home_id.email', string="Private Email", groups="hr.group_hr_user") + country_id = fields.Many2one( + 'res.country', 'Nationality (Country)', groups="hr.group_hr_user", tracking=True) + gender = fields.Selection([ + ('male', 'Male'), + ('female', 'Female'), + ('other', 'Other') + ], groups="hr.group_hr_user", tracking=True) + marital = fields.Selection([ + ('single', 'Single'), + ('married', 'Married'), + ('cohabitant', 'Legal Cohabitant'), + ('widower', 'Widower'), + ('divorced', 'Divorced') + ], string='Marital Status', groups="hr.group_hr_user", default='single', tracking=True) + spouse_complete_name = fields.Char(string="Spouse Complete Name", groups="hr.group_hr_user", tracking=True) + spouse_birthdate = fields.Date(string="Spouse Birthdate", groups="hr.group_hr_user", tracking=True) + children = fields.Integer(string='Number of Children', groups="hr.group_hr_user", tracking=True) + place_of_birth = fields.Char('Place of Birth', groups="hr.group_hr_user", tracking=True) + country_of_birth = fields.Many2one('res.country', string="Country of Birth", groups="hr.group_hr_user", tracking=True) + birthday = fields.Date('Date of Birth', groups="hr.group_hr_user", tracking=True) + ssnid = fields.Char('SSN No', help='Social Security Number', groups="hr.group_hr_user", tracking=True) + sinid = fields.Char('SIN No', help='Social Insurance Number', groups="hr.group_hr_user", tracking=True) + identification_id = fields.Char(string='Identification No', groups="hr.group_hr_user", tracking=True) + passport_id = fields.Char('Passport No', groups="hr.group_hr_user", tracking=True) + bank_account_id = fields.Many2one( + 'res.partner.bank', 'Bank Account Number', + domain="[('partner_id', '=', address_home_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + groups="hr.group_hr_user", + tracking=True, + help='Employee bank salary account') + permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user", tracking=True) + visa_no = fields.Char('Visa No', groups="hr.group_hr_user", tracking=True) + visa_expire = fields.Date('Visa Expire Date', groups="hr.group_hr_user", tracking=True) + additional_note = fields.Text(string='Additional Note', groups="hr.group_hr_user", tracking=True) + certificate = fields.Selection([ + ('graduate', 'Graduate'), + ('bachelor', 'Bachelor'), + ('master', 'Master'), + ('doctor', 'Doctor'), + ('other', 'Other'), + ], 'Certificate Level', default='other', groups="hr.group_hr_user", tracking=True) + study_field = fields.Char("Field of Study", groups="hr.group_hr_user", tracking=True) + study_school = fields.Char("School", groups="hr.group_hr_user", tracking=True) + emergency_contact = fields.Char("Emergency Contact", groups="hr.group_hr_user", tracking=True) + emergency_phone = fields.Char("Emergency Phone", groups="hr.group_hr_user", tracking=True) + km_home_work = fields.Integer(string="Home-Work Distance", groups="hr.group_hr_user", tracking=True) + + image_1920 = fields.Image(default=_default_image) + phone = fields.Char(related='address_home_id.phone', related_sudo=False, readonly=False, string="Private Phone", groups="hr.group_hr_user") + # employee in company + child_ids = fields.One2many('hr.employee', 'parent_id', string='Direct subordinates') + category_ids = fields.Many2many( + 'hr.employee.category', 'employee_category_rel', + 'emp_id', 'category_id', groups="hr.group_hr_manager", + string='Tags') + # misc + 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", 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) + departure_date = fields.Date(string="Departure Date", 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.") + ] + + 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() + + def _read(self, fields): + if self.check_access_rights('read', raise_exception=False): + return super(HrEmployeePrivate, self)._read(fields) + + res = self.env['hr.employee.public'].browse(self.ids).read(fields) + for r in res: + record = self.browse(r['id']) + record._update_cache({k:v for k,v in r.items() if k in fields}, validate=False) + + 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 load_views(self, views, options=None): + if self.check_access_rights('read', raise_exception=False): + return super(HrEmployeePrivate, self).load_views(views, options=options) + return self.env['hr.employee.public'].load_views(views, options=options) + + @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) + ids = self.env['hr.employee.public']._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) + if not count and isinstance(ids, Query): + # the result is expected from this table, so we should link tables + ids = super(HrEmployeePrivate, self.sudo())._search([('id', 'in', ids)]) + return ids + + 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.with_user(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 + + 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.with_user(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: + if employee.pin and not employee.pin.isdigit(): + raise ValidationError(_("The PIN must be a sequence of digits.")) + + @api.onchange('user_id') + def _onchange_user(self): + if self.user_id: + self.update(self._sync_user(self.user_id, bool(self.image_1920))) + if not self.name: + self.name = self.user_id.name + + @api.onchange('resource_calendar_id') + def _onchange_timezone(self): + if self.resource_calendar_id and not self.tz: + self.tz = self.resource_calendar_id.tz + + def _sync_user(self, user, employee_has_image=False): + vals = dict( + work_email=user.email, + user_id=user.id, + ) + if not employee_has_image: + vals['image_1920'] = user.image_1920 + if user.tz: + vals['tz'] = user.tz + return vals + + @api.model + def create(self, vals): + if vals.get('user_id'): + user = self.env['res.users'].browse(vals['user_id']) + vals.update(self._sync_user(user, vals.get('image_1920') == self._default_image())) + vals['name'] = vals.get('name', user.name) + employee = super(HrEmployeePrivate, self).create(vals) + url = '/web#%s' % url_encode({ + 'action': 'hr.plan_wizard_action', + 'active_id': employee.id, + 'active_model': 'hr.employee', + 'menu_id': self.env.ref('hr.menu_hr_root').id, + }) + employee._message_log(body=_('<b>Congratulations!</b> May I recommend you to setup an <a href="%s">onboarding plan?</a>') % (url)) + if employee.department_id: + self.env['mail.channel'].sudo().search([ + ('subscription_department_ids', 'in', employee.department_id.id) + ])._subscribe_users() + return employee + + def write(self, vals): + if 'address_home_id' in vals: + account_id = vals.get('bank_account_id') or self.bank_account_id.id + if account_id: + self.env['res.partner.bank'].browse(account_id).partner_id = vals['address_home_id'] + if vals.get('user_id'): + # Update the profile pictures with user, except if provided + vals.update(self._sync_user(self.env['res.users'].browse(vals['user_id']), bool(vals.get('image_1920')))) + 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 + self.env['mail.channel'].sudo().search([ + ('subscription_department_ids', 'in', department_id) + ])._subscribe_users() + return res + + def unlink(self): + resources = self.mapped('resource_id') + super(HrEmployeePrivate, self).unlink() + return resources.unlink() + + def toggle_active(self): + res = super(HrEmployeePrivate, self).toggle_active() + unarchived_employees = self.filtered(lambda employee: employee.active) + unarchived_employees.write({ + 'departure_reason': False, + 'departure_description': False, + 'departure_date': False + }) + archived_addresses = unarchived_employees.mapped('address_home_id').filtered(lambda addr: not addr.active) + archived_addresses.toggle_active() + if len(self) == 1 and not self.active: + return { + 'type': 'ir.actions.act_window', + 'name': _('Register Departure'), + 'res_model': 'hr.departure.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': {'active_id': self.id}, + 'views': [[False, 'form']] + } + return res + + def generate_random_barcode(self): + for employee in self: + employee.barcode = '041'+"".join(choice(digits) for i in range(9)) + + @api.depends('address_home_id.parent_id') + def _compute_is_address_home_a_company(self): + """Checks that chosen address (res.partner) is not linked to a company. + """ + for employee in self: + try: + employee.is_address_home_a_company = employee.address_home_id.parent_id.id is not False + except AccessError: + employee.is_address_home_a_company = False + + # --------------------------------------------------------- + # Business Methods + # --------------------------------------------------------- + + @api.model + def get_import_templates(self): + return [{ + 'label': _('Import Template for Employees'), + 'template': '/hr/static/xls/hr_employee.xls' + }] + + def _post_author(self): + """ + When a user updates his own employee's data, all operations are performed + by super user. However, tracking messages should not be posted as OdooBot + but as the actual user. + This method is used in the overrides of `_message_log` and `message_post` + to post messages as the correct user. + """ + real_user = self.env.context.get('binary_field_real_user') + if self.env.is_superuser() and real_user: + self = self.with_user(real_user) + return self + + # --------------------------------------------------------- + # Messaging + # --------------------------------------------------------- + + def _message_log(self, **kwargs): + return super(HrEmployeePrivate, self._post_author())._message_log(**kwargs) + + @api.returns('mail.message', lambda value: value.id) + def message_post(self, **kwargs): + return super(HrEmployeePrivate, self._post_author()).message_post(**kwargs) + + def _sms_get_partner_fields(self): + return ['user_partner_id'] + + def _sms_get_number_fields(self): + return ['mobile_phone'] diff --git a/addons/hr/models/hr_employee_base.py b/addons/hr/models/hr_employee_base.py new file mode 100644 index 00000000..9732f6da --- /dev/null +++ b/addons/hr/models/hr_employee_base.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from ast import literal_eval + +from odoo import api, fields, models +from pytz import timezone, UTC, utc +from datetime import timedelta + +from odoo.tools import format_time + + +class HrEmployeeBase(models.AbstractModel): + _name = "hr.employee.base" + _description = "Basic Employee" + _order = 'name' + + name = fields.Char() + active = fields.Boolean("Active") + color = fields.Integer('Color Index', default=0) + department_id = fields.Many2one('hr.department', 'Department', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + job_id = fields.Many2one('hr.job', 'Job Position', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + job_title = fields.Char("Job Title", compute="_compute_job_title", store=True, readonly=False) + company_id = fields.Many2one('res.company', 'Company') + address_id = fields.Many2one('res.partner', 'Work Address', compute="_compute_address_id", store=True, readonly=False, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + work_phone = fields.Char('Work Phone', compute="_compute_phones", store=True, readonly=False) + 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', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + parent_id = fields.Many2one('hr.employee', 'Manager', compute="_compute_parent_id", store=True, readonly=False, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + coach_id = fields.Many2one( + 'hr.employee', 'Coach', compute='_compute_coach', store=True, readonly=False, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", + help='Select the "Employee" who is the coach of this employee.\n' + 'The "Coach" has no specific rights or responsibilities by default.') + tz = fields.Selection( + string='Timezone', related='resource_id.tz', readonly=False, + help="This field is used in order to define in which timezone the resources will work.") + hr_presence_state = fields.Selection([ + ('present', 'Present'), + ('absent', 'Absent'), + ('to_define', 'To Define')], compute='_compute_presence_state', default='to_define') + last_activity = fields.Date(compute="_compute_last_activity") + last_activity_time = fields.Char(compute="_compute_last_activity") + hr_icon_display = fields.Selection([ + ('presence_present', 'Present'), + ('presence_absent_active', 'Present but not active'), + ('presence_absent', 'Absent'), + ('presence_to_define', 'To define'), + ('presence_undetermined', 'Undetermined')], compute='_compute_presence_icon') + + @api.depends('user_id.im_status') + def _compute_presence_state(self): + """ + This method is overritten in several other modules which add additional + presence criterions. e.g. hr_attendance, hr_holidays + """ + # Check on login + check_login = literal_eval(self.env['ir.config_parameter'].sudo().get_param('hr.hr_presence_control_login', 'False')) + employee_to_check_working = self.filtered(lambda e: e.user_id.im_status == 'offline') + working_now_list = employee_to_check_working._get_employee_working_now() + for employee in self: + state = 'to_define' + if check_login: + if employee.user_id.im_status == 'online' or employee.last_activity: + state = 'present' + elif employee.user_id.im_status == 'offline' and employee.id not in working_now_list: + state = 'absent' + employee.hr_presence_state = state + + @api.depends('user_id') + def _compute_last_activity(self): + presences = self.env['bus.presence'].search_read([('user_id', 'in', self.mapped('user_id').ids)], ['user_id', 'last_presence']) + # transform the result to a dict with this format {user.id: last_presence} + presences = {p['user_id'][0]: p['last_presence'] for p in presences} + + for employee in self: + tz = employee.tz + last_presence = presences.get(employee.user_id.id, False) + if last_presence: + last_activity_datetime = last_presence.replace(tzinfo=UTC).astimezone(timezone(tz)).replace(tzinfo=None) + employee.last_activity = last_activity_datetime.date() + if employee.last_activity == fields.Date.today(): + employee.last_activity_time = format_time(self.env, last_activity_datetime, time_format='short') + else: + employee.last_activity_time = False + else: + employee.last_activity = False + employee.last_activity_time = False + + @api.depends('parent_id') + def _compute_coach(self): + for employee in self: + manager = employee.parent_id + previous_manager = employee._origin.parent_id + if manager and (employee.coach_id == previous_manager or not employee.coach_id): + employee.coach_id = manager + elif not employee.coach_id: + employee.coach_id = False + + @api.depends('job_id') + def _compute_job_title(self): + for employee in self.filtered('job_id'): + employee.job_title = employee.job_id.name + + @api.depends('address_id') + def _compute_phones(self): + for employee in self: + if employee.address_id and employee.address_id.phone: + employee.work_phone = employee.address_id.phone + else: + employee.work_phone = False + + @api.depends('company_id') + def _compute_address_id(self): + for employee in self: + address = employee.company_id.partner_id.address_get(['default']) + employee.address_id = address['default'] if address else False + + @api.depends('department_id') + def _compute_parent_id(self): + for employee in self.filtered('department_id.manager_id'): + employee.parent_id = employee.department_id.manager_id + + @api.depends('resource_calendar_id', 'hr_presence_state') + def _compute_presence_icon(self): + """ + This method compute the state defining the display icon in the kanban view. + It can be overriden to add other possibilities, like time off or attendances recordings. + """ + working_now_list = self.filtered(lambda e: e.hr_presence_state == 'present')._get_employee_working_now() + for employee in self: + if employee.hr_presence_state == 'present': + if employee.id in working_now_list: + icon = 'presence_present' + else: + icon = 'presence_absent_active' + elif employee.hr_presence_state == 'absent': + # employee is not in the working_now_list and he has a user_id + icon = 'presence_absent' + else: + # without attendance, default employee state is 'to_define' without confirmed presence/absence + # we need to check why they are not there + if employee.user_id: + # Display an orange icon on internal users. + icon = 'presence_to_define' + else: + # We don't want non-user employee to have icon. + icon = 'presence_undetermined' + employee.hr_icon_display = icon + + @api.model + def _get_employee_working_now(self): + working_now = [] + # We loop over all the employee tz and the resource calendar_id to detect working hours in batch. + all_employee_tz = set(self.mapped('tz')) + for tz in all_employee_tz: + employee_ids = self.filtered(lambda e: e.tz == tz) + resource_calendar_ids = employee_ids.mapped('resource_calendar_id') + for calendar_id in resource_calendar_ids: + res_employee_ids = employee_ids.filtered(lambda e: e.resource_calendar_id.id == calendar_id.id) + start_dt = fields.Datetime.now() + stop_dt = start_dt + timedelta(hours=1) + from_datetime = utc.localize(start_dt).astimezone(timezone(tz or 'UTC')) + to_datetime = utc.localize(stop_dt).astimezone(timezone(tz or 'UTC')) + # Getting work interval of the first is working. Functions called on resource_calendar_id + # are waiting for singleton + work_interval = res_employee_ids[0].resource_calendar_id._work_intervals(from_datetime, to_datetime) + # Employee that is not supposed to work have empty items. + if len(work_interval._items) > 0: + # The employees should be working now according to their work schedule + working_now += res_employee_ids.ids + return working_now + diff --git a/addons/hr/models/hr_employee_category.py b/addons/hr/models/hr_employee_category.py new file mode 100644 index 00000000..41f8bedf --- /dev/null +++ b/addons/hr/models/hr_employee_category.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from random import randint + +from odoo import fields, models + + +class EmployeeCategory(models.Model): + + _name = "hr.employee.category" + _description = "Employee Category" + + def _get_default_color(self): + return randint(1, 11) + + name = fields.Char(string="Tag Name", required=True) + color = fields.Integer(string='Color Index', default=_get_default_color) + 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 00000000..6ac8091f --- /dev/null +++ b/addons/hr/models/hr_employee_public.py @@ -0,0 +1,65 @@ +# -*- 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) + tz = fields.Selection(readonly=True) + color = fields.Integer(readonly=True) + + # hr.employee.public specific fields + child_ids = fields.One2many('hr.employee.public', 'parent_id', string='Direct subordinates', readonly=True) + image_1920 = fields.Image("Original Image", compute='_compute_image', compute_sudo=True) + image_1024 = fields.Image("Image 1024", compute='_compute_image', compute_sudo=True) + image_512 = fields.Image("Image 512", compute='_compute_image', compute_sudo=True) + image_256 = fields.Image("Image 256", compute='_compute_image', compute_sudo=True) + image_128 = fields.Image("Image 128", compute='_compute_image', compute_sudo=True) + parent_id = fields.Many2one('hr.employee.public', 'Manager', readonly=True) + coach_id = fields.Many2one('hr.employee.public', 'Coach', readonly=True) + user_partner_id = fields.Many2one(related='user_id.partner_id', related_sudo=False, string="User's partner") + + 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_1920 = employee_id.image_1920 + employee.image_1024 = employee_id.image_1024 + employee.image_512 = employee_id.image_512 + employee.image_256 = employee_id.image_256 + employee.image_128 = employee_id.image_128 + + @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']) + + 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 00000000..51271f41 --- /dev/null +++ b/addons/hr/models/hr_job.py @@ -0,0 +1,68 @@ +# -*- 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', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + 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.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) + + 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 + + def set_open(self): + return self.write({ + 'state': 'open', + 'no_of_recruitment': 0, + 'no_of_hired_employee': 0 + }) diff --git a/addons/hr/models/hr_plan.py b/addons/hr/models/hr_plan.py new file mode 100644 index 00000000..47b4ecc9 --- /dev/null +++ b/addons/hr/models/hr_plan.py @@ -0,0 +1,64 @@ +# -*- 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 UserError + + +class HrPlanActivityType(models.Model): + _name = 'hr.plan.activity.type' + _description = 'Plan activity type' + _rec_name = 'summary' + + activity_type_id = fields.Many2one( + 'mail.activity.type', 'Activity Type', + default=lambda self: self.env.ref('mail.mail_activity_data_todo'), + domain=lambda self: ['|', ('res_model_id', '=', False), ('res_model_id', '=', self.env['ir.model']._get('hr.employee').id)], + ondelete='restrict' + ) + summary = fields.Char('Summary', compute="_compute_default_summary", store=True, readonly=False) + responsible = fields.Selection([ + ('coach', 'Coach'), + ('manager', 'Manager'), + ('employee', 'Employee'), + ('other', 'Other')], default='employee', string='Responsible', required=True) + responsible_id = fields.Many2one('res.users', 'Responsible Person', help='Specific responsible of activity if not linked to the employee.') + note = fields.Html('Note') + + @api.depends('activity_type_id') + def _compute_default_summary(self): + for plan_type in self: + if not plan_type.summary and plan_type.activity_type_id and plan_type.activity_type_id.summary: + plan_type.summary = plan_type.activity_type_id.summary + + def get_responsible_id(self, employee): + if self.responsible == 'coach': + if not employee.coach_id: + raise UserError(_('Coach of employee %s is not set.', employee.name)) + responsible = employee.coach_id.user_id + if not responsible: + raise UserError(_('User of coach of employee %s is not set.', employee.name)) + elif self.responsible == 'manager': + if not employee.parent_id: + raise UserError(_('Manager of employee %s is not set.', employee.name)) + responsible = employee.parent_id.user_id + if not responsible: + raise UserError(_('User of manager of employee %s is not set.', employee.name)) + elif self.responsible == 'employee': + responsible = employee.user_id + if not responsible: + raise UserError(_('User linked to employee %s is required.', employee.name)) + elif self.responsible == 'other': + responsible = self.responsible_id + if not responsible: + raise UserError(_('No specific user given on activity %s.', self.activity_type_id.name)) + return responsible + + +class HrPlan(models.Model): + _name = 'hr.plan' + _description = 'plan' + + name = fields.Char('Name', required=True) + plan_activity_type_ids = fields.Many2many('hr.plan.activity.type', string='Activities') + active = fields.Boolean(default=True) diff --git a/addons/hr/models/mail_alias.py b/addons/hr/models/mail_alias.py new file mode 100644 index 00000000..69042cfb --- /dev/null +++ b/addons/hr/models/mail_alias.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, _ + + +class Alias(models.Model): + _inherit = 'mail.alias' + + alias_contact = fields.Selection(selection_add=[ + ('employees', 'Authenticated Employees'), + ], ondelete={'employees': 'cascade'}) + + def _get_alias_bounced_body_fallback(self, message_dict): + if self.alias_contact == 'employees': + return _("""Hi,<br/> +Your document has not been created because your email address is not recognized.<br/> +Please send emails with the email address recorded on your employee information, or contact your HR manager.""") + return super(Alias, self)._get_alias_bounced_body_fallback(message_dict) diff --git a/addons/hr/models/mail_channel.py b/addons/hr/models/mail_channel.py new file mode 100644 index 00000000..5ea45fe3 --- /dev/null +++ b/addons/hr/models/mail_channel.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class Channel(models.Model): + _inherit = 'mail.channel' + + subscription_department_ids = fields.Many2many( + 'hr.department', string='HR Departments', + help='Automatically subscribe members of those departments to the channel.') + + def _subscribe_users(self): + """ Auto-subscribe members of a department to a channel """ + super(Channel, self)._subscribe_users() + for mail_channel in self: + if mail_channel.subscription_department_ids: + mail_channel.write( + {'channel_partner_ids': + [(4, partner_id) for partner_id in mail_channel.mapped('subscription_department_ids.member_ids.user_id.partner_id').ids]}) + + def write(self, vals): + res = super(Channel, self).write(vals) + if vals.get('subscription_department_ids'): + self._subscribe_users() + return res diff --git a/addons/hr/models/models.py b/addons/hr/models/models.py new file mode 100644 index 00000000..fcad6611 --- /dev/null +++ b/addons/hr/models/models.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, tools, _ + + +class BaseModel(models.AbstractModel): + _inherit = 'base' + + def _alias_get_error_message(self, message, message_dict, alias): + if alias.alias_contact == 'employees': + email_from = tools.decode_message_header(message, 'From') + email_address = tools.email_split(email_from)[0] + employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1) + if not employee: + employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1) + if not employee: + return _('restricted to employees') + return False + return super(BaseModel, self)._alias_get_error_message(message, message_dict, alias) diff --git a/addons/hr/models/res_company.py b/addons/hr/models/res_company.py new file mode 100644 index 00000000..cd1937d6 --- /dev/null +++ b/addons/hr/models/res_company.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 Company(models.Model): + _inherit = 'res.company' + + hr_presence_control_email_amount = fields.Integer(string="# emails to send") + hr_presence_control_ip_list = fields.Char(string="Valid IP addresses") diff --git a/addons/hr/models/res_config_settings.py b/addons/hr/models/res_config_settings.py new file mode 100644 index 00000000..80c972fa --- /dev/null +++ b/addons/hr/models/res_config_settings.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + resource_calendar_id = fields.Many2one( + 'resource.calendar', 'Company Working Hours', + related='company_id.resource_calendar_id', readonly=False) + module_hr_presence = fields.Boolean(string="Advanced Presence Control") + module_hr_skills = fields.Boolean(string="Skills Management") + hr_presence_control_login = fields.Boolean(string="Based on user status in system", config_parameter='hr.hr_presence_control_login') + hr_presence_control_email = fields.Boolean(string="Based on number of emails sent", config_parameter='hr_presence.hr_presence_control_email') + hr_presence_control_ip = fields.Boolean(string="Based on IP Address", config_parameter='hr_presence.hr_presence_control_ip') + module_hr_attendance = fields.Boolean(string="Based on attendances") + hr_presence_control_email_amount = fields.Integer(related="company_id.hr_presence_control_email_amount", readonly=False) + hr_presence_control_ip_list = fields.Char(related="company_id.hr_presence_control_ip_list", readonly=False) + hr_employee_self_edit = fields.Boolean(string="Employee Editing", config_parameter='hr.hr_employee_self_edit') diff --git a/addons/hr/models/res_partner.py b/addons/hr/models/res_partner.py new file mode 100644 index 00000000..e51f0069 --- /dev/null +++ b/addons/hr/models/res_partner.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models +from odoo.exceptions import AccessError + + +class Partner(models.Model): + + _inherit = ['res.partner'] + + def name_get(self): + """ Override to allow an employee to see its private address in his profile. + This avoids to relax access rules on `res.parter` and to add an `ir.rule`. + (advantage in both security and performance). + Use a try/except instead of systematically checking to minimize the impact on performance. + """ + try: + return super(Partner, self).name_get() + except AccessError as e: + if len(self) == 1 and self in self.env.user.employee_ids.mapped('address_home_id'): + return super(Partner, self.sudo()).name_get() + raise e diff --git a/addons/hr/models/res_users.py b/addons/hr/models/res_users.py new file mode 100644 index 00000000..91eaeadf --- /dev/null +++ b/addons/hr/models/res_users.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields, _, SUPERUSER_ID +from odoo.exceptions import AccessError + + +class User(models.Model): + _inherit = ['res.users'] + + # note: a user can only be linked to one employee per company (see sql constraint in ´hr.employee´) + employee_ids = fields.One2many('hr.employee', 'user_id', string='Related employee') + employee_id = fields.Many2one('hr.employee', string="Company employee", + compute='_compute_company_employee', search='_search_company_employee', store=False) + + job_title = fields.Char(related='employee_id.job_title', readonly=False, related_sudo=False) + work_phone = fields.Char(related='employee_id.work_phone', readonly=False, related_sudo=False) + mobile_phone = fields.Char(related='employee_id.mobile_phone', readonly=False, related_sudo=False) + employee_phone = fields.Char(related='employee_id.phone', readonly=False, related_sudo=False) + work_email = fields.Char(related='employee_id.work_email', readonly=False, related_sudo=False) + category_ids = fields.Many2many(related='employee_id.category_ids', string="Employee Tags", readonly=False, related_sudo=False) + department_id = fields.Many2one(related='employee_id.department_id', readonly=False, related_sudo=False) + address_id = fields.Many2one(related='employee_id.address_id', readonly=False, related_sudo=False) + work_location = fields.Char(related='employee_id.work_location', readonly=False, related_sudo=False) + employee_parent_id = fields.Many2one(related='employee_id.parent_id', readonly=False, related_sudo=False) + coach_id = fields.Many2one(related='employee_id.coach_id', readonly=False, related_sudo=False) + address_home_id = fields.Many2one(related='employee_id.address_home_id', readonly=False, related_sudo=False) + is_address_home_a_company = fields.Boolean(related='employee_id.is_address_home_a_company', readonly=False, related_sudo=False) + private_email = fields.Char(related='address_home_id.email', string="Private Email", readonly=False) + km_home_work = fields.Integer(related='employee_id.km_home_work', readonly=False, related_sudo=False) + # res.users already have a field bank_account_id and country_id from the res.partner inheritance: don't redefine them + employee_bank_account_id = fields.Many2one(related='employee_id.bank_account_id', string="Employee's Bank Account Number", related_sudo=False, readonly=False) + employee_country_id = fields.Many2one(related='employee_id.country_id', string="Employee's Country", readonly=False, related_sudo=False) + identification_id = fields.Char(related='employee_id.identification_id', readonly=False, related_sudo=False) + passport_id = fields.Char(related='employee_id.passport_id', readonly=False, related_sudo=False) + gender = fields.Selection(related='employee_id.gender', readonly=False, related_sudo=False) + birthday = fields.Date(related='employee_id.birthday', readonly=False, related_sudo=False) + place_of_birth = fields.Char(related='employee_id.place_of_birth', readonly=False, related_sudo=False) + country_of_birth = fields.Many2one(related='employee_id.country_of_birth', readonly=False, related_sudo=False) + marital = fields.Selection(related='employee_id.marital', readonly=False, related_sudo=False) + spouse_complete_name = fields.Char(related='employee_id.spouse_complete_name', readonly=False, related_sudo=False) + spouse_birthdate = fields.Date(related='employee_id.spouse_birthdate', readonly=False, related_sudo=False) + children = fields.Integer(related='employee_id.children', readonly=False, related_sudo=False) + emergency_contact = fields.Char(related='employee_id.emergency_contact', readonly=False, related_sudo=False) + emergency_phone = fields.Char(related='employee_id.emergency_phone', readonly=False, related_sudo=False) + visa_no = fields.Char(related='employee_id.visa_no', readonly=False, related_sudo=False) + permit_no = fields.Char(related='employee_id.permit_no', readonly=False, related_sudo=False) + visa_expire = fields.Date(related='employee_id.visa_expire', readonly=False, related_sudo=False) + additional_note = fields.Text(related='employee_id.additional_note', readonly=False, related_sudo=False) + barcode = fields.Char(related='employee_id.barcode', readonly=False, related_sudo=False) + pin = fields.Char(related='employee_id.pin', readonly=False, related_sudo=False) + certificate = fields.Selection(related='employee_id.certificate', readonly=False, related_sudo=False) + study_field = fields.Char(related='employee_id.study_field', readonly=False, related_sudo=False) + study_school = fields.Char(related='employee_id.study_school', readonly=False, related_sudo=False) + employee_count = fields.Integer(compute='_compute_employee_count') + hr_presence_state = fields.Selection(related='employee_id.hr_presence_state') + last_activity = fields.Date(related='employee_id.last_activity') + last_activity_time = fields.Char(related='employee_id.last_activity_time') + + can_edit = fields.Boolean(compute='_compute_can_edit') + + def _compute_can_edit(self): + can_edit = self.env['ir.config_parameter'].sudo().get_param('hr.hr_employee_self_edit') or self.env.user.has_group('hr.group_hr_user') + for user in self: + user.can_edit = can_edit + + @api.depends('employee_ids') + def _compute_employee_count(self): + for user in self.with_context(active_test=False): + user.employee_count = len(user.employee_ids) + + def __init__(self, pool, cr): + """ Override of __init__ to add access rights. + Access rights are disabled by default, but allowed + on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS. + """ + hr_readable_fields = [ + 'active', + 'child_ids', + 'employee_id', + 'employee_ids', + 'employee_parent_id', + 'hr_presence_state', + 'last_activity', + 'last_activity_time', + 'can_edit', + ] + + hr_writable_fields = [ + 'additional_note', + 'address_home_id', + 'address_id', + 'barcode', + 'birthday', + 'category_ids', + 'children', + 'coach_id', + 'country_of_birth', + 'department_id', + 'display_name', + 'emergency_contact', + 'emergency_phone', + 'employee_bank_account_id', + 'employee_country_id', + 'gender', + 'identification_id', + 'is_address_home_a_company', + 'job_title', + 'private_email', + 'km_home_work', + 'marital', + 'mobile_phone', + 'notes', + 'employee_parent_id', + 'passport_id', + 'permit_no', + 'employee_phone', + 'pin', + 'place_of_birth', + 'spouse_birthdate', + 'spouse_complete_name', + 'visa_expire', + 'visa_no', + 'work_email', + 'work_location', + 'work_phone', + 'certificate', + 'study_field', + 'study_school', + ] + + init_res = super(User, self).__init__(pool, cr) + # duplicate list to avoid modifying the original reference + type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + hr_readable_fields + hr_writable_fields + type(self).SELF_WRITEABLE_FIELDS = type(self).SELF_WRITEABLE_FIELDS + hr_writable_fields + return init_res + + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + # When the front-end loads the views it gets the list of available fields + # for the user (according to its access rights). Later, when the front-end wants to + # populate the view with data, it only asks to read those available fields. + # However, in this case, we want the user to be able to read/write its own data, + # even if they are protected by groups. + # We make the front-end aware of those fields by sending all field definitions. + # Note: limit the `sudo` to the only action of "editing own profile" action in order to + # avoid breaking `groups` mecanism on res.users form view. + profile_view = self.env.ref("hr.res_users_view_form_profile") + if profile_view and view_id == profile_view.id: + self = self.with_user(SUPERUSER_ID) + return super(User, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + + def write(self, vals): + """ + Synchronize user and its related employee + and check access rights if employees are not allowed to update + their own data (otherwise sudo is applied for self data). + """ + hr_fields = { + field + for field_name, field in self._fields.items() + if field.related_field and field.related_field.model_name == 'hr.employee' and field_name in vals + } + can_edit_self = self.env['ir.config_parameter'].sudo().get_param('hr.hr_employee_self_edit') or self.env.user.has_group('hr.group_hr_user') + if hr_fields and not can_edit_self: + # Raise meaningful error message + raise AccessError(_("You are only allowed to update your preferences. Please contact a HR officer to update other information.")) + + result = super(User, self).write(vals) + + employee_values = {} + for fname in [f for f in ['name', 'email', 'image_1920', 'tz'] if f in vals]: + employee_values[fname] = vals[fname] + if employee_values: + if 'email' in employee_values: + employee_values['work_email'] = employee_values.pop('email') + if 'image_1920' in vals: + without_image = self.env['hr.employee'].sudo().search([('user_id', 'in', self.ids), ('image_1920', '=', False)]) + with_image = self.env['hr.employee'].sudo().search([('user_id', 'in', self.ids), ('image_1920', '!=', False)]) + without_image.write(employee_values) + if not can_edit_self: + employee_values.pop('image_1920') + with_image.write(employee_values) + else: + self.env['hr.employee'].sudo().search([('user_id', 'in', self.ids)]).write(employee_values) + return result + + @api.model + def action_get(self): + if self.env.user.employee_id: + return self.env['ir.actions.act_window']._for_xml_id('hr.res_users_action_my') + return super(User, self).action_get() + + @api.depends('employee_ids') + @api.depends_context('company') + def _compute_company_employee(self): + for user in self: + user.employee_id = self.env['hr.employee'].search([('id', 'in', user.employee_ids.ids), ('company_id', '=', self.env.company.id)], limit=1) + + def _search_company_employee(self, operator, value): + return [('employee_ids', operator, value)] + + def action_create_employee(self): + self.ensure_one() + self.env['hr.employee'].create(dict( + name=self.name, + company_id=self.env.company.id, + **self.env['hr.employee']._sync_user(self) + )) diff --git a/addons/hr/models/resource.py b/addons/hr/models/resource.py new file mode 100644 index 00000000..b59767f6 --- /dev/null +++ b/addons/hr/models/resource.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResourceResource(models.Model): + _inherit = "resource.resource" + + user_id = fields.Many2one(copy=False) |
