Creating ZIP files

We recently needed to generate a ZIP file from X++ and fairly quickly realised that we could do this quite easily with calls to the .NET framework. Its not likely to be something you do every day but we thought it worthwhile sharing how it was done in any case.

The code sample requires .NET Framework 4.5 (supported on AX 2012 RTM CU5 and above) and shows how a ZIP file can be both created and extracted.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    CodeAccessPermission    perm;
    System.Exception        ex;
 
    str filePath    = @'C:\Temp\ArchiveDocs\TestZipFolder';
    str zipPath     = @'C:\Temp\ArchiveDocs\test.zip';
    str extractPath = @'C:\Temp\ArchiveDocs\UnZip';
 
    try
    {
        perm = new InteropPermission(InteropKind::CLRInterop);
        perm.assert();
 
        //Archive folder content to file
        System.IO.Compression.ZipFile::CreateFromDirectory(filePath, zipPath);
 
        //extract Zip to folder
        System.IO.Compression.ZipFile::ExtractToDirectory(zipPath, extractPath);
 
        CodeAccessPermission::revertAssert();
    }
 
    catch (Exception::CLRError)
    {
        ex = ClrInterop::getLastException();
        if (ex != null)
        {
            ex = ex.get_InnerException();
            throw error(ex.ToString());
       }
    }

Tax amount override in free text invoices

Introduction

One of the common issues we encounter when importing free text invoices into Dynamics AX is overriding the tax. Usually AX calculates the tax when sales invoice lines are inserted into the free text invoice. On some occasions, such as when integrating AX with external systems, users will want the tax calculated by AX to be overridden with the value form the external system so that the tax in  both system matches to the penny.

Override the tax manually

To override tax manually in free text invoices, go to Sales ledger/Common/Free text invoices/All free text invoices. Choose one of the free text invoices that has not been posted yet (e.g. no invoice Id was assigned). Then from the action pane, click on VAT under Details group. This will open a form that will look like this…

B1

Click on the Adjustment tab and you will see the following:

B2

In the form above, you can tick the “Override calculated VAT” field and then set the new tax value in the “Actual VAT amount” text box. Before you close the form, click apply.

Override the tax in x++

We will do the same exercise but from x++ code. There are many blogs and articles on how to do this but in order not to jump into different websites to get the code, I copied the code from a random website here below and then added my code afterwards.

So, the original code will look something like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void FreeTextTaxAdjust(Args _args)  
{  
  CustInvoiceTable    custInvoiceTable;  
  CustInvoiceCalcTax  custInvoiceCalcTax;  
  TaxFreeInvoice      taxFreeInvoice;  
  TaxRegulation       taxRegulation;  
  ;  
 
  ttsbegin;  
 
  select firstonly custInvoiceTable  
  where custInvoiceTable.RecId == 54371652769;  
 
  custInvoiceCalcTax = new CustInvoiceCalcTax_Table(custInvoiceTable);  
  taxFreeInvoice     = new TaxFreeInvoice(custInvoiceCalcTax);  
  taxFreeInvoice.calc();  
  taxRegulation = TaxRegulation::newTaxRegulation(taxFreeInvoice);  
  taxRegulation.allocateAmount(2.01);  
  taxRegulation.saveTaxRegulation();  
 
  ttscommit;  
}

As shown in the code above, this will override the total tax amount of the free text invoice from the automatically calculated £2 to £2.01. The issue with this is that AX will merge the taxes by the VAT code. So, if in the form above we had two tax codes (STD and RED) instead of only one tax code (STD), then the code above will not handle it. The code above will only allow inserting the total tax amount for both tax codes. It will not allow splitting the values for each tax code.

I managed to fix the issue by splitting the above into two steps:

  1. A method to get the tax code. In my case, I cross reference the Tax Group with the Tax Item Group to get the tax code. This can be done by using the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static TaxCode CWFGetCrossRefTaxCode(TaxGroup _taxGroup, 
                                            TaxItemGroup _taxItemGroup)
{
    TaxOnItem           taxOnItem;
    TaxGroupData        taxGroupData;
    TaxTable            taxTable;
    ;
 
    select firstonly TaxCode from taxTable
    join firstonly TaxGroup, TaxCode from taxGroupData
    where taxGroupData.TaxCode == taxTable.TaxCode
       && taxGroupData.TaxGroup == _taxGroup
    join firstonly TaxItemGroup, TaxCode from taxOnItem
    where taxOnItem.TaxCode == taxTable.TaxCode
       && taxOnItem.TaxItemGroup == _taxItemGroup;
 
    return taxTable.TaxCode;
}
  1. Use the tax code from the method above and pass it to the method below along with the new tax amount:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static void CWFCalculateTax(TaxCode _taxCode, 
                                   TaxAmount _taxAmount, 
                                   CustInvoiceTable _custInvoiceTable)
{
    CustInvoiceCalcTax          custInvoiceCalcTax;
    TaxFreeInvoice              taxFreeInvoice;
    TaxRegulation               taxRegulation;
    TmpTaxWorkTrans             tmpTaxWorkTrans;
    TmpTaxRegulation            tmpTaxRegulation;
    ;
 
    ttsbegin;
 
    custInvoiceCalcTax = new CustInvoiceCalcTax_Table(_custInvoiceTable);
    taxFreeInvoice     = new TaxFreeInvoice(custInvoiceCalcTax, true, true, true);
    taxFreeInvoice.calcTax();
 
    taxRegulation = TaxRegulation::newTaxRegulation(taxFreeInvoice);
    tmpTaxRegulation = taxRegulation.tmpTaxRegulation();
    tmpTaxWorkTrans = taxRegulation.tmpTaxWorkTrans();
 
    while select tmpTaxRegulation
    where tmpTaxRegulation.TaxCode == _taxCode
    {
        tmpTaxRegulation.SourceRegulateAmountCur = _taxAmount;
        tmpTaxRegulation.OverrideCalculatedTax = NoYes::Yes;
        tmpTaxRegulation.update();
    }
 
    while select tmpTaxWorkTrans
        where tmpTaxWorkTrans.TaxCode == _taxCode
    {
        tmpTaxWorkTrans.SourceRegulateAmountCur = _taxAmount;
        tmpTaxWorkTrans.update();
    }
 
    taxRegulation.setTmpTaxWorkTransTmpData(tmpTaxWorkTrans);
    taxRegulation.saveTaxRegulation();
    ttscommit;
}

With the code above, I managed to override the tax amount for multiple tax codes only when I’ve updated the table TmpTaxWorkTrans.

Happy DAXing!

Create your own SysBPCheckIgnore macro to avoid code collisions

I’m a firm believer in ensuring that the code that we produce is free of Best Practice errors. I don’t want to evangelise about the benefits of doing this here as there is already enough of that around – just do it! The thing about the best practice checks are that there’s always times when you code something in a particular way knowing that its going to generate a best practice error but knowing that, in this instance, its the right solution. When we need to tell the Best Practice checker that we want it to ignore a specific instance of a Best Practice error, we just have to create an entry in the macro SysBPCheckIgnore. This macro isn’t actually a macro at all, its an XML file that’s consumed by the Best Practice checker to tell it which errors it should ignore. In Dynamics AX 2012 R3 RTM this macro is already over 1200 lines long making it no fun at all to modify – it takes a fairly long time to load into the editor and doing code compares on it is a pretty painful experience. The problem with the SysBPCheckIgnore macro is that just about every solution written for AX has to modify it. This means that every solution will have to perform code merges on the macro when taking upgrades or hotfixes from Microsoft and if you have multiple ISV solutions in your system then you’re certain to get a conflict on this macro and you’ll have to do a code merge to combine all of the solutions changes to the macro.

Utopia

Wouldn’t it be nice if everyone could create their own best practice ignore macro and have the Best Practice check consume that along with the standard macro to build a consolidated ignore list? Unfortunately, there’s no way to do this out of the box and the way that Microsoft engineered the Best Practice checker doesn’t make it easy for us to add in our own macros without modifying the checker itself. I’ve had a functionality change request logged with Microsoft on the Connect site since June 2014 asking them to re-engineer the best practice checker to allow multiple BP Check Ignore macros with little sign so far of it being taken up. You can join me in calling for this change by voting at https://connect.microsoft.com/dynamicssuggestions/feedback/details/904328/allow-multiple-sysbpcheckignore-macros. Rather than wait for Microsoft to change this I thought that I would look at whether I could come up with a solution that could be implemented by everyone without creating any points of code collision. This would mean that it had to be possible to code the solution without making any changes to standard Microsoft code. Happily I found that this is perfectly feasible and requires very little effort – that said, I would still rather see a standard solution from Microsoft as that’s likely to be far more widely adopted than what I’m suggesting below.

Reality

