Contact Hierarchy in Odoo Migrations: Keeping Customer, Shipping, Billing, and Suite Records Connected
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.
Odoo Contact Hierarchy Migration at a Glance
- Names and addresses are easy; relationships are not. The hard part is keeping each shipping, billing and suite record linked to the right customer.
- Odoo uses parent and child contacts. A company sits at the top, with its invoice, delivery and other addresses underneath, each with an address type.
- QuickBooks implies the links by naming. Records like “Acme Facilities LLC : Suite 100” import as orphans or duplicates unless they are mapped.
- Migrate parents before children. Deduplicate customers first, then create every address as a child with an explicit type and the source QuickBooks ID.
- Validate a sample first. Check a few multi-address customers end to end, including a test invoice, before loading the rest.
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.
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.
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_idset, 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_idset — 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.
Migrating Contact Hierarchies: FAQ
How does Odoo store a customer’s billing, shipping and other addresses?
As child contacts under the company contact. Each child has a type (Contact, Invoice, Delivery or Other) that tells Odoo which address belongs on quotes, invoices and deliveries for that customer.
Why do QuickBooks sub-customers cause problems when migrating to Odoo?
QuickBooks often implies the link between a customer and its ship-to, bill-to or suite records through naming, such as Acme Facilities LLC : Suite 100, rather than through an enforced parent-child field. Imported as-is, these records become orphaned addresses or duplicate top-level customers. The import shows no error, so the problem only surfaces later on documents.
In what order should customer contacts be migrated to Odoo?
Deduplicate and load the parent customers first. Then create every ship-to, bill-to and suite record as a child of its resolved parent, with an explicit address type. Validate a small sample of multi-address customers, including a test quote or invoice, before loading the rest.
How can a contact import be re-run without creating duplicates?
Carry a stable external reference, such as the source QuickBooks ID, on every migrated record. Odoo’s import tool uses External IDs so the same file can be imported again without creating duplicates, and a child record can reference its parent’s External ID to link to it.
Why does Odoo still put the wrong address on a sales order?
Check that each child contact’s type is set to Invoice or Delivery rather than left at the default. Also check that the Customer Addresses setting is turned on in the Accounting settings, under Customer Invoices. With it on, quotations and sales orders fill the Invoice Address and Delivery Address from the customer’s contact record by default, and delivery orders use the Delivery Address.
Keep the Relationships, Not Just the Records
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