Skip to content

Accounting & Finance

Accounting in ERPNext is built on real double-entry bookkeeping. Every transaction — whether a Sales Invoice, Payment Entry, or Stock Entry — produces General Ledger entries that keep your books balanced automatically. In this chapter we set up a full Chart of Accounts for an Indian franchise ice cream business, configure GST, create invoices and payments programmatically, and explore multi-company consolidated reporting.

If you’re coming from a Node.js stack, think of the GL as an append-only ledger the framework writes for you: you never hand-craft balancing rows the way you’d build double-entry logic by hand in an Express service. You post a business document; ERPNext derives the balanced GL entries.

ERPNext organises accounts in a tree hierarchy. Every account belongs to one of five root types:

Root TypePurposeExamples
AssetWhat the business ownsBank, Cash, Inventory, Equipment
LiabilityWhat the business owesCreditors, GST Payable, Loans
EquityOwner’s stakeCapital, Retained Earnings
IncomeRevenue streamsSales, Service Income, Interest
ExpenseCosts incurredCOGS, Rent, Salaries, Marketing

Within the tree, nodes are either Groups (containers) or Ledgers (leaf accounts where transactions post). You can only post transactions to Ledger accounts.

Each account also carries an account_type that tells ERPNext how to treat it:

Account TypeUsed For
ReceivableCustomer outstanding (Debtors)
PayableSupplier outstanding (Creditors)
BankBank accounts for reconciliation
CashCash-in-hand accounts
StockInventory valuation accounts
Cost of Goods SoldCOGS posting from stock transactions
TaxGST, TDS, and other tax ledgers
DepreciationFixed asset depreciation

Setting up the Chart of Accounts for ScoopJoy

Section titled “Setting up the Chart of Accounts for ScoopJoy”

When you create a Company in ERPNext with country set to India, the system bootstraps a standard Indian COA from a JSON template stored at erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard.json. You then customise it.

Here is the target structure for ScoopJoy Ice Creams Pvt Ltd:

Application of Funds (Assets) [Asset]
Current Assets
Accounts Receivable
Debtors - SJ
Bank Accounts
HDFC Current Account - SJ
Razorpay Settlement Account - SJ
Cash In Hand
Cash - SJ
POS Cash - SJ
Stock Assets
Stock In Hand - SJ
Loans and Advances
TDS Receivable - SJ
Fixed Assets
Furniture and Fixtures - SJ
Kitchen Equipment - SJ
Refrigeration Equipment - SJ
Accumulated Depreciation - SJ
Source of Funds (Liabilities) [Liability]
Current Liabilities
Accounts Payable
Creditors - SJ
Duties and Taxes
Output Tax CGST - SJ
Output Tax SGST - SJ
Output Tax IGST - SJ
Input Tax CGST - SJ
Input Tax SGST - SJ
Input Tax IGST - SJ
TDS Payable - SJ
Provisions
Provision for Expenses - SJ
Equity [Equity]
Capital Account
Share Capital - SJ
Retained Earnings - SJ
Income [Income]
Direct Income
Sales - SJ
Franchise Royalty Income - SJ
Indirect Income
Interest Income - SJ
Expenses [Expense]
Direct Expenses
Cost of Goods Sold - SJ
Stock Adjustment - SJ
Indirect Expenses
Rent - SJ
Salaries - SJ
Electricity - SJ
Marketing - SJ
Delivery Charges - SJ
Depreciation - SJ
Miscellaneous Expenses - SJ

Accounts are just DocTypes, so you create them with the ORM like any other record:

Create a ledger account under Indirect Expenses
import frappe
account = frappe.get_doc({
"doctype": "Account",
"account_name": "Packaging Expenses",
"parent_account": "Indirect Expenses - SJ",
"root_type": "Expense",
"account_type": "",
"is_group": 0,
"company": "ScoopJoy Ice Creams Pvt Ltd"
})
account.insert()
frappe.db.commit()

The same thing over the auto-generated REST API:

Terminal window
curl -X POST https://erp.scoopjoy.com/api/resource/Account \
-H "Authorization: token api_key:api_secret" \
-H "Content-Type: application/json" \
-d '{
"account_name": "Packaging Expenses",
"parent_account": "Indirect Expenses - SJ",
"root_type": "Expense",
"is_group": 0,
"company": "ScoopJoy Ice Creams Pvt Ltd"
}'

Before any accounting works, you need a Company and a Fiscal Year.

Create the company
company = frappe.get_doc({
"doctype": "Company",
"company_name": "ScoopJoy Ice Creams Pvt Ltd",
"abbr": "SJ",
"country": "India",
"default_currency": "INR",
"chart_of_accounts": "Standard",
"enable_perpetual_inventory": 1, # Stock posts to GL automatically
"default_receivable_account": "Debtors - SJ",
"default_payable_account": "Creditors - SJ",
"default_income_account": "Sales - SJ",
"default_expense_account": "Cost of Goods Sold - SJ",
"default_cash_account": "Cash - SJ",
"cost_center": "Main - SJ"
})
company.insert()
Create the fiscal year
fy = frappe.get_doc({
"doctype": "Fiscal Year",
"year": "2025-2026",
"year_start_date": "2025-04-01",
"year_end_date": "2026-03-31",
"companies": [
{"company": "ScoopJoy Ice Creams Pvt Ltd"}
]
})
fy.insert()

India’s fiscal year runs April to March. ERPNext enforces this across all accounting reports.

Journal Entry: manual double-entry bookkeeping

Section titled “Journal Entry: manual double-entry bookkeeping”

A Journal Entry is used when no specialised document (Sales Invoice, Payment Entry) fits. Common uses: adjustments, write-offs, provisions, opening balances.

Say the founders invest INR 50,00,000 into the business. We debit the bank and credit share capital:

Record initial capital investment
je = frappe.get_doc({
"doctype": "Journal Entry",
"posting_date": "2025-04-01",
"company": "ScoopJoy Ice Creams Pvt Ltd",
"accounts": [
{
"account": "HDFC Current Account - SJ",
"debit_in_account_currency": 5000000,
"credit_in_account_currency": 0
},
{
"account": "Share Capital - SJ",
"debit_in_account_currency": 0,
"credit_in_account_currency": 5000000
}
],
"user_remark": "Initial capital investment by founders"
})
je.insert()
je.submit()

The GL entries produced:

AccountDebit (INR)Credit (INR)
HDFC Current Account - SJ50,00,000
Share Capital - SJ50,00,000

This is the most common accounting flow for retail ice cream sales. The document posts revenue, tax, and (because Perpetual Inventory is on) the COGS side too — then a Payment Entry settles the receivable.

Sales Invoice → Payment posting flow
Rendering diagram…
  1. Create a Sales Invoice. Via the UI: Accounts > Sales Invoice > New. Programmatically (a POS sale):

    POS Sales Invoice
    si = frappe.get_doc({
    "doctype": "Sales Invoice",
    "customer": "Walk-in Customer",
    "posting_date": "2025-07-15",
    "posting_time": "14:30:00",
    "company": "ScoopJoy Ice Creams Pvt Ltd",
    "currency": "INR",
    "selling_price_list": "Standard Selling",
    "debit_to": "Debtors - SJ",
    "is_pos": 1,
    "pos_profile": "ScoopJoy Outlet 1 POS",
    "cost_center": "Outlet 1 - SJ",
    "taxes_and_charges": "In-State GST - SJ",
    "items": [
    {
    "item_code": "SCOOP-VAN-REG",
    "item_name": "Vanilla Ice Cream - Regular",
    "qty": 2,
    "rate": 120,
    "warehouse": "Outlet 1 - SJ",
    "cost_center": "Outlet 1 - SJ"
    },
    {
    "item_code": "SCOOP-CHOC-LRG",
    "item_name": "Chocolate Ice Cream - Large",
    "qty": 1,
    "rate": 180,
    "warehouse": "Outlet 1 - SJ",
    "cost_center": "Outlet 1 - SJ"
    }
    ],
    "payments": [
    {
    "mode_of_payment": "Cash",
    "amount": 495.60 # after GST
    }
    ]
    })
    si.insert()
    si.submit()
    frappe.db.commit()
    print(f"Invoice: {si.name}, Grand Total: {si.grand_total}")

    The submitted Sales Invoice generates these GL entries:

    AccountDebit (INR)Credit (INR)
    Debtors - SJ495.60
    Sales - SJ420.00
    Output Tax CGST - SJ37.80
    Output Tax SGST - SJ37.80
    Cost of Goods Sold - SJ210.00
    Stock In Hand - SJ210.00

    (The COGS entries appear because Perpetual Inventory is enabled.)

  2. Create a Payment Entry for non-POS invoices where payment comes later:

    Payment Entry from a Sales Invoice
    from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
    pe = get_payment_entry("Sales Invoice", si.name)
    pe.mode_of_payment = "Bank Transfer"
    pe.paid_to = "HDFC Current Account - SJ"
    pe.reference_no = "UTR-20250715-001"
    pe.reference_date = "2025-07-15"
    pe.insert()
    pe.submit()

