summaryrefslogtreecommitdiff
path: root/addons/hr/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/hr/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/hr/models')
-rw-r--r--addons/hr/models/__init__.py18
-rw-r--r--addons/hr/models/hr_department.py86
-rw-r--r--addons/hr/models/hr_employee.py343
-rw-r--r--addons/hr/models/hr_employee_base.py179
-rw-r--r--addons/hr/models/hr_employee_category.py23
-rw-r--r--addons/hr/models/hr_employee_public.py65
-rw-r--r--addons/hr/models/hr_job.py68
-rw-r--r--addons/hr/models/hr_plan.py64
-rw-r--r--addons/hr/models/mail_alias.py19
-rw-r--r--addons/hr/models/mail_channel.py27
-rw-r--r--addons/hr/models/models.py20
-rw-r--r--addons/hr/models/res_company.py11
-rw-r--r--addons/hr/models/res_config_settings.py20
-rw-r--r--addons/hr/models/res_partner.py23
-rw-r--r--addons/hr/models/res_users.py209
-rw-r--r--addons/hr/models/resource.py10
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)