Как я подружил Quickbooks и PHP сайт с помощью Web Connector’а

от автора

Как-то однажды понадобилось клиенту интеграция Quickbooks (далее QB) и сайта, который мы ему и делаем. Первый вопрос который у меня возник по этому поводу: "Что такое QB, и можно ли вообще это реализовать?".

Немного погуглив я нашел то что искал. Quickbooks — это бухгалтерская программа для малого бизнеса (основной рынок использования США). Это что-то типа 1С но только с нормальным GUI и некоторыми прикольными плюшками. QB — это приложение, которое пользователь ставит у себя на компе (only for Windows) и с помощью пару кликов, разворачивает себе компанию в которой ведет бухгалтерию.

Ладно, теперь я, хотя бы, знаю своего врага в лицо, одной проблемой меньше. Что касается интеграции то тут все немного сложнее. С чем можно интегрировать QB вы можете посмотреть здесь. Что же мы там видим:

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

Мда, PHP SDK (Coming Soon) — последняя надежда… Я почти отчаялся, но меня спасло это. Что же эта за штука такая — Web Connector? На офф сайта по этому есть небольшая страничка, на которой предлагают скачать QuickBooks Web Connector Programmer’s Guide, и все (по крайней мере мне надоело искать информацию на офф сайте).

Что же из себя представляет Web Connector?
Web Connector — это своеобразный посредник между QB и web-сервером (он устанавливается вместе с QB). По timeout’у или клику мышки он стучится на определенный url на вашем сайте, получает от сайта запрос, который нужно спросить у QB и передает его; ждет ответа от QB, а когда дождется то постучится на сайт и пришлет вам ответ от QB.

И так приступим…
Для начала нам нужно сказать Web Connector’у куда нужно стучаться, а делается это с помьщью файла *.QWC.

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 — название службы, которое будет отобржатся в списке Web Connector’а
  • AppID — зачем это нужно я так и не понял, но без нее не работает
  • AppURL — здесь указывается url на который Web Connector будет стучатся. Здесь есть оговорка, http можно использовать только в отладочных целях, те. если домен содержит слово localhost (test-localhost-serv, localhost-admin… ) то можно использовать http. А вот если не содержит, то нужно использовать https и тут без вариантов.
  • AppDescription — описание службы
  • AppSupport — url по который будет отображаться в списке web connector’a, как линка на справку (тут можно указывать http)
  • UserName — имя пользователя, из-под которого мы будет общатся к базе QB (такой пользователь болжен быть создан в QB)
  • OwnerID и FileID — это уникальные последовательности, состоящие из шестнадцатеричных символов (для каждой службы свое, я просто менял одну значение и все)
  • QBType — это тип подключения Web Connector’a к QB (возможные значения QBFS или QBPOS)
  • IsReadOnly — если ваша служба изменяет, удаляет, добавляет данные в QB, то должна быть true

Если вам нужно что бы служба запускалать автоматически каждые 5 минут, то нужно довавить следующее:

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

Краткое дополнене к AppURL: если у вас нет возможности настроить https (или нет денег на настоящий сертификат) на сервере, то тут есть 2 лазейки:

1) В хосты на котором стоит QB прописываем IP сервера и доменное имя с localhost, не забудьте проситать этот домен в настройках апача на сервере
2) Ставим самопальный сертификат и добавляем его в список доверенных серверов, иначе не заработает (пример)

Для того чтобы добавить qwc нужно:
— включить QB и открыть компанию с которой будет работать приложение
— открыть Web Connector
— в Web Connector’e нажимаете кнопку Add an application, указываете qwc файл.
— когда вы нажмете OK, QB вас спросит, хотите ли вы дать доступ данному приложению в базу QB (нужно выбрать пользователя, «admin» в нашем случае)
— когда нажмете «Done» на последнем диалоговом окне, вернитесь в Web Connector и введите пароль для пользователя «admin»
— что бы запустить приложение, нужно поставить чекбокс и нажать кнопку Update Selected

