So many Email addresses

From Termatrac
Revision as of 08:25, 22 May 2026 by Wikiadmin (talk | contribs) (Created page with "(mostly answered by Claude.AI) At the UI level, there is '''no direct Email ID field on the Customer DocType itself'''. It '''is''' redundant by design, and it's a known architectural quirk in ERPNext. Here's why both exist and what each one actually does: ---- == The Two Fields Explained == === 1. <code>Contact.is_primary_contact</code> (on the Contact DocType) === * This is a '''flag on the Contact record itself''' * It means: ''"among all contacts linked to this...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

(mostly answered by Claude.AI)

At the UI level, there is no direct Email ID field on the Customer DocType itself.

It is redundant by design, and it's a known architectural quirk in ERPNext. Here's why both exist and what each one actually does:


The Two Fields Explained

1. Contact.is_primary_contact (on the Contact DocType)

  • This is a flag on the Contact record itself
  • It means: "among all contacts linked to this customer, this one is the primary"
  • It is set when you tick "Is Primary Contact" on the Contact form
  • ERPNext uses this to auto-populate contact fields in transactions (Sales Order, Sales Invoice, etc.)
  • Problem: Nothing enforces uniqueness — two contacts for the same customer can both have is_primary_contact = 1, which is a data integrity gap

2. Customer.customer_primary_contact (on the Customer DocType)

  • This is a direct link field (a Link field pointing to Contact) stored on the Customer record itself
  • It gives you a single, authoritative lookup: "what is the primary contact for this customer?" without having to query the Contact table at all
  • It is updated automatically by ERPNext when a contact is marked as primary
  • Much faster for API queries — one GET on the Customer record returns the primary contact name immediately

Why Both Exist — The Real Reason

Customer ──────────────────────────────────────────────────────────┐
  customer_primary_contact = "Jane Smith"  ← fast denormalised     │
                                              lookup                │
                                                                    │
Contact (Jane Smith)                                                │
  links → [{ link_doctype: "Customer", link_name: "CUST-00001" }]  │
  is_primary_contact = 1  ← flag used during transaction           │
                             auto-population & filtering ───────────┘

It is essentially denormalisation for performance — a classic database trade-off:

Concern is_primary_contact customer_primary_contact
Where stored Contact record Customer record
Purpose Flag for filtering/transactions Fast direct lookup
Query cost Requires JOIN / filter on Contact Single field on Customer fetch
Risk Multiple contacts can be flagged 1 Can go stale if not synced
Updated by User manually / Contact save hook ERPNext hook on Contact save

The Sync Risk

The real danger is they can fall out of sync:

  • If you update is_primary_contact via a direct API PATCH on the Contact but ERPNext's server-side hook doesn't fire correctly, customer_primary_contact on the Customer won't update
  • If you delete a Contact that is the primary, customer_primary_contact may hold a dead reference until another contact is promoted

Practical Advice

Use case Use this field
Fetch primary contact quickly in one API call Customer.customer_primary_contact
Filter all contacts to find which is primary Contact.is_primary_contact = 1
Trust for data integrity Neither alone — verify both match
Setting a new primary contact Always do it via the ERPNext UI or server-side method so both fields stay in sync

It's a deliberate denormalisation that trades data integrity risk for query performance, and it's something ERPNext developers themselves have debated in the Frappe GitHub issues over the years.


ERPNext's Address DocType also has its own email_id field, completely separate from the Contact email chain.


The Full Picture

Customer (CUST-00001 - "Acme Corp")
  │
  ├──► customer_primary_contact ──► Contact "Jane Smith"
  │         │                           └── email_ids[]
  │         │                                 └── jane@acme.com  ← person email
  │
  ├──► customer_primary_address ──► Address "Acme Corp-Billing"
  │                                     └── email_id: accounts@acme.com  ← address email
  │
  └──► Dynamic Links (multiple)
            │
            ├── Address "Acme Corp-Billing"
            │     └── email_id: accounts@acme.com
            │
            ├── Address "Acme Corp-Shipping"
            │     └── email_id: warehouse@acme.com
            │
            └── Address "Acme Corp-Head Office"
                  └── email_id: info@acme.com

Three Completely Separate Email Pools

Source Field Typical Use Example
Contact.email_ids[] email_id Person-to-person communication jane@acme.com
Address.email_id email_id Location/department email accounts@acme.com
Customer (none)

Each pool is entirely independent — no sync, no relationship between them whatsoever.


Why Address Has Its Own Email

The Address DocType is designed to represent a physical or operational location, and in B2B that location often has its own functional email:

Acme Corp - Billing Address  → accounts@acme.com   (AP/AR team)
Acme Corp - Shipping Address → warehouse@acme.com  (logistics team)
Acme Corp - Head Office      → info@acme.com       (general enquiries)

This makes sense in the real world — when you send an invoice, you email the billing address email, not Jane's personal email.


How ERPNext Decides Which Email to Use

This is where it gets messy. Depending on the transaction type, ERPNext picks email from different sources:

Transaction Where ERPNext looks for email
Sales Invoice (send via email) customer_primary_contact → email_id
Print & Email from Address Address.email_id
Dunning / Payment Reminder customer_primary_contact → email_id
Shipping Notification Address.email_id (shipping address)
Portal / User login Contact.email_ids[]

There is no single unified "customer email" — it depends entirely on the context and which button you press.



Summary of the Design Problem

Issue Impact
No single Customer.email field Must chase 2+ DocTypes to find any email
Address email ≠ Contact email Easy to send invoices to the wrong place
No enforced "use this email for invoices" rule Relies on user discipline
Multiple addresses, each with own email No clear hierarchy without is_primary_address logic
ERPNext picks email source based on transaction Inconsistent and surprising behaviour

It is a flexible but fragile design — powerful when set up correctly, but very easy to end up with missing or stale email data across the system.