mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
This release fixes multiple logical problems with Odoo's payroll, including the difference in behavior on payslips with 'recursive' category calculations needed for many.
242 lines
9.2 KiB
Python
Executable File
242 lines
9.2 KiB
Python
Executable File
# 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 datetime import timedelta
|
|
|
|
from odoo.tests import common
|
|
from odoo.tools.float_utils import float_round as odoo_float_round
|
|
from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
|
|
|
|
|
|
def process_payslip(payslip):
|
|
try:
|
|
payslip.action_payslip_done()
|
|
except AttributeError:
|
|
# v9
|
|
payslip.process_sheet()
|
|
|
|
|
|
class TestUsPayslip(common.TransactionCase):
|
|
debug = False
|
|
_logger = getLogger(__name__)
|
|
|
|
def setUp(self):
|
|
super(TestUsPayslip, self).setUp()
|
|
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to')
|
|
|
|
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.warn(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 _createContract(self, employee, **kwargs):
|
|
if not 'schedule_pay' in kwargs:
|
|
kwargs['schedule_pay'] = 'monthly'
|
|
schedule_pay = kwargs['schedule_pay']
|
|
config_model = self.env['hr.contract.us_payroll_config']
|
|
contract_model = self.env['hr.contract']
|
|
config_values = {
|
|
'name': 'Test Config Values',
|
|
'employee_id': employee.id,
|
|
}
|
|
contract_values = {
|
|
'name': 'Test Contract',
|
|
'employee_id': employee.id,
|
|
}
|
|
|
|
# Backwards compatability with 'futa_type'
|
|
if 'futa_type' in kwargs:
|
|
kwargs['fed_940_type'] = kwargs['futa_type']
|
|
|
|
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(contract_model, key):
|
|
contract_values[key] = val
|
|
found = True
|
|
if hasattr(config_model, key):
|
|
config_values[key] = val
|
|
found = True
|
|
if not found:
|
|
self._logger.warn('cannot locate attribute names "%s" on contract or payroll config' % (key, ))
|
|
|
|
# US Payroll Config Defaults Should be set on the Model
|
|
config = config_model.create(config_values)
|
|
contract_values['us_payroll_config_id'] = config.id
|
|
|
|
# Some Basic Defaults
|
|
if not contract_values.get('state'):
|
|
contract_values['state'] = 'open' # Running
|
|
if not contract_values.get('struct_id'):
|
|
contract_values['struct_id'] = self.ref('l10n_us_hr_payroll.structure_type_employee')
|
|
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.ref('resource.resource_calendar_std')
|
|
|
|
# Compatibility with earlier Odoo versions
|
|
if not contract_values.get('journal_id') and hasattr(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
|
|
|
|
contract = contract_model.create(contract_values)
|
|
|
|
return contract
|
|
|
|
def _createPayslip(self, employee, date_from, date_to):
|
|
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
|
|
})
|
|
if hasattr(slip, '_onchange_employee'):
|
|
slip._onchange_employee()
|
|
if hasattr(slip, 'onchange_employee'):
|
|
# Odoo 12
|
|
slip.onchange_employee()
|
|
if self.debug:
|
|
self._logger.warn(slip.read())
|
|
self._logger.warn(slip.contract_id.read())
|
|
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)
|
|
|
|
def get_us_state(self, code, cache={}):
|
|
country_key = 'US_COUNTRY'
|
|
if code in cache:
|
|
return cache[code]
|
|
if country_key not in cache:
|
|
cache[country_key] = self.env.ref('base.us')
|
|
us_country = cache[country_key]
|
|
us_state = self.env['res.country.state'].search([
|
|
('country_id', '=', us_country.id),
|
|
('code', '=', code),
|
|
], limit=1)
|
|
cache[code] = us_state
|
|
return us_state
|
|
|
|
def _test_suta(self, category, state_code, rate, date, wage_base=None, relaxed=False, **extra_contract):
|
|
if relaxed:
|
|
_assert = self.assertPayrollAlmostEqual
|
|
else:
|
|
_assert = self.assertPayrollEqual
|
|
if wage_base:
|
|
# Slightly larger than 1/2 the wage_base
|
|
wage = round(wage_base / 2.0) + 100.0
|
|
self.assertTrue((2 * wage) > wage_base, 'Granularity of wage_base too low.')
|
|
else:
|
|
wage = 1000.0
|
|
|
|
employee = self._createEmployee()
|
|
contract = self._createContract(employee,
|
|
wage=wage,
|
|
state_id=self.get_us_state(state_code),
|
|
**extra_contract)
|
|
|
|
rate = -rate / 100.0 # Assumed passed as percent positive
|
|
|
|
# Tests
|
|
payslip = self._createPayslip(employee, date, date + timedelta(days=30))
|
|
|
|
# Test exemptions
|
|
contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_EXEMPT
|
|
payslip.compute_sheet()
|
|
cats = self._getCategories(payslip)
|
|
_assert(cats.get(category, 0.0), 0.0)
|
|
|
|
contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_BASIC
|
|
payslip.compute_sheet()
|
|
cats = self._getCategories(payslip)
|
|
_assert(cats.get(category, 0.0), 0.0)
|
|
|
|
# Test Normal
|
|
contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_NORMAL
|
|
payslip.compute_sheet()
|
|
cats = self._getCategories(payslip)
|
|
_assert(cats.get(category, 0.0), wage * rate)
|
|
process_payslip(payslip)
|
|
|
|
# Second Payslip
|
|
payslip = self._createPayslip(employee, date + timedelta(days=31), date + timedelta(days=60))
|
|
payslip.compute_sheet()
|
|
cats = self._getCategories(payslip)
|
|
|
|
if wage_base:
|
|
remaining_unemp_wages = wage_base - wage
|
|
self.assertTrue((remaining_unemp_wages * rate) <= 0.01) # less than 0.01 because rate is negative
|
|
_assert(cats.get(category, 0.0), remaining_unemp_wages * rate)
|
|
|
|
# As if they were paid once already, so the first "two payslips" would remove all of the tax obligation
|
|
# 1 wage - Payslip (confirmed)
|
|
# 1 wage - external_wages
|
|
# 1 wage - current Payslip
|
|
contract.external_wages = wage
|
|
payslip.compute_sheet()
|
|
cats = self._getCategories(payslip)
|
|
_assert(cats.get(category, 0.0), 0.0)
|
|
else:
|
|
_assert(cats.get(category, 0.0), wage * rate)
|
|
|
|
def _test_er_suta(self, state_code, rate, date, wage_base=None, relaxed=False, **extra_contract):
|
|
self._test_suta('ER_US_SUTA', state_code, rate, date, wage_base=wage_base, relaxed=relaxed, **extra_contract)
|
|
|
|
def _test_ee_suta(self, state_code, rate, date, wage_base=None, relaxed=False, **extra_contract):
|
|
self._test_suta('EE_US_SUTA', state_code, rate, date, wage_base=wage_base, relaxed=relaxed, **extra_contract)
|