# Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict from odoo.addons.hr_contract_salary.controllers import main from odoo.addons.hr_contract_salary.utils.hr_version import hr_version_context from odoo import http, _ from odoo.exceptions import UserError from odoo.http import request from odoo.tools.float_utils import float_compare class HrContractSalary(main.HrContractSalary): def _can_submit_offer(self, values): return not values.get('is_simulation_offer', False) and super()._can_submit_offer(values) def _check_link_access(self, offer, **kw): if not request.env.user.has_group('hr.group_hr_manager') and offer.is_simulation_offer: if not self._check_access_token(offer, kw.get('token')): return False, _('This link is invalid. Please contact the HR Responsible to get a new one...') return super()._check_link_access(offer, **kw) def _get_default_template_values(self, version, offer): values = super()._get_default_template_values(version, offer) values.update({ 'is_simulation_offer': offer.is_simulation_offer }) return values def _get_new_version_values(self, version_vals, employee, benefits, offer): new_version_vals = super()._get_new_version_values(version_vals, employee, benefits, offer) new_version_vals['work_entry_source'] = version_vals.get('work_entry_source') new_version_vals['standard_calendar_id'] = version_vals.get('standard_calendar_id') if version_vals.get('wage_type') == 'hourly': new_version_vals['hourly_wage'] = version_vals.get('hourly_wage') return new_version_vals def _get_personal_infos(self, version, offer): if not offer.is_simulation_offer: return super()._get_personal_infos(version, offer) mapped_personal_infos, new_dropdown_options, new_initial_values = super()._get_personal_infos(version, offer) new_mapped_personal_infos = defaultdict(lambda: request.env['hr.contract.salary.personal.info']) for key, personal_infos in mapped_personal_infos.items(): for personal_info in personal_infos: if personal_info.impacts_net_salary: new_mapped_personal_infos[key] |= personal_info else: new_initial_values.pop(personal_info.field, None) new_initial_values.pop(personal_info.field + '_filename', None) new_dropdown_options.pop(personal_info.field, None) return new_mapped_personal_infos, new_dropdown_options, new_initial_values def _get_payslip_line_values(self, payslip, codes): return payslip._get_line_values(codes) def _get_compute_results(self, new_version): schedule_pay_label = dict(request.env['hr.version']._fields['schedule_pay']._description_selection(request.env)) def _get_period_name(category_id, version): if category_id == request.env.ref("hr_contract_salary.hr_contract_salary_resume_category_monthly_salary"): # ISSUE: # The salary configurator was originally designed with the assumption that # the pay schedule is always "monthly". When we changed the schedule pay # selection labels (e.g., "monthly" -> "month"), the configurator started # displaying "month Salary" instead of the expected "Monthly Salary" # and "Gross (Incl. Comm)" instead of "Gross". # FIX: # As a temporary workaround, we return the category_id.name (to fetch the correct translated name depending on the language, e.g. "Monthly Salary" in English) # when version.schedule_pay == 'monthly' because the configurator is not adapted for other pay schedule than monthly. # For all other schedule_pay values, we keep using the schedule_pay_label mapping, # until the configurator is properly adapted to support non-monthly schedules. if version.schedule_pay == 'monthly': return category_id.name period_name = schedule_pay_label.get(version.schedule_pay, "Monthly") return f"{period_name} Salary" return category_id.name result = super()._get_compute_results(new_version) # generate a payslip corresponding to only this version payslip = new_version._generate_salary_simulation_payslip() result['payslip_lines'] = [( line.name, f'{line.total:.2f}', line.code, 'total' if line.code in ['BASIC', 'SALARY', 'GROSS', 'NET', 'GROSSIP'] else float_compare(line.total, 0, precision_digits=2), new_version.company_id.currency_id.position, new_version.company_id.currency_id.symbol ) for line in payslip.line_ids.filtered(lambda l: l.appears_on_payslip)] # 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', ['payslip', 'monthly_total'])]) monthly_total = 0 monthly_total_lines = resume_lines.filtered(lambda l: l.value_type == 'monthly_total') # new categories could be introduced at this step # recreate resume_categories resume_categories = 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']), ('id', 'in', resume_lines.ids)]).category_id result['resume_categories'] = [_get_period_name(c, new_version) for c in sorted(resume_categories, key=lambda x: x.sequence)] all_codes = (resume_lines - monthly_total_lines).mapped('code') line_values = self._get_payslip_line_values(payslip, all_codes) if all_codes else False for resume_line in resume_lines - monthly_total_lines: value = round(line_values[resume_line.code][payslip.id]['total'], 2) resume_explanation = False if resume_line.code == 'GROSS' and new_version.wage_type == 'hourly': hours = payslip.worked_days_line_ids.number_of_hours resume_explanation = self.env._('This is the gross calculated for the current month with a total of %s hours.', hours) result['resume_lines_mapped'][_get_period_name(resume_line.category_id, new_version)][resume_line.code] = (resume_line.name, value, new_version.company_id.currency_id.symbol, resume_explanation, new_version.company_id.currency_id.position, resume_line.uom) if resume_line.impacts_monthly_total: monthly_total += value / 12.0 if resume_line.category_id.periodicity == 'yearly' else value for resume_line in monthly_total_lines: super_line = result['resume_lines_mapped'][_get_period_name(resume_line.category_id, new_version)][resume_line.code] line_name = super_line[0] if resume_line.category_id == request.env.ref("hr_contract_salary.hr_contract_salary_resume_category_total"): period_name = schedule_pay_label.get(new_version.schedule_pay, "Monthly") line_name = f"{period_name} Equivalent" new_value = (line_name, round(super_line[1] + float(monthly_total), 2), super_line[2], False, new_version.company_id.currency_id.position, resume_line.uom) result['resume_lines_mapped'][_get_period_name(resume_line.category_id, new_version)][resume_line.code] = new_value return result def _update_version_payroll_properties(self, version, benefits): version_benefits = request.env['hr.contract.salary.benefit'].sudo().search([ ('structure_type_id', '=', version.structure_type_id.id), ('source', '=', 'rule')]) variants = [ '', "_manual", "_radio", "_slider", ] for benefit in version_benefits: fields = ['%s%s' % (benefit.field, k) for k in variants] value = next((benefits[field] for field in fields if field in benefits), None) or 0.0 version._set_property_input_value(benefit.salary_rule_id.code, float(value)) def create_new_version(self, version_vals, offer_id, benefits, no_write=False, **kw): new_version, version_diff = super().create_new_version(version_vals, offer_id, benefits, no_write=no_write, **kw) benefits_values = benefits['version'] with hr_version_context(request): offer = request.env['hr.contract.salary.offer'].sudo().browse(offer_id) version = offer._get_version() new_version.write({ 'payroll_properties': dict(version.payroll_properties), }) self._update_version_payroll_properties(new_version, benefits_values) return new_version, version_diff @http.route() def submit(self, offer_id=None, benefits=None, **kw): offer = request.env['hr.contract.salary.offer'].sudo().browse(offer_id) if offer.is_simulation_offer: raise UserError(self.env._('This offer is a simulation. You cannot submit this salary package.')) return super().submit(offer_id, benefits, **kw)