IMP l10n_us_hr_payroll Allow configurable changes to payslip summing behavior.

In stock Odoo, summing anything in payroll rules (but most importantly rule amounts and category amounts by code), the considered payslips are referenced from their `date_from` field.  However in the USA, it is in fact the `date_to` that is more important (or accounting date). A Payslip made for 2019-12-20 to 2020-01-04 should in fact be considered a '2020' payslip, and thus the summation on other '2020' payslips must find it by considering payslips `date_to`.
This commit is contained in:
Jared Kipe
2020-04-18 15:57:42 -07:00
parent 8f2f9297ec
commit ea8d98433d
9 changed files with 194 additions and 32 deletions

View File

@@ -1,3 +1,12 @@
# 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 models from . import models
def _post_install_hook(cr, registry):
"""
This method will set the default for the Payslip Sum Behavior
"""
cr.execute("SELECT id FROM ir_config_parameter WHERE key = 'hr_payroll.payslip.sum_behavior';")
existing = cr.fetchall()
if not existing:
cr.execute("INSERT INTO ir_config_parameter (key, value) VALUES ('hr_payroll.payslip.sum_behavior', 'date');")

View File

@@ -58,8 +58,13 @@ USA Payroll Rules.
'data/state/wa_washington.xml', 'data/state/wa_washington.xml',
'data/final.xml', 'data/final.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',
], ],
'installable': True, 'installable': True,
'demo': [
],
'auto_install': False,
'post_init_hook': '_post_install_hook',
'license': 'OPL-1', 'license': 'OPL-1',
} }

View File

@@ -3,4 +3,5 @@
from . import hr_contract from . import hr_contract
from . import hr_payslip from . import hr_payslip
from . import hr_salary_rule from . import hr_salary_rule
from . import res_config_settings
from . import us_payroll_config from . import us_payroll_config

View File

@@ -116,34 +116,52 @@ class HRPayslip(models.Model):
self.employee_id = employee_id self.employee_id = employee_id
self.dict = dict self.dict = dict
self.env = env 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): def __getattr__(self, attr):
return attr in self.dict and self.dict.__getitem__(attr) or 0.0 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
def _compile_browsable_query(self, sum_field):
pass
class InputLine(BrowsableObject): class InputLine(BrowsableObject):
"""a class that will be used into the python code, mainly for usability purposes""" """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: def _compile_browsable_query(self, sum_field):
to_date = fields.Date.today() self.__browsable_query = """
self.env.cr.execute("""
SELECT sum(amount) as sum SELECT sum(amount) as sum
FROM hr_payslip as hp, hr_payslip_input as pi FROM hr_payslip as hp, hr_payslip_input as pi
WHERE hp.employee_id = %s AND hp.state = 'done' 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""", 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)
(self.employee_id, from_date, to_date, code))
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 return self.env.cr.fetchone()[0] or 0.0
class WorkedDays(BrowsableObject): class WorkedDays(BrowsableObject):
"""a class that will be used into the python code, mainly for usability purposes""" """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: def _compile_browsable_query(self, sum_field):
to_date = fields.Date.today() self.__browsable_query = """
self.env.cr.execute("""
SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours 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 FROM hr_payslip as hp, hr_payslip_worked_days as pi
WHERE hp.employee_id = %s AND hp.state = 'done' 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""", 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)
(self.employee_id, from_date, to_date, code))
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() return self.env.cr.fetchone()
def sum(self, code, from_date, to_date=None): def sum(self, code, from_date, to_date=None):
@@ -157,28 +175,37 @@ class HRPayslip(models.Model):
class Payslips(BrowsableObject): class Payslips(BrowsableObject):
"""a class that will be used into the python code, mainly for usability purposes""" """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)
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)
def sum(self, code, from_date, to_date=None): def sum(self, code, from_date, to_date=None):
if to_date is None: if to_date is None:
to_date = fields.Date.today() to_date = fields.Date.today()
self.env.cr.execute("""SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end) self.env.cr.execute(self.__browsable_query_rule, (self.employee_id, from_date, to_date, code))
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() res = self.env.cr.fetchone()
return res and res[0] or 0.0 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): def sum_category(self, code, from_date, to_date=None):
# Hibou Backport # Hibou Backport
if to_date is None: if to_date is None:
to_date = fields.Date.today() to_date = fields.Date.today()
self.env.cr.execute("""SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end) self.env.cr.execute(self.__browsable_query_category, (self.employee_id, from_date, to_date, code))
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.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id
AND rc.id = pl.category_id AND rc.code = %s""",
(self.employee_id, from_date, to_date, code))
res = self.env.cr.fetchone() res = self.env.cr.fetchone()
return res and res[0] or 0.0 return res and res[0] or 0.0

View File

@@ -0,0 +1,24 @@
# 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')

View File

@@ -1,6 +1,9 @@
# 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 common from . import common
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

View File

@@ -22,6 +22,10 @@ class TestUsPayslip(common.TransactionCase):
debug = False debug = False
_logger = getLogger(__name__) _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 float_info = sys_float_info
def float_round(self, value, digits): def float_round(self, value, digits):
@@ -154,15 +158,6 @@ class TestUsPayslip(common.TransactionCase):
def assertPayrollAlmostEqual(self, first, second): def assertPayrollAlmostEqual(self, first, second):
self.assertAlmostEqual(first, second, self.payroll_digits-1) self.assertAlmostEqual(first, second, self.payroll_digits-1)
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')
payslip.compute_sheet()
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:

View File

@@ -0,0 +1,66 @@
from .common import TestUsPayslip, process_payslip
class TestSpecial(TestUsPayslip):
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')
payslip.compute_sheet()
def test_payslip_sum_behavior(self):
us_structure = self.env.ref('l10n_us_hr_payroll.structure_type_employee')
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,
'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
'''
})
us_structure.write({'rule_ids': [(4, test_rule.id, 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)

View File

@@ -0,0 +1,32 @@
<?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>