The Best Practice checker is implemented through the classes SysBPCheck, SysBPCheckBase and its sub-classes, all of which is fairly easy to follow. Once I looked at them, I very quickly established that the SysBPCheck.initIgnoreMap() method contains the code that consumes the SysBPCheckIgnore macro. Interestingly, the method already processes two macros (SysBPCheckIgnore and SysBPStyleCheckExceptions) using an Array object that’s loaded with AOT TreeNode references to these two macros. Unfortunately, the Array object is declared locally within the method so we have no way of adding extra elements to the array without modifying the standard code – if only Microsoft had implemented this using a List object declared as a class instance variable with a parm accessor method. No matter which way I looked at it I couldn’t find a way to hook into the code in this method to make it consider my custom best practice ignore macro along with the two that it was already processing.

My solution

Accepting that I couldn’t hook into the existing code directly I decided to look at duplicating the existing code and then triggering it from a post event handler hung off the existing method. Here’s the steps that I went through…

  1. Create a new BP ignore macro adding in <ignorelist> and </ignorelist> tags
  2. Create a new post event handler on class SysBPCheck giving it a unique name (I called mine K3initIgnoreMapPostHandler)
  3. Copy the code shown below into your new post event handler ensuring that you choose your own name for the event handler method (step 2 above) and that you insert the name of your macro at the top of the method in place of the name that I have used
  4. Create a new Event Handler Subscription on the SysBPCheck.initIgnoreMap() method, setting it as a “post” handler and pointing it at the post event handler method that you created in step 2.

Using this approach you can create a BPCheckIgnore macro that’s unique to your solution knowing that, so long as you name it carefully, you’ll never have a code collision with any ISV solution that you choose to use and that you’ll never need to look at the standard SysBPCheckIgnore macro during an AX version or hotfix upgrade. If you’re using multiple ISV solution you might even consider encouraging them to go down this route too so that when you bring all of their models into your solution they can all live together in your system without you needing to do a code merge on the SysBPCheckIgnore macro to combine their changes to the macro. Here’s the code that I used for my post event handler. I’ve chosen to keep the code as close to the standard initIgnoreMap code as I can so it still uses an Array even though there is only one macro loaded into the array. When you create your own copies of this you only need to change the two lines at the top that have have tagged with comments. Remember that you still need to restart the AX client each time you add things to your ignore macro because the Best Practice checker caches the contents of the macros, only loading them the first time that you perform a best practice check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public static void K3InitIgnoreMapPostHandler(XppPrePostArgs _args) // Use your own method name here
{
    #define.IgnoreMacro('\\macros\\K3BPCheckIgnore')                // Use your own macro name here
 
    int             errorCode;
    str             errorCodeStr;
    TreeNodePath    path;
    Set             set;
    XmlReader       xmlReader;
    Map             errorCodeMap = new Map(Types::String, Types::Integer);
    TreeNode        macroNode;
    TreeNode        bpIgnoreMacroNode = TreeNode::findNode(#IgnoreMacro);
    Array           macroArray = new Array(Types::Class);
    int             i;
 
    SysBPCheck      sysBPCheck = _args.getThis();
    Map             ignoreMap = SysBPCheck.parmIgnoreMap();
 
    if (!bpIgnoreMacroNode)
    {
        throw error("@SYS28152", #IgnoreMacro);
    }
 
    macroArray.value(1, bpIgnoreMacroNode);
 
    for(i=1;i<=macroArray.lastIndex();i++)
    {
        macroNode = macroArray.value(i);
        xmlReader = XmlReader::newXml(macroNode.AOTgetSource());
        while (xmlReader.read())
        {
            if (xmlReader.name() == #IgnoreListViolation)
            {
                errorCodeStr = xmlReader.getAttribute2(#IgnoreListErrorCode);
                if (errorCodeMap.exists(errorCodeStr))
                {
                    errorCode = errorCodeMap.lookup(errorCodeStr);
                }
                else
                {
                    //BP Deviation Documented
                    errorCode = runbuf(strFmt('int convert(){#SysBPCheck return %1;}', errorCodeStr));
                    errorCodeMap.insert(errorCodeStr, errorCode);
                }
 
                path = xmlReader.getAttribute2(#IgnoreListPath);
 
                if (ignoreMap.exists(errorCode))
                {
                    set = ignoreMap.lookup(errorCode);
                }
                else
                {
                    set = new Set(Types::String);
                    ignoreMap.insert(errorCode, set);
                }
                set.add(path);
            }
        }
    }
}