Archive for the ‘Tutorials’ Category.

VB(A) and InternetExplorer and XMLHttp

Over the past few weeks, I’ve had reason to explore the use of VBA to access information on web pages and through web services using both InternetExplorer and XMLHttp. While my study of the two is far from exhaustive, I decided to document the research for general consumption expecting it to take a few hours. As I wrote more issues cropped up and the “few hours” project took several days. But, it’s finally uploaded at
VBA & web services
http://www.tushar-mehta.com/publish_train/xl_vba_cases/vba_web_pages_services/index.htm

Website update

I imagine this is the way it is for most people:

Vast number of ideas of what I’d like to do
    A few get implemented as add-ins, utilities, tutorials, what have you
        Even fewer find their way to the website
            And, it takes even longer to find a proper location for very new topics.

I spent the better part of today adding one new tutorial, one new template, and hooking up several old pages that were already part of the www.tushar-mehta.com website but were never part of the site navigation structure. In the process, I added a new high level category called “Templates.” The following introduces each item.

Enhanced Find: This is a source form utility that I wrote for my own use. I imagine it would have been much simpler if Microsoft had added the UI ‘Find All’ feature to the Excel object model. But, it didn’t, and, in any case, the utility does a lot more than just ‘find all.’ To those who will want to tell me I should have used the TreeView control in the userform used to display the result: I did. {grin} Then, I decided it would be prudent to forgo it since I don’t know how that control finds its way onto a particular machine.

Chart Image to Data: I wrote this utility to help out someone who wanted to convert an image of a graph into the associated data points. No, it doesn’t use artificial intelligence, or OCR, or some fancy heuristic to figure out the data. {grin} Instead, it relies on the user first calibrating the image and then clicking on various points on a series to convert the click location into a data value.

The templates hierarchy: Several templates that I created for various reasons have languished on the website with no good place to put them. So, I finally broke down and added a high level category, Templates. As usual, whenever a file moves from one directory to another, I leave — or at least try to leave — a stub in the old location pointing to the new one.

Solver template to find a subset of entries that add up to a given total: One would expect that when someone makes a payment against a list of different charges (invoices), they would list which charges invoices are being paid. Yet, every so often we find a request for help from someone who has a total and now wants to find a possible subset of pending payments that total the payment amount. This template contains the necessary Solver set up for the task. Obviously, this is not an easy task and the ability to find an acceptable solution is limited both by the requirements and limitations placed on Solver and of the algorithm used by Solver.

A calendar template: Even if I say so myself, I think this is worth checking out. It’s a template that lets one generate a calendar for any year from 1900 to 3000 with the starting-day-of-week set to any weekday. The result can be in one of 2 formats — one month-per-page or 12 months-per-page. The design is clean and simple. And, best of all, no programming!

Compare sizes of TVs, computer monitors, and broadcasts: Over the past month or so, I replaced my dying TV with a HD TV and added a widescreen computer monitor. In the process, I spent a fair amount of time understanding what’s what with widescreen devices and HD broadcasts. This template addresses one specific aspect of that research: comparing the sizes of various devices. One thing I realized very quickly in my research was that a widescreen device would have to be somewhat larger than its standard counterpart if it was to have at least the same height. I did most of my work with algebra. But, then decided I’d share some of it in the form of a template. While creating the template I also realized it could be used to explain the bands that appear when one views a broadcast in one format (say widescreen) on an incompatible TV (i.e., a standard size TV). I also realized that the work in the template would form a good tutorial for data validation.

Data Validation I: There’s a whole bunch of stuff I have on data validation that I’d like to share. Given the big weakness in Excel’s native data validation (copy+paste into a cell to wipe out the validation criterion in it!), I prefer alternatives. This tutorial documents some simple ways to validate data without Excel’s Data Validation or VBA. There’s a whole bunch more I will share in the weeks/months to come.

