mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[ADD] hr_payroll_hibou: for Odoo Enterprise 14.0
Abstract fixes and behaviors.
- Adds configurable option for Payslip rule calculation sum date field.
- Fixes inconsistency between .sum_category('CODE') and .categories['CODE']
- Adds semi-monthly Semi-monthly schedule_pay
All features are tested and the tests themselves should serve as test harnesses for other 'payroll' modules.
This commit is contained in:
1
hr_payroll_hibou/__init__.py
Normal file
1
hr_payroll_hibou/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
27
hr_payroll_hibou/__manifest__.py
Normal file
27
hr_payroll_hibou/__manifest__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Hibou Payroll',
|
||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Payroll Localization',
|
||||
'depends': [
|
||||
'hr_payroll',
|
||||
'hr_contract_reports',
|
||||
'hibou_professional',
|
||||
],
|
||||
'description': """
|
||||
Hibou Payroll
|
||||
=============
|
||||
|
||||
Base module for fixing specific qwerks or assumptions in the way Payroll Odoo Enterprise Edition behaves.
|
||||
|
||||
""",
|
||||
'data': [
|
||||
'views/res_config_settings_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
],
|
||||
'auto_install': True,
|
||||
'license': 'OPL-1',
|
||||
}
|
||||
4
hr_payroll_hibou/models/__init__.py
Normal file
4
hr_payroll_hibou/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import browsable_object
|
||||
from . import hr_payslip
|
||||
from . import hr_salary_rule
|
||||
from . import res_config_settings
|
||||
151
hr_payroll_hibou/models/browsable_object.py
Normal file
151
hr_payroll_hibou/models/browsable_object.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.hr_payroll.models import browsable_object
|
||||
|
||||
|
||||
class BrowsableObject(object):
|
||||
def __init__(self, employee_id, dict, env):
|
||||
self.employee_id = employee_id
|
||||
self.dict = dict
|
||||
self.env = env
|
||||
# Customization to allow changing the behavior of the discrete browsable objects.
|
||||
# you can think of this as 'compiling' the query based on the configuration.
|
||||
sum_field = env['ir.config_parameter'].sudo().get_param('hr_payroll.payslip.sum_behavior', 'date_from')
|
||||
if sum_field == 'date' and 'date' not in env['hr.payslip']:
|
||||
# missing attribute, closest by definition
|
||||
sum_field = 'date_to'
|
||||
if not sum_field:
|
||||
sum_field = 'date_from'
|
||||
self._compile_browsable_query(sum_field)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return attr in self.dict and self.dict.__getitem__(attr) or 0.0
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.dict[key] or 0.0
|
||||
|
||||
def _compile_browsable_query(self, sum_field):
|
||||
pass
|
||||
|
||||
|
||||
class InputLine(BrowsableObject):
|
||||
"""a class that will be used into the python code, mainly for usability purposes"""
|
||||
def _compile_browsable_query(self, sum_field):
|
||||
self.__browsable_query = """
|
||||
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.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""".format(sum_field=sum_field)
|
||||
|
||||
def sum(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
self.env.cr.execute(self.__browsable_query, (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 _compile_browsable_query(self, sum_field):
|
||||
self.__browsable_query = """
|
||||
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.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""".format(sum_field=sum_field)
|
||||
|
||||
def _sum(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
self.env.cr.execute(self.__browsable_query, (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 _compile_browsable_query(self, sum_field):
|
||||
# Note that the core odoo has this as `hp.credit_note = False` but what if it is NULL?
|
||||
# reverse of the desired behavior.
|
||||
self.__browsable_query_rule = """
|
||||
SELECT sum(case when hp.credit_note is not True 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.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""".format(sum_field=sum_field)
|
||||
# Original (non-recursive)
|
||||
# self.__browsable_query_category = """
|
||||
# SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end)
|
||||
# FROM hr_payslip as hp, hr_payslip_line as pl, hr_salary_rule_category as rc
|
||||
# WHERE hp.employee_id = %s AND hp.state = 'done'
|
||||
# AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id
|
||||
# AND rc.id = pl.category_id AND rc.code = %s""".format(sum_field=sum_field)
|
||||
|
||||
# Hibou Recursive version
|
||||
self.__browsable_query_category = """
|
||||
WITH RECURSIVE
|
||||
category_by_code as (
|
||||
SELECT id
|
||||
FROM hr_salary_rule_category
|
||||
WHERE code = %s
|
||||
),
|
||||
category_ids as (
|
||||
SELECT COALESCE((SELECT id FROM category_by_code), -1) AS id
|
||||
UNION ALL
|
||||
SELECT rc.id
|
||||
FROM hr_salary_rule_category AS rc
|
||||
JOIN category_ids AS rcs ON rcs.id = rc.parent_id
|
||||
)
|
||||
|
||||
SELECT sum(case when hp.credit_note is not True 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.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id
|
||||
AND pl.category_id in (SELECT id from category_ids)""".format(sum_field=sum_field)
|
||||
|
||||
def sum(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
self.env.cr.execute(self.__browsable_query_rule, (self.employee_id, from_date, to_date, code))
|
||||
res = self.env.cr.fetchone()
|
||||
return res and res[0] or 0.0
|
||||
|
||||
def rule_parameter(self, code):
|
||||
return self.env['hr.rule.parameter']._get_parameter_from_code(code, self.dict.date_to)
|
||||
|
||||
def sum_category(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
|
||||
self.env['hr.payslip'].flush(['credit_note', 'employee_id', 'state', 'date_from', 'date_to'])
|
||||
self.env['hr.payslip.line'].flush(['total', 'slip_id', 'category_id'])
|
||||
self.env['hr.salary.rule.category'].flush(['code'])
|
||||
|
||||
# standard version
|
||||
# self.env.cr.execute(self.__browsable_query_category, (self.employee_id, from_date, to_date, code))
|
||||
# recursive category version
|
||||
self.env.cr.execute(self.__browsable_query_category, (code, self.employee_id, from_date, to_date))
|
||||
res = self.env.cr.fetchone()
|
||||
return res and res[0] or 0.0
|
||||
|
||||
@property
|
||||
def paid_amount(self):
|
||||
return self.dict._get_paid_amount()
|
||||
|
||||
|
||||
# Patch over Core
|
||||
browsable_object.BrowsableObject.__init__ = BrowsableObject.__init__
|
||||
browsable_object.BrowsableObject._compile_browsable_query = BrowsableObject._compile_browsable_query
|
||||
browsable_object.InputLine._compile_browsable_query = InputLine._compile_browsable_query
|
||||
browsable_object.InputLine.sum = InputLine.sum
|
||||
browsable_object.WorkedDays._compile_browsable_query = WorkedDays._compile_browsable_query
|
||||
browsable_object.WorkedDays.sum = WorkedDays.sum
|
||||
browsable_object.Payslips._compile_browsable_query = Payslips._compile_browsable_query
|
||||
browsable_object.Payslips.sum = Payslips.sum
|
||||
browsable_object.Payslips.sum_category = Payslips.sum_category
|
||||
14
hr_payroll_hibou/models/hr_payslip.py
Normal file
14
hr_payroll_hibou/models/hr_payslip.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_inherit = 'hr.payslip'
|
||||
|
||||
def get_year(self):
|
||||
"""
|
||||
# Helper method to get the year (normalized between Odoo Versions)
|
||||
:return: int year of payslip
|
||||
"""
|
||||
return self.date_to.year
|
||||
19
hr_payroll_hibou/models/hr_salary_rule.py
Normal file
19
hr_payroll_hibou/models/hr_salary_rule.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayrollStructure(models.Model):
|
||||
_inherit = 'hr.payroll.structure'
|
||||
|
||||
schedule_pay = fields.Selection(selection_add=[
|
||||
('semi-monthly', 'Semi-monthly'),
|
||||
], ondelete={'semi-monthly': 'set null'})
|
||||
|
||||
|
||||
class HrPayrollStructureType(models.Model):
|
||||
_inherit = 'hr.payroll.structure.type'
|
||||
|
||||
default_schedule_pay = fields.Selection(selection_add=[
|
||||
('semi-monthly', 'Semi-monthly'),
|
||||
])
|
||||
27
hr_payroll_hibou/models/res_config_settings.py
Normal file
27
hr_payroll_hibou/models/res_config_settings.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
# TODO We need MORE here...
|
||||
module_l10n_us_hr_payroll = fields.Boolean(string='USA Payroll')
|
||||
|
||||
payslip_sum_type = fields.Selection([
|
||||
('date_from', 'Date From'),
|
||||
('date_to', 'Date To'),
|
||||
('date', 'Accounting Date'),
|
||||
], 'Payslip Sum Behavior', help="Behavior for what payslips are considered "
|
||||
"during rule execution. Stock Odoo behavior "
|
||||
"would not consider a payslip starting on 2019-12-30 "
|
||||
"ending on 2020-01-07 when summing a 2020 payslip category.\n\n"
|
||||
"Accounting Date requires Payroll Accounting and will "
|
||||
"fall back to date_to as the 'closest behavior'.",
|
||||
config_parameter='hr_payroll.payslip.sum_behavior')
|
||||
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior',
|
||||
self.payslip_sum_type or 'date_from')
|
||||
5
hr_payroll_hibou/tests/__init__.py
Normal file
5
hr_payroll_hibou/tests/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
|
||||
from . import test_special
|
||||
153
hr_payroll_hibou/tests/common.py
Executable file
153
hr_payroll_hibou/tests/common.py
Executable file
@@ -0,0 +1,153 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from logging import getLogger
|
||||
from sys import float_info as sys_float_info
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo.tests import common
|
||||
from odoo.tools.float_utils import float_round as odoo_float_round
|
||||
|
||||
|
||||
def process_payslip(payslip):
|
||||
try:
|
||||
payslip.action_payslip_done()
|
||||
except AttributeError:
|
||||
# v9
|
||||
payslip.process_sheet()
|
||||
|
||||
|
||||
class TestPayslip(common.TransactionCase):
|
||||
debug = False
|
||||
_logger = getLogger(__name__)
|
||||
|
||||
def setUp(self):
|
||||
super(TestPayslip, self).setUp()
|
||||
self.contract_model = self.env['hr.contract']
|
||||
self.env.user.tz = 'PST8PDT'
|
||||
self.env.ref('resource.resource_calendar_std').tz = 'PST8PDT'
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to')
|
||||
self.structure_type = self.env['hr.payroll.structure.type'].create({
|
||||
'name': 'Test Structure Type',
|
||||
})
|
||||
self.structure = self.env['hr.payroll.structure'].create({
|
||||
'name': 'Test Structure',
|
||||
'type_id': self.structure_type.id,
|
||||
})
|
||||
self._log('structue_type %s and structure %s' % (self.structure_type, self.structure))
|
||||
self.structure_type.default_struct_id = self.structure
|
||||
self.resource_calendar = self.ref('resource.resource_calendar_std')
|
||||
|
||||
float_info = sys_float_info
|
||||
|
||||
def float_round(self, value, digits):
|
||||
return odoo_float_round(value, digits)
|
||||
|
||||
_payroll_digits = -1
|
||||
|
||||
@property
|
||||
def payroll_digits(self):
|
||||
if self._payroll_digits == -1:
|
||||
self._payroll_digits = self.env['decimal.precision'].precision_get('Payroll')
|
||||
return self._payroll_digits
|
||||
|
||||
def _log(self, message):
|
||||
if self.debug:
|
||||
self._logger.warning(message)
|
||||
|
||||
def _createEmployee(self):
|
||||
return self.env['hr.employee'].create({
|
||||
'birthday': '1985-03-14',
|
||||
'country_id': self.ref('base.us'),
|
||||
'department_id': self.ref('hr.dep_rd'),
|
||||
'gender': 'male',
|
||||
'name': 'Jared'
|
||||
})
|
||||
|
||||
def _get_contract_defaults(self, contract_values):
|
||||
if not contract_values.get('state'):
|
||||
contract_values['state'] = 'open' # Running
|
||||
if not contract_values.get('structure_type_id'):
|
||||
contract_values['structure_type_id'] = self.structure_type.id
|
||||
if not contract_values.get('date_start'):
|
||||
contract_values['date_start'] = '2016-01-01'
|
||||
if not contract_values.get('date_end'):
|
||||
contract_values['date_end'] = '2030-12-31'
|
||||
if not contract_values.get('resource_calendar_id'):
|
||||
contract_values['resource_calendar_id'] = self.resource_calendar
|
||||
|
||||
# Compatibility with earlier Odoo versions
|
||||
if not contract_values.get('journal_id') and hasattr(self.contract_model, 'journal_id'):
|
||||
try:
|
||||
contract_values['journal_id'] = self.env['account.journal'].search([('type', '=', 'general')], limit=1).id
|
||||
except KeyError:
|
||||
# Accounting not installed
|
||||
pass
|
||||
|
||||
def _createContract(self, employee, **kwargs):
|
||||
if not 'schedule_pay' in kwargs:
|
||||
kwargs['schedule_pay'] = 'monthly'
|
||||
schedule_pay = kwargs['schedule_pay']
|
||||
contract_values = {
|
||||
'name': 'Test Contract',
|
||||
'employee_id': employee.id,
|
||||
}
|
||||
|
||||
for key, val in kwargs.items():
|
||||
# Assume any Odoo object is in a Many2one
|
||||
if hasattr(val, 'id'):
|
||||
val = val.id
|
||||
found = False
|
||||
if hasattr(self.contract_model, key):
|
||||
contract_values[key] = val
|
||||
found = True
|
||||
if not found:
|
||||
self._logger.warn('cannot locate attribute names "%s" on hr.contract().' % (key, ))
|
||||
|
||||
self._get_contract_defaults(contract_values)
|
||||
contract = self.contract_model.create(contract_values)
|
||||
|
||||
# Compatibility with Odoo 14
|
||||
contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay
|
||||
return contract
|
||||
|
||||
def _createPayslip(self, employee, date_from, date_to, skip_compute=False):
|
||||
slip = self.env['hr.payslip'].create({
|
||||
'name': 'Test %s From: %s To: %s' % (employee.name, date_from, date_to),
|
||||
'employee_id': employee.id,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to
|
||||
})
|
||||
# Included in hr.payslip.action_refresh_from_work_entries() as ov 14.0 EE
|
||||
# slip._onchange_employee()
|
||||
# as is the 'compute' that is almost always called immediaately after
|
||||
if not skip_compute:
|
||||
slip.action_refresh_from_work_entries()
|
||||
return slip
|
||||
|
||||
def _getCategories(self, payslip):
|
||||
categories = defaultdict(float)
|
||||
for line in payslip.line_ids:
|
||||
self._log(' line code: ' + str(line.code) +
|
||||
' category code: ' + line.category_id.code +
|
||||
' total: ' + str(line.total) +
|
||||
' rate: ' + str(line.rate) +
|
||||
' amount: ' + str(line.amount))
|
||||
category_id = line.category_id
|
||||
category_code = line.category_id.code
|
||||
while category_code:
|
||||
categories[category_code] += line.total
|
||||
category_id = category_id.parent_id
|
||||
category_code = category_id.code
|
||||
return categories
|
||||
|
||||
def _getRules(self, payslip):
|
||||
rules = defaultdict(float)
|
||||
for line in payslip.line_ids:
|
||||
rules[line.code] += line.total
|
||||
return rules
|
||||
|
||||
def assertPayrollEqual(self, first, second):
|
||||
self.assertAlmostEqual(first, second, self.payroll_digits)
|
||||
|
||||
def assertPayrollAlmostEqual(self, first, second):
|
||||
self.assertAlmostEqual(first, second, self.payroll_digits-1)
|
||||
127
hr_payroll_hibou/tests/test_special.py
Normal file
127
hr_payroll_hibou/tests/test_special.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from .common import TestPayslip, process_payslip
|
||||
|
||||
|
||||
class TestSpecial(TestPayslip):
|
||||
|
||||
def test_get_year(self):
|
||||
salary = 80000.0
|
||||
employee = self._createEmployee()
|
||||
# so the schedule_pay is now on the Structure...
|
||||
contract = self._createContract(employee, wage=salary)
|
||||
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-14')
|
||||
self.assertEqual(payslip.get_year(), 2020)
|
||||
|
||||
def test_semi_monthly(self):
|
||||
salary = 80000.0
|
||||
employee = self._createEmployee()
|
||||
# so the schedule_pay is now on the Structure...
|
||||
contract = self._createContract(employee, wage=salary, schedule_pay='semi-monthly')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-14')
|
||||
|
||||
def test_payslip_sum_behavior(self):
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date')
|
||||
rule_category_comp = self.env.ref('hr_payroll.COMP')
|
||||
test_rule_category = self.env['hr.salary.rule.category'].create({
|
||||
'name': 'Test Sum Behavior',
|
||||
'code': 'test_sum_behavior',
|
||||
'parent_id': rule_category_comp.id,
|
||||
})
|
||||
test_rule = self.env['hr.salary.rule'].create({
|
||||
'sequence': 450,
|
||||
'struct_id': self.structure.id,
|
||||
'category_id': test_rule_category.id,
|
||||
'name': 'Test Sum Behavior',
|
||||
'code': 'test_sum_behavior',
|
||||
'condition_select': 'python',
|
||||
'condition_python': 'result = 1',
|
||||
'amount_select': 'code',
|
||||
'amount_python_compute': '''
|
||||
ytd_category = payslip.sum_category('test_sum_behavior', '2020-01-01', '2021-01-01')
|
||||
ytd_rule = payslip.sum('test_sum_behavior', '2020-01-01', '2021-01-01')
|
||||
result = 0.0
|
||||
if ytd_category != ytd_rule:
|
||||
# error
|
||||
result = -1.0
|
||||
elif ytd_rule == 0.0:
|
||||
# first payslip in period
|
||||
result = 1.0
|
||||
'''
|
||||
})
|
||||
salary = 80000.0
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, wage=salary, schedule_pay='bi-weekly')
|
||||
payslip = self._createPayslip(employee, '2019-12-30', '2020-01-12')
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertEqual(cats['BASIC'], salary)
|
||||
self.assertEqual(cats['test_sum_behavior'], 1.0)
|
||||
process_payslip(payslip)
|
||||
|
||||
# Basic date_from behavior.
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_from')
|
||||
# The the date_from on the last payslip will not be found
|
||||
payslip = self._createPayslip(employee, '2020-01-13', '2020-01-27')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertEqual(cats['test_sum_behavior'], 1.0)
|
||||
|
||||
# date_to behavior.
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to')
|
||||
# The date_to on the last payslip is found
|
||||
payslip = self._createPayslip(employee, '2020-01-13', '2020-01-27')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertEqual(cats['test_sum_behavior'], 0.0)
|
||||
|
||||
def test_recursive_salary_rule_category(self):
|
||||
self.debug = True
|
||||
# In this scenario, you are in rule code that will check for the category
|
||||
# and a subcategory will also
|
||||
alw_category = self.env.ref('hr_payroll.ALW')
|
||||
ded_category = self.env.ref('hr_payroll.DED')
|
||||
test_category = self.env['hr.salary.rule.category'].create({
|
||||
'name': 'Special ALW',
|
||||
'code': 'ALW_SPECIAL_RECURSIVE',
|
||||
'parent_id': alw_category.id,
|
||||
})
|
||||
test_special_alw = self.env['hr.salary.rule'].create({
|
||||
'name': 'Flat amount 200',
|
||||
'code': 'ALW_SPECIAL_RECURSIVE',
|
||||
'category_id': test_category.id,
|
||||
'condition_select': 'none',
|
||||
'amount_select': 'fix',
|
||||
'amount_fix': 200.0,
|
||||
'struct_id': self.structure.id,
|
||||
})
|
||||
test_recursion = self.env['hr.salary.rule'].create({
|
||||
'name': 'Actual Test Behavior',
|
||||
'code': 'RECURSION_TEST',
|
||||
'category_id': ded_category.id,
|
||||
'condition_select': 'none',
|
||||
'amount_select': 'code',
|
||||
'amount_python_compute': """
|
||||
# this rule will always be the total of the ALW category and YTD ALW category
|
||||
result = categories.ALW
|
||||
# Note, this tests the hr.payslip.get_year() to return an integer rep of year
|
||||
year = payslip.dict.get_year()
|
||||
result += payslip.sum_category('ALW', str(year) + '-01-01', str(year+1) + '-01-01')
|
||||
""",
|
||||
'sequence': 101,
|
||||
'struct_id': self.structure.id,
|
||||
})
|
||||
|
||||
salary = 80000.0
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, wage=salary, schedule_pay='bi-weekly')
|
||||
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-14')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
rules = self._getRules(payslip)
|
||||
self.assertEqual(rules['RECURSION_TEST'], 200.0)
|
||||
process_payslip(payslip)
|
||||
|
||||
payslip = self._createPayslip(employee, '2020-01-15', '2020-01-27')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
rules = self._getRules(payslip)
|
||||
# two hundred is in the YTD ALW
|
||||
self.assertEqual(rules['RECURSION_TEST'], 200.0 + 200.0)
|
||||
32
hr_payroll_hibou/views/res_config_settings_views.xml
Normal file
32
hr_payroll_hibou/views/res_config_settings_views.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form_hibou" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.hr.payroll.hibou</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="50"/>
|
||||
<field name="inherit_id" ref="hr_payroll.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@data-key='hr_payroll']" position="inside">
|
||||
<field name="module_l10n_us_hr_payroll" invisible="1"/>
|
||||
<!-- TODO payment, attendance, timesheet, ovetrtime... -->
|
||||
<h2>Hibou Payroll</h2>
|
||||
<div class="row mt16 o_settings_container" id="hr_payroll_hibou">
|
||||
<div class="col-lg-6 col-12 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="payslip_sum_type" string="Payslip Sum Behavior"/>
|
||||
<div class="text-muted">
|
||||
Customize the behavior of what payslips are eligible when summing over date ranges in rules.
|
||||
Generally, "Date To" or "Accounting Date" would be preferred in the United States and anywhere
|
||||
else where the ending date on the payslip is used to calculate wage bases.
|
||||
</div>
|
||||
<field name="payslip_sum_type"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user