mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[MIG] l10n_us_hr_payroll: to Odoo 14.0
Require `hr_payroll_hibou`, remove a lot of 'fixes' and stuff that is useful for 'all' payroll. Failing tests: Deleware 2020 is low by ~0.1% in SIT only. Pennsylvania 2019, 2020 is low by ~0.01% in SIT only. I did not try to correct, only verify that the data and calculation is the same between versions. (which it is) Washington 2019, 2020 hours was off (183.99972222222223 != 184) Fixed by turning it into assertAlmostEqual
This commit is contained in:
@@ -3,12 +3,10 @@
|
|||||||
{
|
{
|
||||||
'name': 'United States of America - Payroll',
|
'name': 'United States of America - Payroll',
|
||||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||||
'version': '13.0.2020.0.0',
|
'version': '14.0.2020.0.0',
|
||||||
'category': 'Payroll Localization',
|
'category': 'Payroll Localization',
|
||||||
'depends': [
|
'depends': [
|
||||||
'hr_payroll',
|
'hr_payroll_hibou',
|
||||||
'hr_contract_reports',
|
|
||||||
'hibou_professional',
|
|
||||||
],
|
],
|
||||||
'description': """
|
'description': """
|
||||||
United States of America - Payroll Rules.
|
United States of America - Payroll Rules.
|
||||||
@@ -74,7 +72,6 @@ United States of America - Payroll Rules.
|
|||||||
'data/state/wv_west_virginia.xml',
|
'data/state/wv_west_virginia.xml',
|
||||||
'data/state/wy_wyoming.xml',
|
'data/state/wy_wyoming.xml',
|
||||||
'views/hr_contract_views.xml',
|
'views/hr_contract_views.xml',
|
||||||
'views/res_config_settings_views.xml',
|
|
||||||
'views/us_payroll_config_views.xml',
|
'views/us_payroll_config_views.xml',
|
||||||
],
|
],
|
||||||
'demo': [
|
'demo': [
|
||||||
|
|||||||
@@ -7,14 +7,19 @@
|
|||||||
<field name="country_id" ref="base.us"/>
|
<field name="country_id" ref="base.us"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<record id="hr_payroll_structure" model="hr.payroll.structure">
|
<record id="hr_payroll_structure" model="hr.payroll.structure">
|
||||||
<field name="name">USA Employee Standard</field>
|
<field name="name">USA Employee Standard</field>
|
||||||
<field name="country_id" ref="base.us"/>
|
<field name="country_id" ref="base.us"/>
|
||||||
<field name="type_id" ref="l10n_us_hr_payroll.structure_type_employee"/>
|
<field name="type_id" ref="l10n_us_hr_payroll.structure_type_employee"/>
|
||||||
<field name="regular_pay" eval="True"/>
|
|
||||||
<field name="unpaid_work_entry_type_ids" eval="[
|
<field name="unpaid_work_entry_type_ids" eval="[
|
||||||
(4, ref('hr_payroll.work_entry_type_unpaid_leave')),
|
(4, ref('hr_work_entry_contract.work_entry_type_unpaid_leave')),
|
||||||
]"/>
|
]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="l10n_us_hr_payroll.structure_type_employee" model="hr.payroll.structure.type">
|
||||||
|
<field name="default_struct_id" ref="hr_payroll_structure"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- State Unemployment -->
|
<!-- State Unemployment -->
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import browsable_object
|
|
||||||
from . import hr_contract
|
from . import hr_contract
|
||||||
from . import hr_payslip
|
from . import hr_payslip
|
||||||
from . import res_config_settings
|
|
||||||
from . import update
|
from . import update
|
||||||
from . import us_payroll_config
|
from . import us_payroll_config
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
# 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 _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
|
|
||||||
@@ -6,16 +6,6 @@ from .us_payroll_config import FUTA_TYPE_NORMAL, \
|
|||||||
FUTA_TYPE_EXEMPT
|
FUTA_TYPE_EXEMPT
|
||||||
|
|
||||||
|
|
||||||
class HrPayrollStructureType(models.Model):
|
|
||||||
_inherit = 'hr.payroll.structure.type'
|
|
||||||
default_schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')])
|
|
||||||
|
|
||||||
|
|
||||||
class HrPayrollStructure(models.Model):
|
|
||||||
_inherit = 'hr.payroll.structure'
|
|
||||||
schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')])
|
|
||||||
|
|
||||||
|
|
||||||
class USHRContract(models.Model):
|
class USHRContract(models.Model):
|
||||||
_inherit = 'hr.contract'
|
_inherit = 'hr.contract'
|
||||||
|
|
||||||
|
|||||||
@@ -124,9 +124,5 @@ class HRPayslip(models.Model):
|
|||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_year(self):
|
|
||||||
# Helper method to get the year (normalized between Odoo Versions)
|
|
||||||
return self.date_to.year
|
|
||||||
|
|
||||||
def get_pay_periods_in_year(self):
|
def get_pay_periods_in_year(self):
|
||||||
return self.PAY_PERIODS_IN_YEAR.get(self.contract_id.schedule_pay, 0)
|
return self.PAY_PERIODS_IN_YEAR.get(self.contract_id.schedule_pay, 0)
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# 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'
|
|
||||||
|
|
||||||
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')
|
|
||||||
@@ -7,6 +7,7 @@ from . import test_special
|
|||||||
from . import test_us_payslip_2019
|
from . import test_us_payslip_2019
|
||||||
from . import test_us_payslip_2020
|
from . import test_us_payslip_2020
|
||||||
|
|
||||||
|
|
||||||
from . import test_us_ak_alaska_payslip_2019
|
from . import test_us_ak_alaska_payslip_2019
|
||||||
from . import test_us_ak_alaska_payslip_2020
|
from . import test_us_ak_alaska_payslip_2020
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,26 @@
|
|||||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
# 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 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
|
from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
|
||||||
|
|
||||||
|
from odoo.addons.hr_payroll_hibou.tests import common
|
||||||
def process_payslip(payslip):
|
|
||||||
try:
|
|
||||||
payslip.action_payslip_done()
|
|
||||||
except AttributeError:
|
|
||||||
# v9
|
|
||||||
payslip.process_sheet()
|
|
||||||
|
|
||||||
|
|
||||||
class TestUsPayslip(common.TransactionCase):
|
process_payslip = common.process_payslip
|
||||||
debug = False
|
|
||||||
_logger = getLogger(__name__)
|
|
||||||
|
class TestUsPayslip(common.TestPayslip):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestUsPayslip, self).setUp()
|
super(TestUsPayslip, self).setUp()
|
||||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to')
|
self.structure_type = self.env.ref('l10n_us_hr_payroll.structure_type_employee')
|
||||||
self.structure_type_id = self.ref('l10n_us_hr_payroll.structure_type_employee')
|
self.structure = self.env.ref('l10n_us_hr_payroll.hr_payroll_structure')
|
||||||
self.resource_calendar_id = self.ref('resource.resource_calendar_std')
|
# self.structure_type.default_struct_id = self.structure
|
||||||
|
self._log('US structue_type %s and structure %s' % (self.structure_type, self.structure))
|
||||||
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):
|
def _createContract(self, employee, **kwargs):
|
||||||
|
# Override
|
||||||
if not 'schedule_pay' in kwargs:
|
if not 'schedule_pay' in kwargs:
|
||||||
kwargs['schedule_pay'] = 'monthly'
|
kwargs['schedule_pay'] = 'monthly'
|
||||||
schedule_pay = kwargs['schedule_pay']
|
schedule_pay = kwargs['schedule_pay']
|
||||||
@@ -90,71 +56,14 @@ class TestUsPayslip(common.TransactionCase):
|
|||||||
# US Payroll Config Defaults Should be set on the Model
|
# US Payroll Config Defaults Should be set on the Model
|
||||||
config = config_model.create(config_values)
|
config = config_model.create(config_values)
|
||||||
contract_values['us_payroll_config_id'] = config.id
|
contract_values['us_payroll_config_id'] = config.id
|
||||||
|
self._get_contract_defaults(contract_values)
|
||||||
# Some Basic Defaults
|
self._log('creating contract with finial values: %s' % (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_id
|
|
||||||
|
|
||||||
# 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)
|
contract = contract_model.create(contract_values)
|
||||||
|
|
||||||
# Compatibility with Odoo 13
|
# Compatibility with Odoo 13/14
|
||||||
contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay
|
contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay
|
||||||
return contract
|
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
|
|
||||||
})
|
|
||||||
slip._onchange_employee()
|
|
||||||
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={}):
|
def get_us_state(self, code, cache={}):
|
||||||
country_key = 'US_COUNTRY'
|
country_key = 'US_COUNTRY'
|
||||||
if code in cache:
|
if code in cache:
|
||||||
|
|||||||
@@ -2,118 +2,4 @@ from .common import TestUsPayslip, process_payslip
|
|||||||
|
|
||||||
|
|
||||||
class TestSpecial(TestUsPayslip):
|
class TestSpecial(TestUsPayslip):
|
||||||
def test_semi_monthly(self):
|
pass
|
||||||
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')
|
|
||||||
payslip.compute_sheet()
|
|
||||||
|
|
||||||
def test_payslip_sum_behavior(self):
|
|
||||||
us_structure = self.env.ref('l10n_us_hr_payroll.hr_payroll_structure')
|
|
||||||
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': us_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')
|
|
||||||
payslip.compute_sheet()
|
|
||||||
cats = self._getCategories(payslip)
|
|
||||||
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
|
|
||||||
us_structure = self.env.ref('l10n_us_hr_payroll.hr_payroll_structure')
|
|
||||||
# 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': us_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
|
|
||||||
year = payslip.dict.get_year()
|
|
||||||
result += payslip.sum_category('ALW', str(year) + '-01-01', str(year+1) + '-01-01')
|
|
||||||
""",
|
|
||||||
'sequence': 101,
|
|
||||||
'struct_id': us_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)
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ class TestUsPayslip2020(TestUsPayslip):
|
|||||||
###
|
###
|
||||||
|
|
||||||
def test_2020_taxes(self):
|
def test_2020_taxes(self):
|
||||||
self.debug = False
|
|
||||||
# salary is high so that second payslip runs over max
|
# salary is high so that second payslip runs over max
|
||||||
# social security salary
|
# social security salary
|
||||||
salary = 80000.0
|
salary = 80000.0
|
||||||
@@ -42,17 +41,16 @@ class TestUsPayslip2020(TestUsPayslip):
|
|||||||
|
|
||||||
self._log('2019 tax last slip')
|
self._log('2019 tax last slip')
|
||||||
payslip = self._createPayslip(employee, '2019-12-01', '2019-12-31')
|
payslip = self._createPayslip(employee, '2019-12-01', '2019-12-31')
|
||||||
payslip.compute_sheet()
|
self.assertEqual(payslip.contract_id, contract)
|
||||||
self._log(payslip.read())
|
self._log(payslip.read())
|
||||||
process_payslip(payslip)
|
process_payslip(payslip)
|
||||||
|
|
||||||
# Ensure amounts are there, they shouldn't be added in the next year...
|
# Ensure amounts are there, they shouldn't be added in the next year...
|
||||||
cats = self._getCategories(payslip)
|
cats = self._getCategories(payslip)
|
||||||
self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
|
self.assertTrue(cats['ER_US_940_FUTA'], ' Value should be well above whatever was available that year!')
|
||||||
|
|
||||||
self._log('2020 tax first payslip:')
|
self._log('2020 tax first payslip:')
|
||||||
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
|
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
|
||||||
payslip.compute_sheet()
|
|
||||||
|
|
||||||
cats = self._getCategories(payslip)
|
cats = self._getCategories(payslip)
|
||||||
rules = self._getRules(payslip)
|
rules = self._getRules(payslip)
|
||||||
@@ -74,8 +72,6 @@ class TestUsPayslip2020(TestUsPayslip):
|
|||||||
self._log('2020 tax second payslip:')
|
self._log('2020 tax second payslip:')
|
||||||
payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28')
|
payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28')
|
||||||
|
|
||||||
payslip.compute_sheet()
|
|
||||||
|
|
||||||
cats = self._getCategories(payslip)
|
cats = self._getCategories(payslip)
|
||||||
rules = self._getRules(payslip)
|
rules = self._getRules(payslip)
|
||||||
|
|
||||||
@@ -90,8 +86,6 @@ class TestUsPayslip2020(TestUsPayslip):
|
|||||||
self._log('2020 tax third payslip:')
|
self._log('2020 tax third payslip:')
|
||||||
payslip = self._createPayslip(employee, '2020-03-01', '2020-03-31')
|
payslip = self._createPayslip(employee, '2020-03-01', '2020-03-31')
|
||||||
|
|
||||||
payslip.compute_sheet()
|
|
||||||
|
|
||||||
cats = self._getCategories(payslip)
|
cats = self._getCategories(payslip)
|
||||||
rules = self._getRules(payslip)
|
rules = self._getRules(payslip)
|
||||||
|
|
||||||
@@ -104,8 +98,6 @@ class TestUsPayslip2020(TestUsPayslip):
|
|||||||
self._log('2020 tax fourth payslip:')
|
self._log('2020 tax fourth payslip:')
|
||||||
payslip = self._createPayslip(employee, '2020-04-01', '2020-04-30')
|
payslip = self._createPayslip(employee, '2020-04-01', '2020-04-30')
|
||||||
|
|
||||||
payslip.compute_sheet()
|
|
||||||
|
|
||||||
cats = self._getCategories(payslip)
|
cats = self._getCategories(payslip)
|
||||||
rules = self._getRules(payslip)
|
rules = self._getRules(payslip)
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class TestUsWAPayslip(TestUsPayslip):
|
|||||||
self._log('2019 Washington tax first payslip:')
|
self._log('2019 Washington tax first payslip:')
|
||||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||||
hours_in_period = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'WORK100').number_of_hours
|
hours_in_period = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'WORK100').number_of_hours
|
||||||
self.assertEqual(hours_in_period, 184) # only asserted to test algorithm
|
self.assertAlmostEqual(hours_in_period, 184) # only asserted to test algorithm
|
||||||
payslip.compute_sheet()
|
payslip.compute_sheet()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class TestUsWAPayslip(TestUsPayslip):
|
|||||||
self._log('2020 Washington tax first payslip:')
|
self._log('2020 Washington tax first payslip:')
|
||||||
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
|
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
|
||||||
hours_in_period = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'WORK100').number_of_hours
|
hours_in_period = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'WORK100').number_of_hours
|
||||||
self.assertEqual(hours_in_period, 184) # only asserted to test algorithm
|
self.assertAlmostEqual(hours_in_period, 184) # only asserted to test algorithm
|
||||||
payslip.compute_sheet()
|
payslip.compute_sheet()
|
||||||
|
|
||||||
rules = self._getRules(payslip)
|
rules = self._getRules(payslip)
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="res_config_settings_view_form_inherit" model="ir.ui.view">
|
|
||||||
<field name="name">res.config.settings.view.form.inherit</field>
|
|
||||||
<field name="model">res.config.settings</field>
|
|
||||||
<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">
|
|
||||||
<div class="row mt16 o_settings_container" id="hr_payroll_accountant">
|
|
||||||
<div class="col-lg-6 col-12 o_setting_box">
|
|
||||||
<div class="o_setting_right_pane">
|
|
||||||
<span class="o_form_label">Payslip Sum Behavior</span>
|
|
||||||
<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>
|
|
||||||
<div class="content-group">
|
|
||||||
<div class="row mt16" id="mail_alias_domain">
|
|
||||||
<label for="payslip_sum_type" class="col-lg-3 o_light_label"/>
|
|
||||||
<field name="payslip_sum_type"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
Reference in New Issue
Block a user