Userform coding interface: I started work on a VBA chapter — and it is far from complete — that addresses two favorite topics of mine: (1) The structure of the interface between the code that manages a userform and the code that does the actual task of a utility, and (2) The RefEdit control including a workaround that lets one simulate its functionality in a modeless userform, together with a class module that lets one “drop” the solution into any userform.

Quickbooks SDK Part IV

Part I: Introduction and Controlling Procedure
Part II: Creating the Quickbooks Queries
Part III: Filling Class Modules
Part IV: Writing to Worksheets

In the last part, I take the collection of custom objects from Part III and write each object to a worksheet. For instance, I filled a collection with CSalesOrder objects and now I pass that collection to the following procedure. It accesses the properties of the CSalesOrder class to determine if that specific sales order belongs on the backorder report and writes certain columns to a table on the worksheet wshSO.

Sub WriteBackorder(ByRef colSO As Collection)
   
    Dim clsSO As CSalesOrder
    Dim i As Long
    Dim rStart As Range
    Dim lRow As Long
    Dim aTitle As Variant
   
    With wshSO.Range("A1:K1")
        .Parent.UsedRange.ClearContents
        Set rStart = .Item(1)
        aTitle = Array("TxnID", "TxnNumber", "CustomerRefFullName", "RefNumber", _
            "DueDate", "ShipDate", "IsManuallyClosed", "IsFullyInvoiced", "SalesOrderLineItemRefFullName", _
            "SalesOrderLineQuantity", "SalesOrderLineInvoiced")
        .Value = aTitle
        .Font.Bold = True
    End With
   
    lRow = 1
   
    For i = 1 To colSO.Count
        Set clsSO = colSO.Item(i)
           
        If Not clsSO.IsLoanerLog And _
            Not clsSO.IsFullyInvoiced And _
            Not clsSO.SOIsManuallyClosed And _
            Not clsSO.IsInRepair Then
       
            rStart.Offset(lRow, 0).Value = clsSO.TxnID
            rStart.Offset(lRow, 1).Value = clsSO.TxnNumber
            rStart.Offset(lRow, 2).Value = clsSO.CustName
            rStart.Offset(lRow, 3).Value = clsSO.SONum
            rStart.Offset(lRow, 4).Value = clsSO.DueDate
            rStart.Offset(lRow, 5).Value = clsSO.PromiseDate
            rStart.Offset(lRow, 6).Value = clsSO.SOIsManuallyClosed
            rStart.Offset(lRow, 7).Value = clsSO.IsFullyInvoiced
            rStart.Offset(lRow, 8).Value = clsSO.InventoryItem
            rStart.Offset(lRow, 9).Value = clsSO.QtyOrdered
            rStart.Offset(lRow, 10).Value = clsSO.QtyShipped
               
            lRow = lRow + 1
        End If
    Next i
   
    AdjustNames wshSO, aTitle, lRow
   
End Sub

I can pass this same collection to the WriteRepairs procedure where I can get at the sales orders that have been designated as repairs.

If clsSO.IsInRepair And _
            Not clsSO.IsFullyInvoiced And _
            Not clsSO.SOIsManuallyClosed Then

At the end of the "write" procedures, I call the AdjustNames procedure, passing it an array of headings and the number of the last row to which I wrote data. This procedure adjusts the defined names on the worksheet, which have the same name as the heading, to encompass all of the data.

Sub AdjustNames(ws As Worksheet, vArr As Variant, lRow As Long)
   
    Dim i As Long
   
    For i = LBound(vArr) To UBound(vArr)
        ws.Names(vArr(i)).RefersTo = "=" & ws.Cells(2, i + 1).Resize(lRow).Address(True, True, xlA1)
    Next i
           
End Sub

At this point, I have several worksheets of data that I can access with formulas and summarize however I wish.

Download QBMRP.zip.

<< Part III

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 msTxnID As String
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.

Function GetPurchaseOrder(ByRef CurResp As IResponse) As Collection
 
    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:

If Not curSO.IsFullyInvoiced Is Nothing And _
     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:

If clsSO.CustName Like "Loaner*" And _
            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.

<< Part II Part IV >>

Quickbooks SDK Part II

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 entry point procedure that controls the flow. Once the connection to Quickbooks is made, I create XML requests for each query I want to run. I'm using the QBFC object model so I don't have to put the XML together myself. For each query, I add the columns that I want. I found that bringing over the whole table takes significantly longer than if I limit the number of columns.

For the sales orders, I set the boolean to include line items. The line items are what contain the inventory item detail. Without the line items, I would just be getting sales order header type information which is of no use to me. The same goes for the purchase orders.

For the inventory (assemblies and raw materials), I filter the list to include only active inventory items. Items that aren't bought or sold have been made inactive and I don't need them. There are plenty of other filters that can be applied to inventory items, sales orders, or just about anything else you get from Quickbooks. The documentation for the SDK lists all the filters that can be applied. I could, for instance, only pull sales orders that were created in the last year.

To include columns, I "add" the field name to the IncludeRetElementList object. If I spell the field name wrong, it returns nothing; no error, it just ignores that column. Most fields are simple values, like TxnID is a String value. Some fields are structures within themselves. CustomerRef returns a reference to the customer from which you can get at other customer related properties. You can't pull in just the customer name, though, you have to pull the whole CustomerRef structure.

Here are the four procedures that set up the queries (XML requests) that will be sent to Quickbooks:

Sub QuerySalesOrder(ByRef qrySO As ISalesOrderQuery)
 
    'Identiry columns to return
    With qrySO.IncludeRetElementList
        .Add "TxnID"
        .Add "TxnNumber"
        .Add "CustomerRef"
        .Add "RefNumber"
        .Add "DueDate"
        .Add "ShipDate"
        .Add "IsManuallyClosed"
        .Add "IsFullyInvoiced"
        .Add "ShipAddress"
        .Add "SalesOrderLineRet"
    End With
   
    'Include line details
    qrySO.IncludeLineItems.SetValue True
   
End Sub
 
Sub QueryPurchaseOrder(ByRef qryPO As IPurchaseOrderQuery)
 
    With qryPO.IncludeRetElementList
        .Add "TxnID"
        .Add "TxnNumber"
        .Add "VendorRef"
        .Add "TxnDate"
        .Add "RefNumber"
        .Add "ExpectedDate"
        .Add "IsManuallyClosed"
        .Add "IsFullyReceived"
        .Add "PurchaseOrderLineRet"
    End With
   
    qryPO.IncludeLineItems.SetValue True
   
End Sub
 
Sub QueryAssembly(ByRef qryAssembly As IItemInventoryAssemblyQuery)
 
    With qryAssembly.IncludeRetElementList
        .Add "ListID"
        .Add "Name"
        .Add "FullName"
        .Add "IsActive"
        .Add "BuildPoint"
        .Add "QuantityOnHand"
        .Add "AverageCost"
        .Add "QuantityOnOrder"
        .Add "QuantityOnSalesOrder"
        .Add "Sublevel"
    End With
 
    qryAssembly.ORListQuery.ListFilter.ActiveStatus.SetValue asActiveOnly
   
End Sub
 
Sub QueryRaw(ByRef qryRaw As IItemInventoryQuery)
 
    With qryRaw.IncludeRetElementList
        .Add "ListID"
        .Add "Name"
        .Add "FullName"
        .Add "IsActive"
        .Add "ReorderPoint"
        .Add "QuantityOnHand"
        .Add "AverageCost"
        .Add "QuantityOnOrder"
        .Add "QuantityOnSalesOrder"
        .Add "PurchaseDesc"
    End With
   
    qryRaw.ORListQuery.ListFilter.ActiveStatus.SetValue asActiveOnly
   
End Sub

<< Part I Part III >>

Quickbooks SDK Part I

Part I: Introduction and Controlling Procedure
Part II: Creating the Quickbooks Queries
Part III: Filling Class Modules
Part IV: Writing to Worksheets

