diff --git a/hr_expense_employee_payment/README.rst b/hr_expense_employee_payment/README.rst new file mode 100644 index 000000000..4ba658db2 --- /dev/null +++ b/hr_expense_employee_payment/README.rst @@ -0,0 +1,151 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============================= +Hr Expense - Employee Payment +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:01267e882ec6fd7a95898e583d214925d9a5b9d4f49bb8c3d49853a763696792 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr--expense-lightgray.png?logo=github + :target: https://github.com/OCA/hr-expense/tree/19.0/hr_expense_employee_payment + :alt: OCA/hr-expense +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-expense-19-0/hr-expense-19-0-hr_expense_employee_payment + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/hr-expense&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the expense functionality so that expenses paid by +employees, which are actual **Vendor Bills**, can be paid to the +employee. + +To do this, an intermediate journal entry is created that transfers the +debt from the supplier to the employee and also modifies the payment +wizard so that the employee receives the payment. Partial payments are +also compatible. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +This module has been created to allow payment to the employee when +supplier invoices are recorded in expenses. + +It will be useful for you if the legislation in your country doesn't +allow to create vendor bills to the employees. + +Configuration +============= + +To configure this module, you need to: + +1. Go to Expenses > Configuration > Settings +2. On Accounting group, set Employee Expense Journal. + +In case you need a different Journal for Intermediate Entries (Ensure +that this journal allows for the creation of 'entry' type moves): + +1. On Accounting group, set Employee Expense Intermediate Journal. + +Usage +===== + +To use this module, you need to: + +1. Go to Expenses > Configuration > Settings and set *Employee Expense + Journal* and *Employee Expense Intermediate Journal* +2. Go to Expenses and create a new one that is paid by Employee (to + reimburse) +3. Submit to Manager +4. Click on Post Journal Entries +5. Select a Journal to register the real Vendor Bill on the Wizard +6. Click on Reset to Draft +7. Modify the Vendor Bill like a normal Vendor Bill (partner, bill ref, + journal, etc) +8. Confirm the Bill +9. Click on Pay and fill the payment for the Employee. Partial payments + are allowed +10. If you make a partial payment or cancel the payment over the Bill, + you can continue the Payment on the Expense +11. On the Expense, you could see a new one intermediate entry on the + Smart Button +12. Finish the payment process if you partially pay by clicking again on + Pay + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Moduon + +Contributors +------------ + +- Eduardo de Miguel (`Moduon `__) +- Rafael Blasco (`Moduon `__) + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-Shide| image:: https://github.com/Shide.png?size=40px + :target: https://github.com/Shide + :alt: Shide +.. |maintainer-rafaelbn| image:: https://github.com/rafaelbn.png?size=40px + :target: https://github.com/rafaelbn + :alt: rafaelbn + +Current `maintainers `__: + +|maintainer-Shide| |maintainer-rafaelbn| + +This module is part of the `OCA/hr-expense `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_expense_employee_payment/__init__.py b/hr_expense_employee_payment/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/hr_expense_employee_payment/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_expense_employee_payment/__manifest__.py b/hr_expense_employee_payment/__manifest__.py new file mode 100644 index 000000000..e019c3276 --- /dev/null +++ b/hr_expense_employee_payment/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2025 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +{ + "name": "Hr Expense - Employee Payment", + "summary": "Allow to pay employees for expense Vendor Bills", + "version": "19.0.1.0.0", + "development_status": "Alpha", + "category": "Human Resources/Expenses", + "website": "https://github.com/OCA/hr-expense", + "author": "Moduon, Odoo Community Association (OCA)", + "maintainers": ["Shide", "rafaelbn"], + "license": "LGPL-3", + "application": False, + "installable": True, + "depends": [ + "hr_expense", + ], + "data": [ + "views/res_config_settings_views.xml", + "views/hr_expense_views.xml", + ], +} diff --git a/hr_expense_employee_payment/i18n/es.po b/hr_expense_employee_payment/i18n/es.po new file mode 100644 index 000000000..22de5a1fe --- /dev/null +++ b/hr_expense_employee_payment/i18n/es.po @@ -0,0 +1,114 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_expense_employee_payment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-06-10 09:58+0000\n" +"PO-Revision-Date: 2026-06-10 12:05+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.9\n" + +#. module: hr_expense_employee_payment +#. odoo-python +#: code:addons/hr_expense_employee_payment/models/hr_expense.py:0 +msgid "%(expense_name)s | Expense conciliation" +msgstr "%(expense_name)s | Conciliación de gastos" + +#. module: hr_expense_employee_payment +#: model_terms:ir.ui.view,arch_db:hr_expense_employee_payment.res_config_settings_view_form +msgid "" +"Accounting journal for intermediate entries to allow pay real Bills to " +"employees. This journal must allow the creation of moves of type 'entry'." +msgstr "" +"Diario para los asientos intermedios que permiten pagar facturas reales " +"a los empleados. Este diario debe permitir la creación de movimientos de " +"tipo 'entry'." + +#. module: hr_expense_employee_payment +#: model:ir.model,name:hr_expense_employee_payment.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_hr_expense__conciliation_move_id +msgid "Conciliation Entry" +msgstr "Asiento de conciliación" + +#. module: hr_expense_employee_payment +#: model:ir.model,name:hr_expense_employee_payment.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de configuración" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_hr_expense__display_name +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_company__display_name +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_config_settings__display_name +msgid "Display Name" +msgstr "Nombre para mostrar" + +#. module: hr_expense_employee_payment +#: model_terms:ir.ui.view,arch_db:hr_expense_employee_payment.res_config_settings_view_form +msgid "Employee Expense Intermediate Journal" +msgstr "Diario Intermedio de Gastos del Empleado" + +#. module: hr_expense_employee_payment +#: model:ir.model,name:hr_expense_employee_payment.model_hr_expense +msgid "Expense" +msgstr "Gasto" + +#. module: hr_expense_employee_payment +#. odoo-python +#: code:addons/hr_expense_employee_payment/models/hr_expense.py:0 +msgid "" +"Expense conciliation entry for '%(expense_name)s' expense and taking in " +"consideration '%(vendor_move_names)s' moves." +msgstr "" +"Asiento de conciliación para los gastos '%(expense_name)s' teniendo en " +"cuenta los movimientos '%(vendor_move_names)s'." + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_hr_expense__id +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_company__id +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_config_settings__id +msgid "ID" +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_company__expense_intermediate_journal_id +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_config_settings__expense_intermediate_journal_id +msgid "Intermediate Expense Journal" +msgstr "Diario Intermedio de Gastos" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,help:hr_expense_employee_payment.field_hr_expense__conciliation_move_id +msgid "" +"Intermediate entry created to reconcile with the vendor bill and payment " +"later." +msgstr "" +"Asiento intermedio creado para conciliar la factura del Vendedor y el " +"pago a posteriori." + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,help:hr_expense_employee_payment.field_res_company__expense_intermediate_journal_id +#: model:ir.model.fields,help:hr_expense_employee_payment.field_res_config_settings__expense_intermediate_journal_id +msgid "" +"Journal used to record intermediate entries for employee expenses.\n" +"If not set, Expense Journal will be used instead." +msgstr "" +"Diario utilizado para guardar los asientos intermedios para los gastos e " +"empleados.\n" +"Si no está establecido, se utilizará el diario de gastos." + +#. module: hr_expense_employee_payment +#: model_terms:ir.ui.view,arch_db:hr_expense_employee_payment.res_config_settings_view_form +msgid "Same as Employee Expense Journal" +msgstr "Igual que el Diario de Gastos del Empleado" diff --git a/hr_expense_employee_payment/i18n/hr_expense_employee_payment.pot b/hr_expense_employee_payment/i18n/hr_expense_employee_payment.pot new file mode 100644 index 000000000..0a3880f0a --- /dev/null +++ b/hr_expense_employee_payment/i18n/hr_expense_employee_payment.pot @@ -0,0 +1,102 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_expense_employee_payment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-06-10 09:58+0000\n" +"PO-Revision-Date: 2026-06-10 09:58+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_expense_employee_payment +#. odoo-python +#: code:addons/hr_expense_employee_payment/models/hr_expense.py:0 +msgid "%(expense_name)s | Expense conciliation" +msgstr "" + +#. module: hr_expense_employee_payment +#: model_terms:ir.ui.view,arch_db:hr_expense_employee_payment.res_config_settings_view_form +msgid "" +"Accounting journal for intermediate entries to allow pay real Bills to " +"employees. This journal must allow the creation of moves of type 'entry'." +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model,name:hr_expense_employee_payment.model_res_company +msgid "Companies" +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_hr_expense__conciliation_move_id +msgid "Conciliation Entry" +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model,name:hr_expense_employee_payment.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_hr_expense__display_name +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_company__display_name +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_config_settings__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_expense_employee_payment +#: model_terms:ir.ui.view,arch_db:hr_expense_employee_payment.res_config_settings_view_form +msgid "Employee Expense Intermediate Journal" +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model,name:hr_expense_employee_payment.model_hr_expense +msgid "Expense" +msgstr "" + +#. module: hr_expense_employee_payment +#. odoo-python +#: code:addons/hr_expense_employee_payment/models/hr_expense.py:0 +msgid "" +"Expense conciliation entry for '%(expense_name)s' expense and taking in " +"consideration '%(vendor_move_names)s' moves." +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_hr_expense__id +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_company__id +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_config_settings__id +msgid "ID" +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_company__expense_intermediate_journal_id +#: model:ir.model.fields,field_description:hr_expense_employee_payment.field_res_config_settings__expense_intermediate_journal_id +msgid "Intermediate Expense Journal" +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,help:hr_expense_employee_payment.field_hr_expense__conciliation_move_id +msgid "" +"Intermediate entry created to reconcile with the vendor bill and payment " +"later." +msgstr "" + +#. module: hr_expense_employee_payment +#: model:ir.model.fields,help:hr_expense_employee_payment.field_res_company__expense_intermediate_journal_id +#: model:ir.model.fields,help:hr_expense_employee_payment.field_res_config_settings__expense_intermediate_journal_id +msgid "" +"Journal used to record intermediate entries for employee expenses.\n" +"If not set, Expense Journal will be used instead." +msgstr "" + +#. module: hr_expense_employee_payment +#: model_terms:ir.ui.view,arch_db:hr_expense_employee_payment.res_config_settings_view_form +msgid "Same as Employee Expense Journal" +msgstr "" diff --git a/hr_expense_employee_payment/models/__init__.py b/hr_expense_employee_payment/models/__init__.py new file mode 100644 index 000000000..60b22f4ee --- /dev/null +++ b/hr_expense_employee_payment/models/__init__.py @@ -0,0 +1,4 @@ +from . import res_company +from . import res_config_settings +from . import hr_expense +from . import account_move diff --git a/hr_expense_employee_payment/models/account_move.py b/hr_expense_employee_payment/models/account_move.py new file mode 100644 index 000000000..191a83f7c --- /dev/null +++ b/hr_expense_employee_payment/models/account_move.py @@ -0,0 +1,31 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo import models +from odoo.fields import Domain + + +class AccountMove(models.Model): + _inherit = "account.move" + + def action_force_register_payment(self): + # Check if there are any Expense Vendor Moves in order to modify normal workflow + expense_vendor_moves_domain = Domain.AND( + [ + Domain("expense_ids", "!=", False), + self.env["hr.expense"]._get_vendor_moves_domain(), + ] + ) + expense_moves = self.filtered_domain(expense_vendor_moves_domain) + if not expense_moves: + return super().action_force_register_payment() + + # Other Payable lines + other_moves = self - expense_moves + payable_lines = other_moves.line_ids + # Payable Employee Expense lines + expense_moves.expense_ids._create_conciliation_moves(auto_reconcile=True) + payable_lines |= expense_moves.expense_ids._get_conciliation_payable_move_lines( + for_employee=True + ) + return payable_lines.action_register_payment() diff --git a/hr_expense_employee_payment/models/hr_expense.py b/hr_expense_employee_payment/models/hr_expense.py new file mode 100644 index 000000000..7b9899676 --- /dev/null +++ b/hr_expense_employee_payment/models/hr_expense.py @@ -0,0 +1,202 @@ +# Copyright 2025 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo import Command, api, fields, models +from odoo.tools import float_is_zero + + +class HrExpense(models.Model): + _inherit = "hr.expense" + + conciliation_move_id = fields.Many2one( + comodel_name="account.move", + string="Conciliation Entry", + help="Intermediate entry created to reconcile with the vendor " + "bill and payment later.", + ) + + @api.model + def _get_vendor_moves_domain(self): + """Return the domain to get the vendor moves linked to the expense.""" + return fields.Domain("move_type", "=", "in_invoice") + + def _get_vendor_move(self): + """Get the vendor move linked to the expense.""" + vendor_moves = self.env["account.move"].browse() + vendor_moves_domain = self._get_vendor_moves_domain() + for expense in self: + if expense.account_move_id.filtered_domain(vendor_moves_domain): + vendor_moves |= expense.account_move_id + return vendor_moves + + def _get_vendor_payable_move_lines(self): + """Get the payable move lines from the vendor bills linked to + selected expense sheets that has *something to be paid*.""" + return self._get_vendor_move().line_ids.filtered( + lambda line: line.display_type == "payment_term" + and not float_is_zero( + line.amount_residual, precision_rounding=line.currency_id.rounding + ) + ) + + def _prepare_conciliation_move_vals(self): + """Prepare the values for the conciliation entry move to + reconcile with the vendor bill and payment later.""" + result = [] + for expense in self: + employee_p = expense.employee_id.sudo().work_contact_id + company = expense.company_id + exp_intermediate_journal = ( + company.expense_intermediate_journal_id + or company.expense_journal_id + or expense.journal_id + ) + vendor_payable_move_lines = expense._get_vendor_payable_move_lines() + vendor_date = vendor_payable_move_lines.move_id[:1].date + result.append( + { + "company_id": company.id, + "journal_id": exp_intermediate_journal.id, + "ref": self.env._( + "%(expense_name)s | Expense conciliation", + expense_name=expense.name, + ), + "narration": self.env._( + "Expense conciliation entry for " + "'%(expense_name)s' expense " + "and taking in consideration '%(vendor_move_names)s' moves.", + expense_name=expense.name, + vendor_move_names=", ".join( + vendor_payable_move_lines.mapped("move_id.name") + ), + ), + "move_type": "entry", + "date": vendor_date, + "currency_id": expense.currency_id.id, + "line_ids": [ + Command.create( + { + "name": expense.name, + "partner_id": employee_p.id, + "account_id": employee_p.property_account_payable_id.id, + "debit": 0.0, + "credit": expense.amount_residual, + } + ), + *[ + Command.create( + { + "name": f"{payable_move_line.move_id.name} " + f"({payable_move_line.move_id.ref})", + "partner_id": payable_move_line.partner_id.id, + "account_id": payable_move_line.account_id.id, + "debit": -payable_move_line.amount_residual, + "credit": 0.0, + } + ) + for payable_move_line in vendor_payable_move_lines + ], + ], + } + ) + + return result + + def _get_conciliation_payable_move_lines(self, for_employee=False): + """Obtain the posted payable move lines from the conciliation entries + for selected expense sheets. + + :param for_employee: True: Lines for the employee. False: Lines for the vendor. + :return: A recordset of account.move.line matching conciliation payable lines. + """ + conciliation_payable_lines = self.env["account.move.line"].browse() + for expense in self.filtered("conciliation_move_id"): + if for_employee: + partner = expense.employee_id.sudo().work_contact_id # Employee + else: + partner = expense._get_vendor_move().partner_id # Vendor + amount_field = "credit" if for_employee else "debit" + conciliation_payable_lines |= ( + expense.conciliation_move_id.line_ids.filtered_domain( + [ + ("partner_id", "=", partner.id), + (amount_field, ">", 0.0), + ] + ) + ) + return conciliation_payable_lines + + def _reconcile_conciliation_move_with_vendor_bills(self): + """Reconcile the Conciliation Entry and the Vendor Bill.""" + for expense in self.filtered("conciliation_move_id"): + if expense.conciliation_move_id.state == "draft": + expense.conciliation_move_id.sudo().action_post() + # Select the Vendor lines to reconcile + conciliation_vendor_payable_move_lines = ( + expense._get_conciliation_payable_move_lines(for_employee=False) + ) + vendor_payable_move_lines = expense._get_vendor_payable_move_lines() + # Check Vendor Bills that needs to be reconciled + if conciliation_vendor_payable_move_lines and vendor_payable_move_lines: + lines_to_reconcile = ( + conciliation_vendor_payable_move_lines | vendor_payable_move_lines + ) + lines_to_reconcile.reconcile() + + def _create_conciliation_moves(self, auto_reconcile=True): + """Create an intermediate entry to reconcile with the vendor bills + and payment later. + + :param auto_reconcile: Automatically reconcile the conciliation moves + with the vendor bills. + :return: A recordset of account.move with the created conciliation moves. + """ + conciliation_moves = self.env["account.move"].browse() + for expense in self: + if not expense.conciliation_move_id: + expense.conciliation_move_id = ( + self.env["account.move"] + .sudo() + .create(expense._prepare_conciliation_move_vals()) + ) + conciliation_moves |= expense.conciliation_move_id + if auto_reconcile: + self._reconcile_conciliation_move_with_vendor_bills() + return conciliation_moves + + def action_open_conciliation_move(self): + """Open the conciliation entry created for the expense.""" + self.ensure_one() + return { + "type": "ir.actions.act_window", + "res_model": "account.move", + "name": self.conciliation_move_id.name, + "view_mode": "form", + "res_id": self.conciliation_move_id.id, + "views": [(False, "form")], + } + + @api.depends("conciliation_move_id.line_ids.reconciled") + def _compute_state(self): + res = super()._compute_state() + for expense in self: + if not expense.conciliation_move_id or not expense.account_move_id: + continue + conciliation_employee_payable_move_lines = ( + expense._get_conciliation_payable_move_lines( + for_employee=True, + ) + ) + if all(conciliation_employee_payable_move_lines.mapped("reconciled")): + expense.state = "paid" + else: + # Conciliation entry exists but is not fully paid + expense.state = "in_payment" + return res + + def action_pay_employee(self): + """Register employee payment""" + conciliation_employee_payable_move_lines = ( + self._get_conciliation_payable_move_lines(for_employee=True) + ) + return conciliation_employee_payable_move_lines.action_register_payment() diff --git a/hr_expense_employee_payment/models/res_company.py b/hr_expense_employee_payment/models/res_company.py new file mode 100644 index 000000000..39c67dd24 --- /dev/null +++ b/hr_expense_employee_payment/models/res_company.py @@ -0,0 +1,17 @@ +# Copyright 2025 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + expense_intermediate_journal_id = fields.Many2one( + comodel_name="account.journal", + string="Intermediate Expense Journal", + check_company=True, + domain=[("type", "=", "general")], + help="Journal used to record intermediate entries for employee expenses.\n" + "If not set, Expense Journal will be used instead.", + ) diff --git a/hr_expense_employee_payment/models/res_config_settings.py b/hr_expense_employee_payment/models/res_config_settings.py new file mode 100644 index 000000000..08d38edda --- /dev/null +++ b/hr_expense_employee_payment/models/res_config_settings.py @@ -0,0 +1,16 @@ +# Copyright 2025 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + expense_intermediate_journal_id = fields.Many2one( + comodel_name="account.journal", + related="company_id.expense_intermediate_journal_id", + readonly=False, + check_company=True, + domain="[('type', '=', 'general')]", + ) diff --git a/hr_expense_employee_payment/pyproject.toml b/hr_expense_employee_payment/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/hr_expense_employee_payment/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/hr_expense_employee_payment/readme/CONFIGURE.md b/hr_expense_employee_payment/readme/CONFIGURE.md new file mode 100644 index 000000000..46b0367e8 --- /dev/null +++ b/hr_expense_employee_payment/readme/CONFIGURE.md @@ -0,0 +1,8 @@ +To configure this module, you need to: + +1. Go to Expenses > Configuration > Settings +1. On Accounting group, set Employee Expense Journal. + +In case you need a different Journal for Intermediate Entries (Ensure that this journal allows for the creation of 'entry' type moves): + +1. On Accounting group, set Employee Expense Intermediate Journal. diff --git a/hr_expense_employee_payment/readme/CONTEXT.md b/hr_expense_employee_payment/readme/CONTEXT.md new file mode 100644 index 000000000..6cde4ef0b --- /dev/null +++ b/hr_expense_employee_payment/readme/CONTEXT.md @@ -0,0 +1,3 @@ +This module has been created to allow payment to the employee when supplier invoices are recorded in expenses. + +It will be useful for you if the legislation in your country doesn't allow to create vendor bills to the employees. diff --git a/hr_expense_employee_payment/readme/CONTRIBUTORS.md b/hr_expense_employee_payment/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..0ca1be35b --- /dev/null +++ b/hr_expense_employee_payment/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Eduardo de Miguel ([Moduon](https://www.moduon.team/)) +- Rafael Blasco ([Moduon](https://www.moduon.team/)) diff --git a/hr_expense_employee_payment/readme/DESCRIPTION.md b/hr_expense_employee_payment/readme/DESCRIPTION.md new file mode 100644 index 000000000..8fca38030 --- /dev/null +++ b/hr_expense_employee_payment/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module extends the expense functionality so that expenses paid by employees, +which are actual **Vendor Bills**, can be paid to the employee. + +To do this, an intermediate journal entry is created that transfers the debt +from the supplier to the employee and also modifies the payment wizard so that +the employee receives the payment. Partial payments are also compatible. diff --git a/hr_expense_employee_payment/readme/USAGE.md b/hr_expense_employee_payment/readme/USAGE.md new file mode 100644 index 000000000..34bbf0a00 --- /dev/null +++ b/hr_expense_employee_payment/readme/USAGE.md @@ -0,0 +1,14 @@ +To use this module, you need to: + +1. Go to Expenses > Configuration > Settings and set *Employee Expense Journal* and *Employee Expense Intermediate Journal* +1. Go to Expenses and create a new one that is paid by Employee (to reimburse) +1. Submit to Manager +1. Click on Post Journal Entries +1. Select a Journal to register the real Vendor Bill on the Wizard +1. Click on Reset to Draft +1. Modify the Vendor Bill like a normal Vendor Bill (partner, bill ref, journal, etc) +1. Confirm the Bill +1. Click on Pay and fill the payment for the Employee. Partial payments are allowed +1. If you make a partial payment or cancel the payment over the Bill, you can continue the Payment on the Expense +1. On the Expense, you could see a new one intermediate entry on the Smart Button +1. Finish the payment process if you partially pay by clicking again on Pay diff --git a/hr_expense_employee_payment/static/description/icon.png b/hr_expense_employee_payment/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/hr_expense_employee_payment/static/description/icon.png differ diff --git a/hr_expense_employee_payment/static/description/index.html b/hr_expense_employee_payment/static/description/index.html new file mode 100644 index 000000000..0bcecea39 --- /dev/null +++ b/hr_expense_employee_payment/static/description/index.html @@ -0,0 +1,492 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Hr Expense - Employee Payment

