Skip to content

[MIG] hr_expense_invoice: Migration to 19.0#329

Open
cav-adhoc wants to merge 62 commits intoOCA:19.0from
adhoc-dev:19.0-mig-hr_expense_invoice
Open

[MIG] hr_expense_invoice: Migration to 19.0#329
cav-adhoc wants to merge 62 commits intoOCA:19.0from
adhoc-dev:19.0-mig-hr_expense_invoice

Conversation

@cav-adhoc
Copy link
Contributor

No description provided.

pedrobaeza and others added 30 commits March 4, 2026 17:09
Set supplier invoices on HR expenses
====================================

This module should be used when a supplier invoice is paid by an employee. It
allows to set  a supplier invoice for each expense line, adding the
corresponding journal items to transfer the debt to the employee.

Installation
============

Install the module the regular way.

Configuration
=============

You don't need to configure anything more to use this module.

Usage
=====

Instead of coding a full expense line, select an existing supplier invoice,
and then the rest of the fields will be auto-filled and grayed.

When you generate the expenses account entries, lines with invoices filled
will be generated as opposite of the payable move line of the invoice, and
both will be reconciled, letting the employee payable account as the only
open balance.

Known issues / Roadmap
======================

* Multiple payment terms for a supplier invoice are not handled correctly.
* Partial reconcile supplier invoices are also not correctly handled.

OCA Transbot updated translations from Transifex
…se view

OCA Transbot updated translations from Transifex
…l amount (OCA#237)

On the same expense, when we have 2 or more lines with different invoices, and each invoices have the same total amount, reconcile is not possible.

The fix is to exclude reconcile account.move.line, and the first time if we have more than one line to reconcile on the same amount, we keep the first.

OCA Transbot updated translations from Transifex
OCA Transbot updated translations from Transifex
[UPD] Update hr_expense_invoice.pot
Currently translated at 100.0% (4 of 4 strings)

Translation: hr-11.0/hr-11.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-11-0/hr-11-0-hr_expense_invoice/de/

Update translation files

Updated by Update PO files to match POT (msgmerge) hook in Weblate.
[UPD] Update hr_expense_invoice.pot

Update translation files

Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: hr-12.0/hr-12.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-12-0/hr-12-0-hr_expense_invoice/
Add expense info to invoice info or create/edit
From expense sheet, add action "Create Invoice" from multiple expenses
Change the way reference invoice_id is checked.
- No more onchange invoice_id that set values to expense
- Instead, check amount on expense and invoice during post entry
- Change the way to allow reconcile with > 2 account move lines
Currently translated at 100.0% (4 of 4 strings)

Translation: hr-12.0/hr-12.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-12-0/hr-12-0-hr_expense_invoice/de/
Currently translated at 100.0% (4 of 4 strings)

Translation: hr-12.0/hr-12.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-12-0/hr-12-0-hr_expense_invoice/es/

[UPD] README.rst

[UPD] Update hr_expense_invoice.pot

[UPD] README.rst

hr_expense_invoice 12.0.1.3.0

Update translation files

Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: hr-12.0/hr-12.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-12-0/hr-12-0-hr_expense_invoice/
hr_expense_invoice 12.0.1.3.1
[UPD] Update hr_expense_invoice.pot

Update translation files

Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: hr-expense-13.0/hr-expense-13.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-13-0/hr-expense-13-0-hr_expense_invoice/

Update translation files

Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: hr-expense-13.0/hr-expense-13.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-13-0/hr-expense-13-0-hr_expense_invoice/
If not, the total amount of the expense won't match to invoices
with taxes.

[UPD] README.rst
… partners.

- Intercept properly the sheet paid action for not marking it as paid when
  reconciling the expense lines that belongs to invoices.
- Prevent set invoices paid when sheet paid by company.
- Some docstring.

Co-authored-by: Pedro M. Baeza <[email protected]>
Co-authored-by: Víctor Martínez <[email protected]>

hr_expense_invoice 13.0.1.1.2

[UPD] Update hr_expense_invoice.pot

Update translation files

Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: hr-expense-13.0/hr-expense-13.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-13-0/hr-expense-13-0-hr_expense_invoice/
…ing to sheet

hr_expense_invoice 13.0.1.2.0

[UPD] Update hr_expense_invoice.pot

Update translation files

Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: hr-expense-13.0/hr-expense-13.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-13-0/hr-expense-13-0-hr_expense_invoice/
…ted with expense and improve domain to prevent set same invoice in different expenses.
…e is set)

hr_expense_invoice 13.0.1.3.0

[UPD] Update hr_expense_invoice.pot

