What?
This is a quick article to demonstrate how to compare two datetime values with the timezone specified.

Why?
A client's ZohoCRM had a different timezone setting than the user a script would be run as. The time difference was just one hour but this caused problems if comparing two datetime values. In this particular case, we needed to check on the expiry of an access token used in OAuth2.0 for an API.

How?
All with deluge but we will split the date and time value obtained from a CRM field and compare it to the current time combined with a timezone. In this particular case, we will switch the current time to Europe/London (You can use an abbreviation such as GMT but this doesn't seem to check if daylight savings is in effect):

Xero API - Standard PHP/cURL Create Invoice

What?
This is an article documenting a generic script that can be used to push an invoice to the demo Xero environment. Following the steps below will connect you to the Demonstration environment of Xero at no cost to you the developer.

How?
Similar to my ZohoAPI script the process is:
  1. Start with a HTML form to add your client ID/client Secret/scopes and Redirect URI. These get stored in a temporary file to retrieve later.
  2. Then you get a button that will return the "CODE" variable via GET (URL). Clicking on this will get the CODE variable and use it to generate a refresh token.
  3. Then with the refresh token generate an access token.
  4. Then with the access token get a connection "TenantID".
  5. Then with the tenantID we can retrieve records and/or finally create an invoice (accounts receivables).
Xero Signup
So first you need to be a user of Xero. For this demonstration, we will sign up to the demo company that Xero has already setup:
  1. Sign up for a free account
    Xero Signup Step 1
  2. You will be emailed an activation link. Copy the link and paste in a browser using a dedicated profile. I am using "Guest" as per my screenshots but you should use the profile that your Xero account will be auto logged in.
  3. Set a password, phone number and click on "Activate Your Account"
    Xero Signup Step 2
  4. You'll be asked to setup your organization but as a developer, just click on the "Try the demo company" link.
    Xero Signup Step 3
  5. Done. Welcome to Xero!
App Registration
As per most third-party APIs, you need to register your app:
  1. Go to the Xero Developer My Apps page
  2. Cilck on "Add App"
  3. Complete the form as appropriate, in my case:
    App Registration Step 1
  4. Generate and copy to an external file the Client ID and Client Secret (not shown again):App Registration Step 2
The App
The following are 2 generic scripts that can be used for any app and should be uploaded to your server that will be executing the scripts.

The redirect_uri.php file I have merely contains the following line:
<?php
	header('Location: https://api.joellipman.com/xero/get_tokens.php?' . http_build_query($_GET));
?>
This simply redirects to our main script and forwards all the variables sent to the redirect_uri script.

The get_tokens.php file is our main file that initially displays a HTML form to enter the client ID/secret and scope:
<?php
/*
	---------------------------------------------------------------------------
	Xero Authorization via OAuth2.0 for REST API v2
	---------------------------------------------------------------------------

    Xero API v2:	https://go.xero.com/Dashboard/
                    https://developer.xero.com/myapps/details?appId=2698e616-ca1e-44ac-9245-5cc27224f21d

	Documentation:	https://developer.xero.com/documentation/oauth2/auth-flow

    Available Scopes
        oauth       offline_access, openid, profile, email
        accounting  accounting.transactions, accounting.transactions.read,
                    accounting.reports.read, accounting.journals.read,
                    accounting.settings, accounting.settings.read,
                    accounting.contacts, accounting.contacts.read,
                    accounting.attachments, accounting.attachments.read
        payroll(AU) payroll.employees, payroll.employees.read, payroll.payruns,
                    payroll.payruns.read, payroll.payslip, payroll.payslip.read,
                    payroll.timesheets, payroll.timesheets.read, payroll.settings,
                    payroll.settings.read
        payroll(UK) payroll.employees, payroll.employees.read, payroll.payruns,
                    payroll.payruns.read, payroll.payslip, payroll.payslip.read,
                    payroll.timesheets, payroll.timesheets.read, payroll.settings,
                    payroll.settings.read
        payroll(NZ) payroll.employees, payroll.employees.read, payroll.payruns,
                    payroll.payruns.read, payroll.payslip, payroll.payslip.read,
                    payroll.timesheets, payroll.timesheets.read, payroll.settings,
                    payroll.settings.read
        files       files, files.read
        assets      assets, assets.read
        projects    projects, projects.read
        payments    paymentservices
        bank feed   bankfeeds
*/
header("Content-Type: text/html");
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// init
$b_DemoMode = true;
$b_CreateInvoice = false;
$v_AccessTokenPath = $v_RefreshTokenPath = "";
$a_CheckFields = $a_GrantFields = $a_Header = $a_Payload = array();
$v_RootFolder = "../App_Data/ab";  // must be a writeable folder on your server
$v_TmpFolder = $v_RootFolder . "/tmp/xero";
$v_DelFolder = $v_TmpFolder . "/delete_me";
$v_WorkingFolder = $v_TmpFolder;
$v_TmpFile = "grant_info_" . date("YmdH") . ".dat";  // stores the initial form details
$v_AuthEndpoint = "https://login.xero.com/identity/connect/authorize";
$v_TokenEndpoint = "https://identity.xero.com/connect/token";

// your own values here or leave blank
$v_PresetClientID = "";  // can be overwritten in initial form
$v_PresetClientSecret = "";  // can be overwritten in initial form
$v_ExistingInvoiceID = "";  // id of 1 invoice record to test data retrieval
$v_YourXeroContactID = "";  // id of 1 contact record to create invoice in this contacts name
$v_RedirectUri = "https://" . $_SERVER['SERVER_NAME'] . "/xeroapi/redirect_uri.php";  // has to be https protocol
$v_Scope = "offline_access openid profile email accounting.transactions accounting.contacts accounting.attachments accounting.settings.read files assets";  // can be overwritten in initial form

// create temp folder if not exists
if (!file_exists($v_TmpFolder)) {
    mkdir($v_TmpFolder, 0777, true);
}
if (!file_exists($v_DelFolder)) {
    mkdir($v_DelFolder, 0777, true);
}

// if client id in url then get and store
if(isset($_GET['client_id'])){
    if($_GET['client_id'] != ""){
        file_put_contents($v_TmpFolder . "/" . $v_TmpFile, json_encode($_GET, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
    }
}

// begin HTML output
echo "<html><head><title>JoelLipman - Xero API v2 Tokenizer</title><style>";
$v_AppStyle = "body{font-family:Arial,Verdana,Sans-serif;background-color:#ccc;}select,option,input.txt{padding:5px 10px;outline:0;border:1px solid#ccc;border-radius:5px;}input.btn{padding:10px 20px;outline:0;color: #fff;border-color: #5cac00;background-color: #7bd000;text-shadow: 0 1px rgba(0,0,0,0.1);background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiP…dpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==);background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #7bd000),color-stop(100%, #6bb101));background: -webkit-linear-gradient(#7bd000,#6bb101);background: -webkit-linear-gradient(#7bd000, #6bb101);background: linear-gradient(#7bd000,#6bb101);border-radius:2px;cursor:pointer;}input.txt{width:400px;}form{margin:20px;}h3{margin:0;}select{float:right;}td{padding:2px 5px;}label{display:inline-block;width:100px;}div.panel{background-color:#fff;border-radius:10px;margin:20px 10px;padding:20px 10px;-webkit-box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);-moz-box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);}form{margin:0;padding:0;}#copyright{bottom:0;margin:0 auto;text-align:center;width:97%;margin-bottom:10px;}#copyright a{color:#999;text-decoration:none;font-size:10pt;line-height:20px;}#menu_bottom a{color:#f00;text-decoration:none;bottom:30;font-size:10pt;line-height:20px;}.centered{text-align:center;margin:0 auto;}h5{color:#999;font-weight:100;font-size:10pt;margin-bottom:5px;margin-top:10px;}tt.source{color:#fff;background-color:#000;}tt.target{background-color:yellow;}tt.bold{color:red;font-weight:700;}ul{margin:10px 0 0 85px;font-size:75%;color:#666;padding:0;}pre{margin:0;}.match{background-color:red;color:#fff;}";
echo $v_AppStyle . "</style></head><body>";
if(isset($_GET['scope']) && file_exists($v_TmpFolder . "/" . $v_TmpFile)){
    //
    // 0. initialize vars
    //
    echo "<div class='panel'>";
    echo "<h3>0. INITIALIZE...</h3>";
    echo "<h5>Info:</h5>";
    if(file_exists($v_TmpFolder . "/" . $v_TmpFile) && isset($_GET['code'])){
        $m_GrantInfo = json_decode(file_get_contents($v_TmpFolder . "/" . $v_TmpFile), true);
        $a_CheckFields['client_id'] = $m_GrantInfo['client_id'];
        $a_CheckFields['client_secret'] = $m_GrantInfo['client_secret'];
        $a_CheckFields['scope'] = $m_GrantInfo['scope'];
        $a_CheckFields['redirect_uri'] = $v_RedirectUri;
        rename($v_TmpFolder . "/" . $v_TmpFile, $v_DelFolder . "/" . $v_TmpFile);
    }else{
        $a_CheckFields['client_id'] = $_GET['client_id'];
        $a_CheckFields['client_secret'] = $_GET['client_secret'];
        $a_CheckFields['scope'] = $_GET['scope'];
        $a_CheckFields['redirect_uri'] = $v_RedirectUri;
    }
    $a_InitFields['client_id'] = $a_CheckFields['client_id'];
    $a_InitFields['client_secret'] = $a_CheckFields['client_secret'];
    $a_InitFields['scope'] = $a_CheckFields['scope'];
    $a_InitFields['redirect_uri'] = $a_CheckFields['redirect_uri'];
    $a_Info = array();
    $a_Info['api']['header'] = false;
    $a_Info['api']['ssl_check'] = false;
    $a_Info['demo'] = $b_DemoMode ? "<tt class='bold'>true</tt>" : "<tt class='bold'>false</tt>";
    echo "<pre>" . json_encode(array_merge($a_InitFields,$a_Info), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
    //
    // 1. get grant token
    //
    echo "</div><div class='panel'>";
    echo "<h3>1. GRANT TOKEN...</h3>";
    if($a_CheckFields['client_id']!="")
    {
        $v_GrantTokenPath = $v_RootFolder . "/" . $a_CheckFields['client_id'] . "/grant_token.dat";
        $v_RefreshTokenPath = $v_RootFolder . "/" . $a_CheckFields['client_id'] . "/refresh_token.dat";
        $v_AccessTokenPath = $v_RootFolder . "/" . $a_CheckFields['client_id'] . "/access_token.dat";
        $v_WorkingFolder = $v_RootFolder . "/" . $a_CheckFields['client_id'];
        if (!file_exists($v_WorkingFolder)) {
            mkdir($v_WorkingFolder, 0777, true);
        }
    }
    echo "<h5>Info:</h5>";
    $a_Info = array();
    $a_Info['api']['endpoint'] = $v_AuthEndpoint;
    $a_Info['api']['method'] = "GET";
    $a_Info['api']['header'] = false;
    $a_Info['api']['ssl_check'] = false;
    $a_Info['expires'] = "<tt class='bold'>" . "in 1 to 10 minutes" . "</tt>";
    $a_Info['date_created'] = file_exists($v_GrantTokenPath) ? date("Y-m-d H:i:s P", filectime($v_GrantTokenPath)) : date("Y-m-d H:i:s P");
    $a_Info['date_modified'] = file_exists($v_GrantTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_GrantTokenPath)) : date("Y-m-d H:i:s P");
    $a_Info['date_expires'] = file_exists($v_GrantTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_GrantTokenPath) + 300) : date("Y-m-d H:i:s P", strtotime(time() + 300));
    echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
    echo "<h5>Request (GET):</h5>";
    $a_Request = array();
    $a_Request['client_id'] = $a_CheckFields['client_id'];
    $a_Request['redirect_uri'] = $a_CheckFields['redirect_uri'];
    $a_Request['scope'] = $a_CheckFields['scope'];
    $a_Request['response_type'] = "code";
    $a_Request['access_type'] = "offline";
    $a_Request['prompt'] = "consent";
    $a_Request['state'] = "123";
    echo "<pre>" . json_encode($a_Request, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
    $v_GrantTokenUrl = $v_AuthEndpoint . "?" . http_build_query($a_Request);
    if(isset($_GET['code']))
    {
        echo "<h5>Response (GET):</h5>";
        $a_Response = array();
        $a_Response['state'] = $_GET['state'];
        $a_Response['code'] = $_GET['code'];
        $a_Response['scope'] = $_GET['scope'];
        $a_Response['session_state'] = $_GET['session_state'];
        if($v_GrantTokenPath != "" && $b_DemoMode){
            file_put_contents($v_GrantTokenPath, json_encode($a_Response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        foreach($a_Response as $v_Key => $v_Value){
            if($v_Key == "code"){
                $a_Response['code'] = "<tt class='source'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        //
        // 2. get refresh token
        //
        echo "</div><div class='panel'>";
        echo "<h3>2. REFRESH TOKEN...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = $v_TokenEndpoint;
        $a_Info['api']['method'] = "POST";
        $a_Info['api']['header'] = false;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['expires'] = "<tt class='bold'>" . "The refresh token expires after everytime it has been used." . "</tt>";
        $a_Info['date_created'] = file_exists($v_RefreshTokenPath) ? date("Y-m-d H:i:s P", filectime($v_RefreshTokenPath)) : date("Y-m-d H:i:s P");
        $a_Info['date_modified'] = file_exists($v_RefreshTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_RefreshTokenPath)) : date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (POST):</h5>";
        $a_Payload = array();
        $a_Payload['code'] = $_GET['code'];
        $a_Payload['client_id'] = $a_CheckFields['client_id'];
        $a_Payload['client_secret'] = $a_CheckFields['client_secret'];
        $a_Payload['redirect_uri'] = $v_RedirectUri;
        $a_Payload['grant_type'] = "authorization_code";
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $v_TokenEndpoint,
            CURLOPT_CUSTOMREQUEST => "POST",
            CURLOPT_POSTFIELDS => $a_Payload,
            CURLOPT_HEADER => 0,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_RefreshCurl = curl_exec($h_Curl);
        curl_close($h_Curl);
        foreach($a_Payload as $v_Key => $v_Value){
            if($v_Key == "code"){
                $a_Payload['code'] = "<tt class='target'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (POST):</h5>";
        $a_RefreshResponse = json_decode($v_RefreshCurl, true);
        if($v_RefreshTokenPath != ""){
            file_put_contents($v_RefreshTokenPath, json_encode($a_RefreshResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        $v_RefreshToken = $a_RefreshResponse['refresh_token'];
        foreach($a_RefreshResponse as $v_Key => $v_Value){
            if($v_Key == "refresh_token"){
                $a_RefreshResponse['refresh_token'] = "<tt class='source'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_RefreshResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        //
        // 3. get access token
        //
        echo "</div><div class='panel'>";
        echo "<h3>3. ACCESS TOKEN...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = $v_TokenEndpoint;
        $a_Info['api']['method'] = "POST";
        $a_Info['api']['header'] = false;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['expires'] = "<tt class='bold'>" . "in 30 minutes after creation" . "</tt>";
        $a_Info['date_created'] = file_exists($v_AccessTokenPath) ? date("Y-m-d H:i:s P", filectime($v_AccessTokenPath)) : date("Y-m-d H:i:s P");
        $a_Info['date_modified'] = file_exists($v_AccessTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_AccessTokenPath)) : date("Y-m-d H:i:s P");
        $a_Info['date_expires'] = file_exists($v_AccessTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_AccessTokenPath) + 1800) : date("Y-m-d H:i:s P", strtotime(time() + 1800));
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (POST):</h5>";
        $a_Payload = array();
        $a_Payload['refresh_token'] = $v_RefreshToken;
        $a_Payload['client_id'] = $a_CheckFields['client_id'];
        $a_Payload['client_secret'] = $a_CheckFields['client_secret'];
        $a_Payload['redirect_uri'] = $v_RedirectUri;
        $a_Payload['grant_type'] = "refresh_token";
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $v_TokenEndpoint,
            CURLOPT_CUSTOMREQUEST => "POST",
            CURLOPT_POSTFIELDS => $a_Payload,
            CURLOPT_HEADER => 0,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_AccessCurl = curl_exec($h_Curl);
        curl_close($h_Curl);
        $a_AccessResponse = json_decode($v_AccessCurl, true);
        if($v_AccessTokenPath != ""){
            file_put_contents($v_AccessTokenPath, json_encode($a_AccessResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        foreach($a_Payload as $v_Key => $v_Value){
            if($v_Key == "refresh_token"){
                $a_Payload['refresh_token'] = "<tt class='target'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (POST):</h5>";
        $v_AccessToken = $a_AccessResponse['access_token'];
        foreach($a_AccessResponse as $v_Key => $v_Value){
            if($v_Key == "access_token"){
                $a_AccessResponse['access_token'] = "<tt class='source'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_AccessResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        //
        // 4. get connections
        //
        echo "</div><div class='panel'>";
        echo "<h3>4. CONNECTIONS...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = "https://api.xero.com/connections";
        $a_Info['api']['method'] = "GET";
        $a_Info['api']['header'] = true;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['date'] = date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (GET):</h5>";
        $a_Header = array();
        $a_Header[] = "Authorization: Bearer " . $v_AccessToken;
        $a_Header[] = "Content-Type: application/json;charset=UTF-8";
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $a_Info['api']['endpoint'],
            CURLOPT_CUSTOMREQUEST => "GET",
            CURLOPT_HEADER => 1,
            CURLOPT_HTTPHEADER => $a_Header,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_ConnectionCurl = curl_exec($h_Curl);
        $a_Header[0] = "Authorization: Bearer <tt class='target'>" . $v_AccessToken . "</tt>";
        echo "<pre>" . json_encode($a_Header, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (GET):</h5>";
        if(stripos($v_ConnectionCurl, '"tenantId"')>=0){
            $v_ConnectionStart = stripos($v_ConnectionCurl, '[');
            $v_ConnectionStr = trim(substr($v_ConnectionCurl, $v_ConnectionStart));
            $a_ConnectionResponse = json_decode($v_ConnectionStr, true);
            foreach($a_ConnectionResponse as $v_Key => $v_Value){
                if($v_Key == "tenantId"){
                    $v_TenantID = $a_ConnectionResponse[0]['tenantId'];
                    $a_ConnectionResponse[0]['tenantId'] = "<tt class='source'>" . $a_ConnectionResponse[0]['tenantId'] . "</tt>";
                }
            }
            echo "<pre>" . json_encode($a_ConnectionResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }else{
            echo "<pre>" . $v_ConnectionCurl . "</pre>";
        }
        //
        // 5. get data sample (first page of invoices)
        //
        echo "</div><div class='panel'>";
        echo "<h3>5. DATA SAMPLE...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = "https://api.xero.com/api.xro/2.0/Invoices/<tt class='match'>" . $v_ExistingInvoiceID . "</tt>";
        $v_ApiEndpoint = "https://api.xero.com/api.xro/2.0/Invoices/" . $v_ExistingInvoiceID;
        $a_Info['api']['method'] = "GET";
        $a_Info['api']['header'] = true;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['date'] = date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (GET):</h5>";
        $a_Header = array();
        $a_Header[] = "Authorization: Bearer " . $v_AccessToken;
        $a_Header[] = "Accept: application/json";
        $a_Header[] = "Xero-tenant-id: " . $v_TenantID;
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $v_ApiEndpoint,
            CURLOPT_CUSTOMREQUEST => "GET",
            CURLOPT_HEADER => 1,
            CURLOPT_HTTPHEADER => $a_Header,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_DataCurl = curl_exec($h_Curl);
        foreach($a_Header as $v_Value){
            if(stripos($v_Value, "Xero-tenant-id")>=0){
                $a_Header[2] = str_replace("Xero-tenant-id: ","Xero-tenant-id: <tt class='target'>", $v_Value) . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Header, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (GET):</h5>";
        if(stripos($v_DataCurl, '"ProviderName"')>0){
            $v_DataStart = stripos($v_DataCurl, '{');
            $v_DataStr = trim(substr($v_DataCurl, $v_DataStart));
            $a_DataResponse = json_decode($v_DataStr, true);
            foreach($a_DataResponse as $v_Key => $v_Value){
                if($v_Key == "Invoices"){
                    $a_Invoices = $v_Value;
                    foreach($a_Invoices as $v_Key2 => $v_Value2){
                        if($v_Key2 == "InvoiceID"){
                            $a_DataResponse['Invoices'][0]['InvoiceID'] = "<tt class='match'>" . $a_DataResponse['Invoices'][0]['InvoiceID'] . "</tt>";
                        }
                    }
                }
            }
            echo "<pre>" . json_encode($a_DataResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }else{
            echo "<pre>" . $v_DataCurl . "</pre>";
        }
        //
        // 6. create sales invoice (receivables)
        //
        echo "</div><div class='panel'>";
        echo "<h3>6. PUSH SALES INVOICE...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = "https://api.xero.com/api.xro/2.0/Invoices";
        $a_Info['api']['method'] = "POST";
        $a_Info['api']['header'] = true;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['date'] = date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (POST):</h5>";
        $a_Header = array();
        $a_Header[] = "Authorization: Bearer " . $v_AccessToken;
        $a_Header[] = "Accept: application/json";
        $a_Header[] = "Xero-tenant-id: " . $v_TenantID;
        echo "<pre>" . json_encode($a_Header, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        $a_Payload = array();
        $a_Payload['Type'] = "ACCREC";
        $a_Payload['Contact']['ContactID'] = $v_YourXeroContactID ;
        $a_Payload['Date'] = date("Y-m-d");
        $a_Payload['DueDate'] = date("Y-m-d", strtotime("+1 day"));
        // sample 1 line item: enter your own description, quantity, unit amount and discount rate
        $a_LineItems = array();
        $a_LineItems[0]['Description'] = "Consulting services as agreed (20% off standard rate)";
        $a_LineItems[0]['Quantity'] = "5";
        $a_LineItems[0]['UnitAmount'] = "199.99";
        $a_LineItems[0]['AccountCode'] = "200";
        $a_LineItems[0]['DiscountRate'] = "10";
        $a_Payload['LineItems'] = $a_LineItems;
        $v_DataCurl = "";
        if($b_CreateInvoice){
            $h_Curl=curl_init();
            $a_CurlOptions = array(
                CURLOPT_URL => $a_Info['api']['endpoint'],
                CURLOPT_HEADER => 1,
                CURLOPT_HTTPHEADER => $a_Header,
                CURLOPT_POST => 1,
                CURLOPT_POSTFIELDS => json_encode($a_Payload),
                CURLOPT_VERBOSE => 0,
                CURLOPT_RETURNTRANSFER => 1,
                CURLOPT_SSL_VERIFYPEER => 0,
                CURLOPT_SSL_VERIFYHOST => 0
            );
            curl_setopt_array($h_Curl, $a_CurlOptions);
            $v_DataCurl = curl_exec($h_Curl);
        }
        echo "<pre>" . json_encode($a_Payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (POST):</h5>";
        if($b_CreateInvoice){
            if(stripos($v_DataCurl, '"ProviderName"')>0){
                $v_DataStart = stripos($v_DataCurl, '{');
                $v_DataStr = trim(substr($v_DataCurl, $v_DataStart));
                $a_DataResponse = json_decode($v_DataStr, true);
                foreach($a_DataResponse as $v_Key => $v_Value){
                    if($v_Key == "Invoices"){
                        $a_Invoices = $v_Value;
                        foreach($a_Invoices as $v_Key2 => $v_Value2){
                            if($v_Key2 == "InvoiceID"){
                                $a_DataResponse['Invoices'][0]['InvoiceID'] = "<tt class='match'>" . $a_DataResponse['Invoices'][0]['InvoiceID'] . "</tt>";
                            }
                        }
                    }
                }
                echo "<pre>" . json_encode($a_DataResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
            }else{
                echo "<pre>" . $v_DataCurl . "</pre>";
            }
        }else{
            $a_Info = array();
            $a_Info['MODE'] = "Test";
            $a_Info['Request'] = "Do NOT create invoice";
            echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }
        echo "</div>";
    }
    else
    {
        echo "<br /><form>";
        echo '<input type="button" value="Click here to open App Permissions »" onclick="javascript:top.location.href=\'' . $v_GrantTokenUrl . '\';" class="btn" style="margin-left: 70px;" />';
        echo "</form>
        <ul>
        <li>Login as a Xero User with access to the API</li>
        <li>Read and click on accept to allow the app access.</li>
        </ul><br />";
    }

    // END
    echo "</div><div class='panel'>";
    echo "<h5>Script finished.</h5>";
    echo "<div id='menu_bottom' style='position:normal'><a href='./get_tokens.php' target='_top'>» Start Over</a></div></div>";
    echo "<div id='copyright' style='position:normal'><a href='//joellipman.com/' target='_top'>Copyright © ".date("Y")." Joel Lipman Ltd</a></div></div>";
}else{
    echo "
<div class='panel'><form method='get' action='get_tokens.php'>
    <h3>Configure App</h3>
    <table>
    <tr><td colspan='2' class='centered'><br /><input type='button' value='Open Zoho App Registration' onclick='javascript:openAppConsole();' class='btn' /></td></tr>
    <tr><td colspan='2'> </td></tr>
    <tr><td>Client ID</td><td><input name='client_id' value='" . $v_PresetClientID . "' class='txt' onclick='this.select();' /></td></tr>
    <tr><td>Client Secret</td><td><input name='client_secret' value='" . $v_PresetClientSecret . "' class='txt' onclick='this.select();' /></td></tr>
    <tr><td>Scopes</td><td><input name='scope' value='" . $v_Scope . "' class='txt' onclick='this.select();' /></td></tr>
    <tr><td>Redirect URI</td><td><input name='redirect_uri' value='" . $v_RedirectUri . "' class='txt' onclick='this.select();' /></td></tr>
    <tr><td colspan='2' style='text-align:center;'><input type='submit' name='submit' value='Get Grant Token' class='btn' /></td></tr>
    </table>
</form></div>
<script>
function openAppConsole() {
    var e = document.getElementById('tldSelect');
    var strOption = e.options[e.selectedIndex].value;
    window.open('https://developer.xero.com/myapps', '_blank', 'location=yes,height=600,width=800,scrollbars=yes,status=yes');
}
</script>
";
    echo "<div id='copyright' style='position:fixed'><a href='//joellipman.com/' target='_top'>Copyright © ".date("Y")." Joel Lipman Ltd</a></div>";
}
echo "</body></html>";


Usage:
I've censored any keys and identifying data in the following screenshots but yours will be different values anyway.
  1. Modify the redirect_uri.php file with the web url of your get_tokens.php file.
  2. Modify the get_tokens.php file as follows:
    • Change $b_CreateInvoice to true if you want the script to create an invoice in Xero's Demo Company (leave as "false" if you are still testing authorization and data retrieval).
    • Change $v_RootFolder to a writeable folder on your server where tokens will be stored.
    • Change $v_PresetClientID to the Client ID used for your app (or leave blank and enter it into the HTML form)
    • Change $v_PresetClientSecret to Client Secret for your app (or leave blank and enter into the initial HTML form)
    • Change $v_ExistingInvoiceID to a sample invoice ID in the Xero Demo Company instance (used for testing data retrieval of 1 invoice record). Should be a hexadecimal value like: 7946c88d-d19b-428e-88d3-8622ef656b0c
    • Change $v_YourXeroContactID to a sample customer ID that we will create an invoice for (used for creating 1 invoice record). Should be a hexadecimal value like: 2b818f62-bfb8-4901-9d80-9d6afd152e9c
    • Change $v_RedirectURI to the web URL of your redirect_uri.php file (can be overwritte in the HTML form)
    • Change $v_Scope if you want but the current value as per this script is enough to carry out this demonstration.
  3. Upload both scripts to the same folder on your server.
  4. Browse to the get_tokens.php file with your Xero login profile.
  5. Enter your Client ID and Client Secret in the initial HTML form and submit the form by clicking on "Get Grant Token":
    App Usage Step 1
  6. Click on the button "Click here to open app permissions":
    App Usage Step 2
  7. Xero will now ask you to allow your app to access data:
    App Usage Step 3
  8. After which you will be redirected back to our script:
    App Usage Step 4
    Legend:
    1. This panel displays what was submitted in the initial HTML form.
    2. This panel displays what was sent to get the Grant Token (or CODE variable) and highlight in black the value we will use for the next request.
    3. This panel displays our request to get a Refresh Token (highlighted in black).
    4. This panel displays our request to get an Access Token (highlighted in black).
    5. This panel displays our connection request to get the TenantID (highlighted in black).
    6. Using both the Access Token and the Tenant ID, we can retrieve 1 invoice record.
    7. Using both the Access Token and the Tenant ID, we can push an invoice into Xero.

Additional Note(s):
  • You may note that I haven't followed the documentation when it comes to including the Client ID and Client Secret into the header (base64encode). Maybe this will be necessary at some point but I had adapted the above script to Xero from Zoho and it worked... So the access token is added to the header and a standard Authorization Bearer request.
  • The initial HTML form in get_tokens.php will create a file called grant_info.dat in your token folder. This is used just to cache the HTML values you submitted. You should not have 2 developers using this same script, same app in the same Xero instance at the same time as only 1 grant_info.dat file is created and overwritten whenever the script is executed.
  • As of today 10th January 2020, Xero still have not resolved the issue that the refresh_token expires immediately after use and is overwritten with another one. You will need to store that one but everytime you use it and generate another access/refresh token, you will need to store the new value in a file for later use.
  • Xero requires this is done over the HTTPS protocol (HTTP Secure). However I have tested this on client's servers that do not have SSL. Run the script and when prompted that the target site is not secure, click on "Advanced" and "Proceed Anyway".

Development/Testing/Demo Company to LIVE/Production/Your Company
So if you followed the above instructions the app you have just made will create invoices in the "Demo Company" instance of the client's Xero Instance. Let's say you've made it all work swimmingly and tested thoroughly, you will now want to push the invoices to the client's LIVE Xero instance. The process is quite simple, you need to create a new app and when authorizing with a Xero login, you will be asked which company you want the app to access as per the following screenshot:
From Demo Company to Production


Source(s):

Applies To:
  • MS Windows 10 Pro v10.0.18362 Build 18362 (64-bit)
  • Google Chrome Browser v79.0.3945.88 (Official Build) (64-bit)
  • AutoHotkey v1.1.30.01
What?
This is an article to create a standalone application which lists all the Google Chrome Profiles on your Windows 10 workstation in alphabetical order.

Why?
This program will be redundant if Google ever update their Chrome browser to list the multiple profiles in alphabetical order like it used to be... but to date they have not. So I made a program that lists the profiles in alphabetical order and can be double-clicked to open the Chrome browser with that profile.

How?
So you will need to be able to run AutoHotkey or create executables from an AHK file.

What?
This is a quick note on how to reduce a whole bunch of CSS into a single line without unnecessary spaces and new lines.

Why?
What I have:

#copyright a{
    margin: 10px 0 0 85px;
    box-shadow: 5px 5px 5px 0px rgba(51, 51, 51, 0.3);
}

What I want:

#copyright a{margin:10px 0 0 85px;box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);}


How?
So I'm doing this with a regular expression to get rid of newlines:

$v_AppStyle = "
#copyright a{
    margin: 10px 0 0 85px;
    box-shadow: 5px 5px 5px 0px rgba(51, 51, 51, 0.3);
}";
$v_AppStyleFormatted = preg_replace('/\s+/', ' ', $v_AppStyle);

and a few str_replace arrays:

// exceptions
$a_ReplaceFrom1 = array("px ", "0 ", " a");
$a_ReplaceTo1 = array("px?", "0?", "?a");
$v_AppStyleFormatted = str_replace($a_ReplaceFrom1, $a_ReplaceTo1, $v_AppStyleFormatted);

// replace all spaces to empty and replace question marks back to spaces
$a_ReplaceFrom2 = array(" ", "?");
$a_ReplaceTo2 = array("", " ");
$v_AppStyleFormatted = str_replace($a_ReplaceFrom2, $a_ReplaceTo2, $v_AppStyleFormatted);
echo $v_AppStyleFormatted;

What?
Thought I'd put an article here to remind me how to make the retrieval of a record case-insensitive.

Why?
Consider that I have the following creator table:
Product_Name   Product_SKU   
-------------- -------------
MyProduct1     TEST01
Myproduct2     TEST02
myproduct3     TEST03

I'm trying to insert a record for a new product if it doesn't exist in the table but if it does exist then to simply skip adding the product.

What?
An article on setting a date field to either the first Monday of the next month or to the last Tuesday of the current/next month. What I mean by the last Tuesday is if the last Tuesday of the month is before the current date, then set it to the last Tuesday of next month.

Why?
This was a request by a client who instead of specifying the 1st of every month, or 15th of each month, to say the first Monday of the month (ignoring bank holidays) or to say the last Tuesday of the month.

How?
Quite easily. For the first Monday of the month, it is likely that even if today was the first day of the month and coincidentally a Monday, then we would want to specify the field date to be the first Monday of the next month. If however we want the last Tuesday of the month and that Tuesday happens before today, then we want the last Tuesday of the next month.

Related Articles

Joes Revolver Map

Joes Word Cloud

error   site   copy   need   files   added   value   license   work   script   mysql   zoho   server   select   first   uploaded   windows   date   table   source   program   parameter   joomla   system   google   example   solution   where   code   list   page   data   file   display   note   time   used   content   user   version   name   using   report   following   could   would   find   search   website   database   JoelLipman.Com

Accreditation

Badge - Certified Zoho Creator Associate
Badge - Certified Zoho Creator Associate

Donate & Support

If you like my content, and would like to support this sharing site, feel free to donate using a method below:

Paypal:
Donate to Joel Lipman via PayPal

Bitcoin:
Donate to Joel Lipman with Bitcoin - Valid till 8 May 2022 3QnhmaBX7LQSRsC9hh6Je9rGQKEGNQNfPb
© 2021 Joel Lipman .com. All Rights Reserved.