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

Share Management / Cap Table Tracking

A design reference for tracking share ownership, transfers, and folio identity for a legal entity's own shareholder register

1. Requirements

1.1 Functional requirements

  • Maintain, per legal entity, a register of Shareholder records, each with an auto-assigned unique folio number, holding a position expressed as one or more numbered Share Lots — contiguous ranges of share certificate numbers of one share type, held at one rate.
  • Represent the issuing company itself as a Shareholder record (an Issuer Mirror Record, flagged is_company) so every lot the company issues is mirrored on its own books, not just the recipient’s.
  • Support exactly three transfer directions on one submittable record (transfer_type: Issue, Transfer, Purchase), each adding and/or removing specific Share Lots on specific Shareholder records.
  • Before submission, validate that the requested share-number range does not already exist (Issue) or does fully exist (Transfer/Purchase) against the relevant Shareholder’s current lots, scoped per share type.
  • Auto-assign a folio number the first time a transfer references a shareholder without one, and validate later references stay consistent with what is stored.
  • Support amend/cancel, with cancellation being the exact inverse of the additions/removals made at submit time.
  • Allow a user to generate, on demand, a draft two-line manual accounting voucher pre-filled from a submitted transfer — never created or submitted automatically.

1.2 Non-functional requirements

  • Auditability: change tracking is enabled on all four constructs (Shareholder, Share Transfer, Share Balance row, Share Type).
  • Reversibility: cancel handlers are the algebraic inverse of submit handlers, so no separate “undo” logic exists — cancelling replays the opposite mutation.
  • Consistency over incremental efficiency: a Shareholder’s lot list is recomputed by dropping and rebuilding the entire list on every mutating transfer, rather than incrementing a running total.
  • Numeric integrity: strict consistency checks reject a transfer whose share count doesn’t match its numeric range, or whose amount doesn’t match rate times share count.

1.3 Constraints

  • Both shareholders referenced by a transfer must belong to the same legal entity as the transfer itself.
  • Share-number ranges are unique only within a share type — the same numeric range can exist independently under two different share types.
  • Nothing in this module creates a ledger posting automatically. The equity/liability and asset account fields exist only to pre-fill an optional, manually-triggered voucher.
  • Every operation is range-based; there is no quantity-only decrement independent of the certificate-number range it corresponds to.

2. High-Level Design

2.1 Component diagram

2.2 Transfer-direction branching

The three transfer_type values are not different documents — they are one submittable record whose on_submit/on_cancel logic branches on this field, mutating a different pair of Shareholder records each time:

Note that Purchase never adds a lot anywhere — the seller loses the lot and the Issuer Mirror Record’s own record of it is removed too, so a buyback retires the shares rather than transferring them back to the company as a holder.


3. Deep Dive

3.1 Data model

Shareholder — title, a company link (the legal entity), an is_company flag marking the Issuer Mirror Record, a unique read-only folio_no, contact details, and a read-only share_balance child table (its Share Lots), mutated only by Share Transfer’s submit/cancel logic.

Share Type — a simple named category (title, description) with no fixed enumeration; a test fixture uses “Equity” as an example, but any admin-created name is valid.

Share Balance row (Share Lot)share_type, from_no, to_no (inclusive certificate-number bounds), rate, no_of_shares, amount, plus is_company/current_state fields populated only on the Issuer Mirror Record’s own rows (set to Issued on creation by an Issue) — ordinary holder-side rows leave both blank. The Purchased option exists in the select list but no code path here ever assigns it. Whenever a Shareholder is saved, before_save unconditionally recomputes every row’s amount as no_of_shares × rate.

Share Transfertransfer_type, date, from_shareholder/from_folio_no and to_shareholder/to_folio_no (folio fields fetched read-only from the shareholder), equity_or_liability_account (required; an Equity- or Liability-type ledger account, chosen by the preparer, not enforced by type), asset_account (required unless Transfer), share_type, from_no/to_no, rate, no_of_shares, a read-only computed amount, company, remarks, amended_from. Submittable and amendable.

3.2 Balance recomputation (not incrementation)

Removing or splitting a lot walks every existing row for the affected share type and rebuilds the list from scratch rather than decrementing a counter:

for each existing Share Lot row of this share_type on the Shareholder:
    outside [from_no, to_no]:        keep row unchanged
    [from_no, to_no] covers row:     drop the row entirely
    row covers [from_no, to_no]:     split into up to two surviving
                                      sub-lots, trimmed at each boundary
    partial overlap on one side:     trim row to its surviving portion
