All posts
Integrator10 min read

Five Odoo integrations that break after every upgrade - and how to future-proof them

Some Odoo integrations break on almost every major version upgrade. These five are the most common, the root cause is predictable in each case, and the fix is the same pattern every time.

Odoo major version upgrades have a predictable set of integration casualties. Every consultancy that has run two or three upgrades for the same client has seen the same integrations fail on the same categories of change. The failures are not random - they come from specific coupling points between the integration and Odoo's internal structure.

This post covers the five most common breakage patterns, what causes each one, and the pattern that makes the integration resilient across versions.

1. Integrations that reference Odoo's internal view XML IDs#

The breakage. An integration reads Odoo's database directly or calls execute_kw with hard-coded view or action XML IDs: sale.view_order_form, account.action_move_in_invoice_type. In the upgrade, Odoo renames, merges, or removes that record. The integration call fails with "record not found."

Why it happens. Odoo's XML IDs for views and actions are implementation details. They are documented in source code, not in the public API contract. Odoo S.A. treats them as internal and changes them freely between versions.

The fix. Never hard-code XML IDs for Odoo's own views and actions in integrations. If you need to trigger an action, call the business method directly rather than opening a view:

python
# Fragile  -  action XML ID may change between versions
models.execute_kw(db, uid, password, "ir.actions.act_window", "search",
    [[["res_model", "=", "sale.order"]]])

# Resilient  -  call the business method directly
models.execute_kw(db, uid, password, "sale.order", "action_confirm",
    [[order_id]])

For your own custom modules, the XML IDs you define are stable because you control them. Odoo's are not.

2. Integrations that parse ir.config_parameter values#

The breakage. An integration reads system parameters via ir.config_parameter and parses the value as a known format: a URL, a JSON blob, a comma-separated list. Between upgrades, Odoo changes the format, renames the parameter key, or splits one parameter into two.

Why it happens. ir.config_parameter is a key-value store with string values. The keys and value formats are documented inconsistently. When Odoo refactors a subsystem, the parameters change with it.

The fix. If you must read system parameters, verify the value format at startup and fail fast with a clear error if it does not match your expectation:

python
def get_base_url(models_proxy, db, uid, password):
    result = models_proxy.execute_kw(
        db, uid, password,
        "ir.config_parameter", "get_param",
        ["web.base.url"],
    )
    if not isinstance(result, str) or not result.startswith("http"):
        raise ValueError(
            f"Unexpected value for web.base.url: {result!r}. "
            "Check for Odoo version changes."
        )
    return result.rstrip("/")

A fail-fast integration is easier to debug than one that silently passes an unexpected value down the pipeline.

3. Integrations that use Odoo's internal Many2many command lists#

The breakage. An integration writes Many2many fields using the (4, id, 0) command syntax for linking, (3, id, 0) for unlinking, (5, 0, 0) for clearing. Between Odoo 14 and 16, the command syntax was formalized with named constants, and edge cases in how the ORM processes commands in batch operations changed. Some command combinations that were accepted in 14 raise validation errors in 16.

Why it happens. The command list format is documented but the ORM's handling of edge cases - duplicate link commands, link+unlink in the same operation - has varied across versions. Integrations that use commands in creative ways encounter these edge cases.

The fix. Prefer the simplest command for your intent and avoid compound operations in a single write:

python
# Instead of constructing complex command lists:
# models.write(db, uid, pw, "sale.order", [[id]], {"tag_ids": [(6, 0, [1,2,3])]})

# Read, then write the final set:
current_tags = models.execute_kw(
    db, uid, password, "sale.order", "read",
    [[order_id]], {"fields": ["tag_ids"]}
)[0]["tag_ids"]

new_tags = list(set(current_tags) | {new_tag_id})
models.execute_kw(
    db, uid, password, "sale.order", "write",
    [[order_id]], {"tag_ids": [(6, 0, new_tags)]}
)

