ZohoAnalytics & ZohoBooks: Custom Related List from Analytics
- Category: Zoho Books
- Hits: 40571
A quick article to document 2 features in deluge code: a custom related list in ZohoBooks, and a reminder on how to read a table from ZohoAnalytics.
Why?
My use-case here is that we have a client who uses their purchase orders and sales orders as part of a logistics solution where items are purchased from a supplier, sent to another supplier for grading, and then sent on to the end user/customer.
A custom field against the item record has been added which is a lookup to the Sales Order module. This means that on a purchase order, and per line item, the staff can specify which Sales Order the item relates to.
How?
At time of print, adding the lookup to the line item will automatically display a related list on the Sales Order but does not associate any records... not sure why this is. So we have a workaround, by sending all the data including the custom fields per line items to ZohoAnalytics; and in ZohoAnalytics importing a table based on a query which joins the sales orders and purchase order items by the custom lookup field.
Zoho Books / Inventory: Get Item Rate from a Price Book/List
- Category: Zoho Books
- Hits: 35141
A quick article on how to get the pricebook entry using Zoho Deluge for a specific product in your ZohoBooks or ZohoInventory instance.
Why?
This took me the best part of an hour to determine by going through forum posts from 7 years to 2 years ago. The following will work in May 2024 following the API domain change.
The use-case is that the customer wants the item/product rate taken from the item record, if a pricebook is specified and the item exists within it, then it takes the rate from the pricelist. Note that when I refer to pricebook, this is also referred to as the pricelist... and vice-versa.
And in this use-case, my client has added an incredible number of price books/ price lists and then has in excess of 20k items. Previously, the function that would need to get the price book rate post saving a record in Zoho Deluge would fail because it was making too many function statements.
How?
I have this code triggered in a workflow when the sales order is created to do some further calculations based on a surcharge rate held in a custom module. The custom module could only be created in Zoho Books at time of print but Zoho Inventory is where our workflow and code is sitting right now.
ZohoBooks: Stripe Terminal Integration
- Category: Zoho Books
- Hits: 43511
A quick article on some code added to a button in ZohoBooks off the invoice module to initiate your Stripe terminal to take a payment.
Why?
Just to make it easy on the staff at a counter or on the phone. They bring up the invoie in ZohoBooks, click on the button, and the Stripe terminal will ask for the amount on the invoice.
Well almost. We've gone the extra step in that we added a custom field that can override the full balance, to allow partial payments such as a deposit or instalment.
How?
I won't go in to how to create a button in ZohoBooks but you simply add it to the invoice and then when it prompts for some code, you give it the snippet below.
The Magic
You would create a button for each terminal
/* *******************************************************************************
Function: Map Take_Payment( Map invoice, Map organization, Map user)
Label: Take Payment
Trigger: On button click
Purpose: Preps stripe terminal to take payment for balance of invoice.
Inputs: invoice
Outputs: -
Date Created: 2023-02-24
- Initial release
- Reads Books Invoice and sends the amount to the reader for a payment atempt
Date Modified: 2023-02-24 (Joel Lipman)
- If custom field "Amount To Be Taken" is not greater than zero, then defaults to balance due of invoice.
More Information:
TEST PAYMENT SCENARIOS WITH PHYSICAL TEST CARD
// Send in as payment endings: eg. $100.00 == payment approved
00 Payment Approved
01 Payment Declined // call issuer code
05 Payment Declined Generic
55 Payment Declined Incorrect Pin
65 Payment Declined withdrawal_count_limit_exceeded
75 Pin try exceeded
******************************************************************************* */
v_BooksOrgID = organization.get("organization_id");
//
// some Stripe variables (add your own here)
v_StripeTerminalID = "tmr_ABCDEFGabcdefg";
v_StripeCustomerKey = "sk_live_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ";
V_StripeLocationsEndpoint = " https://api.stripe.com/v1/terminal/locations";
//
// set Stripe header
m_Headers = Map();
m_Headers.put("Authorization","Bearer " + v_StripeCustomerKey);
//
// default to balance due on this Invoice
v_AmountToPay = ifnull(invoice.get("balance"),0);
//
// Get custom Amount to Pay from Invoice
l_CustomFields = invoice.get("custom_fields");
if(l_CustomFields.size() > 0)
{
for each m_CustomField in l_CustomFields
{
if(m_CustomField.get("label") == "Amount To Be Taken")
{
if(m_CustomField.get("value") > 0)
{
v_AmountToPay = m_CustomField.get("value");
}
}
}
}
//
// format to Stripe amount
v_AmountToPay = v_AmountToPay.truncate(2);
v_StripeAmount = v_AmountToPay * 100;
v_StripeAmount = v_StripeAmount.floor();
v_StripeAmount = v_StripeAmount.toNumber();
info v_StripeAmount;
//
// Create payment intent in Stripe
v_PaymentIntentEndpoint = "https://api.stripe.com/v1/payment_intents";
m_Params = Map();
m_Params.put("amount",v_StripeAmount);
m_Params.put("currency","gbp");
//m_Params.put("automatic_payment_methods[enabled]", false);
m_Params.put("payment_method_types[]","card_present");
m_Params.put("capture_method","manual");
v_DescriptionString = "IN: " + invoice.get("invoice_number") + " ID: " + invoice.get("invoice_id");
m_Params.put("description",v_DescriptionString);
// Later Add Code to "customer_id": "123456700000001234567", get customer id then Email
v_BooksCustomerID = invoice.get("customer_id");
r_CustomerDetails = zoho.books.getRecordsByID("contacts",v_BooksOrgID,v_BooksCustomerID,"zbooks");
v_CustomerCheckCode = r_CustomerDetails.get("code");
if(v_CustomerCheckCode == 0)
{
m_ContactDetails = r_CustomerDetails.get("contact");
if(m_ContactDetails != null)
{
m_Params.put("receipt_email",m_ContactDetails.get("email"));
}
}
r_CreatePaymentIntent = invokeurl
[
url :v_PaymentIntentEndpoint
type :POST
parameters:m_Params
headers:m_Headers
];
info "Payment Intent Create";
info r_CreatePaymentIntent;
//
v_CheckObject = ifnull(r_CreatePaymentIntent.get("object"),"-");
v_CheckAmount = ifnull(r_CreatePaymentIntent.get("amount"),"-");
//
// Process Payment Intent
if(v_CheckObject == "payment_intent")
{
v_PaymentIntentID = r_CreatePaymentIntent.get("id");
info "Payment Intent Created Successfuly!!! ID: ";
info v_PaymentIntentID;
//
// Hand Off Payment Intent to Reader
v_ReaderHandOffEndpoint = "https://api.stripe.com/v1/terminal/readers/" + v_StripeTerminalID + "/process_payment_intent";
m_PaymentHandOffParams = Map();
m_PaymentHandOffParams.put("payment_intent",v_PaymentIntentID);
r_ReaderPaymentHandOff = invokeurl
[
url :v_ReaderHandOffEndpoint
type :POST
parameters:m_PaymentHandOffParams
headers:m_Headers
];
info r_ReaderPaymentHandOff;
}
return r_ReaderPaymentHandOff;
The incoming webhook
You now need to receive the Stripe webhook when it comes back into ZohoBooks to record it against the invoice. Note how we added the invoice reference and Zoho ID in the description of the payment capture in our previous bit of code:
/* *******************************************************************************
Function: Map stripe_terminal_payment( Map invoice, Map organization, Map user)
Label: stripe_terminal_payment
Trigger: Incoming Webhook
Purpose: Listens for stripe terminal payments.
OAuth URL: https://www.zohoapis.com/books/v3/settings/incomingwebhooks/iw_stripe_terminal_payment/execute?auth_type=oauth
Inputs: invoice
Outputs: -
Date Created: 2023-03-23
- Initial release
- been successfully proccessed by the physical reader
Date Modified: 2024-05-21 (Joel Lipman)
- Revamp of code as per best practices
- Correct error: Check and update the code in line 74 as there is a Exception : Value is empty and 'get' function cannot be applied
More Information:
Navigate to payments, then find pending webhook response event, terminal.reader.action_succeeded, then view event details
******************************************************************************* */
//
// initialize
m_Blank = Map();
m_Response = Map();
v_BooksOrgID = organization.get("organization_id");
//
// Stripe API Key (add your own here)
v_StripeCustomerKey = "sk_live_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ";
//
// set Stripe header
m_Headers = Map();
m_Headers.put("Authorization","Bearer " + v_StripeCustomerKey);
//
// capture response webhook
m_Webhook = Map();
m_Webhook.put("data",body.get("data"));
m_Webhook.put("type",body.get("type"));
//
// Check Type
m_Data = ifnull(m_Webhook.get("data"),m_Blank);
v_Type = ifnull(m_Webhook.get("type"),"-");
if(v_Type == "terminal.reader.action_succeeded")
{
// Get Payment Intent that needs to be Captured
m_Object = m_Data.get("object");
m_Action = m_Object.get("action");
m_ProcessPaymentIntent = m_Action.get("process_payment_intent");
v_PaymentIntentID = m_ProcessPaymentIntent.get("payment_intent");
//
// Retrieve details on the Payment Intent
v_PaymentIntentEndpoint = "https://api.stripe.com/v1/payment_intents/" + v_PaymentIntentID;
r_PaymentIntentDetails = invokeurl
[
url :v_PaymentIntentEndpoint
type :GET
headers:m_Headers
];
//
// get amount (used to capture payment intent)
v_Amount = ifnull(r_PaymentIntentDetails.get("amount"),0);
v_AmountReceived = ifnull(r_PaymentIntentDetails.get("amount_received"),0);
v_AmountCapturable = ifnull(r_PaymentIntentDetails.get("amount_capturable"),0);
v_CaptureIntentEndpoint = "https://api.stripe.com/v1/payment_intents/" + v_PaymentIntentID + "/capture";
m_CaptureParams = Map();
m_CaptureParams.put("amount_to_capture",v_AmountCapturable);
r_CapturePayment = invokeurl
[
url :v_CaptureIntentEndpoint
type :POST
parameters:m_CaptureParams
headers:m_Headers
];
//
// get card details (we need to store last 4 digits)
v_StripeReference = "";
v_Last4Digits = "";
v_ZB_InvoiceID = 0;
m_Charges = ifnull(r_PaymentIntentDetails.get("charges"),m_Blank);
l_Data = ifnull(m_Charges.get("data"),{});
for each m_Data in l_Data
{
if(m_Data.get("id") != null)
{
v_StripeReference = m_Data.get("id");
}
if(m_Data.get("payment_method_details") != null)
{
m_CardPresent = ifnull(m_Data.get("payment_method_details").get("card_present"),m_Blank);
v_Last4Digits = ifnull(m_CardPresent.get("last4"),"");
}
if(m_Data.get("description") != null)
{
v_ChargeDescription = ifnull(m_Data.get("description"),"");
v_ZB_InvoiceID = v_ChargeDescription.getSuffix("ID: ");
v_ZB_InvoiceID = if(isNumber(v_ZB_InvoiceID),v_ZB_InvoiceID,0).toLong();
}
}
//
// create payment record
if(v_ZB_InvoiceID != 0)
{
//
// get ZohoBooks nominal account for Stripe
v_NominalAccountID = "";
r_ChartOfAccounts = invokeurl
[
url :"https://www.zohoapis.com/books/v3/chartofaccounts?organization_id=" + v_BooksOrgID
type :GET
connection:"zbooks"
];
if(r_ChartOfAccounts.get("chartofaccounts") != null)
{
for each m_NomAccount in r_ChartOfAccounts.get("chartofaccounts")
{
if(m_NomAccount.get("account_name").equalsIgnoreCase("Stripe Clearing"))
{
v_NominalAccountID = m_NomAccount.get("account_id");
}
}
}
//
// retrieve invoice details from ZohoBooks
r_InvoiceDetails = zoho.books.getRecordsByID("invoices",v_BooksOrgID,v_ZB_InvoiceID,"zbooks");
m_Invoice = ifnull(r_InvoiceDetails.get("invoice"),m_Blank);
if(m_Invoice.get("customer_id") != null)
{
m_CreatePayment = Map();
m_CreatePayment.put("customer_id",m_Invoice.get("customer_id"));
m_CreatePayment.put("payment_mode","In Person - Card");
m_CreatePayment.put("amount",v_AmountReceived / 100);
m_CreatePayment.put("date",zoho.currentdate.toString("yyyy-MM-dd"));
//
l_Invoices = List();
m_ThisInvoice = Map();
m_ThisInvoice.put("invoice_id",v_ZB_InvoiceID.toString());
m_ThisInvoice.put("amount_applied",v_AmountReceived / 100);
l_Invoices.add(m_ThisInvoice);
m_CreatePayment.put("invoices",l_Invoices);
m_CreatePayment.put("invoice_id",v_ZB_InvoiceID.toString());
m_CreatePayment.put("amount_applied",v_AmountReceived / 100);
//
v_PaymentRef = if(v_StripeReference=="", m_Invoice.get("invoice_number"), v_StripeReference);
m_CreatePayment.put("reference_number",v_PaymentRef);
m_CreatePayment.put("account_id",v_NominalAccountID);
//info m_CreatePayment;
//
r_CreatePayment = zoho.books.createRecord("customerpayments",v_BooksOrgID,m_CreatePayment,"zbooks");
//info r_CreatePayment;
if(r_CreatePayment.get("message") != null)
{
if(r_CreatePayment.get("message").contains("payment has been created"))
{
//
// update the invoice (request by client to store last 4 digits on invoice)
l_CustomFields = List();
m_CustomField = Map();
m_CustomField.put("api_name","cf_last_4_digits");
m_CustomField.put("value",v_Last4Digits);
l_CustomFields.add(m_CustomField);
m_UpdateInvoice = Map();
m_UpdateInvoice.put("custom_fields",l_CustomFields);
r_UpdateInvoice = zoho.books.updateRecord("Invoices",v_BooksOrgID,v_ZB_InvoiceID,m_UpdateInvoice,"zbooks");
}
}
}
}
}
m_Response.put("message",r_CreatePayment.get("message"));
m_Response.put("code",0);
return m_Response;
Source(s):
- Stripe Dashboard - Developers - API Keys
- Stripe Docs - Decline codes - Learn about decline codes and how to resolve them when a charge fails.
Zoho Books: Error 7008: There are no contact persons associated with this Invoice
- Category: Zoho Books
- Hits: 29515
A quick article on how to get around this error.
Why?
The use-case here is that I am using the send invoice API and I kept getting this error despite including the customer_id and there was a primary contact on the customer record.
This is from within ZohoCreator, requesting for an invoice in ZohoBooks to be sent.
How?
So the key here is that there were no contact persons on the customer record. Instead, when building the invoice, this had to be added (don't ever remember having to do this) as contact_ids (array/list). Instead the below script will show you how to use this API having a few email addresses as the recipients:
Zoho Books: System Values Maps
- Category: Zoho Books
- Hits: 26366
A collection of code snippets I seem to be regularly using to generate a dynamic map of system values held in a ZohoBooks instance.
Why?
Rather than hard-coding and having a ton of if..then statements, I can feed these maps a textual value and it returns the ID to use. Some of these can be found elsewhere in my site but I'm putting all of them here just for quick reference.
How?
Note that for the below, I recently updated this article (2024-05-21) due to the API domain name change from https://books.zoho.com to https://www.zohoapis.com/books and changed the Top Level Domain (or Zoho DataCenter) from EU to COM as I was using this for a client on the US datacenter.
Zoho Books: Generate Bank Text File for Download
- Category: Zoho Books
- Hits: 33047
This is an article to document how we created a button off a Bill record in ZohoBooks which generates a text file to be sent to a bank and downloads it to the staff member's workstation.
Why?
Why is this a challenge? The file contains bank details and should not be stored anywhere. Generating a text or CSV file and then having it emailed to anyone is an easy task but here we cannot have that file sat on someone's mailbox. It must be downloaded (as in browser downloads it to the computer immediately) and then removed from wherever it was downloaded from... All within ZohoBooks without using any other apps or software.
How?
Here's how to use it:
- Staff member clicks on button
- File downloads to their workstation
- File is generated
- FIle is attached to the last record of the source data
- File identifying number is returned from being attached
- File is opened in a new tab/window to trigger downloading.
- File is deleted from attachments based on its identifying number (allows other attachments to remain intact)
ZohoBooks: Error Code 15: Ensure Billing Address has less than 100 characters
- Category: Zoho Books
- Hits: 39971
Another article on something I learned today despite never running into this issue before; but sending a billing/shipping address included in a request to create or update an estimate in ZohoBooks will fail...
Why?
I have a function to push a ZohoCRM quote to a ZohoBooks estimate and a client asked that the address on the CRM record 'pulls through'. Sending the address in the same request however gives me the following error:
{ "code": 15, "message": "Please ensure that the billing_address has less than 100 characters." }
How?
So the quick answer is 2 separate API calls after you have sent the code to either create or update the estimate in ZohoBooks. You will need the returned estimate ID and I'm not 100% sure the attention/phone number goes along as my client didn't include these in her estimate template... But I'm sending them anyway.
Zoho Books: Get Invoice Payment Terms via API
- Category: Zoho Books
- Hits: 24832
A quick article on getting the payment terms in Zoho Books along with their IDs.
Why?
I often need to send through an automatic payment term on the creation of an invoice but lots of my clients set their due dates differently.
How?
The following snippet of code will query the metadata api in Zoho Books and return a JSON of what the payment terms are.

