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. +
+
+
+
+
+
+
+
+
+
+
+ +