About Experience Engineering Projects Infrastructure Blog Contact
ERPNext & Frappe ๐Ÿงฉ 9-Part Course ยท Part 5: Module Integration

ERPNext Module Integration: Accounting & Stock Impact

Published: Aug 30, 2026

๐ŸŒŠ The Complete Data Flow Diagram

Part 1's Module Interconnection Master Diagram and Flagship Document Flows showed which documents feed into which modules. This part goes one layer deeper: what actually happens, step by step, inside the framework, from the moment a user clicks a button to the moment the resulting ledger impact shows up in a report.

Userclicks "Submit" on a form
โ†“
UIthe Desk form view
โ†“
Formcollected field + child table values
โ†“
Client Scriptbrowser-side JS validation/behavior runs first
โ†“
Serverrequest reaches the Frappe backend
โ†“
Permission CheckRole / User / Document permission layers
โ†“
DocType Controllerthe Python class for this DocType
โ†“
Business Logicvalidate(), on_submit() โ€” DocType-specific
โ†“
Databasethe document row itself is written
โ†“
GL Entry / Stock Ledger Entry / Communication Logside-effect records created by the business logic step
โ†“
Reportlist view, report view, or dashboard reads it back
โ†“
Usersees the updated state

This exact shape โ€” UI โ†’ client script โ†’ server โ†’ permission check โ†’ controller โ†’ business logic โ†’ database โ†’ side-effect records โ†’ report โ€” is the same for every module and every DocType in ERPNext. Only two steps ever change: the Business Logic step (what a Sales Invoice's controller does is different from what a Leave Application's controller does) and the resulting side-effect records (GL Entry and Stock Ledger Entry for transaction DocTypes, but a Communication Log entry for a support Issue, or nothing at all for a pure master record like Customer). Once this one diagram is internalized, tracing any new document type through the system is a matter of asking "what does step 8 do here?" rather than learning a new architecture.

๐Ÿ” Document Lifecycle Internals

Every transaction DocType in ERPNext carries a docstatus field with exactly three possible values โ€” this is verified core Frappe behavior, stable across recent versions: 0 = Draft, 1 = Submitted, 2 = Cancelled. What's less obvious from the UI is what actually fires at each transition:

ActiondocstatusWhat happens internally
Savestays 0validate() runs (field-level checks, defaults, computed totals), then before_save, then the record is written to its table with docstatus = 0. No GL Entry or Stock Ledger Entry exists yet for a transaction document at this point.
Submit0 โ†’ 1validate() runs again, then before_submit, then docstatus flips to 1, then on_submit runs. For transaction DocTypes (Sales Invoice, Delivery Note, Stock Entry, Payment Entry, โ€ฆ), on_submit is typically where the actual GL Entry and/or Stock Ledger Entry rows get created โ€” see GL Entry generation and Stock Ledger Entry generation below.
Cancel1 โ†’ 2on_cancel runs, docstatus flips to 2, and โ€” this is the important part โ€” the framework auto-cancels/reverses any GL Entries and Stock Ledger Entries that were linked to this document, rather than leaving them as orphaned postings against a now-cancelled voucher. The linkage that makes this possible is the same voucher_type / voucher_no pairing described in GL Entry generation below.
Amendnew doc at 0Amending a cancelled document does not edit it in place. It creates a fresh new document, copied from the cancelled one's field values, linked back to it via the amended_from field. The original cancelled record is preserved untouched as history rather than rewritten.
๐Ÿ’ก Reverse, don't rewrite history. Cancel-and-reverse instead of edit-in-place, and amend-as-a-new-linked-document instead of mutating the original, are both instances of the same principle covered in depth for the accounting layer specifically in Invoice vs Payment: Audit Trail โ€” ERPNext's document lifecycle is that principle implemented at the framework level, applied to every submittable DocType, not just financial ones.

๐Ÿ’ฐ How a Sales Invoice Becomes a GL Entry

