# -*- coding:utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging from datetime import date from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.models import MAGIC_COLUMNS from odoo.fields import Date from odoo.exceptions import ValidationError from odoo.tools import html_sanitize _logger = logging.getLogger(__name__) class HrContract(models.Model): _inherit = 'hr.contract' origin_contract_id = fields.Many2one('hr.contract', string="Origin Contract", domain="[('company_id', '=', company_id)]", help="The contract from which this contract has been duplicated.") is_origin_contract_template = fields.Boolean(compute='_compute_is_origin_contract_template', string='Is origin contract a contract template?', readonly=True) hash_token = fields.Char('Created From Token', copy=False) applicant_id = fields.Many2one('hr.applicant', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") contract_reviews_count = fields.Integer(compute="_compute_contract_reviews_count", string="Proposed Contracts Count") default_contract_id = fields.Many2one( 'hr.contract', string="Contract Template", compute="_compute_default_contract", store=True, readonly=False, domain="[('company_id', '=', company_id), ('employee_id', '=', False)]", help="Default contract used when making an offer to an applicant.") sign_template_id = fields.Many2one('sign.template', compute='_compute_sign_template_id', readonly=False, store=True, string="New Contract Document Template", help="Default document that the applicant will have to sign to accept a contract offer.") contract_update_template_id = fields.Many2one( 'sign.template', string="Contract Update Document Template", compute='_compute_contract_update_template_id', store=True, readonly=False, help="Default document that the employee will have to sign to update his contract.") signatures_count = fields.Integer(compute='_compute_signatures_count', string='# Signatures', help="The number of signatures on the pdf contract with the most signatures.") image_1920_filename = fields.Char() image_1920 = fields.Image(related='employee_id.image_1920', groups="hr_contract.group_hr_contract_manager", readonly=False) # YTI FIXME: holidays and wage_with_holidays are defined twice... holidays = fields.Float(string='Extra Time Off', help="Number of days of paid leaves the employee gets per year.") wage_with_holidays = fields.Monetary(compute='_compute_wage_with_holidays', inverse='_inverse_wage_with_holidays', tracking=True, string="Wage with Holidays") wage_on_signature = fields.Monetary(string="Wage on Payroll", help="Wage on contract signature", tracking=True, group_operator="avg") salary_offer_ids = fields.One2many('hr.contract.salary.offer', 'employee_contract_id') salary_offers_count = fields.Integer(compute='_compute_salary_offers_count', compute_sudo=True) # Employer costs fields final_yearly_costs = fields.Monetary( compute='_compute_final_yearly_costs', readonly=False, store=True, string="Yearly Cost (Real)", tracking=True, help="Total real yearly cost of the employee for the employer.", group_operator="avg") monthly_yearly_costs = fields.Monetary( compute='_compute_monthly_yearly_costs', string='Monthly Cost (Real)', readonly=True, help="Total real monthly cost of the employee for the employer.") @api.constrains('hr_responsible_id', 'sign_template_id') def _check_hr_responsible_id(self): for contract in self: if contract.sign_template_id and not (contract.hr_responsible_id.has_group('sign.group_sign_user') and contract.hr_responsible_id.email_formatted): raise ValidationError(_("HR Responsible %s should be a User of Sign and have a valid email address when New Contract Document Template is specified", contract.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_contract_id') def _compute_is_origin_contract_template(self): for contract in self: contract.is_origin_contract_template = contract.origin_contract_id and not contract.origin_contract_id.employee_id @api.depends('job_id') def _compute_default_contract(self): for contract in self: if not contract.job_id or not contract.job_id.default_contract_id: continue contract.default_contract_id = contract.job_id.default_contract_id @api.onchange('default_contract_id') def _onchange_default_contract_id(self): if self.default_contract_id.hr_responsible_id: self.hr_responsible_id = self.default_contract_id.hr_responsible_id def _compute_salary_offers_count(self): offers_data = self.env['hr.contract.salary.offer']._read_group( domain=[('employee_contract_id', 'in', self.ids)], groupby=['employee_contract_id'], aggregates=['__count']) mapped_data = {contract.id: count for contract, count in offers_data} for contract in self: contract.salary_offers_count = mapped_data.get(contract.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 contract in self: if contract._is_salary_sacrifice(): yearly_cost = contract._get_yearly_cost_from_wage_with_holidays() contract.wage_with_holidays = contract._get_gross_from_employer_costs(yearly_cost) else: contract.wage_with_holidays = contract.wage def _inverse_wage_with_holidays(self): for contract in self: if contract._is_salary_sacrifice(): yearly = contract._get_yearly_cost_from_wage_with_holidays(self.wage_with_holidays) contract.wage = contract._get_gross_from_employer_costs(yearly) else: if contract.wage != contract.wage_with_holidays: contract.wage = contract.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 = set( 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)) if not triggers: benefit_fields |= {'wage_with_holidays'} return tuple(benefit_fields - self._benefit_black_list()) @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.impacts_net_salary', *self._get_benefit_fields())) def _compute_final_yearly_costs(self): for contract in self: if abs(contract.final_yearly_costs - contract._get_yearly_cost_from_wage()) > 0.10: contract.final_yearly_costs = contract._get_yearly_cost_from_wage() @api.depends('company_id', 'job_id') def _compute_structure_type_id(self): contracts = self.env['hr.contract'] for contract in self: if contract.job_id and contract.job_id.default_contract_id and contract.job_id.default_contract_id.structure_type_id: contract.structure_type_id = contract.job_id.default_contract_id.structure_type_id else: contracts |= contract super(HrContract, contracts)._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 contract in self: contract.monthly_yearly_costs = contract.final_yearly_costs / 12.0 def _get_benefits_costs(self): self.ensure_one() benefits = self.env['hr.contract.salary.benefit'].search([ ('impacts_net_salary', '=', True), ('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] if benefit.cost_field in self else 0 for benefit in monthly_benefits) yearly_cost = sum(self[benefit.cost_field] if benefit.cost_field in self else 0 for benefit in benefits - monthly_benefits) 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() return remaining_for_gross / self._get_salary_costs_factor() @api.depends('sign_request_ids.nb_closed') def _compute_signatures_count(self): for contract in self: contract.signatures_count = max(contract.sign_request_ids.mapped('nb_closed') or [0]) @api.depends('origin_contract_id') def _compute_contract_reviews_count(self): for contract in self: contract.contract_reviews_count = self.with_context(active_test=False).search_count( [('origin_contract_id', '=', contract.id)]) @api.depends('default_contract_id') def _compute_sign_template_id(self): for contract in self: if contract.default_contract_id: contract.sign_template_id = contract.default_contract_id.sign_template_id @api.depends('default_contract_id') def _compute_contract_update_template_id(self): for contract in self: if contract.default_contract_id and contract.id != contract.default_contract_id.id: contract.contract_update_template_id = contract.default_contract_id.contract_update_template_id def _get_redundant_salary_data(self): employees = self.mapped('employee_id').filtered(lambda employee: not employee.active) partners = employees.work_contact_id.filtered(lambda partner: not partner.active) return [employees, partners] def _clean_redundant_salary_data(self): # Unlink archived draft contract older than 7 days linked to a signature # Unlink the related employee, partner, and new car (if any) seven_days_ago = date.today() + relativedelta(days=-7) contracts = self.search([ ('state', '=', 'draft'), ('active', '=', False), ('sign_request_ids', '!=', False), ('create_date', '<=', Date.to_string(seven_days_ago))]) records_to_unlink = contracts._get_redundant_salary_data() for records in records_to_unlink: if not records: continue _logger.info('Salary: About to unlink %s: %s' % (records._name, records.ids)) for record in records: try: record.unlink() except ValueError: pass def action_show_contract_reviews(self): return { "type": "ir.actions.act_window", "res_model": "hr.contract", "views": [[False, "tree"], [False, "form"]], "domain": [["origin_contract_id", "=", self.id], '|', ["active", "=", False], ["active", "=", True]], "name": "Contracts Reviews", } def action_view_origin_contract(self): action = self.env["ir.actions.actions"]._for_xml_id("hr_contract.action_hr_contract") action['views'] = [(self.env.ref('hr_contract.hr_contract_view_form').id, 'form')] action['res_id'] = self.origin_contract_id.id return action def action_show_offers(self): self.ensure_one() action = self.env['ir.actions.act_window']._for_xml_id('hr_contract_salary.hr_contract_salary_offer_action') action['domain'] = [('id', 'in', self.salary_offer_ids.ids)] action['context'] = {'default_employee_contract_id': self.id} if self.salary_offers_count == 1: action.update({ "views": [[False, "form"]], "res_id": self.salary_offer_ids.id, }) return action 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.contract', 'default_res_ids': self.ids, 'default_template_id': template_id, 'default_composition_mode': 'comment', 'salary_package_url': self.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([('default_contract_id', 'in', self.ids)]) job_positions.default_contract_id = False return res