Open anything in the ERPNext desk and you’re looking at a document of some DocType. Sales Invoice, Customer, Item, Employee: each one is a DocType, and so is the form you use to define new ones. Once you see ERPNext this way, most of the framework starts to make sense.
What a DocType decides
When you save a DocType, Frappe creates or alters a table in the site database. The table name is the DocType name with a tab prefix, so Library Member becomes tabLibrary Member. By convention the name is singular: Article, not Articles.
The definition covers:
- Fields. Each row is a DocField with a type such as Data, Link, Currency, Date, Select or Table. Layout types like Section Break and Tab Break shape the form but don’t add columns.
- Standard fields you never add yourself:
name,owner,creation,modified,modified_by,docstatusandidx. - Naming, from a naming series to a field value or a hash.
- Permissions per role: read, write, create, submit, cancel and so on.
- Behaviour flags. Is Submittable gives documents the draft, submitted and cancelled states (
docstatus0, 1 and 2). Is Child Table makes it a child table that only lives inside a parent. Is Single keeps one record, which suits settings pages. Is Tree and Is Virtual cover hierarchies and data stored outside the database.
Standard and custom DocTypes
This distinction causes more confusion than any other. A standard DocType belongs to a module inside a Frappe app. With developer mode on, Frappe writes its definition to a JSON file in that app, alongside a Python controller, and bench migrate applies it to any other site that installs the app.
A DocType with Custom ticked is stored only in the database. Frappe won’t let you create a standard one without developer mode; it tells you to switch developer mode on or make the DocType custom. Custom DocTypes are fine for experiments, but they have no controller file and no Git history.
Changing a DocType that ships with ERPNext is a different job again. You don’t edit its JSON in the erpnext folder. You add a custom field or a property setter and keep those in your own app. That’s the rule erpfly follows too: new DocTypes go into your app as standard ones, and changes to ERPNext’s own forms are exported as fixtures.
What it looks like on disk
A standard DocType named Library Member in a library_management app produces a folder at apps/library_management/library_management/library_management/doctype/library_member/ containing library_member.json (the definition), library_member.py (the controller), library_member.js (the form script) and a test file. Logic goes in the controller:
import frappe
from frappe.model.document import Document
from frappe.utils import getdate
class LibraryMember(Document):
def validate(self):
if self.membership_end and getdate(self.membership_end) < getdate(self.membership_start):
frappe.throw("Membership can't end before it starts")
validate runs on every insert and save. Other controller methods, such as on_update, on_submit and on_cancel, hook into later points of the document lifecycle.
Mistakes we keep seeing
Careless fieldnames come first. A label can change any time, but the fieldname is a database column that reports, print formats and code refer to. Next is building DocTypes directly on production, where they end up as database-only records that no other site has. Then there’s adding a whole new DocType when two custom fields on Customer would have done the job.
For the full sequence, from developer mode to naming rules and permissions, follow our step-by-step guide to creating a custom DocType.