Odoo Migration

Contact Hierarchy in Odoo Migrations: Keeping Customer, Shipping, Billing, and Suite Records Connected

By Shravya Shetty•••8 min read•
118 views

Ask most teams what the hardest part of a QuickBooks-to-Odoo migration is, and they'll say the chart of accounts, or the transaction history. Almost nobody says "the customer list" — and that's exactly why contact hierarchy is where migrations quietly go wrong. Moving the names and addresses over is trivial. Moving the relationships between them — which shipping dock belongs to which customer, which suite gets invoiced separately, which address is billing versus delivery — is where a straight data dump falls apart.

In this article, we'll walk through why contact hierarchy breaks during migration, what it looks like when it does, and the approach that actually holds up — using a pattern we've run into repeatedly on real Odoo implementations.

What "Contact Hierarchy" Actually Means in Odoo

In Odoo, a contact isn't just a name and an address. Every contact record can have a parent-child relationship to other contacts: a company contact sits at the top, and its shipping addresses, billing addresses, and individual people (or in this case, suite-level records) hang underneath it as child contacts. Each child carries an address type — Invoice Address, Delivery Address, or Other Address — that tells the rest of Odoo which record to pull from automatically when a quote, invoice, or delivery order is created.

Contacts → Hierarchy
Parent Contact (Company)
Acme Facilities LLC
Invoice Address
Billing Address
AP Office – Suite 400
Delivery Address
Shipping Address
Loading Dock – Suite 100
Other Address
Suite Record
Unit Contact – Suite 210

Every child record carries a parent_id pointing back to Acme Facilities LLC and an address_type that tells Odoo which record to use for invoices versus deliveries.

That structure is what lets Odoo automatically address an invoice to the AP office and a delivery to the loading dock, for the same customer, without anyone picking the right address by hand every time.

Where It Breaks: QuickBooks Wasn't Built for This Structure

QuickBooks represents these relationships far more loosely. Ship-to and bill-to information is often stored as sub-customers or "jobs" under a parent customer, or as free-text fields on the customer record, rather than as a true, enforced parent-child link. The relationship is implied by naming convention — "Acme Facilities LLC : Suite 100" — rather than by a structured field a migration tool can read reliably.

QuickBooks vs. Odoo Contact Model
QuickBooks
Acme Facilities LLC
Acme Facilities LLC : Suite 100 Ship To
Acme Facilities LLC : Suite 400 Bill To
Free-text job/sub-customer names — the relationship is implied, not enforced.
Odoo
Acme Facilities LLC (parent)
↳ Suite 100 — Delivery Address
↳ Suite 400 — Invoice Address
Structured parent-child records — the relationship is a real field, not a naming convention.

Migrated as-is, that naming-convention approach produces a specific, predictable set of problems:

  • Orphaned address records. A shipping or suite record imports as a standalone contact with no parent_id set, so it never shows up as an option when someone creates an invoice for the actual customer.
  • Duplicate customers. "Acme Facilities LLC" and "Acme Facilities LLC : Suite 100" import as two unrelated top-level companies instead of one company with a child address, splitting that customer's history across records.
  • Wrong address on documents. Without a correctly set address type, Odoo has no way to know that Suite 400 is where invoices go and Suite 100 is where deliveries go — so it defaults to whichever address it finds first.

None of this shows up as an error during import. The records load fine. It surfaces later, when a customer calls asking why their invoice went to the wrong suite.

The Approach That Holds Up: Migrate Parents Before Children

There are a few ways to handle this, ranging from a single flat import with post-migration cleanup, to a fully staged, validated migration. In our experience, the staged approach is the one worth recommending — and it isn't close.

The reasoning is simple: a flat import treats every relationship problem as a cleanup task after the fact, which means finding mis-linked addresses by combing through however many thousand contact records already look "done." A staged import treats the relationship as something to get right before more data — invoices, opportunities, service history — gets layered on top of a broken link. Concretely, that means:

  • Deduplicate parent customers first. Before any address imports, identify every genuinely distinct customer in QuickBooks and resolve naming-convention variants ("Acme Facilities LLC" vs. "Acme Facilities LLC : Suite 100") down to one parent record.
  • Map each address as a child contact. Every Ship To, Bill To, and suite-level record gets created as a child of its resolved parent, with an explicit address type — never as a new standalone customer.
  • Carry a stable external reference through the import. Keeping the source QuickBooks ID on each migrated record makes it possible to re-run or correct an import without creating duplicate contacts the second time around.
  • Validate a sample before the full load. Migrate a representative slice of customers with multiple addresses first, and manually confirm every child record landed under the right parent with the right address type before running the rest.

The favorite here isn't close: validate the hierarchy on a small sample first. It's far cheaper to catch a mis-mapped suite record across twenty test customers than to discover the same pattern repeated across two thousand live ones after invoices have already gone out.

A Validation Checklist Before You Trust the Data

Once addresses are migrated, this is the order that catches problems before they reach a customer:

  • Spot-check that every migrated child contact has a parent_id set — a child contact with no parent is an orphaned record.
  • Confirm the address type on each child matches its real-world purpose — Invoice, Delivery, or Other — not left at the default.
  • Search for near-duplicate top-level companies (naming-convention variants of the same customer) that should have been merged into one parent with child addresses.
  • Create a test quote or invoice for a multi-address customer and confirm Odoo pulls the correct billing and shipping address without manual selection.
  • Re-check hierarchy after any historical transactions are migrated in — a transaction pointing at the wrong contact can re-surface a mapping issue that looked fixed.

Final Thoughts

Contact hierarchy is easy to underestimate precisely because it looks like plain data — names, addresses, a few extra fields. What actually determines whether a migration succeeds is whether the relationships between those records survive the move intact. QuickBooks' looser, naming-convention-based structure doesn't map cleanly onto Odoo's parent-child model by default, and the gap between the two is exactly where invoices go to the wrong suite and duplicate customers start piling up.

Migrate parents before children, keep a stable reference back to the source record, and validate a sample before trusting the full load — and the structure holds. Skip that order, and you'll be untangling it by hand months after go-live, one confused customer call at a time.

Migrating from QuickBooks and worried about your contact data?

Our Odoo migration team can map your customer, shipping, billing, and suite records into a clean parent-child hierarchy before anything goes live — and validate it before your customers ever see an invoice.

Talk to Our Odoo Team
Odoo MigrationContact HierarchyQuickBooks to OdooData MigrationOdoo ERP