From 7fe275a21dd3615f68af92dc2594ef9e3875af61 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sat, 21 Nov 2020 17:19:37 -0800 Subject: [PATCH 1/7] [ADD] hr_payroll_hibou: for Odoo Enterprise 14.0 Abstract fixes and behaviors. - Adds configurable option for Payslip rule calculation sum date field. - Fixes inconsistency between .sum_category('CODE') and .categories['CODE'] - Adds semi-monthly Semi-monthly schedule_pay All features are tested and the tests themselves should serve as test harnesses for other 'payroll' modules. --- hr_payroll_hibou/__init__.py | 1 + hr_payroll_hibou/__manifest__.py | 27 ++++ hr_payroll_hibou/models/__init__.py | 4 + hr_payroll_hibou/models/browsable_object.py | 151 +++++++++++++++++ hr_payroll_hibou/models/hr_payslip.py | 14 ++ hr_payroll_hibou/models/hr_salary_rule.py | 19 +++ .../models/res_config_settings.py | 27 ++++ hr_payroll_hibou/tests/__init__.py | 5 + hr_payroll_hibou/tests/common.py | 153 ++++++++++++++++++ hr_payroll_hibou/tests/test_special.py | 127 +++++++++++++++ .../views/res_config_settings_views.xml | 32 ++++ 11 files changed, 560 insertions(+) create mode 100644 hr_payroll_hibou/__init__.py create mode 100644 hr_payroll_hibou/__manifest__.py create mode 100644 hr_payroll_hibou/models/__init__.py create mode 100644 hr_payroll_hibou/models/browsable_object.py create mode 100644 hr_payroll_hibou/models/hr_payslip.py create mode 100644 hr_payroll_hibou/models/hr_salary_rule.py create mode 100644 hr_payroll_hibou/models/res_config_settings.py create mode 100644 hr_payroll_hibou/tests/__init__.py create mode 100755 hr_payroll_hibou/tests/common.py create mode 100644 hr_payroll_hibou/tests/test_special.py create mode 100644 hr_payroll_hibou/views/res_config_settings_views.xml diff --git a/hr_payroll_hibou/__init__.py b/hr_payroll_hibou/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/hr_payroll_hibou/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_payroll_hibou/__manifest__.py b/hr_payroll_hibou/__manifest__.py new file mode 100644 index 00000000..94ba5190 --- /dev/null +++ b/hr_payroll_hibou/__manifest__.py @@ -0,0 +1,27 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'Hibou Payroll', + 'author': 'Hibou Corp. ', + 'version': '14.0.1.0.0', + 'category': 'Payroll Localization', + 'depends': [ + 'hr_payroll', + 'hr_contract_reports', + 'hibou_professional', + ], + 'description': """ +Hibou Payroll +============= + +Base module for fixing specific qwerks or assumptions in the way Payroll Odoo Enterprise Edition behaves. + + """, + 'data': [ + 'views/res_config_settings_views.xml', + ], + 'demo': [ + ], + 'auto_install': True, + 'license': 'OPL-1', +} diff --git a/hr_payroll_hibou/models/__init__.py b/hr_payroll_hibou/models/__init__.py new file mode 100644 index 00000000..4538599a --- /dev/null +++ b/hr_payroll_hibou/models/__init__.py @@ -0,0 +1,4 @@ +from . import browsable_object +from . import hr_payslip +from . import hr_salary_rule +from . import res_config_settings diff --git a/hr_payroll_hibou/models/browsable_object.py b/hr_payroll_hibou/models/browsable_object.py new file mode 100644 index 00000000..60543eb8 --- /dev/null +++ b/hr_payroll_hibou/models/browsable_object.py @@ -0,0 +1,151 @@ +# 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 __getitem__(self, key): + return self.dict[key] 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 diff --git a/hr_payroll_hibou/models/hr_payslip.py b/hr_payroll_hibou/models/hr_payslip.py new file mode 100644 index 00000000..1a3ffffc --- /dev/null +++ b/hr_payroll_hibou/models/hr_payslip.py @@ -0,0 +1,14 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + def get_year(self): + """ + # Helper method to get the year (normalized between Odoo Versions) + :return: int year of payslip + """ + return self.date_to.year diff --git a/hr_payroll_hibou/models/hr_salary_rule.py b/hr_payroll_hibou/models/hr_salary_rule.py new file mode 100644 index 00000000..9ad2234d --- /dev/null +++ b/hr_payroll_hibou/models/hr_salary_rule.py @@ -0,0 +1,19 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class HrPayrollStructure(models.Model): + _inherit = 'hr.payroll.structure' + + schedule_pay = fields.Selection(selection_add=[ + ('semi-monthly', 'Semi-monthly'), + ], ondelete={'semi-monthly': 'set null'}) + + +class HrPayrollStructureType(models.Model): + _inherit = 'hr.payroll.structure.type' + + default_schedule_pay = fields.Selection(selection_add=[ + ('semi-monthly', 'Semi-monthly'), + ]) diff --git a/hr_payroll_hibou/models/res_config_settings.py b/hr_payroll_hibou/models/res_config_settings.py new file mode 100644 index 00000000..98faeeac --- /dev/null +++ b/hr_payroll_hibou/models/res_config_settings.py @@ -0,0 +1,27 @@ +# 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' + + # TODO We need MORE here... + module_l10n_us_hr_payroll = fields.Boolean(string='USA Payroll') + + 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/hr_payroll_hibou/tests/__init__.py b/hr_payroll_hibou/tests/__init__.py new file mode 100644 index 00000000..cf74eea4 --- /dev/null +++ b/hr_payroll_hibou/tests/__init__.py @@ -0,0 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import common + +from . import test_special diff --git a/hr_payroll_hibou/tests/common.py b/hr_payroll_hibou/tests/common.py new file mode 100755 index 00000000..d1423abe --- /dev/null +++ b/hr_payroll_hibou/tests/common.py @@ -0,0 +1,153 @@ +# 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 odoo.tests import common +from odoo.tools.float_utils import float_round as odoo_float_round + + +def process_payslip(payslip): + try: + payslip.action_payslip_done() + except AttributeError: + # v9 + payslip.process_sheet() + + +class TestPayslip(common.TransactionCase): + debug = False + _logger = getLogger(__name__) + + def setUp(self): + super(TestPayslip, self).setUp() + self.contract_model = self.env['hr.contract'] + self.env.user.tz = 'PST8PDT' + self.env.ref('resource.resource_calendar_std').tz = 'PST8PDT' + self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to') + self.structure_type = self.env['hr.payroll.structure.type'].create({ + 'name': 'Test Structure Type', + }) + self.structure = self.env['hr.payroll.structure'].create({ + 'name': 'Test Structure', + 'type_id': self.structure_type.id, + }) + self._log('structue_type %s and structure %s' % (self.structure_type, self.structure)) + self.structure_type.default_struct_id = self.structure + self.resource_calendar = self.ref('resource.resource_calendar_std') + + 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.warning(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 _get_contract_defaults(self, 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 + + # Compatibility with earlier Odoo versions + if not contract_values.get('journal_id') and hasattr(self.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 + + def _createContract(self, employee, **kwargs): + if not 'schedule_pay' in kwargs: + kwargs['schedule_pay'] = 'monthly' + schedule_pay = kwargs['schedule_pay'] + contract_values = { + 'name': 'Test Contract', + 'employee_id': employee.id, + } + + for key, val in kwargs.items(): + # Assume any Odoo object is in a Many2one + if hasattr(val, 'id'): + val = val.id + found = False + if hasattr(self.contract_model, key): + contract_values[key] = val + found = True + if not found: + self._logger.warn('cannot locate attribute names "%s" on hr.contract().' % (key, )) + + self._get_contract_defaults(contract_values) + contract = self.contract_model.create(contract_values) + + # Compatibility with Odoo 14 + contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay + return contract + + def _createPayslip(self, employee, date_from, date_to, skip_compute=False): + 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 + }) + # Included in hr.payslip.action_refresh_from_work_entries() as ov 14.0 EE + # slip._onchange_employee() + # as is the 'compute' that is almost always called immediaately after + if not skip_compute: + slip.action_refresh_from_work_entries() + 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) diff --git a/hr_payroll_hibou/tests/test_special.py b/hr_payroll_hibou/tests/test_special.py new file mode 100644 index 00000000..077f2de3 --- /dev/null +++ b/hr_payroll_hibou/tests/test_special.py @@ -0,0 +1,127 @@ +from .common import TestPayslip, process_payslip + + +class TestSpecial(TestPayslip): + + def test_get_year(self): + salary = 80000.0 + employee = self._createEmployee() + # so the schedule_pay is now on the Structure... + contract = self._createContract(employee, wage=salary) + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-14') + self.assertEqual(payslip.get_year(), 2020) + + 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') + + def test_payslip_sum_behavior(self): + self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date') + 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': self.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') + cats = self._getCategories(payslip) + self.assertEqual(cats['BASIC'], salary) + 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 + # 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': self.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 +# Note, this tests the hr.payslip.get_year() to return an integer rep of year +year = payslip.dict.get_year() +result += payslip.sum_category('ALW', str(year) + '-01-01', str(year+1) + '-01-01') + """, + 'sequence': 101, + 'struct_id': self.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) diff --git a/hr_payroll_hibou/views/res_config_settings_views.xml b/hr_payroll_hibou/views/res_config_settings_views.xml new file mode 100644 index 00000000..a4dc1854 --- /dev/null +++ b/hr_payroll_hibou/views/res_config_settings_views.xml @@ -0,0 +1,32 @@ + + + + + res.config.settings.view.form.inherit.hr.payroll.hibou + res.config.settings + + + + + + +

