How I made friends Quickbooks and PHP site using Web Connector

One day, a client needed the integration of Quickbooks (hereinafter QB) and the site that we are doing to him. The first question I had about this is: “ What is QB, and is it even possible to implement it? ”.

A little googling, I found what I was looking for. Quickbooks is an accounting program for small businesses (the main market for US use). This is something like 1C but only with a normal GUI and some cool goodies. QB is an application that a user installs on his computer ( only for Windows ) and, with a couple of clicks, deploys a company in which he conducts accounting.

Okay, now I, at least, know my enemy in person, one less problem. As for integration, everything here is a little more complicated. With what you can integrate QB you can see here . What do we see there:

  • .NET SDK
  • Java SDK
  • PHP SDK (Coming Soon)
  • Windows Azure SDK
  • QuickBooks QBXML v12 SDK (but on desktop scenarios only )


Hmm, the PHP SDK (Coming Soon) is the last hope ... I almost despaired, but it saved me . What kind of thing is this - Web Connector ? On the off site, for this, there is a small page on which they offer to download the QuickBooks Web Connector Programmer's Guide , and that's all (at least I'm tired of looking for information on the off site).


What is a Web Connector?
Web Connector is a kind of intermediary between QB and a web server (it is installed with QB). By timeout or mouse click, he knocks to a specific url on your site, receives a request from the site that you need to ask QB and passes it; waits for a response from QB, and when he waits he will knock on the site and send you a response from QB.

And so
let's get started ... First, we need to tell the Web Connector where to knock, and this is done using the * .QWC file .

clients.QWC
<?xml version="1.0"?><QBWCXML><AppName>QuickBooks Integrator (clients)</AppName><AppID></AppID><AppURL>http://localhost/quickbooks/clients.php</AppURL><AppDescription>Export Customers from QB to csv file</AppDescription><AppSupport>http://localhost/</AppSupport><UserName>admin</UserName><OwnerID>{90A44FB7-33D6-4815-AC85-AC86A7E7123B}</OwnerID><FileID>{57F3B9B6-86F6-4FCC-B1FF-967DE1813123}</FileID><QBType>QBFS</QBType><IsReadOnly>false</IsReadOnly></QBWCXML>



  • AppName - the name of the service that will be displayed in the list of Web Connector
  • AppID - I don’t understand why this is needed, but without it it doesn’t work
  • AppURL - here indicates the url on which the Web Connector will be knocked. There is a caveat here, http can only be used for debugging purposes, those. if the domain contains the word localhost (test-localhost-serv, localhost-admin ...) then you can use http. But if it does not, then you need to use https and here without options.
  • AppDescription - service description
  • AppSupport - url for which will be displayed in the list of web connector'a, as a link to the help (here you can specify http)
  • UserName - the username from which we will communicate to the QB database (such a user must be created in QB)
  • OwnerID and FileID are unique sequences consisting of hexadecimal characters (for each service, I just changed one value and that's it)
  • QBType is the type of connection of the Web Connector to QB (possible values ​​of QBFS or QBPOS )
  • IsReadOnly - if your service modifies, deletes, adds data to QB, then it must be true


If you need the service to start automatically every 5 minutes, then you need to add the following:

<Scheduler><RunEveryNMinutes>5</RunEveryNMinutes></Scheduler>


A short supplement to AppURL : if you don’t have the opportunity to configure https (or there is no money for a real certificate) on the server, then there are 2 loopholes:

1) In the hosts on which QB stands, we register the IP server and domain name with localhost , do not forget to read this domain in the Apache settings on the server
2) We set up a self-made certificate and add it to the list of trusted servers, otherwise it won’t work ( example )

To add qwc you need to:
- enable QB and open the company the application will work with
- open the Web Connector
- on the Web Connector'e press the button the Add an application , uk binding qwc file.
- when you click OK, QB will ask you if you want to give this application access to the QB database (you need to select the user, “admin” in our case)
- when you click “Done” in the last dialog box, return to the Web Connector and enter the password for the user “admin”
- to launch the application, you need to put a checkbox and click the Update Selected button.





So, now it’s the turn to prepare the site for receiving Web Connector.
As you remember, we indicated localhost/quickbooks/clients.phpthat we will now be engaged in its creation. The Web Connector uses the SOAP protocol, which means that the site will have to raise the SOAP server.

