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

ZohoBooks 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:
copyraw
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.// ... 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"); }
- // ... 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");
- }
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:
copyraw
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_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
- 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
copyraw
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));
- 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:
copyraw
So in practice, and for the sake of your finance team's sanity, // ------------------------- // 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
- // -------------------------
- // 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
copyraw
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)); } }
- 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));
- }
- }

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.
- Zoho Books API Documentation: Create Invoice: adjustment_description "Customize the adjustment description. E.g. Rounding off."
Category: Zoho :: Article: 901
Add comment