## Rows to columns for price breaks

No doubt your sales team want to go on the road with a human friendly version of your prices for the customers to read. It is possible to do this with a SQL table function to extract the prices from GP with price breaks. The following example assumes you know how many price breaks you have in your price lists and will result in output something like the following screen shot. These results may be squirted into excel with more columns as described by your business requirements.

Two key SQL server functions that many people I find are not familiar with but are vital for this kind of data manipulation are; “ROW_NUMBER()” and “PARTITION BY” , one way to learn is to dive in with an example.

## GP Price Table

Natively the prices are held in the table IV00108 of your company database.

 ITEMNMBR CURNCYID PRCLEVEL UOFM TOQTY FROMQTY UOMPRICE WIRE100 Z-US\$ RETAIL Foot 100 0.01 0.35 WIRE100 Z-US\$ RETAIL Foot 999999999999.99 100.01 0.29 WIRE100 Z-US\$ RETAIL Spool 999999999999.99 0.01 190 WIRE100 Z-US\$ RETAIL Yard 999999999999.99 0.01 0.65 WIRE100 Z-US\$ EXTPRCLVL Foot 999999999999.99 0.01 0 WIRE100 Z-US\$ EXTPRCLVL Yard 999999999999.99 0.01 0 WIRE100 Z-US\$ EXTPRCLVL Spool 999999999999.99 0.01 0

There is a row per “price point”. Each row contains, the item sku, Currency of the price list, price list name, unit of measure, quantity break range and price.

This is unreadable to humans once you get, say 15,000 products, five currencies and ten or so price levels. From experience, one company this solution is used with has 1,623,586 rows in the price table IV00108.

## Table Partitioning

Firstly the rows are grouped together by the common factor each output row should be sharing. Each row in this example should have the same Item, Currency, Price Level and unit of measure. A row number is added for each successive row within this grouping;

`SELECT ITEMNMBR,CURNCYID, PRCLEVEL, UOMPRICE, FROMQTY, UOFM, TOQTY, ROW_NUMBER()         OVER(PARTITION BY             ITEMNMBR,PRCLEVEL, CURNCYID, UOFM             ORDER BY toqty ASC) AS 'RowNumber'FROM iv00108 (NOLOCK) WHERE itemnmbr='WIRE100'`

The above TSQL partitions the returned rows from IV00108 by ITEMNMBR,PRCLEVEL, CURNCYID, UOFM, for each row in the group a row number is generated by ROW_NUMBER() see the following output example. For this example, there are two quanity break columns for the prices of the “foot” unit of measure.
These are breaks of; 0.01+ and 100+, resulting in row numbers one and two for this unit of measure.

 ITEMNMBR CURNCYID PRCLEVEL UOMPRICE FROMQTY UOFM TOQTY RowNumber WIRE100 Z-US\$ EXTPRCLVL 0.00000 0.01000 Foot 999999999999.99 1 WIRE100 Z-US\$ EXTPRCLVL 0.00000 0.01000 Spool 999999999999.99 1 WIRE100 Z-US\$ EXTPRCLVL 0.00000 0.01000 Yard 999999999999.99 1 WIRE100 Z-US\$ RETAIL 0.35000 0.01000 Foot 100 1 WIRE100 Z-US\$ RETAIL 0.29000 100.01000 Foot 999999999999.99 2 WIRE100 Z-US\$ RETAIL 190.00000 0.01000 Spool 999999999999.99 1 WIRE100 Z-US\$ RETAIL 0.65000 0.01000 Yard 999999999999.99 1

Now that we have the RowNumber, this can act as the anchor field to crosstab the data with. It makes sense to wrap this query in a common table expression (CTE) to clean it up. The output from the below should be identical to that above.

`WITH PriceTable (ITEMNMBR, CURNCYID, PRCLEVEL, UOMPRICE, FROMQTY,UOFM, TOQTY,[RowNumber]) AS (SELECT ITEMNMBR,CURNCYID, PRCLEVEL, UOMPRICE, FROMQTY, UOFM, toqty, ROW_NUMBER()         OVER(PARTITION BY         ITEMNMBR,PRCLEVEL, CURNCYID,UOFM         ORDER BY TOQTY ASC) AS 'RowNumber'    FROM iv00108 (NOLOCK) where itemnmbr='WIRE100' )SELECT * FROM PriceTable`

## Crosstabbing the Common Table Expression

Now building on the select statement from the CTE, it is crosstabbed by using CASE statements as shown below. All that has changed between these two scripts is the select out of the CTE. The select is also add “+” to the price from column results as well as some NULL handling to make the presentation cleaner for Excel should it end up there. This is optional, it might be more appropriate for other uses to keep the results as numeric values and do that kind of processing in the reporting tool.

`WITH PriceTable (ITEMNMBR, CURNCYID, PRCLEVEL, UOMPRICE, FROMQTY,UOFM, TOQTY,[RowNumber]) AS (SELECT ITEMNMBR,CURNCYID, PRCLEVEL, UOMPRICE, FROMQTY, UOFM, toqty, ROW_NUMBER()         OVER(PARTITION BY         ITEMNMBR,PRCLEVEL, CURNCYID,UOFM         ORDER BY TOQTY ASC) AS 'RowNumber'    FROM iv00108 (NOLOCK) where itemnmbr='WIRE100' )select itemnmbr,   isnull(max(case when PriceTable.[RowNumber] = 1 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break1, max(case when PriceTable.[RowNumber] = 1 then     uomprice end) as Price1, isnull(max(case when PriceTable.[RowNumber] = 2 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break2, max(case when PriceTable.[RowNumber] = 2 then     uomprice end) as Price2,isnull( max(case when PriceTable.[RowNumber] = 3 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break3, max(case when PriceTable.[RowNumber] = 3 then     uomprice end) as Price3,isnull( max(case when PriceTable.[RowNumber] = 4 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break4, max(case when PriceTable.[RowNumber] = 4 then    uomprice end) as Price4, isnull(max(case when PriceTable.[RowNumber] = 5 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break5, max(case when PriceTable.[RowNumber] = 5 then    uomprice end) as Price5, isnull(max(case when PriceTable.[RowNumber] = 6 then     LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break6, max(case when PriceTable.[RowNumber] = 6 then     uomprice end) as Price6from PriceTablegroup by itemnmbr, curncyid, prclevel, UOFM;`

The above TSQL generates the following table, where the rows have been transformed into columns by TSQL, just as required.
 itemnmbr Break1 Price1 Break2 Price2 Break3 Price3 Break4 Price4 Break5 Price5 Break6 Price6 WIRE100 0+ 0.00000 NULL NULL NULL NULL NULL WIRE100 0+ 0.00000 NULL NULL NULL NULL NULL WIRE100 0+ 0.00000 NULL NULL NULL NULL NULL WIRE100 0+ 0.35000 100+ 0.29000 NULL NULL NULL NULL WIRE100 0+ 190.00000 NULL NULL NULL NULL NULL WIRE100 0+ 0.65000 NULL NULL NULL NULL NULL

## Table valued function

Great there we have it, price table partitioned and crosstabbed. Lets not stop there as this is much more useful as a table valued function. This is achieved by wrapping the above SQL as shown below. Here we have decided that the calling script should provide the currency, pricelist, item pattern and unit of measure to export. Your application may differ and not require the expensive type conversions.

