ON THIS PAGE…
- Sample Extension
- Creating an Extension
- Custom Module for Salespersons
- Connection for the Extension
- Custom Function to Calculate the Salesperson’s Commission
- Workflow Rule for the Custom Function
- Salesperson Name Custom Field in the Expenses Module
- Global Field to Store the Salesperson’s Data
- Settings Widget for the Salesperson Extension
- Sync New Salesperson With the Salesperson Module
- Sync Existing Salespersons During Extension Installation
Sample Extension
Let’s see how to build an extension for Zoho Books from scratch. In this help document, we’ll explain the steps involved in building the Salesperson extension as a sample extension to learn this better. First, let’s understand why Salesperson extension is required.
In Zoho Books, it is currently not possible to track salespersons within an organization. Using the Salesperson extension, admins of an organization can keep track of the salespersons and their activities, and assign commissions to them each time a customer associated with them pays for an invoice.
Now that you’ve understood the need for the Salesperson extension let’s start building the extension. The steps involved in building this extension are:
- Creating an extension
- Custom module for salespersons
- Connection for extension
- Custom function to calculate the salesperson’s commission
- Workflow rule for custom function
- Salesperson Name custom field in the Expenses module
- Global field to store salesperson’s data
- Settings widget for the extension
- Syncing new salesperson with the Salesperson module
- Syncing existing salespersons during extension installation
Creating an Extension
The first step is to create an extension. To do this:
- Login to Zoho Sigma.
- If you have a single workspace you’ll be redirected to your workspace. Or, if you have multiple workspaces, select the workspace in which you want to create the extension. The All Extensions tab under Extensions will open by default.
- Click New Extension in the top right corner.
-
On the New Extension page:
- Enter the Name and Description of the extension.
- Select the Zoho Books logo under Services.
- Agree to the terms and conditions.
- Click Create.
With this, you have created the Salesperson extension. The next step is to add the necessary components for the extension. To do this:
- Click the Edit Extension button in the salesperson extension’s card. You’ll be redirected to Zoho Books Developer Portal.
Custom Module for Salespersons
You need a module to keep track of all the Salespersons in your organization. You can use the Custom Modules feature to create a module for Salesperson. Here’s how:
-
Go to the Build section at the top.
-
The Preferences tab will open by default. In the Preferences pane of the tab, scroll down and click View More in the Custom Modules card.
-
Click Create Module.
-
A pop-up will appear. In the Module Details section:
- Enter the Module Name, Plural Name, and Description for the custom module.
- Click Next.
-
In the Primary Field Properties section:
- Enter a name for the primary data type of the custom module in the Label Name field.
- Select the data type of the primary field in the Data Type field.
- Click Save.
You’ll have to create the additional fields for the Salesperson module. Here’s how you can do this:
- Switch to the Fields tab at the top of the Salesperson custom module.
- Click + New Field in the top right corner.
- Fill in the required details on the New Field page.
Refer to the image below to view the additional fields that you’ll have to create and their details.
Note
- For the Salesperson Account field, select Accounts as the Module that has to be looked up.
- For the Salesperson Commission Type field, enter Percentage and Amount as the dropdown options.
Connection for the Extension
You have to create a connection so that the extension has the permissions required to fetch the data necessary from Zoho Books. To create a connection:
-
Go to the Build section at the top.
-
Click Connections in the left sidebar.
-
Click New Connection in the top right corner.
-
On the following page:
-
Select Zoho Books in the Default Services tab of the Pick Your Services section.
-
Enter Zoho Books - Salesperson as the Connection Name in the Connection Details section.
-
Enable the following scopes:
- ZohoBooks.customerpayments.READ
- ZohoBooks.custommodules.All
- ZohoBooks.expenses.CREATE
- ZohoBooks.expenses.READ
- ZohoBooks.expenses.UPDATE
- ZohoBooks.invoices.READ
- ZohoBooks.settings.CREATE
- ZohoBooks.settings.READ
- ZohoBooks.settings.UPDATE
-
Click Create and Connect.
-
Custom Function to Calculate the Salesperson’s Commission
You have to create a custom function that automatically calculates the salesperson’s commission when the salesperson creates an invoice. Also, the custom function will automatically create an expense for the saleseprson’s commission once the invoice’s status is Paid. This will help you keep track of the commissions you owe to each salesperson in your organization.
To create the custom function:
-
Click Build at the top.
-
Click Automation in the left sidebar and select Workflow Actions from the dropdown.
-
Select Custom Functions on the Workflow Actions pane.
-
Click + New Custom Function on the top right corner of the page.
-
On the New Custom Function page:
- Enter the Function Name.
- Select the module from which you’re creating the custom function from the dropdown next to the Module field.
- Check the Allow use of Global Fields in this custom function option next to Global Fields.
- Paste the following code in the Deluge screen.
organizationID = organization.get("organization_id"); domainUrl = organization.get("api_root_endpoint"); customModuleApiName = "cm__u76nt_salesperson"; expense_api_name = "cf__u76nt_salesperson_name"; cmNameApiName = "cf__u76nt_salesperson_name"; cmEmailApiName = "cf__u76nt_salesperson_e_mail"; cmAccountApiName = "cf__u76nt_salesperson_account"; cmTypeApiName = "cf__u76nt_salesperson_commission_type"; cmRateApiName = "cf__u76nt_salesperson_commission_rate"; cmStatusApiName = "cf__u76nt_salesperson_status"; globalFieldApiName = "vl__u76nt_data_store"; dataStore = global_fields.get(globalFieldApiName).get("value"); user_given_commission = dataStore.get("commission"); user_given_expense_id = dataStore.get("expense_account").get("id"); user_given_commission_type = dataStore.get("type"); user_given_status = dataStore.get("status"); user_given_paid_id = dataStore.get("paid_through_account").get("id"); user_given_specification_type = dataStore.get("specification_type"); customer_id = invoice.get("customer_id"); invoice_status = invoice.get("status"); invoice_number = invoice.get("invoice_number"); invoice_id = invoice.get("invoice_id"); invoice_salesperson_id = invoice.get("salesperson_id"); invoice_salesperson_name = invoice.get("salesperson_name"); invoiceMap = Map(); invoiceMap.put("salesperson_id",invoice_salesperson_id); invoiceMap.put("salesperson_name",invoice_salesperson_name); connection_link_name = "salesperson_commission_books"; total_page = invokeurl [ url :domainUrl + "/" + customModuleApiName + "?page=1&per_page=200&response_option=2&organization_id=" + organizationID type :GET connection:"salesperson_commission_books" ]; total_page = total_page.get("page_context").get("total_pages"); // total page count make as loop loop = rightpad(",",total_page).toList(""); // Get Salesperson Details salesperson_response = invokeurl [ url :domainUrl + "/salespersons?organization_id=" + organizationID type :GET connection:"salesperson_commission_books" ]; if(salesperson_response.get("code") == 0) { data = salesperson_response.get("data"); if(invoice_status == "paid" && user_given_status == 'paid') { // based upon subtotal info "paidStatus"; if(user_given_specification_type == 'SubTotal') { invoice_amount = invoice.get("sub_total"); } // based upon the total else if(user_given_specification_type == 'Total') { invoice_amount = invoice.get("total"); } payment_id_responses = invokeurl [ url :domainUrl + "/invoices/" + invoice_id + "/payments?organization_id=" + organizationID type :GET connection:"salesperson_commission_books" ]; if(payment_id_responses.get("code") == 0) { customerpayments_id = payment_id_responses.get("payments").get(0).get("payment_id"); // get paid_through_account payment_response = invokeurl [ url :domainUrl + "/customerpayments/" + customerpayments_id + "?organization_id=" + organizationID type :GET connection:"salesperson_commission_books" ]; paid_through_id = payment_response.get("payment").get("account_id"); } info payment_id_responses.get("message"); } else if(invoice_status == "sent" && user_given_status == "sent") { // based upon the subtotal info "sentStatus"; if(user_given_specification_type == "SubTotal") { invoice_amount = invoice.get("sub_total"); } //based upon the total else if(user_given_specification_type == 'Total') { invoice_amount = invoice.get("total"); } paid_through_id = user_given_paid_id; } for each count in data { if(count.get("salesperson_id") == invoice_salesperson_id) { salesperson_email = count.get("salesperson_email"); salesperson_status = count.get("is_active"); } } if(salesperson_status == false) { salesperson_status = "Inactive"; } else { salesperson_status = "Active"; } count = 0; page = 0; invoice_amount = invoice_amount.toDecimal(); for each execute in loop { page = page + 1; // Get Custom Module Details customModule_response = invokeurl [ url :(domainUrl + '/') + customModuleApiName + '?page=' + page + '&usestate=false&organization_id=' + organizationID type :GET connection:"salesperson_commission_books" ]; customModule_records = customModule_response.get("module_records"); for each records in customModule_records { if(records.get(cmEmailApiName) == salesperson_email) { count = count + 1; customModule_id = records.get('module_record_id'); salesperson_commission_type = records.get(cmTypeApiName); salesperson_commission_rate = records.get(cmRateApiName); salesperson_expense_id = records.get(cmAccountApiName); body = {"JSONString":{cmNameApiName:invoice_salesperson_name,cmTypeApiName:salesperson_commission_type,cmRateApiName:salesperson_commission_rate,cmEmailApiName:salesperson_email,cmAccountApiName:salesperson_expense_id,cmStatusApiName:salesperson_status}}; if(salesperson_commission_type == 'Percentage') { amount = (invoice_amount * salesperson_commission_rate.toDecimal()) / 100; } else { amount = salesperson_commission_rate; } updatecustomModuleFields = invokeurl [ url :domainUrl + "/" + customModuleApiName + "/" + customModule_id + "?organization_id=" + organizationID type :PUT parameters:body connection:"salesperson_commission_books" ]; if(updatecustomModuleFields.get("code") == 0) { info "updateCustomModule"; } info updatecustomModuleFields.get("message"); } } } if(count == 0) { body = {"JSONString":{cmNameApiName:invoice_salesperson_name,cmTypeApiName:user_given_commission_type,cmRateApiName:user_given_commission,cmEmailApiName:salesperson_email,cmAccountApiName:user_given_expense_id,cmStatusApiName:salesperson_status}}; createcustomModuleFields = invokeurl [ url :domainUrl + "/" + customModuleApiName + "?organization_id=" + organizationID type :POST parameters:body connection:"salesperson_commission_books" ]; if(createcustomModuleFields.get("code") == 0) { info "createCustomModule"; salesperson_commission_type = user_given_commission_type; salesperson_commission_rate = user_given_commission; customModule_id = createcustomModuleFields.get('module_record').get('module_record_id'); salesperson_expense_id = user_given_expense_id; if(salesperson_commission_type == 'Percentage') { amount = (invoice_amount * salesperson_commission_rate.toDecimal()) / 100; } else { amount = salesperson_commission_rate; } } info createcustomModuleFields.get("message"); } if(salesperson_commission_type == 'Percentage') { body = {"JSONString":{"account_id":salesperson_expense_id,"amount":amount,"paid_through_account_id":paid_through_id,"reference_number":invoice_number,"customer_id":customer_id,"description":"SalespersonCommision" + " " + salesperson_commission_rate + "%" + " " + "from invoice to" + " " + invoice_salesperson_name,"custom_fields":{{"value":customModule_id,"api_name":expense_api_name}}}}; } else { body = {"JSONString":{"account_id":salesperson_expense_id,"amount":amount,"paid_through_account_id":paid_through_id,"reference_number":invoice_number,"customer_id":customer_id,"description":"SalespersonCommision" + " " + "₹" + salesperson_commission_rate + " " + "from invoice to" + " " + invoice_salesperson_name,"custom_fields":{{"value":customModule_id,"api_name":expense_api_name}}}}; } // expense total page count expense_count_response = invokeurl [ url :domainUrl + "/expenses?per_page=200&response_option=2&organization_id=" + organizationID type :GET connection:"salesperson_commission_books" ]; expense_records_count = expense_count_response.get("page_context").get("total_pages"); execution_of_loop = rightpad(",",expense_records_count).toList(""); page = 0; for each execute in execution_of_loop { page = page + 1; expense_response = invokeurl [ url :domainUrl + "/expenses?page=" + page + "&per_page=200&filter_by=Status.All&sort_column=created_time&sort_order=D&usestate=true&organization_id=" + organizationID type :GET connection:"salesperson_commission_books" ]; expense_list = expense_response.get("expenses"); // Find the reference number for each count in expense_list { if(count.get("reference_number") == invoice_number) { expense_id = count.get("expense_id"); break; } } if(expense_id != null) { expense_response = invokeurl [ url :domainUrl + "/expenses/" + expense_id + "?organization_id=" + organizationID type :PUT parameters:body connection:"salesperson_commission_books" ]; if(expense_response.get("code") == 0) { info "updateExpense"; } info expense_response.get("message"); break; } } if(expense_id == null) { expense_response = invokeurl [ url :domainUrl + "/expenses?organization_id=" + organizationID type :POST parameters:body connection:"salesperson_commission_books" ]; if(expense_response.get("code") == 0) { info "createExpense"; } info expense_response.get("message"); } } else { info salesperson_response.get("message"); }
- Click Save.
Workflow Rule for the Custom Function
For the custom function to automatically calculate the salesperson’s commission once they create an invoice, you have to create a workflow rule that gets triggered automatically once an invoice is created or edited. You can associate the custom function with the workflow rule.
To create the custom function:
-
Click Build at the top.
-
Click Automation in the left sidebar and select Workflow Rules from the dropdown.
-
Click + New Workflow Rule in the top right corner.
-
In the Name your workflow section:
- Enter the Workflow Rule Name.
- Select the module for which you’re creating the workflow rule from the dropdown next to the Module field.
-
In the Choose when to trigger section:
- Select Created or Edited for the When Invoice is field.
- Select Any selected field is updated from the dropdown next to the Execute the workflow when field.
- In the dropdown next to it, select Salesperson, Status, and Total as the fields based on which the workflow rule should execute.
-
Select the filters as shown in the image below in the Filter the triggers section.
-
In the Actions section:
- Click the dropdown below the Type column and select Custom Functions.
- Click the dropdown below the Name column and select the custom function you created earlier.
-
Click Save.
Salesperson Name Custom Field in the Expenses Module
In Zoho Books, the Salesperson field is available in the sales modules such as Quotes, Sales Orders, and Invoices. You have to create the Salesperson field in the Expenses module to keep track of the Salesperson for whom the expense is created. To do this, you can use the Custom Fields feature in Zoho Books. Here’s how:
-
Go to Build at the top.
-
Click Preferences in the left sidebar.
-
Select Expenses in the Preferences pane.
-
Click + New Custom Field.
-
On the New Custom Field page:
- Enter the custom field’s name in the Label Name field.
- Select Lookup as the Data Type for the custom field.
- Select Salesperson as the module from which you want to lookup the data in the Module field.
- Enter the Related List Name.
- Click Save.
Global Field to Store the Salesperson’s Data
You have to create a global field to store the salesperson’s data. Here’s how you can do this:
-
Go to the Configure section at the top of the Developer Portal.
-
The Global Fields tab will open by default. Click + New Field in the top right corner.
-
On the New Field page:
- Enter the Name and Description of the global field.
- Select the Data Type.
- Click Save.
Settings Widget for the Salesperson Extension
You need to create a settings widget for the Salesperson extension. The widget will contain the UI components required for the initial configuration of the Salesperson extension. You can create the settings widget using any framework of your choice. In this extension, we’ve used the Vue, React, and Vanilla JS.
After you create the widget, you need to pack it and upload it into the global field.
Sync New Salesperson With the Salesperson Module
Whenever you create a new salesperson, you’ll have to add them to the Salespersons module. This can be a time-consuming process. Instead, you can create a custom button in the Salesperson module. On clicking the custom button, the newly added salesperson will be automatically synced with the Salesperson module. Here’s how you can do this:
-
Go to Build section at the top.
-
Select the Salesperson custom module in the Preferences pane.
-
Switch to the Custom Buttons tab.
-
Click + New in the top right corner.
-
On the New Custom Button page:
- Enter the Custom Button Name. For example, Sync New Salesperson.
- Choose the custom button’s Visibility.
- Select the Location of the custom button as List Page - Action Menu.
- Paste the following code in the Deluge screen.
organizationID = organization.get("organization_id"); domainUrl = organization.get("api_root_endpoint"); customModuleApiName = "cm__u76nt_salesperson"; cmNameApiName = "cf__u76nt_salesperson_name"; cmEmailApiName = "cf__u76nt_salesperson_e_mail"; cmAccountApiName = "cf__u76nt_salesperson_account"; cmTypeApiName = "cf__u76nt_salesperson_commission_type"; cmRateApiName = "cf__u76nt_salesperson_commission_rate"; cmStatusApiName = "cf__u76nt_salesperson_status"; globalFieldApiName = "vl__u76nt_data_store"; dataStore = global_fields.get(globalFieldApiName).get("value"); user_given_commission = dataStore.get("commission"); user_given_expense_id = dataStore.get("expense_account").get("id"); user_given_commission_type = dataStore.get("type"); connection_link_name = "salesperson_commission_books"; // GET total page count of Custom Module . total_page = invokeurl [ url :domainUrl + "/" + customModuleApiName + "?page=1&per_page=200&response_option=2&organization_id=" + organizationID type :GET connection:"salesperson_commission_books" ]; total_page = total_page.get("page_context").get("total_pages"); // total page count make as loop loop = rightpad(",",total_page).toList(""); // Get Salesperson Details salesperson_response = invokeurl [ url :domainUrl + "/salespersons?organization_id=" + organizationID type :GET connection:"salesperson_commission_books" ]; data = salesperson_response.get("data"); customModule_email_map = Map(); for each record in data { is_present = 0; salesperson_name = record.get("salesperson_name"); salesperson_email = record.get("salesperson_email"); salesperson_status = record.get("is_active"); if(salesperson_status == false) { salesperson_status = "Inactive"; } else { salesperson_status = "Active"; } body = {"JSONString":{cmNameApiName:salesperson_name,cmTypeApiName:user_given_commission_type,cmRateApiName:user_given_commission,cmEmailApiName:salesperson_email,cmAccountApiName:user_given_expense_id,cmStatusApiName:salesperson_status}}; page = 0; for each index in loop { page = page + 1; // Get Custom Module Details customModule_response = invokeurl [ url :(domainUrl + '/') + customModuleApiName + '?page=' + page + '&usestate=false&organization_id=' + organizationID type :GET connection:"salesperson_commission_books" ]; customModule_records = customModule_response.get("module_records"); for each records in customModule_records { email = records.get(cmEmailApiName); customModule_email_map.put(email,email); if(records.get(cmEmailApiName) == salesperson_email) { is_present = is_present + 1; customModule_id = records.get('module_record_id'); salesperson_commission_type = records.get(cmTypeApiName); salesperson_commission_rate = records.get(cmRateApiName); salesperson_expense_id = records.get(cmAccountApiName); body = {"JSONString":{cmNameApiName:salesperson_name,cmTypeApiName:user_given_commission_type,cmRateApiName:user_given_commission,cmEmailApiName:salesperson_email,cmAccountApiName:user_given_expense_id,cmStatusApiName:salesperson_status}}; updatecustomModuleFields = invokeurl [ url :domainUrl + "/" + customModuleApiName + "/" + customModule_id + "?organization_id=" + organizationID type :PUT parameters:body connection:"salesperson_commission_books" ]; } } if(is_present == 0) { createcustomModuleFields = invokeurl [ url :domainUrl + "/" + customModuleApiName + "?organization_id=" + organizationID type :POST parameters:body connection:"salesperson_commission_books" ]; info createcustomModuleFields; } } } for each count in data { salesperson_email = count.get("salesperson_email"); customModule_email_map.remove(salesperson_email); } for each index in customModule_email_map { for each records in customModule_records { if(records.get(cmEmailApiName) == index) { customModule_id = records.get('module_record_id'); salesperson_commission_type = records.get(cmTypeApiName); salesperson_commission_rate = records.get(cmRateApiName); salesperson_expense_id = records.get(cmAccountApiName); salesperson_name = records.get(cmNameApiName); body = {"JSONString":{cmNameApiName:salesperson_name,cmTypeApiName:salesperson_commission_type,cmRateApiName:salesperson_commission_rate,cmEmailApiName:index,cmAccountApiName:salesperson_expense_id,cmStatusApiName:"This Salesperson Name not Exist"}}; updatecustomModuleFields = invokeurl [ url :domainUrl + "/" + customModuleApiName + "/" + customModule_id + "?organization_id=" + organizationID type :PUT parameters:body connection:"salesperson_commission_books" ]; } } } resultMap = Map(); resultMap.put("code",0); return resultMap;
- Click Save or Save and Execute.
Sync Existing Salespersons During Extension Installation
Before an organization installs the Salesperson extension, they might already have created salespersons. Using the On Installation component, you can sync these salespersons to the Salesperson module, when the organization installs your extensions. Here’s how:
- Go to the Configure section at the top.
- Select Install Actions in the left sidebar.
- The On Installation tab will open by default.
- Paste the following code in the Deluge screen.
organizationID = organization.get("organization_id");
domainUrl = organization.get("api_root_endpoint");
customModuleApiName = "cm__u76nt_salesperson";
cmNameApiName = "cf__u76nt_salesperson_name";
cmEmailApiName = "cf__u76nt_salesperson_e_mail";
cmAccountApiName = "cf__u76nt_salesperson_account";
cmTypeApiName = "cf__u76nt_salesperson_commission_type";
cmRateApiName = "cf__u76nt_salesperson_commission_rate";
cmStatusApiName = "cf__u76nt_salesperson_status";
globalFieldApiName = "vl__u76nt_data_store";
dataStore = global_fields.get(globalFieldApiName).get("value");
user_given_commission = dataStore.get("commission");
user_given_expense_id = dataStore.get("expense_account").get("id");
user_given_commission_type = dataStore.get("type");
connection_link_name = "salesperson_commission_books";
// GET total page count of Custom Module .
total_page = invokeurl
[
url :domainUrl + "/" + customModuleApiName + "?page=1&per_page=200&response_option=2&organization_id=" + organizationID
type :GET
connection:"salesperson_commission_books"
];
total_page = total_page.get("page_context").get("total_pages");
// total page count make as loop
loop = rightpad(",",total_page).toList("");
// Get Salesperson Details
salesperson_response = invokeurl
[
url :domainUrl + "/salespersons?organization_id=" + organizationID
type :GET
connection:"salesperson_commission_books"
];
data = salesperson_response.get("data");
for each record in data
{
is_present = 0;
salesperson_name = record.get("salesperson_name");
salesperson_email = record.get("salesperson_email");
salesperson_status = record.get("is_active");
if(salesperson_status == false)
{
salesperson_status = "Inactive";
}
else
{
salesperson_status = "Active";
}
body = {"JSONString":{cmNameApiName:salesperson_name,cmTypeApiName:user_given_commission_type,cmRateApiName:user_given_commission,cmEmailApiName:salesperson_email,cmAccountApiName:user_given_expense_id,cmStatusApiName:salesperson_status}};
page = 0;
for each index in loop
{
page = page + 1;
// Get Custom Module Details
customModule_response = invokeurl
[
url :(domainUrl + '/') + customModuleApiName + '?page=' + page + '&usestate=false&organization_id=' + organizationID
type :GET
connection:"salesperson_commission_books"
];
customModule_records = customModule_response.get("module_records");
for each records in customModule_records
{
if(records.get(cmEmailApiName) == salesperson_email)
{
is_present = is_present + 1;
customModule_id = records.get('module_record_id');
salesperson_commission_type = records.get(cmTypeApiName);
salesperson_commission_rate = records.get(cmRateApiName);
salesperson_expense_id = records.get(cmAccountApiName);
body = {"JSONString":{cmNameApiName:salesperson_name,cmTypeApiName:user_given_commission_type,cmRateApiName:user_given_commission,cmEmailApiName:salesperson_email,cmAccountApiName:user_given_expense_id,cmStatusApiName:salesperson_status}};
updatecustomModuleFields = invokeurl
[
url :domainUrl + "/" + customModuleApiName + "/" + customModule_id + "?organization_id=" + organizationID
type :PUT
parameters:body
connection:"salesperson_commission_books"
];
}
}
if(is_present == 0)
{
createcustomModuleFields = invokeurl
[
url :domainUrl + "/" + customModuleApiName + "?organization_id=" + organizationID
type :POST
parameters:body
connection:"salesperson_commission_books"
];
info createcustomModuleFields;
}
}
}
- Click Save or Save and Execute.
With this, you have build all the components necessary for the Salesperson extension. You can test your extension in the Zoho Books Sandbox environment and publish your extension in Zoho Marketplace.