+ +

Alpha License: LGPL-3 OCA/hr-expense Translate me on Weblate Try me on Runboat

+

This module extends the expense functionality so that expenses paid by +employees, which are actual Vendor Bills, can be paid to the +employee.

+

To do this, an intermediate journal entry is created that transfers the +debt from the supplier to the employee and also modifies the payment +wizard so that the employee receives the payment. Partial payments are +also compatible.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Use Cases / Context

+

This module has been created to allow payment to the employee when +supplier invoices are recorded in expenses.

+

It will be useful for you if the legislation in your country doesn’t +allow to create vendor bills to the employees.

+
+
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Go to Expenses > Configuration > Settings
  2. +
  3. On Accounting group, set Employee Expense Journal.
  4. +
+

In case you need a different Journal for Intermediate Entries (Ensure +that this journal allows for the creation of ‘entry’ type moves):

+
    +
  1. On Accounting group, set Employee Expense Intermediate Journal.
  2. +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to Expenses > Configuration > Settings and set Employee Expense +Journal and Employee Expense Intermediate Journal
  2. +
  3. Go to Expenses and create a new one that is paid by Employee (to +reimburse)
  4. +
  5. Submit to Manager
  6. +
  7. Click on Post Journal Entries
  8. +
  9. Select a Journal to register the real Vendor Bill on the Wizard
  10. +
  11. Click on Reset to Draft
  12. +
  13. Modify the Vendor Bill like a normal Vendor Bill (partner, bill ref, +journal, etc)
  14. +
  15. Confirm the Bill
  16. +
  17. Click on Pay and fill the payment for the Employee. Partial payments +are allowed
  18. +
  19. If you make a partial payment or cancel the payment over the Bill, +you can continue the Payment on the Expense
  20. +
  21. On the Expense, you could see a new one intermediate entry on the +Smart Button
  22. +
  23. Finish the payment process if you partially pay by clicking again on +Pay
  24. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Moduon
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

