# Odoo 18 to 19 Accounting migration: follow the invoice, test the result

> Upgrade custom Accounting addons from Odoo 18 to 19: account-field changes, reconciliation links, report behavior, original code, and practical tests.

**What changes in Odoo 19 Accounting?** For Community custom addons, review the `account.account.deprecated` → `active` rename, removed account–journal restriction fields, the removed `account.full.reconcile.exchange_move_id` relation, and removed model-specific `read_group` overrides. The sections below connect these changes to account selection, invoice lines, reconciliation, and report tests.

Take one invoice your addon handles differently from standard Odoo. Follow it from account selection through reconciliation to the report your customer reads. That gives you a useful route through this upgrade: which choices must remain possible, which relationships your code follows, and which numbers must still mean the same thing.

The Odoo 18 → 19 changes below touch all three. An account-status field changes name and default; journal restriction fields disappear; several reporting overrides are removed. These deserve different kinds of review. A missing field gives you a reference to fix. A removed reporting override gives you a behavior to investigate.

This guide covers selected **Community edition** Accounting changes. Start with the part of the workflow your addon extends, then follow its dependencies. The scenarios are suggested tests, not claims about failures observed in Odoo 19.

## Before the invoice: can the right account still be chosen?

There is a connection worth noticing before you edit anything: the Odoo 18 journal field `account_control_ids` contains a domain on `deprecated`. Both are affected by the upgrade.

On `account.account`, `deprecated` becomes `active`, with a different default:

**Odoo 18**

```python
deprecated = fields.Boolean(default=False, tracking=True)
```

**Odoo 19**

```python
active = fields.Boolean(default=True, tracking=True)
```

Source: Odoo's account definitions in [18.0](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/models/account_account.py) and [19.0](https://github.com/odoo/odoo/blob/2cdf412984ae942b7b39e681e1c125bb942808cd/addons/account/models/account_account.py).

Read each existing condition as a sentence before rewriting it. Does it mean “offer accounts that can be used,” “exclude accounts we have retired,” or something specific to your addon? The declarations show the rename and default change; they do not settle the migration of every custom domain or stored value.

Now look at the other half of account selection. Odoo 19 removes `account.account.allowed_journal_ids` and `account.journal.account_control_ids`. If your addon uses these fields to constrain posting, review those rules alongside the account-status change.

<details>
<summary>Inspect the Odoo 18 restriction fields</summary>

On `account.account`:

```python
allowed_journal_ids = fields.Many2many(
        'account.journal',
        string="Allowed Journals",
        help="Define in which journals this account can be used. If empty, can be used in all journals.",
        check_company=True,
    )
```

On `account.journal` — notice the `deprecated` condition:

```python
account_control_ids = fields.Many2many('account.account', 'journal_account_control_rel', 'journal_id', 'account_id', string='Allowed accounts',
        check_company=True,
        domain="[('deprecated', '=', False), ('account_type', '!=', 'off_balance')]")
```

These are the Odoo 18 definitions of fields removed in Odoo 19, not replacement code.

Source: [Odoo 18 account restrictions](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/models/account_account.py) and [journal restrictions](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/models/account_journal.py).

</details>

**The useful test is a pair of attempts.** Try an account–journal combination your customization should allow, then one it should reject. Repeat with an account that should be unavailable. For a multi-company addon, include an account from another company. Record both what the selector offers and what happens when the transaction is submitted.

You are ready to move on when those outcomes match the intended rules—not merely when the old field names have disappeared from your code.

## On the invoice line: does the selector still express your rule?

An invoice-line customization may depend on a shortcut that no longer exists: `account.move.line.product_uom_category_id` is removed.

<details>
<summary>Inspect the removed unit-of-measure shortcut</summary>

**Odoo 18 definition**

```python
product_uom_category_id = fields.Many2one(
        comodel_name='uom.category',
        related='product_id.uom_id.category_id',
    )
```

Source: [Odoo 18 invoice-line fields](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/models/account_move_line.py).

</details>

The old related path explains what the field exposed. It is not evidence that following the same path directly is a valid Odoo 19 replacement. Check the target fields before making that substitution.

Put the form through the interaction your users perform: choose a product, inspect the permitted units, then change the product. If your addon supplies a domain or validation rule here, check both the choices displayed and the value accepted when the line is saved.

## After payment: can your code still follow the reconciliation?

Move from the invoice to the journal entry your customization expects to find after reconciliation. In Odoo 18, `account.full.reconcile.exchange_move_id` provides a relation to `account.move`. That field is removed in Odoo 19.

**Odoo 18 — removed relation**

```python
exchange_move_id = fields.Many2one('account.move', index="btree_not_null")
```

Source: [Odoo 18 full reconciliation](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/models/account_full_reconcile.py).

If your addon follows that relation to display a link, export an entry, or continue processing, use a reconciliation with an exchange difference as your test case. Write down the entry or information the feature needs to reach, then establish the appropriate Odoo 19 route to it.

