Skip to content

Fix Odoo Partner Merge and Duplicate Contact Issues: Deduplication Guide

DeployMonkey Team · March 23, 2026 10 min read

The Duplicate Partner Problem

Duplicate contacts (res.partner) accumulate in every Odoo database over time. Different users create the same customer, CSV imports duplicate entries, website registrations create new partners. Duplicates cause wrong invoicing, split customer history, inaccurate reporting, and CRM confusion.

Finding Duplicates

Using the Built-in Deduplication Tool

# Contacts → Configuration → Merge Contacts
# (requires base.group_erp_manager)

# The wizard finds duplicates based on:
# - Same email
# - Same name
# - Same VAT number
# - Same phone/mobile

# Select matching criteria → search → review → merge

Using SQL to Find Duplicates

# Find duplicates by email:
SELECT email, COUNT(*), string_agg(id::text, ', ') as ids
FROM res_partner
WHERE email IS NOT NULL AND email != ''
    AND active = true
GROUP BY email
HAVING COUNT(*) > 1
ORDER BY COUNT(*) DESC
LIMIT 20;

# Find duplicates by name:
SELECT name, COUNT(*), string_agg(id::text, ', ') as ids
FROM res_partner
WHERE is_company = true AND active = true
GROUP BY name
HAVING COUNT(*) > 1
ORDER BY COUNT(*) DESC;

# Find duplicates by phone:
SELECT phone, COUNT(*), string_agg(id::text, ', ') as ids
FROM res_partner
WHERE phone IS NOT NULL AND phone != ''
    AND active = true
GROUP BY phone
HAVING COUNT(*) > 1;

Merging Partners

Using the Merge Wizard

# 1. Contacts → select duplicate contacts (check boxes)
# 2. Action menu → Merge Contacts
# 3. Select which contact to keep as the "master"
# 4. Click Merge

# What merge does:
# - All references (invoices, orders, etc.) point to the master
# - Emails, phone, address from slave copied to master (if empty)
# - Slave contact is archived or deleted
# - Followers and messages are combined

Common Merge Errors

Error 1: Cannot Merge — Related Records

# Error: "Cannot merge contacts with related accounting entries"
# Or: IntegrityError during merge

# Cause: Foreign key constraints prevent reassignment
# Some records have unique constraints that would be violated

# Fix:
# 1. Check what records reference each partner:
SELECT DISTINCT
    tc.table_name,
    kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
    ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
    ON ccu.constraint_name = tc.constraint_name
WHERE ccu.table_name = 'res_partner'
    AND tc.constraint_type = 'FOREIGN KEY';

# 2. Manually reassign conflicting records first
# Then retry the merge

Error 2: Users Cannot Be Merged

# Error: "Contacts linked to a user cannot be merged"

# Cause: Each user has exactly one partner. Merging user-linked
# partners would break the user record.

# Fix:
# 1. Deactivate one user account first
# 2. Unlink the user from the partner:
#    Settings → Users → select user → archive
# 3. Then merge the partner records
# 4. Create a new user linked to the merged partner if needed

Error 3: Company/Contact Merge Conflict

# Trying to merge a company with an individual contact

# Fix: Only merge contacts of the same type:
# - Company with Company
# - Individual with Individual
# Do not merge a company with its own child contact

# Check before merging:
SELECT id, name, is_company, parent_id
FROM res_partner
WHERE id IN (ID1, ID2);

Manual Merge via Code

# For complex merges or bulk deduplication:

# Odoo shell:
from odoo.addons.base.wizard.base_partner_merge_automatic import MergePartnerAutomatic

# Merge partner 102 into partner 101 (101 is the master):
wizard = env['base.partner.merge.automatic.wizard'].create({
    'state': 'selection',
    'dst_partner_id': 101,  # Keep this one
    'partner_ids': [(6, 0, [101, 102])],  # Both partners
})
wizard.action_merge()
env.cr.commit()

# Bulk merge by email:
from collections import defaultdict
partners_by_email = defaultdict(list)
for p in env['res.partner'].search([('email', '!=', False), ('active', '=', True)]):
    partners_by_email[p.email.lower()].append(p)

for email, partners in partners_by_email.items():
    if len(partners) > 1:
        master = min(partners, key=lambda p: p.id)  # Keep oldest
        print(f'Would merge {[p.id for p in partners]} -> {master.id} ({email})')

Preventing Duplicates

1. Unique Constraints

# Add a unique constraint on email for companies:
CREATE UNIQUE INDEX unique_company_email
ON res_partner (lower(email))
WHERE is_company = true AND active = true AND email IS NOT NULL;

# WARNING: This prevents duplicate emails but may block
# legitimate cases (branches with same domain email)

2. Check Before Creating

# In custom code, always check for existing partner first:
def _get_or_create_partner(self, email, name):
    existing = self.env['res.partner'].search([
        ('email', '=ilike', email),
        ('active', '=', True),
    ], limit=1)
    if existing:
        return existing
    return self.env['res.partner'].create({
        'name': name,
        'email': email,
    })

3. Import Deduplication

# When importing CSV, use External ID to prevent duplicates:
# CSV columns: id,name,email
# __import__.partner_john,John Smith,[email protected]

# The External ID ensures re-import updates instead of creating
# Without External ID, each import creates a new record

4. Regular Cleanup

# Schedule monthly deduplication:
# Use the merge wizard or a cron job
# Focus on: same email, same phone, same VAT number
# Always review before merging — never auto-merge blindly