`CREATE function [dbo].[Extract_PricesCrosstabTable] ( @CURNCYID varchar(15), @PRCLEVEL varchar(11), @ITEMPATTERN  nvarchar(31) = '%', @UOFM varchar(9) = '%')RETURNS @retTable TABLE (    [ITEMNMBR] [varchar](31) primary key NOT NULL,    [BREAK1] [varchar](255) NOT NULL,    [PRICE1] [numeric](19, 5) NULL,    [BREAK2] [varchar](255) NOT NULL,    [PRICE2] [numeric](19, 5) NULL,    [BREAK3] [varchar](255) NOT NULL,    [PRICE3] [numeric](19, 5) NULL,    [BREAK4] [varchar](255) NOT NULL,    [PRICE4] [numeric](19, 5) NULL,    [BREAK5] [varchar](255) NOT NULL,    [PRICE5] [numeric](19, 5) NULL,    [BREAK6] [varchar](255) NOT NULL,    [PRICE6] [numeric](19, 5) NULL)ASBEGINWITH PriceTable (ITEMNMBR, CURNCYID, PRCLEVEL, UOMPRICE, FROMQTY,UOFM, TOQTY,[RowNumber]) AS (SELECT ITEMNMBR,CURNCYID, PRCLEVEL, UOMPRICE, FROMQTY, UOFM, toqty, ROW_NUMBER()         OVER(PARTITION BY         ITEMNMBR,PRCLEVEL, CURNCYID,UOFM         ORDER BY TOQTY ASC) AS 'RowNumber'    FROM iv00108 (NOLOCK) where itemnmbr like @ITEMPATTERN and PRCLEVEL= @PRCLEVEL     AND CURNCYID=@CURNCYID AND UOFM LIKE @UOFM)INSERT @retTableselect itemnmbr,   isnull(max(case when PriceTable.[RowNumber] = 1 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break1, max(case when PriceTable.[RowNumber] = 1 then     uomprice end) as Price1, isnull(max(case when PriceTable.[RowNumber] = 2 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break2, max(case when PriceTable.[RowNumber] = 2 then     uomprice end) as Price2,isnull( max(case when PriceTable.[RowNumber] = 3 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break3, max(case when PriceTable.[RowNumber] = 3 then     uomprice end) as Price3,isnull( max(case when PriceTable.[RowNumber] = 4 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break4, max(case when PriceTable.[RowNumber] = 4 then    uomprice end) as Price4, isnull(max(case when PriceTable.[RowNumber] = 5 then    LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break5, max(case when PriceTable.[RowNumber] = 5 then    uomprice end) as Price5, isnull(max(case when PriceTable.[RowNumber] = 6 then     LTRIM( STR(FROMQTY,6,0)) + '+' end),'') as Break6, max(case when PriceTable.[RowNumber] = 6 then     uomprice end) as Price6from PriceTablegroup by itemnmbr, curncyid, prclevel, UOFM;RETURNEND;`

## Putting it to work

Now it is a table valued function, this allows a crosstabbed price table to be used as if it were a table. For example to add in the item description from the item master table IV00101, the following is used;

`SELECT IV00101.ITEMDESC, PricesCrossTab.* From Extract_PricesCrosstabTable('Z-US\$','RETAIL','WIRE%','Foot') PricesCrossTabJOIN IV00101ON PricesCrossTab.ITEMNMBR= IV00101.ITEMNMBR`

The unit of measure has been used as a parameter here for selection, however by changing the schema of the table valued function returned table type to include unit of measure as part of the primary key, all units of measure can be returned. This is the foundations of some scripts that can be amended to produce the results that you require for your particular circumstances.

### Problem

Suppose for a moment you wish to transfer credit card authorisation ticket information or other supporting transaction information from a website into your Dynamics GP database.

### Solution

You can piggyback on your eConnect integration rather than introducing your own integration. The minds behind eConnect thankfully provided some very easy to use extensibility points in the product.

It is really simple. By adding your own XML node to the XML document that is submitted to eConnect it is possible to move your data to the destination Dynamics database. A stored procedure is called there (stored proc name must match the XML node name) and in that stored procedure you can do anything at all -especially if you are into writing CLR code for SQL server.

### Example

In the following example we feed an order created by a website, as an XML document (xmlOrder) into a function. Contained within the xml of teh order are the authorisation details for online credit card authorisations. These details need to end up in a custom table in the Dynamics GP company database. The function returns a LINQ XElement that can then be inserted into the eConnect XML that is subsequently submitted to Dynamics GP via the eConnect API. Lets have a look at the VB.NET.

