mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-23 21:52:04 +02:00
19.0 vanilla
This commit is contained in:
parent
a1137a1456
commit
e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions
|
|
@ -1,23 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_payroll_structure_type
|
||||
from . import hr_job
|
||||
from . import hr_version
|
||||
from . import hr_contract_type
|
||||
from . import hr_employee_base
|
||||
from . import hr_employee
|
||||
from . import hr_mixin
|
||||
from . import hr_employee_category
|
||||
from . import hr_employee_public
|
||||
from . import hr_department
|
||||
from . import hr_departure_reason
|
||||
from . import hr_job
|
||||
from . import hr_plan
|
||||
from . import hr_plan_activity_type
|
||||
from . import hr_work_location
|
||||
from . import mail_activity_plan
|
||||
from . import mail_activity_plan_template
|
||||
from . import mail_alias
|
||||
from . import mail_channel
|
||||
from . import discuss_channel
|
||||
from . import models
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
from . import res_company
|
||||
from . import res_partner
|
||||
from . import res_partner_bank
|
||||
from . import resource
|
||||
from . import resource_calendar
|
||||
from . import resource_calendar_leaves
|
||||
from . import ir_ui_menu
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ from odoo import _, api, fields, models
|
|||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
_inherit = 'mail.channel'
|
||||
class DiscussChannel(models.Model):
|
||||
_inherit = 'discuss.channel'
|
||||
|
||||
subscription_department_ids = fields.Many2many(
|
||||
'hr.department', string='HR Departments',
|
||||
|
|
@ -20,7 +20,7 @@ class Channel(models.Model):
|
|||
|
||||
def _subscribe_users_automatically_get_members(self):
|
||||
""" Auto-subscribe members of a department to a channel """
|
||||
new_members = super(Channel, self)._subscribe_users_automatically_get_members()
|
||||
new_members = super()._subscribe_users_automatically_get_members()
|
||||
for channel in self:
|
||||
new_members[channel.id] = list(
|
||||
set(new_members[channel.id]) |
|
||||
|
|
@ -29,7 +29,7 @@ class Channel(models.Model):
|
|||
return new_members
|
||||
|
||||
def write(self, vals):
|
||||
res = super(Channel, self).write(vals)
|
||||
res = super().write(vals)
|
||||
if vals.get('subscription_department_ids'):
|
||||
self._subscribe_users_automatically()
|
||||
return res
|
||||
|
|
@ -1,13 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ContractType(models.Model):
|
||||
class HrContractType(models.Model):
|
||||
_name = 'hr.contract.type'
|
||||
_description = 'Contract Type'
|
||||
_order = 'sequence'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(compute='_compute_code', store=True, readonly=False)
|
||||
sequence = fields.Integer()
|
||||
country_id = fields.Many2one('res.country', domain=lambda self: [('id', 'in', self.env.companies.country_id.ids)])
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_code(self):
|
||||
for contract_type in self:
|
||||
if contract_type.code:
|
||||
continue
|
||||
contract_type.code = contract_type.name
|
||||
|
|
|
|||
|
|
@ -1,44 +1,82 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
import re
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.fields import Domain
|
||||
|
||||
|
||||
class Department(models.Model):
|
||||
_name = "hr.department"
|
||||
class HrDepartment(models.Model):
|
||||
_name = 'hr.department'
|
||||
_description = "Department"
|
||||
_inherit = ['mail.thread']
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = "name"
|
||||
_rec_name = 'complete_name'
|
||||
_parent_store = True
|
||||
|
||||
name = fields.Char('Department Name', required=True)
|
||||
complete_name = fields.Char('Complete Name', compute='_compute_complete_name', recursive=True, store=True)
|
||||
name = fields.Char('Department Name', required=True, translate=True)
|
||||
complete_name = fields.Char('Complete Name', compute='_compute_complete_name', recursive=True, search='_search_complete_name')
|
||||
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)]")
|
||||
company_id = fields.Many2one('res.company', string='Company', compute="_compute_company_id", store=True, recursive=True, index=True, readonly=False, tracking=True, default=lambda self: self.env.company)
|
||||
parent_id = fields.Many2one('hr.department', string='Parent Department', index=True, check_company=True)
|
||||
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)]")
|
||||
manager_id = fields.Many2one('hr.employee', string='Manager', tracking=True, domain="['|', ('company_id', '=', False), ('company_id', 'in', allowed_company_ids)]")
|
||||
member_ids = fields.One2many('hr.employee', 'department_id', string='Members', readonly=True)
|
||||
total_employee = fields.Integer(compute='_compute_total_employee', string='Total Employee')
|
||||
has_read_access = fields.Boolean(search="_search_has_read_access", store=False, export_string_translation=False)
|
||||
total_employee = fields.Integer(compute='_compute_total_employee', string='Total Employee',
|
||||
export_string_translation=False)
|
||||
jobs_ids = fields.One2many('hr.job', 'department_id', string='Jobs')
|
||||
plan_ids = fields.One2many('hr.plan', 'department_id')
|
||||
plan_ids = fields.One2many('mail.activity.plan', 'department_id')
|
||||
plans_count = fields.Integer(compute='_compute_plan_count')
|
||||
note = fields.Text('Note')
|
||||
color = fields.Integer('Color Index')
|
||||
parent_path = fields.Char(index=True, unaccent=False)
|
||||
parent_path = fields.Char(index=True)
|
||||
master_department_id = fields.Many2one(
|
||||
'hr.department', 'Master Department', compute='_compute_master_department_id', store=True)
|
||||
|
||||
def name_get(self):
|
||||
if not self.env.context.get('hierarchical_naming', True):
|
||||
return [(record.id, record.name) for record in self]
|
||||
return super(Department, self).name_get()
|
||||
@api.depends_context('hierarchical_naming')
|
||||
def _compute_display_name(self):
|
||||
if self.env.context.get('hierarchical_naming', True):
|
||||
return super()._compute_display_name()
|
||||
for record in self:
|
||||
record.display_name = record.name
|
||||
|
||||
def _search_has_read_access(self, operator, value):
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
if self.env['hr.employee'].has_access('read'):
|
||||
return [(1, "=", 1)]
|
||||
departments_ids = self.env['hr.department'].sudo().search([('manager_id', 'in', self.env.user.employee_ids.ids)]).ids
|
||||
return [('id', 'child_of', departments_ids)]
|
||||
|
||||
def _search_complete_name(self, operator, value):
|
||||
supported_operators = ["=", "!=", "ilike", "not ilike", "in", "not in", "=ilike"]
|
||||
if operator not in supported_operators or not isinstance(value, (str, list)):
|
||||
raise NotImplementedError(_('Operation not Supported.'))
|
||||
department = self.env['hr.department'].search([])
|
||||
if operator == '=':
|
||||
department = department.filtered(lambda m: m.complete_name == value)
|
||||
elif operator == '!=':
|
||||
department = department.filtered(lambda m: m.complete_name != value)
|
||||
elif operator == 'ilike':
|
||||
department = department.filtered(lambda m: value.lower() in m.complete_name.lower())
|
||||
elif operator == 'not ilike':
|
||||
department = department.filtered(lambda m: value.lower() not in m.complete_name.lower())
|
||||
elif operator == 'in':
|
||||
department = department.filtered(lambda m: m.complete_name in value)
|
||||
elif operator == 'not in':
|
||||
department = department.filtered(lambda m: m.complete_name not in value)
|
||||
elif operator == '=ilike':
|
||||
pattern = re.compile(re.escape(value).replace('%', '.*').replace('_', '.'), flags=re.IGNORECASE)
|
||||
department = department.filtered(lambda m: pattern.fullmatch(m.complete_name))
|
||||
return [('id', 'in', department.ids)]
|
||||
|
||||
@api.model
|
||||
def name_create(self, name):
|
||||
return self.create({'name': name}).name_get()[0]
|
||||
record = self.create({'name': name})
|
||||
return record.id, record.display_name
|
||||
|
||||
@api.depends('name', 'parent_id.complete_name')
|
||||
def _compute_complete_name(self):
|
||||
|
|
@ -54,51 +92,49 @@ class Department(models.Model):
|
|||
department.master_department_id = int(department.parent_path.split('/')[0])
|
||||
|
||||
def _compute_total_employee(self):
|
||||
emp_data = self.env['hr.employee']._read_group([('department_id', 'in', self.ids)], ['department_id'], ['department_id'])
|
||||
result = dict((data['department_id'][0], data['department_id_count']) for data in emp_data)
|
||||
emp_data = self.env['hr.employee'].sudo()._read_group([('department_id', 'in', self.ids), ('company_id', 'in', self.env.companies.ids)], ['department_id'], ['__count'])
|
||||
result = {department.id: count for department, count in emp_data}
|
||||
for department in self:
|
||||
department.total_employee = result.get(department.id, 0)
|
||||
|
||||
def _compute_plan_count(self):
|
||||
plans_data = self.env['hr.plan']._read_group([('department_id', 'in', self.ids)], ['department_id'], ['department_id'])
|
||||
plans_count = {x['department_id'][0]: x['department_id_count'] for x in plans_data}
|
||||
plans_data = self.env['mail.activity.plan']._read_group(
|
||||
domain=[
|
||||
'|',
|
||||
('department_id', '=', False),
|
||||
('department_id', 'in', self.ids),
|
||||
('company_id', 'in', self.env.companies.ids + [False])
|
||||
],
|
||||
groupby=['department_id'],
|
||||
aggregates=['__count'],
|
||||
)
|
||||
plans_count = {department.id: count for department, count in plans_data}
|
||||
for department in self:
|
||||
department.plans_count = plans_count.get(department.id, 0)
|
||||
department.plans_count = plans_count.get(department.id, 0) + plans_count.get(False, 0)
|
||||
|
||||
@api.constrains('parent_id')
|
||||
def _check_parent_id(self):
|
||||
if not self._check_recursion():
|
||||
if self._has_cycle():
|
||||
raise ValidationError(_('You cannot create recursive departments.'))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# 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.
|
||||
departments = super(Department, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
|
||||
for department, vals in zip(departments, vals_list):
|
||||
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 departments
|
||||
return super(HrDepartment, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
|
||||
|
||||
@api.depends('parent_id', 'parent_id.company_id')
|
||||
def _compute_company_id(self):
|
||||
if self.parent_id and self.parent_id.company_id:
|
||||
self.company_id = self.parent_id.company_id
|
||||
|
||||
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)
|
||||
return super().write(vals)
|
||||
|
||||
def _update_employee_manager(self, manager_id):
|
||||
employees = self.env['hr.employee']
|
||||
|
|
@ -112,12 +148,11 @@ class Department(models.Model):
|
|||
|
||||
def get_formview_action(self, access_uid=None):
|
||||
res = super().get_formview_action(access_uid=access_uid)
|
||||
if (not self.user_has_groups('hr.group_hr_user') and
|
||||
if (not self.env.user.has_group('hr.group_hr_user') and
|
||||
self.env.context.get('open_employees_kanban', False)):
|
||||
res.update({
|
||||
'name': self.name,
|
||||
'res_model': 'hr.employee.public',
|
||||
'view_type': 'kanban',
|
||||
'view_mode': 'kanban',
|
||||
'views': [(False, 'kanban'), (False, 'form')],
|
||||
'context': {'searchpanel_default_department_id': self.id},
|
||||
|
|
@ -126,9 +161,82 @@ class Department(models.Model):
|
|||
return res
|
||||
|
||||
def action_plan_from_department(self):
|
||||
action = self.env['ir.actions.actions']._for_xml_id('hr.hr_plan_action')
|
||||
action['context'] = {'default_department_id': self.id, 'search_default_department_id': self.id}
|
||||
action = self.env['ir.actions.actions']._for_xml_id('hr.mail_activity_plan_action')
|
||||
action['context'] = dict(ast.literal_eval(action.get('context')), default_department_id=self.id)
|
||||
domain = [
|
||||
'|',
|
||||
('department_id', '=', False),
|
||||
('department_id', 'in', self.ids),
|
||||
]
|
||||
if 'domain' in action:
|
||||
allowed_company_ids = self.env.context.get('allowed_company_ids', [])
|
||||
action['domain'] = Domain.AND([
|
||||
ast.literal_eval(action['domain'].replace('allowed_company_ids', str(allowed_company_ids))), domain
|
||||
])
|
||||
else:
|
||||
action['domain'] = domain
|
||||
if self.plans_count == 0:
|
||||
action['views'] = [(False, 'form')]
|
||||
return action
|
||||
|
||||
def action_employee_from_department(self):
|
||||
if self.env['hr.employee'].has_access('read'):
|
||||
res_model = "hr.employee"
|
||||
search_view_id = self.env.ref('hr.view_employee_filter').id
|
||||
else:
|
||||
res_model = "hr.employee.public"
|
||||
search_view_id = self.env.ref('hr.hr_employee_public_view_search').id
|
||||
return {
|
||||
'name': _("Employees"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': res_model,
|
||||
'view_mode': 'list,kanban,form',
|
||||
'views': [(False, 'list'), (False, 'kanban'), (False, 'form')],
|
||||
'search_view_id': [search_view_id, 'search'],
|
||||
'context': {
|
||||
'searchpanel_default_department_id': self.id,
|
||||
'default_department_id': self.id,
|
||||
'search_default_group_department': 1,
|
||||
'search_default_department_id': self.id,
|
||||
'expand': 1
|
||||
},
|
||||
}
|
||||
|
||||
def get_children_department_ids(self):
|
||||
return self.env['hr.department'].search([('id', 'child_of', self.ids)])
|
||||
|
||||
def action_open_view_child_departments(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.department",
|
||||
"views": [[False, "kanban"], [False, "list"], [False, "form"]],
|
||||
"domain": [['id', 'in', self.get_children_department_ids().ids]],
|
||||
"name": "Child departments",
|
||||
}
|
||||
|
||||
def get_department_hierarchy(self):
|
||||
if not self:
|
||||
return {}
|
||||
|
||||
hierarchy = {
|
||||
'parent': {
|
||||
'id': self.parent_id.id,
|
||||
'name': self.parent_id.name,
|
||||
'employees': self.parent_id.total_employee,
|
||||
} if self.parent_id else False,
|
||||
'self': {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'employees': self.total_employee,
|
||||
},
|
||||
'children': [
|
||||
{
|
||||
'id': child.id,
|
||||
'name': child.name,
|
||||
'employees': child.total_employee
|
||||
} for child in self.child_ids
|
||||
]
|
||||
}
|
||||
|
||||
return hierarchy
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
# -*- 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 DepartureReason(models.Model):
|
||||
_name = "hr.departure.reason"
|
||||
class HrDepartureReason(models.Model):
|
||||
_name = 'hr.departure.reason'
|
||||
_description = "Departure Reason"
|
||||
_order = "sequence"
|
||||
|
||||
sequence = fields.Integer("Sequence", default=10)
|
||||
name = fields.Char(string="Reason", required=True, translate=True)
|
||||
# YTI TODO: Move reason_code to hr + adapt _unlink_except_default_departure_reasons
|
||||
# to use the codes instead of refs
|
||||
country_id = fields.Many2one('res.country', string='Country', default=lambda self: self.env.company.country_id)
|
||||
country_code = fields.Char(related='country_id.code')
|
||||
|
||||
@api.model
|
||||
def _get_default_departure_reasons(self):
|
||||
return {
|
||||
'fired': 342,
|
||||
'resigned': 343,
|
||||
'retired': 340,
|
||||
}
|
||||
return {self.env.ref(reason_ref) for reason_ref in (
|
||||
'hr.departure_fired',
|
||||
'hr.departure_resigned',
|
||||
'hr.departure_retired',
|
||||
)}
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_default_departure_reasons(self):
|
||||
master_reasons = [self.env.ref('hr.departure_fired', False), self.env.ref('hr.departure_resigned', False), self.env.ref('hr.departure_retired', False)]
|
||||
if any(reason in master_reasons for reason in self):
|
||||
master_departure_codes = self._get_default_departure_reasons()
|
||||
if any(reason in master_departure_codes for reason in self):
|
||||
raise UserError(_('Default departure reasons cannot be deleted.'))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,287 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from ast import literal_eval
|
||||
|
||||
from pytz import timezone, UTC, utc
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import clean_context, 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)]")
|
||||
member_of_department = fields.Boolean("Member of department", compute='_compute_part_of_department', search='_search_part_of_department',
|
||||
help="Whether the employee is a member of the active user's department or one of it's child department.")
|
||||
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', compute="_compute_work_contact_details", store=True, inverse='_inverse_work_contact_details')
|
||||
work_email = fields.Char('Work Email', compute="_compute_work_contact_details", store=True, inverse='_inverse_work_contact_details')
|
||||
work_contact_id = fields.Many2one('res.partner', 'Work Contact', copy=False)
|
||||
related_contact_ids = fields.Many2many('res.partner', string='Related Contacts', compute='_compute_related_contacts')
|
||||
related_contacts_count = fields.Integer('Number of related contacts', compute='_compute_related_contacts_count')
|
||||
work_location_id = fields.Many2one('hr.work.location', 'Work Location', compute="_compute_work_location_id", store=True, readonly=False,
|
||||
domain="[('address_id', '=', address_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
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')
|
||||
show_hr_icon_display = fields.Boolean(compute='_compute_presence_icon')
|
||||
employee_type = fields.Selection([
|
||||
('employee', 'Employee'),
|
||||
('student', 'Student'),
|
||||
('trainee', 'Trainee'),
|
||||
('contractor', 'Contractor'),
|
||||
('freelance', 'Freelancer'),
|
||||
], string='Employee Type', default='employee', required=True,
|
||||
help="The employee type. Although the primary purpose may seem to categorize employees, this field has also an impact in the Contract History. Only Employee type is supposed to be under contract and will have a Contract History.")
|
||||
|
||||
|
||||
def _get_valid_employee_for_user(self):
|
||||
user = self.env.user
|
||||
# retrieve the employee of the current active company for the user
|
||||
employee = user.employee_id
|
||||
if not employee:
|
||||
# search for all employees as superadmin to not get blocked by multi-company rules
|
||||
user_employees = user.employee_id.sudo().search([
|
||||
('user_id', '=', user.id)
|
||||
])
|
||||
# the default company employee is most likely the correct one, but fallback to the first if not available
|
||||
employee = user_employees.filtered(lambda r: r.company_id == user.company_id) or user_employees[:1]
|
||||
return employee
|
||||
|
||||
@api.depends_context('uid', 'company')
|
||||
@api.depends('department_id')
|
||||
def _compute_part_of_department(self):
|
||||
user_employee = self._get_valid_employee_for_user()
|
||||
active_department = user_employee.department_id
|
||||
if not active_department:
|
||||
self.member_of_department = False
|
||||
else:
|
||||
def get_all_children(department):
|
||||
children = department.child_ids
|
||||
if not children:
|
||||
return self.env['hr.department']
|
||||
return children + get_all_children(children)
|
||||
|
||||
child_departments = active_department + get_all_children(active_department)
|
||||
for employee in self:
|
||||
employee.member_of_department = employee.department_id in child_departments
|
||||
|
||||
def _search_part_of_department(self, operator, value):
|
||||
if operator not in ('=', '!=') or not isinstance(value, bool):
|
||||
raise UserError(_('Operation not supported'))
|
||||
|
||||
user_employee = self._get_valid_employee_for_user()
|
||||
# Double negation
|
||||
if not value:
|
||||
operator = '!=' if operator == '=' else '='
|
||||
if not user_employee.department_id:
|
||||
return [('id', operator, user_employee.id)]
|
||||
return (['!'] if operator == '!=' else []) + [('department_id', 'child_of', user_employee.department_id.id)]
|
||||
|
||||
@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 in ['online', 'leave_online']:
|
||||
state = 'present'
|
||||
elif employee.user_id.im_status in ['offline', 'leave_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_presence, 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('work_contact_id', 'work_contact_id.mobile', 'work_contact_id.email')
|
||||
def _compute_work_contact_details(self):
|
||||
for employee in self:
|
||||
if employee.work_contact_id:
|
||||
employee.mobile_phone = employee.work_contact_id.mobile
|
||||
employee.work_email = employee.work_contact_id.email
|
||||
|
||||
def _inverse_work_contact_details(self):
|
||||
for employee in self:
|
||||
if not employee.work_contact_id:
|
||||
employee.work_contact_id = self.env['res.partner'].sudo().with_context(clean_context(self._context)).create({
|
||||
'email': employee.work_email,
|
||||
'mobile': employee.mobile_phone,
|
||||
'name': employee.name,
|
||||
'image_1920': employee.image_1920,
|
||||
'company_id': employee.company_id.id
|
||||
})
|
||||
else:
|
||||
employee.work_contact_id.sudo().write({
|
||||
'email': employee.work_email,
|
||||
'mobile': employee.mobile_phone,
|
||||
})
|
||||
|
||||
@api.depends('work_contact_id')
|
||||
def _compute_related_contacts(self):
|
||||
for employee in self:
|
||||
employee.related_contact_ids = employee.work_contact_id
|
||||
|
||||
@api.depends('related_contact_ids')
|
||||
def _compute_related_contacts_count(self):
|
||||
for employee in self:
|
||||
employee.related_contacts_count = len(employee.related_contact_ids)
|
||||
|
||||
def action_related_contacts(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _("Related Contacts"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'kanban,tree,form',
|
||||
'res_model': 'res.partner',
|
||||
'domain': [('id', 'in', self.related_contact_ids.ids)]
|
||||
}
|
||||
|
||||
@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:
|
||||
show_icon = True
|
||||
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
|
||||
# Display an orange icon on internal users.
|
||||
icon = 'presence_to_define'
|
||||
if not employee.user_id:
|
||||
# We don't want non-user employee to have icon.
|
||||
show_icon = False
|
||||
employee.hr_icon_display = icon
|
||||
employee.show_hr_icon_display = show_icon
|
||||
|
||||
@api.depends('address_id')
|
||||
def _compute_work_location_id(self):
|
||||
to_reset = self.filtered(lambda e: e.address_id != e.work_location_id.address_id)
|
||||
to_reset.work_location_id = False
|
||||
|
||||
@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_batch(from_datetime, to_datetime)[False]
|
||||
# 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
|
||||
|
|
@ -6,9 +6,9 @@ from random import randint
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class EmployeeCategory(models.Model):
|
||||
class HrEmployeeCategory(models.Model):
|
||||
_name = 'hr.employee.category'
|
||||
|
||||
_name = "hr.employee.category"
|
||||
_description = "Employee Category"
|
||||
|
||||
def _get_default_color(self):
|
||||
|
|
@ -16,8 +16,9 @@ class EmployeeCategory(models.Model):
|
|||
|
||||
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')
|
||||
employee_ids = fields.Many2many('hr.employee', 'employee_category_rel', 'category_id', 'employee_id', string='Employees')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
_name_uniq = models.Constraint(
|
||||
'unique (name)',
|
||||
'Tag name already exists!',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,40 +1,63 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
from pytz import timezone, UTC
|
||||
|
||||
from odoo import api, fields, models, tools
|
||||
from odoo.tools import format_time
|
||||
|
||||
|
||||
class HrEmployeePublic(models.Model):
|
||||
_name = "hr.employee.public"
|
||||
_inherit = ["hr.employee.base"]
|
||||
_name = 'hr.employee.public'
|
||||
_description = 'Public Employee'
|
||||
_order = 'name'
|
||||
_auto = False
|
||||
_log_access = True # Include magic fields
|
||||
_log_access = True # Include magic fields
|
||||
|
||||
# Fields coming from hr.employee.base
|
||||
# Fields coming from hr.employee
|
||||
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)
|
||||
department_id = fields.Many2one('hr.department', readonly=True)
|
||||
member_of_department = fields.Boolean(compute='_compute_member_of_department', search='_search_part_of_department')
|
||||
job_id = fields.Many2one('hr.job', readonly=True)
|
||||
job_title = fields.Char(related='employee_id.job_title')
|
||||
company_id = fields.Many2one('res.company', readonly=True)
|
||||
address_id = fields.Many2one('res.partner', readonly=True)
|
||||
mobile_phone = fields.Char(readonly=True)
|
||||
work_phone = fields.Char(readonly=True)
|
||||
work_email = fields.Char(readonly=True)
|
||||
work_contact_id = fields.Many2one(readonly=True)
|
||||
related_contact_ids = fields.Many2many(readonly=True)
|
||||
work_location_id = fields.Many2one(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)
|
||||
share = fields.Boolean(related='employee_id.share')
|
||||
phone = fields.Char(related='employee_id.phone')
|
||||
im_status = fields.Char(related='employee_id.im_status')
|
||||
email = fields.Char(related='employee_id.email')
|
||||
work_contact_id = fields.Many2one('res.partner', readonly=True)
|
||||
work_location_id = fields.Many2one('hr.work.location', readonly=True)
|
||||
work_location_name = fields.Char(related='employee_id.work_location_name')
|
||||
work_location_type = fields.Selection(related='employee_id.work_location_type')
|
||||
user_id = fields.Many2one('res.users', readonly=True)
|
||||
resource_id = fields.Many2one('resource.resource', readonly=True)
|
||||
tz = fields.Selection(related='resource_id.tz')
|
||||
color = fields.Integer(readonly=True)
|
||||
employee_type = fields.Selection(readonly=True)
|
||||
hr_presence_state = fields.Selection([
|
||||
('present', 'Present'),
|
||||
('absent', 'Absent'),
|
||||
('archive', 'Archived'),
|
||||
('out_of_working_hour', 'Off-Hours')], compute='_compute_presence_state', default='out_of_working_hour')
|
||||
hr_icon_display = fields.Selection(
|
||||
selection='_get_selection_hr_icon_display',
|
||||
compute='_compute_presence_icon')
|
||||
show_hr_icon_display = fields.Boolean(compute='_compute_presence_icon')
|
||||
last_activity = fields.Date(compute="_compute_last_activity")
|
||||
last_activity_time = fields.Char(compute="_compute_last_activity")
|
||||
resource_calendar_id = fields.Many2one('resource.calendar', readonly=True)
|
||||
country_code = fields.Char(compute='_compute_country_code')
|
||||
|
||||
employee_id = fields.Many2one('hr.employee', 'Employee', compute="_compute_employee_id", search="_search_employee_id", compute_sudo=True)
|
||||
# Manager-only fields
|
||||
is_manager = fields.Boolean(compute='_compute_is_manager')
|
||||
is_user = fields.Boolean(compute='_compute_is_user')
|
||||
|
||||
employee_id = fields.Many2one('hr.employee', 'Employee', 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("Image", related='employee_id.image_1920', compute_sudo=True)
|
||||
|
|
@ -50,28 +73,133 @@ class HrEmployeePublic(models.Model):
|
|||
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")
|
||||
birthday_public_display_string = fields.Char("Public Date of Birth", related='employee_id.birthday_public_display_string')
|
||||
|
||||
def _search_employee_id(self, operator, value):
|
||||
return [('id', operator, value)]
|
||||
newly_hired = fields.Boolean('Newly Hired', compute='_compute_newly_hired', search='_search_newly_hired')
|
||||
|
||||
def _compute_employee_id(self):
|
||||
def _get_selection_hr_icon_display(self):
|
||||
return self.env['hr.employee']._fields['hr_icon_display']._description_selection(self.env)
|
||||
|
||||
def _compute_from_employee(self, field_names):
|
||||
if isinstance(field_names, str):
|
||||
field_names = [field_names]
|
||||
employees_sudo = self.sudo().env['hr.employee'].browse(self.ids)
|
||||
employee_per_id = {emp.id: emp for emp in employees_sudo}
|
||||
for public_employee in self:
|
||||
employee = employee_per_id[public_employee.id]
|
||||
for field_name in field_names:
|
||||
public_employee[field_name] = employee[field_name]
|
||||
|
||||
@api.depends('user_id')
|
||||
def _compute_last_activity(self):
|
||||
for employee in self:
|
||||
employee.employee_id = self.env['hr.employee'].browse(employee.id)
|
||||
tz = employee.tz
|
||||
# sudo: res.users - can access presence of accessible user
|
||||
if last_presence := employee.user_id.sudo().presence_ids.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_presence, time_format='short')
|
||||
else:
|
||||
employee.last_activity_time = False
|
||||
else:
|
||||
employee.last_activity = False
|
||||
employee.last_activity_time = False
|
||||
|
||||
@api.depends('user_partner_id')
|
||||
def _compute_related_contacts(self):
|
||||
super()._compute_related_contacts()
|
||||
def _compute_country_code(self):
|
||||
self._compute_from_employee('country_code')
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends('parent_id')
|
||||
def _compute_is_manager(self):
|
||||
all_reports = self.env['hr.employee.public'].search([('id', 'child_of', self.env.user.employee_id.id)]).ids
|
||||
for employee in self:
|
||||
employee.related_contact_ids |= employee.user_partner_id
|
||||
employee.is_manager = employee.id in all_reports
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_is_user(self):
|
||||
user_employee_id = self.env.user.employee_id.id
|
||||
for employee in self:
|
||||
employee.is_user = employee.id == user_employee_id
|
||||
|
||||
def _compute_presence_state(self):
|
||||
self._compute_from_employee('hr_presence_state')
|
||||
|
||||
def _compute_presence_icon(self):
|
||||
self._compute_from_employee('hr_icon_display')
|
||||
self._compute_from_employee('show_hr_icon_display')
|
||||
|
||||
def _compute_member_of_department(self):
|
||||
self._compute_from_employee('member_of_department')
|
||||
|
||||
def _get_manager_only_fields(self):
|
||||
return []
|
||||
|
||||
def _search_part_of_department(self, operator, value):
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
|
||||
user_employee = self._get_valid_employee_for_user()
|
||||
if not user_employee.department_id:
|
||||
return [('id', 'in', user_employee.ids)]
|
||||
return [('department_id', 'child_of', user_employee.department_id.ids)]
|
||||
|
||||
def _get_valid_employee_for_user(self):
|
||||
user = self.env.user
|
||||
# retrieve the employee of the current active company for the user
|
||||
employee = user.employee_id
|
||||
if not employee:
|
||||
# search for all employees as superadmin to not get blocked by multi-company rules
|
||||
user_employees = user.employee_id.sudo().search([
|
||||
('user_id', '=', user.id)
|
||||
])
|
||||
# the default company employee is most likely the correct one, but fallback to the first if not available
|
||||
employee = user_employees.filtered(lambda r: r.company_id == user.company_id) or user_employees[:1]
|
||||
return employee
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_manager_only_fields(self):
|
||||
manager_fields = self._get_manager_only_fields()
|
||||
for employee in self:
|
||||
if employee.is_manager:
|
||||
employee_sudo = employee.employee_id.sudo()
|
||||
for f in manager_fields:
|
||||
employee[f] = employee_sudo[f]
|
||||
else:
|
||||
for f in manager_fields:
|
||||
employee[f] = False
|
||||
|
||||
def _compute_newly_hired(self):
|
||||
self._compute_from_employee('newly_hired')
|
||||
|
||||
def _search_newly_hired(self, operator, value):
|
||||
if operator not in ('in', 'not in'):
|
||||
return NotImplemented
|
||||
new_hire_field = self.env['hr.employee']._get_new_hire_field()
|
||||
new_hires = self.env['hr.employee'].sudo().search([
|
||||
(new_hire_field, '>', fields.Datetime.now() - timedelta(days=90))
|
||||
])
|
||||
return [('id', operator, new_hires.ids)]
|
||||
|
||||
@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'])
|
||||
base_fields = ('id', 'employee_id', 'name', 'active')
|
||||
version_fields = self.env['hr.version']._fields
|
||||
return 'e.id AS id,e.id AS employee_id,e.name AS name,e.active AS active,' + ','.join(
|
||||
(f'v.{name}' if name in version_fields and version_fields[name].store else f'e.{name}')
|
||||
for name, field in self._fields.items()
|
||||
if name not in base_fields and field.store and field.column_type
|
||||
)
|
||||
|
||||
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
|
||||
FROM hr_employee e
|
||||
JOIN hr_version v
|
||||
ON v.id = e.current_version_id
|
||||
)""" % (self._table, self._get_fields()))
|
||||
|
||||
def get_avatar_card_data(self, fields):
|
||||
return self.read(fields)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.web_editor.controllers.main import handle_history_divergence
|
||||
from odoo.addons.html_editor.tools import handle_history_divergence
|
||||
|
||||
|
||||
class Job(models.Model):
|
||||
|
||||
_name = "hr.job"
|
||||
class HrJob(models.Model):
|
||||
_name = 'hr.job'
|
||||
_description = "Job Position"
|
||||
_inherit = ['mail.thread']
|
||||
_order = 'sequence'
|
||||
|
|
@ -15,48 +14,80 @@ class Job(models.Model):
|
|||
active = fields.Boolean(default=True)
|
||||
name = fields.Char(string='Job Position', required=True, index='trigram', translate=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
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.')
|
||||
expected_employees = fields.Integer(compute='_compute_employees', string='Total Forecasted Employees',
|
||||
help='Expected number of employees for this job position after new recruitment.', groups="hr.group_hr_user")
|
||||
no_of_employee = fields.Integer(compute='_compute_employees', string="Current Number of Employees",
|
||||
help='Number of employees currently occupying this job position.', groups="hr.group_hr_user")
|
||||
no_of_recruitment = fields.Integer(string='Target', 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.Html(string='Job Description', sanitize_attributes=False)
|
||||
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)
|
||||
contract_type_id = fields.Many2one('hr.contract.type', string='Employment Type')
|
||||
requirements = fields.Text('Requirements', groups="hr.group_hr_user")
|
||||
user_id = fields.Many2one(
|
||||
"res.users",
|
||||
"Recruiter",
|
||||
domain="[('share', '=', False), ('company_ids', '=?', company_id)]",
|
||||
default=lambda self: self.env.user,
|
||||
groups="hr.group_hr_user",
|
||||
tracking=True,
|
||||
help="The Recruiter will be the default value for all Applicants in this job \
|
||||
position. The Recruiter is automatically added to all meetings with the Applicant.",
|
||||
)
|
||||
# TODO (master): remove the field `allowed_user_ids`.
|
||||
allowed_user_ids = fields.Many2many('res.users', compute='_compute_allowed_user_ids', readonly=True)
|
||||
department_id = fields.Many2one('hr.department', string='Department', check_company=True, tracking=True, index='btree_not_null')
|
||||
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company, tracking=True)
|
||||
contract_type_id = fields.Many2one('hr.contract.type', string='Employment Type', tracking=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_company_uniq', 'unique(name, company_id, department_id)', 'The name of the job position must be unique per department in company!'),
|
||||
('no_of_recruitment_positive', 'CHECK(no_of_recruitment >= 0)', 'The expected number of new employees must be positive.')
|
||||
]
|
||||
_name_company_uniq = models.Constraint(
|
||||
'unique(name, company_id, department_id)',
|
||||
'The name of the job position must be unique per department in company!',
|
||||
)
|
||||
_no_of_recruitment_positive = models.Constraint(
|
||||
'CHECK(no_of_recruitment >= 0)',
|
||||
'The expected number of new employees must be positive.',
|
||||
)
|
||||
|
||||
@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)
|
||||
employee_data = self.env['hr.employee']._read_group([('job_id', 'in', self.ids)], ['job_id'], ['__count'])
|
||||
result = {job.id: count for job, count 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.depends("company_id")
|
||||
def _compute_allowed_user_ids(self):
|
||||
company_ids = self.mapped("company_id.id")
|
||||
domain = [("share", "=", False)]
|
||||
if company_ids:
|
||||
domain += [("company_ids", "in", company_ids)]
|
||||
|
||||
users_by_company = dict(
|
||||
self.env["res.users"]._read_group(
|
||||
domain=domain,
|
||||
groupby=["company_id"],
|
||||
aggregates=["id:recordset"],
|
||||
),
|
||||
)
|
||||
|
||||
all_users = self.env["res.users"]
|
||||
for users in users_by_company.values():
|
||||
all_users |= users
|
||||
|
||||
for job in self:
|
||||
job.allowed_user_ids = users_by_company.get(job.company_id, all_users)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" 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(vals_list)
|
||||
return super(HrJob, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
|
||||
|
||||
@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 copy_data(self, default=None):
|
||||
vals_list = super().copy_data(default=default)
|
||||
return [dict(vals, name=self.env._("%s (copy)", job.name)) for job, vals in zip(self, vals_list)]
|
||||
|
||||
def write(self, vals):
|
||||
if len(self) == 1:
|
||||
handle_history_divergence(self, 'description', vals)
|
||||
return super(Job, self).write(vals)
|
||||
return super().write(vals)
|
||||
|
|
|
|||
26
odoo-bringout-oca-ocb-hr/hr/models/hr_mixin.py
Normal file
26
odoo-bringout-oca-ocb-hr/hr/models/hr_mixin.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
from .hr_employee import _ALLOW_READ_HR_EMPLOYEE
|
||||
|
||||
|
||||
class HrMixin(models.AbstractModel):
|
||||
_name = _description = 'hr.mixin'
|
||||
|
||||
# Those overrides deal with many2many fields to comodel 'hr.employee'. In
|
||||
# the past, one could assign such a many2many field without having any
|
||||
# access to its comodel. Since Odoo 19, one must have read access to the
|
||||
# comodel to modify the relation. The hack consists in passing a special
|
||||
# value in the context, and pretend 'hr.employee' records to be readable
|
||||
# when that value is present.
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
special_self = self.with_context(_allow_read_hr_employee=_ALLOW_READ_HR_EMPLOYEE)
|
||||
records = super(HrMixin, special_self).create(vals_list)
|
||||
return records.with_env(self.env)
|
||||
|
||||
def write(self, vals):
|
||||
special_self = self.with_context(_allow_read_hr_employee=_ALLOW_READ_HR_EMPLOYEE)
|
||||
return super(HrMixin, special_self).write(vals)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayrollStructureType(models.Model):
|
||||
_name = 'hr.payroll.structure.type'
|
||||
_description = 'Salary Structure Type'
|
||||
|
||||
name = fields.Char('Salary Structure Type')
|
||||
default_resource_calendar_id = fields.Many2one(
|
||||
'resource.calendar', 'Working Hours',
|
||||
default=lambda self: self.env.company.resource_calendar_id)
|
||||
country_id = fields.Many2one(
|
||||
'res.country',
|
||||
string='Country',
|
||||
default=lambda self: self.env.company.country_id,
|
||||
domain=lambda self: [('id', 'in', self.env.companies.country_id.ids)]
|
||||
)
|
||||
country_code = fields.Char(related="country_id.code")
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrPlan(models.Model):
|
||||
_name = 'hr.plan'
|
||||
_description = 'plan'
|
||||
|
||||
name = fields.Char('Name', required=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', default=lambda self: self.env.company)
|
||||
department_id = fields.Many2one('hr.department', check_company=True)
|
||||
plan_activity_type_ids = fields.One2many(
|
||||
'hr.plan.activity.type', 'plan_id',
|
||||
string='Activities',
|
||||
domain="[('company_id', '=', company_id)]")
|
||||
active = fields.Boolean(default=True)
|
||||
steps_count = fields.Integer(compute='_compute_steps_count')
|
||||
|
||||
@api.depends('plan_activity_type_ids')
|
||||
def _compute_steps_count(self):
|
||||
activity_type_data = self.env['hr.plan.activity.type']._read_group([('plan_id', 'in', self.ids)], ['plan_id'], ['plan_id'])
|
||||
steps_count = {x['plan_id'][0]: x['plan_id_count'] for x in activity_type_data}
|
||||
for plan in self:
|
||||
plan.steps_count = steps_count.get(plan.id, 0)
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
# -*- 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'
|
||||
_check_company_auto = True
|
||||
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||
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', '=', False), ('res_model', '=', 'hr.employee')],
|
||||
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',
|
||||
'Other Responsible',
|
||||
check_company=True,
|
||||
help='Specific responsible of activity if not linked to the employee.')
|
||||
plan_id = fields.Many2one('hr.plan')
|
||||
note = fields.Html('Note')
|
||||
|
||||
@api.depends('activity_type_id')
|
||||
def _compute_default_summary(self):
|
||||
for plan_type in self:
|
||||
if plan_type.activity_type_id and plan_type.activity_type_id.summary:
|
||||
plan_type.summary = plan_type.activity_type_id.summary
|
||||
else:
|
||||
plan_type.summary = False
|
||||
|
||||
def get_responsible_id(self, employee):
|
||||
warning = False
|
||||
if self.responsible == 'coach':
|
||||
if not employee.coach_id:
|
||||
warning = _('Coach of employee %s is not set.', employee.name)
|
||||
responsible = employee.coach_id.user_id
|
||||
if employee.coach_id and not responsible:
|
||||
warning = _("The user of %s's coach is not set.", employee.name)
|
||||
elif self.responsible == 'manager':
|
||||
if not employee.parent_id:
|
||||
warning = _('Manager of employee %s is not set.', employee.name)
|
||||
responsible = employee.parent_id.user_id
|
||||
if employee.parent_id and not responsible:
|
||||
warning = _("The manager of %s should be linked to a user.", employee.name)
|
||||
elif self.responsible == 'employee':
|
||||
responsible = employee.user_id
|
||||
if not responsible:
|
||||
warning = _('The employee %s should be linked to a user.', employee.name)
|
||||
elif self.responsible == 'other':
|
||||
responsible = self.responsible_id
|
||||
if not responsible:
|
||||
warning = _('No specific user given on activity %s.', self.activity_type_id.name)
|
||||
return {
|
||||
'responsible': responsible,
|
||||
'warning': warning,
|
||||
}
|
||||
620
odoo-bringout-oca-ocb-hr/hr/models/hr_version.py
Normal file
620
odoo-bringout-oca-ocb-hr/hr/models/hr_version.py
Normal file
|
|
@ -0,0 +1,620 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from babel.dates import format_date, get_date_format
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import get_lang, babel_locale_parse
|
||||
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_date_abbr(env, date):
|
||||
lang = get_lang(env)
|
||||
locale = babel_locale_parse(lang.code)
|
||||
date_format = get_date_format('medium', locale=locale).pattern
|
||||
return format_date(date, date_format, locale=locale)
|
||||
|
||||
|
||||
class HrVersion(models.Model):
|
||||
_name = 'hr.version'
|
||||
_description = 'Version'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin'] # TODO: remove later ? (see if still needed because contract template)
|
||||
_mail_post_access = 'read'
|
||||
_order = 'date_version'
|
||||
_rec_name = 'name'
|
||||
|
||||
def _get_default_address_id(self):
|
||||
address = self.env.company.partner_id.address_get(['default'])
|
||||
return address['default'] if address else False
|
||||
|
||||
def _default_salary_structure(self):
|
||||
return (
|
||||
self.env['hr.payroll.structure.type'].sudo().search([('country_id', '=', self.env.company.country_id.id)], limit=1)
|
||||
or self.env['hr.payroll.structure.type'].sudo().search([('country_id', '=', False)], limit=1)
|
||||
)
|
||||
|
||||
company_id = fields.Many2one('res.company', compute='_compute_company_id', readonly=False,
|
||||
store=True, default=lambda self: self.env.company, tracking=True)
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
string='Employee',
|
||||
tracking=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
index=True)
|
||||
name = fields.Char(tracking=True)
|
||||
display_name = fields.Char(compute='_compute_display_name')
|
||||
active = fields.Boolean(default=True, tracking=True)
|
||||
|
||||
date_version = fields.Date(required=True, default=fields.Date.today, tracking=True, groups="hr.group_hr_user")
|
||||
last_modified_uid = fields.Many2one('res.users', string='Last Modified by',
|
||||
default=lambda self: self.env.uid, required=True, groups="hr.group_hr_user")
|
||||
last_modified_date = fields.Datetime(string='Last Modified on', default=fields.Datetime.now, required=True,
|
||||
groups="hr.group_hr_user")
|
||||
|
||||
# Personal Information
|
||||
country_id = fields.Many2one(
|
||||
'res.country', 'Nationality (Country)', groups="hr.group_hr_user", tracking=True)
|
||||
identification_id = fields.Char(
|
||||
string='Identification No',
|
||||
help="Enter the employee's National Identification Number issued by the government (e.g., Aadhaar, SIN, NIN). This is used for official records and statutory compliance.",
|
||||
groups="hr.group_hr_user",
|
||||
tracking=True)
|
||||
ssnid = fields.Char('SSN No', help='Social Security Number', groups="hr.group_hr_user", tracking=True)
|
||||
passport_id = fields.Char('Passport No', groups="hr.group_hr_user", tracking=True)
|
||||
passport_expiration_date = fields.Date('Passport Expiration Date', groups="hr.group_hr_user", tracking=True)
|
||||
sex = fields.Selection([
|
||||
('male', 'Male'),
|
||||
('female', 'Female'),
|
||||
('other', 'Other'),
|
||||
], groups="hr.group_hr_user", tracking=True, help="This is the legal sex recognized by the state.", string='Gender')
|
||||
|
||||
private_street = fields.Char(string="Private Street", groups="hr.group_hr_user", tracking=True)
|
||||
private_street2 = fields.Char(string="Private Street2", groups="hr.group_hr_user", tracking=True)
|
||||
private_city = fields.Char(string="Private City", groups="hr.group_hr_user", tracking=True)
|
||||
allowed_country_state_ids = fields.Many2many("res.country.state", compute='_compute_allowed_country_state_ids', groups="hr.group_hr_user")
|
||||
private_state_id = fields.Many2one(
|
||||
"res.country.state", string="Private State",
|
||||
domain="[('id', 'in', allowed_country_state_ids)]",
|
||||
groups="hr.group_hr_user", tracking=True)
|
||||
private_zip = fields.Char(string="Private Zip", groups="hr.group_hr_user", tracking=True)
|
||||
private_country_id = fields.Many2one("res.country", string="Private Country",
|
||||
groups="hr.group_hr_user", tracking=True)
|
||||
|
||||
distance_home_work = fields.Integer(string="Home-Work Distance", groups="hr.group_hr_user", tracking=True)
|
||||
km_home_work = fields.Integer(string="Home-Work Distance in Km", groups="hr.group_hr_user",
|
||||
compute="_compute_km_home_work", inverse="_inverse_km_home_work", store=True, tracking=True)
|
||||
distance_home_work_unit = fields.Selection([
|
||||
('kilometers', 'km'),
|
||||
('miles', 'mi'),
|
||||
], 'Home-Work Distance unit', groups="hr.group_hr_user", default='kilometers', required=True, tracking=True)
|
||||
|
||||
marital = fields.Selection(
|
||||
selection='_get_marital_status_selection',
|
||||
string='Marital Status',
|
||||
groups="hr.group_hr_user",
|
||||
default='single',
|
||||
required=True,
|
||||
tracking=True)
|
||||
spouse_complete_name = fields.Char(string="Spouse Legal 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='Dependent Children', groups="hr.group_hr_user", tracking=True)
|
||||
|
||||
# Work Information
|
||||
employee_type = fields.Selection([
|
||||
('employee', 'Employee'),
|
||||
('worker', 'Worker'),
|
||||
('student', 'Student'),
|
||||
('trainee', 'Trainee'),
|
||||
('contractor', 'Contractor'),
|
||||
('freelance', 'Freelancer'),
|
||||
], string='Employee Type', default='employee', required=True, groups="hr.group_hr_user", tracking=True)
|
||||
department_id = fields.Many2one('hr.department', check_company=True, tracking=True, index=True)
|
||||
member_of_department = fields.Boolean("Member of department", compute='_compute_part_of_department', search='_search_part_of_department',
|
||||
help="Whether the employee is a member of the active user's department or one of it's child department.")
|
||||
job_id = fields.Many2one('hr.job', check_company=True, tracking=True, index=True)
|
||||
job_title = fields.Char(compute="_compute_job_title", inverse="_inverse_job_title", store=True, readonly=False,
|
||||
string="Job Title", tracking=True)
|
||||
is_custom_job_title = fields.Boolean(compute='_compute_is_custom_job_title', store=True, default=False, groups="hr.group_hr_user")
|
||||
address_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Work Address',
|
||||
default=_get_default_address_id,
|
||||
store=True,
|
||||
readonly=False,
|
||||
check_company=True,
|
||||
tracking=True)
|
||||
work_location_id = fields.Many2one('hr.work.location', 'Work Location',
|
||||
domain="[('address_id', '=', address_id)]", tracking=True)
|
||||
|
||||
departure_reason_id = fields.Many2one("hr.departure.reason", string="Departure Reason",
|
||||
groups="hr.group_hr_user", copy=False, ondelete='restrict', tracking=True)
|
||||
departure_description = fields.Html(string="Additional Information", groups="hr.group_hr_user", copy=False)
|
||||
departure_date = fields.Date(string="Departure Date", groups="hr.group_hr_user", copy=False, tracking=True)
|
||||
|
||||
resource_calendar_id = fields.Many2one('resource.calendar', inverse='_inverse_resource_calendar_id', check_company=True, string="Working Hours", tracking=True)
|
||||
is_flexible = fields.Boolean(compute='_compute_is_flexible', store=True, groups="hr.group_hr_user")
|
||||
is_fully_flexible = fields.Boolean(compute='_compute_is_flexible', store=True, groups="hr.group_hr_user")
|
||||
tz = fields.Selection(related='employee_id.tz')
|
||||
|
||||
# Contract Information
|
||||
contract_date_start = fields.Date('Contract Start Date', tracking=True, groups="hr.group_hr_manager")
|
||||
contract_date_end = fields.Date(
|
||||
'Contract End Date', tracking=True, help="End date of the contract (if it's a fixed-term contract).",
|
||||
groups="hr.group_hr_manager")
|
||||
trial_date_end = fields.Date('End of Trial Period', help="End date of the trial period (if there is one).",
|
||||
groups="hr.group_hr_manager", tracking=True)
|
||||
date_start = fields.Date(compute='_compute_dates', groups="hr.group_hr_manager", search="_search_start_date")
|
||||
date_end = fields.Date(compute='_compute_dates', groups="hr.group_hr_manager", search="_search_end_date")
|
||||
is_current = fields.Boolean(compute='_compute_is_current', groups="hr.group_hr_manager")
|
||||
is_past = fields.Boolean(compute='_compute_is_past', groups="hr.group_hr_manager")
|
||||
is_future = fields.Boolean(compute='_compute_is_future', groups="hr.group_hr_manager")
|
||||
is_in_contract = fields.Boolean(compute='_compute_is_in_contract', groups="hr.group_hr_manager")
|
||||
|
||||
contract_template_id = fields.Many2one(
|
||||
'hr.version', string="Contract Template", groups="hr.group_hr_user",
|
||||
domain="[('company_id', '=', company_id), ('employee_id', '=', False)]", tracking=True,
|
||||
help="Select a contract template to auto-fill the contract form with predefined values. You can still edit the fields as needed after applying the template.")
|
||||
structure_type_id = fields.Many2one('hr.payroll.structure.type', string="Salary Structure Type",
|
||||
compute="_compute_structure_type_id", readonly=False, store=True, tracking=True,
|
||||
groups="hr.group_hr_manager", default=_default_salary_structure)
|
||||
active_employee = fields.Boolean(related="employee_id.active", string="Active Employee", groups="hr.group_hr_user")
|
||||
currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True)
|
||||
wage = fields.Monetary('Wage', tracking=True, help="Employee's monthly gross wage.", aggregator="avg",
|
||||
groups="hr.group_hr_manager")
|
||||
contract_wage = fields.Monetary('Contract Wage', compute='_compute_contract_wage', groups="hr.group_hr_manager")
|
||||
# [XBO] TODO: remove me in master
|
||||
company_country_id = fields.Many2one('res.country', string="Company country",
|
||||
related='company_id.country_id', readonly=True)
|
||||
country_code = fields.Char(related='company_country_id.code', depends=['company_country_id'], readonly=True)
|
||||
contract_type_id = fields.Many2one('hr.contract.type', "Contract Type", tracking=True,
|
||||
groups="hr.group_hr_manager")
|
||||
additional_note = fields.Text(string='Additional Note', groups="hr.group_hr_user", tracking=True)
|
||||
|
||||
def _get_hr_responsible_domain(self):
|
||||
return "[('share', '=', False), ('company_ids', 'in', company_id), ('all_group_ids', 'in', %s)]" % self.env.ref('hr.group_hr_user').id
|
||||
|
||||
hr_responsible_id = fields.Many2one(
|
||||
'res.users', 'HR Responsible', tracking=True,
|
||||
help='Person responsible for validating the employee\'s contracts.', domain=_get_hr_responsible_domain,
|
||||
default=lambda self: self.env.user, required=True, groups="hr.group_hr_user")
|
||||
|
||||
_check_contract_start_date_defined = models.Constraint(
|
||||
'CHECK(contract_date_end IS NULL OR contract_date_start IS NOT NULL)',
|
||||
'The contract must have a start date.',
|
||||
)
|
||||
|
||||
_check_unique_date_version = models.UniqueIndex(
|
||||
'(employee_id, date_version) WHERE active = TRUE AND employee_id IS NOT NULL',
|
||||
'An employee cannot have multiple active versions sharing the same effective date.',
|
||||
)
|
||||
|
||||
@api.depends('employee_id.company_id')
|
||||
def _compute_company_id(self):
|
||||
for version in self:
|
||||
if version.employee_id:
|
||||
version.company_id = version.employee_id.company_id
|
||||
|
||||
@api.depends('job_id.name')
|
||||
def _compute_job_title(self):
|
||||
for version in self.filtered('job_id'):
|
||||
if version._origin.job_id != version.job_id or not version.is_custom_job_title:
|
||||
version.job_title = version.job_id.name
|
||||
|
||||
def _inverse_job_title(self):
|
||||
for version in self:
|
||||
version.is_custom_job_title = version.job_title != version.job_id.name
|
||||
|
||||
@api.depends('job_id')
|
||||
def _compute_is_custom_job_title(self):
|
||||
for version in self.filtered('job_id'):
|
||||
if version._origin.job_id != version.job_id:
|
||||
version.is_custom_job_title = False
|
||||
|
||||
@api.depends("private_country_id")
|
||||
def _compute_allowed_country_state_ids(self):
|
||||
states = self.env["res.country.state"].search([])
|
||||
for version in self:
|
||||
if version.private_country_id:
|
||||
version.allowed_country_state_ids = version.private_country_id.state_ids
|
||||
else:
|
||||
version.allowed_country_state_ids = states
|
||||
|
||||
@api.constrains('employee_id', 'contract_date_start', 'contract_date_end')
|
||||
def _check_dates(self):
|
||||
version_read_group = self.env['hr.version'].sudo()._read_group(
|
||||
[
|
||||
('id', 'not in', self.ids),
|
||||
('employee_id', 'in', self.employee_id.ids),
|
||||
('contract_date_start', '!=', False),
|
||||
],
|
||||
['employee_id', 'contract_date_start:day', 'contract_date_end:day'],
|
||||
['id:recordset'],
|
||||
)
|
||||
dates_per_employee = defaultdict(list)
|
||||
for employee, date_start, date_end, versions in version_read_group:
|
||||
dates_per_employee[employee].append((date_start, date_end, versions))
|
||||
for version in self.sudo(): # sudo needed to read contract dates
|
||||
if not version.contract_date_start or not version.employee_id:
|
||||
continue
|
||||
if version.contract_date_end and version.contract_date_start > version.contract_date_end:
|
||||
raise ValidationError(self.env._(
|
||||
'Start date (%(start)s) must be earlier than contract end date (%(end)s).',
|
||||
start=version.contract_date_start, end=version.contract_date_end,
|
||||
))
|
||||
if not version.active:
|
||||
continue
|
||||
contract_date_end = version.contract_date_end or date.max
|
||||
contract_period_exists = False
|
||||
for date_start, date_end, versions in dates_per_employee[version.employee_id]:
|
||||
date_to = date_end or date.max
|
||||
if date_start == version.contract_date_start and date_to == contract_date_end:
|
||||
contract_period_exists = True
|
||||
continue
|
||||
if date_start <= contract_date_end and version.contract_date_start <= date_to:
|
||||
raise ValidationError(self.env._(
|
||||
"%s already has a contract running during the selected period.\n\n"
|
||||
"Please either:\n\n"
|
||||
"- Change the start date so that it doesn't overlap with the existing contract, or\n"
|
||||
"- Create a new employee if this employee should have multiple active contracts.",
|
||||
version.employee_id.display_name))
|
||||
if not contract_period_exists:
|
||||
dates_per_employee[version.employee_id].append((version.contract_date_start, version.contract_date_end, version))
|
||||
|
||||
def check_contract_finished(self):
|
||||
if self.contract_date_start and not self.contract_date_end:
|
||||
raise ValidationError(self.env._("Before creating a new contract, close the current one by setting an end date."))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
Version = self.env['hr.version']
|
||||
for vals in vals_list:
|
||||
if 'contract_template_id' in vals:
|
||||
contract_vals = Version.get_values_from_contract_template(Version.browse(vals['contract_template_id']))
|
||||
# take vals from template, but priority given to the original vals
|
||||
vals.update({**contract_vals, **vals})
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_last_version(self):
|
||||
for employee_id, versions in self.grouped('employee_id').items():
|
||||
if employee_id.version_ids == versions:
|
||||
raise ValidationError(
|
||||
self.env._('Employee %s must always have at least one active version.') % employee_id.name
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
# Employee Versions Validation
|
||||
if 'employee_id' in vals:
|
||||
if self.filtered(lambda v: v.employee_id and v.employee_id.version_ids <= self and vals['employee_id'] != v.employee_id.id):
|
||||
raise ValidationError(self.env._("Cannot unassign all the active versions of an employee."))
|
||||
if 'active' in vals and not vals['active']:
|
||||
if self.filtered(lambda v: v.employee_id and v.employee_id.version_ids <= self):
|
||||
raise ValidationError(self.env._("Cannot archive all the active versions of an employee."))
|
||||
|
||||
if self.env.context.get('sync_contract_dates') or ("contract_date_start" not in vals and "contract_date_end" not in vals):
|
||||
return super().write(vals)
|
||||
|
||||
for versions_by_employee in self.grouped('employee_id').values():
|
||||
if len(versions_by_employee.grouped('contract_date_start').keys()) > 1:
|
||||
raise ValidationError(self.env._("Cannot modify multiple versions contract dates with different contracts at once."))
|
||||
|
||||
multiple_versions = self
|
||||
if vals.get("contract_date_start"):
|
||||
unique_versions = multiple_versions.filtered(lambda v: len(v.employee_id.version_ids) == 1)
|
||||
multiple_versions -= unique_versions
|
||||
if len(unique_versions):
|
||||
unique_versions.with_context(sync_contract_dates=True).write({
|
||||
**vals,
|
||||
"date_version": vals["contract_date_start"]
|
||||
})
|
||||
|
||||
if not any(multiple_versions.mapped('contract_date_start')):
|
||||
return super(HrVersion, multiple_versions).write(vals)
|
||||
|
||||
new_vals = {
|
||||
f_name: f_value
|
||||
for f_name, f_value in vals.items()
|
||||
if (f_name != 'contract_date_start' or not f_value) and f_name != 'contract_date_end'
|
||||
}
|
||||
for employee, versions in multiple_versions.grouped('employee_id').items():
|
||||
|
||||
dates_vals = {}
|
||||
first_version = next(iter(versions), versions)
|
||||
|
||||
if "contract_date_start" in vals:
|
||||
dates_vals["contract_date_start"] = fields.Date.to_date(vals.get('contract_date_start'))
|
||||
else:
|
||||
dates_vals["contract_date_start"] = first_version.contract_date_start
|
||||
if "contract_date_end" in vals:
|
||||
dates_vals["contract_date_end"] = fields.Date.to_date(vals.get('contract_date_end'))
|
||||
else:
|
||||
dates_vals["contract_date_end"] = first_version.contract_date_end
|
||||
|
||||
if first_version.contract_date_start:
|
||||
versions_to_sync = employee._get_contract_versions(
|
||||
date_start=first_version.contract_date_start,
|
||||
date_end=first_version.contract_date_end,
|
||||
)
|
||||
all_versions_to_sync = self.env['hr.version']
|
||||
for contract_versions in versions_to_sync.values():
|
||||
all_versions_to_sync |= next(iter(contract_versions.values()))
|
||||
|
||||
if all_versions_to_sync:
|
||||
all_versions_to_sync.with_context(sync_contract_dates=True).write(dates_vals)
|
||||
|
||||
else:
|
||||
versions.with_context(sync_contract_dates=True).write(dates_vals)
|
||||
|
||||
return super(HrVersion, multiple_versions).write(new_vals)
|
||||
|
||||
def get_formview_action(self, access_uid=None):
|
||||
"""
|
||||
Override this method in order to redirect many2one towards the right model
|
||||
- Contract template -> hr.version
|
||||
- Employee record -> hr.employee(.public) with version_id in context
|
||||
"""
|
||||
res = super().get_formview_action(access_uid=access_uid)
|
||||
context = res.get('context', {})
|
||||
if self.employee_id:
|
||||
user = self.env.user
|
||||
if access_uid:
|
||||
user = self.env['res.users'].browse(access_uid)
|
||||
res['res_model'] = 'hr.employee' if user.has_group('hr.group_hr_user') else 'hr.employee.public'
|
||||
res['res_id'] = self.employee_id.id
|
||||
res['context'] = dict(context, version_id=self.id)
|
||||
else:
|
||||
if not context.get('form_view_ref', False):
|
||||
res['context'] = dict(context, form_view_ref='hr.hr_contract_template_form_view')
|
||||
return res
|
||||
|
||||
@api.depends_context('lang')
|
||||
@api.depends('date_version')
|
||||
def _compute_display_name(self):
|
||||
for version in self:
|
||||
version.display_name = version.name if not version.employee_id else format_date_abbr(version.env, version.date_version)
|
||||
|
||||
def _compute_is_current(self):
|
||||
today = fields.Date.today()
|
||||
for version in self:
|
||||
version.is_current = version.date_start <= today and (not version.date_end or version.date_end >= today)
|
||||
|
||||
def _compute_is_past(self):
|
||||
today = fields.Date.today()
|
||||
for version in self:
|
||||
version.is_past = version.date_end and version.date_end < today
|
||||
|
||||
def _compute_is_future(self):
|
||||
today = fields.Date.today()
|
||||
for version in self:
|
||||
version.is_future = version.date_start > today
|
||||
|
||||
def _compute_is_in_contract(self):
|
||||
for version in self:
|
||||
version.is_in_contract = version._is_in_contract()
|
||||
|
||||
def _is_in_contract(self, date=fields.Date.today()):
|
||||
# Return True if the employee is in contract on a given date
|
||||
if not self.contract_date_start:
|
||||
return False
|
||||
return self.date_start <= date and (not self.date_end or self.date_end >= date)
|
||||
|
||||
def _is_overlapping_period(self, date_from, date_to):
|
||||
"""
|
||||
Return True if the employee is at least in contract one day during the period given
|
||||
:param date date_from: the start of the period
|
||||
:param date date_to: the stop of the period
|
||||
"""
|
||||
if not (self.contract_date_start and date_from and date_to):
|
||||
return False
|
||||
period_start = date_from or date.min
|
||||
period_end = date_to or date.max
|
||||
contract_end = self.date_end or date.max
|
||||
return period_start <= contract_end and self.date_start <= period_end
|
||||
|
||||
def _is_fully_flexible(self):
|
||||
""" return True if the version has a fully flexible working calendar """
|
||||
self.ensure_one()
|
||||
return not self.resource_calendar_id
|
||||
|
||||
@api.depends('resource_calendar_id.flexible_hours')
|
||||
def _compute_is_flexible(self):
|
||||
for version in self:
|
||||
version.is_fully_flexible = version._is_fully_flexible()
|
||||
version.is_flexible = version.is_fully_flexible or version.resource_calendar_id.flexible_hours
|
||||
|
||||
@api.model
|
||||
def _get_whitelist_fields_from_template(self):
|
||||
# Add here any field that you want to copy from a contract template
|
||||
# Those fields should have tracking=True in hr.version to see the change
|
||||
return ['job_id', 'department_id', 'contract_type_id', 'structure_type_id', 'wage', 'resource_calendar_id', 'hr_responsible_id']
|
||||
|
||||
def get_values_from_contract_template(self, contract_template_id):
|
||||
if not contract_template_id:
|
||||
return {}
|
||||
company = contract_template_id.company_id or self.env.company
|
||||
whitelist = self.with_company(company)._get_whitelist_fields_from_template()
|
||||
contract_template_vals = contract_template_id.copy_data()[0]
|
||||
return {
|
||||
field: value
|
||||
for field, value in contract_template_vals.items()
|
||||
if field in whitelist and not self.env['hr.version']._fields[field].related
|
||||
}
|
||||
|
||||
@api.depends('wage')
|
||||
def _compute_contract_wage(self):
|
||||
for version in self:
|
||||
version.contract_wage = version._get_contract_wage()
|
||||
|
||||
def _get_contract_wage(self):
|
||||
if not self:
|
||||
return 0
|
||||
self.ensure_one()
|
||||
return self[self._get_contract_wage_field()]
|
||||
|
||||
def _get_contract_wage_field(self):
|
||||
self.ensure_one()
|
||||
return 'wage'
|
||||
|
||||
def _get_normalized_wage(self):
|
||||
""" This method is overridden in hr_payroll, as without that module, nothing allows to know
|
||||
there's no way to determine the employee's pay frequency.
|
||||
"""
|
||||
wage = self._get_contract_wage()
|
||||
# without payroll installed, we suppose that the employee with a specific schedule has a monthly salary
|
||||
if self.resource_calendar_id:
|
||||
if not self.resource_calendar_id.hours_per_week:
|
||||
return 0
|
||||
return wage * 12 / 52 / self.resource_calendar_id.hours_per_week
|
||||
# without any calendar, the employee has a fully flexible schedule and is supposedly working on an hourly wage
|
||||
return wage
|
||||
|
||||
def _get_valid_employee_for_user(self):
|
||||
user = self.env.user
|
||||
# retrieve the employee of the current active company for the user
|
||||
employee = user.employee_id
|
||||
if not employee:
|
||||
# search for all employees as superadmin to not get blocked by multi-company rules
|
||||
user_employees = user.employee_id.sudo().search([
|
||||
('user_id', '=', user.id)
|
||||
])
|
||||
# the default company employee is most likely the correct one, but fallback to the first if not available
|
||||
employee = user_employees.filtered(lambda r: r.company_id == user.company_id) or user_employees[:1]
|
||||
return employee
|
||||
|
||||
@api.constrains('ssnid')
|
||||
def _check_ssnid(self):
|
||||
# By default, a Social Security Number is always valid, but each localization
|
||||
# may want to add its own constraints
|
||||
pass
|
||||
|
||||
@api.depends_context('uid', 'company')
|
||||
@api.depends('department_id')
|
||||
def _compute_part_of_department(self):
|
||||
user_employee = self._get_valid_employee_for_user()
|
||||
active_department = user_employee.department_id
|
||||
if not active_department:
|
||||
self.member_of_department = False
|
||||
else:
|
||||
def get_all_children(department):
|
||||
children = department.child_ids
|
||||
if not children:
|
||||
return self.env['hr.department']
|
||||
return children + get_all_children(children)
|
||||
|
||||
child_departments = active_department + get_all_children(active_department)
|
||||
for version in self:
|
||||
version.member_of_department = version.department_id in child_departments
|
||||
|
||||
def _search_part_of_department(self, operator, value):
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
|
||||
user_employee = self._get_valid_employee_for_user()
|
||||
if not user_employee.department_id:
|
||||
return [('id', 'in', user_employee.ids)]
|
||||
return [('department_id', 'child_of', user_employee.department_id.ids)]
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_structure_type_id(self):
|
||||
|
||||
default_structure_by_country = {}
|
||||
|
||||
def _default_salary_structure(country_id):
|
||||
default_structure = default_structure_by_country.get(country_id)
|
||||
if default_structure is None:
|
||||
default_structure = default_structure_by_country[country_id] = (
|
||||
self.env['hr.payroll.structure.type'].search([('country_id', '=', country_id)], limit=1)
|
||||
or self.env['hr.payroll.structure.type'].search([('country_id', '=', False)], limit=1)
|
||||
)
|
||||
return default_structure
|
||||
|
||||
for version in self:
|
||||
if not version.structure_type_id or (version.structure_type_id.country_id and version.structure_type_id.country_id != version.company_id.country_id):
|
||||
version.structure_type_id = _default_salary_structure(version.company_id.country_id.id)
|
||||
|
||||
@api.depends('distance_home_work', 'distance_home_work_unit')
|
||||
def _compute_km_home_work(self):
|
||||
for version in self:
|
||||
version.km_home_work = version.distance_home_work * 1.609 if version.distance_home_work_unit == "miles" else version.distance_home_work
|
||||
|
||||
def _inverse_km_home_work(self):
|
||||
for version in self:
|
||||
version.distance_home_work = version.km_home_work / 1.609 if version.distance_home_work_unit == "miles" else version.km_home_work
|
||||
|
||||
@api.depends(
|
||||
'contract_date_start', 'contract_date_end', 'date_version', 'employee_id',
|
||||
'employee_id.version_ids.date_version')
|
||||
def _compute_dates(self):
|
||||
for version in self:
|
||||
version.date_start = max(version.date_version, version.contract_date_start) \
|
||||
if version.contract_date_start \
|
||||
else version.date_version
|
||||
|
||||
next_version = self.env['hr.version'].search([
|
||||
('employee_id', 'in', version.employee_id.ids),
|
||||
('date_version', '>', version.date_version)], limit=1)
|
||||
date_version_end = next_version.date_version + relativedelta(days=-1) if next_version else False
|
||||
|
||||
if date_version_end and version.contract_date_end:
|
||||
version.date_end = min(date_version_end, version.contract_date_end)
|
||||
elif date_version_end:
|
||||
version.date_end = date_version_end
|
||||
else:
|
||||
version.date_end = version.contract_date_end
|
||||
|
||||
def _search_start_date(self, operator, value):
|
||||
return [('contract_date_start', operator, value)]
|
||||
|
||||
def _search_end_date(self, operator, value):
|
||||
return [('contract_date_end', operator, value)]
|
||||
|
||||
@api.model
|
||||
def _get_marital_status_selection(self):
|
||||
return [
|
||||
('single', self.env._('Single')),
|
||||
('married', self.env._('Married')),
|
||||
('cohabitant', self.env._('Legal Cohabitant')),
|
||||
('widower', self.env._('Widower')),
|
||||
('divorced', self.env._('Divorced')),
|
||||
]
|
||||
|
||||
def _inverse_resource_calendar_id(self):
|
||||
for employee, versions in self.grouped('employee_id').items():
|
||||
current_version = employee.current_version_id
|
||||
for version in versions:
|
||||
if version == current_version and employee.resource_id.calendar_id != version.resource_calendar_id:
|
||||
employee.resource_id.calendar_id = version.resource_calendar_id
|
||||
|
||||
def _get_salary_costs_factor(self):
|
||||
self.ensure_one()
|
||||
return 12.0
|
||||
|
||||
def _is_struct_from_country(self, country_code):
|
||||
self.ensure_one()
|
||||
self_sudo = self.sudo()
|
||||
return self_sudo.structure_type_id and self_sudo.structure_type_id.country_id.code == country_code
|
||||
|
||||
def _get_tz(self):
|
||||
if self.resource_calendar_id and self.resource_calendar_id.tz:
|
||||
return self.resource_calendar_id.tz
|
||||
else:
|
||||
return self.tz
|
||||
|
||||
def action_open_version(self):
|
||||
self.ensure_one()
|
||||
|
||||
return {
|
||||
'type': "ir.actions.act_window",
|
||||
'res_model': "hr.employee",
|
||||
'res_id': self.employee_id.id,
|
||||
'views': [[False, "form"]],
|
||||
'target': "current",
|
||||
'context': {
|
||||
'version_id': self.id,
|
||||
},
|
||||
}
|
||||
|
|
@ -4,13 +4,17 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class WorkLocation(models.Model):
|
||||
_name = "hr.work.location"
|
||||
class HrWorkLocation(models.Model):
|
||||
_name = 'hr.work.location'
|
||||
_description = "Work Location"
|
||||
_order = 'name'
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
name = fields.Char(string="Work Location", required=True)
|
||||
company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company)
|
||||
address_id = fields.Many2one('res.partner', required=True, string="Work Address", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
location_type = fields.Selection([
|
||||
('home', 'Home'),
|
||||
('office', 'Office'),
|
||||
('other', 'Other')], string='Cover Image', default='office', required=True)
|
||||
address_id = fields.Many2one('res.partner', required=True, string="Work Address", check_company=True)
|
||||
location_number = fields.Char()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api, tools
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrUiMenu(models.Model):
|
||||
|
|
@ -9,7 +8,12 @@ class IrUiMenu(models.Model):
|
|||
|
||||
def _load_menus_blacklist(self):
|
||||
res = super()._load_menus_blacklist()
|
||||
emp_menu = self.env.ref('hr.menu_hr_employee', raise_if_not_found=False)
|
||||
if emp_menu and self.env.user.has_group('hr.group_hr_user'):
|
||||
if self.env.user.has_group('hr.group_hr_user') and (emp_menu := self.env.ref('hr.menu_hr_employee', raise_if_not_found=False)):
|
||||
res.append(emp_menu.id)
|
||||
else:
|
||||
is_department_manager = bool(self.env["hr.department"].search_count([
|
||||
('manager_id', 'in', self.env.user.employee_ids.ids)
|
||||
], limit=1))
|
||||
if not is_department_manager and (dep_menu := self.env.ref('hr.menu_hr_department_kanban', raise_if_not_found=False)):
|
||||
res.append(dep_menu.id)
|
||||
return res
|
||||
|
|
|
|||
45
odoo-bringout-oca-ocb-hr/hr/models/mail_activity_plan.py
Normal file
45
odoo-bringout-oca-ocb-hr/hr/models/mail_activity_plan.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# -*- 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 MailActivityPlan(models.Model):
|
||||
_inherit = 'mail.activity.plan'
|
||||
|
||||
department_id = fields.Many2one(
|
||||
'hr.department', check_company=True, index='btree_not_null',
|
||||
compute='_compute_department_id', ondelete='cascade', readonly=False, store=True)
|
||||
department_assignable = fields.Boolean(compute='_compute_department_assignable')
|
||||
|
||||
@api.constrains('res_model')
|
||||
def _check_compatibility_with_model(self):
|
||||
""" Check that when the model is updated to a model different from employee,
|
||||
there are no remaining specific values to employee. """
|
||||
plan_tocheck = self.filtered(lambda plan: not plan.department_assignable)
|
||||
failing_plans = plan_tocheck.filtered('department_id')
|
||||
if failing_plans:
|
||||
raise UserError(
|
||||
_('Plan %(plan_names)s cannot use a department as it is used only for some HR plans.',
|
||||
plan_names=', '.join(failing_plans.mapped('name')))
|
||||
)
|
||||
plan_tocheck = self.filtered(lambda plan: plan.res_model != 'hr.employee')
|
||||
failing_templates = plan_tocheck.template_ids.filtered(
|
||||
lambda tpl: tpl.responsible_type in {'coach', 'manager', 'employee'}
|
||||
)
|
||||
if failing_templates:
|
||||
raise UserError(
|
||||
_('Plan activities %(template_names)s cannot use coach, manager or employee responsible as it is used only for employee plans.',
|
||||
template_names=', '.join(failing_templates.mapped('activity_type_id.name')))
|
||||
)
|
||||
|
||||
@api.depends('res_model')
|
||||
def _compute_department_assignable(self):
|
||||
for plan in self:
|
||||
plan.department_assignable = plan.res_model == 'hr.employee'
|
||||
|
||||
@api.depends('res_model')
|
||||
def _compute_department_id(self):
|
||||
for plan in self.filtered(lambda plan: not plan.department_assignable):
|
||||
plan.department_id = False
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
# -*- 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 MailActivityPlanTemplate(models.Model):
|
||||
_inherit = 'mail.activity.plan.template'
|
||||
|
||||
responsible_type = fields.Selection(selection_add=[
|
||||
('coach', 'Coach'),
|
||||
('manager', 'Manager'),
|
||||
('employee', 'Employee'),
|
||||
], ondelete={'coach': 'cascade', 'manager': 'cascade', 'employee': 'cascade'})
|
||||
|
||||
@api.constrains('plan_id', 'responsible_type')
|
||||
def _check_responsible_hr(self):
|
||||
""" Ensure that hr types are used only on employee model """
|
||||
for template in self.filtered(lambda tpl: tpl.plan_id.res_model != 'hr.employee'):
|
||||
if template.responsible_type in {'coach', 'manager', 'employee'}:
|
||||
raise ValidationError(_('Those responsible types are limited to Employee plans.'))
|
||||
|
||||
def _get_closest_parent_user(self, employee, responsible, error_message):
|
||||
responsible_parent = responsible
|
||||
viewed_responsible = [employee]
|
||||
while True:
|
||||
if not responsible_parent:
|
||||
return {
|
||||
'error': False,
|
||||
'responsible': self.env.user,
|
||||
'warning': error_message
|
||||
}
|
||||
if responsible_parent.user_id:
|
||||
return {
|
||||
'error': False,
|
||||
'responsible': responsible_parent.user_id,
|
||||
'warning': False
|
||||
}
|
||||
if responsible_parent in viewed_responsible:
|
||||
return {
|
||||
"error": _(
|
||||
"Oops! It seems there is a problem with your team structure.\
|
||||
We found a circular reporting loop and no one in that loop is linked to a user.\
|
||||
Please double-check that everyone reports to the correct manager."
|
||||
),
|
||||
'warning': False,
|
||||
"responsible": False,
|
||||
}
|
||||
else:
|
||||
viewed_responsible.append(responsible_parent)
|
||||
responsible_parent = responsible_parent.parent_id
|
||||
|
||||
def _determine_responsible(self, on_demand_responsible, employee):
|
||||
if self.plan_id.res_model != 'hr.employee' or self.responsible_type not in {'coach', 'manager', 'employee'}:
|
||||
return super()._determine_responsible(on_demand_responsible, employee)
|
||||
result = {"error": "", "warning": "", "responsible": False}
|
||||
if self.responsible_type == 'coach':
|
||||
if not employee.coach_id:
|
||||
result['error'] = _('Coach of employee %s is not set.', employee.name)
|
||||
result['responsible'] = employee.coach_id.user_id
|
||||
if employee.coach_id and not result['responsible']:
|
||||
# If a plan cannot be launched due to the coach not being linked to an user,
|
||||
# attempt to assign it to the coach's manager user. If that manager is also not linked
|
||||
# to an user, continue searching upwards until a manager with a linked user is found.
|
||||
# If no one is found still, assign to current user.
|
||||
result = self._get_closest_parent_user(
|
||||
employee=employee,
|
||||
responsible=employee.coach_id.parent_id,
|
||||
error_message=_(
|
||||
"The user of %s's coach is not set.", employee.name
|
||||
),
|
||||
)
|
||||
|
||||
elif self.responsible_type == 'manager':
|
||||
if not employee.parent_id:
|
||||
result['error'] = _('Manager of employee %s is not set.', employee.name)
|
||||
result['responsible'] = employee.parent_id.user_id
|
||||
if employee.parent_id and not result['responsible']:
|
||||
# If a plan cannot be launched due to the manager not being linked to an user,
|
||||
# attempt to assign it to the manager's manager user. If that manager is also not linked
|
||||
# to an user, continue searching upwards until a manager with a linked user is found.
|
||||
# If no one is found still, assign to current user.
|
||||
result = self._get_closest_parent_user(
|
||||
employee=employee,
|
||||
responsible=employee.parent_id.parent_id,
|
||||
error_message=_(
|
||||
"The manager of %s should be linked to a user.", employee.name
|
||||
),
|
||||
)
|
||||
|
||||
elif self.responsible_type == 'employee':
|
||||
result['responsible'] = employee.user_id
|
||||
if not result['responsible']:
|
||||
# If a plan cannot be launched due to the employee not being linked to an user,
|
||||
# attempt to assign it to the manager's user. If the manager is also not linked
|
||||
# to an user, continue searching upwards until a manager with a linked user is found.
|
||||
# If no one is found still, assign to current user.
|
||||
result = self._get_closest_parent_user(
|
||||
employee=employee,
|
||||
responsible=employee.parent_id,
|
||||
error_message=_(
|
||||
"The employee %s should be linked to a user.", employee.name
|
||||
),
|
||||
)
|
||||
|
||||
if result['error'] or result['responsible']:
|
||||
return result
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
from odoo import fields, models, _
|
||||
|
||||
|
||||
class Alias(models.Model):
|
||||
class MailAlias(models.Model):
|
||||
_inherit = 'mail.alias'
|
||||
|
||||
alias_contact = fields.Selection(selection_add=[
|
||||
|
|
@ -14,4 +14,4 @@ class Alias(models.Model):
|
|||
def _get_alias_contact_description(self):
|
||||
if self.alias_contact == 'employees':
|
||||
return _('addresses linked to registered employees')
|
||||
return super(Alias, self)._get_alias_contact_description()
|
||||
return super()._get_alias_contact_description()
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, tools, _
|
||||
from odoo.addons.mail.tools.alias_error import AliasError
|
||||
|
||||
|
||||
class BaseModel(models.AbstractModel):
|
||||
class Base(models.AbstractModel):
|
||||
_inherit = 'base'
|
||||
|
||||
def _alias_get_error_message(self, message, message_dict, alias):
|
||||
def _alias_get_error(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]
|
||||
email_from = tools.mail.decode_message_header(message, 'From')
|
||||
email_address = tools.email_normalize(email_from, strict=False)
|
||||
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 AliasError('error_hr_employee_restricted', _('restricted to employees'))
|
||||
return False
|
||||
return super(BaseModel, self)._alias_get_error_message(message, message_dict, alias)
|
||||
return super()._alias_get_error(message, message_dict, alias)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
class ResCompany(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")
|
||||
employee_properties_definition = fields.PropertiesDefinition('Employee Properties')
|
||||
hr_presence_control_login = fields.Boolean(string="Based on user status in system", default=True)
|
||||
hr_presence_control_email = fields.Boolean(string="Based on number of emails sent")
|
||||
hr_presence_control_ip = fields.Boolean(string="Based on IP Address")
|
||||
hr_presence_control_attendance = fields.Boolean(string="Based on attendances")
|
||||
contract_expiration_notice_period = fields.Integer("Contract Expiry Notice Period", default=7)
|
||||
work_permit_expiration_notice_period = fields.Integer("Work Permit Expiry Notice Period", default=60)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import threading
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
|
|
@ -13,21 +9,11 @@ class ResConfigSettings(models.TransientModel):
|
|||
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")
|
||||
module_hr_homeworking = fields.Boolean(string="Homeworking")
|
||||
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_login = fields.Boolean(related='company_id.hr_presence_control_login', readonly=False)
|
||||
hr_presence_control_email = fields.Boolean(related='company_id.hr_presence_control_email', readonly=False)
|
||||
hr_presence_control_ip = fields.Boolean(related='company_id.hr_presence_control_ip', readonly=False)
|
||||
module_hr_attendance = fields.Boolean(related='company_id.hr_presence_control_attendance', readonly=False)
|
||||
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')
|
||||
|
||||
@api.constrains('module_hr_presence', 'hr_presence_control_email', 'hr_presence_control_ip')
|
||||
def _check_advanced_presence(self):
|
||||
test_mode = self.env.registry.in_test_mode() or getattr(threading.current_thread(), 'testing', False)
|
||||
if self.env.context.get('install_mode', False) or test_mode:
|
||||
return
|
||||
|
||||
for settings in self:
|
||||
if settings.module_hr_presence and not (settings.hr_presence_control_email or settings.hr_presence_control_ip):
|
||||
raise ValidationError(_('You should select at least one Advanced Presence Control option.'))
|
||||
contract_expiration_notice_period = fields.Integer(string="Contract Expiry Notice Period", related='company_id.contract_expiration_notice_period', readonly=False)
|
||||
work_permit_expiration_notice_period = fields.Integer(string="Work Permit Expiry Notice Period", related='company_id.work_permit_expiration_notice_period', readonly=False)
|
||||
|
|
|
|||
|
|
@ -1,54 +1,111 @@
|
|||
# -*- 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 AccessError
|
||||
from odoo.exceptions import RedirectWarning, UserError
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = ['res.partner']
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
employee_ids = fields.One2many(
|
||||
'hr.employee', 'address_home_id', string='Employees', groups="hr.group_hr_user",
|
||||
'hr.employee', 'work_contact_id', string='Employees', groups="hr.group_hr_user",
|
||||
help="Related employees based on their private address")
|
||||
employees_count = fields.Integer(compute='_compute_employees_count', groups="hr.group_hr_user")
|
||||
|
||||
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
|
||||
employee = fields.Boolean(help="Whether this contact is an Employee.", compute='_compute_employee', store=True, readonly=False, copy=False)
|
||||
|
||||
def _compute_employees_count(self):
|
||||
for partner in self:
|
||||
partner.employees_count = len(partner.employee_ids)
|
||||
partner.employees_count = len(partner.sudo().employee_ids.filtered(lambda e: e.company_id in self.env.companies))
|
||||
|
||||
def action_open_employees(self):
|
||||
self.ensure_one()
|
||||
if self.employees_count > 1:
|
||||
return {
|
||||
'name': _('Related Employees'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'hr.employee',
|
||||
'view_mode': 'kanban',
|
||||
'domain': [('id', 'in', self.employee_ids.ids),
|
||||
('company_id', 'in', self.env.companies.ids)],
|
||||
}
|
||||
return {
|
||||
'name': _('Related Employees'),
|
||||
'name': _('Employee'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'hr.employee',
|
||||
'view_mode': 'kanban,tree,form',
|
||||
'domain': [('id', 'in', self.employee_ids.ids)],
|
||||
'res_id': self.employee_ids.filtered(lambda e: e.company_id in self.env.companies).id,
|
||||
'view_mode': 'form',
|
||||
}
|
||||
|
||||
class ResPartnerBank(models.Model):
|
||||
_inherit = ['res.partner.bank']
|
||||
def _get_all_addr(self):
|
||||
self.ensure_one()
|
||||
employee_id = self.env['hr.employee'].search(
|
||||
[('id', 'in', self.employee_ids.ids)],
|
||||
limit=1,
|
||||
)
|
||||
if not employee_id:
|
||||
return super()._get_all_addr()
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_display_name(self):
|
||||
account_employee = self.browse()
|
||||
if not self.user_has_groups('hr.group_hr_user'):
|
||||
account_employee = self.sudo().filtered("partner_id.employee_ids")
|
||||
for account in account_employee:
|
||||
account.sudo(self.env.su).display_name = \
|
||||
account.acc_number[:2] + "*" * len(account.acc_number[2:-4]) + account.acc_number[-4:]
|
||||
super(ResPartnerBank, self - account_employee)._compute_display_name()
|
||||
pstl_addr = {
|
||||
'contact_type': 'employee',
|
||||
'street': employee_id.private_street,
|
||||
'zip': employee_id.private_zip,
|
||||
'city': employee_id.private_city,
|
||||
'country': employee_id.private_country_id.code,
|
||||
}
|
||||
return [pstl_addr] + super()._get_all_addr()
|
||||
|
||||
@api.depends('employee_ids')
|
||||
def _compute_employee(self):
|
||||
employee_data = self.env['hr.employee']._read_group(
|
||||
domain=[('work_contact_id', 'in', self.ids)],
|
||||
groupby=['work_contact_id'],
|
||||
)
|
||||
employees = {employee for [employee] in employee_data}
|
||||
for partner in self:
|
||||
partner.employee = partner in employees
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_contact_rel_employee(self):
|
||||
partners = self.filtered(lambda partner: partner.sudo().employee_ids)
|
||||
if len(self) == 1 and len(partners) == 1 and self.id == partners[0].id:
|
||||
raise UserError(_('You cannot delete contact that are linked to an employee, please archive them instead.'))
|
||||
if partners:
|
||||
error_msg = _(
|
||||
'You cannot delete contact(s) linked to employee(s).\n'
|
||||
'Please archive them instead.\n\n'
|
||||
'Affected contact(s): %(names)s', names=", ".join([u.name for u in partners]),
|
||||
)
|
||||
action_error = partners._action_show()
|
||||
raise RedirectWarning(error_msg, action_error, _('Go to contact'))
|
||||
|
||||
def _action_show(self):
|
||||
"""If self is a singleton, directly access the form view. If it is a recordset, open a list view"""
|
||||
view_id = self.env.ref('base.view_partner_form').id
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'res.partner',
|
||||
'context': {'create': False},
|
||||
}
|
||||
if len(self) > 1:
|
||||
action.update({
|
||||
'name': _('Contacts'),
|
||||
'view_mode': 'list,form',
|
||||
'views': [[None, 'list'], [view_id, 'form']],
|
||||
'domain': [('id', 'in', self.ids)],
|
||||
})
|
||||
else:
|
||||
action.update({
|
||||
'view_mode': 'form',
|
||||
'views': [[view_id, 'form']],
|
||||
'res_id': self.id,
|
||||
})
|
||||
return action
|
||||
|
||||
def _get_store_avatar_card_fields(self, target):
|
||||
avatar_card_fields = super()._get_store_avatar_card_fields(target)
|
||||
if target.is_internal(self.env):
|
||||
# sudo: res.partner - internal users can access employee information of partner
|
||||
employee_fields = self.sudo().employee_ids._get_store_avatar_card_fields(target)
|
||||
avatar_card_fields.append(Store.Many("employee_ids", employee_fields, mode="ADD", sudo=True))
|
||||
return avatar_card_fields
|
||||
|
|
|
|||
58
odoo-bringout-oca-ocb-hr/hr/models/res_partner_bank.py
Normal file
58
odoo-bringout-oca-ocb-hr/hr/models/res_partner_bank.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResPartnerBank(models.Model):
|
||||
_inherit = 'res.partner.bank'
|
||||
|
||||
bank_street = fields.Char(related='bank_id.street', readonly=False)
|
||||
bank_street2 = fields.Char(related='bank_id.street2', readonly=False)
|
||||
bank_zip = fields.Char(related='bank_id.zip', readonly=False)
|
||||
bank_city = fields.Char(related='bank_id.city', readonly=False)
|
||||
bank_state = fields.Many2one(related='bank_id.state', readonly=False)
|
||||
bank_country = fields.Many2one(related='bank_id.country', readonly=False)
|
||||
bank_email = fields.Char(related='bank_id.email', readonly=False)
|
||||
bank_phone = fields.Char(related='bank_id.phone', readonly=False)
|
||||
employee_id = fields.Many2many('hr.employee', 'Employee', compute="_compute_employee_id", search="_search_employee_id")
|
||||
employee_salary_amount = fields.Float(string='Salary Allocation', compute='_compute_salary_amount', digits=(16, 4), readonly=True, store=False)
|
||||
employee_salary_amount_is_percentage = fields.Boolean(compute='_compute_salary_amount', readonly=True, store=False)
|
||||
currency_symbol = fields.Char(related='currency_id.symbol')
|
||||
employee_has_multiple_bank_accounts = fields.Boolean(related="employee_id.has_multiple_bank_accounts")
|
||||
|
||||
@api.depends('employee_id.salary_distribution')
|
||||
def _compute_salary_amount(self):
|
||||
for bank in self:
|
||||
if bank.employee_id and bank.employee_id.salary_distribution:
|
||||
bank.employee_salary_amount, bank.employee_salary_amount_is_percentage = bank.employee_id.get_bank_account_salary_allocation(bank.id)
|
||||
continue
|
||||
bank.employee_salary_amount_is_percentage = True
|
||||
if bank.employee_id.salary_distribution:
|
||||
bank.employee_salary_amount = bank.employee_id.get_remaining_percentage()
|
||||
else:
|
||||
bank.employee_salary_amount = 0
|
||||
|
||||
def _search_employee_id(self, operator, value):
|
||||
matching_employees = self.env['hr.employee'].sudo().search([('id', operator, value)])
|
||||
return [('id', 'in', matching_employees.bank_account_ids.ids)]
|
||||
|
||||
def action_open_allocation_wizard(self):
|
||||
self.ensure_one()
|
||||
return self.employee_id.action_open_allocation_wizard()
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_employee_id(self):
|
||||
for bank in self:
|
||||
if bank.partner_id.employee:
|
||||
bank.employee_id = bank.partner_id.employee_ids.filtered(lambda e: e.company_id in self.env.companies)[:1]
|
||||
else:
|
||||
bank.employee_id = False
|
||||
|
||||
def _compute_display_name(self):
|
||||
account_employee = self.browse()
|
||||
if not self.env.user.has_group('hr.group_hr_user'):
|
||||
account_employee = self.sudo().filtered("partner_id.employee_ids")
|
||||
for account in account_employee:
|
||||
account.sudo(self.env.su).display_name = \
|
||||
account.acc_number[:2] + "*" * len(account.acc_number[2:-4]) + account.acc_number[-4:]
|
||||
super(ResPartnerBank, self - account_employee)._compute_display_name()
|
||||
|
|
@ -1,24 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import api, models, fields, _, SUPERUSER_ID
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.fields import Domain
|
||||
from odoo.tools.misc import clean_context
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
|
||||
HR_READABLE_FIELDS = [
|
||||
'active',
|
||||
'child_ids',
|
||||
'employee_id',
|
||||
'address_home_id',
|
||||
'employee_ids',
|
||||
'employee_parent_id',
|
||||
'hr_presence_state',
|
||||
'last_activity',
|
||||
'last_activity_time',
|
||||
'can_edit',
|
||||
'is_hr_user',
|
||||
'is_system',
|
||||
'employee_resource_calendar_id',
|
||||
'work_contact_id',
|
||||
'bank_account_ids',
|
||||
]
|
||||
|
||||
HR_WRITABLE_FIELDS = [
|
||||
|
|
@ -29,131 +32,85 @@ HR_WRITABLE_FIELDS = [
|
|||
'private_state_id',
|
||||
'private_zip',
|
||||
'private_country_id',
|
||||
'address_id',
|
||||
'private_phone',
|
||||
'private_email',
|
||||
'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',
|
||||
'employee_bank_account_ids',
|
||||
'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_id',
|
||||
'work_phone',
|
||||
'certificate',
|
||||
'study_field',
|
||||
'study_school',
|
||||
'private_lang',
|
||||
'employee_type',
|
||||
]
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
_inherit = ['res.users']
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
def _employee_ids_domain(self):
|
||||
# employee_ids is considered a safe field and as such will be fetched as sudo.
|
||||
# So try to enforce the security rules on the field to make sure we do not load employees outside of active companies
|
||||
return [('company_id', 'in', self.env.company.ids + self.env.context.get('allowed_company_ids', []))]
|
||||
|
||||
# note: a user can only be linked to one employee per company (see sql constraint in ´hr.employee´)
|
||||
# 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', domain=_employee_ids_domain)
|
||||
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)
|
||||
job_title = fields.Char(related='employee_id.job_title')
|
||||
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_contact_id = fields.Many2one(related='employee_id.work_contact_id', readonly=False, related_sudo=False)
|
||||
work_location_id = fields.Many2one(related='employee_id.work_location_id', 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)
|
||||
private_street = fields.Char(related='address_home_id.street', string="Private Street", readonly=False, related_sudo=False)
|
||||
private_street2 = fields.Char(related='address_home_id.street2', string="Private Street2", readonly=False, related_sudo=False)
|
||||
private_city = fields.Char(related='address_home_id.city', string="Private City", readonly=False, related_sudo=False)
|
||||
work_location_name = fields.Char(related="employee_id.work_location_name")
|
||||
work_location_type = fields.Selection(related="employee_id.work_location_type")
|
||||
private_street = fields.Char(related='employee_id.private_street', string="Private Street", readonly=False, related_sudo=False)
|
||||
private_street2 = fields.Char(related='employee_id.private_street2', string="Private Street2", readonly=False, related_sudo=False)
|
||||
private_city = fields.Char(related='employee_id.private_city', string="Private City", readonly=False, related_sudo=False)
|
||||
private_state_id = fields.Many2one(
|
||||
related='address_home_id.state_id', string="Private State", readonly=False, related_sudo=False,
|
||||
related='employee_id.private_state_id', string="Private State", readonly=False, related_sudo=False,
|
||||
domain="[('country_id', '=?', private_country_id)]")
|
||||
private_zip = fields.Char(related='address_home_id.zip', readonly=False, string="Private Zip", related_sudo=False)
|
||||
private_country_id = fields.Many2one(related='address_home_id.country_id', string="Private Country", 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)
|
||||
private_lang = fields.Selection(related='address_home_id.lang', string="Employee Lang", readonly=False)
|
||||
private_zip = fields.Char(related='employee_id.private_zip', readonly=False, string="Private Zip", related_sudo=False)
|
||||
private_country_id = fields.Many2one(related='employee_id.private_country_id', string="Private Country", readonly=False, related_sudo=False)
|
||||
private_phone = fields.Char(related='employee_id.private_phone', readonly=False, related_sudo=False)
|
||||
private_email = fields.Char(related='employee_id.private_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)
|
||||
# This field no longer appears to be in use. To avoid breaking anything it must only be removed after the freeze of v19.
|
||||
employee_bank_account_ids = fields.Many2many('res.partner.bank', related='employee_id.bank_account_ids', string="Employee's Bank Accounts", related_sudo=False, readonly=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')
|
||||
employee_type = fields.Selection(related='employee_id.employee_type', readonly=False, related_sudo=False)
|
||||
employee_resource_calendar_id = fields.Many2one(related='employee_id.resource_calendar_id', string="Employee's Working Hours", readonly=True)
|
||||
bank_account_ids = fields.Many2many(related="employee_id.bank_account_ids")
|
||||
|
||||
create_employee = fields.Boolean(store=False, default=True, copy=False, string="Technical field, whether to create an employee")
|
||||
create_employee = fields.Boolean(store=False, default=False, copy=False, string="Technical field, whether to create an employee")
|
||||
create_employee_id = fields.Many2one('hr.employee', store=False, copy=False, string="Technical field, bind user to this employee on create")
|
||||
|
||||
can_edit = fields.Boolean(compute='_compute_can_edit')
|
||||
is_system = fields.Boolean(compute="_compute_is_system")
|
||||
is_hr_user = fields.Boolean(compute='_compute_is_hr_user')
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_is_system(self):
|
||||
self.is_system = self.env.user._is_system()
|
||||
|
||||
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')
|
||||
def _compute_is_hr_user(self):
|
||||
is_hr_user = self.env.user.has_group('hr.group_hr_user')
|
||||
for user in self:
|
||||
user.can_edit = can_edit
|
||||
user.is_hr_user = is_hr_user
|
||||
|
||||
@api.depends('employee_ids')
|
||||
def _compute_employee_count(self):
|
||||
|
|
@ -168,17 +125,22 @@ class User(models.Model):
|
|||
def SELF_WRITEABLE_FIELDS(self):
|
||||
return super().SELF_WRITEABLE_FIELDS + HR_WRITABLE_FIELDS
|
||||
|
||||
@api.onchange("private_state_id")
|
||||
def _onchange_private_state_id(self):
|
||||
if self.private_state_id:
|
||||
self.private_country_id = self.private_state_id.country_id
|
||||
|
||||
@api.model
|
||||
def get_views(self, views, options=None):
|
||||
# Requests the My Profile form view as last.
|
||||
# Requests the My Preferences form view as last.
|
||||
# Otherwise the fields of the 'search' view will take precedence
|
||||
# and will omit the fields that are requested as SUPERUSER
|
||||
# in `get_view()`.
|
||||
profile_view = self.env.ref("hr.res_users_view_form_profile")
|
||||
profile_form = profile_view and [profile_view.id, 'form']
|
||||
if profile_form and profile_form in views:
|
||||
views.remove(profile_form)
|
||||
views.append(profile_form)
|
||||
preferences_view = self.env.ref("hr.res_users_view_form_preferences")
|
||||
preferences_form = preferences_view and [preferences_view.id, 'form']
|
||||
if preferences_form and preferences_form in views:
|
||||
views.remove(preferences_form)
|
||||
views.append(preferences_form)
|
||||
result = super().get_views(views, options)
|
||||
return result
|
||||
|
||||
|
|
@ -190,12 +152,12 @@ class User(models.Model):
|
|||
# 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
|
||||
# Note: limit the `sudo` to the only action of "editing own preferences" 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:
|
||||
preferences_view = self.env.ref("hr.res_users_view_form_preferences")
|
||||
if preferences_view and view_id == preferences_view.id:
|
||||
self = self.with_user(SUPERUSER_ID)
|
||||
result = super(User, self).get_view(view_id, view_type, **options)
|
||||
result = super().get_view(view_id, view_type, **options)
|
||||
return result
|
||||
|
||||
@api.model_create_multi
|
||||
|
|
@ -222,6 +184,14 @@ class User(models.Model):
|
|||
"""
|
||||
return ['name', 'email', 'image_1920', 'tz']
|
||||
|
||||
def _get_personal_info_partner_ids_to_notify(self, employee):
|
||||
if employee.version_id.hr_responsible_id:
|
||||
return (
|
||||
_("You are receiving this message because you are the HR Responsible of this employee."),
|
||||
employee.version_id.hr_responsible_id.partner_id.ids,
|
||||
)
|
||||
return ('', [])
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Synchronize user and its related employee
|
||||
|
|
@ -229,16 +199,34 @@ class User(models.Model):
|
|||
their own data (otherwise sudo is applied for self data).
|
||||
"""
|
||||
hr_fields = {
|
||||
field
|
||||
field_name: 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_domain = [
|
||||
*self.env['hr.employee']._check_company_domain(self.env.company),
|
||||
('user_id', 'in', self.ids),
|
||||
]
|
||||
if hr_fields:
|
||||
employees = self.env['hr.employee'].sudo().search(employee_domain)
|
||||
get_field = self.env['ir.model.fields']._get
|
||||
field_names = Markup().join([
|
||||
Markup("<li>%s</li>") % get_field("res.users", fname).field_description for fname in hr_fields
|
||||
])
|
||||
for employee in employees:
|
||||
reason_message, partner_ids = self._get_personal_info_partner_ids_to_notify(employee)
|
||||
if partner_ids:
|
||||
employee.message_notify(
|
||||
body=Markup("<p>%s</p><p>%s</p><ul>%s</ul><p><em>%s</em></p>") % (
|
||||
_('Personal information update.'),
|
||||
_("The following fields were modified by %s", employee.name),
|
||||
field_names,
|
||||
reason_message,
|
||||
),
|
||||
partner_ids=partner_ids,
|
||||
)
|
||||
result = super().write(vals)
|
||||
|
||||
employee_values = {}
|
||||
for fname in [f for f in self._get_employee_fields_to_sync() if f in vals]:
|
||||
|
|
@ -248,14 +236,12 @@ class User(models.Model):
|
|||
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 = self.env['hr.employee'].sudo().search(employee_domain + [('image_1920', '=', False)])
|
||||
with_image = self.env['hr.employee'].sudo().search(employee_domain + [('image_1920', '!=', False)])
|
||||
without_image.write(employee_values)
|
||||
if not can_edit_self:
|
||||
employee_values.pop('image_1920')
|
||||
with_image.write(employee_values)
|
||||
else:
|
||||
employees = self.env['hr.employee'].sudo().search([('user_id', 'in', self.ids)])
|
||||
employees = self.env['hr.employee'].sudo().search(employee_domain)
|
||||
if employees:
|
||||
employees.write(employee_values)
|
||||
return result
|
||||
|
|
@ -263,8 +249,17 @@ class User(models.Model):
|
|||
@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()
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('hr.res_users_action_my')
|
||||
groups = {
|
||||
group_xml_id[0]: True
|
||||
for group_xml_id in self.env.user.all_group_ids._get_external_ids().values()
|
||||
if group_xml_id
|
||||
}
|
||||
action_context = ast.literal_eval(action['context']) if action['context'] else {}
|
||||
action_context.update(groups)
|
||||
action['context'] = str(action_context)
|
||||
return action
|
||||
return super().action_get()
|
||||
|
||||
@api.depends('employee_ids')
|
||||
@api.depends_context('company')
|
||||
|
|
@ -277,12 +272,68 @@ class User(models.Model):
|
|||
user.employee_id = employee_per_user.get(user)
|
||||
|
||||
def _search_company_employee(self, operator, value):
|
||||
return [('employee_ids', operator, value)]
|
||||
# Equivalent to `[('employee_ids', operator, value)]`,
|
||||
# but we inline the ids directly to simplify final queries and improve performance,
|
||||
# as it's part of a few ir.rules.
|
||||
# If we're going to inject too many `ids`, we fall back on the default behavior
|
||||
# to avoid a performance regression.
|
||||
IN_MAX = 10_000
|
||||
domain = Domain('employee_ids', operator, value)
|
||||
user_ids = self.env['res.users'].with_context(active_test=False)._search(domain, limit=IN_MAX).get_result_ids()
|
||||
if len(user_ids) < IN_MAX:
|
||||
return Domain('id', 'in', user_ids)
|
||||
|
||||
return domain
|
||||
|
||||
def action_create_employee(self):
|
||||
self.ensure_one()
|
||||
if self.env.company not in self.company_ids:
|
||||
raise AccessError(_("You are not allowed to create an employee because the user does not have access rights for %s", self.env.company.name))
|
||||
self.env['hr.employee'].create(dict(
|
||||
name=self.name,
|
||||
company_id=self.env.company.id,
|
||||
**self.env['hr.employee']._sync_user(self)
|
||||
))
|
||||
|
||||
def action_open_employees(self):
|
||||
self.ensure_one()
|
||||
employees = self.employee_ids
|
||||
model = 'hr.employee' if self.env.user.has_group('hr.group_hr_user') else 'hr.employee.public'
|
||||
if len(employees) > 1:
|
||||
return {
|
||||
'name': _('Related Employees'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': model,
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('id', 'in', employees.ids)],
|
||||
}
|
||||
return {
|
||||
'name': _('Employee'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': model,
|
||||
'res_id': employees.id,
|
||||
'view_mode': 'form',
|
||||
}
|
||||
|
||||
def action_related_contact(self):
|
||||
return {
|
||||
'name': _("Related Contact"),
|
||||
'res_id': self.partner_id.id,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'res.partner',
|
||||
'view_mode': 'form',
|
||||
}
|
||||
|
||||
def get_formview_action(self, access_uid=None):
|
||||
""" Override this method in order to redirect many2one towards the full user form view
|
||||
incase the user is ERP manager and the request coming from employee form."""
|
||||
|
||||
res = super().get_formview_action(access_uid=access_uid)
|
||||
user = self.env.user
|
||||
if access_uid:
|
||||
user = self.env['res.users'].browse(access_uid).sudo()
|
||||
|
||||
if self.env.context.get('default_create_employee_id') and user.has_group('base.group_erp_manager'):
|
||||
res['views'] = [(self.env.ref('base.view_users_form').id, 'form')]
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -1,14 +1,137 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from pytz import utc, timezone
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo.addons.resource.models.resource import Intervals
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pytz import timezone
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.intervals import Intervals
|
||||
|
||||
|
||||
class ResourceResource(models.Model):
|
||||
_inherit = "resource.resource"
|
||||
|
||||
user_id = fields.Many2one(copy=False)
|
||||
employee_id = fields.One2many('hr.employee', 'resource_id', domain="[('company_id', '=', company_id)]")
|
||||
employee_id = fields.One2many('hr.employee', 'resource_id', check_company=True, context={'active_test': False})
|
||||
|
||||
job_title = fields.Char(compute='_compute_job_title', compute_sudo=True)
|
||||
department_id = fields.Many2one('hr.department', compute='_compute_department_id', compute_sudo=True)
|
||||
work_location_id = fields.Many2one(related='employee_id.work_location_id')
|
||||
work_email = fields.Char(related='employee_id.work_email')
|
||||
work_phone = fields.Char(related='employee_id.work_phone')
|
||||
show_hr_icon_display = fields.Boolean(related='employee_id.show_hr_icon_display')
|
||||
hr_icon_display = fields.Selection(related='employee_id.hr_icon_display')
|
||||
calendar_id = fields.Many2one(inverse='_inverse_calendar_id')
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_job_title(self):
|
||||
for resource in self:
|
||||
resource.job_title = resource.employee_id.job_title
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_department_id(self):
|
||||
for resource in self:
|
||||
resource.department_id = resource.employee_id.department_id
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_avatar_128(self):
|
||||
is_hr_user = self.env.user.has_group('hr.group_hr_user')
|
||||
if not is_hr_user:
|
||||
public_employees = self.env['hr.employee.public'].with_context(active_test=False).search([
|
||||
('resource_id', 'in', self.ids),
|
||||
])
|
||||
avatar_per_employee_id = {emp.id: emp.avatar_128 for emp in public_employees}
|
||||
|
||||
for resource in self:
|
||||
employee = resource.employee_id
|
||||
if not employee:
|
||||
resource.avatar_128 = False
|
||||
continue
|
||||
if is_hr_user:
|
||||
resource.avatar_128 = employee[0].avatar_128
|
||||
else:
|
||||
resource.avatar_128 = avatar_per_employee_id[employee[0].id]
|
||||
|
||||
def _inverse_calendar_id(self):
|
||||
for resource in self:
|
||||
if resource.calendar_id != resource.employee_id.resource_calendar_id:
|
||||
resource.employee_id.resource_calendar_id = resource.calendar_id
|
||||
|
||||
def _get_resource_without_contract(self):
|
||||
employee_ids_with_active_contracts = {
|
||||
employee.id for [employee] in
|
||||
self.env['hr.version']._read_group(
|
||||
domain=[
|
||||
('employee_id', 'in', self.employee_id.ids),
|
||||
('contract_date_start', '!=', False),
|
||||
],
|
||||
groupby=['employee_id'],
|
||||
)
|
||||
}
|
||||
return self.filtered(
|
||||
lambda r: not r.employee_id
|
||||
or not r.employee_id.id in employee_ids_with_active_contracts
|
||||
)
|
||||
|
||||
def _get_contracts_valid_periods(self, start, end):
|
||||
res = defaultdict(lambda: defaultdict(Intervals))
|
||||
timezones = {resource.tz for resource in self}
|
||||
date_start = min(start.astimezone(timezone(tz)).date() for tz in timezones)
|
||||
date_end = max(end.astimezone(timezone(tz)).date() for tz in timezones)
|
||||
contracts = self.employee_id._get_versions_with_contract_overlap_with_period(date_start, date_end)
|
||||
for contract in contracts:
|
||||
tz = timezone(contract.employee_id.tz)
|
||||
res[contract.employee_id.resource_id.id][contract.resource_calendar_id] |= Intervals([(
|
||||
tz.localize(datetime.combine(contract.contract_date_start, datetime.min.time())) if contract.contract_date_start > start.astimezone(tz).date() else start,
|
||||
tz.localize(datetime.combine(contract.contract_date_end, datetime.max.time())) if contract.contract_date_end and contract.contract_date_end < end.astimezone(tz).date() else end,
|
||||
self.env['resource.calendar.attendance']
|
||||
)])
|
||||
return res
|
||||
|
||||
def _get_calendars_validity_within_period(self, start, end, default_company=None):
|
||||
assert start.tzinfo and end.tzinfo
|
||||
if not self:
|
||||
return super()._get_calendars_validity_within_period(start, end, default_company=default_company)
|
||||
calendars_within_period_per_resource = defaultdict(lambda: defaultdict(Intervals)) # keys are [resource id:integer][calendar:self.env['resource.calendar']]
|
||||
# Employees that have ever had an active contract
|
||||
resource_without_contract = self._get_resource_without_contract()
|
||||
if resource_without_contract:
|
||||
calendars_within_period_per_resource.update(
|
||||
super(ResourceResource, resource_without_contract)._get_calendars_validity_within_period(start, end, default_company=default_company)
|
||||
)
|
||||
resource_with_contract = self - resource_without_contract
|
||||
if not resource_with_contract:
|
||||
return calendars_within_period_per_resource
|
||||
|
||||
calendars_within_period_per_resource.update(resource_with_contract._get_contracts_valid_periods(start, end))
|
||||
return calendars_within_period_per_resource
|
||||
|
||||
def _get_flexible_resources_calendars_validity_within_period(self, start, end):
|
||||
assert start.tzinfo and end.tzinfo
|
||||
resource_default_work_intervals = self._get_flexible_resources_default_work_intervals(start, end)
|
||||
|
||||
calendars_within_period_per_resource = defaultdict(lambda: defaultdict(Intervals)) # keys are [resource id:integer][calendar:self.env['resource.calendar']]
|
||||
# Employees that have ever had an active contract
|
||||
resource_without_contract = self.sudo()._get_resource_without_contract()
|
||||
for resource in resource_without_contract:
|
||||
calendar = False if resource._is_fully_flexible() else resource.calendar_id
|
||||
calendars_within_period_per_resource[resource.id][calendar] = resource_default_work_intervals[resource.id]
|
||||
|
||||
resource_with_contract = self - resource_without_contract
|
||||
if resource_with_contract:
|
||||
resource_contracts_valid_periods = resource_with_contract.sudo()._get_contracts_valid_periods(start, end)
|
||||
# r: calendar: Intervals
|
||||
for resource_id, calendar_intervals in resource_contracts_valid_periods.items():
|
||||
for calendar_id, intervals in calendar_intervals.items():
|
||||
calendars_within_period_per_resource[resource_id][calendar_id] = intervals & resource_default_work_intervals[resource_id]
|
||||
|
||||
return calendars_within_period_per_resource
|
||||
|
||||
def _get_calendar_at(self, date_target, tz=False):
|
||||
result = super()._get_calendar_at(date_target)
|
||||
resources_with_employee = self.filtered(lambda r: r.employee_id)
|
||||
employee_calendars = resources_with_employee.employee_id._get_calendars(date_target.astimezone(tz))
|
||||
for resource in resources_with_employee:
|
||||
result[resource] = employee_calendars[resource.employee_id.id]
|
||||
return result
|
||||
|
|
|
|||
24
odoo-bringout-oca-ocb-hr/hr/models/resource_calendar.py
Normal file
24
odoo-bringout-oca-ocb-hr/hr/models/resource_calendar.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
from odoo import fields, models
|
||||
from odoo.fields import Domain
|
||||
|
||||
|
||||
class ResourceCalendar(models.Model):
|
||||
_inherit = 'resource.calendar'
|
||||
|
||||
def transfer_leaves_to(self, other_calendar, resources=None, from_date=None):
|
||||
"""
|
||||
Transfer some resource.calendar.leaves from 'self' to another calendar 'other_calendar'.
|
||||
Transfered leaves linked to `resources` (or all if `resources` is None) and starting
|
||||
after 'from_date' (or today if None).
|
||||
"""
|
||||
from_date = from_date or fields.Datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
domain = [
|
||||
('calendar_id', 'in', self.ids),
|
||||
('date_from', '>=', from_date),
|
||||
]
|
||||
domain = Domain.AND([domain, [('resource_id', 'in', resources.ids)]]) if resources else domain
|
||||
|
||||
self.env['resource.calendar.leaves'].search(domain).write({
|
||||
'calendar_id': other_calendar.id,
|
||||
})
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from pytz import timezone, utc
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class ResourceCalendarLeaves(models.Model):
|
||||
_inherit = 'resource.calendar.leaves'
|
||||
|
||||
@api.depends('date_from')
|
||||
def _compute_calendar_id(self):
|
||||
def date2datetime(date, tz):
|
||||
dt = datetime.fromordinal(date.toordinal())
|
||||
return tz.localize(dt).astimezone(utc).replace(tzinfo=None)
|
||||
|
||||
leaves_by_contract = self.grouped(lambda leave: leave.resource_id.employee_id.version_id)
|
||||
# set aside leaves without version_id for super
|
||||
remaining = leaves_by_contract.pop(
|
||||
self.env['hr.version'],
|
||||
self.env['resource.calendar.leaves'],
|
||||
)
|
||||
for contract, leaves in leaves_by_contract.items():
|
||||
tz = timezone(contract.resource_calendar_id.tz or 'UTC')
|
||||
start_dt = date2datetime(contract.date_start, tz)
|
||||
end_dt = date2datetime(contract.date_end, tz) if contract.date_end else datetime.max
|
||||
# only modify leaves that fall under the active contract
|
||||
leaves.filtered(
|
||||
lambda leave: leave.date_from and start_dt <= leave.date_from < end_dt
|
||||
).calendar_id = contract.resource_calendar_id
|
||||
|
||||
super(ResourceCalendarLeaves, remaining)._compute_calendar_id()
|
||||
Loading…
Add table
Add a link
Reference in a new issue