The buying side mirrors selling. A Purchase Invoice for raw materials credits the supplier (Creditors) and a Payment Entry settles it.

Purchase Invoice for raw materials
pi = frappe.get_doc({
"doctype": "Purchase Invoice",
"supplier": "Fresh Dairy Suppliers",
"posting_date": "2025-07-10",
"company": "ScoopJoy Ice Creams Pvt Ltd",
"credit_to": "Creditors - SJ",
"taxes_and_charges": "In-State GST Purchase - SJ",
"items": [
{
"item_code": "RM-MILK-FULL",
"item_name": "Full Cream Milk",
"qty": 500,
"rate": 55,
"uom": "Litre",
"warehouse": "Central Kitchen - SJ",
"expense_account": "Cost of Goods Sold - SJ"
},
{
"item_code": "RM-SUGAR",
"item_name": "Sugar",
"qty": 50,
"rate": 42,
"uom": "Kg",
"warehouse": "Central Kitchen - SJ",
"expense_account": "Cost of Goods Sold - SJ"
}
]
})
pi.insert()
pi.submit()
Pay the supplier
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
pe = get_payment_entry("Purchase Invoice", pi.name)
pe.mode_of_payment = "Bank Transfer"
pe.paid_from = "HDFC Current Account - SJ"
pe.reference_no = "NEFT-20250712-042"
pe.reference_date = "2025-07-12"
pe.insert()
pe.submit()

For a franchise model, set up a parent company with child companies for each outlet:

ScoopJoy Ice Creams Pvt Ltd (Parent)
├── ScoopJoy Outlet 1 - Indiranagar
├── ScoopJoy Outlet 2 - Koramangala
└── ScoopJoy Outlet 3 - MG Road
Create a child company
outlet = frappe.get_doc({
"doctype": "Company",
"company_name": "ScoopJoy Outlet 1 - Indiranagar",
"abbr": "SJ1",
"country": "India",
"default_currency": "INR",
"parent_company": "ScoopJoy Ice Creams Pvt Ltd",
"chart_of_accounts": "Standard",
"enable_perpetual_inventory": 1
})
outlet.insert()

Each child company gets its own Chart of Accounts, Cost Centers, and independent books while rolling up into consolidated reports.

Inter-company transactions automatically mirror a posting from one company into its counterpart. To set them up:

  1. Mark accounts as Inter Company Account in the Chart of Accounts of both companies.
  2. Create internal customers/suppliers linking the two companies.

Suppose Outlet 1 pays a 5% royalty to the parent company on monthly sales of INR 8,00,000 — that’s INR 40,000:

