Skip to content
ERPNext Data Model
Esc
navigateopen⌘Jpreview
On this page

Multi-Company Party Identity Linking

A design reference for declaring that a Customer record and a Supplier record are the same real-world business entity, and for what that declaration is actually used for

1. Requirements

1.1 Functional requirements

  • Let an operator declare that a specific Customer record and a specific Supplier record represent the same real-world counterparty — a vendor who is also a buyer, or one Legal Entity’s own internal stand-in for another Legal Entity it trades with in both directions.
  • Enforce that this declaration is a strict one-to-one pairing: a given Customer or Supplier record may appear in at most one such link, in either direction, at any time.
  • Once linked, automatically net the outstanding balance on a submitted Sales Invoice or Purchase Invoice against the linked counterpart’s own account, without the operator having to manually raise and allocate an offsetting entry every time either side invoices the other.
  • Reverse that automatic netting cleanly if the invoice that triggered it is later cancelled.
  • Resolve, for any party (Customer, Supplier, or any other registered party role), which specific Ledger Account it posts to for a given Legal Entity — because the same party record can be shared across several Legal Entities’ books, each with its own chart of accounts.
  • Make the whole netting behavior an explicit, global opt-in rather than something that activates the moment two records happen to be linked.

1.2 Non-functional requirements

  • No accidental identity merging: linking two party records must never merge their transaction history, their outstanding balances, or their master data — it only tells one specific downstream process how to net between them. The Customer and the Supplier remain two fully independent records everywhere else in the system.
  • Symmetry of the constraint: whichever record is declared primary or secondary in the link, the one-record-one-link rule applies identically to both roles.
  • Low ceremony: creating the link should be a small, self-contained action (pick the counterpart, save) rather than a multi-step wizard, since the record itself is a single row with no lifecycle of its own.

1.3 Constraints

  • The pairing is restricted to exactly two party roles: Customer and Secondary-role Supplier (or the reverse) — it is not a generic “any two parties are the same entity” mechanism, even though the system separately recognizes other party roles (Employee, Shareholder, and similar) for unrelated ledger purposes.
  • The automatic netting this link enables books its offsetting entry entirely within the invoicing Legal Entity’s own books — it does not, by itself, cross a Legal Entity boundary or touch a second company’s accounts. It solves “this Customer and this Supplier, inside one set of books, are the same business partner,” not “these two companies owe each other money.”

2. High-Level Design

2.1 Component diagram

2.2 Why this exists: netting, not merging

Inside a single Legal Entity’s books, Customer and Supplier are two separate ledgers by construction — one tracks who owes the entity money, the other tracks who the entity owes money to. That separation is correct for most trading partners, but it produces an awkward result whenever the same real-world business is both: a vendor the entity also sells to, or — most relevant to this document set — an internal Customer and an internal Supplier that a Legal Entity maintains to represent the same other Legal Entity for its two trading directions (see the mirroring document referenced below). Without any linking mechanism, an outstanding receivable against one record and an available credit sitting on the other never talk to each other; someone has to notice, manually raise an adjustment, and allocate it by hand every time.

The Party Identity Link exists to remove exactly that manual step, and only that step. It is a single declaration — “these two records are the same business entity” — consumed by one specific piece of invoice-submission logic that automatically books a system-generated Journal Entry netting the just-submitted invoice’s outstanding amount against the linked party’s own account, in the same Legal Entity’s books. Nothing else in the system reads this link: it is not consulted by pricing, by statements, by credit-limit checks, or by any reporting path outside the netting flow itself.

2.3 Relationship to inter-company mirroring

It is tempting to assume this link is what makes cross-entity mirroring (documented separately) resolve which internal party represents which Legal Entity. It is not. Mirroring resolves its counterpart party entirely through the Represented Legal Entity field and the Internal Customer/Internal Supplier flags carried directly on the Customer/Supplier record — a mechanism with no read or write dependency on this link at all. The two features are commonly useful together in an inter-company setup (a Legal Entity that both sells to and buys from the same counterpart Legal Entity will naturally end up with an internal Customer and an internal Supplier representing that same counterpart, and linking those two records lets their balances net automatically) — but the link is an enabler of one optional convenience on top of an already-complete mirroring mechanism, not a dependency of it. A system with inter-company mirroring fully configured and zero identity links created would mirror transactions exactly the same way; it would simply carry the receivable and payable balances against the counterpart separately instead of netting them.


3. Deep Dive

3.1 Data model

Party Identity Link A minimal join record: a primary role (constrained to Customer or Supplier), a primary party, a secondary role, and a secondary party. It carries no status field, no date range, no amount, and no lifecycle — it either exists or it doesn’t. Three checks run on every save:

  1. The primary role must be Customer or Supplier — no other party role may anchor a link.
  2. This exact primary/secondary pair must not already exist.
  3. Neither the primary party nor the secondary party may already appear as either side of a different link.

That third check is what makes the pairing strictly one-to-one: a Customer can be linked to at most one Supplier, and a Supplier to at most one Customer, system-wide, at any given time. There is no supported way to link one Customer to several Suppliers or vice versa.

Per-Legal-Entity account resolution Every Customer and Supplier record carries a small child table mapping a Legal Entity to a default Ledger Account (and, separately, a default advance-type account) for that entity’s books specifically. This is what makes the netting Journal Entry postable at all when a party record is shared across more than one Legal Entity: resolving “the account this party posts to” is never a single global answer, it is always asked for a specific Legal Entity — the one whose invoice triggered the netting.

Party role registry A small, separate lookup table declaring, for any registered party role (Customer, Supplier, Employee, and a handful of others), whether that role sits on the receivable or the payable side of the ledger. The identity link itself hard-codes its two eligible roles rather than consulting this registry, but the broader accounting machinery that ultimately posts the netting entry — the generic “which account type does this party role imply” question used across invoices, journal entries and settlements — is answered by this registry. It is infrastructure the identity link rides on top of, not something the link itself manages.

3.2 The netting algorithm

Triggered on submission of a Sales Invoice or Purchase Invoice, gated behind the global opt-in:

on_invoice_submit(invoice):
    if not netting_enabled_globally():
        return
    if invoice.record_type not in ("Sales Invoice", "Purchase Invoice"):
        return

    link = find_identity_link(secondary_role=invoice.party_type, secondary_party=invoice.party)
    if not link or invoice.outstanding_amount == 0:
        return

    linked_account   = resolve_ledger_account(link.primary_role,   link.primary_party,   invoice.legal_entity)
    invoiced_account = resolve_ledger_account(invoice.party_type,  invoice.party,         invoice.legal_entity)

    journal_entry = new_system_generated_journal_entry(company=invoice.legal_entity)
    journal_entry.add_line(account=invoiced_account, party=invoice.party,      reference=invoice)
    journal_entry.add_line(account=linked_account,   party=link.primary_party, is_advance=(not invoice.is_return))
    # both accounts are converted to a shared currency basis first if the two accounts
    # do not already share the invoicing entity's own base currency
    journal_entry.submit()

The generated entry is tagged as system-generated specifically so it can be found again and reversed automatically. On cancellation of the originating invoice, the system looks for exactly that tagged, still-submitted entry referencing the invoice and cancels it in turn — the netting is fully undone rather than left as an orphaned adjustment. If the invoice’s outstanding amount is already zero at submission (fully paid at the point of submission, or a zero-value document), no netting entry is created at all — there is nothing to net.

Only one leg of the resulting entry is booked as an advance: the leg against the linked counterpart’s account, and only when the originating document is not itself a return, since an advance entry has a fixed debit/credit direction that a return would violate. The leg against the invoice’s own party is a plain reference-carrying line, not an advance.

