Table of Contents
- The minimum version of our core dependencies (Python and NodeJS) have been bumped. You will now require:
- Changes to Navigation
- Separating modules from frappe into new apps
- frappe.db.commit() is not supported from document hooks
- Removed view-specific translations
- Permission system breaking changes
- Default sorting changed from modified to creation
- Country code
- frappe.flags.in_test is deprecated, use frappe.in_test
- db.get_value casts single doctype results
- SQL function syntax changes
- Usage of query builder for get_list and get_all
- Transaction Log
- Some whitelisted methods now require POST
- Changes to how JS code for Reports, Dashboard Charts and Pages is loaded
- Map view now behaves like list view
- bench version command default output format changed
- meta.get_valid_columns() now excludes virtual fields
- get_doc(doctype, name, field=value) doesn't update values
- db.value_cache data structure change.
- db.count(..., cache=True) now works like db.get_value(..., cache=True)
- System Console permissions
- GeoIP features are removed
- Site config and common site configs are cached
- utils.caching.site_cache doesn't support unhashable arguments
- override_doctype hook must use an extended class
- frappe.sendmail(..., now=True) does not commit transaction
- Default value removed from the time field
- Handle != None condition correctly in db_query
- List View
- HTML sanitization
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:
- 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.
- Apps page i.e “/apps” is now deprecated
- 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:
- Energy Points (removed in #32040)
- Newsletter (removed in #32971)
- Backup Integrations (removed in #32351)
- Blog (removed in #32737)
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_dictis deprecated and only returns translations of country names. Useget_translated_countriesinstead. - The methods
frappe.get_lang_dict,frappe.translate.get_dict,frappe.translate.get_lang_jsandfrappe.translate.get_dict_from_hookshave been removed. Translations are available via_(). doc.meta.__messagesdoes no longer hold doc-specific translations
https://github.com/frappe/frappe/pull/22962
Permission system breaking changes
has_permissionhooks now need to explicitly return True. Current behaviour allowed returningNoneor non-False value to allow user. #24253frappe.permission.has_permissionfunction no longer accepts misleading "raise_exception" parameter, useprint_logsinstead. #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:
creationis better default for most business documents (currently primary use case for many Frappe apps)creationensures stable list view even when things are rapidly changing.creationis 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
modifiedtocreation - If you don't change it then both
creationandmodifiedwill be indexed. - If you change sorting to
creationthen 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_listfrappe.db.get_value/frappe.db.get_valuesfrappe.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. Here’s 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 won’t 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.