Pages

Monday, January 30, 2012

How to run AX 2012 HyperV on VirtualBox

In order to run the AX 2012 Beta VPC, it usually requires HyperV, or attaching the disk locally.  This is a great solution I was told about to run it locally.

http://dynamicsnavax.blogspot.com/2011/04/how-to-run-ax2012-hyperv-on-virtualbox.html

The basic idea is you just create a virtual machine, mount the three *.vhd's, and start it.  This blog post highlights two important caveats that you will come across.

  1. If you use the "Create new virtual machine wizard" and check the box to use an existing disk, it defaults the VHD as a SATA controller, which will result in the attached blue screen error over and over.  You need to attach all three disks as IDE controllers
  2. Sharepoint apparently doesn't work without adding to the hosts file
This is the blue screen I kept hitting over and over...very frustrating before seeing this blog post for the solution.

Wednesday, January 25, 2012

How to send emails from AX without requiring Outlook

Sending emails from AX has been somewhat of a pain when it tries to use Outlook.  This post is a simple code modification to one method in \Classes\Info\reportSendMail.  I did not develop this code, I merely tweaked it.  The original poster's blog has disappeared, and I can only find non-working remnants all around the web of this, but it is just too useful not to repost.

If you have Outlook 64bit edition, you might get the "Either there is no default mail client or the current mail client cannot fulfill the messaging request.  Please run Microsoft Outlook and set it as the default mail client."  Followed by an AX MAPI error.

Or sometimes you may get the "A program is trying to access e-mail address information stored in Outlook."...Allow/Deny/Hlep.

This change basically bypasses outlook.  Put it in and give it a shot.



void reportSendMail(PrintJobSettings p1)
{
    //SysINetMail m = new SysINetMail();
    System.Net.Mail.MailMessage mailMessage;
    System.Net.Mail.Attachment attachment;
    System.Net.Mail.AttachmentCollection attachementCollection;
    System.Net.Mail.SmtpClient myMail;
    System.Net.Mail.MailAddress mailFrom;
    System.Net.Mail.MailAddress mailTo;
    str userMailAddress;
    str receiverMailAddress;
    str mailBody;
    str smtpServer;
    fileNameOpen fileNameForEmail;
    str mail;
    FileIOPermission perm;
    userinfo userInfo;
    //end Declaration
    str fileName = 'axaptareport';

    ;
    if (p1.format() == PrintFormat::ASCII)
        fileNameForEmail = subStr(p1.fileName(),strLen(p1.fileName())-3,-999)+'TXT';
    else if (p1.format() == PrintFormat::RTF)
        fileNameForEmail = subStr(p1.fileName(),strLen(p1.fileName())-3,-999)+'RTF';
    else if (p1.format() == PrintFormat::HTML)
        fileNameForEmail = subStr(p1.fileName(),strLen(p1.fileName())-3,-999)+'HTM';
    else if (p1.format() == PrintFormat::PDF || p1.format() == PrintFormat::PDF_EMBED_FONTS)
        fileNameForEmail = subStr(p1.fileName(),strLen(p1.fileName())-3,-999)+'PDF';

    mail = subStr(fileNameforEmail,(strlen(fileNameforEmail)-8),9);

    select firstonly name from userInfo where userInfo.id == SysuserInfo::find().Id; // to find the user name

    if (isRunningOnServer())
        fileNameforEmail = winApiServer::getTempPath() + mail; // store attachment in a temp location
    else
        fileNameforEmail = winApi::getTempPath() + mail; // store attachment in a temp location


    perm = new FileIOPermission(fileNameforEmail,'w');
    if(!perm)
    {
        throw error("Cannot move attachment to temp location.");
        return;
    }
    try
    {
        perm.assert();
    }
    catch
    {
        throw error("Cannot gain access to Temp location.");
        return;
    }

    userMailAddress = SysUserInfo::find().Email; // find current users email address setup up in user //options
    receiverMailAddress = p1.mailTo();

    mailFrom = new System.Net.Mail.MailAddress(userMailAddress,userInfo.name);

    mailTo = new System.Net.Mail.MailAddress(receiverMailAddress,"");

    mailBody = "Email sent from " + CompanyInfo::name() + ", using Dynamics AX";

    smtpServer = SysEmaiLParameters::find(false).SMTPRelayServerName;// using the SMTP server ip //setup in email Parameters

    mailMessage = new System.Net.Mail.MailMessage(mailFrom,mailTo);
    mailmessage.set_Subject(p1.mailSubject());
    mailmessage.set_Body(mailBody);

    //move attachment file to Temp folder, might need to create WinAPIServer::moveFile
    winapi::moveFile(p1.fileName(), fileNameforEmail);

    attachementCollection = mailMessage.get_Attachments();
    attachment = new System.Net.Mail.Attachment(fileNameforEmail);
    attachementCollection.Add(attachment);

    myMail = new System.Net.Mail.SmtpClient(smtpServer);
    mymail.Send(mailmessage);

    mailmessage.Dispose();
    attachment.Dispose();

    if (isRunningOnServer())
        WinAPIServer::deleteFile(fileNameForEmail);
    else
        winApi::deleteFile(fileNameforEmail);

    CodeAccessPermission::revertAssert();
}