Take a concrete example: a Sales Invoice for a customer with a subtotal of 50,000, VAT of 5,000, and a grand total of 55,000. Conceptually โ€” not claiming to reproduce ERPNext's actual source code line by line โ€” the posting logic on submit does the following: it walks the invoice's line items and tax rows and creates one GL Entry row per side of the transaction that needs to balance.

AccountDebitCredit
Accounts Receivable / Debtors (configured on the Customer or Company)55,000โ€”
Sales / Income account (configured per invoice line item)โ€”50,000
Tax Payable account (configured per tax line)โ€”5,000

Two categories of claim in that table are worth separating clearly:

  • Verified structural behavior: every GL Entry row carries an account, a debit amount, a credit amount, a voucher_type (e.g. "Sales Invoice"), a voucher_no (the specific invoice's name), and an against_voucher field used later for reconciliation โ€” this shape is stable across versions and is what makes the ledger queryable and cancellable at all.
  • Configuration-dependent: which specific account names get used (the Receivable account, the Income account, the Tax account) depends entirely on the Customer, Item Group, Company, and Tax Template setup on the specific site โ€” verify the actual accounts on any real installation rather than assuming the names above.

The full theory behind why debits and credits, journal entries, and the double-entry rule work this way at all is covered independently of ERPNext in Double-Entry Bookkeeping and Journal Entries โ€” this section is that theory's concrete ERPNext implementation.

๐Ÿ“ฆ How a Delivery Note Becomes a Stock Ledger Entry

The stock side follows the same pattern with a different table. Take a Delivery Note moving 10 units of an item out of Warehouse A. On submit, a Stock Ledger Entry row is created carrying: item_code, warehouse, posting_date/posting_time, actual_qty (-10 โ€” negative because this is an outward movement), qty_after_transaction, valuation_rate, and stock_value_difference. Immediately after, the Bin record for that Item + Warehouse combination is updated to reflect the new on-hand quantity.

That last step is the distinction worth being precise about: Stock Ledger Entry is the append-only movement history (every stock-affecting transaction ever posted for that item/warehouse), while Bin is a derived, current-snapshot cache of quantity and valuation kept in sync as ledger entries post. Part 4 covers this relationship in schema terms in the Item ER diagram's Bin vs Stock Ledger Entry distinction.

โš ๏ธ Configuration-dependent: if the item's Item Group/Company is set up for perpetual inventory accounting, a matching GL Entry is also typically created alongside the sale โ€” a debit to Cost of Goods Sold and a credit to the Inventory/Stock asset account โ€” connecting this Delivery Note's stock movement to the Sales Invoice's revenue posting from the previous section. Whether this fires, and which accounts it hits, depends entirely on that site's accounting settings; do not assume it is universally on.

๐Ÿ•ธ๏ธ Internal Connections, Fully Enumerated

Part 3's module tour listed which modules connect to which. Here's the same pairing with a third column added: exactly what record gets created when the connection actually fires.

Module pairTriggerWhat actually gets created
Selling โ†” StockDelivery Note submitStock Ledger Entry (outward) + Bin update
Selling โ†” AccountingSales Invoice submitGL Entry (Receivable / Revenue / Tax)
Buying โ†” StockPurchase Receipt submitStock Ledger Entry (inward) + Bin update
Buying โ†” AccountingPurchase Invoice submitGL Entry (Payable / Expense-or-Inventory / Tax)
Manufacturing โ†” StockWork Order + Job Card โ†’ Stock EntryStock Ledger Entry (material transfer out + finished goods receipt in)
Manufacturing โ†” AccountingManufacture Stock Entry submitGL Entry (WIP / Inventory) โ€” where perpetual inventory accounting is enabled
HR โ†” PayrollAttendance + Salary StructureSalary Slip
Payroll โ†” AccountingPayroll Entry submitGL Entry (Salary Payable / Expense)
Projects โ†” AccountingTimesheet โ†’ billed via Sales InvoiceGL Entry (Receivable / Revenue)

The pattern from Part 1 holds under this closer look too: almost every row's "what gets created" column ends in either GL Entry or Stock Ledger Entry โ€” those two tables really are the settlement layer every module ultimately writes to.

๐Ÿ”Œ External Connections in Depth

Part 1's Internal vs External Connections section sketched the external side in one line. Here's the fuller picture of how ERPNext talks to systems outside its own database:

ERPNext

โ†“exposes / calls out via

REST API

  • /api/resource/<DocType>

Webhooks

  • HTTP POST on doc event

OAuth

  • provider or consumer
โ†“reaches

E-commerce

Payment Gateways

SMS / Email Providers

Other ERPs

MechanismWhat it does
REST APIThe framework-generated /api/resource/<DocType> surface covered in Part 1; full deployment, auth, and troubleshooting depth is in Part 8.
WebhooksFire an HTTP POST to an external URL when a chosen DocType event happens โ€” e.g. "on Sales Invoice submit, notify our Next.js app" โ€” configured per DocType and event without writing a custom app.
OAuthLets ERPNext act as either an OAuth provider (issuing tokens so a third-party app can act on a user's behalf) or an OAuth consumer (logging into ERPNext via an external identity provider).
Integration RequestA logging DocType some integrations use to record outbound/inbound API call attempts โ€” payload, response, status โ€” purely for debugging failed integrations after the fact.
Typical external systemsPayment gateways, e-commerce platforms, SMS/email providers, and other ERPs/systems are the most common consumers of the mechanisms above.

๐Ÿงฎ Reconciliation, ERPNext-Style

This series has stayed mostly framework-specific; the accounting series covers the theory framework-agnostically. Here's where they connect directly: ERPNext's Payment Reconciliation Tool, and the against_voucher field on GL Entry plus the Payment Entry Reference child table, are the concrete implementation of the abstract Payment Allocation and Reconciliation concepts described there.

A short worked example, mirroring the "one payment settles many invoices" scenario from that article's scenario playbook: a single Payment Entry of 100,000 comes in from a customer. Rather than being tied to one invoice, it's allocated across two open Sales Invoices โ€” 40,000 against the first, 60,000 against the second โ€” via two rows in the Payment Entry's Payment Entry Reference child table, each row pointing at its invoice through against_voucher. Both invoices move from "Unpaid" to "Paid" without a single GL Entry needing to guess which invoice a given rupee/dollar/taka belongs to โ€” the reference rows carry that mapping explicitly.

๐Ÿšง Common Integration Mistakes

MistakeWhy it breaks things
Manually creating GL Entry or Stock Ledger Entry records via scriptBypasses the validation, permission checks, and voucher linkage that submitting the proper transaction document provides โ€” and breaks the audit trail, since the "reversal" record on cancel won't know these rows exist
Disabling perpetual inventory accounting mid-yearCreates a valuation gap between transactions posted before and after the switch, since stock value was being tracked in the GL for one period and not the other
Reading Bin for "current stock" in a hot path without understanding its roleBin is a derived cache updated from Stock Ledger Entry, not the source of truth itself โ€” code that needs a guaranteed-accurate, point-in-time quantity should reason from Stock Ledger Entry directly
Manually reversing GL/Stock entries after cancelling a documentCancel already auto-reverses linked GL Entries and Stock Ledger Entries โ€” reversing them again creates duplicate offsetting postings
Not setting up default accounts per Item Group/Warehouse/CompanyPostings silently fall back to whatever default account is configured at a higher level, which is rarely the account anyone actually wanted for that transaction

๐Ÿ—บ๏ธ What's Next

This part assumed the module tour from Part 3 and the schema/table background from Part 4 as prerequisites โ€” if either felt unfamiliar while reading this, they're the right place to go back to first. From here, Part 6 covers how to safely extend the posting logic described above with your own Custom Fields, Client/Server Scripts, and hooks, without ever touching ERPNext core.

โ†‘