Skip to content

Migrating to Version 14

Shariq Ansari edited this page Jan 24, 2023 · 33 revisions

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


Replace jenv hook with jinja

  1. Open hooks.py
  2. Rename the jenv hook to jinja
  3. For each string in the methods list remove the part before : (colon) including the colon. It should only be a list of method paths.
  4. Repeat the same for strings in filters.

For e.g.,

- jenv = {
+ jinja = {
    "methods": [
-        "get_fullname:custom_app.jinja.get_fullname"
+        "custom_app.jinja.get_fullname"
    ],
    "filters": [
-        "format_currency:custom_app.jinja.currency_filter"
+        "custom_app.jinja.format_currency"
    ]
}

custom_app/jinja.py

- def currency_filter():
+ def format_currency():
	...

Docs: https://frappeframework.com/docs/user/en/python-api/hooks#jinja-customization

New Build System based on esbuild

The new build system does not support build.json. To make sure bundles are built correctly, you need to create a bundle file for each key in build.json.

Let's say your build.json looks like this:

{
    "js/my_app.js": [
        "public/js/utils.js",
        "public/js/main.js",
        "public/js/support.js"
    ],
    "js/another_file.js": [
        "public/js/another_file.js"
    ],
    "css/my_app.css": [
        "public/less/components.less",
        "public/less/style.less"
    ],
}

Create a file named my_app.bundle.js in the public/js directory and import the 3 files.

// public/js/my_app.bundle.js

import './utils';
import './main';
import './support';

Now, since another_file.js is built with a single file, we can just rename that file from another_file.js to another_file.bundle.js

Now, create a file named my_app.bundle.less in the public/less directory and import the 2 less files.

@import './components.less';
@import './style.less';

Now, assuming you included some of these files as part of app bundle or website bundle in hooks.py, you may need to do the following changes:

- app_include_js = ['/assets/js/my_app.js']
+ app_include_js = ['my_app.bundle.js']
- app_include_css = ['/assets/css/my_app.css']
+ app_include_css = ['my_app.bundle.css']

If you included the assets manually by explicitly writing the script tag in HTML files then you need to do the following changes:

// for js files
- <script src="/assets/js/my_app.js" type="text/javascript">
+ {{ include_script('my_app.bundle.js') }}

// for css files
- <link href="/assets/css/my_app.css" rel="stylesheet">
+ {{ include_style('my_app.bundle.css') }}

If you were lazy loading assets using frappe.require you need to do the following changes:

- frappe.require('/assets/js/another_file.js', ...)
+ frappe.require('another_file.bundle.js', ...)

You can test if your bundles are being compiled by running the bench build command for your app:

bench build --app my_app

Finally, you can delete the build.json file, you no longer need it.

Bundling Target

Version 14 now targets ES2017 aka ES8 by default. So if you use any modern JS syntax like optional chaining in your bundled javascript then it will be automatically transpiled to support previous versions. This lets developers use modern syntax without having to worry about browser compatibility.

Read more here: https://github.com/frappe/frappe/pull/16491

Website routing and rendering refactor

There was a major refractor done for website routing and rendering. During this refactor, few methods were moved to different files. You might have to change the following code in your custom app.

- from frappe.website.render import render
+ from frappe.website.serve import get_response
...
- response = render()
+ response = get_response()
- from frappe.website.render import clear_cache
+ from frappe.website.utils import clear_cache

Workspace 2.0

The Workspace is now upgraded. There were standard workspaces which get generated by JSON files stored in a particular module folder inside the workspace folder. (I will suggest creating a new workspace (copy of your existing workspace) using the awesome Workspace 2.0 feature. It is a much faster approach)

Let's take the example of the build.json file in Frappe App:

// frappe/core/workspace/build/build.json

frappe   
│
└───core
│   │
│   └───workspace
|       └───build
|           build.json

If you have any such JSON files in your Custom App you might have to do the following changes in that JSON file.

- "category": "Modules",
- "is_standard": 1,
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "extends_another_page": 0,
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "extends": "",

+ "for_user": "",
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 31,
+ "title": "Build",

if you have links update each links in following way:

"links": [
 {
  "hidden": 0,
  "is_query_report": 0,
  "label": "Data",
+ "link_count": 0,
  "onboard": 0,
  "type": "Card Break"
 },
 {
  "dependencies": "",
  "hidden": 0,
  "is_query_report": 0,
  "label": "Import Data",
+ "link_count": 0,
  "link_to": "Data Import",
  "link_type": "DocType",
  "onboard": 0,
  "type": "Link"
 },
 ...
]

Also need to add the content on the page which is rendered based on json array which we can create using below content.

Header:

{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}

Chart:

{\"type\": \"chart\", \"data\": {\"chart_name\": \"Profit and Loss\", \"col\": 12}}

Shortcut: shortcut_name is the label of shortcuts.

{\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"System Settings\", \"col\": 4}}

Card: card_name is the label of links of type Card Break.

{\"type\": \"card\", \"data\": {\"card_name\": \"Data\", \"col\": 4}}

On-Boarding:

{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Accounts\", \"col\": 12}}

Spacer: It is used to add gap between Shortcuts and Cards.

{\"type\": \"spacer\", \"data\": {\"col\": 12}}

Combine this based on onboarding, charts, shortcuts, or links in the page to get the content as shown below.

+  "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"DocType\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Workspace\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Report\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Elements\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Modules\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Models\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Views\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Scripting\", \"col\": 4}}]",

return value of frappe.db.exists

frappe.db.exists now only returns value of name if any were found, else None. Previously when passing document dictionary a list of documents were retrieved. If you relied on this behaviour, consider replacing exists with get_all.

New signature of db.exists:

def exists(dt: str | dict, dn: str | dict | list = None, cache: bool = False) -> str | None:
    ...
  • dt can be of type
    • str: the name of a doctype
    • dict: filters in the standard frappe syntax. Filters have to include the "doctype" key. When passing filters, all other parameters will have no effect and can be left empty.
  • dn is needed only if dt was passed as a string. It can be of type:
    • str: name of one specific document. Use this if you want to check if a document with this name exists.
    • dict or list: filters in the standard frappe syntax. Use this to check if a document matching the filter values exists.
  • cache only works if dt and dn are both strings. In this case we cache the result, if cache is set to True.

Using video player

Frappe's video player is now asynchronously loaded on demand. So if you're using frappe.Plyr you need to first load the required libraries using utility function like this.

frappe.utils.load_video_player().then(() => {
    plyr = new frappe.Plyr(...);
})

Defining developer dependencies

In v14 and Frappe bench 5.12, dependencies that are only used during development are specificied in separate section in pyproject.toml section. Example of Frappe Framework's own developer dependencies.

# pyproject.toml
[tool.bench.dev-dependencies]
coverage = "~=6.4.1"
Faker = "~=13.12.1"
pyngrok = "~=5.0.5"
unittest-xml-reporting = "~=3.0.4"

These development dependencies are installed by default in developer mode. They can also be manually installed by using bench setup requirements --dev

Change in frappe.get_cached_value behaviour

get_cached_value uses get_cached_doc internally which used to raise DoesNotExistError if document wasn't found. Since this behavior was inconsistent with other database APIs like frappe.db.get_value the behavior was changed to return None instead of raising an exception.

Changes to Document.get

key is now a mandatory parameter when using doc.get, it no longer returns the the internal __dict__ if key is not passed. This is to avoid hard to trace bugs and maintain consistency with how dict.get works.

Behaviour of array values passed in a request

PR: https://github.com/frappe/frappe/pull/15784

Consider the following API call from a browser client:

fetch('/api/method/app.api.buy_fruits', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    fruits: ['Apple', 'Orange']
  })
})

api.py

@frappe.whitelist()
def buy_fruits(fruits):
  print(fruits) # 'Apple' instead of ['Apple', 'Orange']

As you can see, the value passed in fruits is 'Apple' instead of the list we passed. This is weird and incorrect behavior but is now fixed in Version 14. If you relied on this behavior, you need to make changes on your client-side API calls or server-side whitelisted methods based on your use case.

Data Migration Tool Deprecated

If you are one of the 2 people who is using the Data Migration Tool for syncing data between an external service and a Frappe site, you need to write your own syncing code because Data Migration Tool is removed from Version 14.

If you absolutely want to use the tool, you can pick up the code from the older version and move it into an app and use that instead.

Change of arguments on frappe.log_error

log_error now supports two more parameters to link errors to a particular document.

To simplify error logging this function's signature was changed to flip order of title and message`.

def log_error(title=None, message=None, reference_doctype=None, reference_name=None):
    ...

If you did not specify keyword argument manually, you don't need to change anyting. If you have specified title explicitly and not specified message then you should update function call as per new signature. Example:

- frappe.log_error("Some message", title="error title")
+ frappe.log_error(message="Some message", title="error title")

Removal of standard fields - parent, parenttype and parentfield

These three fields are removed from non-child doctypes. These fields were never used on non-child doctypes. If your code directly accesses three fields you might need to check if the doctype is child type first. Alternatively, you can use doc.get to safely check and get value if it exists.

Change in the package build system

Frappe and ERPNext are now built using a single pyproject.toml file specifying all the dependencies, development dependencies etc. This build system replaces old setup.py and requirements.txt

Read more about this on the PR: https://github.com/frappe/frappe/pull/17174

Breaking change in Virtual DocType interface

Virtual doctypes need to implement a certain set of methods on doctype controller so it can use other data sources as backend. Version 14 improves this interface requirement by converting few object methods to static methods like get_list and get_count.

You can read more about the changed requirements and a practical example on the documentation page: https://frappeframework.com/docs/v14/user/en/basics/doctypes/virtual-doctype

Rating field value

Rating fields used to show value from 0 to 5 start before. Number of stars are now made configurable. This however also means that the storage underlying value needs to be changed to reflect the actual number of starts. Now Rating fields store a decimal number between 0 and 1 representing the rating out of the specified number of stars.

Stars Previous Value New Value
* * * * * 5 1.0
* * * * . 4 0.8
. . . . . 0 0.0
* * * * * * * * . . Not possible 0.8

If you performed any arithmetic operations on the rating field values you need to change the code to consider normalized values.

Dependencies

We have removed following libraries from the codebase in version 14. If you have used these library in your app, please add them manually for your app.

Javascript

  • fluxify
  • bootstrap.js (v3)
  • jquery.hotkeys.js
  • prettyDate.js
  • bootstrap_theme
  • express
  • fuse.js
  • hyper.js

Python

  • html2text
  • md5
  • paytmchecksum
  • razorpay
  • stripe
  • braintree

Icons

  • glyphicons
  • font-awesome (moved to separate folder)
- /assets/frappe/css/font-awesome.css
+ /assets/frappe/css/fonts/fontawesome/font-awesome.min.css # use minified version

We have also removed local copy of sockect.io & sortableJS and used these libraries that are pulled from package managers. So you might have to update the path of these libraries in your application.

Separation(s)

We have moved all the payment gateways (Braintree, Stripe, PayPal, PayTM, Razorpay - along with Payment Gateway DocType) as well as it's dependencies from frappe to a separate app payments. To install this app, run following commands:

$ bench get-app payments
$ bench --site <your-site-name> install-app payments
Clone this wiki locally