Tuesday, February 1, 2011

Wrong Argument Types in Variable Assignment

During the development of a class occasionally we may receive this error message
wrong argument types in variable assignment
The error only occurs when during runtime.  I believe the issue is that the server has one copy of the class whereas your client has a different copy.  you can resolve the issue by clicking on the class and doing a full compile of the class.  If the class has a parent class (extends ___) then occasionally it is necessary to select the  parent class, right-click, and choose Add-ins > Compile forward.

Saturday, January 15, 2011

Emailing embedded images from Dynamics AX

When I first tried sending an email with images it showed the standard image not found image from Microsoft internet exploder.


This was the image in all the emails sent by AX.

We are using AX 4.0 SP2 with hotfixes applied....I began to dig...I found that the image path is replaced with cid:1 because it is an embedded resource in the email.....then discovered two things needed to be true:

  • Any images that you include in an email must exist in the directory specified at SysEmailParameters.AttachmentPath.
  • You must change the code if the location is a shared directory starting with two forward slashes //
The code checking that the image exists in the correct directory has a BUG and well it is overly complex (KISS).  Just replace this method below...

public static boolean isFromAttachmentsFolder(str _pathName)
{
    str attachmentsFolder;
    str pathName;
    ;
    // Fix embedding images in emails
    attachmentsFolder = SysEmailParameters::find().AttachmentsPath;
    pathName = _pathName;

    attachmentsFolder = Global::strReplace(attachmentsFolder,'\\','/');
    pathName = Global::strReplace(pathName,'\\','/');

    // Fix embedding images in emails
    return Global::strStartsWith(strupr(pathName), strupr(attachmentsFolder));
}

Thursday, January 13, 2011

SQL Server Reporting Services: Fixing "Could not generate a list of fields for the query"



Many times when creating an SSRS report I get the message "Could not generate a list of fields for the query" especially when using datasources which include temporary tables or very complex queries.  Sometimes clicking the Refresh fields button on the query toolbar does indeed fix the issue and other times it does not help.

I found the workaround for making it always work.

  1. Close the design view of the report
  2. Right-click the report and choose View code
  3. Find the end of the SQL query that is giving you the error message...you can search for if you want.
  4. Add the area with any  report parameters which this data source uses.
  5. Save
  6. Close
  7. Reopen the report in design view


Monday, January 10, 2011

Determine table ID in SQL

Create this function to easily get the table number within a SQL statement.

-- =============================================
-- Create date: 2010.11.01
-- Description:    Gets the AX table ID for the table name
-- =============================================
ALTER FUNCTION [dbo].[fnAXTableID] 
(
    -- Add the parameters for the function here
    @tableName nvarchar(40)
)
RETURNS int
AS
BEGIN
    -- Declare the return variable here
    DECLARE @tableNum int

    -- Add the T-SQL statements to compute the return value here
    SELECT @tableNum = TableID
    FROM SQLDictionary
    WHERE [Name] = @tableName
        AND FieldID = 0
        AND Array = 0

    -- Return the result of the function
    RETURN @tableNum

END

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).