clients.php
<?php/**
 * File for integration QB
 * QB Webconnector send soap request to this file
 * 
 * @package QB SOAP
 *//**
 * Log function
 *
 * @param string $mess
 */function_log($mess = ''){
    $file_name = './log/clients.log';
    if(!file_exists(dirname($file_name)))
        mkdir(dirname($file_name), 0777);
    $f = fopen($file_name, "ab");
    fwrite($f, "==============================================\n");
    fwrite($f, "[" . date("m/d/Y H:i:s") . "] ".$mess."\n");
    fclose($f);
}
/**
 * Log function
 *
 * @param string $mess
 */functionrequestId($id = ''){
    $file_name = './log/clients_id.log';
    if(!file_exists(dirname($file_name)))
        mkdir(dirname($file_name), 0777);
    // save id into fileif(trim($id) !== ''){
        $f = fopen($file_name, "c+b");
        fwrite($f, $id);
        fclose($f);
    }
    $id = trim(file_get_contents($file_name));
    return $id;
}
/**
 * System variables
 */
define('QB_LOGIN',    'admin');
define('QB_PASSWORD', '');
define('QB_TICKET',   '93f91a390fa604207f40e8a94d0d8fd11005de108ec1664234305e17e');
/**
 * Main class for SOAP SERVER
 */require'qb_clients.php';
/**
 * Create SOAP server
 */
$server = new SoapServer("qbwebconnectorsvc.wsdl", array('cache_wsdl' => WSDL_CACHE_NONE));
$server->setClass("Qb_Clients");
$server->handle();


The requestId () function is required to save the transaction id to a file. In the example that will be further considered, we want to get a list of all customers, and this may be more than one thousand companies. Therefore, we will receive portions of 500, so it is more reliable and the load on the server is less. Why do you need 'QB_LOGIN , QB_PASSWORD and QB_TICKET see later. The last 3 lines - this is the creation of a SOAP server. qbwebconnectorsvc.wsdl I found this file on the open spaces of the site, but I don’t remember where it was (they did a redesign a while ago).

I forgot to say that the Web Connector knows only 8 words: clientVersion , serverVersion , authenticate ,sendRequestXML , receiveResponseXML , connectionError , getLastError and closeConnection .

qb.php
<?php/**
 * File contain base QB class and Result class (empty class for Qb reaponse)
 *//**
 * Response class (empty class)
 * 
 * @package QB SOAP
 * @version 2013-10-20
 */classResponse{
}
/**
 * Base class for QuickBooks integration
 * 
 * @package QB SOAP
 * @version 2013-10-20
 */classQb{
    /**
     * Response object
     * @var string
     */var $response = '';
    /**
    * Constructor
    *
    * @return   void
    * @access   public
    * @version  2013-10-20
    */publicfunction__construct(){
        $this->response = new Response();
    }
    /**
     * Function return client version
     *
     * @return  string
     * @param   object $param
     * @access  public
     * @version 2013-10-20
     */publicfunctionclientVersion($param = ''){
        $response->clientVersionResult = "";
        return $response;
    }
    /**
     * Function return server version
     *
     * @return  string
     * @access  public
     * @version 2013-10-20
     */publicfunctionserverVersion(){
        $this->response->serverVersionResult = "";
        return$this->response;
    }
    /**
     * Function try authenticate user by username/password
     *
     * @return  string
     * @param   object $param
     * @access  public
     * @version 2013-10-20
     */publicfunctionauthenticate($param = ''){
        if(($param->strUserName == QB_LOGIN) && ($param->strPassword == QB_PASSWORD))
            $this->response->authenticateResult = array(QB_TICKET, "");
        else$this->response->authenticateResult = array("", "nvu");
        return$this->response;
    }
    /**
     * Function return last error
     *
     * @return  string
     * @param   object $param
     * @access  public
     * @version 2013-10-20
     */publicfunctionconnectionError($param = ''){
        $this->response->connectionErrorResult = "connectionError";
        return$this->response;
    }
    /**
     * Function return last error
     *
     * @return  string
     * @param   object $param
     * @access  public
     * @version 2013-10-20
     */publicfunctiongetLastError($param = ''){
        $this->response->getLastErrorResult = "getLastError";
        return$this->response;
    }
    /**
     * Function close connection
     *
     * @return  string
     * @param   object $param
     * @access  public
     * @version 2013-10-20
     */publicfunctioncloseConnection($param = ''){
        $this->response->closeConnectionResult = "Complete";
        return$this->response;
    }
}


  • clientVersion - here the Web Connector knocks on us and says: “Listen, the Web Connector with version xxxxx is coming to you. What do you need?". In response, you can say which version you want, or you can keep silent, which I did
  • serverVersion - see above.
  • authenticate - here the Web Connector tells us that such and such a user with such a login (we specified the login in the qwc file, and the password in the password window of the Web Connector), we compare with the valid ones and skip or send an error. If successful, we give the Web Connector'y ticket QB_TICKET , which will be used during the current session
  • sendRequestXML - here we form a request that the Web Connector will pass to QB.
  • receiveResponseXML - receive data in response to our request
  • connectionError - if an error occurred while transmitting data, this method is called
  • getLastError - if we wrote an incorrect request, then this method will be called
  • closeConnection - if everything went according to plan, and we successfully accepted the request