Shide rafaelbn

+

This module is part of the OCA/hr-expense project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/hr_expense_employee_payment/tests/__init__.py b/hr_expense_employee_payment/tests/__init__.py new file mode 100644 index 000000000..29f89d8f3 --- /dev/null +++ b/hr_expense_employee_payment/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_expense diff --git a/hr_expense_employee_payment/tests/test_hr_expense.py b/hr_expense_employee_payment/tests/test_hr_expense.py new file mode 100644 index 000000000..6afe485e5 --- /dev/null +++ b/hr_expense_employee_payment/tests/test_hr_expense.py @@ -0,0 +1,145 @@ +# Copyright 2025 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + + +from odoo.tests import tagged + +from odoo.addons.hr_expense.tests.common import TestExpenseCommon + + +@tagged("-at_install", "post_install") +class TestHrExpenseEmployeePayment(TestExpenseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_one_expense_two_payments(self): + """Test paying one expense with two payment""" + # Create and submit expense + expense = self.create_expenses([{"payment_mode": "own_account"}]) + self.assertEqual(expense.state, "draft") + expense.action_submit() + self.assertEqual(expense.state, "submitted") + expense.action_approve() + self.assertEqual(expense.state, "approved") + # Create Expense Vendor Bill + self.post_expenses_with_wizard(expense) + self.assertEqual(expense.account_move_id.state, "posted") + # Change Vendor Bill to in_invoice + expense.account_move_id.button_draft() + expense.account_move_id.move_type = "in_invoice" + expense.account_move_id.partner_id = self.partner_a.id + expense.account_move_id.action_post() + self.assertEqual(expense.account_move_id.state, "posted") + self.assertEqual(expense.state, "posted") + # Generate Conciliation Entry, but don't pay yet + expense.account_move_id.action_register_payment() + self.assertTrue(expense.conciliation_move_id) + self.assertEqual(expense.state, "in_payment") + # Check vendor Bill has been reconciled with the conciliation entry + vendor_payable_move_line = expense._get_conciliation_payable_move_lines( + for_employee=False + ) + self.assertTrue(vendor_payable_move_line.reconciled) + # Pay from Expense: Payment 1 + payment_wizard_action = expense.action_pay_employee() + payment_wizard = ( + self.env[payment_wizard_action["res_model"]] + .with_context(**payment_wizard_action["context"]) + .create( + { + "amount": 600.0, + "journal_id": self.company_data["default_journal_bank"].id, + } + ) + ) + self.assertEqual( + payment_wizard.partner_bank_id, + self.expense_employee.sudo().bank_account_ids, + ) + payment_wizard.action_create_payments() + self.assertEqual(expense.state, "in_payment") + # Pay from Expense: Payment 2 + payment_wizard_action = expense.action_pay_employee() + payment_wizard = ( + self.env[payment_wizard_action["res_model"]] + .with_context(**payment_wizard_action["context"]) + .create( + { + "amount": 400.0, + "journal_id": self.company_data["default_journal_bank"].id, + } + ) + ) + self.assertEqual( + payment_wizard.partner_bank_id, + self.expense_employee.sudo().bank_account_ids, + ) + payment_wizard.action_create_payments() + self.assertEqual(expense.state, "paid") + + def test_pay_expense_and_vendor_bill(self): + """Test paying one expense with two payment""" + # Create and submit expense + expense = self.create_expenses([{"payment_mode": "own_account"}]) + expense.action_submit() + expense.action_approve() + # Create Expense Vendor Bill + self.post_expenses_with_wizard(expense) + # Change Vendor Bill to in_invoice + expense.account_move_id.button_draft() + expense.account_move_id.move_type = "in_invoice" + expense.account_move_id.partner_id = self.partner_a.id + expense.account_move_id.action_post() + self.assertEqual(expense.account_move_id.state, "posted") + self.assertEqual(expense.state, "posted") + # Create normal Bill (no Expense) + normal_bill = self.init_invoice( + "in_invoice", + partner=self.partner_a, + products=[self.product_a], + # amounts=[600.0], + post=True, + ) + # Pay both Bills at once + payment_wizard_action = ( + expense.account_move_id | normal_bill + ).action_force_register_payment() + payment_wizard = ( + self.env[payment_wizard_action["res_model"]] + .with_context(**payment_wizard_action["context"]) + .create( + { + "amount": normal_bill.amount_total + expense.total_amount, + "journal_id": self.company_data["default_journal_bank"].id, + } + ) + ) + payment_wizard.action_create_payments() + # Check Expense + self.assertEqual(expense.state, "paid") + self.assertTrue(expense.conciliation_move_id) + expense_vendor_payable_move_line = expense._get_conciliation_payable_move_lines( + for_employee=False + ) + self.assertTrue(expense_vendor_payable_move_line.reconciled) + expense_employee_payable_move_line = ( + expense._get_conciliation_payable_move_lines(for_employee=True) + ) + self.assertTrue(expense_employee_payable_move_line.reconciled) + # Check normal Bill + normal_bill_payable_move_line = normal_bill.line_ids.filtered_domain( + [ + ("display_type", "=", "payment_term"), + ] + ) + self.assertTrue(normal_bill_payable_move_line.reconciled) + # Check Bills and Expenses are not conciliated between each other + self.assertEqual( + len( + expense_employee_payable_move_line.full_reconcile_id + | expense_vendor_payable_move_line.full_reconcile_id + | normal_bill_payable_move_line.full_reconcile_id + ), + 3, + ) diff --git a/hr_expense_employee_payment/views/hr_expense_views.xml b/hr_expense_employee_payment/views/hr_expense_views.xml new file mode 100644 index 000000000..931466013 --- /dev/null +++ b/hr_expense_employee_payment/views/hr_expense_views.xml @@ -0,0 +1,35 @@ + + + + hr.expense.view.form + hr.expense + + + + + + +