Inter-company royalty Journal Entry (Outlet 1)
je = frappe.get_doc({
"doctype": "Journal Entry",
"voucher_type": "Inter Company Journal Entry",
"posting_date": "2025-07-31",
"company": "ScoopJoy Outlet 1 - Indiranagar",
"accounts": [
{
"account": "Franchise Royalty Expense - SJ1",
"debit_in_account_currency": 40000,
"cost_center": "Outlet 1 - SJ1"
},
{
"account": "Inter Company Payable - SJ1",
"party_type": "Supplier",
"party": "ScoopJoy Ice Creams Pvt Ltd (Internal)",
"credit_in_account_currency": 40000
}
],
"inter_company_journal_entry_reference": ""
})
je.insert()
je.submit()

When submitted, ERPNext automatically creates a mirror entry in the parent company:

Parent Company GLDebitCredit
Inter Company Receivable - SJ40,000
Franchise Royalty Income - SJ40,000

ERPNext provides Bank Reconciliation under Accounts > Bank Reconciliation. The workflow:

  1. Import a bank statement (CSV/OFX) or connect via Plaid integration.
  2. ERPNext creates Bank Transaction records for each statement line.
  3. Match each Bank Transaction against Payment Entries, Journal Entries, or Expense Claims.
  4. Reconcile matched entries.

In v16, accounting reports are faster thanks to Frappe Caffeine caching, and better periodic inventory automation reduces month-end reconciliation work. The community Mint app (open-source) adds fuzzy matching, bulk reconciliation, and undo history on top of ERPNext’s built-in flow.

Reconcile a bank transaction programmatically
bt = frappe.get_doc("Bank Transaction", "BT-2025-00142")
bt.append("payment_entries", {
"payment_document": "Payment Entry",
"payment_entry": "PE-2025-00089",
"allocated_amount": 15000
})
bt.save()

India compliance in ERPNext is handled via the India Compliance app (india_compliance), which auto-configures GST accounts, HSN codes, and tax templates.

ERPNext creates separate accounts for sales (output) and purchase (input) GST:

AccountTypeUsed In
Output Tax CGSTTaxSales Invoices (intra-state)
Output Tax SGSTTaxSales Invoices (intra-state)
Output Tax IGSTTaxSales Invoices (inter-state)
Input Tax CGSTTaxPurchase Invoices (intra-state)
Input Tax SGSTTaxPurchase Invoices (intra-state)
Input Tax IGSTTaxPurchase Invoices (inter-state)

You need two Sales Tax Templates and two Purchase Tax Templates. Intra-state sales split GST into CGST + SGST:

In-state sales template (CGST + SGST)
tax_template = frappe.get_doc({
"doctype": "Sales Taxes and Charges Template",
"title": "In-State GST - SJ",
"company": "ScoopJoy Ice Creams Pvt Ltd",
"tax_category": "In-State",
"taxes": [
{
"charge_type": "On Net Total",
"account_head": "Output Tax CGST - SJ",
"description": "CGST @ 9%",
"rate": 9
},
{
"charge_type": "On Net Total",
"account_head": "Output Tax SGST - SJ",
"description": "SGST @ 9%",
"rate": 9
}
]
})
tax_template.insert()

Inter-state sales use a single IGST line:

Out-of-state sales template (IGST)
tax_template_igst = frappe.get_doc({
"doctype": "Sales Taxes and Charges Template",
"title": "Out-of-State GST - SJ",
"company": "ScoopJoy Ice Creams Pvt Ltd",
"tax_category": "Out-State",
"taxes": [
{
"charge_type": "On Net Total",
"account_head": "Output Tax IGST - SJ",
"description": "IGST @ 18%",
"rate": 18
}
]
})
tax_template_igst.insert()

Attach the template and the per-item HSN code; ERPNext computes the tax:

