# Upgrading ERPNext without breaking your customizations

Canonical: https://erpfly.com/blog/erpnext-upgrade-keep-customizations/
Last updated: September 10, 2026

Published March 24, 2026 in ERPNext. What breaks when you upgrade ERPNext a major version: core edits, Server Scripts, custom fields and patches. Plus a staging routine and testing checklist.

Upgrading ERPNext keeps your customizations intact as long as they live in the right places: custom fields and property setters in the database or in an app's fixtures, logic in your own Frappe app, and nothing edited inside the `frappe` or `erpnext` folders. What breaks is everything else. Edits to core files get overwritten or block the update, Server Scripts fail at runtime when they call functions that changed, and custom app patches run in the wrong order. The safe routine is always the same: back up with files, restore onto a staging copy, upgrade there, test the processes people actually use, and only then touch production.

Below is how we run a major version upgrade like v15 to v16, and what we check before we're willing to call it done.

### What actually breaks during an upgrade

#### Edits to core files

This is the big one. Someone needed a field hidden on the Sales Invoice print, or a validation relaxed, and changed a `.py` or `.js` file inside `apps/erpnext` directly. It worked, and it was never written down.

When you move to a new major branch, those changes either conflict and stop the update or get thrown away with `--reset`. Either way the behaviour your team relies on disappears. Find them before you start:

```bash
cd ~/frappe-bench/apps/erpnext
git status --short
git diff --stat

cd ~/frappe-bench/apps/frappe
git status --short
```

Any output from `git status` means someone changed core. Each change needs a new home, usually a custom field, a property setter, a `doc_events` hook or an `override_doctype_class` entry in your own app's `hooks.py`.

#### Server Scripts and Client Scripts calling things that moved

Server Scripts are stored in the database, so they survive the upgrade itself. That's the trap. They don't fail during `bench migrate`. They fail on Tuesday morning when someone submits a Delivery Note and the script calls a method whose signature changed, or reads a field that was renamed.

Client Scripts have the same problem with a different cause. Scripts that reach into the DOM, use undocumented form internals or depend on a particular layout tend to break when the desk UI changes between majors.

Pull every script out of the database before you upgrade so you can read them in one place:

```python
# bench --site staging.example.com console
for s in frappe.get_all(
    "Server Script",
    fields=["name", "script_type", "reference_doctype", "disabled", "script"],
):
    print(f"--- {s.name} ({s.script_type}, {s.reference_doctype}, disabled={s.disabled})")
    print(s.script)
```

Do the same for `Client Script`. If you have more than a handful of long scripts, this is a good moment to read our take on [Server Scripts vs a custom app](https://erpfly.com/blog/server-scripts-vs-custom-app-erpnext/). Code in an app can be tested with `bench run-tests` before the upgrade. Code in a Server Script can only be tested by clicking.

#### Custom fields created in the UI vs fixtures

Custom fields added through **Customize Form** are rows in the `Custom Field` table. They survive upgrades fine. The problem is that they exist only on that one site, with no history, so you can't easily tell which fields are still used or who added them.

Custom fields exported as fixtures from your own app are better. They're in Git, they get recreated on every `bench migrate`, and you can install the same app on staging and get an identical schema. Set a **Module** on the fields and property setters that belong to your app, then declare them in `hooks.py`:

```python
fixtures = [
    {"dt": "Custom Field", "filters": [["module", "=", "Acme Customizations"]]},
    {"dt": "Property Setter", "filters": [["module", "=", "Acme Customizations"]]},
]
```

Then export them into the app:

```bash
bench --site erp.example.com export-fixtures --app acme_customizations
```

One thing to watch: a standard field added by the new version can collide with a custom field of the same fieldname. Check your custom fieldnames against the new version's DocType JSON for the documents you've customised most.

#### Patches in your own apps

Your custom app's `patches.txt` runs during `bench migrate`, alongside the core patches. Since v14 the file has `[pre_model_sync]` and `[post_model_sync]` sections. Pre-sync patches run before DocTypes are reloaded, so they're where you copy data out of a field that's about to change type or be renamed. Post-sync patches run after, so anything that writes to a newly added field belongs there. Put a patch in the wrong section and migrate either fails halfway or quietly does nothing.

Old patches that already ran won't run again, because Frappe records them in `Patch Log`. New patches you write for the upgrade should be safe to run twice anyway, in case you have to restore and retry.

#### Custom apps without a matching branch

Every app on the bench has to support the new Frappe major. That includes marketplace apps and your own. If a third-party app you depend on has no branch for the new version yet, you aren't upgrading yet, no matter what else is ready.

For your own apps, update the Frappe version range in `pyproject.toml` and create a branch for the new major so you can still patch the old one.

### The routine we follow

#### 1. Take a full backup of production

```bash
bench --site erp.example.com backup --with-files
ls -lh ~/frappe-bench/sites/erp.example.com/private/backups/
```

That gives you the database dump plus public and private file archives. Copy them off the server. A backup that only exists on the machine you're about to upgrade doesn't count.

#### 2. Restore onto a staging bench that matches production

Same app versions as production, on a separate server or at least a separate bench. Then restore, and stop the site from emailing customers or running scheduled jobs:

```bash
bench --site staging.example.com restore \
  ~/backups/20260320_020000-erp_example_com-database.sql.gz \
  --with-public-files ~/backups/20260320_020000-erp_example_com-files.tar \
  --with-private-files ~/backups/20260320_020000-erp_example_com-private-files.tar

bench --site staging.example.com set-config mute_emails 1
bench --site staging.example.com disable-scheduler
```

Skip muting emails once and you'll send a batch of payment reminders from a test server. People only make that mistake once.

#### 3. Switch branches and migrate on staging

```bash
cd ~/frappe-bench
bench switch-to-branch version-16 frappe erpnext hrms --upgrade
# switch your custom apps to their matching branch too
bench setup requirements
bench build
bench --site staging.example.com migrate
```

Check the release notes for the new major's required Python and Node versions before this step. Major releases have raised both in the past, and a virtualenv on the wrong Python gives confusing errors.

A few `bench update` flags matter for minor updates within a version. `--reset` discards local changes to apps, which is exactly what you want only after you've rescued any core edits. `--no-backup` skips the automatic backup, which we never use on production. `--patch`, `--build` and `--requirements` run just that one step.

#### 4. Read the migrate output, all of it

A migrate that finishes isn't the same as a migrate that worked. Look for patches that were skipped, fixtures that failed to sync and deprecation warnings mentioning your apps. Then check the error log in the desk for anything new.

### Testing checklist before production

Test with real users on the staging copy, working through their actual week. Our minimum list:

- Create, submit and cancel one of each document your team uses daily: Quotation, Sales Order, Delivery Note, Sales Invoice, Purchase Receipt, Payment Entry, Journal Entry.
- Trigger every enabled Server Script at least once, on the event it's attached to.
- Open the forms with Client Scripts and check buttons and field behaviour, not just that the page loads.
- Print every custom Print Format you send to customers. Jinja templates that access fields directly can break without raising an error.
- Run your Script Reports and Query Reports for last month and compare totals to production.
- Walk through each Workflow, including rejection paths, as a user with the right role (not Administrator).
- Run `bench --site staging.example.com run-tests --app acme_customizations` if your app has tests.
- Check integrations: payment gateways, e-invoicing, bank feeds, anything calling your API with a key.
- Re-enable the scheduler briefly and confirm scheduled jobs complete.

### Our opinion: fix the customizations, not the upgrade

When an upgrade goes badly, teams often decide to stay on the old version. That's the worst outcome. You're now on a release that will stop getting fixes, and the next jump is bigger.

The upgrade didn't cause the breakage. The customizations were fragile and the upgrade revealed it. Treat the staging run as an audit. Every core edit becomes a hook. Every long Server Script becomes app code with a test. Every UI-created custom field that matters gets a module and becomes a fixture. It's more work than patching things until they run, and it's the only way the next upgrade is boring.

We'd also avoid upgrading production in the first weeks after a new major is released, unless you need a specific feature. Let the early point releases land and give your third-party apps time to catch up.

### Getting help with the hard parts

If your staging run turns up a pile of core edits and Server Scripts, erpfly can turn them into a proper Frappe app with fixtures, hooks and tests, so they upgrade cleanly next time. See [ERPNext custom module development](https://erpfly.com/erpnext-custom-module-development/) or our broader [ERPNext customization](https://erpfly.com/erpnext-customization/) service. And if you're wondering whether it's worth it, our guide to [ERPNext customization costs](https://erpfly.com/blog/erpnext-customization-cost/) covers how rework like this is usually priced.

### Sources

- [Database Migrations, Frappe Framework documentation](https://docs.frappe.io/framework/user/en/database-migrations)
- [Hooks, Frappe Framework documentation](https://docs.frappe.io/framework/user/en/python-api/hooks)
- [Supported Versions, ERPNext wiki](https://github.com/frappe/erpnext/wiki/Supported-Versions)