1. Public Function CreatePaymentTransactionXML(ByVal xmlOrder As XmlDocument) As XElement
2.         Dim oCustomNode As XElement = _
3.         <eConnect_InsertCardPayment>
4.             <SOPNUMBE><%= CurrentSOPNumber %></SOPNUMBE>
5.             <SOPTYPE><%= 2 %></SOPTYPE>
6.             <VPSTxID><%= xmlOrder.SelectSingleNode("/order/Credit-Card-Tx-Code").InnerText %></VPSTxID>
7.             <TxAuthNo><%= xmlOrder.SelectSingleNode("/order/Credit-Card-Auth-No").InnerText %></TxAuthNo>
8.             <SecurityKey>9999</SecurityKey>
9.             <VendorTxCode><%= xmlOrder.SelectSingleNode("/order/Credit-Card-Order-No").InnerText %></VendorTxCode>
10.             <Amount><%= 0 %></Amount>
11.             <Currency><%= xmlOrder.SelectSingleNode("/order/Currency-Id").InnerText %></Currency>
12.             <VendorName><%= "mycompanyname" %></VendorName>
13.         </eConnect_InsertCardPayment>
14.         Return oCustomNode
15.     End Function

The example uses the VB.NET literal LINQ xml syntax, it is great for working with these kinds of small XML fragment constructs. The value for each node is pulled out of the sales order XML using XPath statements. Note the example is not yet complete, for example the amount field is tied to a static value of zero but it is functional enough to show the process.

Take notice of the name of the element,“<eConnect_InsertCardPayment>”, this name is also the name of the stored procedure called in the company database. So now a stored procedure needs creating on the company database that will be the end point for this transaction. In this example that stored procedure will insert the data held in the XML to a row in a custom database table.

The custom XML fragment we have just created is inserted into the XML document submitted to eConnect. Add the custom XML node nested inside eConnect transaction type.

The developer can take control of when the procedures are called. The choices are before or after the eConnect procedures have executed. Use the <eConnectProcessInfo> node to do this. The <eConnectProcessInfo> node should always immediately follow the transaction type node. It looks like this:
<eConnectProcessInfo>
<eConnectProcsRunFirst>TRUE</eConnectProcsRunFirst>
</eConnectProcessInfo>

One of these process info nodes can be added per transaction in the XML document submitted, executing this in VB code looks like this;
1. Dim oeConnectProcessInfo As New eConnectProcessInfo
2.         oeConnectProcessInfo.eConnectProcsRunFirst = "TRUE"
3.         oeConnectType.SOPTransactionType(0).eConnectProcessInfo = oeConnectProcessInfo

### Dynamics GP Company Custom Stored Procedure

Each XML element within our custom XML fragment is passed into the stored procedure as a parameter with the same name as shown below (proceeded with @I_v for input variable). You should therefore make certain your elements are named uniquely. Within the stored procedure, as a developer you can achieve what you want. In this case the payment card fulfilment information is merely inserted into a table for later processing or reporting.

`CREATE PROCEDURE [dbo].[eConnect_InsertCardPayment]`
`( @I_vSOPNUMBE varchar(31),`
`  @I_vSOPTYPE smallint,`
`  @I_vVPSTxID nvarchar(50),`
`  @I_vTxAuthNo nvarchar(50),`
`  @I_vSecurityKey nvarchar(50),`
`  @I_vVendorTxCode nvarchar(40), `
`  @I_vAmount money =0,`
`  @I_vCurrency char(3),`
`  @I_vVendorName varchar(20),`
`  @O_iErrorState int output, /* Return value: 0 = No Errors, Any Errors > 0 */`
`  @oErrString varchar(255) output /* Return Error Code List */`
`  )`
`  AS`
` `
`  declare `
` `
`    @O_oErrorState int,`
`    @iError int,`
`    @iStatus smallint,`
`    @iAddCodeErrState int`
` `
`/*********************** Initialize locals *****************************************************/`
`select    @O_iErrorState = 0,`
`    @oErrString = '',`
`    @iStatus = 0,`
`    @iAddCodeErrState = 0`
` `
`INSERT INTO [my_PaymentCardTransaction]`
`           ([TxType]`
`           ,[Status]`
`           ,[StatusDetail]`
`           ,[VPSTxID]`
`           ,[TxAuthNo]`
`           ,[SecurityKey]`
`           ,[VendorTxCode]`
`           ,[Amount]`
`           ,[Currency]`
`           ,[SOPTYPE]`
`           ,[SOPNUMBE]`
`           ,[VendorName]`
`           ,[ModifiedDate]`
`           ,[CreatedDate]`
`           )`
`        VALUES`
`           ('DEFERRED',`
`           'OK',`
`           '',`
`           @I_vVPSTxID,`
`           @I_vTxAuthNo,`
`           @I_vSecurityKey,`
`           @I_vVendorTxCode,`
`           @I_vAmount,`
`           @I_vCurrency,`
`           @I_vSOPTYPE, `
`           @I_vSOPNUMBE,`
`           @I_vVendorName,`
`           getdate(),`
`           getdate()          `
` `
`           )`
`  return (@O_iErrorState)`