The Quickbooks SDK is the only way I know of to get data out of Quickbook's proprietary database format and into Excel (SDK=Software Developer's Kit, if you didn't know). It's an XML based system; you send an XML request and it returns and XML response. Assuming it was able to fill your request, the XML response will be a structure filled with the data you requested.

There is a very, very thin object model wrapper that will take care of the XML for you. Thin though it may be, it's still better than creating my own XML strings. Inexplicably, everything in the OM is a method, that is, there are no properties. If you want to get the customer's name, it looks like

x = CustomerName.GetValue

rather than just

x = CustomerName.Value

or even

x = Customer.Name.

But that's not all. If there is no CustomerName object associated with your request, the CustomerName object will be Nothing and the GetValue method will fail. So now to get the customer's name, you need code like this:

If Not CustomerName Is Nothing Then
     x = CustomerName.GetValue
End If

For every "property", you need to test for the nothingness of object, then call it's GetValue method. It seems like it would have been just as easy to create a more robust object model that had real objects, properties, and methods. I would prefer a Customer object with a Name property rather than a CustomerName object. The result is that the code you need to write, compared to say using ADO to access a SQL database, is extremely long. The correct answer is for Intuit to provide an ODBC driver that accesses the data directly. Currently, there is a third party ODBC driver that uses the XML request/response system. In addition to long code, everything runs significantly slower than ADO.

I wanted to put some code out there for other people who may want to do the same things. I didn't want to spend all my time complaining about the SDK, although I can see that's unavoidable. In its defense, the SDK is extremely well documented. You may not like the way you have to do it, but you'll know how to do it. Also, the developer community is very active and helpful.

For my first installment, I'm posting my main procedure. There's a hyperlink on a worksheet that displays the last time this procedure was run. Because it takes more than five minutes to run, I don't want to run it if the information is fairly up to date. The FollowHyperlink event is used to run this procedure when the hyperlink is clicked.

The purpose of this workbook is to manage inventory. I need to have several tables of information to tell me how many pieces I have on hand, how many are out on loan, how many are in repair, how many are on order from vendors, and how many are due to customers. If Quickbooks had an "inventory location" feature, I wouldn't need any of this, but they don't.

The flow of the procedure starts with the standard Quickbooks stuff. I have to create a session, open a connection, begin the session, etc. Once I have a session up and going, I need to create the requests. For my purposes, I have four requests; sales orders, purchase orders, inventory items, and inventory assemblies. For each request, I create a AppendxxxxQuery, then pass it to a procedure that builds the query. I'll post that procedure in a future installment.

I end up with more tables in my workbook than requests because some of my tables are subsets of Quickbooks' tables. My inventory on loan, for instance, is recorded as a sales order to a dummy customer. I query all the sales orders from QB and pull out the loaners to create that table.

Once my queries are completed, I send the request to Quickbooks and get a response back. This takes the longest by far. There's no callback so that I can update a progress bar either. Because I sent four requests, I should get four responses. Each response is sent to a procedure that fills a collection with custom objects. The procedures and the class modules will come in a future post. Setting up the class modules is a pain, but the alternative is checking the stupid objects for Nothing and using GetValue to get the values. This way, I have an object called Assembly which has a number of properties like the one called QtyOnHand. I can get that value like I would a property of an object in Excel's object model.

Now that I have four collections filled with CAssembly, CPurchaseOrder, CRawMaterial, and CSalesOrder objects, I pass those collections to 'Write' procedures that loop through the objects and write tables to worksheets.

That's pretty much it. I have some other procedures that filter items out of these tables and I have a bunch of formulas that draw info out, but that's the meat of it. I'll post the other procedures and class modules soon. I don't know if I'll have a download associated with this. While I think it's nice to see everything in one place, it will hardly work with just an Quickbooks install. Here's the entry point code:

