Five places an ERPNext change can live
Every ERPNext customization ends up in one of five places. Picking the wrong one is the usual reason a site gets painful to upgrade. From lightest to heaviest:
- Customize Form. Custom Fields and Property Setters. Add a field, hide one, make the customer PO number mandatory, rename a label. Stored as records in the database.
- Client Scripts and Server Scripts. Snippets of JavaScript and sandboxed Python typed into the desk. Quick to add, easy to forget about.
- Workflows, Print Formats and reports. Still configured in the browser, but now there’s real logic in them: who approves, what the customer sees, which numbers finance trusts.
- A custom Frappe app. Python in
hooks.pyand controllers, with everything above exported as fixtures so it travels with the code. - A new module. New DocTypes for a process ERPNext doesn’t cover at all. That’s a different job, covered on our page about building ERPNext custom modules as Frappe apps.
Most requests we see belong to layers 1 to 3. Our opinion is that they should still be packaged in layer 4. The UI tools are fine. The problem is that a change you can’t diff is a change you can’t review.
Server Scripts are fine until they aren’t
We get asked about this more than anything else, so here’s where we stand. A Client Script that sets a default or collapses a section is harmless. A Server Script that posts a message to a chat channel on submit is fine too.
Trouble starts when business rules move in. Client Scripts only run in the browser, so a rule enforced there is ignored by the REST API, by Data Import and by any integration that creates documents. Server Scripts run in a restricted sandbox where you can’t import libraries or write proper tests, and on self-hosted benches they stay switched off until someone enables server_script_enabled. Ten of them firing on the same Sales Order, written by three people over two years, is the mess we’re most often asked to untangle.
So erpfly puts logic in Python functions wired through doc_events and keeps form scripts for things the user sees. If you want the long version of that argument, read when a Server Script should become a custom app.
Fixtures, or why staging never matches production
This scene repeats on nearly every ERPNext project. Someone adds four custom fields on staging, tests them, then adds them again by hand on production. One ends up with a slightly different fieldname. Six months later a report breaks on one site and not the other, and it takes a day to work out why.
Fixtures fix that. You list the records the app owns in hooks.py, run bench --site yoursite export-fixtures, and each bench migrate imports them. With developer mode on, the Export Customizations button in Customize Form does something similar, one DocType at a time. Either way, the Custom Field becomes a JSON file in Git.
The trick is the filter. Export every Custom Field on the site and you’ll ship fields that belong to other apps, then watch two apps fight over them. We set the Module on every record we create and filter on that. Workflows need their Workflow State records exported too, otherwise the import fails on a clean site.
Example: credit holds for a building-materials distributor
Take a distributor whose sales reps keep confirming orders for customers who haven’t paid in months. ERPNext already has credit limits on the Customer, and they’re switched on. The gap is that a customer can sit comfortably under their limit and still have a 90-day-old invoice that accounts never chased.
The request is the prompt at the top of this page. Here’s what erpfly would produce for it:
- Two Custom Fields on Sales Order (a read-only Credit Hold check and an Overdue Amount currency field), placed with
insert_afternext to the payment terms. - A
validatehook running one query against submitted Sales Invoices with outstanding amounts past the cutoff. - A Workflow with a Pending Credit Approval state, where the transition condition reads
doc.credit_holdso clean orders skip approval entirely. - A new Credit Controller role, and a Jinja Print Format that prints the hold in red so the warehouse doesn’t pick early.
Nothing inside erpnext is edited. When the site moves to a new major version, the retest list is short: does Sales Order still have the field our custom fields sit after, and does the Sales Invoice query still return the same columns.
Customize ERPNext, or build a module?
Our rule of thumb: if the change hangs off a document ERPNext already has, customize it. If you catch yourself describing a new document with its own lifecycle, list view and permissions, it’s a module.
A second approval on Purchase Order is customization. A calibration log for tools with due dates is a module. A new invoice layout is customization, while milestone billing against project stages sits on the line, and we cover that in the invoice management module. If cost is what’s holding you back, we’ve written up what ERPNext customization tends to cost and where the money usually goes.
When we’d tell you not to customize
- The setting already exists. Selling Settings, Stock Settings and Accounts Settings hide a lot of checkboxes. We look there first, and it’s the answer more often than you’d expect.
- You’re patching over a bad setup. If every invoice needs a field to correct tax, the fix is probably your Item Tax Templates, not the form.
- The change alters how GL entries post. It’s possible with
override_doctype_class. It’s also the kind of change that fails quietly until year end. Whoever writes the code, get an accountant and a senior ERPNext consultant to review it. - You want forty new fields on Sales Order. Each one is a box somebody has to fill in, forever. Cut the list in half and see who complains.
Customizations are only worth it if they come through upgrades intact. Our guide to keeping customizations through an ERPNext upgrade covers the checks we run. If you’re still deciding on a platform, the ERPNext vs Odoo comparison explains how differently the two handle this kind of work.
Sources
The official documentation and source code this page was checked against.
- 01 Hooks, Frappe Framework documentation docs.frappe.io
- 02 Server Script, Frappe Framework documentation docs.frappe.io
- 03 Client Script, Frappe Framework documentation docs.frappe.io
- 04 Credit Limit, ERPNext documentation docs.frappe.io
- 05 Workflows, ERPNext documentation docs.frappe.io