Так, теперь настала очередь подготовки сайта к приему Web Connector’a.
Как помните мы указали <AppURL>http://localhost/quickbooks/clients.php</AppURL>, сейчас займемся его созданием. Web Connector используюет SOAP протокол, значит на сайте придется поднять SOAP сервер.

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  */ function requestId($id = '') {     $file_name = './log/clients_id.log';     if(!file_exists(dirname($file_name)))         mkdir(dirname($file_name), 0777);      // save id into file     if(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(); 

Функция requestId() — небходима для того чтобы сохранить id транзакции в файл. В примере, который будет далее рассмотрен, мы хотим получить список всех клиентов, а это может быть не одна тысяча компаний. Поэтому будем получать порциями по 500, так надежней и нагрузка на сервер меньше. Для чего нужны ‘QB_LOGIN, QB_PASSWORD и QB_TICKET увидите далее. Последние 3 строчки — и есть создание SOAP сервера.qbwebconnectorsvc.wsdl данный файл я нашел на просторах офф сайта, но где имеено не помню (они не давно сделали редизайн).

Забыл сказать, Web Connector знает только 8 слов: clientVersion, serverVersion, authenticate, sendRequestXML, receiveResponseXML, connectionError, getLastError и 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  */ class Response{ }   /**  * Base class for QuickBooks integration  *   * @package QB SOAP  * @version 2013-10-20  */ class Qb {     /**      * Response object      * @var string      */     var $response = '';       /**     * Constructor     *     * @return   void     * @access   public     * @version  2013-10-20     */     public function __construct()     {         $this->response = new Response();     }       /**      * Function return client version      *      * @return  string      * @param   object $param      * @access  public      * @version 2013-10-20      */     public function clientVersion($param = '')     {         $response->clientVersionResult = "";         return $response;     }       /**      * Function return server version      *      * @return  string      * @access  public      * @version 2013-10-20      */     public function serverVersion()     {         $this->response->serverVersionResult = "";         return $this->response;     }       /**      * Function try authenticate user by username/password      *      * @return  string      * @param   object $param      * @access  public      * @version 2013-10-20      */     public function authenticate($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      */     public function connectionError($param = '')     {         $this->response->connectionErrorResult = "connectionError";         return $this->response;     }       /**      * Function return last error      *      * @return  string      * @param   object $param      * @access  public      * @version 2013-10-20      */     public function getLastError($param = '')     {         $this->response->getLastErrorResult = "getLastError";         return $this->response;     }       /**      * Function close connection      *      * @return  string      * @param   object $param      * @access  public      * @version 2013-10-20      */     public function closeConnection($param = '')     {         $this->response->closeConnectionResult = "Complete";         return $this->response;     } } 

  • clientVersion — здесь Web Connector стучится к нам с говорит: «Слышь, к тебе ображется Web Connector с версией xxxxx. Что тебе нужно?». В ответ вы можете сказать какую версию хотите, а можете и промолчать, что я и сделал
  • serverVersion — см. выше.
  • authenticate — здесь Web Connector говорит нам что такой-то пользователь с таким логином (логин мы указали в qwc файле, а пароль в окошке пароля Web Connector’a), мы сравниваем с допустимыми и пропускаем, или посылаем ошибку. В случае успеха мы отдаем Web Connector’y тикет QB_TICKET, который будет использоватся в течении текущей сессии
  • sendRequestXML — здесь мы формируем запрос, который Web Connector передаст QB’y.
  • receiveResponseXML — прием данных в ответ на наш запрос
  • connectionError — если произошла ошибка при передаче данных, то вызовется этот метод
  • getLastError — если наш мы написали не корректный запрос, то вызовется этот метод
  • closeConnection — если все прошло по плану, и мы успешно приняли запрос

В файле, представленном ниже, вы можете видеть как формируется запрос, и как обрабатывается прием данных.

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  */ class Qb_Clients extends Qb {     /**      * Function send request for Quickbooks      *      * @return  string      * @param   object $param      * @access  public      * @version  2013-10-20      */     public function sendRequestXML($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      */     public function receiveResponseXML($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 row             if(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;     } } 

Строка <?qbxml version="12.0"?> говорит о том, что я использую 12’ю версию qbxml. На данный момент это последняя доступная версия (она поддерживается в 13’ом и 14’ом QB). Чем выше версия qbxml, тем больше возможностей работы с QB. Список всех доступных запросов можете найти здесь. Перейдя по ссылке вы увидите все возможные запросы которые можно передать в QB (они отображатся в списке Select Message). Вкладки Request и Response — генерируются в зависимости от того, какой запрос вы выбрали.

PS. Есть одно ‘но‘. Если выбрать к примеру запрос "CustomerAdd", то можете увидеть что данный запрос поддерживает блок «Contacts», который доступен с 12’ой версии qbxml. Но на самом деле он не реализован а только в процессе внедрения (почему его включили в доку — загадка, я потратил не один час работы на эту проблему, пока случайно не зашел на форум где описана это фишка). Поэтому если что-то не работает в qbxml v.12, то не факт что должно 🙂

PSS. Исходный код — тут

ссылка на оригинал статьи http://habrahabr.ru/post/200728/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *