DocType Actions can be used to implement functionality as a button in that DocType’s view.
DocType Links add hyperlinks to related documents (stored as Link fields) in that DocType’s view.
Frappe Custom DocTypes
Field types tips & warnings:
- Datetime field type, in MariaDB, Frappe will create a
datetime
column, which is without timezone (or hopefully will use UTC timezone). In form web UI, it will be shown as-is, i.e. not converted to local timezone. See https://discuss.erpnext.com/t/datetime-field-issue/16555.
Name & Title fields: When using it in a Doctype or Child Table, with Link or Table-MultiSelect, it will show built-in Name (or ID) field by default. If you want to use/display Title instead, there are two choices:
- Set Auto Name to “
field:title
“. This will set the actual primary key ID same as Title. Note that existing documents will need to be renamed, you may want to enable “Allow Rename” as well. - Create another Data field in referring Doctype. Set it as read-only. Then set its Fetch From, assuming the Link field is named
job_role
, then you set Fetch From tojob_role.title
. This way you’ll have two fields (Link field + read-only Title field of that Link) instead of just one Link field.
Table-MultiSelect field: As of ERPNext v13.beta.4, Table-MultiSelect fields are not filterable (originally authored by Faris Ansari). On Sep 23, 2020, Hendy offered to sponsor Faris to work on this filter.
Custom Actions
Tutorial: 3 custom action UIs: custom buttons, custom menus, custom icon buttons
That will give you a doctype edit form UI like this:
DocType action in edit form
The action should be named according to path, like app_name.module_name.doctype.doctype_name.doctype_name.verb
, e.g. lovia.talentiva.doctype.student_account_request.student_account_request
.create_sso_account
. Then implemented as Frappe Remote Method Call.
That method must be implemented as Python function. For the example above, it will be in file frappe-bench/apps/lovia/lovia/talentiva/doctype/student_account_request/student_account_request.py
and function name create_sso_account
. It must be decorated with @frappe.whitelist(). The function needs to accept a str
parameter (JSON-encoded) called doc
. Tip: If you’re not sure about what parameters and what types, you can inspect the REST API request using web browser’s Console.
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Lovia and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class StudentAccountRequest(Document):
pass
@frappe.whitelist()
def create_sso_account(doc: str):
doc_dict = json.loads(doc)
print('doc=%s' % (doc_dict,))
student_account_request = frappe.get_doc('Student Account Request', doc_dict['name'])
print('student_account_request=%s' % (student_account_request,))
return {'message': 'Hello!!!!', 'status': 'oh yes'}
Sample implementation that only prints out the doc contents
Important: When changing Python code, you’ll need to restart web app (bench/frappe serve), otherwise it won’t take effect.
When action is clicked, it will send a POST HTTP request to http://localhost:8000/api/method/lovia.talentiva.doctype.student_account_request.student_account_request.create_sso_account
:
Sample console output
Simple String response will be returned to client/browser as JSON inside “message
” field. After you get the simple response working, a better way to inform user is to use frappe.msgprint()
(see Frappe Python API below).
REST API method call response
Example complete student_account_request.py implementation with Frappe Python API and REST API client request:
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from uuid import uuid4
from datetime import datetime
import requests
@frappe.whitelist()
def create_sso_account(doc: str):
doc_dict = json.loads(doc)
print('doc=%s' % (doc_dict,))
student_account_request = frappe.get_doc('Student Account Request', doc_dict['name'])
print('student_account_request=%s' % (student_account_request,))
sso_id = uuid4()
student_account_request.sso_id = str(sso_id)
student_account_request.sso_username = doc_dict['desired_username']
fusionauth_api_key = FUSIONAUTH_API_KEY
print('Creating User & Lovia User Registration for "%s" with ID "%s"...' % (student_account_request.sso_username, student_account_request.sso_id))
lovia_user_resp = requests.post('https://login.lovia.life/api/user/registration/%s' % (student_account_request.sso_id,),
headers={'Authorization': fusionauth_api_key},
json={
'user': {
"birthDate": student_account_request.birthdate.isoformat(),
"email": student_account_request.email_personal,
"fullName": student_account_request.full_name,
"imageUrl": 'https://erp.lovia.life' + student_account_request.profile_photo,
"mobilePhone": student_account_request.mobile_number,
"passwordChangeRequired": True,
"preferredLanguages": ["id", "en"],
"timezone": "Asia/Jakarta",
"username": student_account_request.sso_username,
'data': {
'email_school': student_account_request.email_school,
'gender': student_account_request.gender,
'school': student_account_request.school,
'student_number': student_account_request.student_number,
'classroom': student_account_request.classroom,
}
},
'sendSetPasswordEmail': True,
'skipVerification': False,
'registration': {
"applicationId": "5691331c-b95d-4214-9de0-7ef0fd067cf9",
"roles": ["user"]
}
})
print('Lovia User: %s %s' % (lovia_user_resp.status_code, lovia_user_resp.text,))
if lovia_user_resp.status_code != 200:
frappe.throw(lovia_user_resp.text, exc=RuntimeError, title='Cannot create Lovia user & registration')
print('Registering Talentiva for "%s" with ID "%s"...' % (student_account_request.sso_username, student_account_request.sso_id))
talentiva_reg_resp = requests.post('https://login.lovia.life/api/user/registration/%s' % (student_account_request.sso_id,),
headers={'Authorization': fusionauth_api_key},
json={
'registration': {
"applicationId": "caf92ff9-cadd-49dc-9255-3e716c1775ab",
"roles": ["user"]
}
})
print('Talentiva Registration: %s %s' % (talentiva_reg_resp.status_code, talentiva_reg_resp.text,))
if talentiva_reg_resp.status_code != 200:
frappe.throw(talentiva_reg_resp.text, exc=RuntimeError, title='Cannot create Talentiva registration')
student_account_request.sso_created_at = datetime.now()
student_account_request.save()
frappe.msgprint(
msg='User "%s" created as "%s" and registered to Lovia & Talentiva.' % (student_account_request.sso_username, student_account_request.sso_id),
title='Lovia User & Registration',
indicator='green')
Frappe/ERPNext Python API
References:
- Frappe Developer API – Python API Reference
- ERPNext Developer Cheatsheet
- Frappe API Documentation – ERPNext Forum
- Document hooks – CRUD events
To throw exception/error, use frappe.throw()
.
To display modal message, use frappe.msgprint().
Using frappe.msgprint() to inform user
For Document hooks and CRUD events, you can put them inside apps/APP_NAME/APP_NAME/MODULE_NAME/doctype/DOCTYPE_NAME/DOCTYPE_NAME.py
. As alternative, check out also APP_NAME/hooks.py. Sample before_save
hook callback below:
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import getdate
from datetime import datetime
import dateutil
class Talent(Document):
pass
def before_save(self):
# Calculate age
if self.birthdate:
born = getdate(self.birthdate)
age = dateutil.relativedelta.relativedelta(getdate(), born)
self.age = age.years
frappe.logger().info('Talent %s was born %s, calculated age=%d' % (self.person_name, born, self.age))
elif self.estimated_birth_year:
self.age = datetime.today().year - self.estimated_birth_year
frappe.logger().info('Talent %s was born estimated year %s, estimated age=%d' % (self.person_name, self.estimated_birth_year, self.age))
# Set last_known_monthly_salary if talent filled previous_monthly_salary
if self.last_known_monthly_salary is None and self.previous_monthly_salary is not None:
self.last_known_monthly_salary = self.previous_monthly_salary
# Set last_known_employment_date if we have last employment information
if self.last_known_employment_date is None:
if self.last_known_employment or self.last_known_employer or self.last_known_job_role or \
(self.last_known_monthly_salary is not None and self.last_known_monthly_salary > 0):
self.last_known_employment_date = datetime.now()
Calculated Field using Custom Doctype Script
Calculate Employee Age from Birthdate
Reference:
- https://discuss.erpnext.com/t/calculate-age-for-a-student/20235/15
- https://discuss.erpnext.com/t/calculate-employee-age-from-date-of-birth/32414
- https://docs.erpnext.com/docs/user/manual/en/customize-erpnext/custom-scripts/update-date-field-based-on-value-in-other-date-field
Custom (Client) Script:
frappe.ui.form.on('Talent', {
refresh: function(frm) {
if (frm.doc.birthdate) {
frm.set_value('age', moment().diff(frm.doc.birthdate, 'years'));
} else {
frm.set_value('age', null);
}
frm.refresh_field('age');
},
birthdate: function(frm, cdt, cdn) {
// https://docs.erpnext.com/docs/user/manual/en/customize-erpnext/custom-scripts/update-date-field-based-on-value-in-other-date-field
// https://discuss.erpnext.com/t/calculate-age-for-a-student/20235/16?u=hendy
if (frm.doc.birthdate) {
frm.set_value('age', moment().diff(frm.doc.birthdate, 'years'));
} else {
frm.set_value('age', null);
}
frm.refresh_field('age');
}
});
To add Custom Client Script, there are two ways:
- Recommended is to change file in app’s module, e.g.
frappe-bench/apps/lovia/lovia/talentiva/doctype/talent/talent.js
. - For ad hoc changes, you can use ERPNext > Custom Script.
If you want to use server-side calculation instead, see https://discuss.erpnext.com/t/how-to-calc-and-update-a-field-before-save/44101/7?u=hendy
def validade(self):
self.calc_estoque()
def calc_estoque(doc, method):
doc.est_disp = (doc.est or 0) - (doc.est_res or 0)
Form Scripting
Reference: https://medium.com/@basawarajsavalagi/form-scripting-in-frappe-framework-996c949c6b76
Custom Fields: Customize Form vs Custom App
Reference: Customize Form vs Editing Doctype. TL;DR: If we want to add/edit fields from built-in doctypes that are specific to a site, we use Customize form. Otherwise, we should use Custom Add, we can add a new doctype, and also add new fields to built-in doctypes.
When we use Customize form or use the custom
folder in custom app, it is the same as adding Custom Field documents. So when you use custom folder, you cannot delete already added fields using the custom app’s JSON definition, you’ll need to use “Custom Field List” UI to delete them.
The third way is custom
folder. This is used by erpnext/erpnext/erpnext_integrations/custom/contact.json
. and erpnext/erpnext/erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json
. It seems to be undocumented by Hendy thinks it’s cleaner. Only two files of all ERPNext uses this technique.
To add custom fields to built-in doctypes using custom app, add a file e.g. frappe-bench/apps/lovia/lovia/lovia/custom/contact.json
:
{
"custom_fields": [
{
"creation": "2020-10-19 11:00:05.000000",
"modified": "2020-10-19 11:00:06.000000",
"dt": "Contact",
"insert_after": "phone_nos",
"fieldname": "lovia_integration_section",
"fieldtype": "Section Break",
"label": "Lovia Integration"
},
{
"creation": "2020-10-19 11:00:03.432994",
"modified": "2020-10-19 11:00:05.000000",
"dt": "Contact",
"insert_after": "lovia_integration_section",
"description": "FusionAuth SSO ID",
"fieldname": "sso_id",
"fieldtype": "Data",
"in_filter": 1,
"in_standard_filter": 1,
"label": "SSO ID",
"search_index": 1,
"in_global_search": 1
},
{
"creation": "2020-10-19 11:00:03.432994",
"modified": "2020-10-19 11:00:05.000000",
"dt": "Contact",
"insert_after": "sso_id",
"description": "Username in Lovia",
"fieldname": "lovia_username",
"fieldtype": "Data",
"in_filter": 1,
"in_standard_filter": 1,
"label": "Lovia Username",
"search_index": 1,
"in_global_search": 1
},
{
"creation": "2020-10-19 11:00:03.432994",
"modified": "2020-10-19 11:00:05.000000",
"dt": "Contact",
"insert_after": "lovia_username",
"fieldname": "lovia_integration_break",
"fieldtype": "Column Break"
},
{
"creation": "2020-10-19 11:00:03.432994",
"modified": "2020-10-19 11:00:06.000000",
"dt": "Contact",
"insert_after": "lovia_integration_break",
"description": "User ID used by Miluv v4 (integer)",
"fieldname": "miluv_v4_id",
"fieldtype": "Int",
"in_filter": 1,
"in_standard_filter": 1,
"label": "Miluv v4 ID",
"search_index": 1,
"in_global_search": 1
}
],
"custom_perms": [],
"doctype": "Contact",
"property_setters": [],
"sync_on_migrate": 1
}
After this you’ll need to do bench migrate
. (or if you need to pull the repository first, bench --pull --patch --apps lovia
)
Result: