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.
Note(s)
- The following code accesses APIs on the EU datacenter.
- I have a connection called "zbooks" which has the necessary scope(s) to create and update an estimate as well as read settings and contacts.
- I have a connection called "zcrm" which has the necessary scope(s) to read from and write to a CRM quote record.
The code:
This is the code used in a workflow triggered within ZohoCRM when a Quote is created or modified:
// // initialize v_BooksOrgID = 123456789; v_BooksCustomerID = 0; m_CreateEstimate = Map(); l_CrmBillingAddress = {"Billing_Street","Billing_Street_2","Billing_City","Billing_State","Billing_Code","Billing_Country"}; l_CrmShippingAddress = {"Shipping_Street","Shipping_Street_2","Shipping_City","Shipping_State","Shipping_Code","Shipping_Country"}; l_BooksAddress = {"address","street2","city","state","zip","country"}; // // evaluate r_QuoteDetails = zoho.crm.getRecordById("Quotes",123456789012345678); // // push to ZohoBooks estimate if(true) { // // ---------------------- GET ZOHO BOOKS TAX IDs ---------------------- m_Taxes = Map(); r_Taxes = invokeurl [ url :"https://books.zoho.eu/api/v3/settings/taxes?organization_id=" + v_BooksOrgID type :GET connection:"zbooks" ]; for each r_Tax in r_Taxes.get("taxes") { m_Taxes.put(r_Tax.get("tax_percentage").toString(),r_Tax.get("tax_id")); } // // ---------------------- DETERMINE ZOHOBOOKS CUSTOMER ID ---------------------- if(!isnull(r_QuoteDetails.get("Account_Name"))) { v_AccountID = r_QuoteDetails.get("Account_Name").get("id"); r_SearchResults = zoho.books.getRecords("Contacts",v_BooksOrgID,"zcrm_account_id=" + v_AccountID,"zbooks"); if(!isnull(r_SearchResults.get("contacts"))) { // tried sync contacts from Account record for each r_Result in r_SearchResults.get("contacts") { if(!isnull(r_Result.get("contact_id"))) { v_BooksCustomerID = r_Result.get("contact_id").toLong(); break; } } } } if(v_BooksCustomerID != 0) { m_CreateEstimate.put("customer_id",v_BooksCustomerID); } // // ---------------------- QUOTE CONTACT NAME/PHONE ---------------------- if(!isnull(r_QuoteDetails.get("Contact_Name"))) { v_ContactID = r_QuoteDetails.get("Contact_Name").get("id"); v_ContactName = r_QuoteDetails.get("Contact_Name").get("name"); r_ContactDetails = zoho.crm.getRecordById("Contacts",v_ContactID); v_ContactNumber = ifnull(r_ContactDetails.get("Mobile"),r_ContactDetails.get("Phone")); } // // ---------------------- QUOTE DATES ---------------------- m_CreateEstimate.put("date",r_QuoteDetails.get("Created_Time").subString(0,10)); if(!isnull(r_QuoteDetails.get("Valid_Till"))) { m_CreateEstimate.put("expiry_date",r_QuoteDetails.get("Valid_Till").subString(0,10)); } // // ---------------------- QUOTE LINKED POTENTIAL/OPPORTUNITY ---------------------- if(!isnull(r_QuoteDetails.get("Deal_Name"))) { m_CreateEstimate.put("zcrm_potential_id",r_QuoteDetails.get("Deal_Name").get("id").toLong()); } // // ---------------------- QUOTE LINE ITEMS ---------------------- // line items get via api v2.1 or greater l_BooksLineItems = List(); r_CrmProductLineItems = invokeurl [ url :"https://www.zohoapis.eu/crm/v2.1/Quotes/" + p_QuoteID type :GET parameters:m_ApprovedConverted connection:"zcrm" ]; // should be checking here that data contains any rows but I'm going on the basis that this quote exists in CRM and at least 1 line item is mandatory l_CrmProductLineItems = r_CrmProductLineItems.get("data").get(0).get("Quoted_Items"); for each r_CrmLineItem in l_CrmProductLineItems { m_BooksLineItem = Map(); v_BooksItemID = 0; v_CrmProductID = r_CrmLineItem.get("Product_Name").get("id"); v_CrmProductName = r_CrmLineItem.get("Product_Name").get("name"); // if the product doesn't exist in Books (sync hasn't happened yet), then you'll need code here to create the item if not found r_SearchResults2 = zoho.books.getRecords("items",v_BooksOrgID,"zcrm_product_id=" + v_CrmProductID,"zbooks"); if(!isnull(r_SearchResults2.get("items"))) { for each r_Result2 in r_SearchResults2.get("items") { if(r_Result2.get("item_name") == v_CrmProductName) { m_BooksLineItem.put("item_id",r_Result2.get("item_id").toLong()); m_BooksLineItem.put("name",v_CrmProductName); m_BooksLineItem.put("description",r_CrmLineItem.get("Description")); m_BooksLineItem.put("quantity",r_CrmLineItem.get("Quantity")); m_BooksLineItem.put("rate",r_CrmLineItem.get("List_Price").toDecimal().round(2)); m_BooksLineItem.put("discount",ifnull(r_CrmLineItem.get("Discount_Percent"),0) + "%"); m_BooksLineItem.put("item_total",r_CrmLineItem.get("Net_Total").toDecimal().round(2)); v_CrmVatPercent = r_CrmLineItem.get("VAT2").replaceAll("[^0-9]",""); v_BooksTaxID = ifnull(m_Taxes.get(v_CrmVatPercent),m_Taxes.get("20")); m_BooksLineItem.put("tax_id",v_BooksTaxID); m_BooksLineItem.put("tax_percentage",v_CrmVatPercent); break; } } } l_BooksLineItems.add(m_BooksLineItem); } m_CreateEstimate.put("line_items",l_BooksLineItems); // // ---------------------- QUOTE SHIPPING CHARGE ---------------------- if(r_QuoteDetails.get("Apply_VAT_to_Shinpping_Charges") && v_ShippingCharges > 0) { m_CreateEstimate.put("shipping_charge_tax_id",m_Taxes.get("20")); } else { m_CreateEstimate.put("shipping_charge_tax_id",""); } m_CreateEstimate.put("shipping_charge",v_ShippingCharges); // // ---------------------- ESTIMATE CREATE OR UPDATE ---------------------- v_BooksEstimateID = 0; if(!isnull(r_QuoteDetails.get("Books_Ref"))) { r_SearchResults3 = zoho.books.getRecords("estimates",v_BooksOrgID,"estimate_number=" + r_QuoteDetails.get("Books_Ref"),"zbooks"); if(!isnull(r_SearchResults3.get("estimates"))) { for each r_BooksEstimate in r_SearchResults3.get("estimates") { if(!isnull(r_BooksEstimate.get("estimate_id"))) { v_BooksEstimateID = r_BooksEstimate.get("estimate_id").toLong(); } } } } if(v_BooksEstimateID == 0) { r_BooksEstimate = zoho.books.createRecord("estimates",v_BooksOrgID,m_CreateEstimate,"zbooks"); } else { r_BooksEstimate = zoho.books.updateRecord("estimates",v_BooksOrgID,v_BooksEstimateID,m_CreateEstimate,"zbooks"); } info "ZohoBooks Estimate REQUEST: "; info m_CreateEstimate; info "ZohoBooks Estimate RESPONSE: "; info r_BooksEstimate; // // ---------------------- UPDATE THE CRM QUOTE WITH ZOHOBOOKS ESTIMATE REF ---------------------- m_UpdateCrmQuote = Map(); if(!isnull(r_BooksEstimate.get("estimate"))) { m_UpdateCrmQuote.put("Books_Ref",r_BooksEstimate.get("estimate").get("estimate_number")); v_BooksEstimateID = r_BooksEstimate.get("estimate").get("estimate_id").toLong(); } if(m_UpdateCrmQuote.size() > 0) { r_UpdateCrmQuote = zoho.crm.updateRecord("Quotes",p_QuoteID,m_UpdateCrmQuote); } // // ---------------------- ADDRESS ON QUOTE/ESTIMATE ---------------------- // if quote address is not blank, then push this to estimate if(v_BooksEstimateID != 0) { if(!isNull(r_QuoteDetails.get("Billing_Street"))) { m_BooksBillingAddress = Map(); for each index v_AddressIndex1 in l_CrmBillingAddress { m_BooksBillingAddress.put(l_BooksAddress.get(v_AddressIndex1),r_QuoteDetails.get(l_CrmBillingAddress.get(v_AddressIndex1))); } v_EndpointBilling = "https://www.zohoapis.eu/books/v3/estimates/" + v_BooksEstimateID + "/address/billing?organization_id=" + v_BooksOrgID; r_EstimateBillingAddress = invokeurl [ url :v_EndpointBilling type :PUT parameters:m_BooksBillingAddress.toString() connection:"zbooks" ]; info "ZohoBooks Estimate Billing Address RESPONSE: "; info r_EstimateBillingAddress; } if(!isNull(r_QuoteDetails.get("Shipping_Street"))) { m_BooksShippingAddress = Map(); for each index v_AddressIndex2 in l_CrmShippingAddress { m_BooksShippingAddress.put(l_BooksAddress.get(v_AddressIndex2),r_QuoteDetails.get(l_CrmShippingAddress.get(v_AddressIndex2))); } m_BooksShippingAddress.put("attention",v_ContactName); if(!isNull(v_ContactNumber)) { m_BooksShippingAddress.put("phone",v_ContactNumber); } v_EndpointShipping = "https://www.zohoapis.eu/books/v3/estimates/" + v_BooksEstimateID + "/address/shipping?organization_id=" + v_BooksOrgID; r_EstimateShippingAddress = invokeurl [ url :v_EndpointShipping type :PUT parameters:m_BooksShippingAddress.toString() connection:"zbooks" ]; info "ZohoBooks Estimate Shipping Address RESPONSE: "; info r_EstimateShippingAddress; } } }
- //
- // initialize
- v_BooksOrgID = 123456789;
- v_BooksCustomerID = 0;
- m_CreateEstimate = Map();
- l_CrmBillingAddress = {"Billing_Street","Billing_Street_2","Billing_City","Billing_State","Billing_Code","Billing_Country"};
- l_CrmShippingAddress = {"Shipping_Street","Shipping_Street_2","Shipping_City","Shipping_State","Shipping_Code","Shipping_Country"};
- l_BooksAddress = {"address","street2","city","state","zip","country"};
- //
- // evaluate
- r_QuoteDetails = zoho.crm.getRecordById("Quotes",123456789012345678);
- //
- // push to ZohoBooks estimate
- if(true)
- {
- //
- // ---------------------- GET ZOHO BOOKS TAX IDs ----------------------
- m_Taxes = Map();
- r_Taxes = invokeUrl
- [
- url :"https://books.zoho.eu/api/v3/settings/taxes?organization_id=" + v_BooksOrgID
- type :GET
- connection:"zbooks"
- ];
- for each r_Tax in r_Taxes.get("taxes")
- {
- m_Taxes.put(r_Tax.get("tax_percentage").toString(),r_Tax.get("tax_id"));
- }
- //
- // ---------------------- DETERMINE ZOHOBOOKS CUSTOMER ID ----------------------
- if(!isnull(r_QuoteDetails.get("Account_Name")))
- {
- v_AccountID = r_QuoteDetails.get("Account_Name").get("id");
- r_SearchResults = zoho.books.getRecords("Contacts",v_BooksOrgID,"zcrm_account_id=" + v_AccountID,"zbooks");
- if(!isnull(r_SearchResults.get("contacts")))
- {
- // tried sync contacts from Account record
- for each r_Result in r_SearchResults.get("contacts")
- {
- if(!isnull(r_Result.get("contact_id")))
- {
- v_BooksCustomerID = r_Result.get("contact_id").toLong();
- break;
- }
- }
- }
- }
- if(v_BooksCustomerID != 0)
- {
- m_CreateEstimate.put("customer_id",v_BooksCustomerID);
- }
- //
- // ---------------------- QUOTE CONTACT NAME/PHONE ----------------------
- if(!isnull(r_QuoteDetails.get("Contact_Name")))
- {
- v_ContactID = r_QuoteDetails.get("Contact_Name").get("id");
- v_ContactName = r_QuoteDetails.get("Contact_Name").get("name");
- r_ContactDetails = zoho.crm.getRecordById("Contacts",v_ContactID);
- v_ContactNumber = ifnull(r_ContactDetails.get("Mobile"),r_ContactDetails.get("Phone"));
- }
- //
- // ---------------------- QUOTE DATES ----------------------
- m_CreateEstimate.put("date",r_QuoteDetails.get("Created_Time").subString(0,10));
- if(!isnull(r_QuoteDetails.get("Valid_Till")))
- {
- m_CreateEstimate.put("expiry_date",r_QuoteDetails.get("Valid_Till").subString(0,10));
- }
- //
- // ---------------------- QUOTE LINKED POTENTIAL/OPPORTUNITY ----------------------
- if(!isnull(r_QuoteDetails.get("Deal_Name")))
- {
- m_CreateEstimate.put("zcrm_potential_id",r_QuoteDetails.get("Deal_Name").get("id").toLong());
- }
- //
- // ---------------------- QUOTE LINE ITEMS ----------------------
- // line items get via api v2.1 or greater
- l_BooksLineItems = List();
- r_CrmProductLineItems = invokeUrl
- [
- url :"https://www.zohoapis.eu/crm/v2.1/Quotes/" + p_QuoteID
- type :GET
- parameters:m_ApprovedConverted
- connection:"zcrm"
- ];
- // should be checking here that data contains any rows but I'm going on the basis that this quote exists in CRM and at least 1 line item is mandatory
- l_CrmProductLineItems = r_CrmProductLineItems.get("data").get(0).get("Quoted_Items");
- for each r_CrmLineItem in l_CrmProductLineItems
- {
- m_BooksLineItem = Map();
- v_BooksItemID = 0;
- v_CrmProductID = r_CrmLineItem.get("Product_Name").get("id");
- v_CrmProductName = r_CrmLineItem.get("Product_Name").get("name");
- // if the product doesn't exist in Books (sync hasn't happened yet), then you'll need code here to create the item if not found
- r_SearchResults2 = zoho.books.getRecords("items",v_BooksOrgID,"zcrm_product_id=" + v_CrmProductID,"zbooks");
- if(!isnull(r_SearchResults2.get("items")))
- {
- for each r_Result2 in r_SearchResults2.get("items")
- {
- if(r_Result2.get("item_name") == v_CrmProductName)
- {
- m_BooksLineItem.put("item_id",r_Result2.get("item_id").toLong());
- m_BooksLineItem.put("name",v_CrmProductName);
- m_BooksLineItem.put("description",r_CrmLineItem.get("Description"));
- m_BooksLineItem.put("quantity",r_CrmLineItem.get("Quantity"));
- m_BooksLineItem.put("rate",r_CrmLineItem.get("List_Price").toDecimal().round(2));
- m_BooksLineItem.put("discount",ifnull(r_CrmLineItem.get("Discount_Percent"),0) + "%");
- m_BooksLineItem.put("item_total",r_CrmLineItem.get("Net_Total").toDecimal().round(2));
- v_CrmVatPercent = r_CrmLineItem.get("VAT2").replaceAll("[^0-9]","");
- v_BooksTaxID = ifnull(m_Taxes.get(v_CrmVatPercent),m_Taxes.get("20"));
- m_BooksLineItem.put("tax_id",v_BooksTaxID);
- m_BooksLineItem.put("tax_percentage",v_CrmVatPercent);
- break;
- }
- }
- }
- l_BooksLineItems.add(m_BooksLineItem);
- }
- m_CreateEstimate.put("line_items",l_BooksLineItems);
- //
- // ---------------------- QUOTE SHIPPING CHARGE ----------------------
- if(r_QuoteDetails.get("Apply_VAT_to_Shinpping_Charges") && v_ShippingCharges > 0)
- {
- m_CreateEstimate.put("shipping_charge_tax_id",m_Taxes.get("20"));
- }
- else
- {
- m_CreateEstimate.put("shipping_charge_tax_id","");
- }
- m_CreateEstimate.put("shipping_charge",v_ShippingCharges);
- //
- // ---------------------- ESTIMATE CREATE OR UPDATE ----------------------
- v_BooksEstimateID = 0;
- if(!isnull(r_QuoteDetails.get("Books_Ref")))
- {
- r_SearchResults3 = zoho.books.getRecords("estimates",v_BooksOrgID,"estimate_number=" + r_QuoteDetails.get("Books_Ref"),"zbooks");
- if(!isnull(r_SearchResults3.get("estimates")))
- {
- for each r_BooksEstimate in r_SearchResults3.get("estimates")
- {
- if(!isnull(r_BooksEstimate.get("estimate_id")))
- {
- v_BooksEstimateID = r_BooksEstimate.get("estimate_id").toLong();
- }
- }
- }
- }
- if(v_BooksEstimateID == 0)
- {
- r_BooksEstimate = zoho.books.createRecord("estimates",v_BooksOrgID,m_CreateEstimate,"zbooks");
- }
- else
- {
- r_BooksEstimate = zoho.books.updateRecord("estimates",v_BooksOrgID,v_BooksEstimateID,m_CreateEstimate,"zbooks");
- }
- info "ZohoBooks Estimate REQUEST: ";
- info m_CreateEstimate;
- info "ZohoBooks Estimate RESPONSE: ";
- info r_BooksEstimate;
- //
- // ---------------------- UPDATE THE CRM QUOTE WITH ZOHOBOOKS ESTIMATE REF ----------------------
- m_UpdateCrmQuote = Map();
- if(!isnull(r_BooksEstimate.get("estimate")))
- {
- m_UpdateCrmQuote.put("Books_Ref",r_BooksEstimate.get("estimate").get("estimate_number"));
- v_BooksEstimateID = r_BooksEstimate.get("estimate").get("estimate_id").toLong();
- }
- if(m_UpdateCrmQuote.size() > 0)
- {
- r_UpdateCrmQuote = zoho.crm.updateRecord("Quotes",p_QuoteID,m_UpdateCrmQuote);
- }
- //
- // ---------------------- ADDRESS ON QUOTE/ESTIMATE ----------------------
- // if quote address is not blank, then push this to estimate
- if(v_BooksEstimateID != 0)
- {
- if(!isNull(r_QuoteDetails.get("Billing_Street")))
- {
- m_BooksBillingAddress = Map();
- for each index v_AddressIndex1 in l_CrmBillingAddress
- {
- m_BooksBillingAddress.put(l_BooksAddress.get(v_AddressIndex1),r_QuoteDetails.get(l_CrmBillingAddress.get(v_AddressIndex1)));
- }
- v_EndpointBilling = "https://www.zohoapis.eu/books/v3/estimates/" + v_BooksEstimateID + "/address/billing?organization_id=" + v_BooksOrgID;
- r_EstimateBillingAddress = invokeUrl
- [
- url :v_EndpointBilling
- type :PUT
- parameters:m_BooksBillingAddress.toString()
- connection:"zbooks"
- ];
- info "ZohoBooks Estimate Billing Address RESPONSE: ";
- info r_EstimateBillingAddress;
- }
- if(!isNull(r_QuoteDetails.get("Shipping_Street")))
- {
- m_BooksShippingAddress = Map();
- for each index v_AddressIndex2 in l_CrmShippingAddress
- {
- m_BooksShippingAddress.put(l_BooksAddress.get(v_AddressIndex2),r_QuoteDetails.get(l_CrmShippingAddress.get(v_AddressIndex2)));
- }
- m_BooksShippingAddress.put("attention",v_ContactName);
- if(!isNull(v_ContactNumber))
- {
- m_BooksShippingAddress.put("phone",v_ContactNumber);
- }
- v_EndpointShipping = "https://www.zohoapis.eu/books/v3/estimates/" + v_BooksEstimateID + "/address/shipping?organization_id=" + v_BooksOrgID;
- r_EstimateShippingAddress = invokeUrl
- [
- url :v_EndpointShipping
- type :PUT
- parameters:m_BooksShippingAddress.toString()
- connection:"zbooks"
- ];
- info "ZohoBooks Estimate Shipping Address RESPONSE: ";
- info r_EstimateShippingAddress;
- }
- }
- }
Source(s):
- Zoho Books Documentation / Update Billing Address
- Zoho Community / Zoho Inventory / General / Error: Please ensure that the billing_address has less than 100 characters