Saturday, December 12, 2015

Compiler warnings when using AXBUILD

When you run AXBUILD with AX2012 R3 you might notice there are some classes that throws warnings, even though you know they are not customized. Why is that?

Here is an example running CU10 and the following classes are flagged for investigation.

*** Bad element: \\Classes\AssetBookBonusMethod_IN
*** Bad element: \\Classes\PCImportModel
*** Bad element: \\Classes\VendInvoicePostBatchCleanup



The reason these classes throws warnings is the SysObsoleteAttribute where you can force compiler warnings with the second parameter.


There are system classes marked as obsolete and while you could argue they should be removed, there may be dependencies to them - out there somewhere -still.

Thursday, November 12, 2015

Error during install of Microsoft Report Viewer 2012

When installing the client for AX2012 R3 CU8 or later, one of the requirements is installing Report Viewer 2012. The prerequisite Validation step will inform you the component is missing, and it will provide the download link.

However, depending on your scenario you might need to download and install Microsoft System CLR Types for SQL Server 2012.

The error is "Setup is missing an installation prerequisite".


You can download the necessary components from Microsoft SQL Server 2012 Feature Pack.

Or you can download it directly using the following links:
32bit http://go.microsoft.com/fwlink/?LinkID=239643&clcid=0x409
64b http://go.microsoft.com/fwlink/?LinkID=239644&clcid=0x409


Monday, June 1, 2015

Fixing firstDayOfWeek and firstWeekOfYear in AX2012

I was asked to have a look at why the date picker in AX still chose Sunday while the user expected Monday. I remember fixing this back in AX2009, so I was curious to see how this was solved in AX2012. The Global class and the methods firstDayOfWeek and firstWeekOfYear caught my attention. One of them used the current users language as key to find the best possible calendar setting, while the other picked the default system setting. Well, let us rewrite this and make it work a bit better, and since sharing is caring - here is how I solved it.

Global::firstWeekOfYear served as an inspiration, but I want it to differentiate between whatever languages my environment is serving. I can live with one cached result for each language, and the performance penalty is low and acceptable.


static int firstWeekOfYear()
{
    #WinAPI
    SysGlobalCache  cache   = classfactory.globalCache();
    int             clientFirstWeekOfYear;
    anytype         calendarWeekRuleValue;
    // Axdata.Skaue ->
    /*
    str             language;
    */
    str             language = currentUserLanguage();
    // Axdata.Skaue <-
    System.Globalization.CultureInfo        userCulture;
    System.Globalization.CalendarWeekRule   calendarWeekRule;
    System.Globalization.DateTimeFormatInfo userDateTimeFormat;

    // Axdata.Skaue ->
    /*
    if (cache.isSet(classStr(Global), funcName()))
    */
    if (cache.isSet(classStr(Global), funcName() + language))
    // Axdata.Skaue <-
    {
        // Axdata.Skaue ->
        /*
        clientFirstWeekOfYear = cache.get(classStr(Global), funcName());
        */
        clientFirstWeekOfYear = cache.get(classStr(Global), funcName() + language);
        // Axdata.Skaue <-
    }
    else
    {
        // Axdata.Skaue ->
        /*
        language = currentUserLanguage();
        */
        // Axdata.Skaue <-
        userCulture = new System.Globalization.CultureInfo(language);
        userDateTimeFormat = userCulture.get_DateTimeFormat();
        calendarWeekRule    = userDateTimeFormat.get_CalendarWeekRule();
        calendarWeekRuleValue = CLRInterop::getAnyTypeForObject(calendarWeekRule);

        switch(calendarWeekRuleValue)
        {
            case CLRInterop::getAnyTypeForObject(System.Globalization.CalendarWeekRule::FirstDay) :
                clientFirstWeekOfYear = 0;
                break;
            case CLRInterop::getAnyTypeForObject(System.Globalization.CalendarWeekRule::FirstFullWeek) :
                clientFirstWeekOfYear = 1;
                break;
            case CLRInterop::getAnyTypeForObject(System.Globalization.CalendarWeekRule::FirstFourDayWeek) :
                clientFirstWeekOfYear = 2;
                break;
        }

        // Axdata.Skaue ->
        /*
        cache.set(classStr(Global), funcName(),clientFirstWeekOfYear);
        */
        cache.set(classStr(Global), funcName() + language,clientFirstWeekOfYear);
        // Axdata.Skaue <-        
    }

    return clientFirstWeekOfYear;
}


