#-*- coding:utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import time from datetime import datetime, timedelta from dateutil import relativedelta import babel from odoo import api, fields, models, tools, _ from odoo.exceptions import UserError, ValidationError from odoo.tools.safe_eval import safe_eval from odoo.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 HrContract(models.Model): """ Employee contract based on the visa, work permits allows to configure different Salary structure """ _inherit = 'hr.contract' _description = 'Employee Contract' struct_id = fields.Many2one('hr.payroll.structure', string='Salary Structure') schedule_pay = fields.Selection([ ('monthly', 'Monthly'), ('quarterly', 'Quarterly'), ('semi-annually', 'Semi-annually'), ('annually', 'Annually'), ('weekly', 'Weekly'), ('bi-weekly', 'Bi-weekly'), ('bi-monthly', 'Bi-monthly'), ], string='Scheduled Pay', index=True, default='monthly') @api.multi def get_all_structures(self): """ @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata """ structures = self.mapped('struct_id') if not structures: return [] # YTI TODO return browse records return list(set(structures._get_parent_structure().ids)) 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 HrPayslipRun(models.Model): _name = 'hr.payslip.run' _description = 'Payslip Batches' name = fields.Char(required=True, readonly=True, states={'draft': [('readonly', False)]}) slip_ids = fields.One2many('hr.payslip', 'payslip_run_id', string='Payslips', readonly=True, states={'draft': [('readonly', False)]}) state = fields.Selection([ ('draft', 'Draft'), ('close', 'Close'), ], string='Status', index=True, readonly=True, copy=False, default='draft') date_start = fields.Date(string='Date From', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=time.strftime('%Y-%m-01')) date_end = fields.Date(string='Date To', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10]) credit_note = fields.Boolean(string='Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="If its checked, indicates that all payslips generated from here are refund payslips.") @api.multi def draft_payslip_run(self): return self.write({'state': 'draft'}) @api.multi def close_payslip_run(self): return self.write({'state': 'close'}) class HrPayslip(models.Model): _name = 'hr.payslip' _description = 'Pay Slip' struct_id = fields.Many2one('hr.payroll.structure', string='Structure', readonly=True, states={'draft': [('readonly', False)]}, help='Defines the rules that have to be applied to this payslip, accordingly ' 'to the contract chosen. If you let empty the field contract, this field isn\'t ' 'mandatory anymore and thus the rules applied will be all the rules set on the ' 'structure of all contracts of the employee valid for the chosen period') name = fields.Char(string='Payslip Name', readonly=True, states={'draft': [('readonly', False)]}) number = fields.Char(string='Reference', readonly=True, copy=False, states={'draft': [('readonly', False)]}) employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}) date_from = fields.Date(string='Date From', readonly=True, required=True, default=time.strftime('%Y-%m-01'), states={'draft': [('readonly', False)]}) date_to = fields.Date(string='Date To', readonly=True, required=True, default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10], states={'draft': [('readonly', False)]}) # this is chaos: 4 states are defined, 3 are used ('verify' isn't) and 5 exist ('confirm' seems to have existed) state = fields.Selection([ ('draft', 'Draft'), ('verify', 'Waiting'), ('done', 'Done'), ('cancel', 'Rejected'), ], string='Status', index=True, readonly=True, copy=False, default='draft', help="""* When the payslip is created the status is \'Draft\' \n* If the payslip is under verification, the status is \'Waiting\'. \n* If the payslip is confirmed then status is set to \'Done\'. \n* When user cancel payslip the status is \'Rejected\'.""") line_ids = fields.One2many('hr.payslip.line', 'slip_id', string='Payslip Lines', readonly=True, states={'draft': [('readonly', False)]}) company_id = fields.Many2one('res.company', string='Company', readonly=True, copy=False, default=lambda self: self.env['res.company']._company_default_get(), states={'draft': [('readonly', False)]}) worked_days_line_ids = fields.One2many('hr.payslip.worked_days', 'payslip_id', string='Payslip Worked Days', copy=True, readonly=True, states={'draft': [('readonly', False)]}) input_line_ids = fields.One2many('hr.payslip.input', 'payslip_id', string='Payslip Inputs', readonly=True, states={'draft': [('readonly', False)]}) paid = fields.Boolean(string='Made Payment Order ? ', readonly=True, copy=False, states={'draft': [('readonly', False)]}) note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)]}) contract_id = fields.Many2one('hr.contract', string='Contract', readonly=True, states={'draft': [('readonly', False)]}) details_by_salary_rule_category = fields.One2many('hr.payslip.line', compute='_compute_details_by_salary_rule_category', string='Details by Salary Rule Category') credit_note = fields.Boolean(string='Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="Indicates this payslip has a refund of another") payslip_run_id = fields.Many2one('hr.payslip.run', string='Payslip Batches', readonly=True, copy=False, states={'draft': [('readonly', False)]}) payslip_count = fields.Integer(compute='_compute_payslip_count', string="Payslip Computation Details") @api.multi def _compute_details_by_salary_rule_category(self): for payslip in self: payslip.details_by_salary_rule_category = payslip.mapped('line_ids').filtered(lambda line: line.category_id) @api.multi def _compute_payslip_count(self): for payslip in self: payslip.payslip_count = len(payslip.line_ids) @api.constrains('date_from', 'date_to') def _check_dates(self): if any(self.filtered(lambda payslip: payslip.date_from > payslip.date_to)): raise ValidationError(_("Payslip 'Date From' must be before 'Date To'.")) @api.multi def action_payslip_draft(self): return self.write({'state': 'draft'}) @api.multi def action_payslip_done(self): self.compute_sheet() return self.write({'state': 'done'}) @api.multi def action_payslip_cancel(self): if self.filtered(lambda slip: slip.state == 'done'): raise UserError(_("Cannot cancel a payslip that is done.")) return self.write({'state': 'cancel'}) @api.multi def refund_sheet(self): for payslip in self: copied_payslip = payslip.copy({'credit_note': True, 'name': _('Refund: ') + payslip.name}) copied_payslip.compute_sheet() copied_payslip.action_payslip_done() formview_ref = self.env.ref('hr_payroll.view_hr_payslip_form', False) treeview_ref = self.env.ref('hr_payroll.view_hr_payslip_tree', False) return { 'name': ("Refund Payslip"), 'view_mode': 'tree, form', 'view_id': False, 'view_type': 'form', 'res_model': 'hr.payslip', 'type': 'ir.actions.act_window', 'target': 'current', 'domain': "[('id', 'in', %s)]" % copied_payslip.ids, 'views': [(treeview_ref and treeview_ref.id or False, 'tree'), (formview_ref and formview_ref.id or False, 'form')], 'context': {} } @api.multi def check_done(self): return True @api.multi def unlink(self): if any(self.filtered(lambda payslip: payslip.state not in ('draft', 'cancel'))): raise UserError(_('You cannot delete a payslip which is not draft or cancelled!')) return super(HrPayslip, self).unlink() #TODO move this function into hr_contract module, on hr.employee object @api.model def get_contract(self, employee, date_from, date_to): """ @param employee: recordset of employee @param date_from: date field @param date_to: date field @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates """ #a contract is valid if it ends between the given dates clause_1 = ['&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)] #OR if it starts between the given dates clause_2 = ['&', ('date_start', '<=', date_to), ('date_start', '>=', date_from)] #OR if it starts before the date_from and finish after the date_end (or never finish) clause_3 = ['&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to)] clause_final = [('employee_id', '=', employee.id), '|', '|'] + clause_1 + clause_2 + clause_3 return self.env['hr.contract'].search(clause_final).ids @api.multi def compute_sheet(self): for payslip in self: number = payslip.number or self.env['ir.sequence'].next_by_code('salary.slip') #delete old payslip lines payslip.line_ids.unlink() # set the list of contract for which the rules have to be applied # if we don't give the contract, then the rules to apply should be for all current contracts of the employee contract_ids = payslip.contract_id.ids or \ self.get_contract(payslip.employee_id, payslip.date_from, payslip.date_to) lines = [(0, 0, line) for line in self.get_payslip_lines(contract_ids, payslip.id)] payslip.write({'line_ids': lines, 'number': number}) return True @api.model def get_worked_day_lines(self, contract_ids, date_from, date_to): """ @param contract_ids: list of contract id @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to """ def was_on_leave_interval(employee_id, date_from, date_to): date_from = fields.Datetime.to_string(date_from) date_to = fields.Datetime.to_string(date_to) return self.env['hr.holidays'].search([ ('state', '=', 'validate'), ('employee_id', '=', employee_id), ('type', '=', 'remove'), ('date_from', '<=', date_from), ('date_to', '>=', date_to) ], limit=1) res = [] #fill only if the contract as a working schedule linked uom_day = self.env.ref('product.product_uom_day', raise_if_not_found=False) for contract in self.env['hr.contract'].browse(contract_ids).filtered(lambda contract: contract.working_hours): uom_hour = contract.employee_id.resource_id.calendar_id.uom_id or self.env.ref('product.product_uom_hour', raise_if_not_found=False) interval_data = [] holidays = self.env['hr.holidays'] attendances = { 'name': _("Normal Working Days paid at 100%"), 'sequence': 1, 'code': 'WORK100', 'number_of_days': 0.0, 'number_of_hours': 0.0, 'contract_id': contract.id, } leaves = {} day_from = fields.Datetime.from_string(date_from) day_to = fields.Datetime.from_string(date_to) nb_of_days = (day_to - day_from).days + 1 # Gather all intervals and holidays for day in range(0, nb_of_days): working_intervals_on_day = contract.working_hours.get_working_intervals_of_day(start_dt=day_from + timedelta(days=day)) for interval in working_intervals_on_day: interval_data.append((interval, was_on_leave_interval(contract.employee_id.id, interval[0], interval[1]))) # Extract information from previous data. A working interval is considered: # - as a leave if a hr.holiday completely covers the period # - as a working period instead for interval, holiday in interval_data: holidays |= holiday hours = (interval[1] - interval[0]).total_seconds() / 3600.0 if holiday: #if he was on leave, fill the leaves dict if holiday.holiday_status_id.name in leaves: leaves[holiday.holiday_status_id.name]['number_of_hours'] += hours else: leaves[holiday.holiday_status_id.name] = { 'name': holiday.holiday_status_id.name, 'sequence': 5, 'code': holiday.holiday_status_id.name, 'number_of_days': 0.0, 'number_of_hours': hours, 'contract_id': contract.id, } else: #add the input vals to tmp (increment if existing) attendances['number_of_hours'] += hours # Clean-up the results leaves = [value for key, value in leaves.items()] for data in [attendances] + leaves: data['number_of_days'] = uom_hour._compute_quantity(data['number_of_hours'], uom_day)\ if uom_day and uom_hour\ else data['number_of_hours'] / 8.0 res.append(data) return res # YTI TODO contract_ids should be a browse record @api.model def get_inputs(self, contract_ids, date_from, date_to): res = [] contracts = self.env['hr.contract'].browse(contract_ids) structure_ids = contracts.get_all_structures() rule_ids = self.env['hr.payroll.structure'].browse(structure_ids).get_all_rules() sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])] inputs = self.env['hr.salary.rule'].browse(sorted_rule_ids).mapped('input_ids') for contract in contracts: for input in inputs: input_data = { 'name': input.name, 'code': input.code, 'contract_id': contract.id, } res += [input_data] return res @api.model def get_payslip_lines(self, contract_ids, payslip_id): def _sum_salary_rule_category(localdict, category, amount): if category.parent_id: localdict = _sum_salary_rule_category(localdict, category.parent_id, amount) if category.code in localdict['categories'].dict: amount += localdict['categories'].dict[category.code] localdict['categories'].dict[category.code] = amount return localdict class BrowsableObject(object): def __init__(self, employee_id, dict, env): self.employee_id = employee_id self.dict = dict self.env = env def __getattr__(self, attr): return attr in self.dict and self.dict.__getitem__(attr) or 0.0 class InputLine(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute(""" SELECT sum(amount) as sum FROM hr_payslip as hp, hr_payslip_input as pi WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", (self.employee_id, from_date, to_date, code)) return self.env.cr.fetchone()[0] or 0.0 class WorkedDays(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def _sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute(""" SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours FROM hr_payslip as hp, hr_payslip_worked_days as pi WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", (self.employee_id, from_date, to_date, code)) return self.env.cr.fetchone() def sum(self, code, from_date, to_date=None): res = self._sum(code, from_date, to_date) return res and res[0] or 0.0 def sum_hours(self, code, from_date, to_date=None): res = self._sum(code, from_date, to_date) return res and res[1] or 0.0 class Payslips(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute("""SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end) FROM hr_payslip as hp, hr_payslip_line as pl WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""", (self.employee_id, from_date, to_date, code)) res = self.env.cr.fetchone() return res and res[0] or 0.0 #we keep a dict with the result because a value can be overwritten by another rule with the same code result_dict = {} rules_dict = {} worked_days_dict = {} inputs_dict = {} blacklist = [] payslip = self.env['hr.payslip'].browse(payslip_id) for worked_days_line in payslip.worked_days_line_ids: worked_days_dict[worked_days_line.code] = worked_days_line for input_line in payslip.input_line_ids: inputs_dict[input_line.code] = input_line categories = BrowsableObject(payslip.employee_id.id, {}, self.env) inputs = InputLine(payslip.employee_id.id, inputs_dict, self.env) worked_days = WorkedDays(payslip.employee_id.id, worked_days_dict, self.env) payslips = Payslips(payslip.employee_id.id, payslip, self.env) rules = BrowsableObject(payslip.employee_id.id, rules_dict, self.env) baselocaldict = {'categories': categories, 'rules': rules, 'payslip': payslips, 'worked_days': worked_days, 'inputs': inputs} #get the ids of the structures on the contracts and their parent id as well contracts = self.env['hr.contract'].browse(contract_ids) structure_ids = contracts.get_all_structures() #get the rules of the structure and thier children rule_ids = self.env['hr.payroll.structure'].browse(structure_ids).get_all_rules() #run the rules by sequence sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])] sorted_rules = self.env['hr.salary.rule'].browse(sorted_rule_ids) for contract in contracts: employee = contract.employee_id localdict = dict(baselocaldict, employee=employee, contract=contract) for rule in sorted_rules: key = rule.code + '-' + str(contract.id) localdict['result'] = None localdict['result_qty'] = 1.0 localdict['result_rate'] = 100 #check if the rule can be applied if rule.satisfy_condition(localdict) and rule.id not in blacklist: #compute the amount of the rule amount, qty, rate = rule.compute_rule(localdict) #check if there is already a rule computed with that code previous_amount = rule.code in localdict and localdict[rule.code] or 0.0 #set/overwrite the amount computed for this rule in the localdict tot_rule = amount * qty * rate / 100.0 localdict[rule.code] = tot_rule rules_dict[rule.code] = rule #sum the amount for its salary category localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount) #create/overwrite the rule in the temporary results result_dict[key] = { 'salary_rule_id': rule.id, 'contract_id': contract.id, 'name': rule.name, 'code': rule.code, 'category_id': rule.category_id.id, 'sequence': rule.sequence, 'appears_on_payslip': rule.appears_on_payslip, 'condition_select': rule.condition_select, 'condition_python': rule.condition_python, 'condition_range': rule.condition_range, 'condition_range_min': rule.condition_range_min, 'condition_range_max': rule.condition_range_max, 'amount_select': rule.amount_select, 'amount_fix': rule.amount_fix, 'amount_python_compute': rule.amount_python_compute, 'amount_percentage': rule.amount_percentage, 'amount_percentage_base': rule.amount_percentage_base, 'register_id': rule.register_id.id, 'amount': amount, 'employee_id': contract.employee_id.id, 'quantity': qty, 'rate': rate, } else: #blacklist this rule and its children blacklist += [id for id, seq in rule._recursive_search_of_rules()] return [value for code, value in result_dict.items()] # YTI TODO To rename. This method is not really an onchange, as it is not in any view # employee_id and contract_id could be browse records @api.multi def onchange_employee_id(self, date_from, date_to, employee_id=False, contract_id=False): #defaults res = { 'value': { 'line_ids': [], #delete old input lines 'input_line_ids': map(lambda x: (2, x,), self.input_line_ids.ids), #delete old worked days lines 'worked_days_line_ids': map(lambda x: (2, x,), self.worked_days_line_ids.ids), #'details_by_salary_head':[], TODO put me back 'name': '', 'contract_id': False, 'struct_id': False, } } if (not employee_id) or (not date_from) or (not date_to): return res ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d"))) employee = self.env['hr.employee'].browse(employee_id) locale = self.env.context.get('lang') or 'en_US' res['value'].update({ 'name': _('Salary Slip of %s for %s') % (employee.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale))), 'company_id': employee.company_id.id, }) if not self.env.context.get('contract'): #fill with the first contract of the employee contract_ids = self.get_contract(employee, date_from, date_to) else: if contract_id: #set the list of contract for which the input have to be filled contract_ids = [contract_id] else: #if we don't give the contract, then the input to fill should be for all current contracts of the employee contract_ids = self.get_contract(employee, date_from, date_to) if not contract_ids: return res contract = self.env['hr.contract'].browse(contract_ids[0]) res['value'].update({ 'contract_id': contract.id }) struct = contract.struct_id if not struct: return res res['value'].update({ 'struct_id': struct.id, }) #computation of the salary input worked_days_line_ids = self.get_worked_day_lines(contract_ids, date_from, date_to) input_line_ids = self.get_inputs(contract_ids, date_from, date_to) res['value'].update({ 'worked_days_line_ids': worked_days_line_ids, 'input_line_ids': input_line_ids, }) return res @api.onchange('employee_id', 'date_from', 'date_to') def onchange_employee(self): if (not self.employee_id) or (not self.date_from) or (not self.date_to): return employee = self.employee_id date_from = self.date_from date_to = self.date_to ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d"))) locale = self.env.context.get('lang') or 'en_US' self.name = _('Salary Slip of %s for %s') % (employee.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale))) self.company_id = employee.company_id if not self.env.context.get('contract') or not self.contract_id: contract_ids = self.get_contract(employee, date_from, date_to) if not contract_ids: return self.contract_id = self.env['hr.contract'].browse(contract_ids[0]) if not self.contract_id.struct_id: return self.struct_id = self.contract_id.struct_id #computation of the salary input worked_days_line_ids = self.get_worked_day_lines(contract_ids, date_from, date_to) worked_days_lines = self.worked_days_line_ids.browse([]) for r in worked_days_line_ids: worked_days_lines += worked_days_lines.new(r) self.worked_days_line_ids = worked_days_lines input_line_ids = self.get_inputs(contract_ids, date_from, date_to) input_lines = self.input_line_ids.browse([]) for r in input_line_ids: input_lines += input_lines.new(r) self.input_line_ids = input_lines return @api.onchange('contract_id') def onchange_contract(self): if not self.contract_id: self.struct_id = False self.with_context(contract=True).onchange_employee() return class HrPayslipWorkedDays(models.Model): _name = 'hr.payslip.worked_days' _description = 'Payslip Worked Days' _order = 'payslip_id, sequence' name = fields.Char(string='Description', required=True) payslip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade', index=True) sequence = fields.Integer(required=True, index=True, default=10) code = fields.Char(required=True, help="The code that can be used in the salary rules") number_of_days = fields.Float(string='Number of Days') number_of_hours = fields.Float(string='Number of Hours') contract_id = fields.Many2one('hr.contract', string='Contract', required=True, help="The contract for which applied this input") class HrPayslipInput(models.Model): _name = 'hr.payslip.input' _description = 'Payslip Input' _order = 'payslip_id, sequence' name = fields.Char(string='Description', required=True) payslip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade', index=True) sequence = fields.Integer(required=True, index=True, default=10) code = fields.Char(required=True, help="The code that can be used in the salary rules") amount = fields.Float(help="It is used in computation. For e.g. A rule for sales having " "1% commission of basic salary for per product can defined in expression " "like result = inputs.SALEURO.amount * contract.wage*0.01.") contract_id = fields.Many2one('hr.contract', string='Contract', required=True, help="The contract for which applied this input") 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) class HrPayslipLine(models.Model): _name = 'hr.payslip.line' _inherit = 'hr.salary.rule' _description = 'Payslip Line' _order = 'contract_id, sequence' slip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade') salary_rule_id = fields.Many2one('hr.salary.rule', string='Rule', required=True) employee_id = fields.Many2one('hr.employee', string='Employee', required=True) contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True) rate = fields.Float(string='Rate (%)', digits=dp.get_precision('Payroll Rate'), default=100.0) amount = fields.Float(digits=dp.get_precision('Payroll')) quantity = fields.Float(digits=dp.get_precision('Payroll'), default=1.0) total = fields.Float(compute='_compute_total', string='Total', digits=dp.get_precision('Payroll'), store=True) @api.depends('quantity', 'amount', 'rate') def _compute_total(self): for line in self: line.total = float(line.quantity) * line.amount * line.rate / 100 @api.model def create(self, values): if 'employee_id' not in values or 'contract_id' not in values: payslip = self.env['hr.payslip'].browse(values.get('slip_id')) values['employee_id'] = values.get('employee_id') or payslip.employee_id.id values['contract_id'] = values.get('contract_id') or payslip.contract_id and payslip.contract_id.id if not values['contract_id']: raise UserError(_('You must set a contract to create a payslip line.')) return super(HrPayslipLine, self).create(values) class HrEmployee(models.Model): _inherit = 'hr.employee' _description = 'Employee' slip_ids = fields.One2many('hr.payslip', 'employee_id', string='Payslips', readonly=True) payslip_count = fields.Integer(compute='_compute_payslip_count', string='Payslips', groups="hr_payroll.group_hr_payroll_user") @api.multi def _compute_payslip_count(self): for employee in self: employee.payslip_count = len(employee.slip_ids)