本文目前仅提供英文版本。翻译即将推出。
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.
作者
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.
相关文章
如何将自定义按钮添加到 Odoo 表单视图 (2026)
将自定义操作按钮添加到 Odoo 19 表单视图:Python 操作方法、视图继承、条件可见性、确认对话框。经过生产测试。
如何在没有 Studio 的情况下在 Odoo 中添加自定义字段 (2026)
通过 Odoo 19 中的自定义模块添加自定义字段:模型继承、视图扩展、计算字段、存储/非存储决策。代码优先,版本控制。
如何使用外部布局在 Odoo 中添加自定义报告
使用 web.external_layout 在 Odoo 19 中构建品牌 PDF 报告:QWeb 模板、paperformat、操作绑定。带有印刷徽标+页脚覆盖。