From ea8d98433dd3d063ac31c88160ed67f945a3d336 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sat, 18 Apr 2020 15:57:42 -0700 Subject: [PATCH] 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`. --- l10n_us_hr_payroll/__init__.py | 9 +++ l10n_us_hr_payroll/__manifest__.py | 5 ++ l10n_us_hr_payroll/models/__init__.py | 1 + l10n_us_hr_payroll/models/hr_payslip.py | 73 +++++++++++++------ .../models/res_config_settings.py | 24 ++++++ l10n_us_hr_payroll/tests/__init__.py | 3 + l10n_us_hr_payroll/tests/common.py | 13 +--- l10n_us_hr_payroll/tests/test_special.py | 66 +++++++++++++++++ .../views/res_config_settings_views.xml | 32 ++++++++ 9 files changed, 194 insertions(+), 32 deletions(-) create mode 100644 l10n_us_hr_payroll/models/res_config_settings.py create mode 100644 l10n_us_hr_payroll/tests/test_special.py create mode 100644 l10n_us_hr_payroll/views/res_config_settings_views.xml diff --git a/l10n_us_hr_payroll/__init__.py b/l10n_us_hr_payroll/__init__.py index 09434554..013f4e73 100755 --- a/l10n_us_hr_payroll/__init__.py +++ b/l10n_us_hr_payroll/__init__.py @@ -1,3 +1,12 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. 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');") diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 68ffea98..564d9fa9 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -58,8 +58,13 @@ USA Payroll Rules. 'data/state/wa_washington.xml', 'data/final.xml', 'views/hr_contract_views.xml', + 'views/res_config_settings_views.xml', 'views/us_payroll_config_views.xml', ], 'installable': True, + 'demo': [ + ], + 'auto_install': False, + 'post_init_hook': '_post_install_hook', 'license': 'OPL-1', } diff --git a/l10n_us_hr_payroll/models/__init__.py b/l10n_us_hr_payroll/models/__init__.py index c208ca19..97bab130 100644 --- a/l10n_us_hr_payroll/models/__init__.py +++ b/l10n_us_hr_payroll/models/__init__.py @@ -3,4 +3,5 @@ from . import hr_contract from . import hr_payslip from . import hr_salary_rule +from . import res_config_settings from . import us_payroll_config diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index 11896ba1..5fa5cad8 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -116,34 +116,52 @@ class HRPayslip(models.Model): 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 sum(self, code, from_date, to_date=None): - if to_date is None: - to_date = fields.Date.today() - self.env.cr.execute(""" + + 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.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)) + 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 _sum(self, code, from_date, to_date=None): - if to_date is None: - to_date = fields.Date.today() - self.env.cr.execute(""" + + 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.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)) + 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): @@ -157,28 +175,37 @@ class HRPayslip(models.Model): 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) + 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): 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)) + 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): # Hibou Backport if to_date is None: 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) - 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)) + self.env.cr.execute(self.__browsable_query_category, (self.employee_id, from_date, to_date, code)) res = self.env.cr.fetchone() return res and res[0] or 0.0 diff --git a/l10n_us_hr_payroll/models/res_config_settings.py b/l10n_us_hr_payroll/models/res_config_settings.py new file mode 100644 index 00000000..05af9430 --- /dev/null +++ b/l10n_us_hr_payroll/models/res_config_settings.py @@ -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') diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index 11a045e2..d2b95c24 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -1,6 +1,9 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. from . import common + +from . import test_special + from . import test_us_payslip_2019 from . import test_us_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py index 5540f5de..a7c6aee5 100755 --- a/l10n_us_hr_payroll/tests/common.py +++ b/l10n_us_hr_payroll/tests/common.py @@ -22,6 +22,10 @@ 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): @@ -154,15 +158,6 @@ class TestUsPayslip(common.TransactionCase): def assertPayrollAlmostEqual(self, first, second): 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={}): country_key = 'US_COUNTRY' if code in cache: diff --git a/l10n_us_hr_payroll/tests/test_special.py b/l10n_us_hr_payroll/tests/test_special.py new file mode 100644 index 00000000..f87ee5d1 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_special.py @@ -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) diff --git a/l10n_us_hr_payroll/views/res_config_settings_views.xml b/l10n_us_hr_payroll/views/res_config_settings_views.xml new file mode 100644 index 00000000..3c69b42f --- /dev/null +++ b/l10n_us_hr_payroll/views/res_config_settings_views.xml @@ -0,0 +1,32 @@ + + + + + res.config.settings.view.form.inherit + res.config.settings + + + +
+
+
+ Payslip Sum Behavior +
+ 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. +
+
+
+
+
+
+
+
+
+
+
+ +