Update translation files

Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: hr-expense-13.0/hr-expense-13.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-13-0/hr-expense-13-0-hr_expense_invoice/
LoisRForgeFlow and others added 26 commits March 4, 2026 17:09
Hide quantity label and UoM field when selecting an invoice, as
it is done with quantity field.

hr_expense_invoice 15.0.1.1.0
… to use amount_total from invoices.

If residual amount from invoice is not the same, it should not show an error.

TT40175

hr_expense_invoice 15.0.1.1.1
Translated using Weblate (Italian)

Currently translated at 100.0% (15 of 15 strings)

Translation: hr-expense-15.0/hr-expense-15.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-15-0/hr-expense-15-0-hr_expense_invoice/it/

[UPD] README.rst

Update translation files

Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: hr-expense-15.0/hr-expense-15.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-15-0/hr-expense-15-0-hr_expense_invoice/
Currently translated at 100.0% (15 of 15 strings)

Translation: hr-expense-15.0/hr-expense-15.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-15-0/hr-expense-15-0-hr_expense_invoice/it/
Currently translated at 100.0% (15 of 15 strings)

Translation: hr-expense-15.0/hr-expense-15.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-15-0/hr-expense-15-0-hr_expense_invoice/es/
Currently translated at 100.0% (15 of 15 strings)

Translation: hr-expense-16.0/hr-expense-16.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-16-0/hr-expense-16-0-hr_expense_invoice/it/
- The resulting entries after posting were not correct. Now, this is
  handled this way:

  * Paid by the company: no entry is created, as the invoice represents
    the AP balance. The expense sheet payment state is controlled
    through the linked invoices payment states.
  * Paid by employee: the vendor bill that core creates is voided, and
    instead, a journal entry transferring the AP balance to the employee
    is created against the original vendor balance, that is reconciled
    and given as paid.

- When an invoice is selected in the expense, the interface was not
  properly adapted, avoiding to change certain data, and the
  corresponding were not filled as well. Now both things are performed.
- Removed the non-sense register payment constraint.
- Added smart-button to navigate from invoice to expense, as there's
  no visible link in that direction.
- Avoid the need of the field `with_invoice_id`, as it was simply a
  question of putting properly the other field as invisible and without
  groups (apart from the already existing one with groups).
- Payment state and residual amount controlled correctly for these new
  cases.
- Tests cleanup, adapted (they were manipulated in some cases to match
  the weird previous result, like in payment states) and redundancies
  were removed.

TT49143
Currently translated at 100.0% (17 of 17 strings)

Translation: hr-expense-16.0/hr-expense-16.0-hr_expense_invoice
Translate-URL: https://translation.odoo-community.org/projects/hr-expense-16-0/hr-expense-16-0-hr_expense_invoice/it/
Actions performed:
- Deprecated `hr.expense.reference` field as part of [1].
- Merged the `hr.expense.is_editable` and `hr.expense.sheet_is_editable`
  fields as part of [1].
- Renamed `hr.expense.unit_amount` to `hr.expense.price_unit`.
- Renamed `hr.expense.attachment_number` to `hr.expense.nb_attachment`.
- Renamed `hr.expense.approve_expense_sheets()` to
  `hr.expense.action_approve_expense_sheets()`.
- Renamed `hr.expense.account_move_id` to `hr.expense.account_move_ids`.
- Renamed `hr.expense.total_amount` to
  `hr.expense.total_amount_currency`.
- Inherit compute of `hr.expense.sheet.state` to consider expenses with
  linked invoices.
- Switched from old %-based string formatting to f-strings.
- Replaced the use of `attrs` in views with their equivalent Python
  expressions as part of [2].
- Adapted the `hr.expense.sheet` records creation in tests.
- Updated to use the employee partner from `hr.employee.work_contact_id`
  instead of `hr.employee.address_home_id` since it has been split into
  multiple fields.
- Added the `hr.expense.amount_residual` field to maintain the one
  invoice per expense functionality.
- Adapted test assertions for `hr.expense.sheet.state` since it is now
  a computed field.

Co-authored-by: desdelinux <[email protected]>

[1]: odoo/odoo#130244
[2]: odoo/odoo#104741
If an expense report includes invoices from different providers,
when posting move the report moves to "paid" and invoices are not
reconciled as paid, as it fails to reconcile the lines with the
transfer moves.

Use the correct partner, from current expense line from the report,
not always the same partner from first expense line.

Fixes OCA#273
…ot all

being postable when invoices are in draft
Odoo is handling the amount always tax included, so we need to transfer
to the invoice the proper base amount, not the price unit.
Copilot AI review requested due to automatic review settings March 4, 2026 20:10
@cav-adhoc cav-adhoc mentioned this pull request Mar 4, 2026
7 tasks
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the hr_expense_invoice addon to Odoo 19.0, reintroducing/adjusting the ability to link HR expenses to vendor bills and handle the related accounting flows (including smart buttons and updated views), with accompanying tests and regenerated documentation.

Changes:

  • Add/adjust model logic to link expenses ↔ vendor bills, create transfer journal entries, and compute payment/state behavior accordingly.
  • Update UI views for HR Expense / Expense Sheet and Account Move (smart buttons, fields, and actions).
  • Add automated tests and refresh addon documentation/readme artifacts and translations.

Reviewed changes

Copilot reviewed 28 out of 29 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
hr_expense_invoice/models/hr_expense_sheet.py Posting/state/payment logic for sheets with linked invoices; creation of AP transfer moves; validations and workflow hooks.
hr_expense_invoice/models/hr_expense.py Adds invoice_id, transfer move linkage, residual tracking, and onchange/compute adjustments when an invoice is linked.
hr_expense_invoice/models/account_move.py Adds reverse linkage from invoices to expenses and smart action(s); adjusts reconcile-related behavior.
hr_expense_invoice/views/hr_expense_views.xml Expense and sheet form/list view inheritances to expose invoice linkage and actions.
hr_expense_invoice/views/account_move_views.xml Adds an expense smart button on vendor bills/journal entries.
hr_expense_invoice/tests/test_hr_expense_invoice.py Coverage for invoice/no-invoice flows, multi-invoice, constraints, and accounting entry behavior.
hr_expense_invoice/static/description/*, hr_expense_invoice/README.rst, hr_expense_invoice/readme/* Generated documentation and module description assets.
hr_expense_invoice/i18n/* Translation templates and language files updated/added.
hr_expense_invoice/__manifest__.py, __init__.py, models/__init__.py, tests/__init__.py Module scaffolding and registration.
hr_expense_invoice/pyproject.toml Build-system metadata for packaging.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +161 to +184
@api.depends()
def _compute_state(self):
"""Set proper state according to linked invoices."""
sheets_with_invoices = self.filtered(
lambda sheet: all(
expense.invoice_id and expense.invoice_id.state == "posted"
for expense in sheet.expense_line_ids
)
and sheet.state == sheet.approval_state
)
company_account_sheets = sheets_with_invoices.filtered(
lambda sheet: sheet.payment_mode == "company_account"
)
company_account_sheets.state = "done"
sheets_with_paid_invoices = (
sheets_with_invoices - company_account_sheets
).filtered(
lambda sheet: all(
expense.invoice_id.payment_state != "not_paid"
for expense in sheet.expense_line_ids
)
)
sheets_with_paid_invoices.state = "post"
return super(HrExpenseSheet, self - sheets_with_invoices)._compute_state()
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_compute_state() is redecorated with @api.depends() with no dependencies. If state is a stored computed field (as in core hr.expense.sheet), this removes the upstream dependencies and can prevent recomputation when invoice/payment fields change, leaving sheets stuck in the wrong state. Keep/extend the same dependency list as the parent implementation instead of an empty @api.depends().

Copilot uses AI. Check for mistakes.
Comment on lines +221 to +225
raise UserError(
self.env._(
"You cannot create accounting entries for an expense \
report without expenses."
)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several translatable error strings use backslash line continuations inside quoted strings (e.g. "expense \n report..."), which produces messages with large runs of spaces and leaks those spaces into the .pot/.po files. Prefer implicit string concatenation or a single-line string so the user-facing message is clean and translations don’t include formatting artifacts.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +49
sheets_all_inovices = self.get_expense_sheets_with_invoices(all)
res = super(HrExpenseSheet, self - sheets_all_inovices).action_sheet_move_post()
# Use 'any' here because there may be mixed sheets
# and we have to create ap moves for those invoices
for expense in self.get_expense_sheets_with_invoices(any):
expense._validate_expense_invoice()
expense._check_can_create_move()
expense._do_create_ap_moves()
# The payment state is set in a fixed way in super, but it depends on the
# payment state of the invoices when there are some of them linked
expense.filtered(
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: sheets_all_inovices is misspelled. Rename it to sheets_all_invoices (and consider renaming the loop variable expense to sheet since it iterates over sheets).

Suggested change
sheets_all_inovices = self.get_expense_sheets_with_invoices(all)
res = super(HrExpenseSheet, self - sheets_all_inovices).action_sheet_move_post()
# Use 'any' here because there may be mixed sheets
# and we have to create ap moves for those invoices
for expense in self.get_expense_sheets_with_invoices(any):
expense._validate_expense_invoice()
expense._check_can_create_move()
expense._do_create_ap_moves()
# The payment state is set in a fixed way in super, but it depends on the
# payment state of the invoices when there are some of them linked
expense.filtered(
sheets_all_invoices = self.get_expense_sheets_with_invoices(all)
res = super(HrExpenseSheet, self - sheets_all_invoices).action_sheet_move_post()
# Use 'any' here because there may be mixed sheets
# and we have to create ap moves for those invoices
for sheet in self.get_expense_sheets_with_invoices(any):
sheet._validate_expense_invoice()
sheet._check_can_create_move()
sheet._do_create_ap_moves()
# The payment state is set in a fixed way in super, but it depends on the
# payment state of the invoices when there are some of them linked
sheet.filtered(

Copilot uses AI. Check for mistakes.
def _compute_tax_ids(self):
with_invoice = self.filtered("invoice_id")
for record in with_invoice:
record.tax_ids = [(5,)]
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_compute_tax_ids() clears the many2many with [(5,)]. Odoo M2M command tuples are expected to be (5, 0, 0) (or use Command.clear()), and [(5,)] can raise a “wrong tuple length” error at runtime. Use record.tax_ids = [Command.clear()] (or the full 3-tuple form) instead.

Suggested change
record.tax_ids = [(5,)]
record.tax_ids = [Command.clear()]

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +133
"""Assure quantity is 1 if an invoice is set for having proper totals, and
the rest of the fields that are not computed writable, avoiding to ud
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring for _onchange_invoice_id() is truncated/unclear ("avoiding to ud") and appears to be missing words. Please fix the wording so it accurately describes what the onchange does.

Suggested change
"""Assure quantity is 1 if an invoice is set for having proper totals, and
the rest of the fields that are not computed writable, avoiding to ud
"""When a vendor bill is set, force the expense to a single unit and
synchronize non-computed fields with the invoice (description, date and
company) while avoiding unnecessary updates of dependent computed fields.

Copilot uses AI. Check for mistakes.
)
source_invoice_expense_id = fields.Many2one(
comodel_name="hr.expense",
help="Reference to the expense with a linked invoice that generated this"
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help text for source_invoice_expense_id is missing a space due to string concatenation: it renders as "generated thistransfer journal entry". Add a trailing space in the first literal or merge into a single string so the help reads correctly.

Suggested change
help="Reference to the expense with a linked invoice that generated this"
help="Reference to the expense with a linked invoice that generated this "

Copilot uses AI. Check for mistakes.

def get_expense_sheets_with_invoices(self, func):
return self.filtered(
lambda sheet: func(expense.invoice_id for expense in sheet.expense_line_ids)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_expense_sheets_with_invoices() uses all(...) / any(...) on a generator over sheet.expense_line_ids. For an empty sheet, all([]) evaluates to True, so a sheet with no lines is treated as “with invoices”, which can cause action_sheet_move_post() to bypass super() and also skip the custom _check_can_create_move() path (because any([]) is False). Add an explicit sheet.expense_line_ids guard (or otherwise handle the empty case) so empty sheets are not misclassified.

Suggested change
lambda sheet: func(expense.invoice_id for expense in sheet.expense_line_ids)
lambda sheet: sheet.expense_line_ids
and func(expense.invoice_id for expense in sheet.expense_line_ids)

Copilot uses AI. Check for mistakes.
if self.state == "approve":
return self.env.ref("hr_expense_invoice.mt_expense_approved_inherited")
else:
super()._track_subtype(init_values)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_track_subtype() doesn’t return the parent result in the else branch. This causes the method to return None for non-approve states, which can break mail subtype tracking. Return the value of super()._track_subtype(init_values) in the else branch.

Suggested change
super()._track_subtype(init_values)
return super()._track_subtype(init_values)

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +35
def _do_create_ap_moves(self):
# Create AP transfer entry for expenses paid by employees
for expense in self.expense_line_ids.filtered("invoice_id"):
if expense.payment_mode == "own_account":
move_vals = expense._prepare_own_account_transfer_move_vals()
move = self.env["account.move"].create(move_vals)
move.action_post()
# reconcile with the invoice
ap_lines = expense.invoice_id.line_ids.filtered(
lambda x: x.display_type == "payment_term"
)
transfer_line = move.line_ids.filtered(
lambda x, partner=expense.invoice_id.partner_id: x.partner_id
== partner
)
(ap_lines + transfer_line).reconcile()
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_do_create_ap_moves() always creates and posts a new transfer journal entry for each invoiced expense in own_account mode. Since action_sheet_move_post() is allowed to run more than once (see comment in _check_can_create_move()), this can generate duplicate transfer moves and duplicate reconciliations for the same expense/invoice. Add an idempotency check (e.g., skip when expense.transfer_move_ids already exists / when a posted move for source_invoice_expense_id is present) before creating a new move.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.