GST-compliant Sales Invoice
si = frappe.get_doc({
"doctype": "Sales Invoice",
"customer": "Café Bliss",
"posting_date": "2025-07-15",
"company": "ScoopJoy Ice Creams Pvt Ltd",
"currency": "INR",
"debit_to": "Debtors - SJ",
# Tax category auto-detects from addresses
"taxes_and_charges": "In-State GST - SJ",
"items": [
{
"item_code": "SCOOP-MIX-5L",
"item_name": "Vanilla Ice Cream Mix - 5L Tub",
"qty": 20,
"rate": 450,
"warehouse": "Central Kitchen - SJ",
"gst_hsn_code": "21050000", # HSN code for ice cream
"cost_center": "Main - SJ"
}
]
})
si.insert()
si.submit()
# Grand Total: 9000 + 810 (CGST) + 810 (SGST) = 10620

ERPNext v16 has a refactored TDS module with simplified workflows. Tag a supplier with a withholding category:

Enable TDS on a supplier
supplier = frappe.get_doc("Supplier", "Marketing Agency XYZ")
supplier.tax_withholding_category = "TDS - 194C - Contractors"
supplier.save()

When you create a Purchase Invoice for this supplier, ERPNext automatically calculates the TDS amount based on the withholding category’s thresholds and rates.

For items with non-standard GST rates (e.g., 5% on basic food items vs. 18% on premium products):

5% GST Item Tax Template
item_tax = frappe.get_doc({
"doctype": "Item Tax Template",
"title": "GST 5% - SJ",
"company": "ScoopJoy Ice Creams Pvt Ltd",
"taxes": [
{
"tax_type": "Output Tax CGST - SJ",
"tax_rate": 2.5
},
{
"tax_type": "Output Tax SGST - SJ",
"tax_rate": 2.5
},
{
"tax_type": "Output Tax IGST - SJ",
"tax_rate": 5
}
]
})
item_tax.insert()

Assign this template to items in the Item master under the Taxes tab.

Cost Centers let you track profitability by outlet, department, or project — independent of the Chart of Accounts hierarchy.

ScoopJoy Ice Creams Pvt Ltd (root)
├── Main - SJ (Head Office)
├── Central Kitchen - SJ (Production)
├── Outlet 1 - SJ (Indiranagar)
├── Outlet 2 - SJ (Koramangala)
├── Outlet 3 - SJ (MG Road)
└── Online Orders - SJ (Swiggy/Zomato)
Create a Cost Center
cc = frappe.get_doc({
"doctype": "Cost Center",
"cost_center_name": "Outlet 1",
"company": "ScoopJoy Ice Creams Pvt Ltd",
"parent_cost_center": "ScoopJoy Ice Creams Pvt Ltd - SJ",
"is_group": 0
})
cc.insert()

Every transaction (invoice, journal entry, expense claim) can tag a cost center. Then use Accounts > Profitability Analysis to see P&L per cost center.

Fetch P&L for one cost center via the report API
from frappe.utils import getdate
result = frappe.call(
"erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement.execute",
filters={
"company": "ScoopJoy Ice Creams Pvt Ltd",
"filter_based_on": "Fiscal Year",
"fiscal_year": "2025-2026",
"cost_center": "Outlet 1 - SJ",
"periodicity": "Monthly"
}
)
columns, data = result

Budgets control spending per cost center and account.

Set up an annual budget
budget = frappe.get_doc({
"doctype": "Budget",
"budget_against": "Cost Center",
"cost_center": "Outlet 1 - SJ",
"company": "ScoopJoy Ice Creams Pvt Ltd",
"fiscal_year": "2025-2026",
"action_if_annual_budget_exceeded": "Stop",
"action_if_accumulated_monthly_budget_exceeded": "Warn",
"accounts": [
{
"account": "Marketing - SJ",
"budget_amount": 120000 # 1.2 lakh per year
},
{
"account": "Rent - SJ",
"budget_amount": 600000 # 6 lakh per year
},
{
"account": "Electricity - SJ",
"budget_amount": 180000
}
]
})
budget.insert()
budget.submit()

When anyone tries to post an expense to Marketing - SJ for Outlet 1 that would exceed the budget, ERPNext will block the transaction (Stop) or show a warning (Warn) based on your configuration.

ERPNext includes all standard financial reports out of the box:

