Files

387 lines
18 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.models import MAGIC_COLUMNS
from odoo.exceptions import ValidationError
from odoo.tools import html_sanitize
_logger = logging.getLogger(__name__)
class HrVersion(models.Model):
_inherit = 'hr.version'
def _default_get_template_warning(self):
sign_template_count = self.env['sign.template'].sudo().search_count([('active', '=', True)], limit=1)
return not sign_template_count and _('No templates are configured yet. Do you want to set up first one?')
origin_version_id = fields.Many2one(
'hr.version', string="Origin Contract", domain="[('company_id', '=', company_id)]",
groups="hr.group_hr_user", help="The contract from which this contract has been duplicated.", tracking=True)
is_origin_contract_template = fields.Boolean(
compute='_compute_is_origin_contract_template', string='Is origin contract a contract template?',
groups="hr.group_hr_user", readonly=True)
hash_token = fields.Char('Created From Token', groups="hr.group_hr_user", tracking=True)
applicant_id = fields.Many2one('hr.applicant', groups="hr.group_hr_user",
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True)
contract_reviews_count = fields.Integer(compute="_compute_contract_reviews_count",
string="Proposed Contracts Count")
contract_template_id = fields.Many2one(default=lambda self: self.job_id.contract_template_id or False)
sign_template_id = fields.Many2one(
'sign.template', compute='_compute_sign_template_id', readonly=False, store=True, copy=True,
string="New Contract Template", groups="hr.group_hr_user",
help="Default document that the applicant will have to sign to accept a contract offer.")
sign_template_signatories_ids = fields.One2many('hr.contract.signatory', 'contract_template_id',
compute="_compute_sign_template_signatories_ids", store=True,
readonly=False, groups="hr.group_hr_user")
contract_update_template_id = fields.Many2one(
'sign.template', string="Contract Update", groups="hr.group_hr_user",
compute='_compute_contract_update_template_id', store=True, readonly=False, copy=True,
help="Default document that the employee will have to sign to update his contract.")
contract_update_signatories_ids = fields.One2many('hr.contract.signatory', 'update_contract_template_id',
compute="_compute_contract_update_signatories_ids", store=True,
readonly=False, groups="hr.group_hr_user")
signatures_count = fields.Integer(compute='_compute_signatures_count', string='# Signatures',
help="The number of signatures on the pdf contract with the most signatures.", groups="hr.group_hr_user")
image_1920_filename = fields.Char(groups="hr.group_hr_user", tracking=True)
image_1920 = fields.Image(related='employee_id.image_1920', groups="hr.group_hr_manager", readonly=False)
# YTI FIXME: holidays and wage_with_holidays are defined twice...
holidays = fields.Float(string='Extra Time Off', groups="hr.group_hr_user",
help="Number of days of paid leaves the employee gets per year.", tracking=True)
wage_with_holidays = fields.Monetary(compute='_compute_wage_with_holidays', inverse='_inverse_wage_with_holidays',
tracking=True, string="Wage with Holidays", groups="hr.group_hr_manager")
wage_on_signature = fields.Monetary(string="Wage on Signature", tracking=True, aggregator="avg",
groups="hr.group_hr_manager")
salary_offer_ids = fields.One2many('hr.contract.salary.offer', 'employee_version_id', groups="hr.group_hr_manager", tracking=True)
originated_offer_id = fields.Many2one('hr.contract.salary.offer', help="The original offer",
groups="hr.group_hr_user", tracking=True)
salary_offers_count = fields.Integer(compute='_compute_salary_offers_count', compute_sudo=True)
template_warning = fields.Char(default=_default_get_template_warning, store=False, groups="hr.group_hr_user")
# Employer costs fields
final_yearly_costs = fields.Monetary(
compute='_compute_final_yearly_costs',
readonly=False, store=True,
string="Yearly Cost",
tracking=True,
aggregator="avg",
groups="hr.group_hr_manager")
monthly_yearly_costs = fields.Monetary(
compute='_compute_monthly_yearly_costs', string='Monthly Cost', readonly=True, groups="hr.group_hr_manager")
@api.constrains('sign_template_signatories_ids')
def _check_signatories_unicity(self):
for version in self:
roles = [i.sign_role_id for i in version.sign_template_signatories_ids]
if len(roles) != len(set(roles)):
raise ValidationError(_("You cannot have multiple person responsible for the same role on contract signature template."))
@api.constrains('contract_update_signatories_ids')
def _check_update_signatories_unicity(self):
for version in self:
roles = [i.sign_role_id for i in version.contract_update_signatories_ids]
if len(roles) != len(set(roles)):
raise ValidationError(_("You cannot have multiple person responsible for the same role on contract update signature template."))
@api.depends('sign_template_id')
def _compute_sign_template_signatories_ids(self):
for version in self:
version.sign_template_signatories_ids = self.env['hr.contract.signatory'].create_empty_signatories(version.sign_template_id)
@api.depends('contract_update_template_id')
def _compute_contract_update_signatories_ids(self):
for version in self:
version.contract_update_signatories_ids = self.env['hr.contract.signatory'].create_empty_signatories(version.contract_update_template_id)
@api.constrains('hr_responsible_id', 'sign_template_id')
def _check_hr_responsible_id(self):
for version in self:
if version.sign_template_id:
if not version.hr_responsible_id.has_group('sign.group_sign_user'):
raise ValidationError(_("HR Responsible %s should be a user of Sign when New Contract Document Template is specified", version.hr_responsible_id.name))
if not version.hr_responsible_id.email_formatted:
raise ValidationError(_("HR Responsible %s should have a valid email address when New Contract Document Template is specified", version.hr_responsible_id.name))
@api.depends('wage', 'wage_on_signature')
def _compute_contract_wage(self):
super()._compute_contract_wage()
def _get_contract_wage_field(self):
self.ensure_one()
if self._is_struct_from_country('BE'):
return 'wage_on_signature'
return super()._get_contract_wage_field()
@api.depends('origin_version_id')
def _compute_is_origin_contract_template(self):
for version in self:
version.is_origin_contract_template = version.origin_version_id and not version.origin_version_id.employee_id
def _compute_salary_offers_count(self):
offers_data = self.env['hr.contract.salary.offer']._read_group(
domain=[('employee_version_id', 'in', self.ids)],
groupby=['employee_version_id'],
aggregates=['__count'])
mapped_data = {version.id: count for version, count in offers_data}
for version in self:
version.salary_offers_count = mapped_data.get(version.id, 0)
def _get_yearly_cost_sacrifice_ratio(self):
return 1.0 - self.holidays / 231.0
def _get_yearly_cost_sacrifice_fixed(self):
return 0.0
def _get_yearly_cost_from_wage_with_holidays(self, wage_with_holidays=False):
self.ensure_one()
ratio = self._get_yearly_cost_sacrifice_ratio()
fixed = self._get_yearly_cost_sacrifice_fixed()
if wage_with_holidays:
return (self._get_benefits_costs() + self._get_salary_costs_factor() * wage_with_holidays + fixed) / ratio
return self.final_yearly_costs * ratio - fixed
def _get_yearly_cost_from_wage(self):
self.ensure_one()
fixed = self._get_yearly_cost_sacrifice_fixed()
return self._get_benefits_costs() + self._get_salary_costs_factor() * self.wage + fixed
def _is_salary_sacrifice(self):
self.ensure_one()
return self.holidays
@api.depends('holidays', 'wage', 'final_yearly_costs')
def _compute_wage_with_holidays(self):
for version in self:
if version._is_salary_sacrifice():
yearly_cost = version._get_yearly_cost_from_wage_with_holidays()
version.wage_with_holidays = version._get_gross_from_employer_costs(yearly_cost)
else:
version.wage_with_holidays = version.wage
def _inverse_wage_with_holidays(self):
for version in self:
if version._is_salary_sacrifice():
yearly = version._get_yearly_cost_from_wage_with_holidays(version.wage_with_holidays)
version.wage = version._get_gross_from_employer_costs(yearly)
else:
if version.wage != version.wage_with_holidays:
version.wage = version.wage_with_holidays
def _get_benefit_description(self, benefit, new_value=None):
self.ensure_one()
if hasattr(self, '_get_description_%s' % benefit.field):
description = getattr(self, '_get_description_%s' % benefit.field)(new_value)
else:
description = benefit.description
return html_sanitize(description)
def _get_benefit_fields(self, triggers=True):
types = ('float', 'integer', 'monetary', 'boolean')
if not triggers:
types += ('text',)
nonstored_whitelist = self._benefit_white_list()
benefit_fields = {
field.name
for field in self._fields.values()
if field.type in types and (field.store or not field.store and field.name in nonstored_whitelist) and not field.name.startswith("x_studio_")
}
if not triggers:
benefit_fields |= {'wage_with_holidays'}
return tuple(benefit_fields - self._benefit_black_list())
def _get_employee_vals_to_update(self):
vals = super()._get_employee_vals_to_update()
if self.originated_offer_id and self.originated_offer_id.job_title:
vals['job_title'] = self.originated_offer_id.job_title
return vals
@api.model
def _benefit_black_list(self):
return set(MAGIC_COLUMNS + [
'wage_on_signature', 'active',
'date_generated_from', 'date_generated_to'])
@api.model
def _benefit_white_list(self):
return []
@api.depends(lambda self: (
'wage',
'structure_type_id.salary_benefits_ids.res_field_id',
'structure_type_id.salary_benefits_ids.cost_res_field_id',
*self._get_benefit_fields()))
def _compute_final_yearly_costs(self):
for version in self:
if abs(version.final_yearly_costs - version._get_yearly_cost_from_wage()) > 0.10:
version.final_yearly_costs = version._get_yearly_cost_from_wage()
@api.depends('company_id', 'job_id')
def _compute_structure_type_id(self):
versions = self.env['hr.version']
for version in self:
if version.job_id and version.job_id.contract_template_id and version.job_id.contract_template_id.structure_type_id:
version.structure_type_id = version.job_id.contract_template_id.structure_type_id
else:
versions |= version
super(HrVersion, versions)._compute_structure_type_id()
@api.onchange("wage_with_holidays")
def _onchange_wage_with_holidays(self):
self._inverse_wage_with_holidays()
@api.onchange('final_yearly_costs')
def _onchange_final_yearly_costs(self):
final_yearly_costs = self.final_yearly_costs
self.wage = self._get_gross_from_employer_costs(final_yearly_costs)
self.env.remove_to_compute(self._fields['final_yearly_costs'], self)
self.final_yearly_costs = final_yearly_costs
@api.depends('final_yearly_costs')
def _compute_monthly_yearly_costs(self):
for version in self:
version.monthly_yearly_costs = version.final_yearly_costs / 12.0
def _get_benefits_costs(self):
self.ensure_one()
benefits = self.env['hr.contract.salary.benefit'].search([
('structure_type_id', '=', self.structure_type_id.id),
('cost_res_field_id', '!=', False),
])
if not benefits:
return 0
monthly_benefits = benefits.filtered(lambda a: a.benefit_type_id.periodicity == 'monthly')
monthly_cost = sum(self[benefit.cost_field] or 0 for benefit in monthly_benefits if benefit.cost_field in self)
yearly_cost = sum(self[benefit.cost_field] or 0 for benefit in benefits - monthly_benefits if benefit.cost_field in self)
return monthly_cost * 12 + yearly_cost
def _get_gross_from_employer_costs(self, yearly_cost):
self.ensure_one()
remaining_for_gross = yearly_cost - self._get_benefits_costs()
salary_costs_factor = self._get_salary_costs_factor()
if salary_costs_factor:
return remaining_for_gross / salary_costs_factor
return 0
def _get_employer_costs_from_gross(self, gross):
self.ensure_one()
return (gross * self._get_salary_costs_factor()) + self._get_benefits_costs()
@api.depends('sign_request_ids.nb_closed')
def _compute_signatures_count(self):
for version in self:
version.signatures_count = max(version.sign_request_ids.mapped('nb_closed') or [0])
@api.depends('origin_version_id')
def _compute_contract_reviews_count(self):
data = dict(self.with_context(active_test=False)._read_group(
[('origin_version_id', 'in', self.ids)],
['origin_version_id'],
['__count'],
))
for version in self:
version.contract_reviews_count = data.get(version, 0)
@api.depends('contract_template_id')
def _compute_sign_template_id(self):
for version in self:
if version.contract_template_id:
version.sign_template_id = version.contract_template_id.sign_template_id
@api.depends('contract_template_id')
def _compute_contract_update_template_id(self):
for version in self:
if version.contract_template_id and version.id != version.contract_template_id.id:
version.contract_update_template_id = version.contract_template_id.contract_update_template_id
def action_show_contract_reviews(self):
return {
"type": "ir.actions.act_window",
"res_model": "hr.version",
"views": [[False, "list"], [False, "form"]],
"domain": [["origin_version_id", "=", self.id]],
"context": {"active_test": False},
"name": "Contracts Reviews",
}
def action_generate_offer(self):
offer_validity_period = int(self.env['ir.config_parameter'].sudo().get_param(
'hr_contract_salary.employee_salary_simulator_link_validity', default=30))
offer_values = self._get_offer_values()
offer_values['validity_days_count'] = offer_validity_period
offer = self.env['hr.contract.salary.offer'].with_context(
default_contract_template_id=self.id).create(offer_values)
self.message_post(
body=_("An %(offer)s has been sent by %(user)s to the employee (mail: %(email)s)",
offer=Markup("<a href='#' data-oe-model='hr.contract.salary.offer' data-oe-id='{offer_id}'>Offer</a>")
.format(offer_id=offer.id),
user=self.env.user.name,
email=self.employee_id.work_email
)
)
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'hr.contract.salary.offer',
'res_id': offer.id,
'views': [(False, 'form')],
'context': {'active_model': 'hr.version', 'default_employee_version_id': self.id}
}
def _get_offer_values(self):
self.ensure_one()
return {
'company_id': self.company_id.id,
'contract_template_id': self.id,
'employee_version_id': self.id,
'final_yearly_costs': self.final_yearly_costs,
'job_title': self.job_id.name,
'employee_job_id': self.job_id.id,
'department_id': self.department_id.id,
}
def _get_values_dict(self):
self.ensure_one()
return self.read(load=None)[0]
def _get_wage_to_apply(self):
# To be overriden in localizations if a new wage applies depending on selected benefits
self.ensure_one()
return self.wage_with_holidays
def send_offer(self):
self.ensure_one()
try:
template_id = self.env.ref('hr_contract_salary.mail_template_send_offer').id
except ValueError:
template_id = False
path = '/salary_package/contract/' + str(self.id)
ctx = {
'default_email_layout_xmlid': 'mail.mail_notification_light',
'default_model': 'hr.version',
'default_res_ids': self.ids,
'default_template_id': template_id,
'default_composition_mode': 'comment',
'salary_package_url': self.env['hr.version'].get_base_url() + path,
}
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [[False, 'form']],
'target': 'new',
'context': ctx,
}
def action_archive(self):
res = super().action_archive()
job_positions = self.env['hr.job'].search([('contract_template_id', 'in', self.ids)])
job_positions.contract_template_id = False
return res