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

Customer Statement of Accounts Generation

A design reference for batch-generating and emailing periodic customer statements

1. Requirements

1.1 Functional requirements

  • Produce a per-customer statement in one of two bodies, selected per batch: a General Ledger body (transaction detail over a from/to date window) or an Accounts Receivable body (an as-of-date outstanding/ageing view), each computed by the reporting engines it shares with the rest of the accounting module. This design covers only recipient assembly, rendering, and delivery — not how those engines compute their rows.
  • Assemble recipients one at a time, or in bulk via a Recipient Collection (Customer Group, Territory, Sales Partner, or Sales Person), fetching every matching, non-disabled customer in one action.
  • Resolve, per recipient, a Billing Contact address (from the contact record flagged as that customer’s billing contact) and the customer master’s own primary address, both editable afterward.
  • Optionally CC a fixed list of internal users, independent of the customers on the batch.
  • Filter by legal entity, ledger account, cost centers, projects (General Ledger mode only), finance book, presentation currency, and mode-specific filters (payment-terms template, sales partner/person, territory for the receivable mode; categorize-by-voucher for the ledger mode).
  • Optionally append an Ageing Summary — fixed 30/60/90/120-day buckets — to either body.
  • Render each statement to PDF via a Jinja print layout: the built-in default, an explicit print format (validated at save to be Jinja, enabled, and matching the chosen mode), or a layout registered by another installed module.
  • Deliver via two manual actions (email each recipient individually, or download one combined PDF) and one scheduled path: an Enable Auto Email flag with a frequency (Daily/Weekly/Biweekly/Monthly/Quarterly) and start date, evaluated daily, advancing its own date window forward by that cadence after each automatic send.

1.2 Non-functional requirements

  • Fail-open per recipient: a customer with no activity, or no resolvable address, is skipped silently rather than aborting the whole send.
  • Scoping integrity: ledger account, cost centers, and projects are validated at save to belong to the batch’s own legal entity.
  • Template safety: subject/body/filename templates are Jinja, validated at save with restricted globals.
  • Traceability: a successful automatic run leaves a comment noting when it fired.
  • No-code configurability: recipients, filters, cadence, and templates are all editable fields, not code.

1.3 Constraints

  • One batch is scoped to exactly one legal entity — no cross-company fan-out in a single run.
  • Only the two report bodies above plug into this mechanism; there is no generic “any report” option.
  • Recipients are always customers — a receivables-facing mechanism only.
  • Evaluation is daily-only, with no catch-up if a day is missed (see §4).

2. High-Level Design

2.1 Component diagram

2.2 Recipient email resolution (a short procedure, not a graph)

  1. Bulk fetch: pull every non-disabled customer under the selected Group/Territory (hierarchy) or Sales Partner/Person (team assignment); if a primary contact is required, customers without one are dropped.
  2. Resolve two addresses: the customer master’s own contact email (primary), and the email on the most recently created Contact flagged as billing contact (billing).
  3. Cache both on the recipient row, editable afterward — resolution does not re-run automatically if the underlying Contact/Customer changes.
  4. At send time, the recipient list is the billing address(es) plus, if required, the primary address. No address on either → the customer is skipped silently.
{ customer:       <link to Customer>,
  primary_email:   <read-only, from the Customer master>,
  billing_email:   <editable, from the billing Contact> }

3. Deep Dive

3.1 Data model

Statement Run — the batch record: one legal entity, report mode, date window (from_date/to_date for the ledger body, posting_date for the receivable body), filters, print/email templates, and the auto-email cadence.

Statement Recipient — one row per customer: the link plus resolved primary/billing addresses.

CC Recipient — internal users cc’d on every message the batch sends.

Cost Center Filter / Project Filter — multi-select child rows, validated at save to belong to the run’s legal entity. The Ledger Account filter is validated the same way — this mechanism parameterizes the shared chart of accounts rather than introducing its own.

3.2 The two report modes, and the schedule-advance algorithm

