ERPNext and the Frappe Framework
Fully open source (GPLv3), Python + MariaDB, and metadata-driven to an unusual degree. The best system to study if you want to understand how ERPs are built, because nothing is hidden.
What it is
- Frappe — a full-stack, metadata-driven web framework
- ERPNext — the ERP application built on Frappe
- GPLv3, no paid edition of the core (Frappe Cloud is hosting, not features)
- Indian-origin (Frappe Technologies), strong adoption in South Asia, Africa, and among SMBs globally
- Genuinely usable for a real small-to-mid business
The DocType — the central idea
Everything is a DocType: a metadata definition that generates the database table, the form, the list view, the REST API, and the permission model.
{
"doctype": "DocType",
"name": "Credit Review",
"module": "Selling",
"naming_rule": "Expression",
"autoname": "CR-.YYYY.-.#####",
"is_submittable": 1,
"fields": [
{ "fieldname": "customer", "fieldtype": "Link", "options": "Customer",
"label": "Customer", "reqd": 1, "in_list_view": 1 },
{ "fieldname": "review_date", "fieldtype": "Date", "label": "Review Date",
"default": "Today" },
{ "fieldname": "credit_limit", "fieldtype": "Currency", "label": "Credit Limit" },
{ "fieldname": "risk_score", "fieldtype": "Int", "label": "Risk Score",
"read_only": 1 },
{ "fieldname": "decision", "fieldtype": "Select",
"options": "Approve\nReject\nRefer", "label": "Decision" }
],
"permissions": [
{ "role": "Sales Manager", "read": 1, "write": 1, "create": 1, "submit": 1 }
]
}
Create that and you get: a table, a form UI, a list view with filters, a REST API, permissions, an audit trail, a comment thread, attachments, and workflow support. No code.
This is metadata-driven ERP taken further than most commercial products, and it’s why studying Frappe teaches you the pattern so clearly.
Field types
Data, Text, Small Text, Long Text, Text Editor, Markdown Editor, Code, HTML, Int, Float, Currency, Percent, Check, Date, Datetime, Time, Duration, Select, Link (foreign key), Dynamic Link (polymorphic FK), Table (child table — the line items), Table MultiSelect, Attach, Attach Image, Signature, Geolocation, Rating, Password, Barcode, JSON.
Link and Table are the two that matter. Link gives you a validated foreign key with a searchable picker for free; Table gives you the header/line-item pattern that every ERP document needs.
The submittable document pattern
The cleanest expression of the ERP draft/posted boundary that any product implements:
docstatus = 0 DRAFT editable, deletable, no ledger effect
docstatus = 1 SUBMITTED immutable, posts to the ledger, has effects
docstatus = 2 CANCELLED reversed, still visible, linked to an amendment
class SalesInvoice(Document):
def validate(self):
"""Runs on every save (draft or submit)"""
self.calculate_totals()
self.validate_credit_limit()
def on_submit(self):
"""Runs once, when moving 0 → 1. Post to the ledger here."""
self.make_gl_entries()
self.update_stock_ledger()
def on_cancel(self):
"""Runs on 1 → 2. Reverse everything."""
self.make_gl_entries(cancel=True)
self.update_stock_ledger(cancel=True)
Amendment is the correction pattern: a cancelled document can be amended into a new document (INV-0001-1) that references the original. Original preserved, correction linked, audit trail intact. This is exactly the pattern described in Documents and Document Flow, implemented cleanly.
Server-side code
import frappe
from frappe.model.document import Document
from frappe.utils import flt, getdate
class CreditReview(Document):
def validate(self):
self.risk_score = self.calculate_risk()
def calculate_risk(self):
overdue = frappe.db.sql("""
SELECT COALESCE(SUM(outstanding_amount), 0)
FROM `tabSales Invoice`
WHERE customer = %s AND docstatus = 1
AND outstanding_amount > 0 AND due_date < CURDATE()
""", self.customer)[0][0]
limit = flt(self.credit_limit) or 1
return min(100, int(flt(overdue) / limit * 100))
def on_submit(self):
if self.decision == "Reject":
frappe.db.set_value("Customer", self.customer, "disabled", 1)
frappe.msgprint(f"Customer {self.customer} has been blocked.")
@frappe.whitelist() # exposes it as an API endpoint
def get_exposure(customer):
return frappe.db.get_value("Customer", customer, "outstanding_amount")
Hooks
hooks.py wires your app into the framework without modifying core:
doc_events = {
"Sales Order": {
"validate": "my_app.credit.check_limit",
"on_submit": "my_app.credit.log_approval",
}
}
scheduler_events = {
"daily": ["my_app.tasks.recalculate_risk_scores"],
"cron": {"0 2 * * *": ["my_app.tasks.nightly_reconciliation"]},
}
override_doctype_class = {"Sales Invoice": "my_app.overrides.CustomSalesInvoice"}
Client-side
frappe.ui.form.on('Sales Order', {
customer(frm) {
frappe.call({
method: 'my_app.credit.get_exposure',
args: { customer: frm.doc.customer },
callback(r) {
if (r.message > 500000) {
frm.dashboard.set_headline_alert(
`High exposure: ${format_currency(r.message)}`, 'orange'
);
}
}
});
},
refresh(frm) {
if (frm.doc.docstatus === 1) {
frm.add_custom_button('Credit Review', () => {
frappe.new_doc('Credit Review', { customer: frm.doc.customer });
});
}
}
});
Built-in framework features
You get these for free on every DocType, which is a lot of what makes Frappe interesting:
- REST API —
/api/resource/Sales Order/SO-0001, full CRUD, filters, and field selection - Role and permission system — role-based, with permission levels per field, user permissions for row-level restriction, and share-per-document
- Workflow engine — states, transitions, and role-based actions, defined as data
- Print formats — Jinja/HTML templates with a visual builder
- Report builder — user-defined list and group-by reports; Query Reports for SQL; Script Reports for Python
- Dashboards and charts
- Background jobs — Redis Queue with short/default/long queues
- Scheduler — cron-style, defined in hooks
- Email — inbound and outbound, threaded onto documents
- Comments and mentions — the chatter/timeline on every document
- Versioning — full field-level change history on every document, automatically
- Naming series — configurable document numbering
- Custom fields and Property Setter — customise without code, and it survives updates
- Multi-company, multi-currency, multi-language
- Bench CLI — site management, migration, backup, app install
The automatic version history on every document is exactly the audit trail an ERP needs, and getting it for free on custom DocTypes is a genuine advantage.
Architecture
Nginx → Gunicorn (Frappe web workers) → MariaDB
↓
Redis (cache, queue, socketio)
↓
Background workers (RQ) + Scheduler
bench manages everything:
bench new-site erp.local
bench --site erp.local install-app erpnext
bench new-app my_app
bench --site erp.local install-app my_app
bench start # dev server with auto-reload
bench --site erp.local migrate # apply schema changes
bench --site erp.local backup
ERPNext modules
Accounts, Stock, Buying, Selling, Manufacturing, CRM, Projects, Assets, Quality, Support, HR & Payroll (a separate app), Website/Portal, and Point of Sale. Plus separate Frappe apps: Helpdesk, Insights (BI), Drive, Books, Learning, HR.
Coverage is genuinely broad for an open-source product. Depth varies — accounting and stock are solid; manufacturing is capable but lighter than SAP PP; there’s no real WMS or APS.
Learning path
1. Install with Docker or bench; use ERPNext as a business user (2 weeks)
2. Understand DocTypes: create one from the UI, inspect its JSON (1 week)
3. Python + Frappe ORM: frappe.get_doc, frappe.db, get_list (2 weeks)
4. Document lifecycle hooks: validate, on_submit, on_cancel (2 weeks)
5. Client scripts, custom buttons, dashboards (1 week)
6. Print formats, reports, workflows (2 weeks)
7. READ THE ERPNEXT SOURCE — accounts, stock, selling (ongoing)
8. Build and publish an app (ongoing)
1–3 months to productive. The lowest barrier of any ERP, and the source is small enough to actually read.
Reality check
Pros: completely free and open, small readable codebase, metadata-driven design you can learn from, excellent for SMB implementations, strong South Asian market, and a genuinely good framework for building any business application — not just ERP.
Cons: a much smaller job market than SAP/Dynamics/NetSuite, less functional depth in manufacturing and warehousing, GPL copyleft (matters if you build a commercial derivative — see Licensing), and localisation depth varies by country.
Best use for a career: as a learning platform and for freelance/SMB implementation work. Reading the ERPNext accounting module is one of the most efficient ways to understand how a general ledger is actually implemented — you can go from “GL entry” in the UI to the exact Python that writes it in about two minutes.