The field's removal does not identify its replacement. Keep that as a specific investigation in your migration work, rather than assigning an unverified field name and moving on.

## At the report: an open screen is only the first check

Three removed `read_group` overrides deserve attention if you customize grouped Accounting reports. Their Odoo 18 implementations do different jobs:

| Report surface | What its Odoo 18 override does | A useful comparison on Odoo 19 |
|---|---|---|
| Invoice analysis — `account.invoice.report` | Calculates `price_average` as `price_subtotal / quantity`, or zero when quantity is zero. | Use unequal quantities and compare the average; include a zero-quantity case. |
| Journal items — `account.move.line` | Suppresses the summed `amount_currency` when results are not grouped by `currency_id`. | Compare groupings with and without currency, using entries in different currencies. |
| Bank statement lines — `account.bank.statement.line` | Supplies a latest running balance for certain groupings when `show_running_balance_latest` is set. | Exercise the grouping and context used by your dashboard; compare the displayed running balance. |

Source: the Odoo 18 overrides for [invoice analysis](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/report/account_invoice_report.py), [journal items](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/models/account_move_line.py), and [bank statement lines](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/models/account_bank_statement_line.py).

These are removals of model-specific implementations. They do **not** establish that `read_group` is unavailable through inheritance, or that all three behaviors vanished from Odoo 19. Trace the target implementation and test what your extension receives.

### A small fixture that makes an average worth checking

Use two invoice lines with unequal quantities: one unit at 100 and nine units at 10. For this **illustrative test fixture**, assume subtotals of 100 and 90 in the same currency and comparable units, with no discounts.

The Odoo 18 override's subtotal-over-quantity calculation gives **190 ÷ 10 = 19**. Simply averaging the two unit prices gives **(100 + 10) ÷ 2 = 55**. This is not an observed Odoo 19 discrepancy; it shows why a test with equal quantities would be less revealing.

If your addon extends this report, preserve the fixture and compare the returned values and displayed result. A screenshot of a report opening cannot answer which average it contains.

### Follow the sign as well as the amount

For tag-based custom calculations, `account.account.tag.tax_negate` is also removed. Its Odoo 18 help text describes negating the absolute balance associated with the tag during tax-report computation.

<details>
<summary>Inspect the removed tax-tag flag</summary>

```python
tax_negate = fields.Boolean(string="Negate Tax Balance", help="Check this box to negate the absolute value of the balance of the lines associated with this tag in tax report computation.")
```

Source: [Odoo 18 account tags](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/models/account_account_tag.py).

</details>

If your code reads or sets the flag, include a transaction that exercises that branch of your calculation. Compare the resulting sign and amount against your intended behavior. Confirm the target reporting logic before choosing a replacement.

## Two side trips, only if your addon uses them

**Customer portal details.** `PortalAccount.details_form_validate` and `PortalAccount.extra_details_form_validate` are removed from the Accounting controller. The former's Odoo 18 implementation checks VAT, name, and company-name edits using `partner.can_edit_vat()`, with an exception for partner creation; the latter checks additional required fields. Test an existing customer's edit, a new partner, and an empty custom required field. Follow the Odoo 19 request path before deciding where your validation belongs.

**Peppol invoice navigation.** `account.journal.action_peppol_ready_moves` is removed. Its Odoo 18 action opens invoices with a ready-state search filter. If your custom button calls it, verify the invoice list the button is meant to open as well as the action that replaces it.

Source: [Odoo 18 Accounting portal](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account/controllers/portal.py) and [Peppol journal action](https://github.com/odoo/odoo/blob/fecca216b2f7eaf9c8c8dfcce82084a0177e6e27/addons/account_peppol/models/account_journal.py).

## Turn the review into an acceptance record

For each part your addon changes, keep one small record: **scenario → intended result → Odoo 19 result → evidence**. Link the evidence to a test, an output comparison, or a reproducible manual run. Use the scenarios below as prompts; completing the worksheet records your review, not automated validation.

| Scenario | What to record |
|---|---|
| Account and journal selection | A permitted combination, a rejected combination, and an unavailable account; company boundaries where relevant. |
| Invoice-line unit selection | Choices and saved value before and after changing the product. |
| Exchange-difference reconciliation | The entry or information your custom feature must reach. |
| Invoice analysis | The unequal-quantity average and the zero-quantity case. |
| Currency and bank groupings | Currency-separated versus combined results; running balance under the dashboard's actual context. |
| Tag-dependent calculation | Expected sign and amount for the transaction that exercises your custom logic. |
| Optional entry points | Portal edit/create/required-field cases and the Peppol invoice list, if customized. |

Once the addon loads and these outcomes have evidence, your review tells the next developer something useful: which behavior was checked, with which data, and what remains unresolved.

## Continue your upgrade planning

Use the [Odoo upgrade guides hub](/guides) to find other published version paths. If you are considering a later target, the [Odoo 18 to 20 migration guide](/guides/odoo-18-to-20-migration-guide) covers that larger jump and is explicitly a **pre-release preview**, not guidance for a stable Odoo 19 migration.