The General Ledger body draws on the posting funnel described in the general-ledger design: a date-bounded, per-party listing with a fixed opening/closing/total scaffold. A result of only that three-row scaffold means no activity in the window, and the customer is skipped. Presentation currency falls back explicit currency → party’s own account currency → legal entity’s base currency.

The Accounts Receivable body is the as-of-date outstanding/ageing view built from Party Balance Entries; an empty result skips the customer the same way. The Ageing Summary, when enabled, is computed separately from the same as-of date and appended regardless of mode. Two filters (ignore exchange-rate-revaluation journals, ignore system credit/debit notes) apply uniformly to both engines.

The scheduled path is the one branching flow worth diagramming:

3.3 Rendering and delivery

Each body is wrapped in a base print scaffold, with letterhead and terms-and-conditions merged in if configured, in the chosen orientation. Template order: an explicit print format overrides a layout registered by another installed module, which overrides the module’s own default for that mode.

The download path joins every recipient’s HTML into one document (optional page-break between statements) and renders one PDF. The email path renders each recipient’s HTML to its own PDF and attaches it individually. Subject/body/filename are rendered per recipient from Jinja templates with both the batch record and the recipient’s customer record in context; each message is queued from a configured mailbox or the acting user’s address, tagged back to the originating batch.

3.4 Error handling

  • Save-time validation catches most misconfiguration (company mismatches, invalid print format, malformed templates) before any send is attempted.
  • Zero-content and zero-address customers are skipped per-recipient; the batch overall still reports success.
  • CC resolution failures (a stale user reference) are caught and simply shorten the CC list rather than failing the send.
  • The per-recipient generation loop is not itself wrapped in a try/except — an unexpected failure on one customer’s data would surface as a failure for the whole batch rather than being isolated. A gap, not a documented safeguard.

4. Scale and Reliability

  • Load shape: low-frequency, bursty — one evaluation per day per batch, fanning into one report computation and PDF render per recipient; cost scales linearly with recipient count.
  • Synchronous-then-async: report computation and rendering happen inline before any message is queued, so a large recipient list has real compute cost before delivery starts.
  • No catch-up on a missed day: the sweep only matches a window-end date equal to today; a missed run means the batch silently never fires again without manual intervention.
  • No de-duplication across reruns: the date-advance step is what makes a normal daily run self-limiting, not an explicit guard.
  • Single-legal-entity scope keeps each batch cheap to evaluate, but multi-company operators need one record per company.

5. Trade-off Analysis

Decision Trade-off
Two hard-coded report modes, not a pluggable registry Simple and covers the common case, but a third statement type needs a code change, not a registration.
Recipient addresses cached at fetch/edit time, not re-resolved at send Lets an operator hand-correct before sending, but a changed email upstream goes stale until someone re-fetches.
Fail-open, skip-silently semantics The batch always completes, but there is no per-recipient send report to say who was skipped or why.
Daily-only scheduler granularity Matches finance cadences with minimal scheduling machinery, but a missed day has no automatic catch-up.
Company-scoped single batch, no cross-entity fan-out Consistent with independent per-entity books, but one record per company to maintain.
Save-time template/format validation over a stricter sandbox Catches misconfiguration early and allows rich Jinja personalization, trusting “restricted globals” as the boundary.

6. What to Revisit as the System Grows

  • Isolate per-recipient failures so one malformed customer/contact record can’t abort the rest of the batch.
  • Add a per-recipient send report (sent / skipped-no-content / skipped-no-address / failed) — today’s only feedback is a boolean and one comment.
  • Add catch-up for missed scheduler days by treating a past-due window-end date as still due, not requiring an exact match.
  • Re-resolve addresses at send time instead of trusting a cached row value.
  • Add an explicit idempotency guard around the send-then-advance sequence.

This is a narrowly-scoped, Low-priority mechanism; the source honestly supports a document at this length rather than a longer one.

Was this page helpful?