Tuesday, November 30, 2010

Change default layers when comparing code

During the integration of system patches or third-party layers I found it annoying to continually select a certain layer when using the compare tool.  Multiply the time it takes by 400 or more nodes and you get an unhappy developer.
Fixing this problem is very easy.  Just add the following code to the end of the \Classes\SysCompare.initContext() method.


    if (comboBox1.getText(comboBox1.selection()) == comboBox2.getText(comboBox2.selection()) && comboBox2.items() > comboBox2.selection() + 1 )
    {
        comboBox2.selection(comboBox2.selection()+1);

        // 2010.11.30  Change default selectons for code comparison
        if (comboBox1.items() == comboBox2.items() - 1)
        {
            if (comboBox1.items() > 2 && Global::strEndsWith(comboBox2.getText(comboBox2.items() - 1), ' (Washed)')) // english only
            {
                // Compare last two layers (not selecting the washed version)
                comboBox1.selection(comboBox1.items() - 2); // Set first drop down to second-to-last possible option
                comboBox2.selection(comboBox2.items() - 2); // Set second drop down to second-to-last possible option
            }
        }
    }
    // 2010.11.30  Change default selections for code comparison when importing
    else if (comboBox1.items() > 1 && comboBox2.items() == 1 && Global::strEndsWith(comboBox2.getText(1 - 1), ' (xpo)'))
    {
        // Compare last layer to the imported XPO
        comboBox1.selection(comboBox1.items() - 1); // Set first drop down to last possible option
    }

Monday, November 15, 2010

Convert Axapta Time in SQL

AX 4.0 stores time in the database using the seconds since midnight.  So in order to view the time (military format) we must divide.  Here is an example:
SELECT TOP 20 StartTime AS [Seconds since midnight],
      CAST(StartTime/60/60 AS VARCHAR(2)) + ':' + RIGHT('0' + CAST(FLOOR((StartTime/60.0/60.0 %1)*60) AS VARCHAR(2)), 2) AS [Start Time]
FROM Batch
WHERE [Status] = 1

Monday, November 1, 2010

Disable users who are not active in Active Directory

Occasionally when auditors come by I like to disable all user accounts in AX which have been disabled in Active Directory.  Even though AD will not let them login auditors have a hard time understanding it, so I disable the users.  Many times we do not get notification that someone has left the company, or sometimes it does not reach the right people in charge of AX security.  So I made the job below which disables users in AX because they are disabled in Active Directory.  The job takes a little while to run.


static void disableUsersMissingInAD(Args _args)
{
    UserInfo                userInfoUpdate;
    xAxaptaUserManager      xAxaptaUserManager;
    xAxaptaUserDetails      xAxaptaUserDetails;
    #Guest
    
    xAxaptaUserManager = new xAxaptaUserManager();

    Global::startLengthyOperation();
    ttsbegin;

    while select forUpdate userInfoUpdate
    order by networkAlias
    where userInfoUpdate.Id != #GuestUser
       && userInfoUpdate.enable == 1
    {
        // Get the single user's details from the kernel class
        xAxaptaUserDetails = xAxaptaUserManager.getDomainUser(userInfoUpdate.NetworkDomain, userInfoUpdate.NetworkAlias);

        // Only show users who are enabled in Active Directory
        if (xAxaptaUserDetails == null || xAxaptaUserDetails.getUserCount() == 0 || !xAxaptaUserDetails.isUserEnabled(0))
        {
            userInfoUpdate.enable = 0;
            userInfoUpdate.update();
        }
    }

    ttscommit;
    Global::endLengthyOperation();
}

Monday, October 25, 2010

Line ###-Offset voucher does not exist in account ______.


If a vendor transaction is reversed but an AP check has been printed (and the payment journal not posted) you may get this error.  Essentially the record which was marked for settlement by the check has been deleted so it does not exist (VendTransOpen was deleted when the invoice was reversed).  If you go to the line in the accounts payable payment journal and click Inquiries > view marked transactions you will find it blank.  AX has lost the relationship of what invoices/credits were settled as part of the payment.  There are really two tables involved SpecTrans and VendTransOpen.

First thing we need to know is which transactions were settled by that payment.  The check will have the transactions shown in the Bank module.  Bank > Checks…select the check…Invoices button.  It lists all the transactions paid.  Go to the vendor transaction which was paid, click the Open button and remember the RecID of the VendTransOpen record for that transaction.

Find the reversal transaction and revoke the reversal (click the Reverse button when you have the reversal transaction selected).  The invoice should now have a balance like any other open invoice.
At this point if the SpecTrans record still exists you can fix it otherwise you must create it.  First open the SpecTrans table in the table browser and filter on SpecRecID = the journal line’s RecID.  If you find one then just fix the RefRecID to be the record ID of the VendTransOpen record.  Otherwise create the missing SpecTrans record by using a job.

static void Job1(Args _args)
{
    SpecTrans       specTrans;
    ;
    SpecTrans.clear();
    SpecTrans.initValue();
    SpecTrans.SpecTableId       = tablenum(LedgerJournalTrans);
    SpecTrans.SpecRecId         = 5638151884; // Record ID of the ledger journal line
    SpecTrans.LineNum           = 1.00;
    SpecTrans.Code              = "USD";
    SpecTrans.Balance01         = -78680.92; // Amount of the invoice which was paid
    SpecTrans.RefTableId        = tablenum(VendTransOpen);
    SpecTrans.RefRecId          = 5637622098; // Record ID of the open transaction
    SpecTrans.Payment           = NoYes::No;
    SpecTrans.PaymentStatus     = CustVendPaymStatus::Sent;
    SpecTrans.ErrorCodePayment  = "";
    SpecTrans.FullSettlement    = NoYes::No;
    SpecTrans.insert();
}

Saturday, October 9, 2010

Get the next unique file name

Sometimes you are saving a temporary file so you don't want to delete or overwrite anything that already exists in a directory...there is a nice function to find the next unique file name.

fileNameTemp = Global::fileNameNext(fileOriginalsPath + fileName);

Wednesday, October 6, 2010

Monday, October 4, 2010

Rename items quickly

Here is a little script to rename items quickly.  Just provide a CSV file with old and new item number.  I found that making the CSV file was faster than making a job to intelligently rename items because I could use Excel functions and review the new item numbers faster.

static void renameFromCSV(Args _args)
{
    #File
    CommaIO                 file;
    Container               values;
    Dialog                  dialog;
    DialogField             dfFileName;
    LineNum                 lineNum;
    ItemID                  itemIDfrom;
    ItemID                  itemIDto;
    InventTable             inventTable;
    ;
    setPrefix(funcName());
    dialog      = new Dialog("Rename items");
    dialog.filenameLookupFilter(["@SYS39829", #AllFilesName + #csv]);
    dfFileName  = dialog.addField(typeid(FileNameOpen));
    if (!dialog.run() || !dfFileName.value())
        return;

    if (!WinAPI::fileExists(dfFileName.value()))
        throw error(strfmt("@SYS18678", dfFileName.value()));

    file = new CommaIO(dfFileName.value(), #io_read);
    if (!file)
        throw error("@SYS74299");
    values = file.read();

    while (values != connull())
    {
        setPrefix(strfmt("Row %1 for %2", lineNum, conpeek(values, 1)));
        if (conlen(values) != 2)
            throw error('Bad row');

        itemIDfrom  = conpeek(values, 1);
        itemIDto    = conpeek(values, 2);

        if (InventTable::exist(itemIDfrom))
        {
            if (!InventTable::exist(itemIDto))
            {
                ttsbegin;
                inventTable = InventTable::find(itemIDfrom, true);
                inventTable.ItemId = itemIDto;
                inventTable.renamePrimaryKey();
                ttscommit;
                
                info(strfmt("%1 to %2", itemIDfrom, itemIDto));
            }
            else
                error(strfmt("@SYS93964", itemIDto));
        }
        else
            error(strfmt("@SYS94719", itemIDfrom));

        lineNum++;
        values = file.read();
    }
    info("@SYS78985");
}

Thursday, September 30, 2010

Modifying tab order

In AX to change tab order normally you change the order of the controls in the form.  You can set the "Skip" property on a control to Yes and when the user tabs it will skip that control.  Certain forms where there are many groups or process flow should be different than the default way require a different approach.  There is a way to specify the tab order for an entire form.

First set AutoDeclaration to Yes on all your controls.
Second override the init method and provide it an array of control IDs in the order that you want to tab.

void init()
{
    Array tabOrder = new Array(Types::Integer);
    ;
    super();

    tabOrder.value(1, Control1.id());
    tabOrder.value(2, Control2.id());
    tabOrder.value(3, Control3.id());
    tabOrder.value(4, Control4.id());
    tabOrder.value(5, Control5.id());
    tabOrder.value(6, Control6.id());
    tabOrder.value(7, Control7.id());
    tabOrder.value(8, Control8.id());
    tabOrder.value(9, Control9.id());
    tabOrder.value(10, Control10.id());
    tabOrder.value(11, Control11.id());
    tabOrder.value(12, Control12.id());

    element.tabOrder(tabOrder);
}

Sunday, September 26, 2010

Nagios: Automatically restart IIS after a CRITICAL alert

You can execute code when a service or host changes state.  These are called event handlers.

  1. Go to the Windows server and create a local user
  2. Create the following script called iisreset.sh in your Nagios plugins directory replacing the username and password in the three locations below



    #!/bin/sh
    #
    # Event handler script for restarting IIS on windows server
    #    Requires a local user on the server with the username and password below
    
    if [ $# -eq 3 ]
    then
            if [ "$1" = "CRITICAL" ]
            then
                    if [ "$2" = "HARD" ]
                    then
                            # Start the service
                            /usr/bin/net rpc service stop W3SVC -I $3 -U User%password >/dev/null
                            sleep 4
                            /usr/bin/net rpc service start W3SVC -I $3 -U User%password >/dev/null 2>&1
                            sleep 4
                            /usr/bin/net rpc service status W3SVC -I $3 -U User%password | grep 'W3SVC'
                            exit 0
                    fi
            fi
    else
            echo "Invalid number of arguments"
            exit 1
    fi
    

  3. Edit your commands.cfg file in the Nagios configuration directory



    define command {
            command_name    iisreset
            command_line    $USER1$/iisreset.sh $SERVICESTATE$ $SERVICESTATETYPE$ $HOSTADDRESS$
            }
    

  4. Add the event handler command to an existing service definition



    define service{
                    host_name                         somehost
                    service_description        HTTP
                    event_handler                  iisreset
                    ...
                    }
    

  5. The script will be called like this.  You can put this in a command line to confirm it will work. /usr/lib/nagios/plugins/iisreset.sh CRITICAL HARD 10.10.0.2
  6. Reload or start Nagios
    /etc/init.d/nagios3 reload

Saturday, September 11, 2010

Tuesday, September 7, 2010

Deleting all transactions in a company

Sometimes when creating a new playground for testing you need to delete all transactions in a company.  There is a nice utility included in AX for this purpose.  The SysDatabaseTransDelete class will delete all tables by checking the TableGroup property on the table.  All WorksheetHeader, WorksheetLine, and Transaction tables will be deleted.  There are a few exceptions so please review the class before running, and NEVER run in the live environment (I shouldn't have to say that).

Sunday, September 5, 2010

How to modify the permissions of the Admin group

After creating a new security key you will need to modify the permissions of the Admin group, but the User group permissions form does not allow it.  This small AX job will grant the Admin group permissions to the new security key.

static void grantSafetyAccess(Args _args)
{
    #Admin
    SecurityKeySet  securitySet;
    ;
    setPrefix(funcName());
 
    securitySet = new SecurityKeySet();
    securitySet.loadGroupRights(#AdminUserGroup, '');
 
    securitySet.access(securitykeynum(MyNewSecurityKey), AccessType::Delete);
 
    xAccessRightsList::saveSecurityRights(securitySet.pack(), #AdminUserGroup, '');
}

Friday, September 3, 2010

Reports being used

I was asked recently which reports were being run by users so that some of the menus could be simplified.  This SQL will list users and the AX report name for every report that has been run by anyone.  It is a good start, but it cannot get us the last date or time that they ran the report.

SELECT UserInfo.[Name], ElementName AS [Report Name], UserInfo.[Enable] AS [Active Employee]
INTO #temp
FROM SysLastValue WITH(NOLOCK)
INNER JOIN UserInfo WITH(NOLOCK)
                ON UserInfo.ID = SysLastValue.UserID
WHERE UserID NOT IN ('Admin', '')
                AND RecordType = 18 /* Report */

SELECT t.[Report Name],
                (SELECT COUNT(DISTINCT [Name])
                                FROM #temp sub
                                WHERE sub.[Active Employee] = 1
                                                AND sub.[End User] = 1
                                                AND sub.[Report Name] = t.[Report Name]) AS [# of Active Employees Who Ran The Report]
FROM #temp t
GROUP BY [Report Name]
ORDER BY [Report Name]

DROP TABLE #temp
}

This AX job returns the label and location of all reports in the menu structure.  We can then add this information with the last used information to come up with a fairly good estimation of which reports are being used.

// Lists where all reports are located in the menu structure
// Does not include reports which are called by classes, or auto printed by a scheduled job
static void listMenuStructure(Args _args)
{
    #AOT
    #Properties
    TreeNode                treeNode;
 
    void traverse(TreeNode tn)
    {
        while (tn)
        {
            switch (tn.AOTgetNodeType())
            {
                case 205: // Folder
                    traverse(tn.AOTfirstChild());
                    break;
                case 331: // Menu reference
                    if (tn.AOTgetProperty(#PropertyName))
                    {
                        treeNode = TreeNode::findNode(#MenusPath + #AOTDelimiter + any2str(tn.AOTgetProperty(#PropertyName)) + #AOTDelimiter + 'Reports');
                        if (treeNode)
                            traverse(treeNode.AOTfirstChild());
                    }
                    break;
                case 307: // Menu item
                    setPrefix(tn.treeNodePath());
                    info(tn.AOTgetProperty(#PropertyName));
                    info(tn.AOTgetProperty(#PropertyMenuitemname));
                    break;
            }
            tn = tn.AOTnextSibling();
        }
    }
    ;
    setPrefix(funcName());
    treeNode = TreeNode::findNode(#MenusPath + #AOTDelimiter + 'MainMenu');
    treeNode = treeNode.AOTfirstChild();
    traverse(treeNode);
}

Saturday, July 17, 2010

Inventory Closing and Adjustment

After writing a report which shows inventory transaction cost as of a specific date we had a small issue where very occasionally certain transactions were showing double cost.  After some poking around I found this post:

http://fedotenko.info/?page_id=31

The developer goes into depth about the procedure conceptually, technically, and differences between AX 4.0 and 2009 costing.  It is the very best explanation of the inventory costing feature in AX and saves many many hours of reading and interpreting code to understand.

Friday, July 16, 2010

Physical remaining quantity and total quantity must have the same sign

When posting a production report as finished journal you may receive this error,
Physical remaining quantity and total quantity must have the same sign.

It is because the remaining quantity on the production order is incorrect.



The quantity should never be negative.  I think it happens after running estimation incorrectly.  Run this job and it will fix the quantity.

static void fixProdNegQty(Args _args)
{
    ProdTable               prodTable;
    ;
    ttsbegin;

    select firstOnly forUpdate prodTable
    where prodTable.ProdId == "W0024381"
       && prodTable.RemainInventPhysical < 0;

    if (prodTable.RecId)
    {
        prodTable.RemainInventPhysical = 0;
        prodTable.modifiedField(fieldnum(ProdTable, RemainInventPhysical));
        prodTable.update();

        info("Updated");
    }

    ttscommit;
}