3.3 Error handling and edge cases

  • No link found: the invoice posts normally with no netting attempted — absence of a link is not an error condition, it is the default state for the vast majority of parties.
  • Cross-currency parties: if the linked accounts do not already share the invoicing Legal Entity’s base currency, the netting entry applies the appropriate exchange rate on each leg rather than assuming a 1 rate.
  • Duplicate or conflicting link attempts: creating a second link touching a party record that is already linked is rejected at save time by the uniqueness checks in §3.1, before any netting logic is ever reached.
  • Cancellation without a matching entry: if no system-generated netting entry can be found for the invoice being cancelled (netting was disabled after the invoice was submitted, for instance), cancellation of the invoice proceeds without attempting to reverse anything that was never created.

4. Scale and Reliability

  • Load pattern: one lookup and, at most, one small Journal Entry per qualifying invoice submission — this is a per-document synchronous side effect, not a batch or scheduled process, and its cost is bounded by however many Sales/Purchase Invoices are submitted against linked parties.
  • No fan-out: because the pairing is strictly one-to-one, resolving a link is a single-row lookup with no possibility of ambiguity or a multi-candidate resolution step — unlike the counterpart-resolution problem inter-company mirroring has to solve when more than one internal party could represent the same Legal Entity.
  • Global toggle as the only guard against surprise: because the feature is opt-in at a single system-wide switch rather than per Legal Entity or per party, enabling it retroactively affects every already-linked pair’s next invoice simultaneously — there is no staged or per-entity rollout path.

5. Trade-off Analysis

Decision Trade-off
A strict one-to-one link rather than a many-to-many mapping Keeps the netting lookup and the automatic entry unambiguous — there is never a “which of several linked counterparts” question to answer — but it cannot model a business partner that is legitimately split across more than one Customer or Supplier record (a common outcome of a merger or a multi-branch vendor), which simply cannot be linked at all under the current rule.
Netting is a single global toggle, not scoped per Legal Entity or per link One flag to reason about, and no per-record configuration to keep consistent — but a multi-entity deployment cannot enable netting for one Legal Entity’s books while leaving another’s untouched; it is all-or-nothing across every company sharing the deployment.
The link carries no metadata beyond the two parties and their roles Trivial to create, trivial to audit, nothing to keep in sync — but it also means the link cannot express “net automatically for these Legal Entities only” or “net only above this threshold,” so any such refinement would have to live in the consuming logic rather than the link record itself.
Netting books its offsetting entry entirely within the invoicing Legal Entity’s own books Simple, single-company accounting with no cross-entity posting to reconcile — but a genuinely cross-entity netting scenario (Legal Entity A’s receivable against Legal Entity B netted against Legal Entity B’s payable to A) is not what this mechanism does at all, despite being adjacent in name to inter-company concerns.
Independent of the inter-company mirroring mechanism, with zero shared code or state Each feature can be understood, tested, and changed without touching the other — but an operator setting up an inter-company relationship has to know, from documentation rather than from any in-product prompt, that a second, separate action is needed if netting is also wanted.

6. What to Revisit as the System Grows

  • Surface the connection to inter-company setup explicitly, at least as a prompt (“this Legal Entity has both an internal Customer and an internal Supplier representing the same counterpart — link them for automatic netting?”) rather than leaving operators to discover the “Link with Supplier” action on their own.
  • Allow scoping the netting toggle, even coarsely (per Legal Entity, or per link), once a deployment has some companies that want automatic netting and others that specifically do not — the current all-or-nothing switch does not support that split.
  • Reconsider the strict one-to-one constraint if a real deployment needs to represent a business partner split across multiple Customer or Supplier records; the current model has no escape hatch for that case short of merging records outright.
  • Add an explicit amount or threshold control on the link itself, if partial or capped netting is ever needed, rather than the current all-or-nothing “the full outstanding amount nets every time” behavior.

Was this page helpful?