diff --git a/fusion_accounting_hr_payroll/__init__.py b/fusion_accounting_hr_payroll/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/fusion_accounting_hr_payroll/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fusion_accounting_hr_payroll/__manifest__.py b/fusion_accounting_hr_payroll/__manifest__.py new file mode 100644 index 00000000..c890e584 --- /dev/null +++ b/fusion_accounting_hr_payroll/__manifest__.py @@ -0,0 +1,59 @@ +{ + 'name': 'Fusion Accounting - HR Payroll Bridge', + 'version': '19.0.1.0.0', + 'category': 'Human Resources/Payroll', + 'summary': 'Bridges payroll (hr_payroll) to accounting via account.move creation when payslips are validated.', + 'description': """ +Fusion Accounting - HR Payroll Bridge +===================================== + +A Fusion-native replacement for Odoo Enterprise's ``hr_payroll_account`` +module. Removes Westin's last payroll-accounting dependency on the +Enterprise ``accountant`` umbrella. + +Scope +----- +- Adds ``account_debit`` / ``account_credit`` / ``analytic_distribution`` to + ``hr.salary.rule`` (company-dependent GL mapping per rule). +- Adds ``move_id`` + ``journal_id`` + ``_fusion_create_account_move`` to + ``hr.payslip``: when a payslip is validated, generates a balanced + ``account.move`` from the salary rule mapping. +- Adds ``fusion_payroll_journal_id`` + ``fusion_payroll_auto_post`` to + ``res.company`` (fallback journal + auto-post toggle). +- Reverse links ``payslip_ids`` / ``payslip_count`` on ``account.move`` + for traceability and reporting. + +Coexistence +----------- +When Odoo Enterprise's ``hr_payroll_account`` is also installed, this +module yields move-creation to it (detected at runtime via +``ir.module.module``) so payslips don't get duplicate entries. After +``hr_payroll_account`` is uninstalled, this module owns the bridge. + +Auto-install +------------ +Auto-installs whenever both ``hr_payroll`` and ``fusion_accounting_core`` +are present. +""", + 'author': 'Nexa Systems Inc.', + 'website': 'https://nexasystems.ca', + 'license': 'LGPL-3', + 'depends': [ + 'fusion_accounting_core', + 'account', + 'hr_payroll', + 'base_iban', + ], + 'data': [ + 'data/hr_salary_rule_data.xml', + 'views/hr_salary_rule_views.xml', + 'views/hr_payslip_views.xml', + 'views/hr_payroll_structure_views.xml', + 'views/res_config_settings_views.xml', + 'views/account_move_views.xml', + ], + 'auto_install': ['hr_payroll', 'fusion_accounting_core'], + 'installable': True, + 'application': False, + 'icon': '/fusion_accounting_hr_payroll/static/description/icon.png', +} diff --git a/fusion_accounting_hr_payroll/data/hr_salary_rule_data.xml b/fusion_accounting_hr_payroll/data/hr_salary_rule_data.xml new file mode 100644 index 00000000..224a0d77 --- /dev/null +++ b/fusion_accounting_hr_payroll/data/hr_salary_rule_data.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fusion_accounting_hr_payroll/models/__init__.py b/fusion_accounting_hr_payroll/models/__init__.py new file mode 100644 index 00000000..7aa9d70e --- /dev/null +++ b/fusion_accounting_hr_payroll/models/__init__.py @@ -0,0 +1,10 @@ +from . import hr_salary_rule +from . import hr_payslip +from . import hr_payslip_line +from . import hr_payslip_run +from . import hr_payroll_structure +from . import account_journal +from . import account_move +from . import account_move_line +from . import res_company +from . import res_config_settings diff --git a/fusion_accounting_hr_payroll/models/account_journal.py b/fusion_accounting_hr_payroll/models/account_journal.py new file mode 100644 index 00000000..951ed690 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/account_journal.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + is_payroll_journal = fields.Boolean( + string='Used for Payroll', + help="Marks this journal as the salary / payroll posting journal " + "for the company. Informational; the actual fallback is set " + "on res.company.fusion_payroll_journal_id.", + ) diff --git a/fusion_accounting_hr_payroll/models/account_move.py b/fusion_accounting_hr_payroll/models/account_move.py new file mode 100644 index 00000000..e44bde12 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/account_move.py @@ -0,0 +1,41 @@ +from odoo import _, fields, models + + +class AccountMove(models.Model): + _inherit = 'account.move' + + payslip_ids = fields.One2many( + comodel_name='hr.payslip', + inverse_name='move_id', + string='Payslips', + readonly=True, + copy=False, + ) + payslip_count = fields.Integer( + string='# of Payslips', + compute='_compute_payslip_count', + compute_sudo=True, + ) + + def _compute_payslip_count(self): + for move in self: + move.payslip_count = len(move.payslip_ids) + + def action_open_payslip(self): + self.ensure_one() + action = { + 'name': _('Payslips'), + 'type': 'ir.actions.act_window', + 'res_model': 'hr.payslip', + } + if self.payslip_count == 1: + action.update({ + 'view_mode': 'form', + 'res_id': self.payslip_ids.id, + }) + else: + action.update({ + 'view_mode': 'list,form', + 'domain': [('id', 'in', self.payslip_ids.ids)], + }) + return action diff --git a/fusion_accounting_hr_payroll/models/account_move_line.py b/fusion_accounting_hr_payroll/models/account_move_line.py new file mode 100644 index 00000000..d02a0be0 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/account_move_line.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class AccountMoveLine(models.Model): + _inherit = 'account.move.line' + + payslip_id = fields.Many2one( + 'hr.payslip', + string='Source Payslip', + readonly=True, + copy=False, + ondelete='set null', + index='btree_not_null', + help="Payslip this journal item was generated from " + "(populated by the Fusion payroll bridge for reporting).", + ) diff --git a/fusion_accounting_hr_payroll/models/hr_payroll_structure.py b/fusion_accounting_hr_payroll/models/hr_payroll_structure.py new file mode 100644 index 00000000..e78e5bb6 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/hr_payroll_structure.py @@ -0,0 +1,26 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class HrPayrollStructure(models.Model): + _inherit = 'hr.payroll.structure' + + journal_id = fields.Many2one( + 'account.journal', + string='Salary Journal', + company_dependent=True, + domain="[('type', '=', 'general')]", + help="Default journal used when generating payroll accounting " + "entries for payslips that follow this structure.", + ) + + @api.constrains('journal_id') + def _check_journal_currency(self): + for record in self.sudo(): + journal = record.journal_id + if journal and journal.currency_id and journal.company_id \ + and journal.currency_id != journal.company_id.currency_id: + raise ValidationError(_( + "The salary journal must be in the same currency as " + "the company.", + )) diff --git a/fusion_accounting_hr_payroll/models/hr_payslip.py b/fusion_accounting_hr_payroll/models/hr_payslip.py new file mode 100644 index 00000000..30b623d8 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/hr_payslip.py @@ -0,0 +1,242 @@ +import logging +from collections import defaultdict + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + move_id = fields.Many2one( + 'account.move', + string='Accounting Entry', + readonly=True, + copy=False, + index='btree_not_null', + ) + move_state = fields.Selection( + related='move_id.state', + string='Move State', + export_string_translation=False, + ) + journal_id = fields.Many2one( + 'account.journal', + string='Salary Journal', + domain="[('type', '=', 'general')]", + ) + + @api.model + def _fusion_enterprise_bridge_active(self): + """Return True when the Enterprise hr_payroll_account module is the + authoritative payslip - GL bridge on this database. Used to avoid + duplicate move creation while both modules coexist.""" + module = self.env['ir.module.module'].sudo().search( + [('name', '=', 'hr_payroll_account')], limit=1, + ) + return bool(module) and module.state == 'installed' + + def _fusion_resolve_journal(self): + """Pick the journal for this payslip's bridge move.""" + self.ensure_one() + if self.journal_id: + return self.journal_id + struct = self.struct_id + if struct and 'journal_id' in struct._fields and struct.journal_id: + return struct.journal_id + company = self.company_id or self.env.company + return company.fusion_payroll_journal_id or False + + def _fusion_resolve_partner(self): + """Pick the best partner reference for the move lines of this payslip.""" + self.ensure_one() + employee = self.employee_id + if not employee: + return False + if 'work_contact_id' in employee._fields and employee.work_contact_id: + return employee.work_contact_id.id + if 'address_home_id' in employee._fields and employee.address_home_id: + return employee.address_home_id.id + return False + + def _fusion_get_line_amount(self, line): + """Hook so a localisation can override which payslip-line value is + posted. Defaults to ``line.total``.""" + return line.total or 0.0 + + def action_payslip_done(self): + res = super().action_payslip_done() + if self._fusion_enterprise_bridge_active(): + return res + for slip in self: + if slip.move_id: + continue + journal = slip._fusion_resolve_journal() + if not journal: + continue + try: + slip._fusion_create_account_move(journal=journal) + except UserError as err: + _logger.warning( + "Fusion payroll bridge: GL move skipped for slip %s: %s", + slip.id, err, + ) + slip.message_post(body=_( + "Fusion Payroll bridge could not create the journal " + "entry: %s", + ) % err) + except Exception: + _logger.exception( + "Fusion payroll bridge: unexpected failure for slip %s", + slip.id, + ) + return res + + def action_payslip_cancel(self): + if hasattr(super(), 'action_payslip_cancel'): + res = super().action_payslip_cancel() + else: + res = True + if self._fusion_enterprise_bridge_active(): + return res + for slip in self: + move = slip.move_id + if not move: + continue + try: + if move.state == 'posted': + move.button_draft() + move.with_context(force_delete=True).unlink() + except Exception: + _logger.exception( + "Fusion payroll bridge: cannot reverse move %s for slip %s", + move.id, slip.id, + ) + return res + + def _fusion_create_account_move(self, journal=None): + """Build a balanced ``account.move`` from this payslip using the + ``account_debit`` / ``account_credit`` mapping on each salary rule. + Returns the created move (or False if there is nothing to post).""" + self.ensure_one() + if not self.line_ids: + return False + journal = journal or self._fusion_resolve_journal() + if not journal: + raise UserError(_( + "No salary journal configured for company %s. " + "Set a fallback journal under Accounting Settings - " + "Fusion Payroll Bridge.", + ) % (self.company_id.display_name if self.company_id else '')) + + debit_per_account = defaultdict(float) + credit_per_account = defaultdict(float) + analytic_per_account = {} + + for line in self.line_ids: + rule = line.salary_rule_id + amount = self._fusion_get_line_amount(line) + if not amount: + continue + debit_account = rule.account_debit + credit_account = rule.account_credit + analytic = ( + rule.fusion_analytic_account_id + if 'fusion_analytic_account_id' in rule._fields + else False + ) + + if amount > 0: + if debit_account: + debit_per_account[debit_account.id] += amount + if credit_account: + credit_per_account[credit_account.id] += amount + else: + pos = -amount + if debit_account: + credit_per_account[debit_account.id] += pos + if credit_account: + debit_per_account[credit_account.id] += pos + + if analytic: + for acc in (debit_account, credit_account): + if acc and acc.id not in analytic_per_account: + analytic_per_account[acc.id] = analytic.id + + partner_id = self._fusion_resolve_partner() + line_label = self.display_name or self.number or _('Payslip') + move_lines = [] + all_accounts = set(debit_per_account) | set(credit_per_account) + + for account_id in all_accounts: + net = ( + debit_per_account.get(account_id, 0.0) + - credit_per_account.get(account_id, 0.0) + ) + if abs(net) < 0.005: + continue + vals = { + 'account_id': account_id, + 'name': line_label, + 'partner_id': partner_id, + } + if net > 0: + vals['debit'] = round(net, 2) + vals['credit'] = 0.0 + else: + vals['debit'] = 0.0 + vals['credit'] = round(-net, 2) + analytic_id = analytic_per_account.get(account_id) + if analytic_id: + vals['analytic_distribution'] = {str(analytic_id): 100.0} + move_lines.append((0, 0, vals)) + + if not move_lines: + return False + + total_debit = sum(vals[2]['debit'] for vals in move_lines) + total_credit = sum(vals[2]['credit'] for vals in move_lines) + if abs(total_debit - total_credit) > 0.01: + raise UserError(_( + "Payroll move not balanced: debit=%(d).2f, credit=%(c).2f. " + "Check the account_debit / account_credit mapping on the " + "salary rules of payslip %(name)s.", + ) % { + 'd': total_debit, + 'c': total_credit, + 'name': self.display_name, + }) + + move_vals = { + 'journal_id': journal.id, + 'date': self.date_to or fields.Date.context_today(self), + 'ref': self.number or self.display_name, + 'line_ids': move_lines, + 'move_type': 'entry', + } + move = self.env['account.move'].sudo().create(move_vals) + if self.company_id and self.company_id.fusion_payroll_auto_post: + try: + move.action_post() + except Exception: + _logger.exception( + "Fusion payroll bridge: auto-post failed for move %s; " + "leaving in draft.", + move.id, + ) + self.move_id = move.id + return move + + def action_open_move(self): + self.ensure_one() + if not self.move_id: + return False + return { + 'type': 'ir.actions.act_window', + 'name': _('Journal Entry'), + 'res_model': 'account.move', + 'view_mode': 'form', + 'res_id': self.move_id.id, + } diff --git a/fusion_accounting_hr_payroll/models/hr_payslip_line.py b/fusion_accounting_hr_payroll/models/hr_payslip_line.py new file mode 100644 index 00000000..eef19da6 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/hr_payslip_line.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class HrPayslipLine(models.Model): + _inherit = 'hr.payslip.line' + + move_line_id = fields.Many2one( + 'account.move.line', + string='Journal Item', + readonly=True, + copy=False, + ondelete='set null', + index='btree_not_null', + help="Account move line this payslip line was rolled up into " + "(set by the Fusion payroll bridge for traceability).", + ) diff --git a/fusion_accounting_hr_payroll/models/hr_payslip_run.py b/fusion_accounting_hr_payroll/models/hr_payslip_run.py new file mode 100644 index 00000000..ec688847 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/hr_payslip_run.py @@ -0,0 +1,29 @@ +from odoo import _, fields, models + + +class HrPayslipRun(models.Model): + _inherit = 'hr.payslip.run' + + move_id = fields.Many2one( + 'account.move', + string='Batch Accounting Entry', + readonly=True, + copy=False, + ondelete='set null', + ) + move_state = fields.Selection( + related='move_id.state', + string='Move State', + ) + + def action_open_move(self): + self.ensure_one() + if not self.move_id: + return False + return { + 'type': 'ir.actions.act_window', + 'name': _('Journal Entry'), + 'res_model': 'account.move', + 'view_mode': 'form', + 'res_id': self.move_id.id, + } diff --git a/fusion_accounting_hr_payroll/models/hr_salary_rule.py b/fusion_accounting_hr_payroll/models/hr_salary_rule.py new file mode 100644 index 00000000..5f7ad978 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/hr_salary_rule.py @@ -0,0 +1,35 @@ +from odoo import fields, models + + +class HrSalaryRule(models.Model): + _inherit = 'hr.salary.rule' + + account_debit = fields.Many2one( + 'account.account', + string='Debit Account', + company_dependent=True, + ondelete='restrict', + help="GL account debited when this rule's amount is posted " + "(typically expense or asset).", + ) + account_credit = fields.Many2one( + 'account.account', + string='Credit Account', + company_dependent=True, + ondelete='restrict', + help="GL account credited when this rule's amount is posted " + "(typically liability).", + ) + fusion_analytic_account_id = fields.Many2one( + 'account.analytic.account', + string='Analytic Account', + company_dependent=True, + help="Optional analytic account applied to both legs of the move.", + ) + not_computed_in_net = fields.Boolean( + string="Excluded from Net", + default=False, + help="If checked, the result of this rule is excluded from the " + "Net salary line in the journal entry. Set a dedicated " + "debit/credit account so the amount is posted independently.", + ) diff --git a/fusion_accounting_hr_payroll/models/res_company.py b/fusion_accounting_hr_payroll/models/res_company.py new file mode 100644 index 00000000..d221f657 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/res_company.py @@ -0,0 +1,19 @@ +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = 'res.company' + + fusion_payroll_journal_id = fields.Many2one( + 'account.journal', + string='Default Payroll Journal', + domain="[('type', '=', 'general'), ('company_id', '=', id)]", + help="Fallback journal used by the Fusion payroll bridge when a " + "payslip's structure does not define one.", + ) + fusion_payroll_auto_post = fields.Boolean( + string='Auto-post Payroll Entries', + default=False, + help="When enabled, payroll-generated journal entries are posted " + "immediately. Otherwise they remain in draft for review.", + ) diff --git a/fusion_accounting_hr_payroll/models/res_config_settings.py b/fusion_accounting_hr_payroll/models/res_config_settings.py new file mode 100644 index 00000000..53c6b1c3 --- /dev/null +++ b/fusion_accounting_hr_payroll/models/res_config_settings.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + fusion_payroll_journal_id = fields.Many2one( + related='company_id.fusion_payroll_journal_id', + string='Default Payroll Journal', + readonly=False, + ) + fusion_payroll_auto_post = fields.Boolean( + related='company_id.fusion_payroll_auto_post', + string='Auto-post Payroll Entries', + readonly=False, + ) diff --git a/fusion_accounting_hr_payroll/static/description/icon.png b/fusion_accounting_hr_payroll/static/description/icon.png new file mode 100644 index 00000000..6773c627 Binary files /dev/null and b/fusion_accounting_hr_payroll/static/description/icon.png differ diff --git a/fusion_accounting_hr_payroll/tests/__init__.py b/fusion_accounting_hr_payroll/tests/__init__.py new file mode 100644 index 00000000..6c249f5f --- /dev/null +++ b/fusion_accounting_hr_payroll/tests/__init__.py @@ -0,0 +1 @@ +from . import test_payslip_to_move diff --git a/fusion_accounting_hr_payroll/tests/test_payslip_to_move.py b/fusion_accounting_hr_payroll/tests/test_payslip_to_move.py new file mode 100644 index 00000000..b656af0e --- /dev/null +++ b/fusion_accounting_hr_payroll/tests/test_payslip_to_move.py @@ -0,0 +1,108 @@ +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged('post_install', '-at_install') +class TestFusionPayrollBridge(TransactionCase): + """Smoke tests for the Fusion payroll bridge. + + Verifies that the field surface required to replace Enterprise's + ``hr_payroll_account`` is present after the module installs. + Full payslip-to-move integration is exercised in a separate + integration test that needs a seeded payroll structure. + """ + + def test_module_installed(self): + module = self.env['ir.module.module'].sudo().search( + [('name', '=', 'fusion_accounting_hr_payroll')], limit=1, + ) + self.assertTrue(module, "Module record must exist") + self.assertEqual( + module.state, 'installed', + "Module should be in 'installed' state for these tests to run", + ) + + def test_salary_rule_has_account_fields(self): + rule_model = self.env['hr.salary.rule'] + for fname in ( + 'account_debit', + 'account_credit', + 'fusion_analytic_account_id', + 'not_computed_in_net', + ): + self.assertIn( + fname, rule_model._fields, + f"hr.salary.rule must expose '{fname}'", + ) + + def test_payslip_has_move_link(self): + slip_model = self.env['hr.payslip'] + for fname in ('move_id', 'move_state', 'journal_id'): + self.assertIn( + fname, slip_model._fields, + f"hr.payslip must expose '{fname}'", + ) + self.assertTrue( + hasattr(slip_model, '_fusion_create_account_move'), + "hr.payslip must expose the _fusion_create_account_move bridge", + ) + self.assertTrue( + hasattr(slip_model, '_fusion_enterprise_bridge_active'), + "hr.payslip must expose the Enterprise-bridge detector", + ) + + def test_payslip_run_has_move_link(self): + run_model = self.env['hr.payslip.run'] + for fname in ('move_id', 'move_state'): + self.assertIn( + fname, run_model._fields, + f"hr.payslip.run must expose '{fname}'", + ) + + def test_company_payroll_journal_field(self): + co_model = self.env['res.company'] + for fname in ('fusion_payroll_journal_id', 'fusion_payroll_auto_post'): + self.assertIn( + fname, co_model._fields, + f"res.company must expose '{fname}'", + ) + + def test_account_move_back_links(self): + move_model = self.env['account.move'] + for fname in ('payslip_ids', 'payslip_count'): + self.assertIn( + fname, move_model._fields, + f"account.move must expose '{fname}'", + ) + line_model = self.env['account.move.line'] + self.assertIn( + 'payslip_id', line_model._fields, + "account.move.line must expose 'payslip_id'", + ) + + def test_payslip_line_has_move_line_link(self): + line_model = self.env['hr.payslip.line'] + self.assertIn( + 'move_line_id', line_model._fields, + "hr.payslip.line must expose 'move_line_id'", + ) + + def test_enterprise_bridge_detector_returns_bool(self): + slip_model = self.env['hr.payslip'] + self.assertIsInstance( + slip_model._fusion_enterprise_bridge_active(), bool, + ) + + def test_account_journal_payroll_flag(self): + journal_model = self.env['account.journal'] + self.assertIn( + 'is_payroll_journal', journal_model._fields, + "account.journal must expose 'is_payroll_journal'", + ) + + def test_payroll_structure_journal_field(self): + struct_model = self.env['hr.payroll.structure'] + self.assertIn( + 'journal_id', struct_model._fields, + "hr.payroll.structure must expose 'journal_id'", + ) diff --git a/fusion_accounting_hr_payroll/views/account_move_views.xml b/fusion_accounting_hr_payroll/views/account_move_views.xml new file mode 100644 index 00000000..53f98074 --- /dev/null +++ b/fusion_accounting_hr_payroll/views/account_move_views.xml @@ -0,0 +1,24 @@ + + + + account.move.form.fusion.payroll.bridge + account.move + + +
+ + +
+
+
+
diff --git a/fusion_accounting_hr_payroll/views/hr_payroll_structure_views.xml b/fusion_accounting_hr_payroll/views/hr_payroll_structure_views.xml new file mode 100644 index 00000000..d07c7475 --- /dev/null +++ b/fusion_accounting_hr_payroll/views/hr_payroll_structure_views.xml @@ -0,0 +1,15 @@ + + + + hr.payroll.structure.form.fusion.payroll.bridge + hr.payroll.structure + + + + + + + + + + diff --git a/fusion_accounting_hr_payroll/views/hr_payslip_views.xml b/fusion_accounting_hr_payroll/views/hr_payslip_views.xml new file mode 100644 index 00000000..dc5ae527 --- /dev/null +++ b/fusion_accounting_hr_payroll/views/hr_payslip_views.xml @@ -0,0 +1,26 @@ + + + + hr.payslip.form.fusion.payroll.bridge + hr.payslip + + +
+ + + +
+
+
+
diff --git a/fusion_accounting_hr_payroll/views/hr_salary_rule_views.xml b/fusion_accounting_hr_payroll/views/hr_salary_rule_views.xml new file mode 100644 index 00000000..1fe9d49e --- /dev/null +++ b/fusion_accounting_hr_payroll/views/hr_salary_rule_views.xml @@ -0,0 +1,26 @@ + + + + hr.salary.rule.form.fusion.payroll.bridge + hr.salary.rule + + + + + + + + + + + + + + + + + + + diff --git a/fusion_accounting_hr_payroll/views/res_config_settings_views.xml b/fusion_accounting_hr_payroll/views/res_config_settings_views.xml new file mode 100644 index 00000000..97782324 --- /dev/null +++ b/fusion_accounting_hr_payroll/views/res_config_settings_views.xml @@ -0,0 +1,24 @@ + + + + res.config.settings.form.fusion.payroll.bridge + res.config.settings + + + + + + + + + + + + + + +