ERPNext 7 min read Updated
How to create a custom DocType in ERPNext
Step by step: developer mode, bench new-app, fields, naming, permissions, controller methods and fixtures, plus the DocType mistakes we see most.
Written by the erpfly team, people who build on Frappe and Odoo for a living.
To create a custom DocType in ERPNext properly, turn on developer mode, create a Frappe app with bench new-app, install it on your site, then create the DocType in the desk with its Module set to your app’s module and Custom left unticked. Frappe writes the JSON definition and a Python controller into your app folder, and you commit those files to Git. Skip the app and the DocType lives only in one site’s database, which is where most of the pain starts.
Below is the full sequence as we do it on v15 and v16 benches, with the mistakes that cost teams the most time.
Step 1: Turn on developer mode
Without developer mode, Frappe won’t write DocType files to disk. Everything you build stays in the database, and you can’t move it to another site except by hand.
bench --site dev.localhost set-config developer_mode 1
bench --site dev.localhost clear-cache
If every site on the bench is a development site, set it globally instead with bench set-config -g developer_mode 1. Don’t do this on production. A production site should receive DocTypes through bench migrate, never create them.
Step 2: Create and install an app
A DocType has to belong to a module, and a module has to belong to an app. You could put it in an existing module like Selling, but that writes your files into the erpnext app, and the next update wipes them out. Make your own app.
cd ~/frappe-bench
bench new-app library_management
bench --site dev.localhost install-app library_management
bench new-app asks a few questions (title, publisher, license and so on). It also creates a module named after the app title, listed in modules.txt. That’s the module you’ll pick in the next step. If your app will hold several unrelated areas, add more lines to modules.txt and matching folders, but one module is fine for most apps.
Initialise Git in apps/library_management right away. You want the first DocType commit to be a clean diff.
Step 3: Create the DocType in the desk
Go to /app/doctype/new. Fill in:
- Name: singular and in Title Case, like
Library Member. Frappe turns it into alibrary_memberfolder and atabLibrary Membertable. Renaming later is possible but messy, so take a minute over it. - Module:
Library Management. - Custom?: leave it unticked. Ticked means “store in the database only”, which defeats the point of having an app.
- Is Submittable if the document gets submitted and cancelled like a Sales Invoice. Is Child Table if it only ever lives inside another document. Is Single for settings pages with one record.
Desk or code?
You can write the JSON by hand, but we don’t recommend it for creating a DocType. The desk validates field options, generates a correct JSON structure and creates the controller, JS and test stubs in one go. Where code wins is everything after: controller logic, form scripts, tests and reviewing JSON diffs in pull requests. So create and edit the schema in the desk, write behaviour in your editor.
After saving, look at the app:
apps/library_management/library_management/library_management/doctype/library_member/
├── __init__.py
├── library_member.js
├── library_member.json
├── library_member.py
└── test_library_member.py
If those files aren’t there, developer mode isn’t on for that site, or Custom was ticked.
Step 4: Add fields that will survive real data
Fields go in the Fields table. A few habits save a lot of trouble later:
- Use Link fields for anything that points to another record (Customer, Item, Employee). Don’t store a customer name in a Data field and hope it stays in sync.
- Tick Mandatory only for things that truly must exist on day one. Adding a mandatory field to a DocType that already has records is fine for new saves, but every old record will fail validation the next time someone edits it.
- Pick Fieldnames carefully. The label can change any time; the fieldname is a database column and gets referenced in code, reports and print formats.
- Use Section Break, Column Break and Tab Break for layout. They don’t create columns in the table.
- Set In List View and In Standard Filter on the three or four fields people actually search by.
For line items, create a second DocType with Is Child Table ticked, then add a Table field on the parent pointing to it.
Step 5: Choose a naming rule
The Naming Rule field decides the document name, which is its primary key. The common choices:
- By “Naming Series” field: add a Select field called
naming_serieswith options likeLIB-MEM-.YYYY.-. Users can pick a series, and it matches how ERPNext names its own documents. - Expression: set Auto Name to something like
format:LIB-{YYYY}-{#####}. No extra field needed. - By fieldname:
field:member_email. Only use this when the value is truly unique and never changes. If it can change, you’ll be renaming documents. - Autoincrement: integer IDs. Good for high-volume log-style records people rarely refer to by name.
- By script: define
autoname(self)in the controller when the name depends on other fields.
We default to a naming series for anything a human will quote on the phone, and autoincrement for everything else.
Step 6: Set permissions
The Permissions table holds one row per role and permission level. Each row ticks Read, Write, Create, Delete, and for submittable DocTypes, Submit, Cancel and Amend. If Owner limits the row to documents the user created.
A few rules we follow:
- Give System Manager full access so admins aren’t locked out.
- Create specific roles (
Librarian,Library Member) rather than reusing broad ones likeStock User. If you add custom roles, ship them as fixtures (see step 8). - Use permission levels to hide sensitive fields. Put fields at level 1 and only give some roles a level 1 row.
- For “users only see records for their own branch”, use User Permissions on the Link field first. Reach for
permission_query_conditionsinhooks.pyonly when User Permissions can’t express the rule.
Step 7: Write the controller
The Python file next to the JSON is the controller. Frappe calls methods on it at points in the document lifecycle. The ones you’ll use most:
validate: runs before every insert and save. Put business rules here.before_save/before_insert: set computed values.after_insertandon_update: side effects after the write, like creating a linked record.on_submit/on_cancel: for submittable DocTypes, where ledger-style postings belong.on_trash: block or clean up on delete.
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_days, date_diff, getdate
class LibraryMembership(Document):
def validate(self):
if getdate(self.to_date) < getdate(self.from_date):
frappe.throw(_("To Date can't be before From Date"))
self.check_overlap()
self.total_days = date_diff(self.to_date, self.from_date) + 1
def check_overlap(self):
clash = frappe.db.exists("Library Membership", {
"library_member": self.library_member,
"docstatus": 1,
"name": ("!=", self.name),
"from_date": ("<=", self.to_date),
"to_date": (">=", self.from_date),
})
if clash:
frappe.throw(_("Member already has an active membership: {0}").format(clash))
@frappe.whitelist()
def extend_by_days(self, days):
# callable from the form with frm.call("extend_by_days", {days: 30})
self.to_date = add_days(self.to_date, int(days))
self.save()
Newer app templates export type annotations, so you may see an auto-generated block of field types at the top of the class. Leave it alone; Frappe rewrites it when the DocType changes.
Keep validation in Python. A rule enforced only in the form’s JavaScript is skipped by anyone who uses the REST API or Data Import.
Step 8: Export customisations as fixtures
The DocType itself is already in your app as JSON. What isn’t are things you did to standard DocTypes: custom fields on Sales Invoice, property setters from Customize Form, custom roles. Those live in the database until you export them.
Set the Module on each Custom Field and Property Setter to your app’s module, then declare fixtures in hooks.py:
fixtures = [
{"dt": "Custom Field", "filters": [["module", "=", "Library Management"]]},
{"dt": "Property Setter", "filters": [["module", "=", "Library Management"]]},
{"dt": "Role", "filters": [["name", "in", ["Librarian", "Library Member"]]]},
]
bench --site dev.localhost export-fixtures --app library_management
This writes JSON files into library_management/fixtures/. They get imported when the app is installed and on every bench migrate. Always filter. An unfiltered Custom Field fixture drags in every custom field on the site, including ones from other apps, and you’ll fight over them forever.
Step 9: Deploy to another site
Push the app, then on the target bench:
bench get-app https://github.com/your-org/library_management
bench --site erp.example.com install-app library_management
# later, after pulling new commits:
bench --site erp.example.com migrate
migrate syncs the DocType JSON into the database, runs patches and imports fixtures. If you changed a field type or renamed something, test the migration on a copy of production first.
Mistakes we keep seeing
Building DocTypes directly on production. Developer mode is off, so they’re created as custom DocTypes in the database. Six months later there are forty of them, no Git history and no way to reproduce the site. Moving them into an app after the fact is doable, but it’s a project on its own.
Picking a standard module. Setting the Module to Stock or HR writes files into the ERPNext or HRMS app folder. They work until the next bench update, then vanish. We covered how to recover from this in our post on keeping customisations through an ERPNext upgrade.
Renaming fieldnames after go-live. The column changes, but reports, print formats, Server Scripts and saved filters still reference the old name. Change the label, keep the fieldname.
Editing JSON while the desk is open. Save the DocType in the browser and your hand edits are overwritten. Pick one place per change.
Putting logic in Server Scripts instead of the controller. Fine for a quick patch, bad for anything you need to test. Our Server Scripts vs custom app comparison goes into where that line sits.
Forgetting that a child table has no permissions of its own. Access follows the parent. If you want to restrict who sees line items, restrict the parent or use permission levels.
If you’d rather skip the boilerplate
Once you’ve done this a few times, the DocType JSON, permissions and fixtures feel like typing, not thinking. That’s the part erpfly generates. You describe the module in plain English and get a Frappe app with DocTypes, controllers and tests as a pull request you can review. See how ERPNext custom module development works with erpfly, or if you only need fields and forms tweaked, our ERPNext customization service. For examples of what a finished module looks like, the CRM module is a good place to start.
Sources
The official documentation and source code this page was checked against.
- 01 Create a DocType, Frappe Framework tutorial docs.frappe.io
- 02 Naming, Frappe Framework documentation docs.frappe.io
- 03 Controllers, Frappe Framework documentation docs.frappe.io
- 04 Users and Permissions, Frappe Framework documentation docs.frappe.io
- 05 Hooks, Frappe Framework documentation docs.frappe.io