# replace the whole child table with the surviving rows and save
doc.share_balance = []
for entry in surviving_rows: doc.append("share_balance", entry)
doc.save()

Adding a lot (Issue, Transfer’s recipient side) is a plain append, never merged with adjacent lots. The pre-submission existence check (share_exists) runs the same range comparison read-only, returning whether the requested range is fully covered, partially covered, or entirely outside the Shareholder’s current lots.

3.3 Folio number handling

A folio number is a unique, read-only identifier on the Shareholder, auto-generated (FN.##### pattern) the first time a transfer references a shareholder that doesn’t yet have one. On every later transfer, the folio number fetched from from_shareholder/to_shareholder is compared against what is stored on the Shareholder itself, and a mismatch is rejected — the transfer carries no folio identity of its own, only a consistency check against the Shareholder’s.

3.4 Manual posting — no automatic ledger integration

Submitting, cancelling, or amending a Share Transfer only ever mutates Share Balance rows. No ledger posting is created automatically anywhere in this module. The only accounting-adjacent affordance is manually invoked once a transfer is submitted and its accounts are set: it assembles a draft two-line voucher (debiting one account, crediting the other, tagged with the shareholder as the party) and hands it to the user as an unsaved document to review and submit themselves through the general posting funnel. There is no stored link from the resulting posting back to the Share Transfer, so matching one to the other afterward is manual.

3.5 Error handling

  • Issue rejected if the requested range already fully or partially exists on the Issuer Mirror Record; Transfer/Purchase rejected if it does not fully exist on the seller’s lots.
  • Seller and buyer must differ; no_of_shares must equal the inclusive range size (to_no − from_no + 1); amount must equal rate × no_of_shares.
  • A folio number mismatch between a transfer and the Shareholder it references is rejected outright.
  • Required fields vary by transfer_type: Purchase needs from_shareholder + asset_account; Issue needs to_shareholder + asset_account; Transfer needs both shareholders.

4. Scale and Reliability

  • Balances are denormalized as per-Shareholder child rows, not a running total in a separate movement ledger; reading a position means loading that Shareholder’s full lot list. Repeated partial transfers fragment a holding into progressively more, smaller lots — nothing here consolidates adjacent lots back together.
  • Every transfer type touches two Shareholder documents per submission (Transfer: from + to; Issue/Purchase: the Issuer Mirror Record plus one holder). No explicit locking or optimistic-concurrency check is visible around these saves, so two transfers concurrently mutating the same Shareholder could race.
  • There is no bulk/batch transfer path — every movement is one submittable record at a time.
  • No observed connection to bank feeds, payment providers, or reconciliation tooling; the only cash-side linkage is the optional, manually-submitted voucher above.

5. Trade-off Analysis

Decision Trade-off
Balance as discrete numbered Share Lots, not a single quantity Preserves which specific certificate numbers a holder owns, at the cost of fragmenting into more rows over time with no consolidation.
Issuing company modeled as an ordinary Shareholder (Issuer Mirror Record), not a counter Reuses the same lot-tracking machinery, but every Issue/Purchase must load and save a second document alongside the actual holder.
No automatic ledger posting; a manual, user-triggered draft voucher instead Avoids assuming a fixed account-mapping policy, but a transfer’s accounting impact can silently drift from the register if the voucher is never created or submitted.
Recompute-by-replace (drop and rebuild the lot list) rather than incremental update Simpler, less error-prone update rule, at the cost of rewriting unrelated rows on every touch.
Folio number as a plain unique field validated by application code No dedicated registrar/sequence service needed, but consistency depends entirely on this module’s validate step, not a database-enforced relationship.

6. What to Revisit as the System Grows

  • Add explicit locking or an optimistic-concurrency check around Share Lot mutations — concurrent transfers on the same Shareholder aren’t currently guarded against.
  • Link a generated voucher back to the Share Transfer that produced it, so the two reconcile automatically instead of by hand.
  • Periodically consolidate adjacent same-rate lots, since repeated partial transfers only ever fragment a holding further.
  • Wire up the Purchased state value on Share Balance rows, or remove it — no code path currently sets it, even though a buyback removes the lot from the Issuer Mirror Record.

This is a narrow, low-traffic subsystem — four small constructs, no adapters, no external integrations, no lifecycle beyond submit/cancel/amend — so this document is intentionally short rather than padded.

Was this page helpful?