29 Migrating to version 16
Raffael Meyer edited this page 2026-04-07 23:52:14 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This page is intended to make it easier for users who maintain custom apps/forks to migrate their installations to Version 16.


The minimum version of our core dependencies (Python and NodeJS) have been bumped. You will now require:

  • Python 3.14+
  • NodeJS 24+

You can find more information about the installation process here.

Changes to Navigation

In version 13 we introduced the idea of workspaces, and ever since then that been the screen which helps you navigate around. Workspaces have 2 types public and private

In version 16, we have introduced a persistent sidebar like all of our new apps. This is powered by Workspace Sidebar doctype. It is autogenerated for the most part. But you should try to make a standard and curated version of it for an better UX.

In version 16, we also introduce the desktop screen. This shows icons for the all the public workspaces. This is also autogenerated for the most part. For autogeneration to work correct or your custom app you need to have the add to apps screen hook mentioned correctly in your hooks.py

Ref: https://docs.frappe.io/framework/user/en/apps-page#how-can-we-show-our-custom-apps-on-apps-page

Private Workspaces are now accessible inside of the My Workspaces icon on the desktop screen.

Breaking Changes:

  1. If you have modified the standard workspace of the app (like “Buying”, “Selling") those changes would go away. You can take a backup (Copy to Clipboard) of them and restore it again after migration.
  2. Apps page i.e “/apps” is now deprecated
  3. Desk frontend is now rerouted from "/app" to "/desk".

Separating modules from frappe into new apps

Continuing last year's trend. some more modules have been moved out as separate apps:

frappe.db.commit() is not supported from document hooks

Document hooks setup via hooks.py can not commit a database transaction anymore. This change was done to avoid data integrity issues, and it's recommended to not bypass this.

Removed view-specific translations

In the previous versions it was possible to add translations for a specific view only. For this, the get_translated_dict hook was used. In the previous versions we started sending all translations regardless of the view. Now we also removed the get_translated_dict hook. If you included view-specific translations in this way, please move them to your app's translation files.

Web forms are the only exception. They don't receive all translations. Instead, they get a small dictionary with translations for the labels used in the web form.

Along with this change we also included the following changes:

  • Currency codes and timezone names no longer get translated. (This used to be the case in Setup Wizard and System Settings only.)
  • Translations of country names are be available everywhere by default.
  • The method frappe.geo.country_info.get_translated_dict is deprecated and only returns translations of country names. Use get_translated_countries instead.
  • The methods frappe.get_lang_dict, frappe.translate.get_dict, frappe.translate.get_lang_js and frappe.translate.get_dict_from_hooks have been removed. Translations are available via _().
  • doc.meta.__messages does no longer hold doc-specific translations

https://github.com/frappe/frappe/pull/22962

Permission system breaking changes

  1. has_permission hooks now need to explicitly return True. Current behaviour allowed returning None or non-False value to allow user. #24253
  2. frappe.permission.has_permission function no longer accepts misleading "raise_exception" parameter, use print_logs instead. #24266

Default sorting changed from modified to creation

  • Starting with v16 all list views by default will be sorted by creation.
  • This decision was taken considering multiple things:
    • creation is better default for most business documents (currently primary use case for many Frappe apps)
    • creation ensures stable list view even when things are rapidly changing.
    • creation is better from performance point of view as the field doesn't change and hence index updates are not required. This reduces contention on index updates.

What do you as app developer need to do?

  • Change default sort ordering in your all of your DocType from modified to creation
  • If you don't change it then both creation and modified will be indexed.
  • If you change sorting to creation then modified index will be dropped while migrating.

WARNING: This change also affects how database APIs work. Most Frappe database APIs implicitly sorted by modified, they'll now implicitly sort by creation.

Example: frappe.get_all("DocType") was actually translated to frappe.get_all("DocType", order_by="modified desc"). This is now changed to frappe.get_all("DocType", order_by="creation desc")

It's possible that some of your business logic dependent on this, so it's advisable to audit your code to find out if modified ordering affects any of your business logic and explicitly add order_by='modified desc'.

Following APIs have changed the implicit sort order:

  • frappe.get_all / frappe.get_list
  • frappe.db.get_value / frappe.db.get_values
  • frappe.qb.get_query

Note: You can re-add index by adding following code in your doctype controller.

def on_doctype_update():
    frappe.db.add_index("Doctype Name", ["modified"])

Country code

Until now you could add almost any data as Country. Now it's necessary to set the Code field and we'll check that it's valid according to ISO 3166 ALPHA-2.

frappe.flags.in_test is deprecated, use frappe.in_test

If you've used frappe.flags.in_test in your code for evaluating whether it is being run as a part of a test, replace it with frappe.in_test instead. frappe.flags.in_test will be removed in a future release. (PR)

If you're changing the value of frappe.flags.in_test in your code, use frappe.tests.utils.toggle_test_mode to do so. This will set both frappe.in_test and frappe.flags.in_test.

db.get_value casts single doctype results

So far db.get_value always returned strings because that's how single doctypes are stored in database. This has been changed to avoid unexpected surprises when using db.get_value on single doctypes. If your code assumed previous behaviour, then you'll likely have to change it.

- if frappe.db.get_value("Settings", "Settings", "enabled") == "1":
+ if frappe.db.get_value("Settings", "Settings", "enabled") == 1:

SQL function syntax changes

After https://github.com/frappe/frappe/pull/32381, you may require some changes based on how you called functions, like sum, like:

Usage of query builder for get_list and get_all

After the query builder refactor, we've switched frappe.get_all() and frappe.get_list() to using this backend for version 16. This may result in some effort on developers to get your apps compatible with version 16.

Some references:

Please refer to query builder migration to know more about the changes required.

Transaction Log

The DocType Transaction Log has been removed, along with the related "Transaction Log Report". This was used as a regional compliance feature for France and Germany, but it turned out to be insufficient/redundant. (Removal PR)

Some whitelisted methods now require POST

State-changing methods now only accept POST requests. If your client code calls these using GET, update to POST:

  • /api/method/logout
  • /api/method/web_logout
  • /api/method/upload_file
  • /api/method/frappe.www.login.send_login_link

Changes to how JS code for Reports, Dashboard Charts and Pages is loaded

The JS code of Reports / Dashboard Charts / Pages was earlier embedded inline in a script tag. This led to variables created in these files polluting the global scope. That led to conflicts when variables of the same name were used across different reports. Starting v16, we are changing how these JS files are loaded - now they will be evaluated as IIFEs. If your code relied on this behaviour to intentionally change global scope, you will need to update it. Albeit not recommended, you can still change global scope from these files by explicitly mutating the window object.

Map view now behaves like list view

The map view (the one reachable via view switcher, not be be confused with a "Geolocation" field) now behaves like the list view, only that it renders the rows on a map.

Previously, we'd try to render all entries at once, which degraded performance when there were many. Now we just render the usual 20 list items, with the option to load more, or set filters.

Details: #32207

bench version command default output format changed

The default output format for the bench version command has been changed from legacy to plain.

  • legacy: Shows only app name and version (e.g., frappe 16.0.0-dev)
  • plain: Shows app name, version, branch, and commit (e.g., frappe 16.0.0-dev version-16-beta (4e51106))

If your scripts or tools parse the output of bench version, you may need to update them to handle the new format or explicitly specify the format using -f legacy.

Reference: #33621

meta.get_valid_columns() now excludes virtual fields

The get_valid_columns() method on DocType meta now skips virtual fields. Previously, virtual fields were included in the list of valid columns returned by this method.

If your code relied on virtual fields being present in meta.get_valid_columns(), you will need to update it to handle this change.

Reference: #27553

get_doc(doctype, name, field=value) doesn't update values

get_doc implicitly used to update any key-value provided as an argument to the fetched document from the database. This behaviour was confusing and never intentionally used. It's now removed.

db.value_cache data structure change.

This is an internal data structure used to cache values fetched from the database. The structure has been changed from a flat dictionary to a nested defaultdict.

- frappe.db.value_cache.pop(("User", "Guest", "full_name"), None)
+ frappe.db.value_cache["User"]["Guest"].pop("full_name", None)

Reference: github.com/frappe/frappe@27e8561197

db.count(..., cache=True) now works like db.get_value(..., cache=True)

Previously, db.count cache was stored in Redis, which was inconsistent with other db APIs. Now the cache is stored in value_cache and gets evicted after the current transaction is complete.

Reference: github.com/frappe/frappe@2d14918814

System Console permissions

System console is now only available to Administrator user by default. You can grant access to other roles by updating permissions using Role Permission Manager.

GeoIP features are removed

GeoIP guessing using the MaxMind database was built into Framework. This was outdated and unreliable; it's now removed from Framework.

Reference: github.com/frappe/frappe@daa52b8802

Site config and common site configs are cached

Both configs are cached for up to one minute, so changing them won't immediately have an effect on running workers. This change was done to improve performance while serving requests.

utils.caching.site_cache doesn't support unhashable arguments

Supporting hashable arguments like dictionaries required serializing arguments to a format like JSON. This rarely used use-case adds overhead and makes caching slower.

e.g. Following function won't support caching.

@site_cache
def cacheable_function(argument: dict)

override_doctype hook must use an extended class

If you override a class using this hook, then your class must inherit from the overridden class. This restriction was added to prevent customisations from severely breaking the original code.

Reference: https://github.com/frappe/frappe/pull/26152

frappe.sendmail(..., now=True) does not commit transaction

Database transaction was implicitly committed by frappe.sendmail which resulted in unexpected bugs. This behaviour is now rectified and as a result you might notice some changes not getting committed. If you desired those changes, then you'll have to manually commit transaction.

Default value removed from the time field

Previously, the time field would take the current time even if no value was set. Now, it will only take the current time if "Now" is specified in the options.

Reference: https://github.com/frappe/frappe/pull/33515

Handle != None condition correctly in db_query

There was an issue in the query builder where the "not equal" (!=) condition was not handled correctly when the value is None. Previously, filtering with != None would not generate the expected SQL IS NOT NULL clause, which could lead to incorrect query results.

Reference: https://github.com/frappe/frappe/pull/34207

List View

Awesome Bar

The design of Awesome Bar has been updated. It now resides in the sidebar. You can open it from there or use the shortcut cmd + k. It opens in a popup like a command palette and includes icons to improve the user experience. Heres a quick demo.

https://github.com/user-attachments/assets/02b68696-756c-437e-ace7-3b0d06d93478

Platform and User Settings

The user profile and settings have moved to the bottom of the sidebar. Notifications now appear in the sidebar with a cleaner list view replacing the old popup. Help and other system related settings can now be accessed at the top of the sidebar.

https://github.com/user-attachments/assets/19783bdd-11f1-4228-ac08-3b68f70f393a

List Sidebar

Another major update in the List View is the removal of the right sidebar. Again this was done to make better use of screen real estate.

Saved Filters

Saved filters are now on the top of the list view, making it easier to access and allowing users to create new filters quickly.

https://github.com/user-attachments/assets/b2e348ca-2e80-495a-b44c-a15932cf2f1e

Customize Filters

Previously, adding a field to quick filters required going through Customize Form or you had to edit the DocType manually.

Now, Users can select quick filters directly from the List View itself, and these settings are user-specific, so other users wont be affected.

https://github.com/user-attachments/assets/011bc29e-2268-4e94-a803-027592a01fd0

HTML sanitization

In v15 frappe.utils.sanitize_html used bleach, which replaced forbidden tags with character codes so that <iframe href="example.com"> would render this literal text instead of showing the iframe. In v16, we use nh3.clean, which completely strips forbidden tags.