It recently downed upon me that I do not have an account of the number of lines of code I have written on my vpc (my Dynamics AX Playground!!).... Unfortunately I could not find an easy way to get an accurate count of the lines of code I wrote.. Hence i wrote some more code :)
Its like writing code to count code!!
Hope this is of help to others out there.. asking themselves the same question.
class mrCountLinesOfCode
{
Map axTypeCount;
Map mrTypeCount;
MapEnumerator axEnumerator;
MapEnumerator mrEnumerator;
str fullPath;
int pos;
int total;
int linesOfCode;
}
void run()
TreeNodeiterator tempNodeIterator;
TreeNodeiterator treeNodeIterator;
TreeNodeiterator rootNodeIterator;
TreeNodeiterator childNodeiterator;
str type;
TreeNode treeNode;
TreeNode rootNode;
TreeNode childNode;
TreeNode tempNode;
;
#AOT
rootNode = TreeNode::rootNode();
rootNodeIterator = rootNode.AOTiterator();
rootNode = rootNodeIterator.next();
axTypeCount = new Map(types::String,types::Integer);
mrTypeCount = new Map(types::String,types::Integer);
while(rootNode)
type = rootNode.treeNodeName();
childNode = rootNodeIterator.next();
childNodeIterator = childNode.AOTiterator();
treeNode = childNodeIterator.next();
while(childNode)
tempNode = treeNode;
tempNodeiterator = tempNode.AOTiterator();
while(treeNode)
// loop till you get to a leaf node - as methods are leafs
while(treeNode.AOTchildNodeCount() > 0)
if(treeNode.AOTiterator().next().AOTchildNodeCount()> 0)
treeNode = tempNodeiterator.next();
if(!treeNode)
treeNode = tempNode;
treeNode = treeNode.AOTiterator().next();
// so we found a Lefe. check if its a method
if(sysTreeNode::hasSource(treeNode))
treeNode = tempNodeiterator.next(); // iterate first.
this.setCount(treeNode,type);
}// end if Has Source
else
treeNode = tempNodeiterator.next(); // leaf is not a method.. hence move to the next node
//childNode = rootNodeIterator.next();
while(childNode && !this.checkIfNodeMightHaveCode(childNode.sysNodeType()))
if(childNode)
rootNode = childNode;
}// end root while
this.printitOut();
void setCount(treeNode _treeNode,str _type)
int tempLines;
fullPath = sysTreeNode::getPath(_treeNode);
pos = 0;
pos = strScan(fullPath,'\mr',1,strlen(fullPath)); //i always prefix any new elements i create with MR
tempLines = this.getLinesOfCode(_treeNode.AOTgetSource());
if(tempLines > 4)
if(pos)
this.countMrLines(tempLines,_type);
this.countAXLines(tempLines,_type);
int getLinesOfCode(Source _source)
Source tmpSource;
Line line;
int ptr;
int numberOfLines;
if (_source)
tmpSource = _source;
while (tmpSource)
line++;
ptr = strfind(tmpSource, '\n', 1, maxint());
if (!ptr)
ptr = strlen(tmpSource);
numberOfLines++;
tmpSource = strdel(tmpSource, 1, ptr);
return numberOfLines;
void countMrLines(int _lines,str _type)
linesOfCode = 0;
if(mrTypeCount.exists(_type))
linesOfCode = mrTypeCount.lookup(_type);
linesOfCode += _lines;
mrTypeCount.insert(_type,linesOfCode);
boolean checkIfNodeMightHaveCode(int _sysNodeType)
#TreeNodeSysNodeType
switch(_sysNodeType)
case #SysNodeTypeTableCollection :
case #SysNodeTypeSystemEnum :
case #NT_DBCOLLECTIONLIST :
case #NT_DBFIELDGROUPLIST :
case #NT_DBENUMLIST :
case #NT_DBREFERENCELIST :
case #NT_CONFIGURATIONKEYLIST :
case #NT_SECURITYKEYLIST :
case #NT_DBLICENSECODELIST :
case #NT_MENU :
case #NT_DBLICENSECODE :
case #NT_CONFIGURATIONKEY :
case #NT_SECURITYKEY :
case #NT_TYPE :
case #NT_REFERENCE :
case #NT_PERSPECTIVE :
case #NT_PERSPECTIVEFIELDLIST :
return false;
default:
return true;
// just in case
Ladies and Gentlemen its convergence time and I hope by now all of you have already registered your spot at this most prestigious event... Convergence 2009 New Orleans, March 10 -13
If not.. Then do so now!
There are a number of reasons why Microsoft thinks you should attend the event:
Sessions: Convergence 2009 features more than 420 sessions covering Microsoft Dynamics and the latest Microsoft-based products.
Hands-on experiences: Grab the opportunity to evaluate products from Microsoft and our most important industry partners.
Community and Learning Center (CLC): Considered the hub of Convergence, this area is designed to be a one-stop shop for networking and learning outside of conference sessions – while only having to visit one room! We offer free technical support, research activities and opportunities to meet fellow users and product experts.
Connections with the community: An event that brings 10,000 Microsoft Dynamics customers, partners, team members and industry experts together translates into an endless number of opportunities to make lasting connections within the Microsoft community.
For me personally, the last point is the most important. I think Convergence is a phenomenal platform that enables partners to meet new potential customers, demonstrate their capabilities and win new deals! it also give them a chance to meet other partners ...from a customer perspective, it give them a chance to witness the Microsoft Dynamics offering first hand and compare the various products out there.
Other good things happening at the event:
· The session catalog (containing 225 sessions) is now live on the Convergence marketing site.
· The Convergence Customer Excellence Awards recognize, honor and celebrate customers who have achieved outstanding success with their Microsoft solutions as well as Microsoft partners' solutions.
More reasons to attend:
Networking.
The Microsoft Dynamics® Convergence® 2009 New Orleans conference is an opportunity for business decision makers (BDMs) to interact with other members of the Microsoft Dynamics community. At Convergence, BDMs will meet with other decision makers, Microsoft employees, and industry experts to gain knowledge, gather business insights, and increase their understanding of Microsoft Dynamics business management solutions.
Gain insights.
At Convergence 2009 New Orleans, BDMs can gain insights into their Microsoft Dynamics business and learn strategies business potential for the coming year.
Learn how to thrive in a challenging economy.
In today’s difficult economic environment, there has never been more value in attending Convergence. This year is geared toward helping attendees leverage their Microsoft Dynamics solutions to increase visibility into operations and maximize efficiencies. Convergence attendees will get exposure to new technologies, will better understand their current business management software, and will have the insights they need to unleash their solution’s full capabilities.
Visit a fun and affordable locale.
This year’s premier Microsoft Dynamics event takes place in New Orleans, a historic, entertaining, and economically friendly city, which allows for affordable travel costs. Attendees can plan for what is sure to be a memorable time without putting too much strain on the travel budget.
Additionally, BDMs will have the opportunity to:
· Learn how to more effectively use Microsoft Dynamics solutions to easily analyze financial data and respond to changing business conditions.
· Gain clearer understanding of reporting and process automation capabilities to more effectively forecast and manage cash flow and compliance.
· Learn how Microsoft Dynamics solutions can give their advisors and representatives access to comprehensive client information and insight.
· Discover new ways Microsoft Dynamics can help improve fiscal responsibility and the delivery of public services.
· Use accurate financial tracking and reporting to boost efficiencies and improve accountability.
Partners can get help with planning and promotion.
The Complete Guide to Convergence for Partners (PartnerSource access is required to access this link) is a valuable planning resource on PartnerSource. There are tips, tools and templates to help partners create the most worthwhile Convergence experience for their company and their customers’ companies.
Have a great day… Hope I get to meet you at Convergence 2009 New Orleans
Alerts are one of the key features that Dynamics AX has to offer.. they are easy to setup, administer and maintain (especially with the new batch framework in Dynamics AX 2009).
However, the fact that an alert can only be setup to ‘alert’ (or inform) 2 users (the user setting up the alert and the user assigned in the ‘Alert who’ section) is quite restrictive.. as in a fast moving business (retail for example) it’s essential that multiple users are kept updated with crucial business activity. Users like information to be pushed to them, no one like hunting around the system to find out what changed and who made the change, etc etc..
I wrote a project which will enable Dynamics ax users to configure alerts that can be distributed to multiple users at the same time.
So, as you can see from the above pic, I added a new button to the Alert setup form, which lets you select the list of users that need to alerted when the event occurs
The person configuring the alert will simply have the select the other users that need to be alerted.
Note: its worth pointing out that this new functionality (and also base Dynamics AX alerts) use the email address that is configured in the user options (Tools > Options).
The following are the list of objects that were created/modified in this project.
Development Environment:
Kernel : 5.0.593.0
Appl: 5.0.593.0
Click here to download the project.
Note: I would like to point out that this is totally untested code, and there are no warranties or guarantees that I or my company provide on the usage of this code... this code was written to show one of the many ways in which Dynamics AX functionality can be extended and was not meant to be used in a production environment
Have a great day!
- Mohammed Rasheed
www.dynamic-ax.co.uk
One of the really good features of Dynamics AX, is the ability to send Emails directly from the application... hence we can email invoices as pdf attachments directly from Dynamics AX..
However, there isnt a automated method of importing Emails from outlook into Dynamics.. one can manually attach emails to a record using the Document Handling functionality.... this functionality is best used in CRM where one can drag a email from Dynamics and drop it in ax (CRM > Encyclopaedia or CRM > Documents).. however the drag and drop (even though its cool) its still manual..
This project basically gives you the ability to import email from outlook in to ax... the code can either be executed on a periodic basis or can be a part of the client start up..
The code here is designed to read emails from the users Inbox and then on a basis of certain parameters, it find out which record in ax the email should be attached to..
The parameters work in 2 ways..
1. Subject Line Prefixes
If you ever worked with a Email integrated ‘Bug tracking System’ then you would know what I am talking about..
Subject line code work the following way..
For example say we create a code for sales.. sales# .. this basically means that if a subject has the word ‘sales#’ then the characters following the code (sales#) signify the sales Id the email needs to be related to.
In this project I added two subject line prefix codes.. one for Sales and the other for Customer... so for example if the subject line has the word ‘cust#’ then the characters following the code specify the customer account number.
2. From Email Address
I added a parameter, so that apart from the above option, one can also search for the record (custTanble or ContactPerson) using the email address of the sender.
Note: you might want to revisit indexes on custTable and Contact person if you like to take this path.
Ohh... in addition to that, if a match is found.. i.e. we find a email that should be attached to a ax record, then after importing the mail into ax, the routine transfer the mail to a folder (specified in the parameters table), hence the mail wont be searched by the routine again.
I dont think I need to say any more.. the code is pretty much self explanatory and were ever I deemed necessary I added comments to explain what I was up to ..
Note: I would like to point out that this is totally untested code, and there are no warranties or guarantees that I or my company provide on the usage of this code... this code was written to show one of the many ways in which ax integrates with Office Systems and was not meant to be used in a production environment.
class mrOutlookImporter
mrOutlookImportParameters outLookPram;
docuRef docuRef;
Microsoft.Office.Interop.Outlook.MailItemClass mailItemClass;
void getMails()
Microsoft.Office.Interop.Outlook._Application outAppl;
Microsoft.Office.Interop.Outlook.MAPIFolder mapiFolder;
Microsoft.Office.Interop.Outlook.NameSpaceClass Nspace;
Microsoft.Office.Interop.Outlook.FoldersClass foldersClass;
Microsoft.Office.Interop.Outlook.ItemsClass itemClass;
Microsoft.Office.Interop.Outlook.MAPIFolder destinationFolder;
//str dd;
str folderName;
str mailSubject;
str fromEmail;
str entryId;
str storeId;
int numOfEmails;
int numOfFolders;
boolean copyToDestination = false;
select firstonly outLookPram;
new InteropPermission(InteropKind::ClrInterop).assert();
outAppl = new Microsoft.Office.Interop.Outlook.ApplicationClass();
nSpace = outAppl.GetNamespace('Mapi');
mapiFolder = nSpace.GetDefaultFolder(Microsoft.Office.Interop.Outlook.OlDefaultFolders::olFolderInbox);
foldersClass = mapiFolder.get_Folders();
numOfFolders = foldersClass.get_Count();
destinationFolder = foldersClass.GetFirst();
while(numOfFolders > 0)
folderName = destinationFolder.get_Name();
if(folderName == outLookPram.outlookFolder)
copyToDestination = true;
break;
destinationFolder = foldersClass.GetNext();
numOfFolders--;
if(!copyToDestination)
try
foldersClass.Add(outLookPram.outlookFolder,Microsoft.Office.Interop.Outlook.OlDefaultFolders::olFolderInbox);
catch(Exception::CLRError)
error('cannot create folder');
itemClass = mapifolder.get_Items();
mailItemClass = itemClass.GetFirst();
numOfEmails = itemClass.get_Count();
while( numOfEmails > 0)
numOfEmails--;
mailSubject = mailItemClass.get_Subject();
fromEmail = mailItemClass.get_SenderEmailAddress();
entryId = mailItemClass.get_EntryID();
storeId = mapifolder.get_StoreID();
if(copyToDestination && this.validateEmail(mailSubject))
if(this.importEmail(entryId,storeId))
mailItemClass.Move(destinationFolder);
else if(copyToDestination && outLookPram.checkUsingEmailAddress == NoYes::Yes)
if(this.findAndSetDocuref('email',fromEmail,mailSubject))
// one might consider moving a mail to a different folder even if a match isnt found.
mailItemClass = itemClass.GetNext(); //
error('cannot read email');
continue;
CodeAccessPermission::revertAssert();
boolean validateEmail(str _subject)
int firstPos;
int secondPos;
//scan sales
if(strScan(_subject,outLookPram.salesPrefix,1,strlen(_subject)))
firstPos = strScan(_subject,outLookPram.salesPrefix,1,strlen(_subject));
secondPos = strScan(_subject,'',firstPos,strlen(_subject));
return this.findAndSetDocuref('sales',substr(_subject,firstPos,(secondPos-FirstPos)),_subject);
else if(strScan(_subject,outLookPram.custPrefix,1,strlen(_subject)))
firstPos = strScan(_subject,outLookPram.custPrefix,1,strlen(_subject));
return this.findAndSetDocuref('Cust',substr(_subject,firstPos,(secondPos-FirstPos)),_subject);
boolean findAndSetDocuref(str _type,str _num,str _subject)
SalesTable salesTable;
CustTable custTable;
ContactPerson contactPerson;
salesId salesId;
custAccount custAccount;
email email;
this.initDocuref(_subject);
Switch(_type)
Case 'sales' :
salesId = _num;
select firstonly recid,ContactPersonId from salesTable where salesTable.SalesId == salesId;
if(salesTable.recid)
docuref.RefRecId = salesTable.RecId;
docuRef.RefTableId = tablenum(SalesTable);
docuref.ContactPersonId = salesTable.ContactPersonId;
Case 'cust' :
custAccount = _num;
select firstonly recid,ContactPersonId from custtable where custTable.AccountNum == custAccount;
if(custTable.RecId)
docuref.RefRecId = custTable.RecId;
docuRef.RefTableId = tablenum(custTable);
docuref.ContactPersonId = custTable.ContactPersonId;
Case 'email' :
email = _num;
select firstonly ContactPersonId,recid from contactPerson
where ( contactPerson.Email == email
|| contactPerson.Email2 == email
|| contactPerson.Email3 == email);
if(contactPerson.ContactPersonId)
docuref.RefRecId = contactPerson.RecId;
docuRef.RefTableId = tablenum(contactPerson);
docuref.ContactPersonId = contactPerson.ContactPersonId;
select firstonly RecId,ContactPersonId from custTable where custTable.Email == email;
if(custTable.AccountNum)
boolean initDocuref(str _subject)
smmDragDropObjectType smmDragDropObjectType = smmDragDropObjectType::InMail;
docuRef.ActualCompanyId = curext();
docuref.AuthorId = smmUtility::getCurrentContact();
docuref.Name = _subject;
docuRef.TypeId = DocuType::findDroppedObjectType(smmDragDropObjectType);
boolean importEmail(str _entryId,str _storeId)
DocuType docuType;
DocuValue docuValue;
NumberSeq numSeq;
str tofilename;
int numOfAttachments;
FilePath archivePath;
int lines = infolog.line();
Query q;
if (!_entryId || !_storeId)
return checkFailed("@SYS87269");
docuType = DocuType::find(docuRef.TypeId);
// Check that the document file location isn't set to "Original location" in the document
if (docuType.FilePlace == DocuFilePlace::NoCopy)
// File location can not be set up to Orginal location for the Outlook mail types
return checkFailed("@SYS87271");
// Is archive set up to be in a Party (do not save in database)
if (smmDocuments::mustArchiveFiles(docuType))
// Get archive path
archivePath = docuType.ArchivePath;
if (archivePath && ! WinAPI::pathExists(archivePath))
q = new Query();
q.addDataSource(tablenum(DocuType)).addRange(fieldnum(DocuType,TypeId)).value(queryValue(docuType.TypeId));
return checkFailed(strfmt("@SYS90175",archivePath),'',
SysInfoAction_FormrunQuery::newFormnameControlnameQuery(
formstr(docuType),
identifierstr(Setup_ArchivePath),q));
// If no path to set up on the document type the general archive path is used instead
if (! archivePath)
archivePath = Docu::archivePath(docuType.DataAreaId);
// The Windows path is used because the Outlook mail must be written somewhere on disk as a temp file before it can be saved in the database
archivePath = WinAPI::getWindowsDirectory();
// Get a new number from the document numbersequence
numSeq = NumberSeq::newGetNum(DocuParameters::numRefDocuNumber(), true);
// Use next number in numbersequence as filename
docuValue.FileName = smmDocuments::getNonExistingFileName(numSeq, archivePath, 'msg');
// Save mail as the msg type file
docuValue.FileType = 'msg';
// If path doesn't end with to backslash it should be appended
archivePath = Docu::fileCheckPath(archivePath);
docuValue.Path = archivePath;
// Create filename based on the path in the document type table and the filename (number)
tofilename = docuValue.Path + docuValue.FileName + (docuValue.FileType ? ('.' + docuValue.FileType) : '');
// Save the e-mail as a .msg file
// Call save function on the Outlook mail item
mailItemClass.SaveAs(tofilename,Microsoft.Office.Interop.Outlook.OlDefaultFolders::olFolderInbox);
catch
Error('Cannot Save Mail as file');
// Check that the file was saved correctly
if (!archivePath || !WinAPI::fileExists(tofilename))
// Free the document numbersequence number that was reserved if the file wasn't found
numSeq.abort();
// The file could not be saved. Please check the path in the document type setup.
return checkFailed("@SYS86983");
// Create the file reference
docuValue.insert();
// Mark the document numbersequence number is used
numSeq.used();
// Create the document
docuRef.ValueRecId = docuValue.RecId;
docuRef.Notes = '';
docuRef.SmmEMailEntryID = _entryID;
docuRef.SmmEMailStoreID = _storeID;
docuRef.insert();
ttscommit;
// Should the file be stored in the Axapta database?
if (docuType.FilePlace == DocuFilePlace::Database)
// Copy the file to the database
docuValue = DocuValue::writeDocuValue(docuRef, tofilename);
// Delete the temp file that was placed in the Windows Party
WinAPI::deleteFile(toFileName);
A couple of guys asked me for code to import inventory in to ax using Counting journals... so here goes..
I basically created a table, called StockLevelImport and added basic fields to it such as itemid, location, warehouse, size,colour and quantity.. Notice: other dimensions such as config, batch were not added..however you can easily do so if you like.
The following bit of code was use to import the stock file in to ax (basically import a csv file)
static int importFromFile()
CommaIo fileIn;
StockLevelImport stockLevelImport;
container fileInCon;
FileIOPermission perm;
int recordsInserted;
#stockLevelImport
// the macro holds the full file name
recordsInserted = 0;
perm = new FileIOPermission(#stockLevelImportFile,'r');
if (perm == null)
error("FileIoPermission error");
return 0;
// Grants permission to execute the CommaIo.new method.
// CommaIo.new runs under code access security.
perm.assert();
fileIn = new CommaIo(#stockLevelImportFile,'r');
delete_from stockLevelImport; // clear table before import
fileIn.inFieldDelimiter(',');
startLengthyOperation();
ttsbegin;
while(fileIn.status() == IO_Status::Ok)
fileInCon = connull();
fileInCon = fileIn.read();
stockLevelImport.ItemId = conpeek(fileInCon,1);
stockLevelImport.InventLocationId = conpeek(fileInCon,2);
stockLevelImport.WMSLocationId = conpeek(fileInCon,3);
stockLevelImport.InventQty = conpeek(fileInCon,4);
stockLevelImport.InventColorId = conpeek(fileInCon,6);
stockLevelImport.InventSizeId = conpeek(fileInCon,5);
if(fileIn.status() == IO_Status::Ok) // to prevent the last insert
stockLevelImport.insert();
recordsInserted++;
endLengthyOperation();
return recordsInserted;
The code below is where meat of the operations are performed..
void countIt()
date transDate;
inventJournalName iJournalName;
InventJournalTable iJournalTable;
InventJournalTrans iJournalTrans;
real line;
Numberseq numberseq;
InventJournalCheckPost checkPost;
SysOperationProgress prog;
int progTot;// total count for progress bar
int ss;
InventTable inventTable;
InventTableModule inventTableModule;
inventdim idimm;
transDate = systemdateget();
#aviFiles
line = 1.00;
select count(RecId) from stockLevelImport;
progTot = stockLevelImport.RecId;
prog = SysOperationProgress::newGeneral(#aviStopWatch,"Adding Stock...", progTot);
numberseq = Numberseq::newGetNumFromCode(InventParameters::numRefInventJournalId().NumberSequence);
iJournalName = inventJournalName::find(InventParameters::find().CountJournalNameId);
if(iJournalName.RecId == 0)
throw error("NO counting journal Name foud");
iJournalTable.initFromInventJournalName(iJournalName);
iJournalTable.JournalId = numberseq.num();
iJournalTable.Reservation = ItemReservation::Automatic;
iJournalTable.SystemBlocked = NoYes::Yes;
iJournalTable.insert();
inventTableModule.clear();
inventTableModule = null;
stockLevelImport = null;
// while select inventtable
while select stockLevelImport where stockLevelImport.InventQty
join Price from inventTableModule where inventTableModule.ItemId == stockLevelImport.ItemId && inventTableModule.ModuleType == ModuleInventPurchSales::Invent
ss++;
iJournalTrans.clear();
iJournalTrans.initFromInventJournalTable(iJournalTable);
iJournalTrans.LineNum = line;
iJournalTrans.CostAmount = decround((stockLevelImport.InventQty * inventTableModule.Price),2);
iJournalTrans.CostPrice = inventTableModule.Price;
iJournalTrans.Counted = stockLevelImport.InventQty;
iJournalTrans.InventDimId = this.getDim();
if(iJournalTrans.InventDimId == "")
info("cannot create dimension for item " + iJournalTrans.ItemId);
iJournalTrans.JournalType = iJournalTable.JournalType;
iJournalTrans.PriceUnit = 1.0;
iJournalTrans.ProfitSet = CostProfitSet::Standard; // Standard
iJournalTrans.ProjSalesPrice = 0.0;
iJournalTrans.Qty = stockLevelImport.InventQty;
iJournalTrans.TransDate = transDate;
iJournalTrans.ItemId = stockLevelImport.ItemId;
iJournalTrans.initFromInventTable(InventTable::find(stockLevelImport.ItemId),false,false,false);
if(!iJournalTrans.Dimension)
info("Dimenstions not specified for item " + iJournalTrans.ItemId);
if(!iJournalTrans.validateWrite())
info("Could not validate write");
iJournalTrans.insert();
// progress bar
prog.setText("Item : " + stockLevelImport.ItemId);
prog.setCount(ss);
} // end while on Lines
iJournalTable.NumOfLines = line;
iJournalTable.update();
// Posting the Journal:
prog.setText("creating inventJournal...hold on");
prog.setCount(1);
if(BOX::yesNo("Do you want to post the Journal? " ,DialogButton::Yes) == DialogButton::Yes)
checkPost = InventJournalCheckPost::newJournalCheckPost(JournalCheckPostType::Post,iJournalTable);
checkpost.run();
iJournalTable.SystemBlocked = NoYes::No;
prog.kill();
info("Lines posted = " + int2str(ss));
And finally a small method to find the correct inventDimId that is to be used on the journal Line.
InventDimId getDim()
if(!stockLevelImport.InventLocationId || ! stockLevelImport.WMSLocationId)
return "";
if(!WMSLocation::exist(stockLevelImport.WMSLocationId,stockLevelImport.InventLocationId))
idimm = null;
idimm.InventLocationId = stockLevelImport.InventLocationId;
idimm.wMSLocationId = stockLevelImport.WMSLocationId;
idimm.InventColorId = stockLevelImport.InventColorId;
idimm.InventSizeId = stockLevelImport.InventSizeId;
return inventdim::findOrCreate(idimm).InventDimId;
- Mohammed Rasheed10:00 GMT | Read comments(0)How to Create a New Role Centre in Dynamics AX 2009
One of the most talked about features of AX 2009 is Role Centres... I thought Microsoft did a phenomenal job by packaging Sharepoint and ax in one box... the result: Extraordinary Technology.
As complex as they might seem role centres are actually very simple to use.. and its actually even simpler to create new one..
This is a step by step guide to help users/developers to create and deploy new role centres..
In this example we are going to create a Merchandiser Role Centre
Step 1:
Log in as an Admin and open up EP in internet explorer..
Well you actually dont need to be an admin.. but you need site creation rights in sharepoint and also access to the AOT in ax.
Then click on ‘Site Actions’ and select the ‘Create’ option
Step 2: select Basic Page..
Note: you might be better of select ‘Web Part Page’.. as a basic page would not have any web parts on it... all the standard role centre pages are Web part pages... I will show you both examples in this article .. hence I started with the basic page. (see the Additional info section below)
Also I like the basic page because it loads up a lot faster J
Step 3: in the next page, type in the Name of the Page (please use good Naming Convention Standard)... also make sure that the Document Library is set to Enterprise Portal
Step 4: as I choose to create a Basic Page a Text editor will pop up.. if I would choose the Web Part pages option then this editor would not appear.
Simply type something and click Save... now you would be directed to the new page.
Step 5: open the AOT in AX and browse down to the Web Menu Item Node .. right click and select New URL.
Step 6: Give the URL a name and a Label (please follow std naming conventions) and then click on the brose icon on the URL Property (the browse icon is represented by 3 dots).. as shown in picture
Then expand the Enterprise Portal folder and find the new Page you created. Select and click OK
Step 7: make sure that you set the Home Page Property to Yes..
If you dont set this to yes, then the page will not be available is the role canter page list on the User profile form (next step)
Save and close the AOT.
Step 8: in AX browse to Administration > Setup > User profiles
Create a new record, set the Profile name and description and then select the new Page as the role centre page
Assign users to the role and your done.
Additional Info:
· You can also export the role centre (user Profile > Export and Export AOT).. the export AOT options make the role centre a part of the AOT resource node.. this is helpful if you want to migrate the role centre from say a dev environment to a Test Environment.
· To create a Web Part Role centre
Rather than selecting the Basic page option ..select the Web Part page option
Then give a name and select EP as the Document Library
Notice I choose to overwrite the role page .. this way I dint have to change my url to point to the Web part page.
References:
http://msdn.microsoft.com/en-us/library/cc558235.aspx
COM Class wrapper is a immensely useful and phenomenally easy to use tool..
Basically it wraps COM objects and make them available to you (a developer) just like a normal x++ class.
You dont necessarily have to use the com wrapper to be able to use com objects.. the old and handy class COM (http://msdn.microsoft.com/en-us/library/aa655160.aspx) is sufficient.. however (with all due respect to the COM class) intellisense does not work with the COM class hence as a developer you either need to know what the com methods are (and their signatures) or you continuously will be flicking though developer documentation on the com interface.
Well COM class wrapper basically put intellisense into COM...... you can say that the COM Wrapper is to COM what References is to .Net
I would like to point out that MS discourages the use of the COM interfaces in AX. Hence, if you have the option, then consider taking the .Net route.
You can find the COM Wrapper under Developer Tools > Wizards > COM Class Wrapper
It does exactly what is says on the tin..
In the next screen one would see a list of all registered com libraries..
Select the one that you are looking for and click next... (the office 12 object library is a bad example.. as I could have done the same thin using References, which is the recommended method).
Specify an element mask.. this makes it easier to find the objects in the AOT. It also resolves the possibility of name conflicts.
So in this example mrOff was the Element Mask.
-Mohammed Rasheed
Dynamics AX Table Maps are one of the most useful elements of the Dynamics AX application Object Tree. I am using the word ‘Table Maps’ to differentiate from the foundation class Map.
The Class Map, enables you to link (or map) Key-Value pairs. However Table Maps (AOT > Data Dictionary > Maps) enable developers to map fields that are common across multiple table and apply logic that could be reused across tables..
Let me give you an example..
I once wrote a Data Conversion application... it started off as a job that basically read csv files and populated staging tables... a user would then validate/correct the data on a form and then on a push of a button, the main ax tables would be populated.. The technique worked really well for us... and we later on adopted the same technique to import Trade Agreements and Item Coverage. However, when are data set became really large, things became a bit hard to manage... we were having duplicate item issues, problems with users not being able to validate all items, hence some of the items were ending up with 0 selling prices..... So very soon I had to add code to check if the item was duped and if so highlight it.... also had to check for item prices on both inventTableModule and PriceDiscTable... Not at all a hard thing to do..
But I was bothered by the fact that I was duplicating code across numerous methods (and jobs for ad hoc updates)..... I wrote a class that would validate data... but I could not write one method that would work for all table buffers.. for example.. I wasn’t only validating itemId.. I was also validating Dimensions, prices, customers, etc etc.... not I could have written methods that took a single field as a parameter.. like a method that validated only itemid, another method that validates dimensions and so on...but that would have meant throwing away my OO design principals, which are really dear to me..
So I had 2 options here..
1. use Reflections.. some fancy things with my code [ dint have the time for it though]
2. Or keep it simple and use Maps..
I obviously decided to use maps. And I have been a fan even since I used them... I think the main benefits of using Dynamics AX Table Maps are:
Lets create a map..
Ok so in this example I created 2 staging table for Items and Trade Agreements.. and I need to validate if the itemId on the staging table actually exists in InventTable.
I created a Dynamics AX Table Map (InventValidationMap_MR) and added the itemID field to it..
The next step was to map it to the 2 staging tables... Notice the itemId field is referred to as PriceDiscItemRelation on the Trade Agreement staging table.
Next I wrote a method on the Map that checks if the item exists in InventTable
boolean checkIfItemExistsInInventTable(InventValidationMap_MR _inventValidationMap)
// this method checks if the item exists in InventTable
return InventTable::exist(_inventValidationMap.ItemId);
Now lets write a job that calls upon the map. Notice that Maps are declared and used just like tables.
static void checkIfItemIsValid(Args _args)
TradeAgreementsStagingTable_MR tradeStagingTable;
InventPriceStagingTable_MR inventStagingTable;
InventValidationMap_MR InventValidationMap;
tradeStagingTable.PriceDiscItemRelation = '1000'; // Valid item id
inventStagingTable.ItemId = '3313ds23'; // invalid item id
if(!InventValidationMap.checkIfItemExistsInInventTable(inventStagingTable))
// i.e. the item dose not exists in invent table...
info("Not in invent");
if(InventValidationMap.checkIfItemExistsInInventTable(tradeStagingTable))
//i.e. the ite exists in invent table
info("In Invent");
If you notice I am using the same method (obviously with the same parameter signature), but passing different table buffer types....Dynamics AX automatically maps the table fields to the map fields.
Have a go at Maps today.. They are really helpful.
-
Mohammed Rasheed
1. http://msdn.microsoft.com/en-us/library/bb278211(AX.10).aspx
2. MorphX IT
AX developers mostly use Dynamics AX ‘View’ for Reports.... however AX Views can be used on forms and invoked in x++ code just like ordinary tables..
Note: Unlike SQL views, AX views are ‘Read Only’. Hence they can be used to present data, but they cannot be used for update or insert operations.
A view essentially is a virtual table that is composed of a result set of a query. Views have fields, methods, field groups and can be accessed just like tables in Dynamics AX.
Views are also a lot easier to handle then queries in x++..
Advantages of using Views:
· Accessibility: if there is a specific query that is called upon regularly through various sources (code, reports, etc)... then it’s better to define the query as a View.. So a developer would not have to rewrite the code that makes up the query every time..
For example..
This view is used to query customers which have credit limit is set to a value less than 50
Also notice that the view only queries 3 fields from the custTable Data source.
This view can now be used in x++
static void viewTest(Args _args)
CustWithHighCredit_MR cus;
while select cus
info(cus.Name);
Don’t know why...but this blog reminds me of a quote I read ages ago... and I am sure, the context in which the quote was said, had nothing to do with DB views or Dynamics AX, but its relevance is striking.....so remember:
“A point of view can be a dangerous luxury when substituted for insight and understanding” - Marshall McLuhan
1. http://msdn.microsoft.com/en-us/library/bb314551(AX.10).aspx
2. http://www.brainyquote.com/quotes/quotes/m/marshallmc135184.html
Often AX consultants are a bit dubious when it comes to setting up SSRS to work with Dynamics AX. This is simply because of the number of different platforms involved (sql server and dynamics ax) and making them work together can be a bit tricky at times..
Anyways.. I recently saw a post on http://community.dynamics.com, and the user asked for steps to setup ssrs. Rather than writing a huge reply on the forum, I decided to write a blog, as there could be others out there searching for info on the same problem.
So here goes...
SQL Server 2005 Reporting Services is a server-based reporting platform for creating reports from relational or multidimensional data sources. Reports are managed and viewed in a Web browser. Microsoft Dynamics AX uses Reporting Services to create report models and to generate ad hoc reports
This document applied to SQL server 2005 and Dynamics AX 4...
Points to Note before you begin:
1. Make sure that the user accounts to run the appropriate services (AX service, sqlServer, reporting Server) have been setup.. also make sure that ax and sql are running under the defined user accounts. Also, add the new Reporting Server user account to the Dynamics AX account user group (if you don’t have a group, then assign the reporting user the same permissions)
2. Make sure that you have a fully functional instance of Dynamics AX (preferably with data loaded)
3. For convenience I will assume that all 3 services are running on the same machine (Note: In a Production Environment, its HIGHLY Recommended to run the each of the services on individual dedicated machines)
Step 1: Install and Configure IIS on the Reporting Server
IMPORTANT: Be aware that if you have installed Microsoft .NET 2.0 prior to installing IIS 6.0, IIS 6.0 will by default use the .NET version installed with your OS. To change this, as you must run .NET 2.0, do the following:
Open IIS manager -> Right click Default Web Site -> click properties -> Click the tab page ASP .NET. In the ASP .NET version filed click the drop down and choose version 2.0. Now restart your IIS server.(to restart IIS server click on Start> Run > type in ‘iisreset’ and presses enter.
It is important that you install IIS 6.0 on the SQL 2005 Server as reporting service is dependent of IIS running.
You must add FrontPage server extension and ASP.NET. Installation of IIS 6.0 can be done via ‘Start -> Control Panels -> Add remove programs. Choose windows components’, click ‘Application Server’, Click ‘Details’, check mark ASP.NET, and click ‘Internet information Services (IIS)’, check mark ‘FrontPage 2002 Server Extensions’. OK. OK. Next.
As we will be accessing reporting services using windows integrated authentication, you should enable anonymous access to the Default Web Site. Later you will also grant anonymous access to any underlying virtual directories, created by the reporting Server Configuration Manager, which we will be running later on in this document.
To enable anonymous access to the default web site do the following:
Open IIS manager -> right click the default web site -> click properties -> click the Directory Security tab page -> for Authentication and Access Control locate and click the Edit button -> Add the check mark for enable anonymous access - > click OK -> Click OK.
Step 2: Install SQL Server Reporting Services
Step 3: Configure SQL Server Reporting Services
Note: You may experience a problem when accessing the Report Manager Homepage; you may get a error relating to access rights to the c:\windows\Microsoft.Net\Framework\v2.0.50727\Temporary ASP.Net files folder, if this happens edit the security setting for this folder and add, the network service “user” and grant write permissions.
Step 4: Installing Reporting Services for Dynamics AX
This part of the Dynamics Ax installation program adds user rights to your Dynamics Ax Database for the domains user account assigned to your Reporting Server Service and creates the required access permission to this Dynamics Ax database.
The above steps completed the installation and deployment of the needed bits, what remains is configuring Dynamics Ax to be able to publish reports to the SQL 2005 Reporting Server.
Step 5: Grant the required rights for the anonymous web user (IUSR).
As all users accessing the Report Mangers website and the Reports website, all use the same account to gain access, the local IUSR_YourServerName we must ensure that this user has the needed access right and execution rights on the reporting server.
First thing we need to do is to add the IUSR_YourServerName to the System User, as well as the Content Manager role on the Reporting Server, to complete this task do the following.
Step 6: Configure Reporting Services from Dynamics AX
Complete the following steps to have reporting services up and running from within Dynamics Ax.
Step 7: Select Model Generation Options and Update Models
Setting Model generation Options
Update Models
Step 8: Building Reports Using Report Builder
Enjoy…
more Articles.... www.dynamic-ax.co.uk
Ever wrote an application in dynamics AX, which processed task/operation in a sequence, even though the tasks/operations are mutually exclusive??
Consider the following code, which gives a list of Sales order and Purchase orders that that an item is present on:
SalesLine salesLine;
PurchLine purchLine;
itemId itemid;
Itemid = “myItem”;
While select salesid from salesline group by salesid where salesLine.itemid == itemId
Info(strfmt(“Sales Order = %1”, salesLine.salesId));
While select purchid from purchLine group by purchid where purchLine.itemid == itemId
Info(strfmt(“Purchase Order = %1”, purchLine. purchid));
...
AX kernel executes the above code in sequence of occurrence, hence, in the example above, the salesOrders are queried and Only After this Task is completed, will the kernel execute the purchLine query.
Would it not make sense to run both the queries simultaneously??? ...... it surely does! So how do we do it....the answer is, Threads!
Threads are an immensely powerful way of improving performance by performing multiple operation as the same time (pseudo parallelism) and take full advantage of the multi core processors commonly available now...
But Remember... “with great power comes great responsibility”.
Threads should to used with extra extra caution. Not only are they considered to be unsecure, but they are also unpredictable! For example, if I call Thread1.run(); followed by thread2.run();, then there is no guarantee that thread1 will complete its operations before thread2. This would not be much of a problem, if the threads are performing task that are mutually exclusive (for example, one thread is querying the salesLine table, while the other thread is querying the PurchLine table). However, if the task are not mutually exclusive (for example, both the threads are writing to a shared resource), then the programmer explicitly needs to write code to handle these situations. Other platforms use semaphores to handle this problem. Semaphores are not native in dynamics ax, but can be easily built if required.
For more information, google for Race condition in threads or Dinning Philosophers problem.
In other platforms, threads are executed by extending an abstract class or implementing an interface, however in Dynamics AX, threads are objects of the class ‘Thread’.
In the following example, I wrote some quick code to show you how we could query the purchLine Table and the salesLine table at (almost) the same time and then display the results. There is some code for state handling, but it can be greatly improved to fit the needs of a production environment.
Note: There is a good Tutorial on threads in DynamicsAX. Look at the form Tutorial_Threads in the AOT.
Elements in my project:
Form/ mrThreadItemTransactions
This is a simple form, that lets the user select an item from a drop down, and will populate 2 list (sales and purch), based on the result of thread execution.
Class/mrThreadProcessor
Has two methods, both accept a thread as a parameter, the thread holds the itemId that should be queried. Once the methods complete execution, they add the list of order to the thread and return the thread to the caller.
Code:
Form:
public class FormRun extends ObjectRun
Thread salesThread;
Thread purchThread;
container salesCon;
container purchCon;
..............
public void init()
super();
if(inventItem.valueStr())
element.getPurchSales();
element.setPurchSales();
element.populatePurchList();
element.populateSalesList();
.............
void getPurchSales()
ExecutePermission perm;
perm = new ExecutePermission();
if (!perm)
return;
salesThread = new Thread();
purchThread = new Thread();
salesThread.removeOnComplete(true);
purchThread.removeOnComplete(true);
purchThread.setInputParm([inventItem.valueStr()]);
salesThread.setInputParm([inventItem.valueStr()]);
salesThread.run(classnum(mrThreadProcessor),staticmethodstr(mrThreadProcessor,getSales));
purchThread.run(classnum(mrThreadProcessor),staticmethodstr(mrThreadProcessor,getPurch));
.........
void populateSalesList()
int conL;
salesList.clear(); // clear list
conL = conLen(salesCon);
while(conL >0)
salesList.add(conpeek(salesCon,conL));
conL--;
.....
void populatePurchList()
purchList.clear(); // clear list
conL = conLen(purchCon);
purchList.add(conpeek(purchCon,conL));
...............
void setPurchSales()
// retry or wait till both the threads are processed
if(salesThread.status() != 2 && purchThread.status() != 2) // Warning:: possible infinite look, use carefully
throw Exception::Internal;
// if status = 2, then that indicates that the thread has completed // execution (I believe, 1 indicates that the thread is inProcess)
salesCon = salesThread.getOutputParm();
purchCon = purchThread.getOutputParm();
purchThread = null;
salesThread = null;
catch(Exception::Internal)
retry;
public boolean modified()
boolean ret;
ret = super();
if(inventItem.valueStr()) // Bad design: I should be check if //the condition holds in the called method, not (only) in the caller
element.redraw();
return ret;
....
Classs
static void getSales(thread _t)
salesLine salesLine;
itemid itemId;
container outContainer;
if(_t.getInputParm())
itemid = conpeek(_t.getInputParm(),1);
info("item not found");
while select salesid from salesLine group by salesid where salesLine.ItemId == itemid
outContainer += salesLine.salesid;
_t.setOutputParm(outContainer);
static void getPurch(thread _t)
purchLine purchLine;
while select purchid from purchline group by purchid where purchline.ItemId == itemid
outContainer += purchline.PurchId;
my web site - www.dynamic-ax.co.uk
Question:
What does the Ticks/Seconds field on the Code Profiler results signify?
Answer:
Ticks/seconds is an indication of the cpu cycles it take to complete an operation ..
To convert the figure into seconds or milliseconds .. one needs to divide the number by the processor frequency..
Can one view the payment file formats as templates in Dynamics AX? what is the file type and the field structure in CustIn Format1 payment format?
the method of payment(mop) file formats are based on well defined national standards, hence the format to a great extent is fixed...what I mean to say is that because paym file formats are fixed, there is no reason why end users should be provided with a template/form to modify the format.. is there?
the file formats are 'very' hard coded...that doesnt mean that they are hard to modify... but only a developer can make those changes..
There is a class for each file format and it fits in to a very well defined class hierarchy... to give you an idea of the class hierarchy.. :
There is a class called CustVendPayFormatCtrl.. this is the root class ... there are 2 classes that extend this class and they define the module (cust, vend) the payment format is for... the classes are called CustPaymFormatCtrl and VendPaymFormatCtrl.
then there is another level of classes, when extend the above 2 classes.. the classes at this level define the purpose of the file format (import, export, return, etc), these classes are CustOutPaym, CustInPaym, etc....
The last level of classes is there the file format is actually defined.. for example CustInPaymBank1..
with regards to the question on CustIn Format 1..bas far as i can see the file needs to be a csv file.. and the fields are
[
point,
collectionTime,
orderingNum,
vendNum,
accountNum,
paymId,
amount,
inPaymentDate,
postingDate
]
How can one make a ‘display method’ field, render as a link on a Dynamics AX report?
Note: the value needs to be stored in a table, it doesn’t matter if the table is not a datasource on the report query... also its better to write the display method on the table (rather than writing it on the report)
1. Create a Extended datatype for that field (after creating the edt, set the edt up on the table>field properties window)
2. Define a relation on the EDT ( this also governs the data a user sees when they click on this fields dropdown on a form).
3. on the table : set the form ref property ( this is the form you want to be opened when the user right click on the report field).
4. make sure that the display method is returning the EDT, rather than a primitive type (for example, display itemid myMethod() instead of display str myMethod() )
5. On the report control (the field in the report body).. this is the field where u have the method set to the display method... now set the table and field properties to the table/form you want opened when the user right clicks on it..
Questions:
How can I create a drop down on a form, which shows all the forms available in the AOT.
Create a new stringEdit on the form and set the extended data type property on the field to "formname".
How can one convert an XML-style date/time string (e.g., 2008-04-16T16:03:02.000-05:00) to X++ date and time values? ... In Dynamics AX 4
Using System.DateTime object.
static void dateTimeTest(Args _args)
System.DateTime netDateTime;
Date myDate;
netDateTime = System.DateTime::Parse("2008-04-16T16:03:02.000-05:00");
mydate = netDateTime.get_Date();
info(date2str(myDate,-1,-1,-1,-1,-1,-1));
Issue:
An error occurs when deploying AIF web services in Dynamics AX 4 Sp2. Error: Unable to generate a temporary class blabla.cs. On an External Server the error would appear as a Runtime Error.
Solution:
One needs to grant the business connector Proxy account permission to the win
temp directory.. %windir%\temp ..
Simply right click on the windows temp folder ... go to the permissions
tab... add the business connector proxy to the account list.. and grant Only the List Folder Contents and Read permissions...
static void createFieldFromCode(Args _args)
TreeNode tableNode;
AotTableFieldList myAOTTablefieldList;
SysDictTable sysDictTable = new SysDictTable(tablenum(moDocuref));
if (! hasSecuritykeyAccess(securitykeynum(SysDevelopment),
AccessType::View))
myAOTTablefieldList = new AotTableFieldList();
tableNode = TreeNode::findNode(#TablesPath+'\\'+sysDictTable.name());
myAOTTablefieldList =
TreeNode::findNode(#TablesPath+'\\'+sysDictTable.name() + "\\Fields");
if(!myAOTTablefieldList.AOTfindChild("newField")) // check if the field
alredy exists
myAOTTablefieldList.addString("newField");
tableNode.AOTsave();
www.Dynamic-ax.co.uk
The web services that are used by my code are provided by webSeerviceX.net. One would have to contact them for rights to use the service.
The wsdl for the web service can be found at: http://www.webservicex.net/sendsmsworld.asmx?wsdl
Currently only the following countries are services by the provider (i.e one can only send text messages to phones in these countries):
Austria, Croatia, Germany, Israel, Lithuania, Maldives, Norway, Switzerland, USA, Canada, France, India, Japan, Malaysia, New Zealand, Spain and Ukraine.
Unfortunately UK mobiles are not supported by the web service.
Anyways... this is how you can do it... click here to view the terms at which this code is to be used.
My form look like this... very basic...the idea was to make this as extensible as possible.. So others can reuse the code to fit their requirements.
the core work is done in a class: moTextFromAX J
I am using an XmlHttp com object to communicate with webServiceX..
Then the usual stuff ... add parm methods to pass variable through code in an efficient manner..
The above method is used to build the url, that is to be called... for more information on the url, visit the webServiceX website.
The next pic shows the code that actually links with webserviceX, sends the message and receives a reply from the web service.. as I said before I am using Microsoft.XMLHttp as the core integration component.
Rest is cosmetic work.. i.e. Form design..you are free to use this code directly on the main ax forms (CustTable, SalesTable, etc) ... I wanted to use a popup, which is why I created a new form.
I am defaulting 44 as the country code, I would suggest changing this to the county/region your client is based in...... its kinda silly of me to default 44 (UK country Phone code)... when UK is not supported by the web service..
The above code show how you can modify the init method to default the customers telephone number..that is if you add the new popup Text from to the customer Form...
In all honesty, I am basically trying to show you how you can streamline the process and default in as many values as possible... so the user won’t have to do the same thing twice.
Next comes the clicked method (on the ‘Send’ button).... very straight forward.... set the parm methods and call the sendText method, which in turn calls the build Url method and invokes the xmlHttp object.
and that’s all it take to make this integration work...
(Note: the message wasn’t sent, the service is not available in the UK yet)
Thanks for reading this post.
Download
People often ask me if Outlook is necessary to send email from Dynamics AX, and my answer is.. Yes! ‘Out Of the Box’, Dynamics AX does need Outlook installed on the same machine as the AX client to send out emails.
However Dynamics AX can very easily be customized to send out standard reports via email, without outlook. We are talking about a really small modification here, there is only one method that needs to be edited, hence for clients that find outlook to be too expensive to fit their budget or if there are architectural issues (for example, installing outlook on Terminal Server),this code could be a life saver...
Note: AX – Outlook integration is confined to email, there are many other benefits of using outlook with dynamics ax, such as synchronization of crm contacts, transferring appointment to outlook calendar, etc. If the client chooses not to install outlook with Dynamics AX, then the company would be loose out on all these features.
Anyways.. this is what you need to do:
The following method is used to send emails from Dynamics AX:
Class\Info\Method\ReportSendMail
Out of the box, AX uses the class SysInetMail, which invokes the user email profile to find the outlook / email client that is being used on the system, obviously if it doesn’t find out it would throw an error.
There are 2 (possibly more, but 2 that I know of) other ways to send out mails from ax (easily).
1. Microsoft.CDO Com Object – Collaboration Data Objects can be used to send email and it comes pre installed on all AX supported versions of Windows.. as a matter of fact the emails generated by Alerts use the class SysMailer, which in turn uses a CDO object.
2. System.Net.Mail Assembly – In this example I choose to this path, simply because of the flexibility of the API.. Note: for this to work System.Net should be a part of the Ax References (AOT > References) ...by default it always is one of AX references, but there is no harm is double checking.
Here is the code... Note: in order to use this code one needs to agree to these Terms.
//<CHANGE>
// <DATE>##/##/###</DATE>
// <TRACKING_ID>Mohammed</TRACKING_ID>
// <DESCRIPTION>
// Email out of ax withot outlook.
// the default ax code uses inet to send email and the sysInetMail class check the users profiles for a outlook account.
// changing code to use System.Net.Mail assembly
// </DESCRIPTION>
//</CHANGE>
void reportSendMail(PrintJobSettings p1)
//Mohammed : Start Declaration
//SysINetMail m = new SysINetMail(); // Mo : Commented out old AX code
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;
userinfo userInfo;
//end Declaration
str fileName = 'axaptareport';
if (p1.format() == PrintFormat::ASCII)
fileNameForEmail = subStr(p1.fileName(),strLen(p1.fileName())-3,-999)+'TXT'; // Mo : NL
//fileName = fileName + '.txt'; // Mo Commented this line
else if (p1.format() == PrintFormat::RTF)
fileNameForEmail = subStr(p1.fileName(),strLen(p1.fileName())-3,-999)+'RTF';
//fileName = fileName + '.rtf';
else if (p1.format() == PrintFormat::HTML)
fileNameForEmail = subStr(p1.fileName(),strLen(p1.fileName())-3,-999)+'HTM';
//fileName = fileName + '.htm';
//else if (p1.format() == PrintFormat::PDF) // Mohammed :Performance Testing : commentign this line and replacing the line below.
else if (p1.format() == PrintFormat::PDF || p1.format() == PrintFormat::PDF_EMBED_FONTS)// Mohammed :Performance Testing :(replacing the above line) addign this line as it was present in the jsRemotecontroller project.. can be removedd later..
fileNameForEmail = subStr(p1.fileName(),strLen(p1.fileName())-3,-999)+'PDF';
//fileName = fileName + '.pdf';
//Mohammed : Start Logic
mail = subStr(fileNameforEmail,(strlen(fileNameforEmail)-8),9);
select firstonly name from userInfo where userInfo.id == SysuserInfo::find().Id; // to find the user name
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.");
throw error("Cannot gain access to Temp location.");
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
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);
winApi::deleteFile(fileNameforEmail); // delete temp file
// Mohammed end
I hope this code is of use to you.
Thanks for reading..