Using the same ideas, I changed Global::firstDayOfWeek. Again, I allowed for one cached result for each language.
static int firstDayOfWeek()
{
    // Axdata.Skaue ->
    /*
    System.Globalization.DateTimeFormatInfo fi;
    */
    int dow;
    str             language = currentUserLanguage();
    System.Globalization.CultureInfo        userCulture;
    System.Globalization.DateTimeFormatInfo userDateTimeFormat;
    // Axdata.Skaue <-

    SysGlobalCache  cache   = classfactory.globalCache();
    int             clientFirstDayOfWeek;

    // Axdata.Skaue ->
    /* 
    if (cache.isSet(classStr(Global), funcName()))
    */
    if (cache.isSet(classStr(Global), funcName() + language))
    // Axdata.Skaue <-
    {
        // Axdata.Skaue ->
        /* 
        clientFirstDayOfWeek = cache.get(classStr(Global), funcName());
        */
        clientFirstDayOfWeek = cache.get(classStr(Global), funcName() + language);
        
    }
    else
    {
        // Axdata.Skaue ->
        userCulture         = new System.Globalization.CultureInfo(language);
        userDateTimeFormat  = userCulture.get_DateTimeFormat();
        dow                 = userDateTimeFormat.get_FirstDayOfWeek();        
        /* Removed
        fi = new System.Globalization.DateTimeFormatInfo();
        dow = fi.get_FirstDayOfWeek();
        */
        // Axdata.Skaue <-
        
        // The .NET API returns 0 for sunday, but we expect sunday to
        // be represented as 6, (monday is 0).
        clientFirstDayOfWeek = (dow + 6) mod 7;

        // Axdata.Skaue ->
        /*
        cache.set(classStr(Global), funcName(),clientFirstDayOfWeek);
        */
        cache.set(classStr(Global), funcName() + language,clientFirstDayOfWeek);
        // Axdata.Skaue <-
    }

    return clientFirstDayOfWeek;
}

So, for those of you who have an environment supporting potential multiple calendar setups, I recommend applying the fix above, or write your own fix. If you know a more efficient and better way, please comment below.

Tuesday, April 28, 2015

Reduce SSRS deployment time for static reports in AX2012

Are you wasting minutes deploying and redeploying static SSRS reports in all the languages provided with AX2012? If you only need a handful of them, you might just as well consider disabling the licenses for the unwanted languages. You can enable them back if you need them later.

Now disabling one language at a time manually might not be your cup of tea, so I would like to share a small job that disables all languages except the ones you want to keep enabled. Just create a new job and paste in the code below. Use at own risk of course, take backups and backups of the backups etc (you know the drill).

