ERPNext Technical Documents¶
Dev vs Production¶
By default when you install bench {and following instructions from here }, frappe and erpnext - all of them will be loaded on development branch. Following are instructions to have master {aka production version installed}
git clone https://github.com/frappe/bench bench-repo
pipenv --python 3.6
pipenv shell
pip3 install --user -e bench-repo
bench init frappe-bench && cd frappe-bench
bench switch-to-branch version-12
bench get-app erpnext https://github.com/frappe/erpnext --branch version-12
bench update --patch or bench update {run this despite getting error}
bench new-site site1.local {make sure we have right db for `site_config.json`}
bench --site site1.local install-app erpnext
bench start
Alternatively this can be achieved using:
wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py
sudo python install.py --production --user frappe
Exporting custom app¶
Make sure developer mode is activiated
Un-check custom checkbox for all DocTypes you want to export
Ensure all your DocTypes are visible in whatever app you’ve created using bench new-app
Initialize empty git repo using git int .
Commit and push all you changes to repo
Deploying custom app¶
Clone repo to apps
Install using pip: ./env/bin/pip install apps/hello_world/ –no-cache-dir
Make sure app name exists in ./sites/app.txt
Install app on associated site: bench –site site1.local install-app hello_world {Here name of out custom application is hello_world}
If you get AttributeError: WebApplicationClient issue solve it using: this solution
Code Examples¶
Dynamically Create Child Rows¶
Following example shows with Default Values {Including Dates}
frappe.ui.form.on("Sales Order", "spot_to_date", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
if(d.spot_from_date === undefined || d.spot_from_date > d.spot_to_date){
frappe.throw(("Please select valid From Date"));
}else{
var current = new Date(d.spot_from_date);
current = new Date(current.setDate(current.getDate() - 1))
var end = new Date(d.spot_to_date);
var i = 0;
while(current < end){
var child = cur_frm.add_child("iro_spot_schedule");
frappe.model.set_value(child.doctype, child.name, 'date', frappe.datetime.add_days(d.spot_from_date, i));
frappe.model.set_value(child.doctype, child.name, 'program_name', d.default_program_name);
var next = new Date(current.setDate(current.getDate() + 1))
current = next;
i += 1;
}
frm.refresh_field("iro_spot_schedule")
}
});
The above example gets triggered on DocType Sales Order when spot_to_date is changed
Triggering code just before saving¶
Lets say you want to do either of following:
Validation
Adjust Fields
Calculate Values at Runtime
following example shows you how
frappe.ui.form.on('Item', {
validate: function(frm) {
var results = [];
var item_code = "Material Code:" + frm.doc.item_code;
results.push(item_code);
if(frm.doc.manufacturers.length > 0){
var firstItem = frm.doc.manufacturers[0];
var manufacturersCode = "Manufacturers Part No:" + firstItem.manufacturer_part_no;
results.push(manufacturersCode);
}
if(frm.doc.gtl_hazmat){
results.push("<b><p style='color:red'>This item is classified as Hazardous Material for shipment</p></b>");
}
if(!frm.doc.gtl_description.includes("Material Code")){
results.push(frm.doc.gtl_description);
frm.set_value("gtl_description", results.join("<br />"));
}
}
});
Iterating over items in Child Table¶
frappe.ui.form.on("Channel", "onload", function(frm) {
console.log("Cool");
$.each(cur_frm.doc.markets_served, function(index, item){
console.log(item);
console.log(item.market_served);
var result = frappe.model.get_doc("Market", "Faridabad");
console.log(result);
});
console.log("Great");
});
Trigger for change in form field¶
frappe.ui.form.on("Channel", {
markets: function(frm){
console.log("channel changed");
}
});
Trigger for change in child table field¶
frappe.ui.form.on('Channel Market Table', {
market_served: function(frm){
console.log("market served changed");
console.log(frm);
}
})
Fetch value from Child Table¶
frappe.ui.form.on("Channel Market Table", "market_served", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
frappe.db.get_value("Market", {"name": d.market_served}, "district_connectivity", function(value) {
frappe.model.set_value(cdt, cdn, 'total_connectivity', value.district_connectivity);
console.log(value.district_connectivity);
});
});
Calculating based on intermediate fields¶
frappe.ui.form.on("Channel Market Table", "reach", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
if(d.total_connectivity > 0 && d.reach > 0){
var result = ((d.reach * d.total_connectivity)/100).toFixed(2);
frappe.model.set_value(cdt, cdn, 'channel_connectivity', result);
}
});
Dynamically add Rows to Child Table¶
frappe.ui.form.on('Sales Order', {
refresh(frm) {
console.log("this is cool");
var child = cur_frm.add_child("iro_spot_schedule");
var today = new Date();
frappe.model.set_value(child.doctype, child.name, 'date', today);
var child = cur_frm.add_child("iro_spot_schedule");
frappe.model.set_value(child.doctype, child.name, 'date', today);
}
})
Populate Child Rows based on Date Range Selection with Validation¶
frappe.ui.form.on("Sales Order", "spot_to_date", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
if(d.spot_from_date === undefined || d.spot_from_date > d.spot_to_date){
frappe.throw(("Please select valid From Date"));
}else{
var current = new Date(d.spot_from_date);
current = new Date(current.setDate(current.getDate() - 1))
var end = new Date(d.spot_to_date);
while(current < end){
console.log(current);
var child = cur_frm.add_child("iro_spot_schedule");
frappe.model.set_value(child.doctype, child.name, 'date', current);
var next = new Date(current.setDate(current.getDate() + 1))
current = next;
}
frm.refresh_field("iro_spot_schedule")
}
});
Updation of Total field based on change in Child Form¶
cur_frm.add_fetch('customer_name', 'customer_terms_and_conditions', 'customer_terms_and_conditions_template');
frappe.ui.form.on('Sales Order', 'refresh', function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
console.log(d.items);
var total = 0;
d.items.forEach(function(item){
console.log(item.gtl_total_price_usd);
total += item.gtl_total_price_usd
});
frappe.model.set_value(cdt, cdn, 'gtl_total_amount_usd', total);
});
frappe.ui.form.on("Sales Order Item", "gtl_unit_price_usd", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'gtl_total_price_usd', d.qty * d.gtl_unit_price_usd);
console.log(d);
});
Import files from Item master and attach to current DocType¶
Example of code from file apps/gtl_custom/gtl_custom/api.py
import json
import frappe
from frappe.utils.file_manager import save_url
@frappe.whitelist()
def attach_all_docs(document, method=None):
"""This function attaches drawings to the purchase order based on the items being ordered"""
document = json.loads(document)
count = 0
for item_doc in document["items"]:
item = frappe.get_doc("Item",item_doc["item_code"])
attachments = []
# Get the path for the attachments
if item.item_attachment_1:
attachments.append(item.item_attachment_1)
if item.item_attachment_2:
attachments.append(item.item_attachment_2)
if item.item_attachment_3:
attachments.append(item.item_attachment_3)
if item.item_attachment_4:
attachments.append(item.item_attachment_4)
for attach in attachments:
count = count + 1
save_url(attach, attach.split("/")[-1], document["doctype"], document["name"], "Home/Attachments", True)
frappe.msgprint("Attached {0} files".format(count))
JS part of this functionality
frappe.ui.form.on("Quotation", {
refresh: function(frm) {
frm.add_custom_button(__("Load Attachments"), function(foo) {
console.log("loading documents");
frappe.call({
method: "gtl_custom.api.attach_all_docs",
args: {
document: cur_frm.doc
},
callback: function(r) {
frm.reload_doc();
}
});
});
}
});
Downloading Child Table as TSV¶
frappe.ui.form.on("Sales Order", {
refresh: function(frm) {
// code to hide buttons
setTimeout(() => {
frm.remove_custom_button('Subscription', 'Create');
frm.remove_custom_button('Pick List', 'Create');
frm.remove_custom_button('Delivery Note', 'Create');
frm.remove_custom_button('Work Order', 'Create');
frm.remove_custom_button('Material Request', 'Create');
frm.remove_custom_button('Request for Raw Materials', 'Create');
frm.remove_custom_button('Project', 'Create');
frm.remove_custom_button('Subscription', 'Create');
},10);
frm.doc.items.forEach(function(item){
if(item.qty){
item.ua_quantity = item.qty * 10;}
else{
item.ua_quantity = 0;
}
});
frm.add_custom_button(__("Export MB (CSV)"), function() {
var URL = "/api/method/updateads.api.fetch_media_buying?iro_name=" + frm.doc.name;
window.open(URL, '_blank');
});
}
});
Key thing to note is frm.add_custom_button(__(“Export MB (CSV)”), function() section. Which contains code for button and linkage to backend.
Following is Python code for Server Side:
def get_media_buying(iro_name):
q = "SELECT state,mso,channel_name,channel_connectivity FROM `tabIRO Channel Table` WHERE parent='{0}' ORDER BY\
idx".format(iro_name)
q1='SELECT s_days,s_fct from `tabSummary Item Table` where parent="{0}" limit 1'.format(iro_name)
results = frappe.db.sql(q1)
df = pd.DataFrame(frappe.db.sql(q, as_list=True), columns=['State','MSO','Channel Name','Connectivity (HH)'])
if results:
df['Days'] = results[0][0]
df['Total FCT'] = results[0][1]
df.to_csv("/tmp/media_buying.csv", index=False)
@frappe.whitelist()
def fetch_media_buying(iro_name):
get_media_buying(iro_name)
frappe.local.response.filename = "{0}-Media-Buying.csv".format(iro_name)
frappe.local.response.filecontent = open("/tmp/media_buying.csv").read()
frappe.local.response.type = "download"
Customization References¶
Frappe Guide is a great collection of articles for development
- Customize ERPNext
This is a meta-list explaining how to customize different aspects
Web Hooks can be defined to make a call back on certain DocType events
Explanation of Cutomising Report {Specifically w.r.t. filtering}
Running script when something is selected
Adding Custom Button
Tutorials
Development References¶
frappe.get_doc is also used to create a new doc
Jinja2 templating engine is used {for HTML templates}
It is recommended of adding customization by Customize Form option as opposed to updating the DocType
frappe.call is used to make Ajax calls
- locals cdt cdn are cache values
Current Data Type
Current Data Name
frappe.form_dict gives all request parameters
frappe.get_doc used to get intance of a doctype {which is model}
frappe.get_all similar to objects.all() of django
frappe.get_list similar to get_all but only show records that current user has permission to
On python end testing is done using unittest module and on JS end Qunit is used
Hooks and hooks.py allow you to plugin to different part of ERPNext. This will also allows you to Rewrite APIs using Hooks
Background Services talks about different services Frappe uses. Redis is basically used as broker for Celery {which in turn is used for background jobs}
DocType Events is equivalent of def save for django. This will allow you to write custom code on various events
Making Public API is simple you need to add decorator @frappe.whitelist(), which will make API accessible from /api/method/myapp.api.get_last_project
On client side cur_frm will allow you to access current form’s object
Custom scripts allow you to modify certain aspect of Form only on client side. Some usecase for Custom Scripts include
Form validation
Fetching values from Masters or Linked DocTypes
One can also write call back for value changes
Community developed custom scripts good reference for custom scripts
Document name is like Primary Key {this is used while accessing API}. Example usage of APIs explains it
frappe UI Dialog is used for showing dialog
Print format builder will allow you to create reports/change their formatting
-
Contains information about different methods for Frappe
Frappe Confusions gives more clarity from third-person prespective
hooks.py allow interaction between core and custom application
doc_events: E.g. {Sales Order} saving of sales order, you want to run a custom function in customization_shipping
Define what event to call on`valiate`: erpnext_shipping.manage_shipping.sales_order_custom.calculate_total_weight
from frappe.utils import flt <- converts None to 0 float
Developer console also gives full stack trace of error
All callbacks across all app are calculated in
Good way to write client side scripting {this allows you to do version control}
fixtures/custom_scripts/ whatever doctype name e.g. Customer.js this is where you write custom logic in JS
It needs to be added to hooks.py. Make sure fixtures should be having Custom Field and Custom Script are present
Exporting print format using custom app is possible, and recommended to store in custom app for maintenance
REST APIs¶
Frappe Client is a good alternative that allows you to interact with Frappe’s installations without having to write raw REST APIs
- Token Authentication for APIs
Tokens are associated with users which has created
This implies that attribution {to assicated user} would be right when creating via APIs
Reporting¶
- Custom Reports in ERPNext, following are some of the report types
Report Builders - using the GUI
Script Report talks about creating custom reports
- Setting up new Query Report
Duplicate Report
For Module Select Custom Application
This will save JSON file with SQL Queries {This gets saved in report folder}
- To show this report edit config.py in Custom Application {E.g. stock.py}
Update get_data() function {Use existing files as a reference}
Update name and doctype attributes
Don’t forget to reload the site, restart bench and clear cache