This is the second page to this blog, I describe the next step after we have received the access code and realm ID, Now we want to do
something with the company data.
We are going to write 3 programs here. The first program will construct what we want. In this example, we will extract all of the
invoices and credit memos for a single year and save that data to a DBF. To do this, we will need to construct an SQL statement, a
generic program to check on the expiration of the access code and to refresh it if needed, then once we have the extraction, to parse the
results into a DBF table. What you do with this table depends on what the project's goal is. This first project was designed to
extract the invoices, add the city shipped so that a business license filing could be done. It turns out that QBO cannot run this
report. The ship-to column is not available in the report writer for the report we want. This is just an example of doing
something that QBO cannot do natively.
Lets get started. We will call our program getinvoices.prg.
As you can see, I am writing this for me to use so it isn't very pretty. I want to filter the results for a single year. Let me
digress, Foxpro 6 DBF's have field names that are max 10 characters so using the QBO field names won't due, they are too long so we
need to create a translation of QBO field to our field name. I have constructed a table for this translation. As I create
additional queries, I will need to expand this table to other files to be extracted such as the customer file. So you might be asking,
where can I get a list of the fields for the Invoice data file. Going back to the developer site, look at the top for API Explorer,
Accounting (These are the two scopes) If you choose Accounting, you will see a list of files on the left side, go down to Invoice,
Invoice Object. I prefer to see this in XML so switch the JSON to XML. In the window, you will see all of the fields and the sub
fields. Here is a structure
Create table fieldlist (Entity c(15), Fname c(50),Colname c(10),Type c(15),Maxlen c(10))
For the Entity, Fill in "Invoice". for Fname, this is the name of the field on the API. Colname is up to 10 character field name in
the resulting DBF.
Type: String, Number or Boolean. Numbers are assumed to be (12,2) you don't need to fill this in, Boolean will create a logical
field (L), String requires some explanation. Foxpro allows up to 254 characters per field but quickbooks allows much more in some
fields. Perhaps you could use a memo field.
I chose to ignore this as it was unlikely that I would need to use more than 254. There doesn't seem to be much documentation as to
the limits for these fields. I asked ChatGPT to get them and it did to some extent. Create for yourself maximums that make
sense. I doubt the invoice number will be more that 20 or so. You will need to add records to the file for each field you want
to get. The complication is for sub fields. For instance CustomerRef has an ID and a name so you need a record with a Fname of
CustomerRef.Name and another record for CustomerRef.Id. Name is C(100), Id is C(10).
You will notice that I run a program which will create a DBF for the Invoice Entity with a file name. There are several ways to create a
table, I chose to use this function which should be down at the bottom of your PRG or you could call it as a separate PRG. Also,
Intuit uses the term Entity instead of transaction type.
FUNCTION createtable
PARAMETER lcentity,lcfilename
USE fieldlist
SET FILTER TO entity=lcentity
mfieldcount=1
SCAN
DIME marray(mfieldcount,4)
marray(mfieldcount,1)=(colname)
DO CASE
CASE TYPE="String"
marray(mfieldcount,2)="C"
marray(mfieldcount,3)=VAL(MAXLEN)
marray(mfieldcount,4)=0
CASE TYPE="Number"
marray(mfieldcount,2)="N"
marray(mfieldcount,3)=12
marray(mfieldcount,4)=2
CASE TYPE="Boulean"
marray(mfieldcount,2)="L"
marray(mfieldcount,3)=1
marray(mfieldcount,4)=0
ENDCASE
mfieldcount=mfieldcount+1
ENDSCAN
**extra fields I use these fields to consolidate the city, state and zip.
**for my purpose, I needed to use the ship address but if it was blank, go to the bill address
**at the end of my program, Load these fields with what I want
DIME marray(mfieldcount,4)
marray(mfieldcount,1)="City"
marray(mfieldcount,2)="C"
marray(mfieldcount,3)=40
marray(mfieldcount,4)=0
mfieldcount=mfieldcount+1
DIME marray(mfieldcount,4)
marray(mfieldcount,1)="State"
marray(mfieldcount,2)="C"
marray(mfieldcount,3)=2
marray(mfieldcount,4)=0
mfieldcount=mfieldcount+1
DIME marray(mfieldcount,4)
marray(mfieldcount,1)="Zip"
marray(mfieldcount,2)="C"
marray(mfieldcount,3)=10
marray(mfieldcount,4)=0
CREATE TABLE (lcfilename) FROM ARRAY marray
In my program, I need to get all of the invoices and all of the credit memos. Each extraction will return and add to my
resulting data file but as I need to net sales, I need to make the credit memos negative. The program returns credit memos with
positive numbers. So I added a multiplier mmultiplier. I also set a variable to indicate I want Intuit to give my request back
to me in XML format. Intuit can do it in JSON or XML. Foxpro does not have a parser for either and XML was easier for me to
handle. But if you can parse JSON, then have at it and write your own parser into a DBF
One big caveat here. There are
several entities (Transaction types) that have child records. In this case, the header record and the line items of an invoice can be
returned from a query to QBO so to handle the line items, I will load them into a separate DBF. So because of the separate DBF, I will
need to have these fields in my fieldlist table. So for instance, if I ask for all fields "Select * from invoice", then the
lines will be returned. If I specify only certain fields and do not use "Line" then the return will not include the line items.
So my calling program will opt to do the lines or not and what file name result to use. My fieldlist will have an entity code if
"invoiceline".
So now comes the SQL. This SQL is not the full SQL and it is not the Foxpro SQL. It has some limits. It
can do some filtering and it can set orders but I don't believe it can do joins and groups and the more sophisticated features. If you
call it and it returns a bad request, it means some part of the SQL is no good. A field name or one of these commands. Intuit is
really picky. Lots of other stuff here. For instance: "Select * from Invoice" works. you get everything even the line items but
it takes longer to process. Where clauses seem to need to be encoded but not the rest of the command??? Another quirk is that an
request will only return up to 1000 records. If you don't put in the maxresults, it will limit to 100 records so if you have a lot of
records, you may have to make multiple calls. The second call you would say STARTPOSITION 1001 MAXRETURNS 1000. And so on.
I don't know of a way to know how many invoices there would be if it were to be unlimited. In the example below, you will see 2
queries for invoices and 1 for credit memos. You might be wondering if the credit memo can be added to the invoice DBF. It turns
out that the data structures are exactly the same otherwise we would need another set of records in the fieldlist.dbf and we would need to
create a separate DBF based on that structure. Then you would need to do whatever you want separately. And after the three
separate calls, And the result has been processed, I needed to do some work on te data file to fix the addresses
*****************************
*This is now the api call
SET SAFETY OFF
CLOSE DATA
CLEAR
SET TALK OFF
myear=SPACE(4)
@1,1 SAY "Enter year" GET myear PICTURE "####"
READ
?"Constructing the sql command"
mstart=myear+"-01-01"
mend=myear+"-12-31"
*shipaddr,billaddr
*Add a space after invoice to keep it from reading any other set of records that start with the same name
mentity="Invoice " &&this is the primary key/nodename. If the call doesn't include this, something is wrong
mentitychild=""
filename="ARmast"
childfilename=""
DO createtable WITH mentity,filename
CLOSE DATA
USE (filename)
mchild=.t.
IF mchild
**need to build a second DBF to hold the child records
**We need to know the file name
**We need to know the entity
**For the lines to be accumulated in the child data base, the line field must be in
** the SQL or the SQL must be all fields
mentitychild="InvoiceLine "
childfilename="ARtran"
SELECT 0
DO createtable WITH mentitychild,childfilename,mchild
ENDIF
mxml=.T.
mmultiplier=1
SQL="select metadata,txndate,synctoken,totalamt,Line,TxnTaxDetail,docnumber,CustomerRef,shipaddr,billaddr from Invoice
where%20TxnDate%20%3E%3D%20%27"+ mstart+"%27and%20TxnDate%20%3C%3D%27"+mend+"%27ORDER BY DocNumber STARTPOSITION 1 MAXRESULTS 1000"
DO makecall WITH SQL,filename,childfilename,mentity,mentitychild
mmultiplier=1
SQL="select metadata,txndate,synctoken,totalamt,Line,TxnTaxDetail,docnumber,CustomerRef,shipaddr,billaddr from Invoice
where%20TxnDate%20%3E%3D%20%27"+ mstart+"%27and%20TxnDate%20%3C%3D%27"+mend+"%27ORDER BY DocNumber STARTPOSITION 1001 MAXRESULTS 1000"
DO makecall WITH SQL,filename,childfilename,mentity,mentitychild
mmultiplier=-1
SQL="select metadata,txndate,synctoken,totalamt,Line,TxnTaxDetail,docnumber,CustomerRef,shipaddr,billaddr from CreditMemo
where%20TxnDate%20%3E%3D%20%27"+ mstart+"%27and%20TxnDate%20%3C%3D%27"+mend+"%27ORDER BY DocNumber STARTPOSITION 1 MAXRESULTS 1000"
DO makecall WITH SQL,filename,childfilename,mentity,mentitychild
SELECT (filename)
REPL ALL city WITH ship_city
REPL ALL state WITH ship_state
REPL ALL zip WITH ship_zip
SET FILTER TO EMPTY(city)
REPL ALL zip WITH RIGHT(TRIM(bill_line1),5) FOR VAL(RIGHT(TRIM(bill_line1),5))>0
REPL ALL zip WITH RIGHT(TRIM(bill_line2),5) FOR VAL(RIGHT(TRIM(bill_line2),5))>0
REPL ALL zip WITH RIGHT(TRIM(bill_line3),5) FOR VAL(RIGHT(TRIM(bill_line3),5))>0
REPL ALL zip WITH RIGHT(TRIM(bill_line4),5) FOR VAL(RIGHT(TRIM(bill_line4),5))>0
REPL ALL zip WITH RIGHT(TRIM(bill_line5),5) FOR VAL(RIGHT(TRIM(bill_line5),5))>0
REPL ALL zip WITH RIGHT(TRIM(bill_line6),5) FOR VAL(RIGHT(TRIM(bill_line6),5))>0
REPL ALL zip WITH RIGHT(TRIM(bill_line7),5) FOR VAL(RIGHT(TRIM(bill_line7),5))>0
REPL ALL zip WITH RIGHT(TRIM(bill_line8),5) FOR VAL(RIGHT(TRIM(bill_line8),5))>0
REPL ALL state WITH SUBSTR(bill_line1,AT(",",bill_line1)+2,2) FOR "," $ bill_line1
REPL ALL state WITH SUBSTR(bill_line2,AT(",",bill_line2)+2,2) FOR "," $ bill_line2
REPL ALL state WITH SUBSTR(bill_line3,AT(",",bill_line3)+2,2) FOR "," $ bill_line3
REPL ALL state WITH SUBSTR(bill_line4,AT(",",bill_line4)+2,2) FOR "," $ bill_line4
REPL ALL state WITH SUBSTR(bill_line5,AT(",",bill_line5)+2,2) FOR "," $ bill_line5
REPL ALL state WITH SUBSTR(bill_line6,AT(",",bill_line6)+2,2) FOR "," $ bill_line6
REPL ALL state WITH SUBSTR(bill_line7,AT(",",bill_line7)+2,2) FOR "," $ bill_line7
REPL ALL state WITH SUBSTR(bill_line8,AT(",",bill_line8)+2,2) FOR "," $ bill_line8
REPL ALL city WITH LEFT(bill_line1,AT(",",bill_line1)-1) FOR "," $ bill_line1
REPL ALL city WITH LEFT(bill_line2,AT(",",bill_line2)-1) FOR "," $ bill_line2
REPL ALL city WITH LEFT(bill_line3,AT(",",bill_line3)-1) FOR "," $ bill_line3
REPL ALL city WITH LEFT(bill_line4,AT(",",bill_line4)-1) FOR "," $ bill_line4
REPL ALL city WITH LEFT(bill_line5,AT(",",bill_line5)-1) FOR "," $ bill_line5
REPL ALL city WITH LEFT(bill_line6,AT(",",bill_line6)-1) FOR "," $ bill_line6
REPL ALL city WITH LEFT(bill_line7,AT(",",bill_line7)-1) FOR "," $ bill_line7
REPL ALL city WITH LEFT(bill_line8,AT(",",bill_line8)-1) FOR "," $ bill_line8
SET FILTER TO
CLEAR
SET TALK ON
SUM amount-totaltax
COPY TO result.XLS TYPE XLS
WAIT WINDOW "Result.xls has been created"
QUIT
Now comes the next step, The MAKECALL.PRG. This program is a function that actually does the querying Intuit for the data
requested but first makes sure the access codes is valid and not expired. And if it is, it exchanges the refresh code (token) for a new
access token
Here is the code. You might notice that I left some code in here if I were to use the Chilkat library just to give you a look at
it. Chilkat is 16 lines of code, without, it is 34 lines.
The calling program above was "DO makecall WITH SQL,filename,childfilename,mentity,mentitychild"
PARAMETERS msql,mfilename,mchildfilename,mentity,mentitychild
*****************************
*This is now the api call
**lets check the dates stored
*Enter the choice of which client and secret for each of my clients
USE oauth_tokens
COUNT FOR NOT EMPTY(atoken)
IF _TALLY = 0
MESSAGEBOX("No OAuth tokens found.")
RETURN
ENDIF
LOCATE
lcaccesstoken = atoken
lcrefreshtoken = rtoken
dtaccessexpires = CTOT(aexpires)
dtrefreshexpires = CTOT(rexpires)
lcrealmid=TRIM(realmid)
CLOSE DATA
* Check if access token expired
mchilkat=.F.
IF DATETIME() >= dtaccessexpires
IF mchilkat
looauth2 = CREATEOBJECT("Chilkat.OAuth2")
looauth2.refreshtoken = lcrefreshtoken
looauth2.authorizationendpoint = "https://appcenter.intuit.com/connect/oauth2"
looauth2.tokenendpoint = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer"
looauth2.appcallbackurl = "http://www.trendsetteryarns.com/auth-callback.asp"
looauth2.clientid = "your client id"
looauth2.clientsecret = "your client secret"
IF looauth2.refreshaccesstoken() = 0
MESSAGEBOX("Refresh failed: " + looauth2.lasterrortext)
RETURN
ENDIF
* Save new tokens and expiration
lcaccesstoken = looauth2.accesstoken
lcrefreshtoken = looauth2.refreshtoken
dtaccessexpires = TTOC(DATETIME() + 3600*24*100)
RELEASE looauth2
ELSE
mclientid = "your client id here"
mclientsecret = "your client secret"
mtokenendpoint = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer"
lohttp = CREATEOBJECT("WinHttp.WinHttpRequest.5.1")
lohttp.OPEN("POST", mtokenendpoint, .F.)
lohttp.setrequestheader("Content-type","application/x-www-form-urlencoded")
lohttp.setrequestheader("Accept","Application/json")
lcbody= ;
"grant_type=refresh_token" + ;
"&refresh_token="+lcrefreshtoken + ;
"&client_id=" + mclientid + ;
"&client_secret=" + mclientsecret
lohttp.SEND(lcbody)
IF lohttp.STATUS = 200
lchtml = lohttp.responsetext
ELSE
MESSAGEBOX("Got nothing back from the exchange." + TRANSFORM(lohttp.STATUS))
CANCEL
ENDIF
?"Parsing the response"
*this is a simple parse, the only things we need are the access and refresh tokens
lcresponse=lohttp.responsetext
losc=CREATEOBJECT("ScriptControl")
losc.language = "JScript"
losc.addcode( ;
"function parseJSON(s) { " + ;
" return eval('(' + s + ')');" + ;
"}")
loobj=losc.RUN("parseJSON",lcresponse)
lcaccesstoken=loobj.access_token
lcrefreshtoken=loobj.refresh_token
dtaccessexpires = TTOC(DATETIME() + 3600)
dtrefreshexpires = DATETIME() + 3600*24*100
ENDIF
USE oauth_tokens EXCLUSIVE
DELETE ALL
PACK
INSERT INTO oauth_tokens VALUES ;
(lcaccesstoken, lcrefreshtoken, dtaccessexpires, DTOC(dtrefreshexpires),lcrealmid)
CLOSE DATA
ENDIF
Now we have a valid access token. We pass the company in the url and the SQL and we
pass the access token in the header of the HTML request. All of the parts are in place either way. If you get a bad request, you
might notice the _cliptext. If you cannot figure what is wrong with the request, you could paste the error message into ChatGPT and it
might tell you what is wrong. For me, it was learning what needed to be encoded. I will also note that I was thinking I could
use this SQL Select Customerref.Name from Invoice. All of the documentation said I could do this but I could never get it to
work. Always bad request. So we will remove extraneous characters and get ready to pass the required fields. I
cannot tell you why some parts are passed through the header and others are passed through the URL but read the code below and you will see
what works.
lcrealmid=STRTRAN(lcrealmid, CHR(13)+CHR(10),"") &&remove newlines
lcrealmid=STRTRAN(lcrealmid, CHR(32),"") &&remove spaces
lcurl = "https://quickbooks.api.intuit.com/v3/company/" + lcrealmid + "/query?query=" + msql + "&minorversion=75"
**Foxpro cannot parse the json so we will ask that the result comes back to us in XML not JSON
?"Setting XML as the type of stream Intiut will send back to us."
loHttp = CREATEOBJECT("WinHttp.WinHttpRequest.5.1")
loHttp.Open("GET", lcUrl, .F.)
loHttp.SetRequestHeader("Accept","application/xml")
loHttp.SetRequestHeader("Authorization", "Bearer " + lcAccessToken)
loHttp.Send()
IF loHttp.Status = 200
lcResponse = loHttp.ResponseText
ELSE
lcResponse = "ERROR: " + TRANSFORM(loHttp.Status) + ;
" - " + loHttp.ResponseText
_CLIPTEXT=loHttp.ResponseText
SUSPEND
CANCEL
ENDIF
?"Removing extraneous characters from the stream."
* Remove leading/trailing whitespace
lcresponse = LTRIM(RTRIM(lcresponse))
* Remove any carriage return / line feed
lcresponse = STRTRAN(lcresponse, CHR(13), "")
lcresponse = STRTRAN(lcresponse, CHR(10), "")
IF loHttp.Status = 200
**IF lohttp.lastmethodsuccess=1 this is used if we do it chilkat
lcxml=STRTRAN(lcresponse,'"',"")
DO dbfit6xml WITH mfilename
ENDIF
That is the MakeCall.prg. It should be universal for all of your API calls.
In my many attempts to figure the last piece out, I tried parsing JSON, I tried parsing XML, I tried parsing with a parsing program that
ChatGPT wrote.
Finally I wrote my own for XML. Whatever you call your PRG, replace my DBFIT6XML with your prg. Sometimes this is the best way so you know what is happening and how to fix what doesn't work.
I also decided to store the response in a separate variable because my parsing technique removes from the string, the fields I get and in the end, my LCresponse has a length of zero.
See the xml parsing blog for that last chunk of code.
Craig Sobel