Files
enterprise-addons/hr_contract_salary/controllers/main.py
T

1122 lines
59 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hashlib
from collections import defaultdict, OrderedDict
from odoo import fields, http, models, _, Command
from odoo.addons.hr_contract_salary.utils.hr_version import hr_version_context
from odoo.addons.sign.controllers.main import Sign
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools import consteq
from odoo.tools.image import image_data_uri
from werkzeug.exceptions import NotFound
from werkzeug.wsgi import get_current_url
from urllib.parse import urlparse, parse_qs
from datetime import datetime, timedelta
class SignContract(Sign):
@http.route()
def sign(self, sign_request_id, token, sms_token=False, signature=None, **kwargs):
result = super().sign(sign_request_id, token, sms_token=sms_token, signature=signature, **kwargs)
if result.get('success'):
request_item = request.env['sign.request.item'].sudo().search([('access_token', '=', token)])
version = request.env['hr.version'].sudo().search([
('sign_request_ids', 'in', request_item.sign_request_id.ids),
'|',
('active', '=', True),
('active', '=', False)
])
offer = request.env['hr.contract.salary.offer'].sudo().search([
('sign_request_ids', 'in', request_item.sign_request_id.ids)])
if offer.state in ['expired', 'refused']:
raise UserError(_('This offer is outdated, please request an updated link...'))
request_template_id = request_item.sign_request_id.template_id.id
# Only if the signed document is the document to sign from the salary package
version_documents = [
version.sign_template_id.id,
version.contract_update_template_id.id,
]
if version and request_template_id in version_documents:
self._update_version_on_signature(request_item, version, offer)
if request_item.sign_request_id.nb_closed == 1:
return dict(result, url='/salary_package/thank_you/' + str(offer.id))
return result
def _update_version_on_signature(self, request_item, version, offer):
wage_to_apply = version._get_wage_to_apply()
# Only the applicant/employee has signed
if request_item.sign_request_id.nb_closed == 1:
version.hash_token = False
if version.applicant_id:
version.applicant_id.employee_id = version.employee_id
# once user is created, never update the work_email through simulator.
if not version.employee_id.user_id:
version.employee_id.work_email = False
version.wage_on_signature = wage_to_apply
if not request_item.sign_request_id.nb_wait:
if version.employee_id:
version.employee_id.active = True
if version.applicant_id:
version.applicant_id._move_to_hired_stage()
if version.employee_id.work_contact_id:
version.employee_id.work_contact_id.active = True
self._create_activity_benefit(version, ('running', 'countersigned'))
self._send_benefit_sign_request(version)
offer.state = "full_signed"
else:
self._create_activity_benefit(version, ('running'))
offer.state = "half_signed"
# All signers have signed
if request_item.sign_request_id.nb_wait == 0:
offer._archive_future_versions()
current_employee_version = offer.employee_version_id if offer.employee_id else version.employee_id.current_version_id
must_archive_current_version = version.applicant_id or False
if current_employee_version.date_version >= version.date_version:
# then replace the current version with the new one signed. We must 'fake' the date_version
# to current date_version + 1 day (future has no versions) in order to be able to unarchive the
# new version without triggering the constraint if the two dates are equal
current_employee_version.date_version = version.date_version + timedelta(days=1)
must_archive_current_version = True
request.env.flush_all()
version.write({'active': True})
if must_archive_current_version:
current_employee_version.write({'active': False})
if version.employee_id:
version.employee_id.active = True
if version.applicant_id:
version.applicant_id._move_to_hired_stage()
if version.employee_id.work_contact_id:
version.employee_id.work_contact_id.active = True
self._create_activity_benefit(version, ('countersigned'))
self._send_benefit_sign_request(version)
offer.state = "full_signed"
def _create_activity_benefit(self, version, contract_states):
benefits = request.env['hr.contract.salary.benefit'].sudo().search([
('structure_type_id', '=', version.structure_type_id.id),
('activity_type_id', '!=', False),
('activity_creation', 'in', contract_states)])
for benefit in benefits:
field = benefit.field
value = version[field] if benefit.source == 'field' else version._get_property_input_value(benefit.salary_rule_id.code)
origin_value = version.origin_version_id[field] if benefit.source == 'field' else version.origin_version_id._get_property_input_value(benefit.salary_rule_id.code)
if (benefit.activity_creation_type == "onchange" and value != origin_value) or \
benefit.activity_creation_type == "always" and value:
version.activity_schedule(
activity_type_id=benefit.activity_type_id.id,
note="%s: %s" % (benefit.name or benefit.field, value),
user_id=benefit.activity_responsible_id.id)
def _send_benefit_sign_request(self, version):
benefits = request.env['hr.contract.salary.benefit'].sudo().search([
('structure_type_id', '=', version.structure_type_id.id),
('sign_template_id', '!=', False)])
# ask the contract responsible to create sign requests
SignRequestSudo = request.env['sign.request'].with_user(version.hr_responsible_id).sudo()
sent_templates = request.env['sign.template']
for benefit in benefits:
field = benefit.field
value = version[field] if benefit.source == 'field' else version._get_property_input_value(benefit.salary_rule_id.code)
origin_value = version.origin_version_id[field] if benefit.source == 'field' else version.origin_version_id._get_property_input_value(benefit.salary_rule_id.code)
sign_template = benefit.sign_template_id
if sign_template in sent_templates:
continue
if (benefit.activity_creation_type == "onchange" and value != origin_value) or \
benefit.activity_creation_type == "always" and value:
sent_templates |= sign_template
request_items = []
template_roles = sign_template.sign_item_ids.responsible_id
if request.env.ref('hr_sign.sign_item_role_employee_signatory') in template_roles:
request_items.append(Command.create({'role_id': request.env.ref('hr_sign.sign_item_role_employee_signatory').id,
'partner_id': version.employee_id.work_contact_id.id}))
if request.env.ref('hr_sign.sign_item_role_job_responsible') in template_roles:
request_items.append(Command.create({'role_id': request.env.ref('hr_sign.sign_item_role_job_responsible').id,
'partner_id': version.hr_responsible_id.partner_id.id}))
sign_request_sudo = SignRequestSudo.create({
'template_id': sign_template.id,
'request_item_ids': request_items,
'reference': _('Signature Request - %s', benefit.name or version.name),
'subject': _('Signature Request - %s', benefit.name or version.name),
})
sign_request_sudo.message_subscribe(partner_ids=benefit.sign_copy_partner_id.ids)
sign_request_sudo.toggle_favorited()
version.sign_request_ids += sign_request_sudo
class HrContractSalary(http.Controller):
def _get_default_template_values(self, version, offer):
values = self._get_salary_package_values(version, offer)
values.update({
'redirect_to_job': False,
# YTI PROBABLY TO REMOVE
'applicant_id': offer.applicant_id.id,
'employee_version_id': offer.employee_version_id.id,
'employee_job_id': offer.employee_job_id.id,
'department_id': offer.department_id.id,
'job_title': offer.job_title,
'whitelist': False,
'part_time': False,
'final_yearly_costs': offer.final_yearly_costs,
})
return values
@http.route(['/salary_package/simulation/version/<int:version_id>'], type='http', auth="public", website=True, sitemap=False)
def salary_package_deprecated(self, version_id=None, **kw):
return request.render('http_routing.http_error', {
'status_code': _('Oops'),
'status_message': _('This offer is outdated, please request an updated link...')})
def _can_submit_offer(self, values):
return not values['redirect_to_job']
def check_access_to_salary_configurator(self, request_token, offer, version):
"""
Methods of access:
1 - User access (inside 'group_hr_manager')
2 - User access (the user of the offered employee, if exists)
3 - Token
"""
if not offer.exists() or offer.state in ['expired', 'refused']:
return False, request.render('http_routing.http_error', {
'status_code': self.env._('Oops'),
'status_message': self.env._('This offer has been updated, please request an updated link..')})
if offer.offer_end_date and offer.offer_end_date < fields.Date.today():
error_msg = self.env._("This link is invalid. Please contact the HR Responsible to get a new one...")
return False, request.render('http_routing.http_error', {
'status_code': self.env._('Oops'),
'status_message': error_msg})
request_user = request.env.user
if request_user.has_group('hr.group_hr_manager'):
return True, None
if offer.access_token and request_token and consteq(offer.access_token, request_token):
return True, None
offer_user = offer.employee_id.user_id
if offer_user:
if offer_user == request_user:
return True, None
else:
version.with_user(request_user.id).with_context(
allowed_company_ids=request_user.company_ids.ids
).check_access('read')
return True, None
if offer.access_token:
if not request_token:
error_msg = self.env._('Access Denied: Missing Token')
else:
error_msg = self.env._('Access Denied: Invalid Token')
else:
raise NotFound()
return False, request.render('http_routing.http_error', {
'status_code': self.env._('Oops'),
'status_message': error_msg})
@http.route(['/salary_package/simulation/offer/<int:offer_id>'], type='http', auth="public", website=True, sitemap=False)
def salary_package(self, offer_id=None, **kw):
response = False
debug = request.session.debug
for bundle_name in ["web.assets_frontend", "web.assets_frontend_lazy"]:
request.env["ir.qweb"]._get_asset_nodes(bundle_name, debug=debug, js=True, css=True)
with hr_version_context(request, invalidate=True):
offer = request.env['hr.contract.salary.offer'].sudo().browse(offer_id)
version = offer._get_version()
has_access, error_page = self.check_access_to_salary_configurator(kw.get('token'), offer, version)
if not has_access:
return error_page
if offer.state == 'full_signed':
return request.render('http_routing.http_error', {
'status_code': _('Oops'),
'status_message': _('This offer has been fully signed, you can not sign it again..')})
if offer.applicant_id:
version = version.with_context(is_applicant=True)
values = self._get_default_template_values(version, offer)
for field_name, value in kw.items():
if field_name == 'job_id':
values['redirect_to_job'] = value
if field_name == 'allow':
values['whitelist'] = value
if field_name == 'part':
values['part_time'] = True
# Allow simulation on url's in public offers
if field_name == 'final_yearly_costs' and not (offer.applicant_id or offer.employee_id or offer.access_token):
values['final_yearly_costs'] = float(value)
new_gross = version.sudo()._get_gross_from_employer_costs(values['final_yearly_costs'])
version.write({
'wage': new_gross,
'final_yearly_costs': values['final_yearly_costs'],
})
refusal_reasons = request.env['hr.contract.salary.offer.refusal.reason'].search([])
values.update({
'need_personal_information': self._can_submit_offer(values),
'submit': self._can_submit_offer(values),
'default_mobile': request.env['ir.default'].sudo()._get('hr.version', 'mobile'),
'original_link': get_current_url(request.httprequest.environ),
'token': kw.get('token'),
'offer_id': offer.id,
'master_department_id': request.env['hr.department'].sudo().browse(int(values['department_id'])).master_department_id.id if values['department_id'] else False,
'refusal_reasons': refusal_reasons,
})
response = request.render("hr_contract_salary.salary_package", values)
response.flatten()
return response
@http.route(['/salary_package/thank_you/<int:offer_id>'], type='http', auth="public", website=True, sitemap=False)
def salary_package_thank_you(self, offer_id=None, **kw):
with hr_version_context(request, invalidate=True):
offer = request.env['hr.contract.salary.offer'].sudo().browse(offer_id)
version = offer._get_version()
result = request.render("hr_contract_salary.salary_package_thank_you", {
'responsible_name': version.hr_responsible_id.partner_id.name or version.job_id.user_id.partner_id.name,
'responsible_email': version.hr_responsible_id.work_email or version.job_id.user_id.partner_id.email,
'responsible_phone': version.hr_responsible_id.work_phone or version.job_id.user_id.partner_id.phone,
})
return result
def _get_personal_infos_countries(self, version, personal_info):
return request.env['res.country'].search([])
def _get_personal_infos_states(self, version, personal_info):
return request.env['res.country.state'].search([])
def _get_personal_infos_langs(self, version, personal_info):
return request.env['res.lang'].search([])
def _get_personal_infos(self, version, offer):
initial_values = {}
dropdown_options = {}
targets = {
'version_personal': version.with_context(active_test=False).employee_id.version_ids.sorted(
'create_date', reverse=True
)[0], # force prefill from the employee's last created version to get last up-to-date info
'employee': version.employee_id,
'bank_account': version.employee_id.primary_bank_account_id,
}
# PERSONAL INFOS
personal_infos = request.env['hr.contract.salary.personal.info'].sudo().search([
'|',
('structure_type_id', '=', False),
('structure_type_id', '=', version.structure_type_id.id)]).sorted(lambda info: (info.info_type_id.sequence, info.sequence))
mapped_personal_infos = defaultdict(lambda: request.env['hr.contract.salary.personal.info'])
for personal_info in personal_infos:
mapped_personal_infos[personal_info.info_type_id.name] |= personal_info
target = targets[personal_info.applies_on]
if personal_info.display_type == 'document':
if personal_info.field in target and target[personal_info.field]:
if target[personal_info.field][:7] == b'JVBERi0':
content = "data:application/pdf;base64,%s" % (target[personal_info.field].decode())
else:
content = image_data_uri(target[personal_info.field])
else:
content = False
initial_values[personal_info.field] = content
filename_field = personal_info.field + '_filename'
if filename_field in version._fields:
initial_values[personal_info.field + '_filename'] = version[filename_field]
elif filename_field in target:
initial_values[personal_info.field + '_filename'] = target[filename_field]
else:
initial_values[personal_info.field + '_filename'] = personal_info.field
else:
initial_values[personal_info.field] = target[personal_info.field] if personal_info.field in target else ''
if personal_info.display_type == 'dropdown':
# Set record id instead of browse record as value
if isinstance(initial_values[personal_info.field], models.BaseModel):
initial_values[personal_info.field] = initial_values[personal_info.field].id
if personal_info.dropdown_selection == 'specific':
values = [(value.value, value.name) for value in personal_info.value_ids]
elif personal_info.dropdown_selection == 'country':
values = [(country.id, country.name) for country in self._get_personal_infos_countries(version, personal_info)]
elif personal_info.dropdown_selection == 'state':
values = [(state.id, state.name, state.country_id.id) for state in self._get_personal_infos_states(version, personal_info)]
elif personal_info.dropdown_selection == 'lang':
values = [(lang.code, lang.name) for lang in self._get_personal_infos_langs(version, personal_info)]
dropdown_options[personal_info.field] = values
return mapped_personal_infos, dropdown_options, initial_values
def _get_benefits(self, version_vals, offer):
return request.env['hr.contract.salary.benefit'].sudo().search([
('structure_type_id', '=', version_vals.get('structure_type_id'))])
def _get_benefits_values(self, version, offer):
initial_values = {}
dropdown_options = {}
dropdown_group_options = {}
# benefits
benefits = self._get_benefits(version._get_values_dict(), offer)
mapped_benefits = defaultdict(lambda: request.env['hr.contract.salary.benefit'])
for benefit in benefits:
mapped_benefits[benefit.benefit_type_id] |= benefit
field = benefit.field
initial_values[field] = version[field] if benefit.source == 'field' else version._get_property_input_value(benefit.salary_rule_id.code)
if benefit.folded:
fold_field = 'fold_%s' % (benefit.field)
benefit_fold_field = benefit.fold_field or benefit.field
initial_values[fold_field] = version[benefit_fold_field] if benefit_fold_field and benefit_fold_field in version else 0
if benefit.display_type == 'manual':
manual_field = '%s_manual' % (benefit.field)
field = benefit.manual_field or benefit.field
initial_values[manual_field] = initial_values.get(field, False) or (version[field] if field and field in version else 0)
if benefit.display_type == 'text':
text_field = '%s_text' % (benefit.field)
field = benefit.manual_field or benefit.field
initial_values[text_field] = version[field] if field and field in version else ''
elif benefit.display_type == 'dropdown' or benefit.display_type == 'dropdown-group':
initial_values['select_%s' % field] = version[field]
dropdown_benefits = benefits.filtered(lambda a: a.display_type == 'dropdown')
for dropdown_benefit in dropdown_benefits:
dropdown_options[dropdown_benefit.field] = \
[(value.value, value.value) for value in dropdown_benefit.value_ids.filtered(lambda v: v.display_type == 'line')]
dropdown_group_benefits = benefits.filtered(lambda a: a.display_type == 'dropdown-group')
for dropdown_group_benefit in dropdown_group_benefits:
values = OrderedDict()
values[""] = []
current_section = ""
for value in dropdown_group_benefit.value_ids:
if value.display_type == 'section':
current_section = value.name
values[current_section] = []
else:
values[current_section].append((value.value, value.value))
dropdown_group_options[dropdown_group_benefit.field] = values
benefit_types = sorted(benefits.mapped('benefit_type_id'), key=lambda x: x.sequence)
mapped_dependent_benefits = defaultdict(lambda: '')
mapped_mandatory_benefits = defaultdict(lambda: '')
# When the dependent benefit is disabled, on hover over we display the information
# regarding which (mandatory) benefits need to be selected, in order to be able to select
# the (dependent) benefit in question. For this purpose, here we build the string for each dependent benefit.
# The string starts with the display name of the dependent benefit and is followed by the display names
# of the mandatory benefits, separated by semicolon.
mapped_mandatory_benefits_names = defaultdict(lambda: '')
for dependent_benefit in benefits:
if not dependent_benefit.field:
continue
mapped_mandatory_benefits_names[dependent_benefit] = (dependent_benefit.fold_label or dependent_benefit.name) + ';'
if dependent_benefit.folded:
dependent_name = 'fold_%s' % (dependent_benefit.field)
else:
dependent_name = dependent_benefit.field + '_' + dependent_benefit.display_type
dependent_benefit_str = dependent_name + ' '
for mandatory_benefit in dependent_benefit.benefit_ids:
mapped_dependent_benefits[mandatory_benefit] += dependent_benefit_str
if mandatory_benefit.folded:
mandatory_name = 'fold_%s' % (mandatory_benefit.field)
else:
mandatory_name = mandatory_benefit.field + '_' + mandatory_benefit.display_type
mapped_mandatory_benefits[dependent_benefit] += mandatory_name + ' '
mapped_mandatory_benefits_names[dependent_benefit] += (mandatory_benefit.fold_label or mandatory_benefit.name) + ';'
return mapped_benefits, mapped_dependent_benefits, mapped_mandatory_benefits, mapped_mandatory_benefits_names, benefit_types, dropdown_options, dropdown_group_options, initial_values
def _get_salary_package_values(self, version, offer):
mapped_personal_infos, dropdown_options_1, initial_values_1 = self._get_personal_infos(version, offer)
mapped_benefits, mapped_dependent_benefits, mandatory_benefits, mandatory_benefits_names, benefit_types, dropdown_options_2, dropdown_group_options, initial_values_2 = self._get_benefits_values(version, offer)
all_initial_values = {**initial_values_1, **initial_values_2}
all_initial_values = {key: round(value, 2) if isinstance(value, float) else value for key, value in all_initial_values.items()}
all_dropdown_options = {**dropdown_options_1, **dropdown_options_2}
return {
'version': version,
'states': request.env['res.country.state'].search([]),
'countries': request.env['res.country'].search([]),
'benefits': mapped_benefits,
'dependent_benefits': mapped_dependent_benefits,
'mandatory_benefits': mandatory_benefits,
'mandatory_benefits_names': mandatory_benefits_names,
'benefit_types': benefit_types,
'mapped_personal_infos': mapped_personal_infos,
'dropdown_options': all_dropdown_options,
'dropdown_group_options': dropdown_group_options,
'initial_values': all_initial_values,
}
def _get_new_version_values(self, version_vals, employee, benefits, offer):
version_benefits = self._get_benefits(version_vals, offer)
company = self.env['res.company'].browse(version_vals.get('company_id'))
# During simulation, the offer temporarily becomes an amendment. After rollback and cache invalidation, compute isn't triggered automatically!
offer._compute_is_contract_amendment()
effective_date = offer.contract_start_date or fields.Date.today().replace(day=1)
contract_date_start = offer.employee_version_id.contract_date_start if offer.is_contract_amendment else effective_date
contract_date_end = offer.employee_version_id.contract_date_end if offer.is_contract_amendment else offer.contract_end_date
new_version_vals = {
'active': False,
'name': version_vals.get('name') or _("Package Simulation"),
'job_id': offer.employee_job_id.id or version_vals.get('job_id') or employee.job_id.id,
'department_id': offer.department_id.id or version_vals.get('department_id') or employee.department_id.id,
'company_id': company.id,
'currency_id': company.currency_id.id,
'employee_id': employee.id,
'structure_type_id': version_vals.get('structure_type_id'),
'wage': benefits['wage'],
'final_yearly_costs': benefits['final_yearly_costs'],
'resource_calendar_id': version_vals.get('resource_calendar_id'),
'contract_template_id': offer.contract_template_id.id,
'hr_responsible_id': version_vals.get('hr_responsible_id'),
'sign_template_id': offer.sign_template_id.id,
'contract_update_template_id': version_vals.get('contract_update_template_id') or offer.contract_template_id.contract_update_template_id.id,
'date_version': effective_date,
'contract_date_start': contract_date_start,
'contract_date_end': contract_date_end,
'contract_type_id': version_vals.get('contract_type_id'),
'originated_offer_id': offer.id,
'address_id': employee.address_id.id,
'work_location_id': employee.work_location_id.id,
}
if 'work_entry_source' in version_vals:
new_version_vals['work_entry_source'] = version_vals.get('work_entry_source')
for benefit in version_benefits:
if not benefit.res_field_id or benefit.field not in version_vals:
continue
if hasattr(self.env['hr.version'], '_get_benefit_values_%s' % (benefit.field)):
new_version_vals.update(getattr(self.env['hr.version'], '_get_benefit_values_%s' % (benefit.field))(version_vals, benefits))
continue
if benefit.folded:
new_version_vals[benefit.fold_field or benefit.field] = benefits['fold_%s' % (benefit.field)]
if benefit.display_type == 'dropdown':
new_version_vals[benefit.field] = benefits[benefit.field]
if benefit.display_type in ['manual', 'text']:
new_version_vals[benefit.manual_field or benefit.field] = benefits['%s_%s' % (benefit.field, 'manual' if benefit.display_type == 'manual' else 'text')]
else:
new_version_vals[benefit.field] = benefits[benefit.field]
for field in offer._fields:
if field.startswith('x_') and field not in new_version_vals and field in version_vals:
new_version_vals[field] = offer[field]
return new_version_vals
def _update_personal_info(self, employee, version, personal_infos_values, no_name_write=False):
def resolve_value(field_name, values):
targets = {
'version_personal': request.env['hr.version'],
'employee': request.env['hr.employee'],
'bank_account': request.env['res.partner.bank'],
}
field_value = values[field_name]
target = targets[personal_info.applies_on]
if field_name in target and target._fields[field_name].relational:
field_value = int(field_value) if field_value else False
return field_value
def _is_valid_date(date):
return fields.Date.from_string(date) >= fields.Date.from_string('1900-01-01')
personal_infos = request.env['hr.contract.salary.personal.info'].sudo().search([
'|', ('structure_type_id', '=', False), ('structure_type_id', '=', version.structure_type_id.id)])
version_infos = personal_infos_values['version_personal']
employee_infos = personal_infos_values['employee']
bank_account_infos = personal_infos_values['bank_account']
for key in ['employee_job_id', 'department_id']:
try:
employee_infos[key] = int(employee_infos[key])
except (ValueError, TypeError):
employee_infos[key] = None
job = request.env['hr.job'].sudo().browse(employee_infos['employee_job_id'])
if not employee_infos['job_title']:
employee_infos['job_title'] = job.name
employee_vals = {}
version_vals = {}
work_contact_vals = {}
bank_account_vals = {}
attachment_create_vals = []
if employee_infos.get('birthday') and not _is_valid_date(employee_infos['birthday']):
employee_infos['birthday'] = ''
for personal_info in personal_infos:
field_name = personal_info.field
if personal_info.display_type == 'document' and not employee_infos.get(field_name):
continue
if field_name in employee_infos and personal_info.applies_on == 'employee':
employee_vals[field_name] = resolve_value(field_name, employee_infos)
elif field_name in version_infos and personal_info.applies_on == 'version_personal':
version_vals[field_name] = resolve_value(field_name, version_infos)
elif field_name in bank_account_infos and personal_info.applies_on == 'bank_account':
bank_account_vals[field_name] = resolve_value(field_name, bank_account_infos)
work_contact_vals['name'] = employee_vals.get('name', '')
work_contact_vals['email'] = employee_vals.get('private_email', '')
# Update personal info on the private address
if employee.work_contact_id:
if no_name_write or employee.user_id.name:
del work_contact_vals['name']
partner = employee.work_contact_id
# We shouldn't modify the partner email like this
if employee.work_contact_id.email:
work_contact_vals.pop('email', None)
partner.write(work_contact_vals)
else:
work_contact_vals['active'] = False
partner = request.env['res.partner'].sudo().with_context(lang=None, tracking_disable=True).create(work_contact_vals)
# Update personal info on the employee
if bank_account_vals:
bank_account = request.env['res.partner.bank'].sudo()._find_or_create_bank_account(
account_number=bank_account_vals['acc_number'],
partner=partner,
company=employee.company_id,
extra_create_vals=bank_account_vals,
)
employee_vals['bank_account_ids'] = [Command.set([bank_account.id])]
acc_holder_name = bank_account_vals.get('acc_holder_name')
if acc_holder_name and bank_account.sudo().acc_holder_name != acc_holder_name:
bank_account.sudo().acc_holder_name = acc_holder_name
employee_vals['work_contact_id'] = partner.id
if job.address_id:
employee_vals['address_id'] = job.address_id.id
if not no_name_write:
employee_vals['name'] = employee_infos.get('name', '')
employee.with_context(tracking_disable=True).write(employee_vals)
version.with_context(tracking_disable=True).write(version_vals)
if attachment_create_vals:
request.env['ir.attachment'].sudo().create(attachment_create_vals)
def create_new_version(self, version_vals, offer_id, benefits, no_write=False, **kw):
# Generate a new version with the current modifications
version_diff = []
benefits_values = benefits['version']
personal_infos = {
'version_personal': benefits['version_personal'],
'employee': benefits['employee'],
'address': benefits['address'],
'bank_account': benefits['bank_account'],
}
offer = request.env['hr.contract.salary.offer'].sudo().browse(offer_id).exists()
applicant = offer.applicant_id
employee = kw.get('employee') or applicant.employee_id or offer.employee_id
if not employee and applicant:
existing_version = request.env['hr.version'].sudo().search([
('applicant_id', '=', applicant.id),
('employee_id', '!=', False),
'|',
('active', '=', True),
('active', '=', False)
], limit=1)
employee = existing_version.employee_id
if not employee:
company = self.env['res.company'].browse(version_vals.get('company_id'))
employee_vals = {
'name': applicant.partner_name if applicant else 'Simulation Employee',
'active': False,
'company_id': company.id,
'lang': company.partner_id.lang,
'resource_calendar_id': version_vals.get('resource_calendar_id'),
}
if 'current_applicant_skill_ids' in offer.applicant_id:
employee_vals['employee_skill_ids'] = [
Command.create({
'skill_id': skill.skill_id.id,
'skill_type_id': skill.skill_type_id.id,
'skill_level_id': skill.skill_level_id.id
})
for skill in offer.applicant_id.current_applicant_skill_ids
]
employee = request.env['hr.employee'].sudo().with_context(
tracking_disable=True,
salary_simulation=not no_write,
).create(employee_vals)
# get differences for personnal information
if no_write:
employee_fields = request.env['hr.employee']._fields
for section in personal_infos:
for field in personal_infos[section]:
if field in employee_fields:
current_value = employee[field]
new_value = personal_infos[section][field]
if isinstance(current_value, type(new_value)) and current_value == new_value:
continue
elif employee_fields[field].relational:
current_value = str(current_value.name)
if new_value:
new_record = request.env[employee_fields[field].comodel_name].sudo().browse(int(new_value))
new_value = new_record['name'] if new_record else ''
elif employee_fields[field].type in ['integer', 'float']:
current_value = str(current_value)
if not new_value:
new_value = '0'
elif employee_fields[field].type == 'date':
current_value = current_value.strftime('%Y-%m-%d') if current_value else ''
elif employee_fields[field].type == 'boolean':
current_value = str(current_value)
new_value = str(new_value)
elif employee_fields[field].type == 'binary':
continue
if current_value != new_value:
employee_field_name = employee_fields[field].string or field
version_diff.append((employee_field_name, current_value, new_value))
new_version = request.env['hr.version'].with_context(
tracking_disable=True,
salary_simulation=True,
).sudo().create(self._get_new_version_values(version_vals, employee, benefits_values, offer))
self._update_personal_info(employee, new_version, personal_infos, no_name_write=bool(kw.get('employee')))
# get differences for version information
if no_write:
version_fields = request.env['hr.version']._fields
for field in version_fields:
if field in benefits_values and version_vals.get(field) != new_version[field]\
and (version_vals.get(field) or new_version[field]):
current_value = version_vals.get(field)
new_value = new_version[field]
version_field_name = version_fields[field].string or field
version_diff.append((version_field_name, current_value, new_value))
if 'original_link' in kw:
start_date = parse_qs(urlparse(kw['original_link']).query).get('version_start_date', False)
if start_date:
new_version.date_version = datetime.strptime(start_date[0], '%Y-%m-%d').date()
new_version.wage_with_holidays = benefits_values['wage']
new_version.final_yearly_costs = float(benefits_values['final_yearly_costs'] or 0.0)
new_version._inverse_wage_with_holidays()
return new_version, version_diff
def _update_salary(self, offer, version, benefits=None, **kw):
"""
Internal implementation of `update_salary`.
Override this method instead of `update_salary` to avoid multiple calls to `_get_version`
"""
result = {}
version_vals = version._get_values_dict()
new_version = self.create_new_version(version_vals, offer.id, benefits, no_write=True)[0]
final_yearly_costs = float(benefits['version']['final_yearly_costs'] or 0.0)
new_gross = new_version._get_gross_from_employer_costs(final_yearly_costs)
new_version.write({
'wage': new_gross,
'final_yearly_costs': final_yearly_costs,
})
result['new_gross'] = round(new_gross, 2)
new_version = new_version.with_context(
origin_version_id=version.id,
simulation_working_schedule=kw.get('simulation_working_schedule', False))
result.update(self._get_compute_results(new_version))
return result
@http.route('/salary_package/update_salary', type="jsonrpc", auth="public")
def update_salary(self, offer_id=None, benefits=None, **kw):
result = {}
with hr_version_context(request, invalidate=True):
offer = request.env['hr.contract.salary.offer'].sudo().browse(offer_id)
version = offer._get_version()
has_access, error_page = self.check_access_to_salary_configurator(kw.get('token'), offer, version)
if not has_access:
return error_page
result.update(self._update_salary(offer, version, benefits, **kw))
return result
def _get_compute_results(self, new_version):
wage_to_apply = new_version._get_wage_to_apply()
new_version.wage_on_signature = wage_to_apply
result = {}
result['wage_with_holidays'] = round(wage_to_apply, 2)
# Allowed company ids might not be filled or request.env.user.company_ids might be wrong
# since we are in route context, force the company to make sure we load everything
resume_lines = request.env['hr.contract.salary.resume'].sudo().with_company(new_version.company_id).search([
'|',
('structure_type_id', '=', False),
('structure_type_id', '=', new_version.structure_type_id.id),
('value_type', 'in', ['fixed', 'version', 'monthly_total', 'sum'])])
result['resume_categories'] = [c.name for c in sorted(resume_lines.mapped('category_id'), key=lambda x: x.sequence)]
result['resume_lines_mapped'] = defaultdict(lambda: {})
monthly_total = 0
monthly_total_lines = resume_lines.filtered(lambda l: l.value_type == 'monthly_total')
uoms = {'days': _('Days'), 'percent': '%', 'currency': new_version.company_id.currency_id.symbol, 'position': new_version.company_id.currency_id.position}
resume_explanation = False
for resume_line in resume_lines - monthly_total_lines:
value = 0
uom = uoms[resume_line.uom]
resume_explanation = False
if resume_line.value_type == 'fixed':
value = resume_line.fixed_value
if resume_line.value_type == 'version':
value = new_version[resume_line.code] if resume_line.code in new_version else 0 # noqa: SIM401
if resume_line.value_type == 'sum':
resume_explanation = _('Equals to the sum of the following values:\n\n%s',
'\n+ '.join(resume_line.benefit_ids.res_field_id.sudo().mapped('field_description')))
for benefit in resume_line.benefit_ids:
if not benefit.fold_field or (benefit.fold_field and new_version[benefit.fold_field]):
field = benefit.field
value += new_version[field] if benefit.source == 'field' else new_version._get_property_input_value(benefit.salary_rule_id.code)
if resume_line.impacts_monthly_total:
monthly_total += value / 12.0 if resume_line.category_id.periodicity == 'yearly' else value
try:
value = round(float(value), 2)
except:
pass
result['resume_lines_mapped'][resume_line.category_id.name][resume_line.code] = (resume_line.name, value, uom, resume_explanation, new_version.company_id.currency_id.position, resume_line.uom)
for resume_line in monthly_total_lines:
result['resume_lines_mapped'][resume_line.category_id.name][resume_line.code] = (resume_line.name, round(float(monthly_total), 2), uoms['currency'], uoms['position'], resume_explanation, resume_line.uom)
return result
def _onchange_benefit(self, offer, version, benefit_field, new_value, benefits, **kw):
benefit = request.env['hr.contract.salary.benefit'].sudo().search([
('structure_type_id', '=', version.structure_type_id.id),
('field', '=', benefit_field)], limit=1)
if hasattr(version, '_get_description_%s' % benefit_field):
description = getattr(version, '_get_description_%s' % benefit_field)(new_value)
else:
description = benefit.description
return {'new_value': new_value, 'description': description, 'extra_values': False}
@http.route(['/salary_package/onchange_benefit'], type='jsonrpc', auth='public')
def onchange_benefit(self, benefit_field, new_value, offer_id, benefits, **kw):
# Return a dictionary describing the new benefit configuration:
# - new_value: The benefit new_value (same by default)
# - description: The dynamic description corresponding to the benefit new value
# - extra_value: A list of tuple (input name, input value) change another input due
# to the benefit new_value
# Override this controllers to add customize
# the returned value for a specific benefit
result = {}
with hr_version_context(request, invalidate=True):
offer = request.env['hr.contract.salary.offer'].sudo().browse(offer_id)
version = offer._get_version()
has_access, error_page = self.check_access_to_salary_configurator(kw.get('token'), offer, version)
if not has_access:
return error_page
result.update(self._onchange_benefit(offer, version, benefit_field, new_value, benefits, **kw))
return result
@http.route(['/salary_package/onchange_personal_info'], type='jsonrpc', auth='public')
def onchange_personal_info(self, field, value):
# sudo as public users can't access ir.model.fields
info = request.env['hr.contract.salary.personal.info'].sudo().search([('field', '=', field)])
if not info.child_ids:
return {}
if info.value_ids:
value = info.value_ids.filtered(lambda v: v.value == value)
return {'hide_children': value.hide_children, 'field': field}
return {'hide_children': not bool(value), 'field': field}
def _get_email_info(self, version, **kw):
wage_to_apply = version._get_wage_to_apply()
field_names = {
model: {
field.name: field.field_description for field in request.env['ir.model.fields'].sudo().search([('model', '=', model)])
} for model in ['hr.employee', 'hr.version', 'res.partner', 'res.partner.bank']}
result = {
_('Salary Package Summary'): {
'General Information': [
(_('Employee Name'), version.employee_id.name),
(_('Job Position'), version.job_id.name),
(_('Job Title'), version.employee_id.job_title),
(_('Contract Type'), version.contract_type_id.name),
(_('Original Link'), kw.get('original_link'))
],
}
}
# Contract Information
version_benefits = request.env['hr.contract.salary.benefit'].sudo().search([('structure_type_id', '=', version.structure_type_id.id)])
version_info = {benefit_type.name: [] for benefit_type in sorted(version_benefits.mapped('benefit_type_id'), key=lambda x: x.sequence)}
for benefit in version_benefits:
if benefit.folded and benefit.fold_field:
value = _('Yes') if version[benefit.fold_field] else _('No')
version_info[benefit.benefit_type_id.name].append((field_names['hr.version'][benefit.fold_field], value))
field_name = benefit.field
if not field_name or field_name not in version:
continue
field_value = version[field_name]
if isinstance(field_value, models.BaseModel):
field_value = field_value.name
elif isinstance(field_value, float):
field_value = round(field_value, 2)
version_info[benefit.benefit_type_id.name].append((field_names['hr.version'][field_name], field_value))
self._append_additional_benefit_info(version_info[benefit.benefit_type_id.name], version, field_names, field_name)
# Add wage information
version_info[_('Wage')] = [
(_('Monthly Gross Salary'), wage_to_apply),
(_('Annual Employer Cost'), version.final_yearly_costs),
]
result[_('Contract Information:')] = version_info
# Personal Information
infos = request.env['hr.contract.salary.personal.info'].sudo().search([('display_type', '!=', 'document'), '|', ('structure_type_id', '=', False), ('structure_type_id', '=', version.structure_type_id.id)])
personal_infos = {personal_info_type.name: [] for personal_info_type in sorted(infos.mapped('info_type_id'), key=lambda x: x.sequence)}
for info in infos:
if info.applies_on == 'employee':
field_label = field_names['hr.employee'][info.field]
field_value = version.employee_id[info.field]
if info.applies_on == 'bank_account':
field_label = field_names['res.partner.bank'][info.field]
field_value = version.employee_id.primary_bank_account_id[info.field]
if info.applies_on == 'version_personal':
field_label = field_names['hr.version'][info.field]
field_value = version[info.field]
if isinstance(field_value, models.BaseModel):
field_value = field_value.name
elif isinstance(field_value, float):
field_value = round(field_value, 2)
personal_infos[info.info_type_id.name].append((field_label, field_value))
result[_('Personal Information')] = personal_infos
return {'mapped_data': result}
def _append_additional_benefit_info(self, benefit_info, version, field_names, field_name):
return
def _send_mail_message(self, offer, template, kw, values, new_version_id=None):
model = 'hr.version' if new_version_id else 'hr.contract.salary.offer'
res_id = new_version_id or offer.id
request.env[model].sudo().browse(res_id).message_post_with_source(
template,
render_values=values,
subtype_xmlid='mail.mt_comment',
)
def send_email(self, offer, version, **kw):
self._send_mail_message(
offer,
'hr_contract_salary.hr_contract_salary_email_template',
kw,
self._get_email_info(version, **kw))
return version.id
def send_diff_email(self, offer, differences, new_version_id, **kw):
self._send_mail_message(
offer,
'hr_contract_salary.hr_contract_salary_diff_email_template',
kw,
{'differences': differences},
new_version_id)
@http.route(['/salary_package/submit'], type='jsonrpc', auth='public')
def submit(self, offer_id=None, benefits=None, **kw):
with hr_version_context(request, invalidate=True):
offer = request.env['hr.contract.salary.offer'].sudo().browse(offer_id).exists()
if not offer.applicant_id and not offer.employee_version_id:
raise UserError(_('This link is invalid. Please contact the HR Responsible to get a new one...'))
version = offer._get_version()
has_access, error_page = self.check_access_to_salary_configurator(kw.get('token'), offer, version)
if not has_access:
return error_page
if version.employee_id.user_id == request.env.user:
kw['employee'] = version.employee_id
version_vals = version._get_values_dict()
kw['package_submit'] = True
new_version = self.create_new_version(version_vals, offer_id, benefits, no_write=True, **kw)
if isinstance(new_version, dict) and new_version.get('error'):
return new_version
new_version, version_diff = new_version
# write on new version differences with current one
current_version = new_version.employee_id.version_id
if current_version:
self.send_diff_email(offer, version_diff, new_version.id, **kw)
self.send_email(offer, new_version, **kw)
applicant = offer.applicant_id
if applicant and offer.access_token:
hash_token_access = hashlib.sha1(kw.get('token').encode("utf-8")).hexdigest()
existing_version = request.env['hr.version'].sudo().search([
('applicant_id', '=', applicant.id), ('hash_token', '=', hash_token_access), ('active', '=', False)])
existing_version.sign_request_ids.write({'state': 'canceled', 'active': False})
existing_version.unlink()
new_version.hash_token = hash_token_access
elif not applicant and offer.employee_id.user_id and offer.employee_id.user_id == request.env.user and kw.get('original_link'):
hash_token_access = hashlib.sha1(kw.get('original_link').encode("utf-8")).hexdigest()
existing_version = request.env['hr.version'].sudo().search([
('employee_id', 'in', request.env.user.employee_ids.ids), ('hash_token', '=', hash_token_access), ('active', '=', False)])
existing_version.sign_request_ids.write({'state': 'canceled', 'active': False})
existing_version.unlink()
new_version.hash_token = hash_token_access
# TODO not sure about this change
new_version.write({
'name': 'New version - ' + new_version.employee_id.name,
'origin_version_id': version_vals.get('id') if offer.employee_id else False,
})
sign_template = offer.sign_template_id
signatories = offer.sign_template_signatories_ids
if not sign_template:
return {'error': 1, 'error_msg': _('No signature template defined on the version. Please contact the HR responsible.')}
if not new_version.hr_responsible_id and 'hr' in signatories.mapped('signatory'):
return {'error': 1, 'error_msg': _('No HR responsible defined on the job position. Please contact an administrator.')}
# ask the contract responsible to create a sign request
SignRequestSudo = request.env['sign.request'].with_user(new_version.hr_responsible_id).sudo()
signatory_dict = {
'employee': new_version.employee_id.work_contact_id.id,
'hr': new_version.hr_responsible_id.work_contact_id.id or new_version.hr_responsible_id.partner_id.id
}
signatories_command = [
Command.create(
{'role_id': signatory.sign_role_id.id,
'partner_id': signatory_dict.get(signatory.signatory, signatory.partner_id.id),
'mail_sent_order': signatory.order})
for signatory in signatories
]
sign_request_sudo = SignRequestSudo.create({
'template_id': sign_template.id,
'request_item_ids': signatories_command,
'reference': _('Signature Request - %s', new_version.name),
'subject': _('Signature Request - %s', new_version.name),
'reference_doc': f'hr.contract.salary.offer,{offer.id}'
})
sign_request_sudo.toggle_favorited()
# Prefill the sign boxes
sign_items = request.env['sign.item'].sudo().search([
('template_id', '=', sign_template.id),
('name', '!=', '')
])
sign_values_by_role = defaultdict(lambda: defaultdict(lambda: request.env['sign.item']))
for item in sign_items:
try:
new_value = None
if item.name == 'car' and new_version.transport_mode_car:
if not new_version.new_car and new_version.car_id:
new_value = new_version.car_id.model_id.name
elif new_version.new_car and new_version.new_car_model_id:
new_value = new_version.new_car_model_id.name
# YTI FIXME: Clean that brol
elif item.name == 'l10n_be_group_insurance_rate':
new_value = 1 if new_version.get(item.name) else 0
elif item.name == "ip_wage_rate":
new_value = new_value if new_version.ip else 0
else:
new_values = new_version.mapped(item.name)
if not new_values or isinstance(new_values, models.BaseModel):
raise Exception
new_value = new_values[0]
if isinstance(new_value, float):
new_value = round(new_value, 2)
if item.type_id.item_type == "checkbox":
new_value = 'on' if new_value else 'off'
if new_value is not None:
sign_values_by_role[item.responsible_id][str(item.id)] = new_value
except Exception:
pass
for sign_request_item in sign_request_sudo.request_item_ids:
if sign_request_item.role_id in sign_values_by_role:
sign_request_item._fill(sign_values_by_role[sign_request_item.role_id])
access_token = request.env['sign.request.item'].sudo().search([
('sign_request_id', '=', sign_request_sudo.id),
('role_id', '=', request.env.ref('hr_sign.sign_item_role_employee_signatory').id)
]).access_token
if not access_token:
employee_roles = request.env['hr.contract.signatory'].search([
('signatory', '=', 'employee'),
('offer_id', '=', offer_id),
], limit=1)
if employee_roles:
access_token = request.env['sign.request.item'].sudo().search([
('sign_request_id', '=', sign_request_sudo.id),
('role_id', 'in', employee_roles.sign_role_id.id),
]).access_token
new_version.sign_request_ids += sign_request_sudo
offer.sign_request_ids += sign_request_sudo
if new_version:
if offer.applicant_id:
new_version.sudo().applicant_id = offer.applicant_id
if offer.employee_version_id:
new_version.sudo().origin_version_id = offer.employee_version_id
return {
'job_id': new_version.job_id.id,
'request_id': sign_request_sudo.id,
'token': access_token,
'error': 0,
'new_version_id': new_version.id
}
@http.route(['/salary_package/post_feedback'], type='jsonrpc', auth='public')
def refuse(self, offer_id, feedback=None, token=None):
with hr_version_context(request, invalidate=True):
offer = request.env['hr.contract.salary.offer'].sudo().browse(offer_id).exists()
if not offer.applicant_id and not offer.employee_version_id:
raise UserError(_('This link is invalid. Please contact the HR Responsible to get a new one...'))
version = offer._get_version()
has_access, error_page = self.check_access_to_salary_configurator(token, offer, version)
if not has_access:
return error_page
if not version and (not token or not consteq(offer.access_token, token)):
raise UserError(_('This link is invalid. Please contact the HR Responsible to get a new one...'))
if feedback:
partner = offer.applicant_id.partner_id or offer.employee_id.work_contact_id
offer.message_post(
body=feedback,
author_id=partner.id,
message_type='comment',
subtype_xmlid='mail.mt_comment',
)
return True
return False