Customer Relationship Management Systems

ZohoAnalytics & ZohoBooks: Custom Related List from Analytics

What?
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.

Humanoid robot looking up some details from a book

What?
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

What?
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):

ZohoBooks: Error 7008: There are no contact persons associated with this Invoice

What?
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

What?
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

What?
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
Here's what's happening in the background:
  • 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

What?
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

What?
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.