### But wait there is more

For all the eConnect operations there is a rudimentary “event model” made available via stored procedures. For example inserting a SOP Sales Document will cause the taSOPHdrIvcInsertPre and taSOPHdrIvcInsertPost procedures in the company database to be called before the record is inserted into SOP10100 and after respectively.

You are allowed to open up these procedures and code your own functionality in there. Perhaps you have some third party modifications that don’t natively support eConnect. You could set some database values in these third party tables using the taSOPHdrIvcInsertPost procedure, after the sales order has been inserted. As this is happening after it has been inserted you have all the defaulted values from the sales order document type/classes available to you in the procedure together with the data inserted from the supplied eConnect XML document.

Perhaps you need to validate the sales order information, say for example checking the customer has not supplied a duplicate PO reference to you. This could be done in the taSOPHdrIvcInsertPre, from where you could raise an error should validation fail.

Another useful technique is to combine the custom XML node technique discussed earlier with this event model. Use the custom node to insert the data into a cache table then pull it out and use it in earnest in the post event of the document transaction once all the defaults have been set. One benefit may be to keep the bulk of the sql in one place for clarity and ease of maintenance.

Another idea is to send an email to the sales team from inside the post stored procedure of the transaction, each time a sales order is created by eConnect. Sales staff now get a neat notification that there is a web sales order to process. If you are into writing CLR stuff in SQL you could go almost anywhere with this. Perhaps an alternative to the notification email would be to create a Microsoft Sharepoint task for someone to deal with the order and spin off a workflow to keep the process on track.

Go ahead use your imagination in our implantation, like I have in ours.

### Further references

Recommend reading are the MSDN pages on this subject to get the full understanding and further examples of options for eConnectProcessInfo. Full schemas for eConnect are also provided there.

MSDN: eConnectProcessInfo http://msdn.microsoft.com/en-us/library/bb648359.aspx

MSDN Custom XML Nodes: http://msdn.microsoft.com/en-us/library/bb625125.aspx

If you need to integrate with Microsoft Dynamics GP one of the options you may choose is to use the eConnect product. eConnect is an API that allows you to submit XML documents to Dynamics GP to perform CRUD operations on most of the document types in GP. Using .NET for integrations, if any issues/problems arise from submissions to eConnect,  via eConnect_EntryPoint or eConnect_Requester methods, then these errors are surfaced as eConnect exceptions. The .Message property of this class contains the error text.

The econnectException class returns the message from the eConnect stored procedure that originated the problem. A table of these messages is held in SQL server, DYNAMICS database, table taErrorCode. This table gives an idea of the error conditions you may not have thought possible and lets you be a bit more proactive at handling errors.

To see the contents of this table see this file:

### Item does not allow back orders

Lets choose an example problem that you may experience. When submitting a SOP sales order document, normally you want any items that are out of stock to be back ordered. This is easy, set the QtyShrtOpt = 4 in the XML to back order the balance. However if you have a situation where you have a telesales team hammering in orders as well as a website taking orders, even with SQL replication you can occasionally get a scenario where a web order comes in for an item that has been set to disallow web orders and it no longer has enough stock to satisfy the web order. This may be due to latency in updating stock on the website for example. eConnect lets us know with the following exception: “Microsoft.GreatPlains.eConnect.eConnectException: Error Number = 4776”

### Gracefully dealing with it

There a few ways I can think of to deal with this, the chosen one is to change the QtyShrtOpt = 6 for the line in question inorder to cancel the qty that can not be allocated from stock. It is wise to then set an order note to let the sales staff who will process the order know about the issue so it can be resolved with the customer, perhaps alternative item offered.

### .NET code

A regular expression is used to parse the eConnect exception text. This allows easy detection of what error has occurred and extracts the item number for the order line raising the exception.

