Skip to content
ERPNext · Frappe

ERPNext customization you can still upgrade next year

Most ERPNext sites don't need a new module. They need a dozen custom fields, a stricter Sales Order, an invoice that looks like the old one and a report the finance head will actually open. erpfly makes those changes and packages them in Git, not in someone's memory of what they clicked.

ERPNext v16
  • Code in your Git repo
  • Tested on a sandbox first
  • No core files edited

Deliverables

What you walk away with

Not a demo, not a mockup. Files you can open, change and deploy without us.

Custom fields as fixtures

Every field and Property Setter is created with its Module set, then exported from the app. Staging and production get identical fieldnames after bench migrate, not near-copies typed in twice.

Rules that the API can't skip

Validation runs in Python through doc_events in hooks.py. Client Scripts are kept for display tweaks, because Data Import and REST calls never run them.

Workflows with the right roles

States, transitions, conditions and any new Role records, exported together so the approval chain arrives on the next site in one piece.

Print formats in Jinja

Quotations, delivery notes and tax invoices laid out the way your customers are used to, with letterhead, tax breakdown and the terms your lawyer asked for.

Reports that answer one question each

Script Reports or Query Reports with filters that match what gets asked in the Monday meeting, instead of a saved Report Builder view only one person can find.

A list of what was touched

Every standard DocType the app changes, with the reason. When v17 arrives, that list is your upgrade checklist.

The process

How the work actually goes

  1. 1

    Let erpfly read the site

    A read-only API key on a staging copy is enough. It lists existing Custom Fields, Property Setters and Server Scripts so it doesn't add a second field that does the same job.

  2. 2

    Describe the change like a ticket

    Which document, what should happen, who is allowed to override it. Screenshots of the current form help more than a long spec.

  3. 3

    Check it on a copy of your data

    The app is installed on a disposable clone. Old records that don't fit a new mandatory field show up here, not on Monday morning in production.

  4. 4

    Merge and migrate

    You get a pull request. Deploy through Frappe Cloud or with bench migrate, and move any older UI-made customizations into the same app while you're at it.

credit_control/credit_control/hooks.py
app_name = "credit_control"
app_title = "Credit Control"
app_publisher = "Your Company"
app_description = "Credit holds on Sales Order"
app_license = "mit"
required_apps = ["erpnext"]

# Only records tagged with our module are exported, so we never
# ship custom fields that belong to another app on the same site.
fixtures = [
    {"dt": "Custom Field", "filters": [["module", "=", "Credit Control"]]},
    {"dt": "Property Setter", "filters": [["module", "=", "Credit Control"]]},
    {"dt": "Role", "filters": [["name", "=", "Credit Controller"]]},
    {"dt": "Workflow State", "filters": [["name", "=", "Pending Credit Approval"]]},
    {"dt": "Workflow", "filters": [["name", "=", "Sales Order Credit Check"]]},
    {"dt": "Print Format", "filters": [["name", "=", "Order Confirmation"]]},
]

doc_events = {
    "Sales Order": {
        "validate": "credit_control.sales_order.flag_overdue_customer",
        "before_submit": "credit_control.sales_order.block_unapproved_hold",
    },
}

# Display only. The real check lives in sales_order.py.
doctype_js = {
    "Sales Order": "public/js/sales_order.js",
}
An excerpt from a generated module. Trimmed for the page.

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:

  1. 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.
  2. Client Scripts and Server Scripts. Snippets of JavaScript and sandboxed Python typed into the desk. Quick to add, easy to forget about.
  3. 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.
  4. A custom Frappe app. Python in hooks.py and controllers, with everything above exported as fixtures so it travels with the code.
  5. 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_after next to the payment terms.
  • A validate hook 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_hold so 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.

Modules people build with this

Guides and terms for this work

Questions people ask us

Something missing? Email hello@erpfly.com and a person will answer.

We already made lots of changes through Customize Form. Can you work with those?

Yes. erpfly reads what's on the site before it writes anything. If you want, it also sets the Module on your existing Custom Fields and Property Setters and exports them into the app, so the old changes finally end up in Git alongside the new ones.

Will these customizations survive an upgrade from v15 to v16?

Custom fields, property setters and doc_events hooks usually carry across major versions without edits. What breaks is code that depends on internal function names or on a standard field that moved. The change list in the app tells you exactly what to retest.

Do you ever edit the ERPNext source code?

No. Nothing in the erpnext or frappe apps is touched. If a change can only be made by patching core, we'll say so and suggest a different approach, because a forked ERPNext is the most expensive thing you can own.

Can my team keep using Customize Form after the app is installed?

You can, but set the Module on anything new and re-export fixtures, or the site will slowly drift away from the repository again. The README in the app shows the two commands to run.

Is Server Script ever the right choice?

For small, low-risk things, yes. A scheduled reminder or a field default is fine as a Server Script. We draw the line at anything that blocks a document, changes amounts or touches ledgers.

What does an ERPNext customization cost with erpfly?

It depends on the plan and how many rounds of changes you need, and the pricing page has the current numbers. Compared with a freelancer, the saving is mostly in the first version arriving the same day rather than after a quote.

Other ways we can help

Your next module is one paragraph away

Write it the way you’d explain it to a new hire. We’ll turn it into an app you can read, test and install.

ERPNext v16

Create your account

Free to start. No card needed.

By signing up you agree to our terms and privacy policy.