What?
A frustrated article about an issue that took me a fair few hours to resolve. So I'm putting it here as I thought I covered this previously but couldn't find it on my website. This was previously titled something along the lines of truncating to 2 decimals but finance is so much more complicated.

Why?
Playing around with VAT / Tax, inclusive /exclusive, but in this case it is the rate that gets rounded. The numbers at the end still need to match what's in CRM and with ZohoBooks rounding differently can make cent/penny errors into 100s of dollar/pound errors.

Zoho CRM has a fun way of rounding which differs to Zoho Books so when pushing an invoice, there's a strong chance of being a cent/penny off. Let's take the following example of an item with a list price of 12345.545:

ZohoCRM Line Item
ZohoCRM to ZohoBooks API: Truncate number to 2 decimal points without Rounding - Zoho CRM Line Item

ZohoBooks Line Item
ZohoCRM to ZohoBooks API: Truncate number to 2 decimal points without Rounding - Zoho Books Line Item

How?

Here's the immediate solution I have applied now until Zoho enable an option to round via API with a 10th of a penny/cent or less.

After creating or updating the invoice, add in the response line something like the following:
// ... UPDATE INVOICE
		r_UpdateBooksInvoice = zoho.books.updateRecord("invoices",v_BooksOrgID,v_ZohoBooksID,m_InvoiceData,"zbooks");
		if(!isNull(r_UpdateBooksInvoice.get("invoice")))
		{
			if(!isNull(r_UpdateBooksInvoice.get("invoice").get("invoice_id")))
			{
				//
				v_OutputResponse = r_UpdateBooksInvoice.get("invoice").get("invoice_number") + ": " + r_UpdateBooksInvoice.get("message");
				//
				// adjustments
				if(r_UpdateBooksInvoice.get("invoice").get("total") != v_Crm_Invoice_GrandTotal)
				{
					v_TotalAdjustments = v_Crm_Invoice_GrandTotal - r_UpdateBooksInvoice.get("invoice").get("total");
					m_ZB_Update = Map();
					//m_ZB_Update.put("roundoff_value",v_TotalAdjustments);
					m_ZB_Update.put("adjustment",v_TotalAdjustments);
					m_ZB_Update.put("adjustment_description","Rounding Off");
					r_ZB_Update = zoho.books.updateRecord("invoices", v_OrgId, r_UpdateBooksInvoice.get("invoice").get("invoice_id"), m_ZB_Update, "zbooks");
				}
		}
		else if(!isNull(r_UpdateBooksInvoice.get("message")))
		{
			v_OutputResponse = "FAILED: " + r_UpdateBooksInvoice.get("message");
		}
It's a cop-out I know, but it beats the accounts staff having to enter the adjustment each time. The create invoice process would be treated the same.



The below are all the various methods I tried to resolve this but the truth is that I do not have a clue how ZohoBooks comes up with its rounding figures. They're wrong in any case as they don't match the ZohoCRM... or both are just not matching... either way, your client will likely only accept the previous solution.

I wanted to use a regex rather than treat the number as a string and truncate it that way. Given the possible options of numbers given, it has to work with every number so here's both in Zoho Deluge Tryout:
v_Rate = 12345.545;
info "Rate: " + v_Rate;

// built-in rounding
v_Rounded = v_Rate.round(2);
info "a) " + v_Rounded;
// Yields: 12345.55

// Regexed
v_RegexedNumber = v_Rate.toString().replaceAll("^(\\d+\\.\\d{2})\\d*$", "$1");
info "b) " + v_RegexedNumber; 
// Yields: 12345.54

// 2 characters after period
v_NotRegexedNumber = v_Rate.toString().subString(0,v_Rate.toString().indexOf(".")+3);
info "c) " + v_NotRegexedNumber;
// Yields: 12345.54

// -------------------------

v_RateUp = 12345.546;
info "Rate Up: " + v_RateUp;

// built-in rounding
v_Rounded = v_RateUp.round(2);
info "d) " + v_Rounded;
// Yields: 12345.55

// Regexed
v_RegexedNumber = v_RateUp.toString().replaceAll("^(\\d+\\.\\d{2})\\d*$", "$1");
info "e) " + v_RegexedNumber; 
// Yields: 12345.54

// 2 characters after period
v_NotRegexedNumber = v_RateUp.toString().subString(0,v_Rate.toString().indexOf(".")+3);
info "f) " + v_NotRegexedNumber;
// Yields: 12345.54

// -------------------------

v_RateDown = 12345.544;
info "Rate Down: " + v_RateDown;

// built-in rounding
v_Rounded = v_RateDown.round(2);
info "g) " + v_Rounded;
// Yields: 12345.54

// Regexed
v_RegexedNumber = v_RateDown.toString().replaceAll("^(\\d+\\.\\d{2})\\d*$", "$1");
info "h) " + v_RegexedNumber; 
// Yields: 12345.54

// 2 characters after period
v_NotRegexedNumber = v_RateDown.toString().subString(0,v_Rate.toString().indexOf(".")+3);
info "i) " + v_NotRegexedNumber;
// Yields: 12345.54
Of course ZohoCRM functions have to treat Regex's slightly differently in ZohoCRM, so if we went with the Regex to truncate, we would need to get rid of the excess escaping backslashes:
v_ListPrice = ifnull(m_ZCrm_LineItem.get("List_Price"),0);
m_ZBooks_LineItem.put("rate",v_ListPrice.toString().replaceAll("^(\d+\.\d{2})\d*$", "$1", false));

Works a treat at truncating but still does NOT match how ZohoBooks does it. ZohoBooks rounds the .545 to .55 and .544 to .54... so let's abandon the truncating and simply deduct 1/10th of a cent/penny:
// -------------------------
// Make .545 = .54 and .546 = .55

// CRM rounding
v_RateCrm = 12345.545;
info "Rate CRM 545: " + v_RateCrm;
v_Rounded = (v_RateCrm - 0.001).round(2);
info "j) " + v_Rounded;
// Yields: 12345.54

// CRM rounding
v_RateCrm = 12345.544;
info "Rate CRM 544: " + v_RateCrm;
v_Rounded = (v_RateCrm - 0.001).round(2);
info "k) " + v_Rounded;
// Yields: 12345.54

// CRM rounding
v_RateCrm = 12345.546;
info "Rate CRM 546: " + v_RateCrm;
v_Rounded = (v_RateCrm - 0.001).round(2);
info "l) " + v_Rounded;
// Yields: 12345.55

// CRM rounding
v_RateCrm = 12345.555;
info "Rate CRM 555: " + v_RateCrm;
v_Rounded = (v_RateCrm - 0.001).round(2);
info "m) " + v_Rounded;
// Yields: 12345.55
So in practice, and for the sake of your finance team's sanity, the most acceptable solution is to deduct the 10th of a cent/penny and as per the documentation for adjustments, we'll run a check after the invoice is created/updated and check that the total matches the CRM grand total and then add the difference as an adjustment with the description "Rounding Off":
v_TotalAdjustments = 0.00;
for each m_ZCrm_LineItem in l_ZCrm_LineItems
{
	v_ListPrice = ifnull(m_ZCrm_LineItem.get("List_Price"),0);
	if(v_ListPrice.toString().contains("."))
	{
		if(v_ListPrice.toString().toList(".").get(1).subString(2,3) == "5")
		{
			v_TotalAdjustments = v_TotalAdjustments + 0.005;
		}
	}
	m_ZB_LineItem.put("rate",v_ListPrice);
	l_ZB_LineItems.add(m_ZB_LineItem);
}
m_ZB_InvoiceData.put("line_items",l_ZB_LineItems);
if(v_TotalAdjustments > 0)
{
	m_ZB_InvoiceData.put("adjustment",v_TotalAdjustments * (-1));
	m_ZB_InvoiceData.put("adjustment_description","Rounding Off");
}
/*
// don't round if no more than 2 decimals
if(v_ListPrice.toString().contains("."))
{
	v_ListPrice = if(v_ListPrice.toString().toList(".").get(1).length()>2, (v_ListPrice - 0.001).round(2), v_ListPrice);
}
*/

// OR


	//
	if(v_ListPrice.toString().contains("."))
	{
		if(v_ListPrice.toString().getSuffix(".").length() > 2)
		{
			v_ThisAdjustment = "0.00" + v_ListPrice.toString().toList(".").get(1).subString(2);
			if(v_ListPrice.toString().startsWith("-"))
			{
				v_TotalAdjustments = v_TotalAdjustments - v_ThisAdjustment.toDecimal();
			}
			else 
            {
				v_TotalAdjustments = v_TotalAdjustments + v_ThisAdjustment.toDecimal();
            }
			info "Adjust: " + v_ThisAdjustment;
			//v_ListPrice = if(v_ListPrice.toString().toList(".").get(1).length() > 2,(v_ListPrice - 0.001).round(2),v_ListPrice.round(2));
		}
	}
Ta daa!
ZohoCRM to ZohoBooks API: Truncate number to 2 decimal points without Rounding - Zoho Books Line Item After Fix

Source(s):
I would mention a few community forum pages I went through which showed me how not to do it, but as none were the solution, I'm not sure putting them here would make this easier for you.