[MIG] hr_expense_invoice: Migration to 19.0#329
Conversation
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
[UPD] README.rst hr_expense_invoice 13.0.1.1.0
… 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/
…ent models (hr.expense.create.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/
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
hr_expense_invoice 15.0.1.1.2
TT41948 hr_expense_invoice 15.0.1.1.3
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.
There was a problem hiding this comment.
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.
| @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() |
There was a problem hiding this comment.
_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().
| raise UserError( | ||
| self.env._( | ||
| "You cannot create accounting entries for an expense \ | ||
| report without expenses." | ||
| ) |
There was a problem hiding this comment.
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.
| 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( |
There was a problem hiding this comment.
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).
| 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( |
| def _compute_tax_ids(self): | ||
| with_invoice = self.filtered("invoice_id") | ||
| for record in with_invoice: | ||
| record.tax_ids = [(5,)] |
There was a problem hiding this comment.
_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.
| record.tax_ids = [(5,)] | |
| record.tax_ids = [Command.clear()] |
| """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 |
There was a problem hiding this comment.
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.
| """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. |
| ) | ||
| source_invoice_expense_id = fields.Many2one( | ||
| comodel_name="hr.expense", | ||
| help="Reference to the expense with a linked invoice that generated this" |
There was a problem hiding this comment.
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.
| help="Reference to the expense with a linked invoice that generated this" | |
| help="Reference to the expense with a linked invoice that generated this " |
|
|
||
| def get_expense_sheets_with_invoices(self, func): | ||
| return self.filtered( | ||
| lambda sheet: func(expense.invoice_id for expense in sheet.expense_line_ids) |
There was a problem hiding this comment.
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.
| 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) |
| if self.state == "approve": | ||
| return self.env.ref("hr_expense_invoice.mt_expense_approved_inherited") | ||
| else: | ||
| super()._track_subtype(init_values) |
There was a problem hiding this comment.
_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.
| super()._track_subtype(init_values) | |
| return super()._track_subtype(init_values) |
| 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() |
There was a problem hiding this comment.
_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.
No description provided.