Merge branch 'mig/15.0/hr_payroll_hibou' into '15.0'

mig/15.0/hr_payroll_hibou into 15.0

See merge request hibou-io/hibou-odoo/suite!1106
This commit is contained in:
Jared Kipe
2021-10-06 20:29:49 +00:00
13 changed files with 699 additions and 0 deletions

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -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. <hello@hibou.io>',
'version': '15.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',
}

View File

@@ -0,0 +1,5 @@
from . import browsable_object
from . import hr_contract
from . import hr_payslip
from . import hr_salary_rule
from . import res_config_settings

View File

@@ -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

View File

@@ -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()

View File

@@ -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 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)

View File

@@ -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'),
])

View File

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

@@ -0,0 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import common
from . import test_contract_wage_type
from . import test_special

158
hr_payroll_hibou/tests/common.py Executable file
View File

@@ -0,0 +1,158 @@
# 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:
return payslip.action_payslip_done()
except AttributeError:
# v9
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']
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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form_hibou" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.hr.payroll.hibou</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="50"/>
<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">
<field name="module_l10n_us_hr_payroll" invisible="1"/>
<!-- TODO payment, attendance, timesheet, ovetrtime... -->
<h2>Hibou Payroll</h2>
<div class="row mt16 o_settings_container" id="hr_payroll_hibou">
<div class="col-lg-6 col-12 o_setting_box">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label for="payslip_sum_type" string="Payslip Sum Behavior"/>
<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>
<field name="payslip_sum_type"/>
</div>
</div>
<div class="col-lg-6 col-12 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_l10n_us_hr_payroll_401k"/>
</div>
<div class="o_setting_right_pane">
<label for="module_l10n_us_hr_payroll_401k" string="USA Payroll 401k"/>
<div class="text-muted">
Provide retirement plans with optional company matching.
<strong>Hibou Professional</strong>
</div>
<div class="mt8" id="l10n_us_hr_payroll_401k_match">
<button name="%(hr_payroll.hr_rule_parameter_action)d" icon="fa-arrow-right" type="action"
string="Configure Matching &amp; Limits" class="btn-link"
context="{'search_default_us_payroll_401k': True}"
attrs="{'invisible': [('module_l10n_us_hr_payroll_401k', '=', False)]}"/>
</div>
</div>
</div>
<div class="col-lg-6 col-12 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_hr_payroll_payment"/>
</div>
<div class="o_setting_right_pane">
<label for="module_hr_payroll_payment"/>
<div class="text-muted">
Register payments on payslips! Have control over journal entries created from
payroll to include partner details, set grouping options, and apply fiscal position
account mappings.
<strong>Hibou Professional</strong>
</div>
</div>
</div>
<div class="col-lg-6 col-12 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_hr_payroll_attendance"/>
</div>
<div class="o_setting_right_pane">
<label for="module_hr_payroll_attendance"/>
<div class="text-muted">
Extend Attendance into Payroll with Work Types, Overtime!
<strong>Hibou Professional</strong>
</div>
</div>
</div>
<div class="col-lg-6 col-12 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_hr_payroll_timesheet"/>
</div>
<div class="o_setting_right_pane">
<label for="module_hr_payroll_timesheet"/>
<div class="text-muted">
Extend Timesheets into Payroll with Work Types, Overtime!
<strong>Hibou Professional</strong>
</div>
</div>
</div>
<div class="col-lg-6 col-12 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_hr_payroll_commission"/>
</div>
<div class="o_setting_right_pane">
<label for="module_hr_payroll_commission"/>
<div class="text-muted">
Pay Commissions in Payroll!
<strong>Hibou Professional</strong>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>