ReportPathPurpose
Profit & LossAccounts > P&L StatementRevenue minus expenses for a period
Balance SheetAccounts > Balance SheetAssets, liabilities, equity snapshot
Trial BalanceAccounts > Trial BalanceDebit/credit balances of all accounts
Cash FlowAccounts > Cash FlowCash inflows/outflows by activity
General LedgerAccounts > General LedgerAll GL entries with filters
Accounts ReceivableAccounts > Accounts ReceivableCustomer-wise outstanding
Accounts PayableAccounts > Accounts PayableSupplier-wise outstanding
Budget VarianceAccounts > Budget VarianceActual vs. budgeted amounts

You can also pull GL data directly with the ORM:

General Ledger for one account
gl_entries = frappe.get_all("GL Entry",
filters={
"company": "ScoopJoy Ice Creams Pvt Ltd",
"account": "Sales - SJ",
"posting_date": ["between", ["2025-07-01", "2025-07-31"]]
},
fields=["posting_date", "voucher_type", "voucher_no", "debit", "credit"],
order_by="posting_date asc"
)

For multi-company setups, ERPNext generates consolidated reports across the parent and all child companies. Navigate to any financial report and select Filter by Company Group to see the consolidated view. ERPNext handles:

  • Currency conversion for child companies in different currencies.
  • Elimination of inter-company transactions.
  • Roll-up of all child company balances.
Consolidated Trial Balance
result = frappe.call(
"erpnext.accounts.report.consolidated_financial_statement.consolidated_financial_statement.execute",
filters={
"company": "ScoopJoy Ice Creams Pvt Ltd",
"filter_based_on": "Fiscal Year",
"fiscal_year": "2025-2026",
"periodicity": "Yearly"
}
)

At the end of each accounting period (month, quarter, or year), use the Period Closing Voucher to transfer net profit/loss to a closing account.

Year-end Period Closing Voucher
pcv = frappe.get_doc({
"doctype": "Period Closing Voucher",
"posting_date": "2026-03-31",
"transaction_date": "2026-03-31",
"fiscal_year": "2025-2026",
"company": "ScoopJoy Ice Creams Pvt Ltd",
"closing_account_head": "Retained Earnings - SJ",
"remarks": "Closing FY 2025-2026"
})
pcv.insert()
pcv.submit()

This zeroes out all Income and Expense accounts and posts the net balance to Retained Earnings, preparing the books for the new fiscal year.

The year-end checklist:

  1. Reconcile all bank accounts.
  2. Verify stock valuation matches GL (Stock Balance vs. Stock In Hand account).
  3. Process all pending depreciation entries.
  4. Review and clear suspense accounts.
  5. Submit the Period Closing Voucher.
  6. Create the next Fiscal Year.
  7. Verify opening balances carry forward correctly.

ERPNext supports payment gateways (Razorpay, Stripe, PayPal) for online payments. Configuration is covered in detail in Chapter 22, but the accounting flow is:

  1. The customer receives a Payment Request via email/SMS.
  2. The customer pays via the gateway.
  3. ERPNext auto-creates a Payment Entry on successful payment.
  4. The amount posts to your Razorpay Settlement Account.
  5. When the settlement hits your bank, reconcile via Bank Reconciliation.
FeatureDescription
Customizable Financial StatementsDesign custom P&L, BS, and CF layouts without code
Consolidated Trial BalanceUnified multi-company view with auto currency conversion
Purchase Expense BookingSegregated COGS vs. service expenses, stock accounting by item group
Enhanced BudgetingReal-time enforcement at transaction time, automated alerts
Refactored TDSSimplified Indian TDS compliance workflow
Refactored Asset ModuleCleaner depreciation and compliance
Landed Cost for Stock EntriesApply freight/duties to manufacturing and subcontracting entries
Periodic Inventory AutomationAutomated journal entries for periodic inventory adjustments
Improved POSFaster billing, better offline stability, smoother scanning
Performance (Frappe Caffeine)Up to 2× faster report generation via caching architecture