The (6, 0, id_list) command replaces the entire set. It is the most explicit and the most stable across versions.

4. Integrations that depend on Odoo's account.move field structure#

The breakage. The account.move model - Odoo's journal entry / invoice model - was substantially restructured in Odoo 13 and has had incremental field changes in every version since. Integrations that write to invoice fields using pre-13 names (account.invoice no longer exists), or that read fields that moved from account.move to account.move.line or vice versa, break on upgrade.

Common specific breakages:

  • amount_untaxed became a computed field; some write paths that worked in earlier versions are now read-only
  • invoice_line_ids is the stable name, but older integrations use invoice_lines (removed in 14)
  • The relationship between move_type values changed in 15 for intercompany transactions

The fix. For accounting integrations, always test against the full matrix of move types (customer invoice, vendor bill, customer credit note, vendor refund, entry) in a staging environment before upgrading. Write a test that creates, validates, and reads back each move type, confirming field values survive the round-trip.

python
def smoke_test_account_move(models_proxy, db, uid, password):
    """Run after every Odoo upgrade to verify accounting integration."""
    journal_id = models_proxy.execute_kw(
        db, uid, password, "account.journal", "search",
        [[["type", "=", "sale"]]], {"limit": 1}
    )[0]

    invoice_id = models_proxy.execute_kw(
        db, uid, password, "account.move", "create",
        [{"move_type": "out_invoice", "journal_id": journal_id,
          "invoice_line_ids": [(0, 0, {"name": "Test", "quantity": 1, "price_unit": 100})]}]
    )
    models_proxy.execute_kw(
        db, uid, password, "account.move", "action_post", [[invoice_id]]
    )
    result = models_proxy.execute_kw(
        db, uid, password, "account.move", "read",
        [[invoice_id]], {"fields": ["state", "amount_total", "amount_residual"]}
    )[0]
    assert result["state"] == "posted", f"Expected posted, got {result['state']}"
    assert result["amount_total"] == 100.0, f"Expected 100, got {result['amount_total']}"
    print("Accounting smoke test passed.")

Run this test immediately after upgrade. If it fails, you have a specific failure point to investigate before the integration goes live on the new version.

5. Integrations that use Odoo's workflow engine#

The breakage. Odoo's formal workflow engine (workflow.activity, workflow.transition) was removed in Odoo 11. Integrations built for Odoo 10 and earlier that call exec_workflow or trigger workflow transitions directly will fail to import when run against Odoo 11+.

This seems like ancient history but many production Odoo instances are still running on 14 or 15 codebases that were migrated from older versions without rewriting the integrations. When those instances upgrade to 16 or 17, the old workflow calls surface.

The fix. Replace exec_workflow calls with direct method calls or field writes:

python
# Old (does not exist in Odoo 11+)
# common.exec_workflow(db, uid, password, "sale.order", "order_confirm", order_id)

# New: call the action method directly
models.execute_kw(
    db, uid, password, "sale.order", "action_confirm", [[order_id]]
)

# Or for simple state machines, write the state field directly
models.execute_kw(
    db, uid, password, "purchase.order", "write",
    [[order_id], {"state": "purchase"}]
)

Not all state transitions accept direct field writes - some trigger side effects through the action method. Test by verifying the state AND checking that expected side effects (emails, picking creation, journal entries) occurred.

The pattern across all five#

Every one of these breakage categories has the same root cause: the integration couples to Odoo's internal structure rather than its stable public API surface. Odoo's stable API is the business method layer: action_confirm, action_post, write, create, search_read on documented fields. Everything else - view XML IDs, internal parameters, ORM command edge cases, internal field structures, deprecated engines - is an implementation detail that Odoo S.A. treats as changeable.

The integration that survives upgrades is one that only touches Odoo through business methods with validated inputs and asserts its assumptions about the response format in a smoke test that runs on every upgrade. The upgrade does not break the integration - it breaks the smoke test first, clearly and early, and the fix is obvious.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free