Thought I already had an article on this and I know my article Zoho Deluge - Connect to Xero API covered a quick query to pull some invoices but this one documents a pull and mapping into CRM invoices.
Why?
This took me a whole afternoon so I wanted a template function I could use in future when I get this request again. The additional benefit of having this template is that it includes creating contacts, accounts, and products on-the-fly as well as recording payments and checks as to which record is more up to date between ZohoCRM and Xero.
How?
The access token is generated by a function documented in my previously mentioned article Zoho Deluge - Connect to Xero API so here's the pull of the first page of invoices by most recent first (invoice date).
There is also a table that returns the name of tax rates and then the percent that these equate to which I've copied below this invoices pull function.
the Code
copyraw
string standalone.fn_Xero_GetInvoices()
{
/* *******************************************************************************
Function: string standalone.fn_Xero_GetInvoices()
Label: Fn - Xero - Get Invoices
Trigger: Standalone / On-Demand / Callable
Purpose: Function to get the first page of most recent invoices from Xero and pull them into ZohoCRM
Inputs: -
Outputs: -
Date Created: 2025-10-08 (Joel Lipman)
- Initial release
Date Modified: 2025-10-13 ( Joel Lipman)
- Parsing Xero Dates and Times to include timezone of Xero instance for accurate time comparisons
- Map Xero tax rates to Zoho tax rates (same names in Zoho)
- output to console if debug mode enabled
More Information:
http://www.joellipman.com/articles/crm/zoho/zoho-deluge-sync-to-xero-api.html
******************************************************************************* */
//
// init
v_OutputMessage = "ERROR: No Access Token or Tenant Connection specified.";
v_Count_FoundInXero = 0;
v_Count_Created = 0;
v_Count_Updated = 0;
v_AccessToken = "";
l_Pages = {1};
v_PageSize = 20;
b_Debug = false;
//
// Xero Invoice Statuses vs your CRM Invoice Statuses
m_TranslateStatuses = Map();
m_TranslateStatuses.put("DRAFT","Draft");
m_TranslateStatuses.put("SUBMITTED","Pending Approval");
m_TranslateStatuses.put("AUTHORISED","Sent to Customer");
m_TranslateStatuses.put("PAID","Paid in Full");
m_TranslateStatuses.put("DELETED","Cancelled");
m_TranslateStatuses.put("VOIDED","Cancelled");
//
// Xero Tax Rates translated to Zoho Tax Rate (manual reference)
r_ZohoTaxRates = standalone.fn_Xero_MapTaxRates();
m_ZohoTaxRates = r_ZohoTaxRates.toMap();
if(b_Debug)
{
info m_ZohoTaxRates;
}
//
// server timezones
v_XeroTimeZone = "Europe/London";
v_ZohoTimeZone = "Europe/London";
//
// enter the CRM record ID of your integrations record (Xero Integration API)
v_TokenCrmID = 123456000000789012;
r_TokenDetails = zoho.crm.getRecordById("Integrations",v_TokenCrmID);
v_DataEndpoint = ifnull(r_TokenDetails.get("Data_Endpoint"),"");
v_TenantID = ifnull(r_TokenDetails.get("Tenant_ID"),"");
//
// get access token (does not need REST API url as we're calling it from within CRM)
v_AccessToken = standalone.fn_API_GetXeroAccessToken();
//
// do Xero stuff
if(v_AccessToken != "" && v_TenantID != "")
{
// set header
m_Header = Map();
m_Header.put("Authorization","Bearer " + v_AccessToken);
m_Header.put("Accept","application/json");
m_Header.put("Xero-tenant-id",v_TenantID);
//
// get Xero invoices (page 1 - first 100 - default order is updated date)
for each v_Page in l_Pages
{
m_Params = Map();
m_Params.put("page",v_Page);
//
// keep the page size low as this function will be creating contacts and products if required
m_Params.put("pageSize",v_PageSize);
//
// order by date descending (most recent first) - sometimes need to use Date%20DESC
m_Params.put("order","UpdatedDateUTC DESC");
//
// get the first page of Xero invoices
v_FilterReceivables = "?where=" + zoho.encryption.urlEncode("Type=\"ACCREC\"");
r_AllXeroInvoices = invokeurl
[
url :v_DataEndpoint + "/Invoices" + v_FilterReceivables
type :GET
parameters:m_Params
headers:m_Header
];
if(b_Debug)
{
info r_AllXeroInvoices;
}
if(!isnull(r_AllXeroInvoices.get("Invoices")))
{
for each m_ThisInvoice in r_AllXeroInvoices.get("Invoices")
{
if(!isnull(m_ThisInvoice.get("InvoiceID")))
{
//
// counter
v_Count_FoundInXero = v_Count_FoundInXero + 1;
//
// Xero Invoice identifier
v_XeroInvoiceID = m_ThisInvoice.get("InvoiceID");
//
m_UpsertCrmInvoice = Map();
m_UpsertCrmInvoice.put("Subject",m_ThisInvoice.get("InvoiceNumber"));
//
// some standard CRM invoice fields we can populate
v_CrmInvoiceStatus = m_TranslateStatuses.get(m_ThisInvoice.get("Status"));
if(m_ThisInvoice.get("Status") == "PAID")
{
v_InvoiceTotal = m_ThisInvoice.get("Total");
v_PaidTotal = 0.0;
// we have a partially paid status in crm so let's check those payments
for each m_XeroPayment in m_ThisInvoice.get("Payments")
{
if(!isNull(m_XeroPayment.get("PaymentID")))
{
v_PaidTotal = v_PaidTotal + m_XeroPayment.get("Amount");
}
}
v_CrmInvoiceStatus = if(v_PaidTotal == v_InvoiceTotal,"Paid in Full","Partially Paid");
}
m_UpsertCrmInvoice.put("Status",v_CrmInvoiceStatus);
v_XeroInvoiceDate = m_ThisInvoice.get("Date");
d_XeroInvoiceDate = v_XeroInvoiceDate.replaceAll("/Date\((\d+)([+-]\d{4})?\)/","$1",false).toLong().toTime("yyyy-MM-dd HH:mm:ss",v_XeroTimeZone);
m_UpsertCrmInvoice.put("Invoice_Date",d_XeroInvoiceDate.toString("yyyy-MM-dd"));
v_XeroInvoiceDueDate = m_ThisInvoice.get("DueDate");
d_XeroInvoiceDueDate = v_XeroInvoiceDueDate.replaceAll("/Date\((\d+)([+-]\d{4})?\)/","$1",false).toLong().toTime("yyyy-MM-dd HH:mm:ss",v_XeroTimeZone);
m_UpsertCrmInvoice.put("Due_Date",d_XeroInvoiceDueDate.toString("yyyy-MM-dd"));
m_UpsertCrmInvoice.put("Currency",m_ThisInvoice.get("CurrencyCode"));
//
// some custom fields I created in CRM to store the data
m_UpsertCrmInvoice.put("Xero_Ref_ID",m_ThisInvoice.get("InvoiceID"));
m_UpsertCrmInvoice.put("Xero_Updated",zoho.currenttime.toString("yyyy-MM-dd'T'HH:mm:ss",v_ZohoTimeZone));
m_UpsertCrmInvoice.put("Amount_Paid",m_ThisInvoice.get("AmountPaid"));
m_UpsertCrmInvoice.put("Amount_Credited",m_ThisInvoice.get("AmountCredited"));
if(!isNull(m_ThisInvoice.get("FullyPaidOnDate")))
{
v_XeroFullyPaidDate = m_ThisInvoice.get("FullyPaidOnDate");
d_XeroFullyPaidDate = v_XeroFullyPaidDate.replaceAll("/Date\((\d+)([+-]\d{4})?\)/","$1",false).toLong().toTime("yyyy-MM-dd HH:mm:ss",v_XeroTimeZone);
m_UpsertCrmInvoice.put("Date_Fully_Paid",d_XeroFullyPaidDate.toString("yyyy-MM-dd"));
}
m_UpsertCrmInvoice.put("Reference",m_ThisInvoice.get("Reference"));
//
// -------------------------------- Invoice Customer --------------------------------
//
// initialize
v_CrmAccountID = "";
v_CrmContactID = "";
v_CrmPhone = "";
v_CrmMobile = "";
b_CreateAccount = true;
b_CreateContact = true;
//
// set date/time of account last sync'd to Xero (100 years ago by default - so that it will be oldest)
d_CrmAccountLastUpdated = zoho.currenttime.toString("yyyy-MM-dd HH:mm:ss").toTime().subYear(100);
v_XeroContactID = m_ThisInvoice.get("Contact").get("ContactID");
v_XeroContactName = m_ThisInvoice.get("Contact").get("Name");
//
// search CRM for this account/customer
l_SearchAccounts = zoho.crm.searchRecords("Accounts","Xero_Ref_ID:equals:" + v_XeroContactID,1,2,{"approved":"both","converted":"both"});
for each m_SearchAccount in l_SearchAccounts
{
if(!isNull(m_SearchAccount.get("id")))
{
b_CreateAccount = false;
v_CrmAccountID = m_SearchAccount.get("id");
//
// if sync'd before then let's use that date/time
d_CrmAccountLastUpdated = ifnull(m_SearchAccount.get("Xero_Updated"),zoho.currenttime).toString("yyyy-MM-dd HH:mm:ss",v_ZohoTimeZone).toTime();
if(b_Debug)
{
info "Found CRM Account: " + v_CrmAccountID;
}
}
}
//
// get account/contact details from Xero (invoice doesn't necessarily hold the details: address, phone, etc)
r_XeroContact = invokeurl
[
url :v_DataEndpoint + "/Contacts/" + v_XeroContactID
type :GET
parameters:m_Params
headers:m_Header
];
l_XeroContacts = ifnull(r_XeroContact.get("Contacts"),List());
for each m_XeroContact in l_XeroContacts
{
if(!isNull(m_XeroContact.get("ContactID")))
{
//
// to check if we want to update the CRM record for the account
v_XeroTime = m_XeroContact.get("UpdatedDateUTC");
d_XeroAccountLastUpdated = v_XeroTime.replaceAll("/Date\((\d+)([+-]\d{4})?\)/","$1",false).toLong().toTime("yyyy-MM-dd HH:mm:ss",v_XeroTimeZone);
//
// build upsert for CRM account
m_CrmAccount = Map();
m_CrmAccount.put("Account_Name",m_ThisInvoice.get("Contact").get("Name"));
m_CrmAccount.put("Xero_Ref_ID",m_XeroContact.get("ContactID"));
m_CrmAccount.put("Xero_Updated",zoho.currenttime.toString("yyyy-MM-dd'T'HH:mm:ss",v_ZohoTimeZone));
//
// addresses
for each m_XeroAddress in m_XeroContact.get("Addresses")
{
if(!isNull(m_XeroAddress.get("AddressLine1")))
{
v_XeroAddressLine1 = m_XeroAddress.get("AddressLine1");
v_XeroAddressLine2 = m_XeroAddress.get("AddressLine2");
v_XeroAddressCity = m_XeroAddress.get("City");
v_XeroAddressZip = m_XeroAddress.get("PostalCode");
v_XeroAddressAttn = m_XeroAddress.get("AttentionTo");
}
}
//
l_AddressStreet = List({v_XeroAddressLine1});
if(!isBlank(v_XeroAddressLine2))
{
l_AddressStreet.add(v_XeroAddressLine2);
}
m_CrmAccount.put("Billing_Street",l_AddressStreet.toString(", "));
m_CrmAccount.put("Billing_City",v_XeroAddressCity);
m_CrmAccount.put("Billing_Code",v_XeroAddressZip);
//
// loop through phones
for each m_XeroPhone in m_XeroContact.get("Phones")
{
if(!isNull(m_XeroPhone.get("PhoneNumber")))
{
v_XeroPhoneType = m_XeroPhone.get("PhoneType");
l_XeroFullPhoneNumberParts = List();
if(!isNull(m_XeroPhone.get("PhoneCountryCode")))
{
l_XeroFullPhoneNumberParts.add(m_XeroPhone.get("PhoneCountryCode"));
}
if(!isNull(m_XeroPhone.get("PhoneAreaCode")))
{
l_XeroFullPhoneNumberParts.add(m_XeroPhone.get("PhoneAreaCode"));
}
if(!isNull(m_XeroPhone.get("PhoneNumber")))
{
l_XeroFullPhoneNumberParts.add(m_XeroPhone.get("PhoneNumber"));
}
v_XeroFullPhoneNumber = l_XeroFullPhoneNumberParts.toString(" ");
if(v_XeroPhoneType == "DEFAULT" || v_XeroPhoneType == "PHONE")
{
v_CrmPhone = v_XeroFullPhoneNumber;
}
else if(v_XeroPhoneType == "MOBILE")
{
v_CrmMobile = v_XeroFullPhoneNumber;
}
}
}
m_CrmAccount.put("Phone",v_CrmPhone);
//
// balances
v_XeroReceivables = 0.0;
v_XeroPayables = 0.0;
for each m_XeroBalance in m_XeroContact.get("Balances")
{
if(!isNull(m_XeroBalance.get("AccountsReceivable")))
{
v_XeroReceivables = m_XeroBalance.get("AccountsReceivable").get("Outstanding");
v_XeroReceivables = v_XeroReceivables + m_XeroBalance.get("AccountsReceivable").get("Overdue");
v_XeroReceivables = v_XeroReceivables * -1;
}
if(!isNull(m_XeroBalance.get("AccountsPayable")))
{
v_XeroPayables = m_XeroBalance.get("AccountsPayable").get("Outstanding");
v_XeroPayables = v_XeroPayables + m_XeroBalance.get("AccountsReceivable").get("Overdue");
}
}
v_XeroBalance = v_XeroPayables - v_XeroReceivables;
m_CrmAccount.put("Xero_Balance",v_XeroBalance);
//
// create CRM account for other contact records
if(b_CreateAccount)
{
r_CreateAccount = zoho.crm.createRecord("Accounts",m_CrmAccount);
if(b_Debug)
{
info "Creating CRM Account: " + r_CreateAccount;
}
if(!isNull(r_CreateAccount.getJSON("id")))
{
v_CrmAccountID = r_CreateAccount.get("id");
}
}
//
// create a contact
v_SearchContactsCriteria = "Email:equals:" + if(isBlank(m_XeroContact.get("EmailAddress")),"Unknown",m_XeroContact.get("EmailAddress"));
l_SearchContacts = zoho.crm.searchRecords("Contacts",v_SearchContactsCriteria);
if(b_Debug)
{
info "Searching Contacts (" + v_SearchContactsCriteria + "): " + l_SearchContacts;
}
for each m_SearchContact in l_SearchContacts
{
if(!isNull(m_SearchContact.getJSON("id")))
{
b_CreateContact = false;
v_CrmContactID = m_SearchContact.get("id");
if(b_Debug)
{
info "Found CRM Contact: " + v_CrmContactID;
}
}
}
//
// build upsert for CRM contact
m_CrmContact = Map();
m_CrmContact.put("First_Name",m_XeroContact.get("FirstName"));
// last name is mandatory for a CRM contact so we're going to put a placeholder one if this is not given
v_CrmContactLastName = ifnull(m_XeroContact.get("LastName"),"-Unknown-");
m_CrmContact.put("Last_Name",v_CrmContactLastName);
m_CrmContact.put("Email",m_XeroContact.get("EmailAddress"));
m_CrmContact.put("Phone",v_CrmPhone);
m_CrmContact.put("Mobile",v_CrmMobile);
m_CrmContact.put("Xero_Ref_ID",m_XeroContact.get("ContactID"));
m_CrmContact.put("Xero_Updated",zoho.currenttime.toString("yyyy-MM-dd'T'HH:mm:ss",v_ZohoTimeZone));
m_CrmContact.put("Mailing_Street",l_AddressStreet.toString(", "));
m_CrmContact.put("Mailing_City",v_XeroAddressCity);
m_CrmContact.put("Mailing_Zip",v_XeroAddressZip);
m_CrmContact.put("Account_Name",v_CrmAccountID);
// last name is mandatory, let's not bother if it wasn't provided
if(b_CreateContact && v_CrmContactLastName != "-Unknown-")
{
r_CreateContact = zoho.crm.createRecord("Contacts",m_CrmContact);
if(b_Debug)
{
info "Creating Primary Contact: " + r_CreateContact;
}
if(!isNull(r_CreateContact.get("id")))
{
v_CrmContactID = r_CreateContact.get("id");
}
//
// create other contacts (retain the map and only change first name, last name, and email)
for each m_OtherContact in m_XeroContact.get("ContactPersons")
{
m_CrmContact.put("First_Name",m_OtherContact.get("FirstName"));
m_CrmContact.put("Last_Name",m_OtherContact.get("LastName"));
m_CrmContact.put("Email",m_OtherContact.get("EmailAddress"));
r_CreateContact2 = zoho.crm.createRecord("Contacts",m_CrmContact);
if(b_Debug)
{
info "Creating Secondary Contact: " + r_CreateContact2;
}
}
}
}
}
//
// if Xero record is more recently updated than the CRM one, then update the account
if(d_XeroAccountLastUpdated >= d_CrmAccountLastUpdated)
{
r_UpdateCrmAccount = zoho.crm.updateRecord("Accounts",v_CrmAccountID,m_CrmAccount);
r_UpdateCrmContact = zoho.crm.updateRecord("Contacts",v_CrmContactID,m_CrmContact);
}
//
// add account/contact to the invoice
m_UpsertCrmInvoice.put("Account_Name",v_CrmAccountID);
m_UpsertCrmInvoice.put("Contact_Name",v_CrmContactID);
//
// -------------------------------- Invoice Line Items --------------------------------
//
// initializing
l_CrmLineItems = List();
//
// loop through line items on the Xero invoice
for each m_XeroLineItem in m_ThisInvoice.get("LineItems")
{
//
// initialize
v_CrmProductID = "";
l_CrmProductAvailableTaxes = List();
l_CrmProductAvailableTaxes.add("NONE");
//
// checking this is a valid line item and not an error message by it having an ItemCode
v_CrmProductName = ifnull(m_XeroLineItem.get("ItemCode"),m_XeroLineItem.get("Description"));
v_CrmProductName = if(isBlank(v_CrmProductName),ifnull(m_XeroLineItem.get("Item"),{"Name":"Product"}).get("Name"),"Product");
if(!isBlank(v_CrmProductName))
{
v_CrmProductCode = ifnull(m_XeroLineItem.get("ItemCode"),"-Unknown-");
v_CrmProductCodeSafe = zoho.encryption.urlEncode(v_CrmProductCode);
v_CrmProductName = if(v_CrmProductName.length() >= 200,v_CrmProductName.subString(0,199),v_CrmProductName);
v_CrmProductNameSafe = zoho.encryption.urlEncode(v_CrmProductName);
v_SearchCriteria = "((Product_Code:equals:" + v_CrmProductCodeSafe + ")or(Product_Name:equals:" + v_CrmProductNameSafe + "))";
l_SearchProducts = zoho.crm.searchRecords("Products",v_SearchCriteria,1,2,{"approved":"both"});
if(b_Debug)
{
info "Searching CRM Products: " + v_SearchCriteria;
}
for each m_SearchProduct in l_SearchProducts
{
if(!isNull(m_SearchProduct.get("id")))
{
v_CrmProductID = m_SearchProduct.get("id");
l_CrmProductAvailableTaxes = ifnull(m_SearchProduct.get("Tax"),List());
if(b_Debug)
{
info "Available Taxes: " + l_CrmProductAvailableTaxes;
}
}
}
//
// couldn't find it so let's create it
m_CrmProduct = Map();
//
// some companies don't use the product lookup in Xero so you would need a placeholder product from CRM.
if(!isNull(m_XeroLineItem.get("Item")))
{
v_CrmProductName = m_XeroLineItem.get("Item").get("Name");
m_CrmProduct.put("Xero_Ref_ID",m_XeroLineItem.get("Item").get("ItemID"));
m_CrmProduct.put("Product_Code",m_XeroLineItem.get("Item").get("Code"));
}
m_CrmProduct.put("Product_Name",v_CrmProductName);
m_CrmProduct.put("Product_Active",true);
m_CrmProduct.put("Description",m_XeroLineItem.get("Description"));
m_CrmProduct.put("Unit_Price",m_XeroLineItem.get("UnitAmount"));
//
// map over tax (even if it's zero)
v_XeroLineItemTaxAmount = ifnull(m_XeroLineItem.get("TaxAmount"),0).toDecimal();
m_CrmProduct.put("Taxable",true);
v_CrmTaxRateString = m_XeroLineItem.get("TaxType") + " - " + ifnull(m_ZohoTaxRates.get(m_XeroLineItem.get("TaxType")),"0.0") + " %";
if(!l_CrmProductAvailableTaxes.contains(v_CrmTaxRateString))
{
l_CrmProductAvailableTaxes.add(m_XeroLineItem.get("TaxType"));
}
m_CrmProduct.put("Tax",l_CrmProductAvailableTaxes);
//
m_CrmProduct.put("Xero_Updated",zoho.currenttime.toString("yyyy-MM-dd'T'HH:mm:ss",v_ZohoTimeZone));
//
if(v_CrmProductID == "")
{
r_CreateCrmProduct = zoho.crm.createRecord("Products",m_CrmProduct);
if(b_Debug)
{
info "Creating CRM Product: " + r_CreateCrmProduct;
}
if(!isNull(r_CreateCrmProduct.get("id")))
{
v_CrmProductID = r_CreateCrmProduct.get("id");
}
else if(r_CreateCrmProduct.get("code").equalsIgnoreCase("DUPLICATE_DATA"))
{
v_CrmProductID = r_CreateCrmProduct.get("details").get("id");
if(b_Debug)
{
info "Duplicate CRM Product: Re-using " + v_CrmProductID;
}
}
}
else
{
//
// update the product (mainly for new applicable taxes)
r_UpdateCrmProduct = zoho.crm.updateRecord("Products",v_CrmProductID,m_CrmProduct);
if(b_Debug)
{
info "Update CRM Product";
info m_CrmProduct;
info r_UpdateCrmProduct;
}
}
//
// let's do the rest of the line item (note that we are going to upsert using CRM API v8)
m_CrmLineItem = Map();
m_CrmLineItem.put("Product_Name",v_CrmProductID);
m_CrmLineItem.put("Description",m_XeroLineItem.get("Description"));
m_CrmLineItem.put("List_Price",m_XeroLineItem.get("UnitAmount"));
m_CrmLineItem.put("Quantity",m_XeroLineItem.get("Quantity"));
v_DiscountPercent = ifnull(m_XeroLineItem.get("DiscountRate"),0.0);
v_DiscountAmount = ifnull(m_XeroLineItem.get("DiscountAmount"),0.0);
if(v_DiscountPercent != 0)
{
// just qty vs unit excluding discount and tax
v_LineItemTotal = m_XeroLineItem.get("Quantity") * m_XeroLineItem.get("UnitAmount");
v_DiscountFactor = v_DiscountPercent / 100;
v_DiscountAmount = v_LineItemTotal * v_DiscountFactor;
}
// tax even if it's zero
l_CrmLineItemTax = List();
m_CrmLineItemTax = Map();
v_ZohoLineItemTaxPercent = ifnull(m_ZohoTaxRates.get(m_XeroLineItem.get("TaxType")),0.0).toDecimal();
v_ZohoLineItemTaxPercent = v_ZohoLineItemTaxPercent * 1;
m_CrmLineItemTax.put("percentage",v_ZohoLineItemTaxPercent);
m_CrmLineItemTax.put("name",ifnull(m_XeroLineItem.get("TaxType"),"NONE"));
m_CrmLineItemTax.put("value",m_XeroLineItem.get("TaxAmount"));
l_CrmLineItemTax.add(m_CrmLineItemTax);
m_CrmLineItem.put("Line_Tax",l_CrmLineItemTax);
//
m_CrmLineItem.put("Discount",v_DiscountAmount);
l_CrmLineItems.add(m_CrmLineItem);
}
}
//
// if the CRM invoice already exists, we are going to upsert so we need to remove the current line items in the CRM invoice
l_SearchInvoices = zoho.crm.searchRecords("Invoices","Xero_Ref_ID:equals:" + v_XeroInvoiceID);
for each m_InvoiceResult in l_SearchInvoices
{
if(!isNull(m_InvoiceResult.get("id")))
{
for each m_ExistingLineItem in m_InvoiceResult.get("Product_Details")
{
m_MiniDeleteMe = Map();
m_MiniDeleteMe.put("id",m_ExistingLineItem.get("id"));
m_MiniDeleteMe.put("_delete",null);
l_CrmLineItems.add(m_MiniDeleteMe);
}
}
}
//
// add line items to the invoice
m_UpsertCrmInvoice.put("Invoiced_Items",l_CrmLineItems);
//
// let's add the billing address retrieved earlier to the invoice
m_UpsertCrmInvoice.put("Billing_Street",l_AddressStreet.toString(", "));
m_UpsertCrmInvoice.put("Billing_City",v_XeroAddressCity);
m_UpsertCrmInvoice.put("Billing_Code",v_XeroAddressZip);
//
// let's upsert
m_Data = Map();
m_Data.put("data",List({m_UpsertCrmInvoice}));
m_Data.put("trigger",{"workflow","approval","blueprint"});
r_UpsertInvoice = invokeurl
[
url :"https://www.zohoapis.eu/crm/v8/Invoices/upsert"
type :POST
parameters:m_Data.toString()
connection:"ab_crm"
];
if(b_Debug)
{
info "Upserting Invoice: " + m_ThisInvoice.get("InvoiceNumber");
info m_UpsertCrmInvoice;
info r_UpsertInvoice;
}
l_ResponseData = ifnull(r_UpsertInvoice.get("data"),List());
for each m_ResponseData in l_ResponseData
{
if(!isNull(m_ResponseData.get("code")))
{
v_Action = m_ResponseData.get("action");
}
info m_ResponseData.get("code");
//
// possible errors, output anyway
b_Error = if(m_ResponseData.get("code").equalsIgnoreCase("SUCCESS"),false,true);
if(b_Error)
{
info m_ThisInvoice.get("InvoiceNumber") + ": FAILED: " + m_ResponseData;
info m_UpsertCrmInvoice;
}
}
if(v_Action == "insert")
{
v_Count_Created = v_Count_Created + 1;
}
else if(v_Action == "update")
{
v_Count_Updated = v_Count_Updated + 1;
}
}
}
}
}
v_OutputMessage = "Created " + v_Count_Created + " and Updated " + v_Count_Updated + " from " + v_Count_FoundInXero;
}
return v_OutputMessage;
}
- string standalone.fn_Xero_GetInvoices()
- {
- /* *******************************************************************************
- Function: string standalone.fn_Xero_GetInvoices()
- Label: Fn - Xero - Get Invoices
- Trigger: Standalone / On-Demand / Callable
- Purpose: Function to get the first page of most recent invoices from Xero and pull them into ZohoCRM
- Inputs: -
- Outputs: -
- Date Created: 2025-10-08 (Joel Lipman)
- - Initial release
- Date Modified: 2025-10-13 ( Joel Lipman)
- - Parsing Xero Dates and Times to include timezone of Xero instance for accurate time comparisons
- - Map Xero tax rates to Zoho tax rates (same names in Zoho)
- - output to console if debug mode enabled
- More Information:
- http://www.joellipman.com/articles/crm/zoho/zoho-deluge-sync-to-xero-api.html
- ******************************************************************************* */
- //
- // init
- v_OutputMessage = "ERROR: No Access Token or Tenant Connection specified.";
- v_Count_FoundInXero = 0;
- v_Count_Created = 0;
- v_Count_Updated = 0;
- v_AccessToken = "";
- l_Pages = {1};
- v_PageSize = 20;
- b_Debug = false;
- //
- // Xero Invoice Statuses vs your CRM Invoice Statuses
- m_TranslateStatuses = Map();
- m_TranslateStatuses.put("DRAFT","Draft");
- m_TranslateStatuses.put("SUBMITTED","Pending Approval");
- m_TranslateStatuses.put("AUTHORISED","Sent to Customer");
- m_TranslateStatuses.put("PAID","Paid in Full");
- m_TranslateStatuses.put("DELETED","Cancelled");
- m_TranslateStatuses.put("VOIDED","Cancelled");
- //
- // Xero Tax Rates translated to Zoho Tax Rate (manual reference)
- r_ZohoTaxRates = standalone.fn_Xero_MapTaxRates();
- m_ZohoTaxRates = r_ZohoTaxRates.toMap();
- if(b_Debug)
- {
- info m_ZohoTaxRates;
- }
- //
- // server timezones
- v_XeroTimeZone = "Europe/London";
- v_ZohoTimeZone = "Europe/London";
- //
- // enter the CRM record ID of your integrations record (Xero Integration API)
- v_TokenCrmID = 123456000000789012;
- r_TokenDetails = zoho.crm.getRecordById("Integrations",v_TokenCrmID);
- v_DataEndpoint = ifnull(r_TokenDetails.get("Data_Endpoint"),"");
- v_TenantID = ifnull(r_TokenDetails.get("Tenant_ID"),"");
- //
- // get access token (does not need REST API url as we're calling it from within CRM)
- v_AccessToken = standalone.fn_API_GetXeroAccessToken();
- //
- // do Xero stuff
- if(v_AccessToken != "" && v_TenantID != "")
- {
- // set header
- m_Header = Map();
- m_Header.put("Authorization","Bearer " + v_AccessToken);
- m_Header.put("Accept","application/json");
- m_Header.put("Xero-tenant-id",v_TenantID);
- //
- // get Xero invoices (page 1 - first 100 - default order is updated date)
- for each v_Page in l_Pages
- {
- m_Params = Map();
- m_Params.put("page",v_Page);
- //
- // keep the page size low as this function will be creating contacts and products if required
- m_Params.put("pageSize",v_PageSize);
- //
- // order by date descending (most recent first) - sometimes need to use Date%20DESC
- m_Params.put("order","UpdatedDateUTC DESC");
- //
- // get the first page of Xero invoices
- v_FilterReceivables = "?where=" + zoho.encryption.urlEncode("Type=\"ACCREC\"");
- r_AllXeroInvoices = invokeUrl
- [
- url :v_DataEndpoint + "/Invoices" + v_FilterReceivables
- type :GET
- parameters:m_Params
- headers:m_Header
- ];
- if(b_Debug)
- {
- info r_AllXeroInvoices;
- }
- if(!isnull(r_AllXeroInvoices.get("Invoices")))
- {
- for each m_ThisInvoice in r_AllXeroInvoices.get("Invoices")
- {
- if(!isnull(m_ThisInvoice.get("InvoiceID")))
- {
- //
- // counter
- v_Count_FoundInXero = v_Count_FoundInXero + 1;
- //
- // Xero Invoice identifier
- v_XeroInvoiceID = m_ThisInvoice.get("InvoiceID");
- //
- m_UpsertCrmInvoice = Map();
- m_UpsertCrmInvoice.put("Subject",m_ThisInvoice.get("InvoiceNumber"));
- //
- // some standard CRM invoice fields we can populate
- v_CrmInvoiceStatus = m_TranslateStatuses.get(m_ThisInvoice.get("Status"));
- if(m_ThisInvoice.get("Status") == "PAID")
- {
- v_InvoiceTotal = m_ThisInvoice.get("Total");
- v_PaidTotal = 0.0;
- // we have a partially paid status in crm so let's check those payments
- for each m_XeroPayment in m_ThisInvoice.get("Payments")
- {
- if(!isNull(m_XeroPayment.get("PaymentID")))
- {
- v_PaidTotal = v_PaidTotal + m_XeroPayment.get("Amount");
- }
- }
- v_CrmInvoiceStatus = if(v_PaidTotal == v_InvoiceTotal,"Paid in Full","Partially Paid");
- }
- m_UpsertCrmInvoice.put("Status",v_CrmInvoiceStatus);
- v_XeroInvoiceDate = m_ThisInvoice.get("Date");
- d_XeroInvoiceDate = v_XeroInvoiceDate.replaceAll("/Date\((\d+)([+-]\d{4})?\)/","$1",false).toLong().toTime("yyyy-MM-dd HH:mm:ss",v_XeroTimeZone);
- m_UpsertCrmInvoice.put("Invoice_Date",d_XeroInvoiceDate.toString("yyyy-MM-dd"));
- v_XeroInvoiceDueDate = m_ThisInvoice.get("DueDate");
- d_XeroInvoiceDueDate = v_XeroInvoiceDueDate.replaceAll("/Date\((\d+)([+-]\d{4})?\)/","$1",false).toLong().toTime("yyyy-MM-dd HH:mm:ss",v_XeroTimeZone);
- m_UpsertCrmInvoice.put("Due_Date",d_XeroInvoiceDueDate.toString("yyyy-MM-dd"));
- m_UpsertCrmInvoice.put("Currency",m_ThisInvoice.get("CurrencyCode"));
- //
- // some custom fields I created in CRM to store the data
- m_UpsertCrmInvoice.put("Xero_Ref_ID",m_ThisInvoice.get("InvoiceID"));
- m_UpsertCrmInvoice.put("Xero_Updated",zoho.currenttime.toString("yyyy-MM-dd'T'HH:mm:ss",v_ZohoTimeZone));
- m_UpsertCrmInvoice.put("Amount_Paid",m_ThisInvoice.get("AmountPaid"));
- m_UpsertCrmInvoice.put("Amount_Credited",m_ThisInvoice.get("AmountCredited"));
- if(!isNull(m_ThisInvoice.get("FullyPaidOnDate")))
- {
- v_XeroFullyPaidDate = m_ThisInvoice.get("FullyPaidOnDate");
- d_XeroFullyPaidDate = v_XeroFullyPaidDate.replaceAll("/Date\((\d+)([+-]\d{4})?\)/","$1",false).toLong().toTime("yyyy-MM-dd HH:mm:ss",v_XeroTimeZone);
- m_UpsertCrmInvoice.put("Date_Fully_Paid",d_XeroFullyPaidDate.toString("yyyy-MM-dd"));
- }
- m_UpsertCrmInvoice.put("Reference",m_ThisInvoice.get("Reference"));
- //
- // -------------------------------- Invoice Customer --------------------------------
- //
- // initialize
- v_CrmAccountID = "";
- v_CrmContactID = "";
- v_CrmPhone = "";
- v_CrmMobile = "";
- b_CreateAccount = true;
- b_CreateContact = true;
- //
- // set date/time of account last sync'd to Xero (100 years ago by default - so that it will be oldest)
- d_CrmAccountLastUpdated = zoho.currenttime.toString("yyyy-MM-dd HH:mm:ss").toTime().subYear(100);
- v_XeroContactID = m_ThisInvoice.get("Contact").get("ContactID");
- v_XeroContactName = m_ThisInvoice.get("Contact").get("Name");
- //
- // search CRM for this account/customer
- l_SearchAccounts = zoho.crm.searchRecords("Accounts","Xero_Ref_ID:equals:" + v_XeroContactID,1,2,{"approved":"both","converted":"both"});
- for each m_SearchAccount in l_SearchAccounts
- {
- if(!isNull(m_SearchAccount.get("id")))
- {
- b_CreateAccount = false;
- v_CrmAccountID = m_SearchAccount.get("id");
- //
- // if sync'd before then let's use that date/time
- d_CrmAccountLastUpdated = ifnull(m_SearchAccount.get("Xero_Updated"),zoho.currenttime).toString("yyyy-MM-dd HH:mm:ss",v_ZohoTimeZone).toTime();
- if(b_Debug)
- {
- info "Found CRM Account: " + v_CrmAccountID;
- }
- }
- }
- //
- // get account/contact details from Xero (invoice doesn't necessarily hold the details: address, phone, etc)
- r_XeroContact = invokeUrl
- [
- url :v_DataEndpoint + "/Contacts/" + v_XeroContactID
- type :GET
- parameters:m_Params
- headers:m_Header
- ];
- l_XeroContacts = ifnull(r_XeroContact.get("Contacts"),List());
- for each m_XeroContact in l_XeroContacts
- {
- if(!isNull(m_XeroContact.get("ContactID")))
- {
- //
- // to check if we want to update the CRM record for the account
- v_XeroTime = m_XeroContact.get("UpdatedDateUTC");
- d_XeroAccountLastUpdated = v_XeroTime.replaceAll("/Date\((\d+)([+-]\d{4})?\)/","$1",false).toLong().toTime("yyyy-MM-dd HH:mm:ss",v_XeroTimeZone);
- //
- // build upsert for CRM account
- m_CrmAccount = Map();
- m_CrmAccount.put("Account_Name",m_ThisInvoice.get("Contact").get("Name"));
- m_CrmAccount.put("Xero_Ref_ID",m_XeroContact.get("ContactID"));
- m_CrmAccount.put("Xero_Updated",zoho.currenttime.toString("yyyy-MM-dd'T'HH:mm:ss",v_ZohoTimeZone));
- //
- // addresses
- for each m_XeroAddress in m_XeroContact.get("Addresses")
- {
- if(!isNull(m_XeroAddress.get("AddressLine1")))
- {
- v_XeroAddressLine1 = m_XeroAddress.get("AddressLine1");
- v_XeroAddressLine2 = m_XeroAddress.get("AddressLine2");
- v_XeroAddressCity = m_XeroAddress.get("City");
- v_XeroAddressZip = m_XeroAddress.get("PostalCode");
- v_XeroAddressAttn = m_XeroAddress.get("AttentionTo");
- }
- }
- //
- l_AddressStreet = List({v_XeroAddressLine1});
- if(!isBlank(v_XeroAddressLine2))
- {
- l_AddressStreet.add(v_XeroAddressLine2);
- }
- m_CrmAccount.put("Billing_Street",l_AddressStreet.toString(", "));
- m_CrmAccount.put("Billing_City",v_XeroAddressCity);
- m_CrmAccount.put("Billing_Code",v_XeroAddressZip);
- //
- // loop through phones
- for each m_XeroPhone in m_XeroContact.get("Phones")
- {
- if(!isNull(m_XeroPhone.get("PhoneNumber")))
- {
- v_XeroPhoneType = m_XeroPhone.get("PhoneType");
- l_XeroFullPhoneNumberParts = List();
- if(!isNull(m_XeroPhone.get("PhoneCountryCode")))
- {
- l_XeroFullPhoneNumberParts.add(m_XeroPhone.get("PhoneCountryCode"));
- }
- if(!isNull(m_XeroPhone.get("PhoneAreaCode")))
- {
- l_XeroFullPhoneNumberParts.add(m_XeroPhone.get("PhoneAreaCode"));
- }
- if(!isNull(m_XeroPhone.get("PhoneNumber")))
- {
- l_XeroFullPhoneNumberParts.add(m_XeroPhone.get("PhoneNumber"));
- }
- v_XeroFullPhoneNumber = l_XeroFullPhoneNumberParts.toString(" ");
- if(v_XeroPhoneType == "DEFAULT" || v_XeroPhoneType == "PHONE")
- {
- v_CrmPhone = v_XeroFullPhoneNumber;
- }
- else if(v_XeroPhoneType == "MOBILE")
- {
- v_CrmMobile = v_XeroFullPhoneNumber;
- }
- }
- }
- m_CrmAccount.put("Phone",v_CrmPhone);
- //
- // balances
- v_XeroReceivables = 0.0;
- v_XeroPayables = 0.0;
- for each m_XeroBalance in m_XeroContact.get("Balances")
- {
- if(!isNull(m_XeroBalance.get("AccountsReceivable")))
- {
- v_XeroReceivables = m_XeroBalance.get("AccountsReceivable").get("Outstanding");
- v_XeroReceivables = v_XeroReceivables + m_XeroBalance.get("AccountsReceivable").get("Overdue");
- v_XeroReceivables = v_XeroReceivables * -1;
- }
- if(!isNull(m_XeroBalance.get("AccountsPayable")))
- {
- v_XeroPayables = m_XeroBalance.get("AccountsPayable").get("Outstanding");
- v_XeroPayables = v_XeroPayables + m_XeroBalance.get("AccountsReceivable").get("Overdue");
- }
- }
- v_XeroBalance = v_XeroPayables - v_XeroReceivables;
- m_CrmAccount.put("Xero_Balance",v_XeroBalance);
- //
- // create CRM account for other contact records
- if(b_CreateAccount)
- {
- r_CreateAccount = zoho.crm.createRecord("Accounts",m_CrmAccount);
- if(b_Debug)
- {
- info "Creating CRM Account: " + r_CreateAccount;
- }
- if(!isNull(r_CreateAccount.getJSON("id")))
- {
- v_CrmAccountID = r_CreateAccount.get("id");
- }
- }
- //
- // create a contact
- v_SearchContactsCriteria = "Email:equals:" + if(isBlank(m_XeroContact.get("EmailAddress")),"Unknown",m_XeroContact.get("EmailAddress"));
- l_SearchContacts = zoho.crm.searchRecords("Contacts",v_SearchContactsCriteria);
- if(b_Debug)
- {
- info "Searching Contacts (" + v_SearchContactsCriteria + "): " + l_SearchContacts;
- }
- for each m_SearchContact in l_SearchContacts
- {
- if(!isNull(m_SearchContact.getJSON("id")))
- {
- b_CreateContact = false;
- v_CrmContactID = m_SearchContact.get("id");
- if(b_Debug)
- {
- info "Found CRM Contact: " + v_CrmContactID;
- }
- }
- }
- //
- // build upsert for CRM contact
- m_CrmContact = Map();
- m_CrmContact.put("First_Name",m_XeroContact.get("FirstName"));
- // last name is mandatory for a CRM contact so we're going to put a placeholder one if this is not given
- v_CrmContactLastName = ifnull(m_XeroContact.get("LastName"),"-Unknown-");
- m_CrmContact.put("Last_Name",v_CrmContactLastName);
- m_CrmContact.put("Email",m_XeroContact.get("EmailAddress"));
- m_CrmContact.put("Phone",v_CrmPhone);
- m_CrmContact.put("Mobile",v_CrmMobile);
- m_CrmContact.put("Xero_Ref_ID",m_XeroContact.get("ContactID"));
- m_CrmContact.put("Xero_Updated",zoho.currenttime.toString("yyyy-MM-dd'T'HH:mm:ss",v_ZohoTimeZone));
- m_CrmContact.put("Mailing_Street",l_AddressStreet.toString(", "));
- m_CrmContact.put("Mailing_City",v_XeroAddressCity);
- m_CrmContact.put("Mailing_Zip",v_XeroAddressZip);
- m_CrmContact.put("Account_Name",v_CrmAccountID);
- // last name is mandatory, let's not bother if it wasn't provided
- if(b_CreateContact && v_CrmContactLastName != "-Unknown-")
- {
- r_CreateContact = zoho.crm.createRecord("Contacts",m_CrmContact);
- if(b_Debug)
- {
- info "Creating Primary Contact: " + r_CreateContact;
- }
- if(!isNull(r_CreateContact.get("id")))
- {
- v_CrmContactID = r_CreateContact.get("id");
- }
- //
- // create other contacts (retain the map and only change first name, last name, and email)
- for each m_OtherContact in m_XeroContact.get("ContactPersons")
- {
- m_CrmContact.put("First_Name",m_OtherContact.get("FirstName"));
- m_CrmContact.put("Last_Name",m_OtherContact.get("LastName"));
- m_CrmContact.put("Email",m_OtherContact.get("EmailAddress"));
- r_CreateContact2 = zoho.crm.createRecord("Contacts",m_CrmContact);
- if(b_Debug)
- {
- info "Creating Secondary Contact: " + r_CreateContact2;
- }
- }
- }
- }
- }
- //
- // if Xero record is more recently updated than the CRM one, then update the account
- if(d_XeroAccountLastUpdated >= d_CrmAccountLastUpdated)
- {
- r_UpdateCrmAccount = zoho.crm.updateRecord("Accounts",v_CrmAccountID,m_CrmAccount);
- r_UpdateCrmContact = zoho.crm.updateRecord("Contacts",v_CrmContactID,m_CrmContact);
- }
- //
- // add account/contact to the invoice
- m_UpsertCrmInvoice.put("Account_Name",v_CrmAccountID);
- m_UpsertCrmInvoice.put("Contact_Name",v_CrmContactID);
- //
- // -------------------------------- Invoice Line Items --------------------------------
- //
- // initializing
- l_CrmLineItems = List();
- //
- // loop through line items on the Xero invoice
- for each m_XeroLineItem in m_ThisInvoice.get("LineItems")
- {
- //
- // initialize
- v_CrmProductID = "";
- l_CrmProductAvailableTaxes = List();
- l_CrmProductAvailableTaxes.add("NONE");
- //
- // checking this is a valid line item and not an error message by it having an ItemCode
- v_CrmProductName = ifnull(m_XeroLineItem.get("ItemCode"),m_XeroLineItem.get("Description"));
- v_CrmProductName = if(isBlank(v_CrmProductName),ifnull(m_XeroLineItem.get("Item"),{"Name":"Product"}).get("Name"),"Product");
- if(!isBlank(v_CrmProductName))
- {
- v_CrmProductCode = ifnull(m_XeroLineItem.get("ItemCode"),"-Unknown-");
- v_CrmProductCodeSafe = zoho.encryption.urlEncode(v_CrmProductCode);
- v_CrmProductName = if(v_CrmProductName.length() >= 200,v_CrmProductName.subString(0,199),v_CrmProductName);
- v_CrmProductNameSafe = zoho.encryption.urlEncode(v_CrmProductName);
- v_SearchCriteria = "((Product_Code:equals:" + v_CrmProductCodeSafe + ")or(Product_Name:equals:" + v_CrmProductNameSafe + "))";
- l_SearchProducts = zoho.crm.searchRecords("Products",v_SearchCriteria,1,2,{"approved":"both"});
- if(b_Debug)
- {
- info "Searching CRM Products: " + v_SearchCriteria;
- }
- for each m_SearchProduct in l_SearchProducts
- {
- if(!isNull(m_SearchProduct.get("id")))
- {
- v_CrmProductID = m_SearchProduct.get("id");
- l_CrmProductAvailableTaxes = ifnull(m_SearchProduct.get("Tax"),List());
- if(b_Debug)
- {
- info "Available Taxes: " + l_CrmProductAvailableTaxes;
- }
- }
- }
- //
- // couldn't find it so let's create it
- m_CrmProduct = Map();
- //
- // some companies don't use the product lookup in Xero so you would need a placeholder product from CRM.
- if(!isNull(m_XeroLineItem.get("Item")))
- {
- v_CrmProductName = m_XeroLineItem.get("Item").get("Name");
- m_CrmProduct.put("Xero_Ref_ID",m_XeroLineItem.get("Item").get("ItemID"));
- m_CrmProduct.put("Product_Code",m_XeroLineItem.get("Item").get("Code"));
- }
- m_CrmProduct.put("Product_Name",v_CrmProductName);
- m_CrmProduct.put("Product_Active",true);
- m_CrmProduct.put("Description",m_XeroLineItem.get("Description"));
- m_CrmProduct.put("Unit_Price",m_XeroLineItem.get("UnitAmount"));
- //
- // map over tax (even if it's zero)
- v_XeroLineItemTaxAmount = ifnull(m_XeroLineItem.get("TaxAmount"),0).toDecimal();
- m_CrmProduct.put("Taxable",true);
- v_CrmTaxRateString = m_XeroLineItem.get("TaxType") + " - " + ifnull(m_ZohoTaxRates.get(m_XeroLineItem.get("TaxType")),"0.0") + " %";
- if(!l_CrmProductAvailableTaxes.contains(v_CrmTaxRateString))
- {
- l_CrmProductAvailableTaxes.add(m_XeroLineItem.get("TaxType"));
- }
- m_CrmProduct.put("Tax",l_CrmProductAvailableTaxes);
- //
- m_CrmProduct.put("Xero_Updated",zoho.currenttime.toString("yyyy-MM-dd'T'HH:mm:ss",v_ZohoTimeZone));
- //
- if(v_CrmProductID == "")
- {
- r_CreateCrmProduct = zoho.crm.createRecord("Products",m_CrmProduct);
- if(b_Debug)
- {
- info "Creating CRM Product: " + r_CreateCrmProduct;
- }
- if(!isNull(r_CreateCrmProduct.get("id")))
- {
- v_CrmProductID = r_CreateCrmProduct.get("id");
- }
- else if(r_CreateCrmProduct.get("code").equalsIgnoreCase("DUPLICATE_DATA"))
- {
- v_CrmProductID = r_CreateCrmProduct.get("details").get("id");
- if(b_Debug)
- {
- info "Duplicate CRM Product: Re-using " + v_CrmProductID;
- }
- }
- }
- else
- {
- //
- // update the product (mainly for new applicable taxes)
- r_UpdateCrmProduct = zoho.crm.updateRecord("Products",v_CrmProductID,m_CrmProduct);
- if(b_Debug)
- {
- info "Update CRM Product";
- info m_CrmProduct;
- info r_UpdateCrmProduct;
- }
- }
- //
- // let's do the rest of the line item (note that we are going to upsert using CRM API v8)
- m_CrmLineItem = Map();
- m_CrmLineItem.put("Product_Name",v_CrmProductID);
- m_CrmLineItem.put("Description",m_XeroLineItem.get("Description"));
- m_CrmLineItem.put("List_Price",m_XeroLineItem.get("UnitAmount"));
- m_CrmLineItem.put("Quantity",m_XeroLineItem.get("Quantity"));
- v_DiscountPercent = ifnull(m_XeroLineItem.get("DiscountRate"),0.0);
- v_DiscountAmount = ifnull(m_XeroLineItem.get("DiscountAmount"),0.0);
- if(v_DiscountPercent != 0)
- {
- // just qty vs unit excluding discount and tax
- v_LineItemTotal = m_XeroLineItem.get("Quantity") * m_XeroLineItem.get("UnitAmount");
- v_DiscountFactor = v_DiscountPercent / 100;
- v_DiscountAmount = v_LineItemTotal * v_DiscountFactor;
- }
- // tax even if it's zero
- l_CrmLineItemTax = List();
- m_CrmLineItemTax = Map();
- v_ZohoLineItemTaxPercent = ifnull(m_ZohoTaxRates.get(m_XeroLineItem.get("TaxType")),0.0).toDecimal();
- v_ZohoLineItemTaxPercent = v_ZohoLineItemTaxPercent * 1;
- m_CrmLineItemTax.put("percentage",v_ZohoLineItemTaxPercent);
- m_CrmLineItemTax.put("name",ifnull(m_XeroLineItem.get("TaxType"),"NONE"));
- m_CrmLineItemTax.put("value",m_XeroLineItem.get("TaxAmount"));
- l_CrmLineItemTax.add(m_CrmLineItemTax);
- m_CrmLineItem.put("Line_Tax",l_CrmLineItemTax);
- //
- m_CrmLineItem.put("Discount",v_DiscountAmount);
- l_CrmLineItems.add(m_CrmLineItem);
- }
- }
- //
- // if the CRM invoice already exists, we are going to upsert so we need to remove the current line items in the CRM invoice
- l_SearchInvoices = zoho.crm.searchRecords("Invoices","Xero_Ref_ID:equals:" + v_XeroInvoiceID);
- for each m_InvoiceResult in l_SearchInvoices
- {
- if(!isNull(m_InvoiceResult.get("id")))
- {
- for each m_ExistingLineItem in m_InvoiceResult.get("Product_Details")
- {
- m_MiniDeleteMe = Map();
- m_MiniDeleteMe.put("id",m_ExistingLineItem.get("id"));
- m_MiniDeleteMe.put("_delete",null);
- l_CrmLineItems.add(m_MiniDeleteMe);
- }
- }
- }
- //
- // add line items to the invoice
- m_UpsertCrmInvoice.put("Invoiced_Items",l_CrmLineItems);
- //
- // let's add the billing address retrieved earlier to the invoice
- m_UpsertCrmInvoice.put("Billing_Street",l_AddressStreet.toString(", "));
- m_UpsertCrmInvoice.put("Billing_City",v_XeroAddressCity);
- m_UpsertCrmInvoice.put("Billing_Code",v_XeroAddressZip);
- //
- // let's upsert
- m_Data = Map();
- m_Data.put("data",List({m_UpsertCrmInvoice}));
- m_Data.put("trigger",{"workflow","approval","blueprint"});
- r_UpsertInvoice = invokeUrl
- [
- url :"https://www.zohoapis.eu/crm/v8/Invoices/upsert"
- type :POST
- parameters:m_Data.toString()
- connection:"ab_crm"
- ];
- if(b_Debug)
- {
- info "Upserting Invoice: " + m_ThisInvoice.get("InvoiceNumber");
- info m_UpsertCrmInvoice;
- info r_UpsertInvoice;
- }
- l_ResponseData = ifnull(r_UpsertInvoice.get("data"),List());
- for each m_ResponseData in l_ResponseData
- {
- if(!isNull(m_ResponseData.get("code")))
- {
- v_Action = m_ResponseData.get("action");
- }
- info m_ResponseData.get("code");
- //
- // possible errors, output anyway
- b_Error = if(m_ResponseData.get("code").equalsIgnoreCase("SUCCESS"),false,true);
- if(b_Error)
- {
- info m_ThisInvoice.get("InvoiceNumber") + ": FAILED: " + m_ResponseData;
- info m_UpsertCrmInvoice;
- }
- }
- if(v_Action == "insert")
- {
- v_Count_Created = v_Count_Created + 1;
- }
- else if(v_Action == "update")
- {
- v_Count_Updated = v_Count_Updated + 1;
- }
- }
- }
- }
- }
- v_OutputMessage = "Created " + v_Count_Created + " and Updated " + v_Count_Updated + " from " + v_Count_FoundInXero;
- }
- return v_OutputMessage;
- }
The Tax Rates function
copyraw
string standalone.fn_Xero_MapTaxRates()
{
/* *******************************************************************************
Function: string standalone.fn_Xero_MapTaxRates()
Label: Fn - Xero - Map Tax Rates
Trigger: Standalone / On-Demand
Purpose: Function used to map Xero tax rates to Zoho ones
Inputs: -
Outputs: -
Date Created: 2025-10-13 (Ascent Business - Joel Lipman)
- Initial release
Date Modified: ???
- ???
More Information:
Ensure that all possible tax rates in Xero match those in Zoho (eg. 20.000, 17.500, 15.000, 10.000, 5.000, ...)
******************************************************************************* */
// init
v_Output = "";
m_OutputTaxRates = Map();
m_ZohoTaxRatesByName = Map();
m_ZohoTaxRatesByRate = Map();
l_ZohoCrmTaxRatesList = List();
v_XeroIntegrationRecordID = "123456000000789012";
//
r_IntegrationRecord = zoho.crm.getRecordById("Integrations",v_XeroIntegrationRecordID);
v_TenantID = r_IntegrationRecord.get("Tenant_ID");
v_AccessToken = standalone.fn_API_GetXeroAccessToken();
//
// get Zoho tax rates
r_TaxRates = invokeurl
[
url :"https://www.zohoapis.eu/crm/v8/org/taxes"
type :GET
connection:"ab_crm"
];
r_OrgTaxes = ifnull(r_TaxRates.get("org_taxes"),Map());
l_TaxRates = ifnull(r_OrgTaxes.get("taxes"),List());
//
for each m_TaxRate in l_TaxRates
{
if(!isNull(m_TaxRate.get("id")))
{
m_ZohoTaxRatesByName.put(m_TaxRate.get("name"),m_TaxRate.get("id"));
m_ZohoTaxRatesByRate.put(m_TaxRate.get("value").toDecimal().round(3).toString(),m_TaxRate.get("id"));
}
}
info m_ZohoTaxRatesByName;
info m_ZohoTaxRatesByRate;
//
// do Xero stuff
if(v_AccessToken != "")
{
m_Header = Map();
m_Header.put("Authorization","Bearer " + v_AccessToken);
m_Header.put("Accept","application/json");
m_Header.put("Xero-tenant-id",v_TenantID);
//
// get CRM invoice details
v_TaxRateEndpoint = "https://api.xero.com/api.xro/2.0/TaxRates";
r_XeroResponse = invokeurl
[
url :v_TaxRateEndpoint
type :GET
headers:m_Header
];
info r_XeroResponse;
l_TaxRates = ifnull(r_XeroResponse.get("TaxRates"),List());
for each m_ThisTaxRate in l_TaxRates
{
if(!isNull(m_ThisTaxRate.get("Name")))
{
v_ThisXeroTaxRateName = m_ThisTaxRate.get("Name");
v_ThisXeroTaxRateRef = m_ThisTaxRate.get("TaxType");
//
v_ThisXeroTaxRateRate = 0.0;
if(!isEmpty(m_ThisTaxRate.get("TaxComponents")))
{
for each m_TaxComponent in m_ThisTaxRate.get("TaxComponents")
{
v_ThisXeroTaxRateRate = m_TaxComponent.get("Rate").toDecimal().round(3).toString();
}
}
//
// map
if(!isNull(m_ZohoTaxRatesByName.get(v_ThisXeroTaxRateName)))
{
m_OutputTaxRates.put(v_ThisXeroTaxRateRef,v_ThisXeroTaxRateRate);
}
else if(!isNull(m_ZohoTaxRatesByName.get(v_ThisXeroTaxRateRef)))
{
m_OutputTaxRates.put(v_ThisXeroTaxRateRef,v_ThisXeroTaxRateRate);
}
else if(!isNull(m_ZohoTaxRatesByRate.get(v_ThisXeroTaxRateRate)))
{
m_OutputTaxRates.put(v_ThisXeroTaxRateRef,v_ThisXeroTaxRateRate);
}
//
// list to copy in Zoho (copy and paste)
m_ZohoCrmTaxRatesList = Map();
m_ZohoCrmTaxRatesList.put(v_ThisXeroTaxRateRef,v_ThisXeroTaxRateRate);
l_ZohoCrmTaxRatesList.add(m_ZohoCrmTaxRatesList);
}
}
info "Copy this list: ";
info l_ZohoCrmTaxRatesList;
v_Output = m_OutputTaxRates.toString();
}
return v_Output;
}
- string standalone.fn_Xero_MapTaxRates()
- {
- /* *******************************************************************************
- Function: string standalone.fn_Xero_MapTaxRates()
- Label: Fn - Xero - Map Tax Rates
- Trigger: Standalone / On-Demand
- Purpose: Function used to map Xero tax rates to Zoho ones
- Inputs: -
- Outputs: -
- Date Created: 2025-10-13 (Ascent Business - Joel Lipman)
- - Initial release
- Date Modified: ???
- - ???
- More Information:
- Ensure that all possible tax rates in Xero match those in Zoho (eg. 20.000, 17.500, 15.000, 10.000, 5.000, ...)
- ******************************************************************************* */
- // init
- v_Output = "";
- m_OutputTaxRates = Map();
- m_ZohoTaxRatesByName = Map();
- m_ZohoTaxRatesByRate = Map();
- l_ZohoCrmTaxRatesList = List();
- v_XeroIntegrationRecordID = "123456000000789012";
- //
- r_IntegrationRecord = zoho.crm.getRecordById("Integrations",v_XeroIntegrationRecordID);
- v_TenantID = r_IntegrationRecord.get("Tenant_ID");
- v_AccessToken = standalone.fn_API_GetXeroAccessToken();
- //
- // get Zoho tax rates
- r_TaxRates = invokeUrl
- [
- url :"https://www.zohoapis.eu/crm/v8/org/taxes"
- type :GET
- connection:"ab_crm"
- ];
- r_OrgTaxes = ifnull(r_TaxRates.get("org_taxes"),Map());
- l_TaxRates = ifnull(r_OrgTaxes.get("taxes"),List());
- //
- for each m_TaxRate in l_TaxRates
- {
- if(!isNull(m_TaxRate.get("id")))
- {
- m_ZohoTaxRatesByName.put(m_TaxRate.get("name"),m_TaxRate.get("id"));
- m_ZohoTaxRatesByRate.put(m_TaxRate.get("value").toDecimal().round(3).toString(),m_TaxRate.get("id"));
- }
- }
- info m_ZohoTaxRatesByName;
- info m_ZohoTaxRatesByRate;
- //
- // do Xero stuff
- if(v_AccessToken != "")
- {
- m_Header = Map();
- m_Header.put("Authorization","Bearer " + v_AccessToken);
- m_Header.put("Accept","application/json");
- m_Header.put("Xero-tenant-id",v_TenantID);
- //
- // get CRM invoice details
- v_TaxRateEndpoint = "https://api.xero.com/api.xro/2.0/TaxRates";
- r_XeroResponse = invokeUrl
- [
- url :v_TaxRateEndpoint
- type :GET
- headers:m_Header
- ];
- info r_XeroResponse;
- l_TaxRates = ifnull(r_XeroResponse.get("TaxRates"),List());
- for each m_ThisTaxRate in l_TaxRates
- {
- if(!isNull(m_ThisTaxRate.get("Name")))
- {
- v_ThisXeroTaxRateName = m_ThisTaxRate.get("Name");
- v_ThisXeroTaxRateRef = m_ThisTaxRate.get("TaxType");
- //
- v_ThisXeroTaxRateRate = 0.0;
- if(!isEmpty(m_ThisTaxRate.get("TaxComponents")))
- {
- for each m_TaxComponent in m_ThisTaxRate.get("TaxComponents")
- {
- v_ThisXeroTaxRateRate = m_TaxComponent.get("Rate").toDecimal().round(3).toString();
- }
- }
- //
- // map
- if(!isNull(m_ZohoTaxRatesByName.get(v_ThisXeroTaxRateName)))
- {
- m_OutputTaxRates.put(v_ThisXeroTaxRateRef,v_ThisXeroTaxRateRate);
- }
- else if(!isNull(m_ZohoTaxRatesByName.get(v_ThisXeroTaxRateRef)))
- {
- m_OutputTaxRates.put(v_ThisXeroTaxRateRef,v_ThisXeroTaxRateRate);
- }
- else if(!isNull(m_ZohoTaxRatesByRate.get(v_ThisXeroTaxRateRate)))
- {
- m_OutputTaxRates.put(v_ThisXeroTaxRateRef,v_ThisXeroTaxRateRate);
- }
- //
- // list to copy in Zoho (copy and paste)
- m_ZohoCrmTaxRatesList = Map();
- m_ZohoCrmTaxRatesList.put(v_ThisXeroTaxRateRef,v_ThisXeroTaxRateRate);
- l_ZohoCrmTaxRatesList.add(m_ZohoCrmTaxRatesList);
- }
- }
- info "Copy this list: ";
- info l_ZohoCrmTaxRatesList;
- v_Output = m_OutputTaxRates.toString();
- }
- return v_Output;
- }
Category: Zoho :: Article: 913



Add comment