diff --git a/l10n_us_hr_payroll/__init__.py b/l10n_us_hr_payroll/__init__.py index 09434554..013f4e73 100644 --- 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 2785d47b..0084f427 100644 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -54,10 +54,12 @@ United States of America - Payroll Rules. 'data/state/va_virginia.xml', 'data/state/wa_washington.xml', 'views/hr_contract_views.xml', + 'views/res_config_settings_views.xml', 'views/us_payroll_config_views.xml', ], '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 c6d607ff..8611b49a 100644 --- a/l10n_us_hr_payroll/models/__init__.py +++ b/l10n_us_hr_payroll/models/__init__.py @@ -1,5 +1,7 @@ # 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_payslip +from . import res_config_settings from . import us_payroll_config diff --git a/l10n_us_hr_payroll/models/browsable_object.py b/l10n_us_hr_payroll/models/browsable_object.py new file mode 100644 index 00000000..17f29c3f --- /dev/null +++ b/l10n_us_hr_payroll/models/browsable_object.py @@ -0,0 +1,122 @@ +# 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) + 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(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']) + + 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 + + @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 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 014d3719..df6491a6 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): @@ -149,15 +153,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..430af0ea --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_special.py @@ -0,0 +1,65 @@ +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.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) 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. +
+
+
+
+
+
+
+
+
+
+
+ +