1. Dim ItemNumberList As New List(Of String)
2. 'Get the item numbers that exhibit this error
3. For Each CurrentMatch As RegularExpressions.Match In _
4.     RegularExpressions.Regex.Matches(ErrorText, _
5.         "(<taSopLineIvcInsert>.*<ITEMNMBR>(.*)</ITEMNMBR>.*</taSopLineIvcInsert>) --->.*Error Number = 4776", _
6.         RegularExpressions.RegexOptions.Singleline)
7.     If CurrentMatch.Groups.Count > 1 Then
8.         'capture 2 has the itemnmbr
10.     End If
11. Next CurrentMatch

Having a list of item numbers with this issue allows us to change the XML of the document being submitted to alter the quantity shortage option flag to cancel the balance (option 6). Promoting LINQ for XML work, load the XML document into a XDocument (LINQ XML Document) class.

1. Dim salesdoc As XDocument = XDocument.Parse(xmlSalesOrder.OuterXml)

Now use a LINQ query to get all the elements that need the backorder option changing and change it to 6.

1. For Each CurrentItem In ItemNumberList
2.     Dim CurrenItemVar As String = CurrentItem
3.     For Each CurrentElement As XElement In From salesline In salesdoc.Elements.Descendants("taSopLineIvcInsert") _
4.                     Where salesline.Element("ITEMNMBR").Value.StartsWith(CurrenItemVar) _
5.                     Select salesline
6.         CurrentElement.SetElementValue("QtyShrtOpt", "6")
7.     Next
8.     salesdoc.Elements.Descendants("taSopHdrIvcInsert")(0) _
9.       .SetElementValue("NOTETEXT", _
10.         salesdoc.Elements.Descendants("taSopHdrIvcInsert")(0).Element("NOTETEXT").Value & vbCr & String.Format( _
11.         "Item: {0} could not be fully ordered due to no back order allowed flag set on item and lack of stock.", CurrenItemVar))
12. Next
For each item fixed, the XML of the document we are submitting has order notes appended to take account of the fact there is an issue with this item. The sales are already always reading the order notes for customer comments coming through from the website so should catch these notes.

Now the altered XML document is resubmitted to Dynamics GP via eConnnect. If it fails this time there is an issue that we have not programmed for so it needs administrative intervention.

To load the xDocument back into the XMLDocument class for submission to eConnect,

### Note on security

Beware exposing your ERP system to your website – if the website gets compromised, then so is your business. eConnect allows most of your business data to be altered and queried - this is something to be very careful of. With the implementation I created, the XML document is punched through the firewalls to a custom web service on the GP segment of the network. This web service only lets through the specific eConnect documents we want to allow through and only those that meet specific criteria to limit the attack potential.

### Summary

By adding to this example, common errors your eConnect integration encounters could be eliminated so IT staff are prevented from spending time supporting disruptive day to day integration issues. Thus these problem cases are handed back to the process owner, be that; buyers, warehouse, or sales staff.

After installing security advisory 960715 , Microsoft Dynamics GP clients went down. This happens on log in to GP, raising exceptions due to missing objects.

It turns out that there are still a few VBA forms used by the GP instance that have not yet been migrated to .NET. These forms used the msdatgrd.ocx control and some others to that it turns out have been identified as having security flaws. There is a mechanism in place that allows a “kill bit” to be issued against an Active X control to kill it off. This is one of the things the security update does, killing your application that uses these controls.

The was not entirely clear a few months ago when the issue was first encountered, but since then research explained the corrective actions required. Basically installing the newer version of Visual Studio 6 installs the newer safe controls into the windows system32 directory. Deleting the cache copies of these controls from C:\Documents and Settings\[username]\Application Data\Microsoft\Forms\ (*.exd) and regsvr32ing the controls sorts it all out.

You must have developer rights to the control i.e. Visual Studio. Luckily since I worked all this out the Visual Studio Office Developer (VSOD) Support Team have put together a comprehensive summary of all the information and guidance required to tackle this issue on these pages here. http://blogs.msdn.com/vsod/archive/2009/06/05/visual-basic-6-controls-stop-working-after-security-advisory-960715.aspx

Meanwhile these VBA forms are to be made redundant by moving them to .NET, exposed through COM Callable Wrapper (CCW).