Hibou Payroll

+
+
+
+
+
+
+
+ + + + + From 487daf1201dac6c1422979d68fd4ea2e75fb16ff Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 23 Nov 2020 17:38:55 -0800 Subject: [PATCH 2/7] [MIG] l10n_us_hr_payroll_401k: for Odoo Enterprise 14.0 Additionally, added search view to assist in configuring and added it to the config section in `hr_payroll_hibou`. --- hr_payroll_hibou/models/res_config_settings.py | 1 + .../views/res_config_settings_views.xml | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/hr_payroll_hibou/models/res_config_settings.py b/hr_payroll_hibou/models/res_config_settings.py index 98faeeac..9d47b99e 100644 --- a/hr_payroll_hibou/models/res_config_settings.py +++ b/hr_payroll_hibou/models/res_config_settings.py @@ -8,6 +8,7 @@ class ResConfigSettings(models.TransientModel): # TODO We need MORE here... module_l10n_us_hr_payroll = fields.Boolean(string='USA Payroll') + module_l10n_us_hr_payroll_401k = fields.Boolean(string='USA Payroll 401k') payslip_sum_type = fields.Selection([ ('date_from', 'Date From'), diff --git a/hr_payroll_hibou/views/res_config_settings_views.xml b/hr_payroll_hibou/views/res_config_settings_views.xml index a4dc1854..65c3bd51 100644 --- a/hr_payroll_hibou/views/res_config_settings_views.xml +++ b/hr_payroll_hibou/views/res_config_settings_views.xml @@ -24,6 +24,23 @@
+
+
+ +
+
+
+
From 30f1414df5fa725e91ae5cee89fa593b274e635d Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 27 Nov 2020 14:22:16 -0800 Subject: [PATCH 3/7] [IMP] hr_payroll_hibou: Control the 'wage_type' at the HR Contract Level This will make it possible to be more abstract with 'work_type' or 'worked days lines' and overtime. --- hr_payroll_hibou/__manifest__.py | 1 + hr_payroll_hibou/models/__init__.py | 1 + hr_payroll_hibou/models/hr_contract.py | 20 ++++++++++++++ hr_payroll_hibou/models/hr_payslip.py | 13 ++++++++++ .../models/res_config_settings.py | 2 ++ hr_payroll_hibou/tests/__init__.py | 1 + hr_payroll_hibou/tests/common.py | 9 +++++-- .../tests/test_contract_wage_type.py | 26 +++++++++++++++++++ hr_payroll_hibou/views/hr_contract_views.xml | 22 ++++++++++++++++ .../views/res_config_settings_views.xml | 25 ++++++++++++++++++ 10 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 hr_payroll_hibou/models/hr_contract.py create mode 100644 hr_payroll_hibou/tests/test_contract_wage_type.py create mode 100755 hr_payroll_hibou/views/hr_contract_views.xml diff --git a/hr_payroll_hibou/__manifest__.py b/hr_payroll_hibou/__manifest__.py index 94ba5190..3c074c14 100644 --- a/hr_payroll_hibou/__manifest__.py +++ b/hr_payroll_hibou/__manifest__.py @@ -18,6 +18,7 @@ Base module for fixing specific qwerks or assumptions in the way Payroll Odoo En """, 'data': [ + 'views/hr_contract_views.xml', 'views/res_config_settings_views.xml', ], 'demo': [ diff --git a/hr_payroll_hibou/models/__init__.py b/hr_payroll_hibou/models/__init__.py index 4538599a..ecd8aaf0 100644 --- a/hr_payroll_hibou/models/__init__.py +++ b/hr_payroll_hibou/models/__init__.py @@ -1,4 +1,5 @@ from . import browsable_object +from . import hr_contract from . import hr_payslip from . import hr_salary_rule from . import res_config_settings diff --git a/hr_payroll_hibou/models/hr_contract.py b/hr_payroll_hibou/models/hr_contract.py new file mode 100644 index 00000000..165c45ed --- /dev/null +++ b/hr_payroll_hibou/models/hr_contract.py @@ -0,0 +1,20 @@ +from odoo import fields, models + + +class HrContract(models.Model): + _inherit = 'hr.contract' + + wage_type = fields.Selection([('monthly', 'Period Fixed Wage'), ('hourly', 'Hourly Wage')], + default='monthly', required=True, related=False) + + def _get_contract_wage(self, work_type=None): + # Override if you pay differently for different work types + # In 14.0, this utilizes new computed field mechanism, + # but will still get the 'wage' field by default. + self.ensure_one() + return self[self._get_contract_wage_field(work_type=work_type)] + + def _get_contract_wage_field(self, work_type=None): + if self.wage_type == 'hourly': + return 'hourly_wage' + return super()._get_contract_wage_field() diff --git a/hr_payroll_hibou/models/hr_payslip.py b/hr_payroll_hibou/models/hr_payslip.py index 1a3ffffc..fab438f5 100644 --- a/hr_payroll_hibou/models/hr_payslip.py +++ b/hr_payroll_hibou/models/hr_payslip.py @@ -6,9 +6,22 @@ from odoo import fields, models class HrPayslip(models.Model): _inherit = 'hr.payslip' + # We need to be able to support more complexity, + # namely, that different employees will be paid by different wage types as 'salary' vs 'hourly' + wage_type = fields.Selection(related='contract_id.wage_type') + def get_year(self): """ # Helper method to get the year (normalized between Odoo Versions) :return: int year of payslip """ return self.date_to.year + + def _get_contract_wage(self, work_type=None): + # Override if you pay differently for different work types + # In 14.0, this utilizes new computed field mechanism, + # but will still get the 'wage' field by default. + + # This would be a good place to override though with a 'work type' + # based mechanism, like a minimum rate or 'rate card' implementation + return self.contract_id._get_contract_wage(work_type=work_type) diff --git a/hr_payroll_hibou/models/res_config_settings.py b/hr_payroll_hibou/models/res_config_settings.py index 9d47b99e..d282e2a7 100644 --- a/hr_payroll_hibou/models/res_config_settings.py +++ b/hr_payroll_hibou/models/res_config_settings.py @@ -7,6 +7,8 @@ class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' # TODO We need MORE here... + module_hr_payroll_attendance = fields.Boolean(string='Attendance Entries & Overtime') + module_hr_payroll_timesheet = fields.Boolean(string='Timesheet Entries & Overtime') module_l10n_us_hr_payroll = fields.Boolean(string='USA Payroll') module_l10n_us_hr_payroll_401k = fields.Boolean(string='USA Payroll 401k') diff --git a/hr_payroll_hibou/tests/__init__.py b/hr_payroll_hibou/tests/__init__.py index cf74eea4..45ce36c6 100644 --- a/hr_payroll_hibou/tests/__init__.py +++ b/hr_payroll_hibou/tests/__init__.py @@ -2,4 +2,5 @@ from . import common +from . import test_contract_wage_type from . import test_special diff --git a/hr_payroll_hibou/tests/common.py b/hr_payroll_hibou/tests/common.py index d1423abe..00a3564b 100755 --- a/hr_payroll_hibou/tests/common.py +++ b/hr_payroll_hibou/tests/common.py @@ -10,16 +10,21 @@ from odoo.tools.float_utils import float_round as odoo_float_round def process_payslip(payslip): try: - payslip.action_payslip_done() + return payslip.action_payslip_done() except AttributeError: # v9 - payslip.process_sheet() + return payslip.process_sheet() class TestPayslip(common.TransactionCase): debug = False _logger = getLogger(__name__) + def process_payslip(self, payslip=None): + if not payslip: + return process_payslip(self.payslip) + return process_payslip(payslip) + def setUp(self): super(TestPayslip, self).setUp() self.contract_model = self.env['hr.contract'] diff --git a/hr_payroll_hibou/tests/test_contract_wage_type.py b/hr_payroll_hibou/tests/test_contract_wage_type.py new file mode 100644 index 00000000..5908f8b5 --- /dev/null +++ b/hr_payroll_hibou/tests/test_contract_wage_type.py @@ -0,0 +1,26 @@ +from .common import TestPayslip, process_payslip + + +class TestContractWageType(TestPayslip): + + def test_per_contract_wage_type_salary(self): + self.debug = True + salary = 80000.0 + employee = self._createEmployee() + contract = self._createContract(employee, wage=salary, hourly_wage=salary/100.0, wage_type='monthly', schedule_pay='bi-weekly') + payslip = self._createPayslip(employee, '2019-12-30', '2020-01-12') + self.assertEqual(contract.wage_type, 'monthly') + self.assertEqual(payslip.wage_type, 'monthly') + cats = self._getCategories(payslip) + self.assertEqual(cats['BASIC'], salary) + + def test_per_contract_wage_type_hourly(self): + self.debug = True + hourly_wage = 21.50 + employee = self._createEmployee() + contract = self._createContract(employee, wage=hourly_wage*100.0, hourly_wage=hourly_wage, wage_type='hourly', schedule_pay='bi-weekly') + payslip = self._createPayslip(employee, '2019-12-30', '2020-01-12') + self.assertEqual(contract.wage_type, 'hourly') + self.assertEqual(payslip.wage_type, 'hourly') + cats = self._getCategories(payslip) + self.assertEqual(cats['BASIC'], hourly_wage * 80.0) diff --git a/hr_payroll_hibou/views/hr_contract_views.xml b/hr_payroll_hibou/views/hr_contract_views.xml new file mode 100755 index 00000000..38934065 --- /dev/null +++ b/hr_payroll_hibou/views/hr_contract_views.xml @@ -0,0 +1,22 @@ + + + + + hr.contract.form.inherit + hr.contract + 20 + + + + + + {} + + + Period Advantages in Cash + + + + + + diff --git a/hr_payroll_hibou/views/res_config_settings_views.xml b/hr_payroll_hibou/views/res_config_settings_views.xml index 65c3bd51..9933edbd 100644 --- a/hr_payroll_hibou/views/res_config_settings_views.xml +++ b/hr_payroll_hibou/views/res_config_settings_views.xml @@ -32,6 +32,7 @@
From 04ff9309901af1ec47b24de8451c7935c3355e51 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 9 Mar 2021 17:02:13 -0800 Subject: [PATCH 4/7] [IMP] hr_payroll_hibou: add module `hr_payroll_payment` --- hr_payroll_hibou/models/res_config_settings.py | 1 + .../views/res_config_settings_views.xml | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/hr_payroll_hibou/models/res_config_settings.py b/hr_payroll_hibou/models/res_config_settings.py index d282e2a7..b1c52e91 100644 --- a/hr_payroll_hibou/models/res_config_settings.py +++ b/hr_payroll_hibou/models/res_config_settings.py @@ -7,6 +7,7 @@ class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' # TODO We need MORE here... + module_hr_payroll_payment = fields.Boolean(string='Payments & Advanced Accounting') module_hr_payroll_attendance = fields.Boolean(string='Attendance Entries & Overtime') module_hr_payroll_timesheet = fields.Boolean(string='Timesheet Entries & Overtime') module_l10n_us_hr_payroll = fields.Boolean(string='USA Payroll') diff --git a/hr_payroll_hibou/views/res_config_settings_views.xml b/hr_payroll_hibou/views/res_config_settings_views.xml index 9933edbd..4d516318 100644 --- a/hr_payroll_hibou/views/res_config_settings_views.xml +++ b/hr_payroll_hibou/views/res_config_settings_views.xml @@ -42,6 +42,20 @@ +
+
+ +
+
+
+
From 7d50bb68822b3a29902c72635915c0e3539caac4 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 24 Sep 2021 13:09:28 -0700 Subject: [PATCH 5/7] [IMP] hr_payroll_hibou: add module for `hr_payroll_commission` --- hr_payroll_hibou/__manifest__.py | 2 +- hr_payroll_hibou/models/res_config_settings.py | 1 + hr_payroll_hibou/views/res_config_settings_views.xml | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/hr_payroll_hibou/__manifest__.py b/hr_payroll_hibou/__manifest__.py index 3c074c14..facc5ee8 100644 --- a/hr_payroll_hibou/__manifest__.py +++ b/hr_payroll_hibou/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Hibou Payroll', 'author': 'Hibou Corp. ', - 'version': '14.0.1.0.0', + 'version': '14.0.1.1.0', 'category': 'Payroll Localization', 'depends': [ 'hr_payroll', diff --git a/hr_payroll_hibou/models/res_config_settings.py b/hr_payroll_hibou/models/res_config_settings.py index b1c52e91..2299c0fb 100644 --- a/hr_payroll_hibou/models/res_config_settings.py +++ b/hr_payroll_hibou/models/res_config_settings.py @@ -10,6 +10,7 @@ class ResConfigSettings(models.TransientModel): module_hr_payroll_payment = fields.Boolean(string='Payments & Advanced Accounting') module_hr_payroll_attendance = fields.Boolean(string='Attendance Entries & Overtime') module_hr_payroll_timesheet = fields.Boolean(string='Timesheet Entries & Overtime') + module_hr_payroll_commission = fields.Boolean(string='Commission') module_l10n_us_hr_payroll = fields.Boolean(string='USA Payroll') module_l10n_us_hr_payroll_401k = fields.Boolean(string='USA Payroll 401k') diff --git a/hr_payroll_hibou/views/res_config_settings_views.xml b/hr_payroll_hibou/views/res_config_settings_views.xml index 4d516318..ef185c82 100644 --- a/hr_payroll_hibou/views/res_config_settings_views.xml +++ b/hr_payroll_hibou/views/res_config_settings_views.xml @@ -80,6 +80,18 @@
+
+
+ +
+
+
+
From 7cca6ac9571b0ff6bfd56dc6e77c3627ad2d43a4 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 24 Sep 2021 16:11:55 -0700 Subject: [PATCH 6/7] [FIX] hr_commission: `account.move` soft deprecated method post --- hr_payroll_hibou/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hr_payroll_hibou/__manifest__.py b/hr_payroll_hibou/__manifest__.py index facc5ee8..c5d28890 100644 --- a/hr_payroll_hibou/__manifest__.py +++ b/hr_payroll_hibou/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Hibou Payroll', 'author': 'Hibou Corp. ', - 'version': '14.0.1.1.0', + 'version': '14.0.1.1.1', 'category': 'Payroll Localization', 'depends': [ 'hr_payroll', From a23f137fa2fd119391c3a3b4dcb9d5f10b0dc781 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 6 Oct 2021 13:25:13 -0700 Subject: [PATCH 7/7] [MIG] hr_payroll_hibou: to Odoo 15.0 --- hr_payroll_hibou/__manifest__.py | 3 +-- hr_payroll_hibou/views/hr_contract_views.xml | 22 -------------------- 2 files changed, 1 insertion(+), 24 deletions(-) delete mode 100755 hr_payroll_hibou/views/hr_contract_views.xml diff --git a/hr_payroll_hibou/__manifest__.py b/hr_payroll_hibou/__manifest__.py index c5d28890..88a5463b 100644 --- a/hr_payroll_hibou/__manifest__.py +++ b/hr_payroll_hibou/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Hibou Payroll', 'author': 'Hibou Corp. ', - 'version': '14.0.1.1.1', + 'version': '15.0.1.0.0', 'category': 'Payroll Localization', 'depends': [ 'hr_payroll', @@ -18,7 +18,6 @@ Base module for fixing specific qwerks or assumptions in the way Payroll Odoo En """, 'data': [ - 'views/hr_contract_views.xml', 'views/res_config_settings_views.xml', ], 'demo': [ diff --git a/hr_payroll_hibou/views/hr_contract_views.xml b/hr_payroll_hibou/views/hr_contract_views.xml deleted file mode 100755 index 38934065..00000000 --- a/hr_payroll_hibou/views/hr_contract_views.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - hr.contract.form.inherit - hr.contract - 20 - - - - - - {} - - - Period Advantages in Cash - - - - - -