本文目前仅提供英文版本。翻译即将推出。
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.
相关文章
BMF Programmablaufplan Lohnsteuer 2026:实施德国官方工资税计算(XML、API、Odoo)
BMF Programmablaufplan Lohnsteuer 2026 开发人员指南:PAP 是什么、XML 伪代码格式、官方测试服务以及到 Odoo 工资单的映射。
2026 年 CRM 系统的成本是多少? 40 多个实施的实际定价
来自 40 多个实施的真实 CRM 定价:每个用户的许可成本、实施费用、隐藏成本以及 Odoo、HubSpot、Salesforce 等的 3 年 TCO。
eMAG Odoo 集成:将罗马尼亚最大的市场连接到您的 ERP(订单、库存、e-Factura)
将 eMAG Marketplace 连接到 Odoo ERP:报价和订单同步、AWB 运输、退货、库存和价格更新,以及卖家的罗马尼亚 e-Factura 合规性。