diff --git a/addons/hr_payroll/__manifest__.py b/addons/hr_payroll/__manifest__.py index e4caa40bbf6..34ff35692b6 100644 --- a/addons/hr_payroll/__manifest__.py +++ b/addons/hr_payroll/__manifest__.py @@ -30,12 +30,15 @@ Generic Payroll system. 'security/hr_payroll_security.xml', 'security/ir.model.access.csv', 'wizard/hr_payroll_payslips_by_employees_views.xml', - 'views/hr_payroll_views.xml', + 'views/hr_contract_views.xml', + 'views/hr_salary_rule_views.xml', + 'views/hr_payslip_views.xml', + 'views/hr_employee_views.xml', 'data/hr_payroll_sequence.xml', 'views/hr_payroll_report.xml', 'data/hr_payroll_data.xml', 'wizard/hr_payroll_contribution_register_report_views.xml', - 'views/res_config_views.xml', + 'views/hr_payroll_config_settings_views.xml', 'views/report_contributionregister_templates.xml', 'views/report_payslip_templates.xml', 'views/report_payslipdetails_templates.xml', diff --git a/addons/hr_payroll/models/__init__.py b/addons/hr_payroll/models/__init__.py index c8c7a73abb8..792c8e2bcd2 100644 --- a/addons/hr_payroll/models/__init__.py +++ b/addons/hr_payroll/models/__init__.py @@ -1,5 +1,8 @@ #-*- coding:utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import hr_payroll -import res_config +import hr_contract +import hr_employee +import hr_payroll_config_settings +import hr_salary_rule +import hr_payslip diff --git a/addons/hr_payroll/models/hr_contract.py b/addons/hr_payroll/models/hr_contract.py new file mode 100644 index 00000000000..389736f3790 --- /dev/null +++ b/addons/hr_payroll/models/hr_contract.py @@ -0,0 +1,52 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +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') + resource_calendar_id = fields.Many2one(required=True) + + @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)) + + @api.multi + def get_attribute(self, code, attribute): + return getattr(self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1), attribute) + + +class HrContractAdvandageTemplate(models.Model): + _name = 'hr.contract.advantage.template' + _description = "Employee's Advantage on Contract" + + name = fields.Char('Name', required=True) + code = fields.Char('Code', required=True) + lower_bound = fields.Float('Lower Bound', help="Lower bound authorized by the employer for this advantage") + upper_bound = fields.Float('Upper Bound', help="Upper bound authorized by the employer for this advantage") + default_value = fields.Float('Default value for this advantage') diff --git a/addons/hr_payroll/models/hr_employee.py b/addons/hr_payroll/models/hr_employee.py new file mode 100644 index 00000000000..6d1283e7346 --- /dev/null +++ b/addons/hr_payroll/models/hr_employee.py @@ -0,0 +1,17 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +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) diff --git a/addons/hr_payroll/models/res_config.py b/addons/hr_payroll/models/hr_payroll_config_settings.py similarity index 100% rename from addons/hr_payroll/models/res_config.py rename to addons/hr_payroll/models/hr_payroll_config_settings.py diff --git a/addons/hr_payroll/models/hr_payroll.py b/addons/hr_payroll/models/hr_payslip.py similarity index 66% rename from addons/hr_payroll/models/hr_payroll.py rename to addons/hr_payroll/models/hr_payslip.py index 14092f50269..894dfa91a35 100644 --- a/addons/hr_payroll/models/hr_payroll.py +++ b/addons/hr_payroll/models/hr_payslip.py @@ -2,172 +2,15 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import time -from datetime import datetime, timedelta +from datetime import datetime from datetime import time as datetime_time 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') - resource_calendar_id = fields.Many2one(required=True) - - @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)) - - @api.multi - def get_attribute(self, code, attribute): - return getattr(self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1), attribute) - - -class HrContractAdvandageTemplate(models.Model): - _name = 'hr.contract.advantage.template' - _description = "Employee's Advantage on Contract" - - name = fields.Char('Name', required=True) - code = fields.Char('Code', required=True) - lower_bound = fields.Float('Lower Bound', help="Lower bound authorized by the employer for this advantage") - upper_bound = fields.Float('Upper Bound', help="Upper bound authorized by the employer for this advantage") - default_value = fields.Float('Default value for this advantage') - -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'}) +from odoo.exceptions import UserError, ValidationError class HrPayslip(models.Model): @@ -286,7 +129,7 @@ class HrPayslip(models.Model): 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 + # TODO move this function into hr_contract module, on hr.employee object @api.model def get_contract(self, employee, date_from, date_to): """ @@ -295,11 +138,11 @@ class HrPayslip(models.Model): @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 + # 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 + # 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) + # 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), ('state', '=', 'open'), '|', '|'] + clause_1 + clause_2 + clause_3 return self.env['hr.contract'].search(clause_final).ids @@ -308,7 +151,7 @@ class HrPayslip(models.Model): 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 + # 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 @@ -642,186 +485,6 @@ class HrPayslip(models.Model): else: return 0.0 -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' @@ -853,14 +516,62 @@ class HrPayslipLine(models.Model): 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") +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 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 _compute_payslip_count(self): - for employee in self: - employee.payslip_count = len(employee.slip_ids) + def draft_payslip_run(self): + return self.write({'state': 'draft'}) + + @api.multi + def close_payslip_run(self): + return self.write({'state': 'close'}) diff --git a/addons/hr_payroll/models/hr_salary_rule.py b/addons/hr_payroll/models/hr_salary_rule.py new file mode 100644 index 00000000000..cf1b01fa60c --- /dev/null +++ b/addons/hr_payroll/models/hr_salary_rule.py @@ -0,0 +1,236 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +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 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) diff --git a/addons/hr_payroll/views/hr_contract_views.xml b/addons/hr_payroll/views/hr_contract_views.xml new file mode 100644 index 00000000000..03d0b09cef2 --- /dev/null +++ b/addons/hr_payroll/views/hr_contract_views.xml @@ -0,0 +1,81 @@ + + + + + + + + + + hr.contract.view.form.inherit + hr.contract + + + + + + + + + + + + + + + + + hr.contract.advantage.template.form + hr.contract.advantage.template + +
+ +
+

+ +

+
+ + + + + + + + + + + + +
+
+
+
+ + + hr.contract.advantage.template.tree + hr.contract.advantage.template + + + + + + + + + + + + + Contract Advantage Templates + hr.contract.advantage.template + form + + + +
\ No newline at end of file diff --git a/addons/hr_payroll/views/hr_employee_views.xml b/addons/hr_payroll/views/hr_employee_views.xml new file mode 100644 index 00000000000..20b765e6910 --- /dev/null +++ b/addons/hr_payroll/views/hr_employee_views.xml @@ -0,0 +1,21 @@ + + + + + + payroll.hr.employee.view.form + hr.employee + + + + + + + \ No newline at end of file diff --git a/addons/hr_payroll/views/res_config_views.xml b/addons/hr_payroll/views/hr_payroll_config_settings_views.xml similarity index 100% rename from addons/hr_payroll/views/res_config_views.xml rename to addons/hr_payroll/views/hr_payroll_config_settings_views.xml diff --git a/addons/hr_payroll/views/hr_payroll_views.xml b/addons/hr_payroll/views/hr_payroll_views.xml deleted file mode 100644 index 1db77988b16..00000000000 --- a/addons/hr_payroll/views/hr_payroll_views.xml +++ /dev/null @@ -1,800 +0,0 @@ - - - - - - - - - - hr.contract.view.form.inherit - hr.contract - - - - - - - - - - - - - - - - - hr.contract.advantage.template.form - hr.contract.advantage.template - -
- -
-

- -

-
- - - - - - - - - - - - -
-
-
-
- - - hr.contract.advantage.template.tree - hr.contract.advantage.template - - - - - - - - - - - - - Contract Advantage Templates - hr.contract.advantage.template - form - - - - - - - - - - hr.payroll.structure.tree - hr.payroll.structure - - - - - - - - - - - hr.payroll.structure.tree - hr.payroll.structure - children_ids - - - - - - - - - - - hr.payroll.structure.select - hr.payroll.structure - - - - - - - - - hr.payroll.structure.form - hr.payroll.structure - -
- - - - - - - - - - - - - - - - - - - -
-
-
- - - Salary Structures - hr.payroll.structure - form - - - - - Salary Structures Hierarchy - hr.payroll.structure - tree - [('parent_id','=',False)] - - - - - - - - hr.payslip.line.tree - hr.payslip.line - - - - - - - - - - - - - - - - - - hr.payslip.line.form - hr.payslip.line - -
- - - - - - - - - - - - - - - - -
-
-
- - - hr.payslip.line.select - hr.payslip.line - - - - - - - - - - - - - - - - - - - - hr.payslip.tree - hr.payslip - - - - - - - - - - - - - - - - - - hr.payslip.form - hr.payslip - -
-
-
- -
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
-
-
- - - - - - - - - - - - - -
- - - - - - - - - - - -
-
-
- - - - - - - - - - - - - - - - - - - - -
- -
-
-
-
- -
-
- - - hr.payslip.select - hr.payslip - - - - - - - - - - - - - - - - - - - Employee Payslips - hr.payslip - form - - - - - - - - hr.payslip - Payslips - form - tree,form - {'search_default_employee_id': [active_id], 'default_employee_id': active_id} - - - - - payroll.hr.employee.view.form - hr.employee - - - - - - - - - hr.salary.rule.category.form - hr.salary.rule.category - -
- - - - - - - - -
-
-
- - - hr.salary.rule.category.tree - hr.salary.rule.category - - - - - - - - - - hr.salary.rule.category.tree.view - hr.salary.rule.category - children_ids - - - - - - - - - - - hr.salary.rule.category.select - hr.salary.rule.category - - - - - - - - - Salary Rule Categories - hr.salary.rule.category - form - - - - - - Salary Rule Categories Hierarchy - hr.salary.rule.category - tree - [('parent_id','=',False)] - - - - - - - hr.contribution.register.tree - hr.contribution.register - - - - - - - - - hr.contribution.register.search - hr.contribution.register - - - - - - - - - hr.contribution.register.form - hr.contribution.register - -
- - - - - - - - - -
-
- - Contribution Registers - hr.contribution.register - form - -

- Click to add a new contribution register. -

- A contribution register is a third party involved in the salary - payment of the employees. It can be the social security, the - estate or anyone that collect or inject money on payslips. -

-
- -
- - - - - - hr.salary.rule.list - hr.salary.rule - - - - - - - - - - - - - hr.salary.rule.tree - hr.salary.rule - child_ids - - - - - - - - - - - - hr.salary.rule.form - hr.salary.rule - -
-
-
- - - hr.salary.rule.select - hr.salary.rule - - - - - - - - - - - - - - Salary Rules - hr.salary.rule - form - - [('parent_rule_id','=',False)] - - - - - - - - - - - - hr.payslip.run.search - hr.payslip.run - - - - - - - - - - - - - hr.payslip.run.tree - hr.payslip.run - - - - - - - - - - - - - hr.payslip.run.form - hr.payslip.run - -
-
-
- - -
-
-
- - - Payslips Batches - hr.payslip.run - form - - - - - - - - - -
diff --git a/addons/hr_payroll/views/hr_payslip_views.xml b/addons/hr_payroll/views/hr_payslip_views.xml new file mode 100644 index 00000000000..74cc888141c --- /dev/null +++ b/addons/hr_payroll/views/hr_payslip_views.xml @@ -0,0 +1,346 @@ + + + + + hr.payslip.line.tree + hr.payslip.line + + + + + + + + + + + + + + + + + + hr.payslip.line.form + hr.payslip.line + +
+ + + + + + + + + + + + + + + + +
+
+
+ + + hr.payslip.line.select + hr.payslip.line + + + + + + + + + + + + + + + + + + + + hr.payslip.tree + hr.payslip + + + + + + + + + + + + + + + + + + hr.payslip.form + hr.payslip + +
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ +
+
+ + + hr.payslip.select + hr.payslip + + + + + + + + + + + + + + + + + + + Employee Payslips + hr.payslip + form + + + + + + + + hr.payslip + Payslips + form + tree,form + {'search_default_employee_id': [active_id], 'default_employee_id': active_id} + + + + + + hr.payslip.run.search + hr.payslip.run + + + + + + + + + + + + + hr.payslip.run.tree + hr.payslip.run + + + + + + + + + + + + + hr.payslip.run.form + hr.payslip.run + +
+
+
+ + +
+
+
+ + + Payslips Batches + hr.payslip.run + form + + + + + + + + + +
\ No newline at end of file diff --git a/addons/hr_payroll/views/hr_salary_rule_views.xml b/addons/hr_payroll/views/hr_salary_rule_views.xml new file mode 100644 index 00000000000..a086dfb795d --- /dev/null +++ b/addons/hr_payroll/views/hr_salary_rule_views.xml @@ -0,0 +1,356 @@ + + + + + + hr.payroll.structure.tree + hr.payroll.structure + + + + + + + + + + + hr.payroll.structure.tree + hr.payroll.structure + children_ids + + + + + + + + + + + hr.payroll.structure.select + hr.payroll.structure + + + + + + + + + hr.payroll.structure.form + hr.payroll.structure + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + + Salary Structures + hr.payroll.structure + form + + + + + Salary Structures Hierarchy + hr.payroll.structure + tree + [('parent_id','=',False)] + + + + + + hr.salary.rule.category.form + hr.salary.rule.category + +
+ + + + + + + + +
+
+
+ + + hr.salary.rule.category.tree + hr.salary.rule.category + + + + + + + + + + hr.salary.rule.category.tree.view + hr.salary.rule.category + children_ids + + + + + + + + + + + hr.salary.rule.category.select + hr.salary.rule.category + + + + + + + + + Salary Rule Categories + hr.salary.rule.category + form + + + + + + Salary Rule Categories Hierarchy + hr.salary.rule.category + tree + [('parent_id','=',False)] + + + + + + + hr.contribution.register.tree + hr.contribution.register + + + + + + + + + hr.contribution.register.search + hr.contribution.register + + + + + + + + + hr.contribution.register.form + hr.contribution.register + +
+ + + + + + + + + +
+
+ + Contribution Registers + hr.contribution.register + form + +

+ Click to add a new contribution register. +

+ A contribution register is a third party involved in the salary + payment of the employees. It can be the social security, the + estate or anyone that collect or inject money on payslips. +

+
+ +
+ + + + + + hr.salary.rule.list + hr.salary.rule + + + + + + + + + + + + + hr.salary.rule.tree + hr.salary.rule + child_ids + + + + + + + + + + + + hr.salary.rule.form + hr.salary.rule + +
+
+
+ + + hr.salary.rule.select + hr.salary.rule + + + + + + + + + + + + + + Salary Rules + hr.salary.rule + form + + [('parent_rule_id','=',False)] + + + + + + + +
\ No newline at end of file