// Remove licence codes for unwanted languages
static void AdLanguageRemover(Args _args)
{
    SysConfig                   sysConfig;
    SysRemoveLicense            remLic;
    
    Query                       query;
    QueryBuildDataSource        qbd;
    QueryBuildRange             qbr;
    QueryRun                    queryRun;
    
    FormRun                     confirmForm;
    Set                         languagesToKeep = new Set(Types::String);
    Set                         licenseCodeSet  = new Set(Types::Integer);
    SetEnumerator               it;
    int                         confCount       = 0;
    boolean                     licenseChanged  = false;
    Args                        args            = new Args(formStr(SysLicenseCompareForm));    
    boolean                     proceed         = false;
    SysLicenseCodeDescription   codeDescription;
    str                         currentLanguageId;
    int                         pos, sysConfigId;    
    
    // List of languages to keep. Add, remove, change to fit your preference
    languagesToKeep.add('nb-no');
    languagesToKeep.add('en-us');
    languagesToKeep.add('sv');
    languagesToKeep.add('nl');
    languagesToKeep.add('fr');
    languagesToKeep.add('da');
    languagesToKeep.add('de');    
    
    query = new Query();
    qbd = query.addDataSource(tableNum(sysConfig));
    
    qbr = qbd.addRange(fieldNum(SysConfig,ConfigType));
    qbr.value(enum2Value(ConfigType::AccessCodes));
    
    qbr = qbd.addRange(fieldNum(SysConfig,Id));
    qbr.value(SysLicenseCodeReadFile::rangeLanguage());
    
    queryRun = new QueryRun(query);
    
    delete_from remLic;
        
    while (queryRun.next())
    {
        if (queryRun.changed(tableNum(sysConfig)))
        {
            sysConfig = queryRun.get(tableNum(sysConfig));
        }
        
        codeDescription     = SysLicenseCodeReadFile::codeDescription(sysConfig.Id);
        pos                 = strFind(codeDescription,'(',strLen(codeDescription),-strLen(codeDescription));
        currentLanguageId   = subStr(codeDescription,pos+1,strLen(codeDescription)-pos-1);
        
        if (!languagesToKeep.in(currentLanguageId))
        {
            warning(strFmt('Removing language %1',SysLicenseCodeReadFile::codeDescription(sysConfig.Id)));
            licenseCodeSet.add(sysConfig.Id);
            remLic.clear();
            remLic.LicenseCode = sysConfig.Id;
            remLic.Description = SysLicenseCodeReadFile::codeDescription(sysConfig.Id);
            remLic.insert();
        }
        else
        {
            info(strFmt('Keeping language %1',SysLicenseCodeReadFile::codeDescription(sysConfig.Id))); 
        }
    
    }
    
    if (licenseCodeSet.elements())
    {
        // if not valid code, then we should display the warning            
        confCount   = SysLicenseCodeReadFile::findConfigKeysFromLicenseCodeSet(licenseCodeSet);

        confirmForm = classfactory.formRunClass(args);
        confirmForm.init();
        confirmForm.run();        
        confirmForm.wait();  
        
        if (confirmForm.closedOk())
        {
            it = licenseCodeSet.getEnumerator();
            while (it.moveNext())
            {
                sysConfigId = it.current();
               
                update_recordSet sysConfig 
                    setting value = '' 
                    where sysConfig.id == sysConfigId;
                
            }
            
            SysLicenseCodeReadFile::codesModified();
        }
    }
}

Allow for a synchronization to run through after the licenses are modified. Remember that this may impact the database schema, but if you really do not want the (ie.) Norwegian language to be enabled, it should be safe to disable. Thanks for reading!

Saturday, March 7, 2015

ExchangeRateEffectiveView not returning cross rates to the Cubes in AX2012

I was asked to assist figuring out why exchange rates wouldn't always be retrieved to the Sales Order Cube in Dynamics AX2012. The solution became a journey through various interesting topics (for me at least):

  • Views in AX
  • View methods
  • Advanced Query range
  • Reverse engineering the SQL behind the views
Starting with the view that did not return all the expected data, we have the SalesOrderCube View. 
Now the Query in itself isn't all that fascinating, but I wasn't aware that you could add ranges across datasources like done in this Query. That is pretty handy!


Notice how the Query uses another View as DataSource. There are plenty of examples of Views and Queries being nested, and this is a powerful way to create results from a rather complex ERP data model. 
Notice also there is a custom Range on ValidFrom and ValidTo. The Ranges compare the Dates from ExchangeRateEffectiveView with the CreatedDateTime from SalesTable.



If we look at the definition of the ExchangeRateEffectiveView we see that ValidTo is a field of type Date. Furthermore we see the field is coming from a View Method. But we know CreatedDateTime on SalesTable is a DateTime.
How can it compare a Date with a DateTime? The answer is that actual date is stored in the database as a datetime where the time part is 00:00:00.000.   


So it compares the values and that works like charm. 

The problem

What happens if you have daily exchange rates in your system? Then your ValidFrom and ValidTo becomes the exact same date and more importantly the same time. This will not work correctly since CreatedDateTime also keeps track of what time on the day a Sales Order was created. 

So let's look at one specific example where we have rates for the 9th of December 2014.


And if we run the Sales Order Cube view, and modify the selected columns so we can see the problem, we will notice that the query is unable to collect the rates. The values from Exchange Rates is NULL.



The Solution

There are probably many ways to solve this, but the solution I went for was to make sure that the ValidTo always returns the time part at the max value, which is 23:59.59.000 (AX doesn't operate on the milliseconds, as far as I know).

So compare the results coming from the ExchangeRateEffectiveView before I apply the change.


By doing some small changes to the validTo View method, I can give the time part a better value.



And the result is that the Sales Order Cube now has the Cross Rates as expected. 


I hope you enjoyed this post. I sure enjoyed solving this challenge.
Here is the method body (ExchangeRateEffectiveView.validTo):
private static server str validTo(int _branchNum)
{
    str returnString, fieldString, fieldString2, fieldString3;
    boolean generateCode = false;
    DictView dictView;

    // Axdata.Skaue.04.03.2015 
    // Fix ValidTo with proper time 00:00:00.000 -> 23.59.59.000 ->
    str adFixValidToString = 'DateAdd(ss,-1,DateAdd(d,1,%1))';
    // Axdata.Skaue.04.03.2015 <-

    dictView = new DictView(tableNum(ExchangeRateEffectiveView));

    switch (_branchNum)
    {
        case 1:
            returnString = dictView.computedColumnString('VarToVarBefore', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            returnString = strFmt(adFixValidToString, returnString); // Axdata.Skaue.04.03.2015
            break;

        case 2:
            fieldString = dictView.computedColumnString('DenToVarEuroBefore', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            fieldString2 = dictView.computedColumnString('DenToVarEuroBefore', 'FixedStartDate1', FieldNameGenerationMode::FieldList, true);
            // Axdata.Skaue.04.03.2015 ->
            fieldString  = strFmt(adFixValidToString, fieldString); 
            fieldString2 = strFmt(adFixValidToString, fieldString2); 
            // Axdata.Skaue.04.03.2015 <-
            generateCode = true;
            break;

        case 3:
            fieldString = dictView.computedColumnString('DenToDenBefore', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            fieldString2 = dictView.computedColumnString('DenToDenBefore', 'FixedStartDate1', FieldNameGenerationMode::FieldList, true);
            fieldString3 = dictView.computedColumnString('DenToDenBefore', 'FixedStartDate2', FieldNameGenerationMode::FieldList, true);
            // Axdata.Skaue.04.03.2015 ->
            fieldString = strFmt(adFixValidToString, fieldString); 
            fieldString2 = strFmt(adFixValidToString, fieldString2);
            fieldString3 = strFmt(adFixValidToString, fieldString3);
            // Axdata.Skaue.04.03.2015 <-
            returnString = 'CASE when ' + fieldString + ' <= ' + fieldString2 +
                ' and ' + fieldString + ' <= ' + fieldString3 + ' then ' + fieldString +
                ' when ' + fieldString2 + ' <= ' + fieldString3 + ' then ' + fieldString2 + ' - 1 ' +
                ' else ' + fieldString3 + ' - 1  end';
           break;

        case 4:
            returnString = dictView.computedColumnString('SameFromTo', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            returnString = strFmt(adFixValidToString, returnString); // Axdata.Skaue.04.03.2015
            break;

        case 5:
            returnString = '\'21541231\'';
            break;

        case 6:
            returnString = '\'21541231\'';
            break;

        case 7:
            returnString = dictView.computedColumnString('DenToVarAfter', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            returnString = strFmt(adFixValidToString, returnString); // Axdata.Skaue.04.03.2015
            break;

        case 8:
            returnString = dictView.computedColumnString('DenToVarAfterRecipical', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            returnString = strFmt(adFixValidToString, returnString); // Axdata.Skaue.04.03.2015
            break;

        case 9:
            returnString = dictView.computedColumnString('VarToDenAfter', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            returnString = strFmt(adFixValidToString, returnString); // Axdata.Skaue.04.03.2015
            break;

        case 10:
            returnString = dictView.computedColumnString('VarToDenAfterRecipical', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            returnString = strFmt(adFixValidToString, returnString); // Axdata.Skaue.04.03.2015
            break;

        case 11:
            returnString = '\'21541231\'';
            break;

        case 12:
            fieldString = dictView.computedColumnString('DenToDenAfterStart1', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            fieldString2 = dictView.computedColumnString('DenToDenAfterStart1', 'StartDate2', FieldNameGenerationMode::FieldList, true);
            // Axdata.Skaue.04.03.2015 ->
            fieldString  = strFmt(adFixValidToString, fieldString); 
            fieldString2 = strFmt(adFixValidToString, fieldString2); 
            // Axdata.Skaue.04.03.2015 <-
            generateCode = true;
            break;

        case 13:
            fieldString = dictView.computedColumnString('DenToDenAfterStart1Recipical', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            fieldString2 = dictView.computedColumnString('DenToDenAfterStart1Recipical', 'StartDate2', FieldNameGenerationMode::FieldList, true);
            // Axdata.Skaue.04.03.2015 ->
            fieldString  = strFmt(adFixValidToString, fieldString); 
            fieldString2 = strFmt(adFixValidToString, fieldString2); 
            // Axdata.Skaue.04.03.2015 <-
            generateCode = true;
            break;

        case 14:
            fieldString = dictView.computedColumnString('DenToDenAfterStart2', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            fieldString2 = dictView.computedColumnString('DenToDenAfterStart2', 'StartDate1', FieldNameGenerationMode::FieldList, true);
            // Axdata.Skaue.04.03.2015 ->
            fieldString  = strFmt(adFixValidToString, fieldString); 
            fieldString2 = strFmt(adFixValidToString, fieldString2); 
            // Axdata.Skaue.04.03.2015 <-
            generateCode = true;
            break;

        case 15:
            fieldString = dictView.computedColumnString('DenToDenAfterStart2Recipical', 'ValidTo', FieldNameGenerationMode::FieldList, true);
            fieldString2 = dictView.computedColumnString('DenToDenAfterStart2Recipical', 'StartDate1', FieldNameGenerationMode::FieldList, true);
            // Axdata.Skaue.04.03.2015 ->
            fieldString  = strFmt(adFixValidToString, fieldString); 
            fieldString2 = strFmt(adFixValidToString, fieldString2); 
            // Axdata.Skaue.04.03.2015 <-
            generateCode = true;
            break;

    }

    if (generateCode)
    {
        returnString = 'CASE when ' + fieldString + ' <= ' + fieldString2 + ' then ' + fieldString +
            ' else ' + fieldString2 + ' - 1 end';
    }

    return returnString;
}

Saturday, January 17, 2015

Supporting multiple Enterprise Portals for AX2012

With AX2012 you can setup the SharePoint sites named "Enterprise Portal". These sites run perfectly well on the free SharePoint Foundation version, and it also runs on SharePoint Foundation 2013 if you have the necessary updates. In this post I will discuss some considerations for supporting multiple environments, which might be needed if you want to support some development and testing scenarios in addition to a production environment.

Installing and configuring SharePoint is in many aspects a separate skill set, so I totally get why IT Pros prefer to leave that to dedicated SharePoint consultants. Having said that, if you just need to setup Enterprise Portal for the purpose of supporting Role Centers and giving your AX2012 install a nice looking Home page, then your SharePoint install doesn't have to be too complicated.

I will use Foundation as example, but SharePoint Server 2013 (Standard or Enterprise) obviously works with AX2012 as well. Foundation is free, but does have the necessary features for supporting Dynamics AX Enterprise Portal with it's role centers. If you plan for utilizing Power BI, OData or any of the more advanced features, you should know that upgrading Foundation to Standard or Enterprise is not supported.

Assuming you're starting with a blank server, you can download SharePoint Foundation 2013 with SP1 and begin installing the prerequisites. If any of the prerequisites doesn't install successfully, you can browse through the log file and look for the download URL and try install the failed component manually. I've experienced having to install prerequisites manually before. Eventually, you should have the necessary binaries installed and you are ready for installing SharePoint binaries.

I recommend you do not run the Configuration Wizard just yet. Rather continue with installing the updates for SharePoint. Head over to the overview of updates and download the most recent cumulative update and install it. With the latest updates installed, you are ready to initialize your SharePoint Foundation 2013 Farm.

A couple of points here:
  • Consider the account you are using to install the SharePoint farm. Typically this account is referred to the SharePoint Setup and Farm account, and you use it again to configure the farm and potentially install more SharePoint servers into the same Farm. You may need to share the credentials of this account with other IT Pros, so avoid using your own personal account.
  • Normally you want a dedicated service account for SharePoint. This is an unattended account that has broad permissions on SharePoint. The administration web application will run under this account. 
  • It is also normal to have a dedicated service accounts for several of the various services you can setup in SharePoint, but that is out of scope for this article. 
After having run the Configuration Wizard, you have the option to run the wizard that creates your first SharePoint Site. I recommend skipping this wizard, and instead create the necessary web applications manually.

Before installing the very first Enterprise Portal, I recommend the following:

  • Install AX2012 Client and Management Utilities. Point the configuration to the environment you want to install the first SharePoint site. Both the local configuration and the business connector configuration should be pointing correctly and have working WCF configurations.  
  • From the SharePoint Administration Site, you need to manually create a new "Managed Account". From the Home page, under Security and General Security, you will find "Configure managed accounts" and from there you can register the business connector account as a new managed account. The SharePoint sites running Enterprise Portal needs to run under this managed account. 
  • From the SharePoint Administration Site you also need to start the Claims to Windows Token Service (aka C2WTS). From Home, under System Settings and Servers you will find "Manage Services on Server". Locate the C2WTS and start it from here. If you start this from Services under Windows Administrative Tools (Control Panel) and not from SharePoint itself, the service will be in a faulty state and you'll get in trouble when installing Enterprise Portal. Trust me, I've been there.
Now you should be ready first install of Enterprise Portal on this SharePoint Farm. The following steps can be repeated for each environment you want to support, let it be multiple DEVs or TESTs. Obviously, this limits itself performance wise if your box can't handle the load. So lets begin:
  1. Make sure Dynamics AX local configuration and business connector configuration points to right AOS, and do not forget to refresh and save the WCF configuration to this config.
  2. Create a new Web Application. Each Portal needs to run on its own Web Application and isolated application pool. Give it a good name, both the site and the application pool. Also give the Content Database a correlating and good name. The managed account must be the one you created earlier using the business connector account. I like to put these sites on ports like 81, 82, 83, etc.
  3. When the application is created, you are ready to install Enterprise Portal using AX2012 Setup. On the step where you select Web Application you choose the one you created in step 2. Give the site a good name, like "DynamicsAXDev" or something that makes it easy to understand what environment this site will support. Imagine looking at the URL in the browser and you can easily see from the address what environment you're currently at.
  4. Assuming the installation at step 3 went through successfully,  your next step is to make sure the new site always connects to the right environment. Copy a working AX Configuration file (AXC-file) locally to the server. Make sure it has an updated and correct WCF-configuration in it. I tend to put the file under c:\inetpub\wwwroot\. Give it a good name (like DEV.axc). I know the official documentation says the file can be on a UNC-share, but I never got that to work, so a local file seems to work ok. Finally, you need to put a reference to this file inside the web.config for this particular Web Application. The file is normally located under C:\inetpub\wwwroot\wss\VirtualDirectories\81 (given this application runs on port 81). Open the file in some notepad or text editor and put in a new XML section:



    Now you can be sure the Web Application points to the right environment independently of whatever is changed in the business connector configuration settings on this server. I put the section after the System.Web section.
  5. Finally, I recommend loading the site itself and edit the Site Permissions. You probably want to make sure either Domain Users or some dedicated AD User Group has at least Read permissions. 
You can repeat these five steps for each environment you want to support. How cool is that? 

Now, what if you need to copy the AX2012 data between the environments? Well, that can be a problem, because when you install Enterprise Portal, setup connects to the AOS and adds data to the database. These data include the URL and the unique ID of the site you installed. If you start copying data around, you might end up with multiple environments pointing at the same Enterprise Portal, and this Portal points to just one of the AOSes, and that isn't very helpful. 

We need to fix that! :-)

Use this PowerShell command to identify the unique ID (GUID) for each site:

Get-SPSite http://fancysharepointserver:81/sites/dynamicsaxtest | 
Select -ExpandProperty AllWebs | 
where {$_.Url -notmatch "dynamicsaxtest/"} | ft -a ID, Url

This will reveal the ID, and you can copy it over to the following SQL command:

DECLARE 
    @EPURL                  AS VARCHAR(255),
    @EPGUID                 AS VARCHAR(255)

SELECT 
    @EPGUID                 = 'e3b7b289-cb17-4c38-8e98-858181af88a5'        ,
    @EPURL                  = 'http://fancysharepointserver:81/sites/dynamicsaxtest'

UPDATE EPGLOBALPARAMETERS
SET    HOMEPAGESITEID    = @EPGUID,
       DEVELOPMENTSITEID = @EPGUID
WHERE  KEY_ = 0

UPDATE EPWEBSITEPARAMETERS
SET    INTERNALURL = @EPURL,
       EXTERNALURL = @EPURL,
       SITEID      = @EPGUID
WHERE  COMPANYID = 'DAT'

The URL and GUID above is just examples, and will obviously differ from your environment, but you get the idea. 

Now save the SQL Command and make sure to include it in your routines when copying data from one environment to another. 

With all of this, you should be good to go and able to have multiple SharePoint applications running Enterprise Portals for different environments, all on the same server.