Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
336
payroll/models/hr_salary_rule.py
Normal file
336
payroll/models/hr_salary_rule.py
Normal file
@@ -0,0 +1,336 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import traceback
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class HrSalaryRule(models.Model):
|
||||
_name = "hr.salary.rule"
|
||||
_order = "sequence, id"
|
||||
_description = "Salary Rule"
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(
|
||||
help="The code of salary rules can be used as reference in computation "
|
||||
"of other rules. In that case, it is case sensitive.",
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
required=True, index=True, default=5, help="Use to arrange calculation sequence"
|
||||
)
|
||||
quantity = fields.Char(
|
||||
default="1.0",
|
||||
help="It is used in computation for percentage and fixed amount. "
|
||||
"For e.g. A rule for Meal Voucher having fixed amount of "
|
||||
"1€ per worked day can have its quantity defined in expression "
|
||||
"like worked_days.WORK100.number_of_days.",
|
||||
)
|
||||
category_id = fields.Many2one("hr.salary.rule.category", string="Category")
|
||||
active = fields.Boolean(
|
||||
default=True,
|
||||
help="If the active field is set to false, it will allow you to hide"
|
||||
" the salary rule without removing it.",
|
||||
)
|
||||
appears_on_payslip = fields.Boolean(
|
||||
string="Appears on Payslip",
|
||||
default=True,
|
||||
help="Used to display the salary rule on payslip.",
|
||||
)
|
||||
parent_rule_id = fields.Many2one(
|
||||
comodel_name="hr.salary.rule", string="Parent Salary Rule", index=True
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
condition_select = fields.Selection(
|
||||
[("none", "Always True"), ("range", "Range"), ("python", "Python Expression")],
|
||||
string="Condition Based on",
|
||||
default="none",
|
||||
required=True,
|
||||
)
|
||||
condition_range = fields.Char(
|
||||
string="Range Based on",
|
||||
default="contract.wage",
|
||||
help="This will be used to compute the % fields values; in general it "
|
||||
"is on basic, but you can also use categories code fields in "
|
||||
"lowercase as a variable names (hra, ma, lta, etc.) and the "
|
||||
"variable basic.",
|
||||
)
|
||||
condition_python = fields.Text(
|
||||
string="Python Condition",
|
||||
required=True,
|
||||
default="""# Available variables:
|
||||
#-------------------------------
|
||||
# payslip: hr.payslip object
|
||||
# payslips: object containing payslips (browsable)
|
||||
# employee: hr.employee object
|
||||
# contract: hr.contract object
|
||||
# rules: object containing the rules code (previously computed)
|
||||
# categories: object containing the computed salary rule categories
|
||||
# (sum of amount of all rules belonging to that category).
|
||||
# worked_days: object containing the computed worked days.
|
||||
# inputs: object containing the computed inputs.
|
||||
# payroll: object containing miscellaneous values related to payroll
|
||||
# current_contract: object with values calculated from the current contract
|
||||
# result_rules: object with a dict of qty, rate, amount an total of calculated rules
|
||||
# tools: object that contain libraries and tools that can be used in calculations
|
||||
|
||||
# Available compute variables:
|
||||
#-------------------------------
|
||||
# result: returned value have to be set in the variable 'result'
|
||||
|
||||
# Example:
|
||||
#-------------------------------
|
||||
# result = worked_days.WORK0 and worked_days.WORK0.number_of_days > 0""", # noqa: E501
|
||||
help="Applied this rule for calculation if condition is true. You can "
|
||||
"specify condition like basic > 1000.",
|
||||
)
|
||||
condition_range_min = fields.Float(
|
||||
string="Minimum Range", help="The minimum amount, applied for this rule."
|
||||
)
|
||||
condition_range_max = fields.Float(
|
||||
string="Maximum Range", help="The maximum amount, applied for this rule."
|
||||
)
|
||||
amount_select = fields.Selection(
|
||||
[
|
||||
("percentage", "Percentage (%)"),
|
||||
("fix", "Fixed Amount"),
|
||||
("code", "Python Code"),
|
||||
],
|
||||
string="Amount Type",
|
||||
index=True,
|
||||
required=True,
|
||||
default="fix",
|
||||
help="The computation method for the rule amount.",
|
||||
)
|
||||
amount_fix = fields.Float(string="Fixed Amount", digits="Payroll")
|
||||
amount_percentage = fields.Float(
|
||||
string="Percentage (%)",
|
||||
digits="Payroll Rate",
|
||||
help="For example, enter 50.0 to apply a percentage of 50%",
|
||||
)
|
||||
amount_python_compute = fields.Text(
|
||||
string="Python Code",
|
||||
default="""# Available variables:
|
||||
#-------------------------------
|
||||
# payslip: hr.payslip object
|
||||
# payslips: object containing payslips (browsable)
|
||||
# employee: hr.employee object
|
||||
# contract: hr.contract object
|
||||
# rules: object containing the rules code (previously computed)
|
||||
# categories: object containing the computed salary rule categories
|
||||
# (sum of amount of all rules belonging to that category).
|
||||
# worked_days: object containing the computed worked days.
|
||||
# inputs: object containing the computed inputs.
|
||||
# payroll: object containing miscellaneous values related to payroll
|
||||
# current_contract: object with values calculated from the current contract
|
||||
# result_rules: object with a dict of qty, rate, amount an total of calculated rules
|
||||
# tools: object that contain libraries and tools that can be used in calculations
|
||||
|
||||
# Available compute variables:
|
||||
#-------------------------------
|
||||
# result: returned value have to be set in the variable 'result'
|
||||
# result_rate: the rate that will be applied to "result".
|
||||
# result_qty: the quantity of units that will be multiplied to "result".
|
||||
# result_name: if this variable is computed, it will contain the name of the line.
|
||||
|
||||
# Example:
|
||||
#-------------------------------
|
||||
# result = contract.wage * 0.10""", # noqa: E501
|
||||
)
|
||||
amount_percentage_base = fields.Char(
|
||||
string="Percentage based on", help="result will be affected to a variable"
|
||||
)
|
||||
child_ids = fields.One2many(
|
||||
"hr.salary.rule", "parent_rule_id", string="Child Salary Rule", copy=True
|
||||
)
|
||||
register_id = fields.Many2one(
|
||||
"hr.contribution.register",
|
||||
string="Contribution Register",
|
||||
help="Eventual third party involved in the salary payment of the employees.",
|
||||
)
|
||||
input_ids = fields.One2many("hr.rule.input", "input_id", string="Inputs", copy=True)
|
||||
note = fields.Text(string="Description")
|
||||
require_code_and_category = fields.Boolean(
|
||||
"Require code and category",
|
||||
compute="_compute_require_code_and_category",
|
||||
default=lambda self: self._compute_require_code_and_category(),
|
||||
)
|
||||
|
||||
@api.constrains("parent_rule_id")
|
||||
def _check_parent_rule_id(self):
|
||||
if self.env.registry[self._name]._name != "hr.salary.rule":
|
||||
# HrPayslipLine inherits from "hr.salary.rule" and we don't
|
||||
# want to do this check on a payslip line
|
||||
return
|
||||
for rule in self:
|
||||
if rule._has_cycle(field_name="parent_rule_id"):
|
||||
raise ValidationError(
|
||||
self.env._(
|
||||
"Error! You cannot create recursive hierarchy of Salary Rules."
|
||||
)
|
||||
)
|
||||
|
||||
def _recursive_search_of_rules(self):
|
||||
"""
|
||||
Returns the rules in reverse dependency order, children first
|
||||
@return: recordset of rules
|
||||
"""
|
||||
if not self:
|
||||
return self.env["hr.salary.rule"]
|
||||
else:
|
||||
return self.child_ids._recursive_search_of_rules() | self
|
||||
|
||||
def _reset_localdict_values(self, localdict):
|
||||
localdict["result_name"] = None
|
||||
localdict["result_qty"] = 1.0
|
||||
localdict["result_rate"] = 100
|
||||
localdict["result"] = None
|
||||
return localdict
|
||||
|
||||
def _compute_require_code_and_category(self):
|
||||
require = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("payroll.require_code_and_category")
|
||||
)
|
||||
self.require_code_and_category = require
|
||||
return require
|
||||
|
||||
# TODO should add some checks on the type of result (should be float)
|
||||
def _compute_rule(self, localdict):
|
||||
"""
|
||||
:param localdict: dictionary containing the environement in which to
|
||||
compute the rule
|
||||
:return: returns a dict with values for the payslip line.
|
||||
The dict should minimum have "name", "quantity", "rate" and "amount".
|
||||
:rtype: {"name": string, "quantity": float, "rate": float, "amount": float}
|
||||
"""
|
||||
self.ensure_one()
|
||||
method = f"_compute_rule_{self.amount_select}"
|
||||
compute = getattr(self, method)
|
||||
return compute(localdict)
|
||||
|
||||
def _compute_rule_fix(self, localdict):
|
||||
try:
|
||||
return {
|
||||
"name": self.name,
|
||||
"quantity": float(safe_eval(self.quantity, localdict)),
|
||||
"rate": 100.0,
|
||||
"amount": self.amount_fix,
|
||||
}
|
||||
except Exception as ex:
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"Wrong quantity defined for salary rule %(nm)s (%(code)s) "
|
||||
"for employee %(ee)s.",
|
||||
nm=self.name,
|
||||
code=self.code,
|
||||
ee=localdict["employee"].name,
|
||||
)
|
||||
) from ex
|
||||
|
||||
def _compute_rule_percentage(self, localdict):
|
||||
try:
|
||||
return {
|
||||
"name": self.name,
|
||||
"quantity": float(safe_eval(self.quantity, localdict)),
|
||||
"rate": self.amount_percentage,
|
||||
"amount": float(safe_eval(self.amount_percentage_base, localdict)),
|
||||
}
|
||||
except Exception as ex:
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"Wrong percentage base or quantity defined for salary "
|
||||
"rule %(nm)s (%(code)s) for employee %(ee)s.",
|
||||
nm=self.name,
|
||||
code=self.code,
|
||||
ee=localdict["employee"].name,
|
||||
)
|
||||
) from ex
|
||||
|
||||
def _compute_rule_code(self, localdict):
|
||||
try:
|
||||
safe_eval(self.amount_python_compute, localdict, mode="exec")
|
||||
except Exception as ex:
|
||||
exc_text = "".join(traceback.format_exception(ex))
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"\nWrong python code defined for salary rule %(nm)s "
|
||||
"(%(code)s) for employee %(ee)s.\nHere is the error "
|
||||
"received:\n\n%(err)s\n",
|
||||
nm=self.name,
|
||||
code=self.code,
|
||||
ee=localdict["employee"].name,
|
||||
err=exc_text,
|
||||
)
|
||||
) from ex
|
||||
return self._get_rule_dict(localdict)
|
||||
|
||||
def _get_rule_dict(self, localdict):
|
||||
name = localdict.get("result_name") or self.name
|
||||
quantity = float(localdict["result_qty"]) if "result_qty" in localdict else 1.0
|
||||
rate = float(localdict["result_rate"]) if "result_rate" in localdict else 100.0
|
||||
return {
|
||||
"name": name,
|
||||
"quantity": quantity,
|
||||
"rate": rate,
|
||||
"amount": float(localdict["result"]),
|
||||
}
|
||||
|
||||
def _satisfy_condition(self, localdict):
|
||||
"""
|
||||
@param contract_id: id of hr.contract to be tested
|
||||
@return: returns True if the given rule match the condition for the
|
||||
given contract. Return False otherwise.
|
||||
"""
|
||||
self.ensure_one()
|
||||
method = f"_satisfy_condition_{self.condition_select}"
|
||||
check = getattr(self, method)
|
||||
if self.parent_rule_id:
|
||||
current_result = check(localdict)
|
||||
parent_result = self.parent_rule_id._satisfy_condition(localdict)
|
||||
return current_result and parent_result
|
||||
return check(localdict)
|
||||
|
||||
def _satisfy_condition_none(self, localdict):
|
||||
return True
|
||||
|
||||
def _satisfy_condition_range(self, localdict):
|
||||
try:
|
||||
result = safe_eval(self.condition_range, localdict)
|
||||
return (
|
||||
self.condition_range_min <= result <= self.condition_range_max or False
|
||||
)
|
||||
except Exception as ex:
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"Wrong range condition defined for salary rule %(nm)s "
|
||||
"(%(code)s) for employee %(ee)s.",
|
||||
nm=self.name,
|
||||
code=self.code,
|
||||
ee=localdict["employee"].name,
|
||||
)
|
||||
) from ex
|
||||
|
||||
def _satisfy_condition_python(self, localdict):
|
||||
try:
|
||||
safe_eval(self.condition_python, localdict, mode="exec")
|
||||
except Exception as ex:
|
||||
exc_text = "".join(traceback.format_exception(ex))
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"\nWrong python condition defined for salary rule %(nm)s "
|
||||
"(%(code)s) for employee %(ee)s.\nHere is the error "
|
||||
"received:\n\n%(err)s\n",
|
||||
nm=self.name,
|
||||
code=self.code,
|
||||
ee=localdict["employee"].name,
|
||||
err=exc_text,
|
||||
)
|
||||
) from ex
|
||||
return "result" in localdict and localdict["result"] or False
|
||||
Reference in New Issue
Block a user