Quickbooks SDK Part III
Part I: Introduction and Controlling Procedure
Part II: Creating the Quickbooks Queries
Part III: Filling Class Modules
Part IV: Writing to Worksheets
In Part I, I listed the controlling procedure that does the Quickbooks related stuff and calls all the other sub procedures. In Part II, I listed the sub procedures that created each of the four queries. Now that the queries are set up, the controlling procedure sends the XML requests to Quickbooks and waits for responses. I takes about four minutes to run the whole thing and three-and-a-half minutes are waiting for Quickbooks to send back the data.
Once the responses are received, I loop through them and use a Select Case statement to handle each request separately. By handle, I mean store the data in such a way that I can use it. For each of the four responses, I created a class module. For instance, I have a CPurchaseOrder class module that will be used to hold the purchase order information.
Private msTxnNumber As String
Private msVendorName As String
Private mdtPoDate As Date
Private msPONumber As String
Private mdtExpectedDate As Date
Private mbIsManuallyClosed As Boolean
Private mbIsFullyReceived As Boolean
Private msItemName As String
Private mdQtyOrdered As Double
Private mdQtyReceived As Double
Private mbLineIsManuallyClosed As Boolean
Public Property Get TxnID() As String
TxnID = msTxnID
End Property
Public Property Let TxnID(ByVal sTxnID As String)
msTxnID = sTxnID
End Property
Public Property Get TxnNumber() As String
TxnNumber = msTxnNumber
End Property
Public Property Let TxnNumber(ByVal sTxnNumber As String)
msTxnNumber = sTxnNumber
End Property
Public Property Get VendorName() As String
VendorName = msVendorName
End Property
Public Property Let VendorName(ByVal sVendorName As String)
msVendorName = sVendorName
End Property
Public Property Get PoDate() As Date
PoDate = mdtPoDate
End Property
Public Property Let PoDate(ByVal dtPoDate As Date)
mdtPoDate = dtPoDate
End Property
Public Property Get PONumber() As String
PONumber = msPONumber
End Property
Public Property Let PONumber(ByVal sPONumber As String)
msPONumber = sPONumber
End Property
Public Property Get ExpectedDate() As Date
ExpectedDate = mdtExpectedDate
End Property
Public Property Let ExpectedDate(ByVal dtExpectedDate As Date)
mdtExpectedDate = dtExpectedDate
End Property
Public Property Get IsManuallyClosed() As Boolean
IsManuallyClosed = mbIsManuallyClosed
End Property
Public Property Let IsManuallyClosed(ByVal bIsManuallyClosed As Boolean)
mbIsManuallyClosed = bIsManuallyClosed
End Property
Public Property Get IsFullyReceived() As Boolean
IsFullyReceived = mbIsFullyReceived
End Property
Public Property Let IsFullyReceived(ByVal bIsFullyReceived As Boolean)
mbIsFullyReceived = bIsFullyReceived
End Property
Public Property Get ItemName() As String
ItemName = msItemName
End Property
Public Property Let ItemName(ByVal sItemName As String)
msItemName = sItemName
End Property
Public Property Get QtyOrdered() As Double
QtyOrdered = mdQtyOrdered
End Property
Public Property Let QtyOrdered(ByVal dQtyOrdered As Double)
mdQtyOrdered = dQtyOrdered
End Property
Public Property Get QtyReceived() As Double
QtyReceived = mdQtyReceived
End Property
Public Property Let QtyReceived(ByVal dQtyReceived As Double)
mdQtyReceived = dQtyReceived
End Property
Public Property Get LineIsManuallyClosed() As Boolean
LineIsManuallyClosed = mbLineIsManuallyClosed
End Property
Public Property Let LineIsManuallyClosed(ByVal bLineIsManuallyClosed As Boolean)
mbLineIsManuallyClosed = bLineIsManuallyClosed
End Property
Nothing too fancy there. I set up a Public variable for every "field" that I'm bringing over, then use MZTools to convert all those Public variables to Property Get/Let procedures. With the class module set up, I can parse the request and put all the data in the class module.
Dim POList As IPurchaseOrderRetList
Dim curPO As IPurchaseOrderRet
Dim POLineList As IORPurchaseOrderLineRetList
Dim curPOLine As IPurchaseOrderLineRet
Dim colPO As Collection
Dim clsPO As CPurchaseOrder
Dim i As Long, j As Long
'Variable to hold the sales orders
Set POList = CurResp.Detail
Set colPO = New Collection
'Loop through the sales order list
For i = 0 To POList.Count - 1
'Get the first one in the list
Set curPO = POList.GetAt(i)
Set POLineList = curPO.ORPurchaseOrderLineRetList
For j = 0 To POLineList.Count - 1
Set curPOLine = POLineList.GetAt(j).PurchaseOrderLineRet
Set clsPO = New CPurchaseOrder
'transaction id
If Not curPO.TxnID Is Nothing Then
clsPO.TxnID = curPO.TxnID.GetValue
End If
'tran num
If Not curPO.TxnNumber Is Nothing Then
clsPO.TxnNumber = curPO.TxnNumber.GetValue
End If
'vendor
If Not curPO.VendorRef Is Nothing Then
If Not curPO.VendorRef.FullName Is Nothing Then
clsPO.VendorName = curPO.VendorRef.FullName.GetValue
End If
End If
'PO date
If Not curPO.TxnDate Is Nothing Then
clsPO.PoDate = curPO.TxnDate.GetValue
End If
'closed
If Not curPO.IsManuallyClosed Is Nothing Then
clsPO.IsManuallyClosed = curPO.IsManuallyClosed.GetValue
End If
'recd
If Not curPO.IsFullyReceived Is Nothing Then
clsPO.IsFullyReceived = curPO.IsFullyReceived.GetValue
End If
'item
If Not curPOLine.ItemRef Is Nothing Then
If Not curPOLine.ItemRef.FullName Is Nothing Then
clsPO.ItemName = curPOLine.ItemRef.FullName.GetValue
End If
End If
'order qty
If Not curPOLine.Quantity Is Nothing Then
clsPO.QtyOrdered = curPOLine.Quantity.GetValue
End If
'rec'd qty
If Not curPOLine.ReceivedQuantity Is Nothing Then
clsPO.QtyReceived = curPOLine.ReceivedQuantity.GetValue
End If
'line is closed
If Not curPOLine.IsManuallyClosed Is Nothing Then
clsPO.LineIsManuallyClosed = curPOLine.IsManuallyClosed.GetValue
End If
colPO.Add clsPO, CStr(curPOLine.TxnLineID.GetValue)
Next j
Next i
Set GetPurchaseOrder = colPO
End Function
The appropriate IResponse object is passed into this function, which then returns a collection. The collection is a bunch of CPurchaseOrder objects and can be passed to procedures that do calculations or write to worksheets or both. For every field that I want to store in the class, I have to determine if Quickbooks returned anything by comparing it to Nothing, then call the GetValue method to set the class module's property.
In addition to CPurchaseOrder and GetPurchaseOrder(), I have the following class module/function pairs: CAssembly, GetAssembly; CRawMaterial, GetRawMaterial; CSalesOrder, GetSalesOrder.
I don't need to use class modules for this. That's probably true for most uses of class modules. But there are a few reasons why I think it makes sense here. I could loop through all of the responses and immediately write the data to a worksheet, since that's all I'm doing in the end anyway. With class modules I can access the Quickbooks data once, create multiple sub procedures without having a bunch of hard-to-maintain module-level variables, and easily add new calculations/output.
Accessing Quickbooks data is key because of the requirement to check the objects for Nothing and use the GetValue method to actually retrieve the data. If I, for instance, wanted to write a subset of my sales orders to a worksheet, I would have to limit them with code like this:
Not curSO.IsManuallyClosed Is Nothing And _
Not curSO.CustomerRef Is Nothing Then
If curSO.CustomerRef.FullName Is Nothing Then
If curSO.CustomerMsgRef.FullName.GetValue Like "Loaner*" And _
Not curSO.IsFullyInvoiced.GetValue And _
Not curSO.IsManuallyClosed Then
I have to make sure that IsFullyInvoiced, IsManuallyClosed, and CustomerRef were returned by Quickbooks before I can access their values. Once that test is passed, I have to make sure that FullName was returned. I can't check for FullName Is Nothing until I know that CustomerRef is there. Once I know that all the objects are there, I can GetValue to actually test my conditions.
With a class module, I test each of these conditions only one time, then put the data into the classes' property. Now I can access the classes' property without all the rigmarole. Compare:
Not clsSO.IsFullyInvoiced And _
Not clsSO.SOIsManuallyClosed Then
So much cleaner and easier to read.
The classes also come in handy for splitting up my procedures. I could use module-level variables to pass the responses back and forth between the procedures, but it's easier and cleaner to pass everything in a collection. In order to put all that data into a collection, I create class modules which are held in the collection. I use sales orders to track product that I've loaned out to potential customers (kludgey workaround, thanks Intuit). I obviously also use sales orders to track sales that haven't shipped - their intended use. With all my sales orders in one class, I can call a WriteSalesOrders procedure and a WriteLoaners procedure and pass each the same collection of CSalesOrder objects.
Once I had this set up, I liked it so much that I decided to use sales orders to track product that needed repair. All the sales orders were already in the class, so I only had to write a WriteRepairs procedure to pull the appropriate sales orders out and stick them on a worksheet. That compartmentalization makes it easy to add and delete features without screwing up stuff you know already works.
Certain comments are subject to moderation and may not appear immediately. You can use HTML tags in your comment. If you include a greater-than or less-than sign or anything else that could be interpreted as HTML, your comment won't look nice. You need to escape those characters. To post VBA code in your comment, use [VB] tags, like this: [VB]Code goes here[/VB].
Leave a comment