summaryrefslogtreecommitdiff
path: root/hr_payroll_community/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 17:14:58 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 17:14:58 +0700
commit1ca3b3df3421961caec3b747a364071c80f5c7da (patch)
tree6778a1f0f3f9b4c6e26d6d87ccde16e24da6c9d6 /hr_payroll_community/models
parentb57188be371d36d96caac4b8d65a40745c0e972c (diff)
initial commit
Diffstat (limited to 'hr_payroll_community/models')
-rw-r--r--hr_payroll_community/models/__init__.py7
-rw-r--r--hr_payroll_community/models/hr_contract.py69
-rw-r--r--hr_payroll_community/models/hr_employee.py21
-rw-r--r--hr_payroll_community/models/hr_payslip.py668
-rw-r--r--hr_payroll_community/models/hr_salary_rule.py249
-rw-r--r--hr_payroll_community/models/res_config_settings.py12
6 files changed, 1026 insertions, 0 deletions
diff --git a/hr_payroll_community/models/__init__.py b/hr_payroll_community/models/__init__.py
new file mode 100644
index 0000000..5c85ef0
--- /dev/null
+++ b/hr_payroll_community/models/__init__.py
@@ -0,0 +1,7 @@
+#-*- coding:utf-8 -*-
+
+from . import hr_contract
+from . import hr_employee
+from . import res_config_settings
+from . import hr_salary_rule
+from . import hr_payslip
diff --git a/hr_payroll_community/models/hr_contract.py b/hr_payroll_community/models/hr_contract.py
new file mode 100644
index 0000000..b0bc43f
--- /dev/null
+++ b/hr_payroll_community/models/hr_contract.py
@@ -0,0 +1,69 @@
+# -*- coding:utf-8 -*-
+
+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',
+ help="Defines the frequency of the wage payment.")
+ resource_calendar_id = fields.Many2one(required=True, help="Employee's working schedule.")
+ hra = fields.Monetary(string='HRA', tracking=True, help="House rent allowance.")
+ travel_allowance = fields.Monetary(string="Travel Allowance", help="Travel allowance")
+ da = fields.Monetary(string="DA", help="Dearness allowance")
+ meal_allowance = fields.Monetary(string="Meal Allowance", help="Meal allowance")
+ medical_allowance = fields.Monetary(string="Medical Allowance", help="Medical allowance")
+ other_allowance = fields.Monetary(string="Other Allowance", help="Other allowances")
+
+ 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))
+
+ def get_attribute(self, code, attribute):
+
+ return self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1)[attribute]
+
+ def set_attribute_value(self, code, active):
+ for contract in self:
+
+ if active:
+
+ value = self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1).default_value
+ contract[code] = value
+ else:
+
+ contract[code] = 0.0
+
+
+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/hr_payroll_community/models/hr_employee.py b/hr_payroll_community/models/hr_employee.py
new file mode 100644
index 0000000..6d33a8b
--- /dev/null
+++ b/hr_payroll_community/models/hr_employee.py
@@ -0,0 +1,21 @@
+# -*- coding:utf-8 -*-
+
+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, help="payslip")
+ payslip_count = fields.Integer(compute='_compute_payslip_count', string='Payslip Count')
+
+ def _compute_payslip_count(self):
+
+ payslip_data = self.env['hr.payslip'].sudo().read_group([('employee_id', 'in', self.ids)],
+ ['employee_id'], ['employee_id'])
+ result = dict((data['employee_id'][0], data['employee_id_count']) for data in payslip_data)
+ for employee in self:
+ employee.payslip_count = result.get(employee.id, 0)
+
+
diff --git a/hr_payroll_community/models/hr_payslip.py b/hr_payroll_community/models/hr_payslip.py
new file mode 100644
index 0000000..83b313e
--- /dev/null
+++ b/hr_payroll_community/models/hr_payslip.py
@@ -0,0 +1,668 @@
+# -*- coding:utf-8 -*-
+
+import babel
+from collections import defaultdict
+from datetime import date, datetime, time
+from datetime import timedelta
+from dateutil.relativedelta import relativedelta
+from pytz import timezone
+from pytz import utc
+
+from odoo import api, fields, models, tools, _
+from odoo.addons import decimal_precision as dp
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools import float_utils
+
+# This will generate 16th of days
+ROUNDING_FACTOR = 16
+
+
+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, help="References",
+ states={'draft': [('readonly', False)]})
+ employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True, help="Employee",
+ states={'draft': [('readonly', False)]})
+ date_from = fields.Date(string='Date From', readonly=True, required=True, help="Start date",
+ default=lambda self: fields.Date.to_string(date.today().replace(day=1)),
+ states={'draft': [('readonly', False)]})
+ date_to = fields.Date(string='Date To', readonly=True, required=True, help="End date",
+ default=lambda self: fields.Date.to_string(
+ (datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()),
+ 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, help="Company",
+ 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,
+ help="Payslip worked days",
+ 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, help="Contract",
+ 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', help="Details from the 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")
+
+ 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)
+
+ 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 earlier 'Date To'."))
+
+ def action_payslip_draft(self):
+
+ return self.write({'state': 'draft'})
+
+ def action_payslip_done(self):
+
+ self.compute_sheet()
+ return self.write({'state': 'done'})
+
+ 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'})
+
+ 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_community.view_hr_payslip_form', False)
+ treeview_ref = self.env.ref('hr_payroll_community.view_hr_payslip_tree', False)
+ return {
+ 'name': ("Refund Payslip"),
+ 'view_mode': 'tree, form',
+ 'view_id': False,
+ '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': {}
+ }
+
+ def check_done(self):
+
+ return True
+
+ 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), ('state', '=', 'open'), '|',
+ '|'] + clause_1 + clause_2 + clause_3
+ return self.env['hr.contract'].search(clause_final).ids
+
+ 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, contracts, date_from, date_to):
+
+ """
+ @param contract: Browse record of contracts
+ @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
+ """
+ res = []
+ # fill only if the contract as a working schedule linked
+ for contract in contracts.filtered(lambda contract: contract.resource_calendar_id):
+ day_from = datetime.combine(fields.Date.from_string(date_from), time.min)
+ day_to = datetime.combine(fields.Date.from_string(date_to), time.max)
+
+ # compute leave days
+ leaves = {}
+ calendar = contract.resource_calendar_id
+ tz = timezone(calendar.tz)
+ day_leave_intervals = contract.employee_id.list_leaves(day_from, day_to,
+ calendar=contract.resource_calendar_id)
+ for day, hours, leave in day_leave_intervals:
+ holiday = leave.holiday_id
+ current_leave_struct = leaves.setdefault(holiday.holiday_status_id, {
+ 'name': holiday.holiday_status_id.name or _('Global Leaves'),
+ 'sequence': 5,
+ 'code': holiday.holiday_status_id.code or 'GLOBAL',
+ 'number_of_days': 0.0,
+ 'number_of_hours': 0.0,
+ 'contract_id': contract.id,
+ })
+ current_leave_struct['number_of_hours'] += hours
+ work_hours = calendar.get_work_hours_count(
+ tz.localize(datetime.combine(day, time.min)),
+ tz.localize(datetime.combine(day, time.max)),
+ compute_leaves=False,
+ )
+ if work_hours:
+ current_leave_struct['number_of_days'] += hours / work_hours
+
+ # compute worked days
+ work_data = contract.employee_id.get_work_days_data(day_from, day_to,
+ calendar=contract.resource_calendar_id)
+ attendances = {
+ 'name': _("Normal Working Days paid at 100%"),
+ 'sequence': 1,
+ 'code': 'WORK100',
+ 'number_of_days': work_data['days'],
+ 'number_of_hours': work_data['hours'],
+ 'contract_id': contract.id,
+ }
+
+ res.append(attendances)
+ res.extend(leaves.values())
+ return res
+
+ @api.model
+ def get_inputs(self, contracts, date_from, date_to):
+
+ res = []
+
+ 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)
+ localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and \
+ localdict['categories'].dict[category.code] + amount or 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)
+ if len(contracts) == 1 and payslip.struct_id:
+ structure_ids = list(set(payslip.struct_id._get_parent_structure().ids))
+ else:
+ 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 list(result_dict.values())
+
+ # 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
+ 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': [(2, x,) for x in self.input_line_ids.ids],
+ # delete old worked days lines
+ 'worked_days_line_ids': [(2, x,) for x in 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.combine(fields.Date.from_string(date_from), time.min)
+ 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
+ contracts = self.env['hr.contract'].browse(contract_ids)
+ worked_days_line_ids = self.get_worked_day_lines(contracts, date_from, date_to)
+ input_line_ids = self.get_inputs(contracts, 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
+ contract_ids = []
+
+ ttyme = datetime.combine(fields.Date.from_string(date_from), time.min)
+ 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
+ if self.contract_id:
+ contract_ids = self.contract_id.ids
+ # computation of the salary input
+ contracts = self.env['hr.contract'].browse(contract_ids)
+ worked_days_line_ids = self.get_worked_day_lines(contracts, 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(contracts, 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
+
+ def get_salary_line_total(self, code):
+
+ self.ensure_one()
+ line = self.line_ids.filtered(lambda line: line.code == code)
+ if line:
+ return line[0].total
+ else:
+ return 0.0
+
+
+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', help="Payslip")
+ salary_rule_id = fields.Many2one('hr.salary.rule', string='Rule', required=True, help="salary rule")
+ employee_id = fields.Many2one('hr.employee', string='Employee', required=True, help="Employee")
+ contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True, help="Contract")
+ 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', help="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_create_multi
+ def create(self, vals_list):
+
+ for values in vals_list:
+ 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(vals_list)
+
+
+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, help="Payslip")
+ sequence = fields.Integer(required=True, index=True, default=10, help="Sequence")
+ 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', help="Number of days worked")
+ number_of_hours = fields.Float(string='Number of Hours', help="Number of hours worked")
+ 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', help="Payslip", index=True)
+ sequence = fields.Integer(required=True, index=True, default=10, help="Sequence")
+ 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, help="start date",
+ states={'draft': [('readonly', False)]},
+ default=lambda self: fields.Date.to_string(date.today().replace(day=1)))
+ date_end = fields.Date(string='Date To', required=True, readonly=True, help="End date",
+ states={'draft': [('readonly', False)]},
+ default=lambda self: fields.Date.to_string(
+ (datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()))
+ 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.")
+
+ def draft_payslip_run(self):
+ return self.write({'state': 'draft'})
+
+ def close_payslip_run(self):
+ return self.write({'state': 'close'})
+
+
+class ResourceMixin(models.AbstractModel):
+ _inherit = "resource.mixin"
+
+ def get_work_days_data(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None):
+ """
+ By default the resource calendar is used, but it can be
+ changed using the `calendar` argument.
+
+ `domain` is used in order to recognise the leaves to take,
+ None means default value ('time_type', '=', 'leave')
+
+ Returns a dict {'days': n, 'hours': h} containing the
+ quantity of working time expressed as days and as hours.
+ """
+ resource = self.resource_id
+ calendar = calendar or self.resource_calendar_id
+
+ # naive datetimes are made explicit in UTC
+ if not from_datetime.tzinfo:
+ from_datetime = from_datetime.replace(tzinfo=utc)
+ if not to_datetime.tzinfo:
+ to_datetime = to_datetime.replace(tzinfo=utc)
+
+ # total hours per day: retrieve attendances with one extra day margin,
+ # in order to compute the total hours on the first and last days
+ from_full = from_datetime - timedelta(days=1)
+ to_full = to_datetime + timedelta(days=1)
+ intervals = calendar._attendance_intervals(from_full, to_full, resource)
+ day_total = defaultdict(float)
+ for start, stop, meta in intervals:
+ day_total[start.date()] += (stop - start).total_seconds() / 3600
+
+ # actual hours per day
+ if compute_leaves:
+ intervals = calendar._work_intervals(from_datetime, to_datetime, resource, domain)
+ else:
+ intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource)
+ day_hours = defaultdict(float)
+ for start, stop, meta in intervals:
+ day_hours[start.date()] += (stop - start).total_seconds() / 3600
+
+ # compute number of days as quarters
+ days = sum(
+ float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR
+ for day in day_hours
+ )
+ return {
+ 'days': days,
+ 'hours': sum(day_hours.values()),
+ }
diff --git a/hr_payroll_community/models/hr_salary_rule.py b/hr_payroll_community/models/hr_salary_rule.py
new file mode 100644
index 0000000..ee8205c
--- /dev/null
+++ b/hr_payroll_community/models/hr_salary_rule.py
@@ -0,0 +1,249 @@
+# -*- coding:utf-8 -*-
+
+from odoo import api, fields, models, _
+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_community.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(_('You cannot create a recursive salary structure.'))
+
+ @api.returns('self', lambda value: value.id)
+ def copy(self, default=None):
+
+ self.ensure_one()
+ default = dict(default or {}, code=_("%s (copy)") % (self.code))
+ return super(HrPayrollStructure, self).copy(default)
+
+ 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
+
+ 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, translate=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())
+
+ @api.constrains('parent_id')
+ def _check_parent_id(self):
+
+ if not self._check_recursion():
+ raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rule Category.'))
+
+
+class HrSalaryRule(models.Model):
+ _name = 'hr.salary.rule'
+ _order = 'sequence, id'
+ _description = 'Salary Rule'
+
+ name = fields.Char(required=True, translate=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.constrains('parent_rule_id')
+ def _check_parent_rule_id(self):
+ if not self._check_recursion(parent='parent_rule_id'):
+ raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rules.'))
+
+ 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)
+ 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))
+
+ 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/hr_payroll_community/models/res_config_settings.py b/hr_payroll_community/models/res_config_settings.py
new file mode 100644
index 0000000..db3c420
--- /dev/null
+++ b/hr_payroll_community/models/res_config_settings.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ module_account_accountant = fields.Boolean(string='Account Accountant')
+ module_l10n_fr_hr_payroll = fields.Boolean(string='French Payroll')
+ module_l10n_be_hr_payroll = fields.Boolean(string='Belgium Payroll')
+ module_l10n_in_hr_payroll = fields.Boolean(string='Indian Payroll')