Cet article est actuellement disponible en anglais uniquement. Traduction à venir.
You try to delete a partner, product, or any other record and Odoo refuses with a red modal:
odoo.exceptions.UserError: You cannot delete this document because it is
referenced by other records.
- "Sales Order" (sale.order) - 14 records
- "Customer Invoice" (account.move) - 23 records
This is Odoo's foreign-key guard doing its job. The error is correct — there are real referential constraints, and deleting the row would leave dangling pointers in other tables. But teams often need to clean up data, merge duplicate partners, or remove archived products. This guide explains the durable ways to do that without breaking your accounting.
Quick Fix
If you just need this one record gone and you have admin rights, archive instead of delete:
record.action_archive()
# or in the UI: open the record, click the gear, "Archive"
Archive sets active = False and the record disappears from default lists. All foreign-key constraints stay valid because the row still exists. This is the right answer 80 percent of the time.
If you genuinely must delete (GDPR right-to-erasure, test data cleanup), reassign the references first, then delete:
# Reassign all sale orders from old partner to new partner, then delete old.
old.sale_order_ids.write({'partner_id': new.id})
old.unlink()
Why This Happens
Odoo respects PostgreSQL foreign keys. When you unlink() a record, PostgreSQL checks every Many2one field across every model that points at this row. If any reference exists with ondelete='restrict' (the safe default), the delete is blocked and Odoo wraps the database error in a friendly UserError.
The four common scenarios:
- Accounting links. You cannot delete a partner who has invoices. You cannot delete a product on any posted journal entry. This is a regulatory feature, not a bug.
- Audit trail.
mail.message,mail.followers,audit.logand similar models keep history references that prevent deletion. - Test data leftovers. A demo partner is referenced by 200 demo records and you want them all gone.
- Merging duplicates. You have two
res.partnerrows for the same company; one has all the data, the other is referenced by a few stragglers.
Knowing which scenario you are in tells you whether to archive, merge, or do a controlled reassign-and-delete.
Step-by-Step Diagnosis
1. Read the error. Odoo lists the referencing models and the reference count. That is your worklist.
2. Confirm references at the SQL layer. For a more complete picture (Odoo only lists models, not all FK columns):
-- Find every column in the database that references res_partner.id
SELECT conrelid::regclass AS table,
a.attname AS column
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY (c.conkey)
WHERE c.contype = 'f'
AND c.confrelid = 'res_partner'::regclass;
3. Count references per table.
SELECT 'sale_order' AS tbl, count(*) FROM sale_order WHERE partner_id = 7
UNION ALL
SELECT 'account_move', count(*) FROM account_move WHERE partner_id = 7;
4. Decide: archive, merge, or delete. If references are accounting documents, you cannot delete — archive. If references are demo records, deletion is safe. If you have duplicate records, merge.
5. Use the built-in deduplication for partners. Settings > Technical > Database Structure > Deduplicate Contacts in Developer Mode. Odoo handles all the FK reassignment for you.
Permanent Fix
For accounting-blocked deletions: archive. Hard-delete is rarely the right move on a system with posted journal entries. Add active = False and the record disappears from filters while history stays intact.
For partner merges: use the built-in wizard, not raw SQL. The wizard base.partner.merge.automatic.wizard reassigns mail, followers, sales orders, invoices, leads, and dozens of other tables in a single transaction.
For test data cleanup: write a controlled cleanup script:
def _cleanup_demo_partners(env):
demo_partners = env['res.partner'].search([
('ref', 'like', 'DEMO-%'),
('create_date', '<', '2025-01-01'),
])
# Reassign or delete dependent records first
env['sale.order'].search([('partner_id', 'in', demo_partners.ids)]).unlink()
env['mail.message'].search([
('model', '=', 'res.partner'),
('res_id', 'in', demo_partners.ids),
]).unlink()
demo_partners.unlink()
Always wrap in a transaction and test on a restored copy of production first.
For SQL-level forced deletion (last resort): if you absolutely must delete and you understand the referential damage:
-- DANGEROUS — only after a full backup, with a lawyer's blessing for accounting data
BEGIN;
ALTER TABLE sale_order ALTER COLUMN partner_id DROP NOT NULL;
UPDATE sale_order SET partner_id = NULL WHERE partner_id = 7;
DELETE FROM res_partner WHERE id = 7;
-- check, then COMMIT or ROLLBACK
This breaks the contract Odoo expects and we strongly recommend against it outside of GDPR-mandated erasure on a fully isolated tenant. ECOSIRE's Odoo support team handles this case under change control.
How to Prevent It
- Default to archive in your UI flows. Train users that "delete" almost always means "archive". Reserve unlink for genuine mistakes within minutes of creation.
- Set
ondelete='set null'on non-critical references. Amail.message.author_idcan sensibly become NULL when the author is deleted; the message stays in the audit trail. Asale.order.partner_idcannot — the order is meaningless without it. Choose per relationship. - Add data-retention policies. GDPR/CCPA require a defined retention rule, not ad-hoc deletes. Implement a scheduled action that anonymizes (not deletes) personal data older than your retention window. Anonymize keeps FKs valid while removing PII.
- Build a "safe to delete" report. A scheduled report that lists records with no references is gold for cleanup teams.
- Avoid
ondelete='cascade'on high-value fields. Cascade deletion silently removes child records. Restrict makes the dependency visible — and yourUserErroris the system telling you something it shouldn't auto-destroy.
Related Errors
- MissingError: Record does not exist — what happens when cascade deletion goes too far.
- Required field not set — sibling exception in the same family.
- PostgreSQL deadlock during stock move — when concurrent FK checks fight each other.
Frequently Asked Questions
Why does Odoo block partner deletion when I see no references in the UI?
The UI only shows business documents. The blocker is often mail.message (chatter history), mail.followers, account.analytic.account (analytic ledger), or audit.log records that the user never sees. Use the SQL probe in step 2 of the diagnosis to find every reference.
Can I bulk-archive instead of bulk-delete?
Yes. In any list view, select the records, click Action > Archive. Most models support it; the few that do not (like account.move once posted) are the ones you should not be deleting anyway.
How do GDPR right-to-erasure requests work in Odoo?
Anonymize, do not delete. Replace name, email, phone, address with placeholders, set active=False, log the erasure in an audit table. The base partner row stays so accounting/reporting integrity is preserved. The OCA data_protection module ships a turnkey workflow for this.
Should I disable the FK constraint to force-delete?
No. Disabling the constraint creates orphan rows that crash later operations and corrupts reports. If you need to remove personal data, anonymize. If the rows are pure junk, fix the originating module (it should not have been creating those references in the first place).
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
Rédigé par
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Transformez votre entreprise avec Odoo ERP
Implémentation, personnalisation et assistance expertes d'Odoo pour rationaliser vos opérations.
Articles connexes
Comment ajouter un bouton personnalisé à une vue de formulaire Odoo (2026)
Ajoutez des boutons d'action personnalisés aux vues de formulaire Odoo 19 : méthode d'action Python, héritage des vues, visibilité conditionnelle, boîtes de dialogue de confirmation. Testé en production.
Comment ajouter un champ personnalisé dans Odoo sans Studio (2026)
Ajoutez des champs personnalisés via le module personnalisé dans Odoo 19 : héritage de modèle, extension de vue, champs calculés, décisions magasin/non-magasin. Code d'abord, contrôle de version.
Comment ajouter un rapport personnalisé dans Odoo à l'aide d'une mise en page externe
Créez un rapport PDF de marque dans Odoo 19 à l'aide de web.external_layout : modèle QWeb, format papier, liaison d'action. Avec logo imprimé + remplacements de pied de page.