/*
void reportSendMail(PrintJobSettings p1)
{
    SysINetMail m = new SysINetMail();

    str fileName = 'axaptareport';

    if (p1.format() == PrintFormat::ASCII || p1.format() == PrintFormat::TEXTUTF8)
        fileName = fileName + '.txt';
    else if (p1.format() == PrintFormat::RTF)
        fileName = fileName + '.rtf';
    else if (p1.format() == PrintFormat::HTML)
        fileName = fileName + '.htm';
    else if (p1.format() == PrintFormat::PDF || p1.format() == PrintFormat::PDF_EMBED_FONTS)
        fileName = fileName + '.pdf';

    m.sendMailAttach(p1.mailTo(),p1.mailCc(), p1.mailSubject(),'Medulla Report', true, p1.fileName(), fileName);
}
*/

Wednesday, November 16, 2011

How Alerts in AX work with templates and emails

Alerts in AX can be very useful, and I wanted to touch on how they work, along with some explanations, tips, and tricks.  Much of this post is basic, but there are some caveats explained as well.  I mainly use alerts to notify a user when a field or record is created, modified, deleted, etc.  A user specifically wanted to know whenever a credit limit changed for a customer...so this is the scenario I will be covering.

You first need to make sure you have an alert email template setup.  This can be done from Basic>Setup>Email Templates.  Here you will create your header alert record, and your line records are for different languages should you need them.



After you create the basic one as pictured, you click the "Template" button where you define your template.  I suggest putting in the following HTML code under the HTML tab from http://technet.microsoft.com/en-us/library/aa834376(AX.50).aspx

You then need to configure the alerts parameters at Basic>Setup>Alerts.  You want to choose the "Alerts" email id template you just created.


The "Drill-down target" can be confusing...but here is my interpretation of it.  You basically put something different for each of your AX environments (Dev/Test/Prod).  When you have an alert email sent automatically with hyperlinks, it will be something like Dynamics://DEV... (instead of http://DEV...) that will be clickable, and open an AX client and go directly to the alert or the alert's origin.

When you click the link, it basically tries to go to your default client config, and then it confirms that the "DEV" found in the URL also is found in the alerts setup.  If it is not, it just doesn't open the client.  So if you are getting alerts from Dev/Test/Prod, only the ones that have the correct Dynamics://[this part] will work.  This obviously means you must have a local client installed.

The types of alerts:
  • Changed based alerts
    • Alerts that occur when something changes, like a customer name, or customer credit limit
  • Due date alerts
    • Alerts that occur when something is due, typically workflow related

These both have batch jobs, located at Basic>Periodic>Alerts that should be running in order to actually create the alerts that meet the rule criteria.  If you aren't getting the alert bell pictures in the lower corner of your session, then one of these is probably not running correctly.

Now let's setup the alert rule to notify us when the credit limit has changed for a customer.  You can set alerts on anything pretty much you right click on.  Go to your customer details form, then the general tab, then right click on the credit limit field and click "Create alert rule" as pictured:


Now base AX only lets you alert one user.  You can create a user that has a distribution list for an email account, or you can do some simple modifications in X++ to support overwriting the alert email, or alerting multiple emails/users.  For now, you can just click OK here and then close the window behind it.

If we go to a customer and change their credit rating, an alert will be generated (provided the batch is running as stated earlier or it is run manually).

For the email portion, the way alerts are setup is to have traceable emails.  So what it does is dump it into an email sending queue that can be found at Administration>Periodic>E-mail processing>E-mail sending status.


This emailing subsystem is very handy, and open for many useful modifications if you're feeling creative.  Emails in this queue are picked up for delivery by the e-mail processing batch, which is located at Administration>Periodic>E-mail processing>Batch.  This must be running for the emails to go out.

You can view sent emails with the body of the message, and also resend them should you choose to do so.

There is also a retry schedule that is located in the same place where you can set your retry intervals for failed email attempts.


The framework is here behind the scenes to open it up for some very useful modifications.  For example, if you periodically email customers a survey, and you want to send them reminders to fill it out, or you are waiting for any other sort of user input and you want to send emails until they perform their action.

This just briefly outlines and touches on alerts and emails.  If you have any questions, feel free to comment.  I will be elaborating on email templates later on.

Tuesday, November 15, 2011

How to call a form from code and make it modal

This is how to open a form from X++ and set it modal.  Modal means it is on top of the other windows and you can not click on the ones behind it.  I have this just in a clicked event on a button.


void clicked()
{
    CustTable   custTable;
    FormRun     formRun;
    Args args = new Args();
    ;
    
    // Just selecting a random record in CustTable
    select firstonly custTable;

    // This is the name of the form you are opening
    args.name(formstr(CustTable));
    
    // This is the optional record you are passing it
    args.record(custTable);

    // This builds the formrun
    formRun = ClassFactory.formRunClass(args);
    formRun.init();
    formRun.run();

    // This waits until the form closes
    if (!formRun.closed())
        formrun.wait(true);
}

Monday, November 7, 2011

Changing dimensions on an item the easy way using xRecord.merge?

Many of us have hit the same issue at some point, where a dimension needs to be changed on an item, and going about doing it can be a very big hassle.

This post is purely experimental, and I am currently assessing the feasibility of it, so please be very critical of it.  I'm looking for feedback on issues I've not yet thought of.

A project I'm working on is changing/removing the serialization dimension on many different items. The customer has gone from AX 3.0 to 4.0 to 2009, so there is a good share of old/bad data, old open transactions, etc.

Our goal is to turn on "blank issue allowed" and "blank receipt allowed" to make transfers/movements easier.

There are two basic ways that I can think of to change the dimensions on the items, and these are very brief steps that would actually require more thought/elaboration in a real scenario:

  • Scrap out all inventory, close all open transactions, then change the dimension
    • Difficulty: High
  • Create a new item with desired dimension, transfer old item to new item, then rename primary key to get the ItemId back to old value
    • Difficulty: Low, but you lose item history, so this often won't work.

I tried to get creative and think out of the box on this one a bit because the amount of work required to close all open transactions is huge, plus could mean several days, maybe even weeks of downtime because of the amount of transactions constantly going on.  This customer can not be shut down for more than a few days too.

My idea is to create a new item with the desired dimension, then use the Kernel xRecord.Merge functionality to merge the item with the old dimension into the newly configured item, then use the primary key functionality to rename the put the ItemId back.  I just tried this yesterday, so I've barely looked at system functionality beyond some simple tests.  The transactions came over, the BOMs came, etc...so it has some promise.

Some initial thoughts where this might have some holes...since we are moving the old item into the new item, the new item will have a new recId.  So if there are any tag tables that reference by RecId, this would clearly cause an issue.  I believe if the correct relation is setup though, that it might propagate correctly.  Tables like the Address or DocuRef (document handling) however that use commons and reference by Table and RecId might have some issues.  I have yet to test this.

There are some other minor steps that will need to be done in this code, such as confirming that those tables I delete have the previous values passed over...things such as item price.  If you have other custom code, you may need to add/remove some of the tables I deleted.  The discovery process on this was pretty simple, I ran it, then the validate would fail and I'd delete records out of the table, and rerun until it worked.

The "Master" item here is the new item with the new dimension.  The "child" item is the original item with the dimension we want to change.

Anyway, here is the proof of concept code you've been waiting for.  Please comment with thoughts/concerns/experience:




static void ItemMerge(Args _args)
{
    ItemId      iMaster = '26101A'; // New item with newly configured dimension
    ItemId      iChild  = '26101';  // Original item with old dimension

    InventTable master;
    InventTable child;
    CUSInventTable          t1;
    InventTableModule       t2;
    InventItemLocation      t3;
    InventItemPurchSetup    t4;
    InventItemSalesSetup    t5;
    ConfigTable             t6;
    ;


    ttsbegin;
    delete_from t1
        where t1.ItemId == iMaster;

    delete_from t2
        where t2.ItemId == iMaster;

    delete_from t3
        where t3.ItemId == iMaster;

    delete_from t4
        where t4.ItemId == iMaster;

    delete_from t5
        where t5.ItemId == iMaster;

    delete_from t6
        where t6.ItemId == iMaster;

    child = InventTable::find(iChild, true);
    master = InventTable::find(iMaster, true);

    child.merge(master);
    master.doUpdate();
    child.doDelete();
    ttscommit;

    info("Done");
}

Thursday, August 4, 2011

How to change a customer's party type

I often see customers that are created wrong in client's systems.  They will have a party type of "organization" when they should really have "person" or vice versa.  The difference between the two, as far as I can tell, is that organizations are permitted to have multiple contacts, and other little things like that, while person types are supposed to be simpler.

Here is a little static method I wrote to change their party type.  One thing to worry about, when you go from Organization to Person, you may want to check if there are multiple contacts before you make the change.  Not exactly sure what it will do.




static server boolean changeCustPartyType(CustTable _custTable, DirPartyType _dirPartyType)
{
    CustTable       custTable;
    ;

    if (_custTable && _custTable.PartyType != _dirPartyType)
    {
        ttsbegin;

        custTable.selectForUpdate(true);
        custTable.data(_custTable);

        custTable.PartyType = _dirPartyType;

        custTable.setNameAlias();

        DirParty::updatePartyFromCommon(custTable.PartyId, custTable, DirSystemPrivacyGroupType::Public, true);

        custTable.doUpdate();

        smmBusRelTable::updateFromCustTableSFA2(custTable);

        custTable.setAccountOnVend(custTable.orig());

        smmTransLog::initTrans(custTable, smmLogAction::update);

        ttscommit;

        return true;
    }

    return false;
}

Wednesday, June 22, 2011

Dynamics AX 2012 Upgrade thoughts

I've been working on upgrading a demo environment of AX 2009 to AX 2012 to get the experience under my belt.  Like any good upgrade, it takes careful planning and execution to be performed properly.  I'll be blogging my adventures through this upgrade as I go, so check back for additional posts.

I have a good deal of AX 4.0 to AX 2009 upgrade experience and AX 2012 upgrade is definitely different.  From AX 4.0 to AX 2009, you basically did three things (not exactly this order):
  • Merged all of your customizations against the new sys/syp layers
  • Extended the ReleaseUpdateDB41* classes if needed to upgrade data
  • Ran the upgrade scripts to upgrade fields and move data around

With AX 2012, there are much more pre-upgrade validation steps where your business data is prepped and validated more closely.  This can make more up-front work, but ideally it will be a more seamless upgrade.  The downside is that it is much more of a team effort initially to upgrade, involving financial decision makers in your organization and a few developers.  I do see the benefit of reducing the potential for error.  The upgrade to 2009 did more blind, data dumping, while 2012 validates/examines what its doing.


I've been working for about 6 hours now hitting a few bumps, mainly because I don't have the answers to some of the financial bits of the upgrade.  Some helpful upgrade whitepapers that were recommended to me can be found here.  My environment is Microsoft's Refresh 3.5 with no layers other than SYS/SYP and a dash of USR.  I just added a couple modifications to see how the new process would work.  The demo data however is not perfect, and because it's a demo company, a good deal of the business inputs required for the upgrade will be guesses by me.

The first steps of the upgrade have you import a giant XPO (9.43 MB) into the USR layer that prepares the system for upgrade.  The first step is a "Check upgrade readiness", which is essentially a bunch of batch jobs that do all sorts of validation on your system.  I had 1228/1229 complete with one erroring.  The results of the completed jobs seemed a little daunting.  Be prepared to have your controller handy.


This step is not required, but let's face it...if you don't get past most of this stuff, it's almost guaranteed you will face issues down the road.  The issues it brings up are for the most part, legitimate issues with data that do need to be fixed.  I chose to ignore most of the errors as I didn't want to spend a long time guessing my way through fixing them, when I don't really know the answers.

The next step "Initialize preprocessing", I think just creates or fills these "Shadow_*" tables in the AOT used to store upgrade data.

Then you have 22 steps to complete, with varying difficulty.  Some of these steps seem like they could have had a "best guess" option.  For example, I had to setup country/region code mapping.  AX 2009 was on ISO 3166-1 alpha-2, and AX 2012 is going to be on ISO 3166-1 alpha-3.  Meaning 2 letter country codes to 3 letter country codes.  This seems like something that can auto-populate.  You also must configure it for each company.  For me, this was 54 country codes such as "US" that I had to map to "USA" across each company.  Just a little time consuming.  Better safe than sorry though.

Other parts of the upgrade require the DAT company to have a number sequence, ledger account, and other small bits of data filled in to proceed.

The errors can seem cryptic, and if you have any bad data in your system, the errors will easily lead you astray.  They are pretty easy to determine via code and the debugger what is causing an error to be thrown.  For example, the demo data had some strange unit conversions for a specific item in one of the companies setup.  The company had 0 items, so the conversions were just bad data.  The errors were just catch-all type messages.  Some quick debugging and it was pretty easy to track down.

I've put in a few long nights after work, so I'm taking breaks in between so I don't wear myself out.  My next steps are:

  • Preprocess data on live system
    • Run live preprocessing scripts
    • Country/region upgrade
    • Run delta preprocessing scripts
  • Preprocess data in single user mode
    • Enter into single-user mode
    • Run single-user mode preprocessing scripts
The single user mode stuff I believe basically bulk copies the prepared data from AX 2009 to the AX 2012 system and finalizes it.

The perk of this is supposed to be a shorter downtime to actually go live.  Stay tuned!