summaryrefslogtreecommitdiff
path: root/addons/hr/models/hr_employee.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/hr/models/hr_employee.py')
-rw-r--r--addons/hr/models/hr_employee.py343
1 files changed, 343 insertions, 0 deletions
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']