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

  1. Make sure developer mode is activiated
  2. Un-check custom checkbox for all DocTypes you want to export
  3. Ensure all your DocTypes are visible in whatever app you’ve created using bench new-app
  4. Initialize empty git repo using git int .
  5. Commit and push all you changes to repo

Deploying custom app

  1. Clone repo to apps
  2. Install using pip: ./env/bin/pip install apps/hello_world/ –no-cache-dir
  3. Make sure app name exists in ./sites/app.txt
  4. Install app on associated site: bench –site site1.local install-app hello_world {Here name of out custom application is hello_world}
  5. 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:

  1. Validation
  2. Adjust Fields
  3. 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);
    }
});

Adding custom buttons

frappe.ui.form.on("Channel", "refresh", function(frm) {
    frm.add_custom_button(__("Do Something"), function() {
        console.log("Cool");
    });
});

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

  1. Frappe Guide is a great collection of articles for development
  2. Customization Articles
  3. Customize ERPNext
    1. This is a meta-list explaining how to customize different aspects
  4. Form Scripting in Frappe
  5. Web Hooks can be defined to make a call back on certain DocType events
  6. 1st Step for new devloper?
  7. Working with Notifications in ERPNext
  8. Explanation of Cutomising Report {Specifically w.r.t. filtering}
  9. Running script when something is selected
  10. Adding Custom Button
  11. Custom Button & Icon
  12. Tutorials
    1. Tutorial 1
    2. Tutorial 2

Development References

  1. frappe.get_doc is also used to create a new doc
  2. Jinja2 templating engine is used {for HTML templates}
  3. It is recommended of adding customization by Customize Form option as opposed to updating the DocType
  4. Custom CSS
  5. frappe.call is used to make Ajax calls
  6. locals cdt cdn are cache values
    1. Current Data Type
    2. Current Data Name
  7. frappe.form_dict gives all request parameters
  8. frappe.get_doc used to get intance of a doctype {which is model}
  9. frappe.get_all similar to objects.all() of django
  10. frappe.get_list similar to get_all but only show records that current user has permission to
  11. On python end testing is done using unittest module and on JS end Qunit is used
  12. Hooks and hooks.py allow you to plugin to different part of ERPNext. This will also allows you to Rewrite APIs using Hooks
  13. Background Services talks about different services Frappe uses. Redis is basically used as broker for Celery {which in turn is used for background jobs}
  14. DocType Events is equivalent of def save for django. This will allow you to write custom code on various events
  15. 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
  16. On client side cur_frm will allow you to access current form’s object
  17. Custom scripts allow you to modify certain aspect of Form only on client side. Some usecase for Custom Scripts include
    1. Form validation
    2. Fetching values from Masters or Linked DocTypes
  18. One can also write call back for value changes
  19. Community developed custom scripts good reference for custom scripts
  20. Document name is like Primary Key {this is used while accessing API}. Example usage of APIs explains it
  21. frappe UI Dialog is used for showing dialog
  22. Print format builder will allow you to create reports/change their formatting
  23. Frappe Cheatsheet
    1. Contains information about different methods for Frappe
  24. Frappe Confusions gives more clarity from third-person prespective
  25. Frappe Database API
  26. Advanced Database queries in Frappe
  27. hooks.py allow interaction between core and custom application
    1. doc_events: E.g. {Sales Order} saving of sales order, you want to run a custom function in customization_shipping
    2. Define what event to call on`valiate`: erpnext_shipping.manage_shipping.sales_order_custom.calculate_total_weight
  28. from frappe.utils import flt <- converts None to 0 float
  29. Developer console also gives full stack trace of error
  30. All callbacks across all app are calculated in
  31. Good way to write client side scripting {this allows you to do version control}
    1. fixtures/custom_scripts/ whatever doctype name e.g. Customer.js this is where you write custom logic in JS
    2. It needs to be added to hooks.py. Make sure fixtures should be having Custom Field and Custom Script are present
  32. Exporting print format using custom app is possible, and recommended to store in custom app for maintenance

REST APIs

  1. Filtering in REST APIs
  2. Example of calling ERPNext’s API Call via Requests
  3. Frappe Client is a good alternative that allows you to interact with Frappe’s installations without having to write raw REST APIs
  4. Token Authentication for APIs
    1. Tokens are associated with users which has created
    2. This implies that attribution {to assicated user} would be right when creating via APIs

Reporting

  1. Custom Reports in ERPNext, following are some of the report types
    1. Report Builders - using the GUI
    2. Query Report
    3. Script Report talks about creating custom reports
  2. How to hide field from print format
  3. Setting up new Query Report
    1. Duplicate Report
    2. For Module Select Custom Application
    3. This will save JSON file with SQL Queries {This gets saved in report folder}
    4. To show this report edit config.py in Custom Application {E.g. stock.py}
      1. Update get_data() function {Use existing files as a reference}
      2. Update name and doctype attributes
    5. Don’t forget to reload the site, restart bench and clear cache