In the file below, you can see how the request is formed, and how data reception is processed.

qb_clients.php
<?php/**
 * File contains class Qb_Clients() extends Qb()
 *//**
 * Include base class for SOAP SERVER
 */require'qb.php';
/**
 * Class for import all clients from Qb
 * 
 * @package QB SOAP
 * @version 2013-10-20
 */classQb_ClientsextendsQb{
    /**
     * Function send request for Quickbooks
     *
     * @return  string
     * @param   object $param
     * @access  public
     * @version  2013-10-20
     */publicfunctionsendRequestXML($param = ''){
        $id = requestId();
        // <!-- ActiveStatus may have one of the following values: ActiveOnly [DEFAULT], InactiveOnly, All -->if($param->ticket == QB_TICKET){
            $request = '<?xml version="1.0" encoding="utf-8"?>
                <?qbxml version="12.0"?>
                <QBXML>
                    <QBXMLMsgsRq onError="stopOnError">
                        <CustomerQueryRq requestID="'.time().'" metaData="NoMetaData" iterator="'.(($id != '')?'Continue':'Start').'" '.(($id != '')?'iteratorID="'.$id.'"':'').'>
                            <MaxReturned>500</MaxReturned>
                            <ActiveStatus>ActiveOnly</ActiveStatus>
                        </CustomerQueryRq>
                    </QBXMLMsgsRq>
                </QBXML>';
            $this->response->sendRequestXMLResult = $request;
        }
        else$this->response->sendRequestXMLResult = "E: Invalid ticket.";
        return$this->response;
    }
    /**
     * Function get response from QB
     *
     * @return  string
     * @param   object $param
     * @access  public
     * @version 2013-03-15
     */publicfunctionreceiveResponseXML($param = ''){
        $response = simplexml_load_string($param->response);
        $iteratorID = trim($response->QBXMLMsgsRs->CustomerQueryRs->attributes()->iteratorID);
        // set new iteratorID
        requestId($iteratorID);
        if( ($param->ticket == QB_TICKET) && isset($response->QBXMLMsgsRs->CustomerQueryRs->CustomerRet) ){
            $rows = $response->QBXMLMsgsRs->CustomerQueryRs;
            settype($rows, 'array');
            // if list contain only one item rowif(isset($rows['CustomerRet']->ListID))
                $rows = array($rows['CustomerRet']);
            else
                $rows = $rows['CustomerRet'];
            $data = array();
            foreach ($rows as $i=>$r) {
                settype($r, 'array');
                $data[] = array(
                    'qb_id' => trim($r['ListID']),
                    'qb_es' => trim($r['EditSequence']),
                    'is_active' => trim($r['IsActive']),
                    'phone' => trim($r['Phone']),
                    'notes' => trim($r['Notes']),
                    'fax'   => trim($r['Fax']),
                    'company_name' => trim($r['Name']),
                    'b_email' => trim($r['Email']),
                    'b_email_other' => trim($r['Cc']),
                    'b_phone' => trim($r['AltPhone']),
                    'b_salutation' => trim($r['Salutation']),
                    'b_fname' => trim($r['FirstName']),
                    'b_lname' => trim($r['LastName']),
                    'b_address' => trim($r['BillAddress']->Addr1),
                    'b_address2' => trim($r['BillAddress']->Addr2),
                    'b_address3' => trim($r['BillAddress']->Addr3),
                    'b_city' => trim($r['BillAddress']->City),
                    'b_state' => trim($r['BillAddress']->State),
                    'b_country' => trim($r['BillAddress']->Country),
                    'b_zip' => trim($r['BillAddress']->PostalCode),
                );
            }
            // echo data into log file
            _log(print_r($data,1));
            $this->response->receiveResponseXMLResult = '30';
        }
        else$this->response->receiveResponseXMLResult = '100';
        return$this->response;
    }
}


The line <?qbxml version="12.0"?>says that I am using the 12th version of qbxml. At the moment, this is the latest available version (it is supported in the 13th and 14th QB). The higher the qbxml version, the greater the possibilities of working with QB. A list of all available queries can be found here . By following the link you will see all the possible requests that can be sent to QB (they will be displayed in the Select Message list ). Request and Response tabs - generated depending on which request you selected.

PS. There is one ' but .' If you select for example the query " CustomerAdd", you can see that this request supports the" Contacts "block, which is available from the 12th version of qbxml. But in fact it is not implemented, but only during the implementation process (why it was included in the doc - riddle, I spent more than one hour of work on this problem, until I accidentally went to the forum where this feature is described.) Therefore, if something does not work in qbxml v.12 , then it is not a fact that it should :)

PSS. Source code - here

Also popular now: