# -*- coding:utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. from flectra import api, fields, models, _ from flectra.exceptions import UserError from flectra.tools.safe_eval import safe_eval from flectra.addons import decimal_precision as dp class HrPayrollStructure(models.Model): """ Salary structure used to defined - Basic - Allowances - Deductions """ _name = 'hr.payroll.structure' _description = 'Salary Structure' @api.model def _get_parent(self): return self.env.ref('hr_payroll.structure_base', False) name = fields.Char(required=True) code = fields.Char(string='Reference', required=True) company_id = fields.Many2one('res.company', string='Company', required=True, copy=False, default=lambda self: self.env['res.company']._company_default_get()) note = fields.Text(string='Description') parent_id = fields.Many2one('hr.payroll.structure', string='Parent', default=_get_parent) children_ids = fields.One2many('hr.payroll.structure', 'parent_id', string='Children', copy=True) rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', string='Salary Rules') @api.constrains('parent_id') def _check_parent_id(self): if not self._check_recursion(): raise ValidationError(_('Error ! You cannot create a recursive Salary Structure.')) @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}, code=_("%s (copy)") % (self.code)) return super(HrPayrollStructure, self).copy(default) @api.multi def get_all_rules(self): """ @return: returns a list of tuple (id, sequence) of rules that are maybe to apply """ all_rules = [] for struct in self: all_rules += struct.rule_ids._recursive_search_of_rules() return all_rules @api.multi def _get_parent_structure(self): parent = self.mapped('parent_id') if parent: parent = parent._get_parent_structure() return parent + self class HrContributionRegister(models.Model): _name = 'hr.contribution.register' _description = 'Contribution Register' company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) partner_id = fields.Many2one('res.partner', string='Partner') name = fields.Char(required=True) register_line_ids = fields.One2many('hr.payslip.line', 'register_id', string='Register Line', readonly=True) note = fields.Text(string='Description') class HrSalaryRuleCategory(models.Model): _name = 'hr.salary.rule.category' _description = 'Salary Rule Category' name = fields.Char(required=True) code = fields.Char(required=True) parent_id = fields.Many2one('hr.salary.rule.category', string='Parent', help="Linking a salary category to its parent is used only for the reporting purpose.") children_ids = fields.One2many('hr.salary.rule.category', 'parent_id', string='Children') note = fields.Text(string='Description') company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) class HrSalaryRule(models.Model): _name = 'hr.salary.rule' name = fields.Char(required=True) code = fields.Char(required=True, 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 " u"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', required=True) 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('hr.salary.rule', string='Parent Salary Rule', index=True) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) 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: object containing the payslips # 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 # Note: returned value have to be set in the variable 'result' result = rules.NET > categories.NET * 0.10''', 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=dp.get_precision('Payroll')) amount_percentage = fields.Float(string='Percentage (%)', digits=dp.get_precision('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: object containing the payslips # 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. # Note: returned value have to be set in the variable 'result' result = contract.wage * 0.10''') 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') @api.multi def _recursive_search_of_rules(self): """ @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids """ children_rules = [] for rule in self.filtered(lambda rule: rule.child_ids): children_rules += rule.child_ids._recursive_search_of_rules() return [(rule.id, rule.sequence) for rule in self] + children_rules #TODO should add some checks on the type of result (should be float) @api.multi def _compute_rule(self, localdict): """ :param localdict: dictionary containing the environement in which to compute the rule :return: returns a tuple build as the base/amount computed, the quantity and the rate :rtype: (float, float, float) """ self.ensure_one() if self.amount_select == 'fix': try: return self.amount_fix, float(safe_eval(self.quantity, localdict)), 100.0 except: raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) elif self.amount_select == 'percentage': try: return (float(safe_eval(self.amount_percentage_base, localdict)), float(safe_eval(self.quantity, localdict)), self.amount_percentage) except: raise UserError(_('Wrong percentage base or quantity defined for salary rule %s (%s).') % (self.name, self.code)) else: try: safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) return float(localdict['result']), 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 except: raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) @api.multi 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() if self.condition_select == 'none': return True elif self.condition_select == 'range': try: result = safe_eval(self.condition_range, localdict) return self.condition_range_min <= result and result <= self.condition_range_max or False except: raise UserError(_('Wrong range condition defined for salary rule %s (%s).') % (self.name, self.code)) else: # python code try: safe_eval(self.condition_python, localdict, mode='exec', nocopy=True) return 'result' in localdict and localdict['result'] or False except: raise UserError(_('Wrong python condition defined for salary rule %s (%s).') % (self.name, self.code)) class HrRuleInput(models.Model): _name = 'hr.rule.input' _description = 'Salary Rule Input' name = fields.Char(string='Description', required=True) code = fields.Char(required=True, help="The code that can be used in the salary rules") input_id = fields.Many2one('hr.salary.rule', string='Salary Rule Input', required=True)