1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/hr_contract_salary/models/hr_contract.py
2024-12-10 09:04:09 +07:00

333 lines
15 KiB
Python

# -*- 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