Sub UpdateQBData()
   
    Dim SessMgr As QBSessionManager                         'Top level object
    Dim msgReq As IMsgSetRequest                            'Container for xml request
    Dim qrySO As ISalesOrderQuery
    Dim qryPO As IPurchaseOrderQuery
    Dim qryAssembly As IItemInventoryAssemblyQuery
    Dim qryRaw As IItemInventoryQuery
   
    Dim resp As IMsgSetResponse                             'Container for xml responses
    Dim respList As IResponseList                           'Collection of responses
    Dim CurResp As IResponse                                'Object variable to loop through IResponseList
   
    Dim colSO As Collection                                 'collection of sales orders
    Dim colPO As Collection                                 'collection of purchase orders
    Dim colAss As Collection                                'collection of inventory assemblies
    Dim colRaw As Collection                                'collection of raw materials
   
    Dim lRespCnt As Long
   
    On Error GoTo ErrorHandler
   
    Application.DisplayAlerts = False
   
    'Calls a pseuod-progress bar userform
    UUpdate.Show
    UUpdate.lbxStatus.AddItem "Creating Queries..."
    DoEvents
   
    'Instantiate variable
    Set SessMgr = New QBSessionManager
    SessMgr.OpenConnection "", "AIM_Prod"
    SessMgr.BeginSession "\\Actserver\QBData\AIM-USA.QBW", omMultiUser
   
    'Create a container for the input message
    Set msgReq = SessMgr.CreateMsgSetRequest("US", 5, 0)
    msgReq.Attributes.OnError = roeContinue
   
    'Create a query to hold details
    Set qrySO = msgReq.AppendSalesOrderQueryRq
    QuerySalesOrder qrySO
   
    Set qryPO = msgReq.AppendPurchaseOrderQueryRq
    QueryPurchaseOrder qryPO
   
    Set qryAssembly = msgReq.AppendItemInventoryAssemblyQueryRq
    QueryAssembly qryAssembly
   
    Set qryRaw = msgReq.AppendItemInventoryQueryRq
    QueryRaw qryRaw
           
    UUpdate.lbxStatus.AddItem "Retrieving Data from Quickbooks..."
    DoEvents
   
    'Send the request to QB and get a response
    Set resp = SessMgr.DoRequests(msgReq)
   
    'Put all responses in a collection
    Set respList = resp.ResponseList
   
    UUpdate.lbxStatus.AddItem "Processing Data..."
    DoEvents
   
    'Loop through responses
    For lRespCnt = 0 To respList.Count - 1
        Set CurResp = respList.GetAt(lRespCnt)
       
        If CurResp.StatusCode = 0 Then  'Status code indicates success
           
            'Fill a collection based on the type of response
            Select Case CurResp.Type.GetValue
                Case rtSalesOrderQueryRs
                    Set colSO = New Collection
                    Set colSO = GetSalesOrder(CurResp)
                   
                Case rtPurchaseOrderQueryRs
                    Set colPO = New Collection
                    Set colPO = GetPurchaseOrder(CurResp)
               
                Case rtItemInventoryAssemblyQueryRs
                    Set colAss = New Collection
                    Set colAss = GetAssembly(CurResp)
               
                Case rtItemInventoryQueryRs
                    Set colRaw = New Collection
                    Set colRaw = GetRaw(CurResp)
               
            End Select
        End If
       
    Next lRespCnt
   
    UUpdate.lbxStatus.AddItem "Writing Data to Worksheets..."
    DoEvents
   
    'Write the data to the sheets
    WriteBackorder colSO
    WriteLoaners colSO
    WriteRepairs colSO
    WritePurchases colPO
    WriteAssembly colAss
    WriteRawMats colRaw
    WriteFinishedGoods colAss
   
    'Update the text of a hyperlink that calls this sub
    wshSummary.Range("LastUpdate").Value = "LastUpdated: " & Format(Now, "m/d/yyyy h:mm AM/PM")
   
ErrorExit:
    On Error Resume Next
    Application.DisplayAlerts = True
    SessMgr.EndSession
    SessMgr.CloseConnection
    Set SessMgr = Nothing
    Unload UUpdate
    Exit Sub
 
ErrorHandler:
    MsgBox Err.Number & vbNewLine & vbNewLine & Err.Description
    Stop
    Resume ErrorExit
 
End Sub

Part II >>