Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
The information contained in this document represents the current view of Microsoft Corporation on the issues
discussed as of the date of publication. Because Microsoft must respond to changing market conditions, it should
not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any
information presented after the date of publication.
This document is for informational purposes only. MICROSOFT MAKES NO WARRANTIES, EXPRESS, IMPLIED
OR STATUTORY, AS TO THE INFORMATION IN THIS DOCUMENT.
Complying with all applicable copyright laws is the responsibility of the user. Without limiting the rights under
copyright, no part of this document may be reproduced, stored in or introduced into a retrieval system, or
transmitted in any form or by any means (electronic, mechanical, photocopying, recording, or otherwise), or for any
purpose, without the express written permission of Microsoft Corporation.
Microsoft may have patents, patent applications, trademarks, copyrights, or other intellectual property rights
covering subject matter in this document. Except as expressly provided in any written license agreement from
Microsoft, the furnishing of this document does not give you any license to these patents, trademarks, copyrights, or
other intellectual property.
2008 Microsoft Corporation. All rights reserved. Microsoft Dynamics, Microsoft PowerPoint Microsoft SQL
Server and Microsoft Dynamics AX MorphX are trademarks or registered trademarks of Microsoft
Corporation. The names of actual companies and products mentioned herein may be the trademarks of their
respective owners.
INTRODUCTION TO DEVELOPMENT IV IN
MICROSOFT DYNAMICS AX 2009
Welcome
We know training is a vital component of retaining the value of your Microsoft
Dynamics AX 2009 investment. Our quality training from industry experts
keeps you up-to-date on your solution and helps you develop the skills necessary
for fully maximizing the value of your solution. Whether you choose Online
Training, Classroom Training, or Training Materials; there is a type of training to
meet everyone's needs. Choose the training type that best suits you so you can
stay ahead of the competition.
Online Training
Online Training delivers convenient, in-depth training to you in the comfort of
your own home or office. Online training provides immediate access to training
24 hours-a-day. It is perfect for the customer who does not have the time or
budget to travel. Our newest online training option, eCourses, combine the
efficiency of online training with the in-depth product coverage of classroom
training, with at least two weeks to complete each course.
Classroom Training
Classroom Training provides serious, in-depth learning through hands-on
interaction. From demonstrations to presentations to classroom activities, you
receive hands-on experience with instruction from our certified staff of experts.
Regularly scheduled throughout North America, you can be sure you will find a
class convenient for you.
Training Materials
Training Materials enable you to learn at your own pace, on your own time with
information-packed training manuals. Our wide variety of training manuals
feature an abundance of tips, tricks, and insights you can refer to again and again:
Look for a complete list of manuals available for purchase on the Microsoft
Dynamics website: www.microsoft.com/Dynamics.
Challenge Yourself!
Level 3 exercises are the most challenging. These exercises are designed for the
experienced student who requires little instruction to complete the required task.
Step by Step
Level 1 exercises are geared towards new users who require detailed instructions
and explanations to complete the exercise. Level 1 exercises guide you through
the task, step by step, including navigation.
Documentation Conventions
The following conventions and icons are used throughout this documentation to
help you quickly and effectively navigate through the information.
CAUTION: Cautions are found throughout the training manual and are preceded by
the word CAUTION in bold. Cautions are used to remind you of a specific result of a
specific action which may be undesirable.
HINT: Hints are found throughout the training manual and are preceded by the word
HINT in bold. Hints are used to suggest time-saving features or alternative methods for
accomplishing a specific task.
NOTE: Notes are found throughout the training manual and are preceded by the word
NOTE in bold. Notes are used to provide information which, while not critical, may be
valuable to an end user.
BEYOND THE BASICS: Advanced information found throughout the training manual
is preceded by the words BEYOND THE BASICS in bold. Beyond the Basics provides
additional detail, outside of standard functionality, that may help you to more optimally
use the application.
EXAMPLE: Examples are found throughout the training manual and are preceded by
the word EXAMPLE in bold. Examples bring to light business scenarios that may better
explain how an application can be used to address a business problem.
Student Objectives
What do you hope to learn by participating in this course?
1.
2.
3.
Introduction
To practice using the knowledge and skills you learn during this course, you will
develop a POS module which encompasses most of the modules that are
discussed.
This lesson also describes the basic POS design which is built upon during the
course, adding functionality appropriate to the lesson being reviewed.
Overview
The following is an example of how the POS form looks when completed.
FIGURE 1.1
FIGURE 1.2
An end of day process is used to post the actual money counted to a cash
account, and any difference between the posted amount and the counted amount
to a difference account.
There is a basic receipt printed, and the buyer is also offered the option of having
the receipt emailed to them in XML format.
Functional Design
The POS module in this scenario is for The Light Company used in the standard
Microsoft Dynamics AX demo data. It is not a supermarket-type cash register,
where very little information is displayed, but shows more information, which
could include information such as: delivery address, quantity on hand and special
instructions.
Because this POS is for people who walk in and pay for items immediately, do
not create a customer account each time. Instead, use a one-time customer
parameter. This is normally used as a template for creating a new customer for
each sale, but you will use one customer for every sale.
Sales Order
The POS has a Register ID to distinguish each register. Payments can be posted
against each register, which enables an end of day routine to be processed.
The sales order type should be Journal, meaning that inventory transactions only
get posted at the time of physical movement. This increases performance when
entering lines. In this case, the time of physical movement is when an invoice is
posted, as this is the only order update available.
Sales Id
Register Id
Item Id
Quantity
Sales Price
Line Amount
Item Name
Include a Payment button that opens the Payment Lines form, which contains the
following fields:
Payment Type
Amount
Include the capability to print a receipt from the sales line (for now, it is a pro-
forma receipt). The receipt should show:
Sales Id
Date
Item Id
Qty
Price
Total
Payment Type
The payment form and the pro-forma receipt are called from buttons on the POS
form.
Technical Design
The POS module sits on top of the standard sales module, and uses some of the
sales module code, tables, and procedures.
Tables
POSTable Form
Register Id can be either prompted for when the form is opened or the selection is
mandatory on an order and remembered for subsequent orders.
SalesQty, SalesPrice, and LineAmount are calculated from the item table when
an item id is entered.
POSRegisterTrans Form
The header section contains two buttons - one to call the POSRegisterTrans form,
and one to call the POSReceipt report.
Reports
Summary
This lesson leads you through creating the base elements for the POS module that
will be used throughout the entire Development IV course.
1.
2.
3.
Introduction
Number sequences handle the automatic allocation of ID numbers, vouchers, and
journal numbers. Numbers are seen and referred to by the user; vouchers are used
by the system for tracking and linking transactions posted at the same time. In
some instances you can define a voucher series to follow a number series, for
example, invoice numbers and invoice vouchers. There are many number
sequences needed for the application, and all can be set up so that they have a
unique format and numbering range.
Overview
A number sequence is created under main menu > basic > setup > number
sequences > number sequence. Specify the smallest and largest numbers
allowed for the particular sequences, and also the next number to be used. The
format field controls the length of the number and is used to insert fixed
characters when desired.
Continuous number sequences ensure that no numbers in the sequence are lost. If
a number is not used, such as when a sales order is deleted before it is saved, that
number can be used later.
Tables
The tables used for number sequences are:
Classes
The main classes used for number sequences are:
Use one of the static methods attached to the NumberSeq class. There are two
sets of methods; one set is called newGet<function> and the other is
newReserve<function>. These methods are identical; newGetNum is the same as
newReserveNum.
The name of the static methods describes their functions. For example,
newGetNumAndVoucherFromCode() enables the instance to return both a
number and a voucher from a number sequence code (instead of a reference), and
may be used when posting an invoice where an invoice number and a voucher
number are required.
NumberSeq = NumberSeq::NewGetNumAndVoucher(
SalesParameter::NumRefInvoiceId(),
SalesParamters::NumRefInvoiceVoucher(), false, false)
To retrieve the number or voucher, you can use the num() or voucher() methods:
Voucher = NumberSeq.voucher();
When writing code to assign a new number, decide what to do if the number
sequence has been set up as continuous. You can commit the number
immediately, or place it in the list to be updated later.
In a form, the decision may be made later, as the user may delete the record. In a
process, you do not need the decision to be made later, as this is controlled using
TTS.
If you do not make the decision later, the system creates the number in
NumberSequenceList, and cleans it up later, during the TTSCOMMIT. The clean
up is called in the xApplication.ttsNotifyCommit() method. The message "System
does not support setup of continuous number sequence." displays. This is caused
by attempting to get a new number from a number sequence set up as continuous,
but not doing so inside a TTS. Adding TTSBEGIN and TTSCOMMIT around
the code fixes the problem.
Format a Number
The method to format a number is called from the num() method, so normally
you do not need to call this method from your code.
If you do require to format a number in your code, this can be done by using the
NumberSeq::numInsertFormat() static method, which takes an integer number
and a format as parameters.
Num = NumberSeq::numInsertFormat(30,"&&&");
Number Pre-Allocation
Processes that use many numbers from a single number sequence can improve
performance by using number pre-allocation. Basically, this pulls a set number of
numbers into the memory and provides faster access. For example, the inventory
close process uses voucher numbers if posting to the ledger. The overall speed
can be increased by allowing pre-allocation on the number sequence for Closing
Vouchers in Inventory Parameters.
Clean Up Process
Automatic clean up is done by storing a list in memory of number sequences that
must be checked.
The clean-up process is called when the current transaction is committed. It goes
through this list (created in setClean()) and finds any entries that have dead
sessions (NumberSeqCleanUp.isProcessDead()).
The manually started clean up process goes through either a specified number
sequence or all number sequences, looks at any records in the
NumberSequenceList, and checks whether they should be deleted.
Form Handler
To help use number sequences in forms correctly, call the
NumberSequenceFormHandler class. Instantiate the class using the static method
NewForm(). The parameters are:
You must add code to call methods in the various datasource methods. The
names of the methods clearly define where they should go. For example:
numberSeqFormHandler.formMethodDataSourceDelete();
The CustTable form shows an example of how to use this class correctly.
NumberSeqReference
To create a new number sequence reference; for example, a newly created data
type that needs sequential numbers assigned to it, have the new data type created
in the number sequence references. This displays it in the appropriate modules
parameters form.
1. Create a new Extended Data Type (EDT). Often this EDT extends
num. This is not mandatory, but it is a best practice.
2. Decide which module's parameters this number sequence reference
should be included in, and find the corresponding
NumberSeqReference sub class.
3. The loadModule() method shows a number of blocks of code, which
creates records in the table NumberSequenceReference.
5. There are a number of wizard fields used for default values when
using the wizard to create number sequences.
6. Create a static method that will be used to retrieve the reference. This
is usually done on the relevant parameters table. Parameters tables
show methods beginning with numRef. Use one of these methods as
a template.
7. The reference can then be referred to using this static method.
For example:
salesParameters::NumRefSalesID()
Summary
This lesson provides an overview of how number sequences are used in the
application. It shows how number sequence API works and how to implement
number sequence API in the code.
3. From which method would you get the number sequence reference used for a
sales order number?
Challenge Yourself!
In the POS Module, create a new reference for the new EDT POSPayId. The
POS module should have its own sub class of NumberSeqReference, but should
be displayed in the SalesParameters form.
Step by Step
Challenge Yourself!
Add the NumberSeqFormHandler methods to the forms POSTable and
POSPayTable to create new number for SalesId and POSPayId.
Step by Step
1.
2.
3.
Solutions
Test Your Knowledge
1. When assigning numbers from the numberseq class, when would you use
newGetNumAndVoucherFromCode() and newGetNumAndVoucher()?
3. From which method would you get the number sequence reference used for a
sales order number?
CHAPTER 3: PRINTJOBSETTINGS
Objectives
The objectives are:
Introduction
The PrintJobSettings class is used with report output. It is a system class that
contains methods and variables to hold all the settings that determine where and
how the output is handled. It can be used both to get and set these options, and
can also format all the options such that they can be easily saved and retrieved
later.
This lesson explains and provides examples on how to set and retrieve print
options and how to use pack and unpack to store these settings. Furthermore, the
class SysPrintOptions is briefly introduced.
All these options, and more, can be set within the code. For example:
printJobSettings.setTarget(PrintMedium::Mail)
This is shown in the Cheque_US report in the init() method, where it forces the
target to be a printer.
Boolean ret;
int trayCount;
int i;
printJobSettings printJobSettings =
element.printJobSettings();
if (ret)
return ret;
The code for this can be seen in the SalesFormLetter.validate() class method,
where the instance of PrintJobSettings is instantiated using a container stored for
each document type and user, and if the target is Screen, then a warning is given.
Last user-selected printer settings are stored in the Usage Data (sysLastValue) as
with values stored in pack and unpack in RunBase. They can be seen in the
Usages Data form, type = Report, name = report name.
Use of SysPrintOptions
SysPrintOptions is a class that has methods written for using PrintJobSettings.
When printing, processing is done on the Application Object Server (AOS), but
the printer may be local to the client, or not installed on the server. Printing
works differently on the server than on the client. SysPrintOptions can be used to
circumvent these issues.
This class is used by the system to control printing, and is not normally needed to
be called by the developer.
printJobSettings =
SysPrintOptions::newPrintJobSettingsOnServer();
sysPrintOptions.setPrintJobSettings(printJobSettings);
sysPrintOptions.buildPrinterMap();
printerMap = sysPrintOptions.getPrinterMap();
mapIterator.begin();
while (mapIterator.more())
{
info(mapIterator.value());
mapIterator.next();
}
Summary
This lesson describes how to use the system class PrintJobSettings in connection
with report output.
The lesson explains and provides examples on how to set and retrieve print
options and how to use pack and unpack to store these settings.
Challenge Yourself!
Use the printJobSettings.suppressScalingMessage() to stop the message from
appearing. The report to modify is SalesLinesExtended.
Step by Step
Add the following code to the init() method of the report SalesLinesExtended. It
should go after the call to super().
element.printJobSettings().suppressScalingMessage(true);
Challenge Yourself!
In the POS module, enable printer options to be set up and saved for each
register.
1. Add a container field to the register table, to store the print job
settings for the register.
2. To see similar functionality where printer settings are set and saved,
look at the Production order Startup parameters, pick list printer
settings.
Step by Step
HINT: Look at the Production order Startup parameterspick list printer settings.
4. Add a button to the POS Register table form to call this new method.
Challenge Yourself!
When the receipt is printed, set the printer options to the options stored against
the appropriate register.
Step By Step
Challenge Yourself!
Remove the ability for the user to change the printer options on the receipt by not
displaying any prompt for the report. Use the print options selected for the
register. As soon as the user clicks the Receipt button, it should print.
Step By Step
1. Override the prompt method on the report, and remove the call to
super(). Return true from the method.
1.
2.
3.
Solutions
Test Your Knowledge
1. What is the syntax for setting the default printer option to e-mail in the
PrintJobSetting class?
Introduction
When using the Microsoft Dynamics AX 2009 Business Connector, other
applications can access Microsoft Dynamics AX 2009 as a .NET object. This
implies that the application can gain access to the data and business logic of
Microsoft Dynamics AX 2009, which enables the use of applications as front
ends, other than Microsoft Dynamics AX 2009.
.NET Platform
The Business Connector is based on the .NET platform and provides a set of
managed classes that provide easy access to X++ functionality in Microsoft
Dynamics AX 2009. It supports the functionality in the Enterprise Portal server,
Reporting server, and Application Integration server roles. The Business
Connector can be installed as a stand-alone component and used to develop third-
party applications that integrate with Microsoft Dynamics AX 2009. The
following are some characteristics of the Business Connector:
Proxy
The Business Connector Proxy is a Windows domain user account that is used to
enable the Business Connector to "act-on-behalf" of Microsoft Dynamics AX
2009 users who cannot be fully authenticated. The Business Connector Proxy has
unique configuration settings that can be modified. A configuration target field
has been added to the utility to enable you to select the Business Connector
Proxy.
For more information on the proxy user, consult the Microsoft Dynamics AX
2009 Administrator Guide, found under the Help menu.
Event Monitoring
The Microsoft Dynamics AX 2009 Business Connector interfaces with the Event
Viewer component, which is an integrated part of the Microsoft Windows Vista,
Windows XP, Windows 2000, and Windows NT 4.0 operating system. The
Microsoft Dynamics AX 2009 Business Connector logs specific events to the
Application Log of the Event Viewer. For instance, the Microsoft Dynamics AX
2009 Business Connector logs whenever it is started or stopped. Any unexpected
events are also logged in the Application Log for further investigation by the
administrator.
All events logged by the Microsoft Dynamics AX 2009 Business Connector have
a source name of "Microsoft Dynamics AX Business Connector."
Debugging
In Microsoft Dynamics AX 2009 it is possible to enable user breakpoints for
X++ code running in the Business Connector, and global breakpoints for X++
code running in the Business Connector or in a user session. A global breakpoint
is one that is set for a computer, instead of a user, and can be shared between
developers.
To start debugging, log on to the client, and follow this path: Tools > Options >
Developer tab.
9. On the profile for the AOS instance that the Business Connector will
talk to, select Enable breakpoints to debug X++ code running on
this server.
10. For a .NET application to use the Microsoft Dynamics AX 2009
debugger for displaying breakpoints, an instance of the Microsoft
Dynamics AX 2009 debugger must be running. This can be achieved
by following this path in a Microsoft Dynamics AX client: Tools >
Development tools > Debugger.
11. When running the .NET code in Visual Studio, it must be run in
debug mode for the X++ breakpoints to work.
To ensure that the Visual Studio debugger jumps to the Microsoft Dynamics AX
2009 debugger, when a breakpoint exists in X++ code called from .NET code,
the following must be in place:
Managed Classes
The following managed classes are provided by the .NET Business Connector:
Axapta
AxaptaBuffer
AxaptaContainer
AxaptaObject
AxaptaRecord
VariantWrapper
These classes can be used in Visual Studio projects to interface with Microsoft
Dynamics AX 2009, through the .NET Business Connector. Each class contains a
collection of methods that can be called to perform business logic in Microsoft
Dynamics AX 2009.
Axapta Class - The Axapta class provides the ability to connect to the Microsoft
Dynamics AX 2009 system, create Microsoft Dynamics AX 2009 classes, create
Microsoft Dynamics AX 2009 record, container, and buffer objects, execute
transactions, and perform other Microsoft Dynamics AX 2009 system tasks. The
Axapta class also contains methods to call static class methods, static table
methods, jobs, and TTS commands.
AxaptaBuffer Class - The AxaptaBuffer class provides the ability to add data to
and retrieve data from a Microsoft Dynamics AX 2009 buffer. An AxaptaBuffer
object can be used with AxaptaContainer objects.
The following are examples of Visual Basic code in Visual Studio, which
execute Microsoft Dynamics AX 2009 business logic through the Business
Connector. (Examples using other .NET languages can be found in the Developer
Help in the Microsoft Dynamics AX 2009 application.)
Subsequent boxes assume that previous variables and objects are still available
(for example, object axapta1).
company ="dmo"
language ="en-us"
objectServer ="objectServerName"
configuration ="configurationName"
Parameter Description
user (String) Name of the Windows user to use for logging
on to Microsoft Dynamics AX 2009. This parameter is
optional.
domain (String) The domain associated with the Windows
user. This parameter is optional
bcProxyCredentials (NetworkCredential) A .NET network credential
object.
company (String) The company to activate. This parameter is
optional and is used to override the corresponding
parameter in the configuration being used.
language (String) The language to use for Microsoft Dynamics
AX 2009 labels. This parameter is optional and is used
to override the corresponding parameter in the
configuration being used.
objectServer (String) Name of the Microsoft Dynamics AX 2009
Object Server to connect to. This parameter is optional
and is used to override the corresponding parameter in
the configuration being used.
Parameter Description
configuration (String) Name of Microsoft Dynamics AX 2009
configuration to use while logging on. This parameter
is optional. If null is specified for this parameter and
Business Connector is being executed within the
context of an interactive Windows account, Business
Connector will use the default configuration
(maintained by the Microsoft Dynamics AX Client
Configuration Utility). If Business Connector is being
executed within the context of a non-interactive
Windows account (such as the Business Connector
Proxy AD user), then this parameter is used to specify
the path which points to an exported configuration file.
company ="dmo"
language ="en-us"
objectServer ="objectServerName"
configuration ="configurationName"
username ="proxyUsername"
domain ="domain"
className ="salesLine_Net"
methodName ="calcPrice"
itemId ="001"
qty ="5"
salesLine_Net = axapta1.CreateAxaptaObject(className)
tableName ="InventTable"
inventTableRecord = axapta1.CreateAxaptaRecord(tableName)
axapta1.ExecuteStmt(statement, inventTableRecord)
Summary
The Business Connector provides a conduit between Microsoft Dynamics AX
2009 and other applications. Besides the most common use of supporting the
connection between SharePoint and Microsoft Dynamics AX 2009 for the
Enterprise Portal, the Business Connector also opens up the existing Microsoft
Dynamics AX 2009 business logic and data to external applications within your
business scope.
2. Do you need to register the .NET connector into the Global Assembly Cache
after it is installed?
6. Using a .NET language, in Visual Studio how would you create a new
instance of the SalesFormLetter Microsoft Dynamics AX 2009 class?
Challenge Yourself!
Create a front end for the POS module using a .NET form that can be used in a
'supermarket' environment. The form should have a large display, can display
item descriptions and prices, and a large number pad (so touch screens can be
used). There should also be buttons that can complete an order and delete the last
line.
Step by Step
As you may not be familiar with .NET, this form has been created for you. It has
been created using C# .NET as the syntax for C# is similar to X++.
To use this version, run the executable POSConsole.exe, included in the file
POSConsole.zip, which then uses the Microsoft Dynamics AX 2009 Business
Connector to communicate with the installation. You still need to write the
necessary code in Microsoft Dynamics AX 2009, as detailed below.
The second version has most of the interface built already, but you need to add
the procedure to call the post invoice function.
Do as follows:
The POS executable calls the following class methods. Create these classes and
methods:
2. Class SalesOrderIntegration
a. SalesOrderIntegration.getItemName(ItemId _itemId): returns
item name or specified item id.
b. SalesOrderIntegration.getSalesPrice(ItemId _itemId, qty _qty):
returns the sales price for the item id and the qty specified.
c. SalesOrderIntegration.getSalesTotal(SalesId _salesId): returns
the total amount for the specified sales order.
Do as follows:
The POS executable calls the following class methods. Create these classes and
methods:
2. Class SalesOrderIntegration
a. SalesOrderIntegration.getItemName(ItemId _itemId): returns
item name or specified item id.
b. SalesOrderIntegration.getSalesPrice(ItemId _itemId, qty _qty):
returns the sales price for the item id and the qty specified.
c. SalesOrderIntegration.getSalesTotal(SalesId _salesId): returns
the total amount for the specified sales order.
Create a front end for the POS module using a .NET form that could be used in a
'supermarket' environment. The form should have a large display, can display
item descriptions and prices, and a large number pad (so touch screens can be
used). There should also be buttons that can complete an order and delete the last
line.
Or:
You may use the front end already created for you in C#. This form already has
most of the interface built already, but you will need to add the procedure to post
the invoice.
Step by Step
Do as follows:
The POS executable calls the following class methods. Create these classes and
methods:
2. Class SalesOrderIntegration
a. SalesOrderIntegration.getItemName(ItemId _itemId): returns
item name or specified item id.
b. SalesOrderIntegration.getSalesPrice(ItemId _itemId, qty _qty):
returns the sales price for the item id and the qty specified.
c. SalesOrderIntegration.getSalesTotal(SalesId _salesId): returns
the total amount for the specified sales order.
1.
2.
3.
Solutions
Test Your Knowledge
1. Describe the purpose of the Business Connector.
2. Do you need to register the .NET connector into the Global Assembly Cache
after it is installed?
6. Using a .NET language, in Visual Studio how would you create a new
instance of the SalesFormLetter Microsoft Dynamics AX 2009 class?
MODEL ANSWER:
//Visual Basic
Dim salesFormLetter As AxaptaObject
Dim axapta1 As Axapta
Dim className As String
className = "salesFormLetter"
salesFormLetter = axapta1.CreateAxaptaObject(className)
Introduction
The Common Runtime Language (CLR) Interoperability (shortened to Interop)
feature enables X++ developers to add CLR assemblies to the AOT and to write
X++ code that interoperates with objects in these assemblies. This provides
access to a vast array of prefabricated code, to leverage functions already
available in the operating system or external applications. This lesson describes
how to reference assemblies in Microsoft Dynamics AX 2009 and leverage them
in X++ code.
To see the assemblies already referenced in the AOT, expand the References
node. The following figure illustrates the sys layer assembly references.
\%winnt%\assembly
Install assemblies available to all clients into the GAC on each Application
Object Server (AOS) servicing the clients. Remember to install server assemblies
into the GAC on the development, test, and production AOS servers.
IMPORTANT: Code using assemblies installed on the AOS must be set to run on
the server, not on the client.
The GAC enforces that all assemblies installed into it are strongly named. A
strong name consists of the assembly's identity (the simple text name, version
number, and culture information (if provided)), a public key and a digital
signature. It is generated from an assembly file (the file that contains the
assembly manifest, which contains the names and hashes of all the files that
make up the assembly), using the corresponding private key. Microsoft Visual
Studio .NET and other development tools provided in the .NET Framework
SDK can assign strong names to an assembly. Assemblies with the same strong
name are expected to be identical.
HINT: Refer to the MSDN on strongly named assemblies for more information.
There are many .NET assemblies available in the Windows operating system.
One example is the System.Net.Mail assembly, which provides code to send e-
mails. The following is an example of X++ code which leverages the code in the
System.Net.Mail assembly, to create an email with a multiple attachments:
System.Net.Mail.MailMessage mailMessage;
System.Net.Mail.Attachment attachment;
System.Net.Mail.AttachmentCollection attachementCollection;
;
mailMessage = new
System.Net.Mail.MailMessage("me@contoso.com","you@contoso.c
om");
attachementCollection = mailMessage.get_Attachments();
attachment = new
System.Net.Mail.Attachment("c:\\myfile.txt");
attachementCollection.Add(attachment);
attachment = new
System.Net.Mail.Attachment("c:\\myphoto.jpg");
attachementCollection.Add(attachment);
//.NET code
namespace SampleCLR
{
public class HelloWorld
{
public string sayHello()
{
return "Hello CLR Interop World!";
}
If this assembly was available as a DLL file, it could be added to the References
in the AOT. Once referenced, call the assemblies contents directly in X++ code.
The following X++ code example leverages the .NET code in the previous
assembly:
//X++ code
static void JobCLR(Args _args)
{
SampleCLR.HelloWorld hw;
str s;
int i;
;
hw = new DemoCLR.HelloWorld();
s = hw.sayHello();
info(s);
i = hw.add(18,29);
info(int2str(i));
}
InteropPermission Class
Recall from the Writing Secure X++ Code topic in the Development III course,
that classes categorized as "secured" require a permission class to run.
The CLR Interop classes are classified as "secured" APIs. They have a
permission class called InteropPermission. The new method of this class
requires an InteropKind type parameter, which is an enum. When granting
permission to CLR Interop classes, use the InteropKind::CLRInterop value.
The following is an example of CLR Interop code with a permission class:
new
InteropPermission(InteropKind::CLRInterop).assert();
xmlDoc = new System.Xml.XmlDocument();
//...some xml code here...
xmlDoc.Save('c:\\test.xml');
}
To consume an external Web service from X++, a reference to the Web service
must first be created. After creating a reference to the Web service, it can be
invoked from X++ and the available methods can be seen using Intellisense.
Calling and managing external Web services is done entirely within Microsoft
Dynamics AX 2009.
3. In the WSDL URL field, enter the location of the service WSDL,
http://soap.search.msn.com/webservices.asmx?wsdl.
This field can contain a path to the local file system, a file share, or
an Internet address. The following are valid formats for the WSDL
URL:
o http://soap.search.msn.com/webservices.asmx?wsdl
o http://localhost/WebServices/SalesTableservice.asmx?WSDL
o c:\WebServices\SalesTableService.wsdl
4. In the .NET code namespace field, enter a unique name for the
.NET namespace such as WindowsLiveSearch. This is the namespace
in which the generated proxy assembly and other files will reside.
5. In the Reference name field, enter a unique name for the Web
service without spaces or special characters such as LiveSearch.
This name is used as the name of the .NET assembly that is created
and the name of the directory that contains the generated Web
reference files.
6. In the Service description field, enter a description for the Web
service.
7. Click OK. The Web reference is generated and the proxy files are
saved to the following directory: <Microsoft Dynamics AX Install
Directory>Application\Appl\DynamicsAx\ServiceReferences\<Web
Service Name>.
1. In the AOT, navigate to the Classes node, right-click and select New
Class.
2. Right-click the new class and select Properties.
3. Set the Name property to LiveSearchTest.
4. Set the RunOn property to Server.
5. Right-click the class and select New Method. Replace the method
with the following code in the code editor. This adds a main method
that calls the search service with a search string and displays the
results in an Infolog window.
6. Right-click the class and select New Method. Replace the method
with the following code in the code editor. This method creates the
source request and puts it in an array.
try
{
new
InteropPermission(InteropKind::ClrInterop).assert();
sourceRequest.set_Source(sourceType);
7. Right-click the class and select New Method. Replace the method
with the following code in the code editor. This is the method that
calls the Live Search Web service and returns the search results in a
container.
searchRequestObj.set_AppID("YOUR_LIVESEARCH_APP_ID");
searchRequestObj.set_CultureInfo("en-us");
searchRequestObj.set_Query(searchQuery);
sourceRequestArray =
LiveSearchTestClass::getSourceRequestArray();
searchRequestObj.set_Requests(sourceRequestArray);
totalHits = ClrInterop::getAnyTypeForObject(
sourceResponse.get_Total());
resultsArray = sourceResponse.get_Results();
resultsLength = resultsArray.get_Length();
searchResultBuff.appendText(#delimiterCRLF);
description =
searchResult.get_Description();
searchResultBuff.appendText(description);
searchResultBuff.appendText(#delimiterCRLF);
url = searchResult.get_Url();
searchResultBuff.appendText(url);
searchResultBuff.appendText(#delimiterCRLF);
searchResultBuff.appendText("--------------
-------
---------------------------------------
-----");
searchResultBuff.appendText(#delimiterCRLF);
}
}
CodeAccessPermission::revertAssert();
}
catch(Exception::CLRError)
{
throw error(AifUtil::getClrErrorMessage());
}
}
You should now have a class named LiveSearchTest with three methods:
getSourceRequestArray
main
SearchLive
8. Compile the class and Run the main method. The search results
should appear in an Infolog window as shown in the following
figure.
Implementing a DLL
A Dynamic Link Library (DLL) is a collection of small Windows programs that
can be called upon to complete a particular function. They are often used to find
information about the current environment, such as the amount of free space on a
disk, or which window is currently active.
Each DLL has one or more functions that can be called. It is not always easy to
find all the available functions and how to use them, as documentation or
definition files are often not installed with the DLL.
MSDN describes some of the functions available in a few main Windows DLL's,
such as Kernel32.dll and user32.dll.
The best example of using a DLL within Microsoft Dynamics AX 2009 is the
WinAPI class. This implements useful functions from kernel32.dll, user32.dll,
and others. For example:
{
int fileSize;
;
fileSize = winAPI::FileSize(c:\\myfile.txt);
}
This method uses two other methods to obtain the desired result. Analyze the
getFileSize() method to understand how to implement a specific function.
ChartFX
Another example of using DLLs in Microsoft Dynamics AX 2009 is the use of
ChartFX.dll. This enables a high-quality graphical representation of data on
forms. Follow this example:
The following example uses customer transactions in a graph, where the x-axis is
time, the y-axis is customer balance, and the z-axis is customers. This form is
dynamically linked to the CustTable, so keeping the new form open together with
the customer form, means a new row appears every time you highlight a new
customer. As a result, the new customer's transactions are added to the graph.
Graphics graphData
5. Override the form's init method and add the following before super():
#MACROLIB.ChartFX
;
graphData = graphics::newGraphicsTitlesLayout(
designGraph, //ActiveX control
750, //Width
375, //Height
"Customer transaction", //Title
"Date", //Title X-Axis
"Amount", //Title Y-Axis
"Customer", //Title Z-Axis
#Bar | /*#CT_SHOWVALUES |*/ #CT_CLUSTER |
#CT_3D | #CT_TOOL | #CT_SHOWZERO |
#CT_LEGEND, //Type
#CS_ALL, //Style
#CTE_STEPLINES, //ExtType
0); //ExtStyle
qbd = query.addDataSource(tablenum(CustTrans));
qbd.addRange(fieldnum(CustTrans,
accountNum)).value(SysQuery::value(custTable.AccountNum));
while (queryRun.next())
{
custTrans = queryRun.get(tablenum(custTrans));
graphData.loadData(
date2str(custTrans.TransDate,123,2,2,2,2,2),
custTrans.AccountNum,custTrans.AmountMST);
}
graphData.showGraph();
}
Clicking the button for a customer in the custTable form opens a form similar to
the following (provided some customer transactions exist):
Summary
The Common Runtime Language (CLR) Interoperability (shortened to Interop)
feature enables X++ developers to add CLR assemblies to the AOT and to write
X++ code that interoperates with objects in these assemblies. This lesson
demonstrated how to:
3. What code do you use, to provide Code Access Security permission to a CLR
Interop class?
6. Which class contains multiple methods, using the kernel32.dll and user32.dll
objects to communicate with the Windows environment?
Challenge Yourself!
Step by Step
SysEmailContents _contents)
System.Net.Mail.MailMessage msg;
System.Net.Mail.SmtpClient smtpClient;
EmplTable eT;
new InteropPermission(InteropKind::ClrInterop).assert();
smtpClient = new
System.Net.Mail.SmtpClient('smtp.abc.com');
adrRecp = new
System.Net.Mail.MailAddress(emplTable.Email);
msg.set_Subject(_subject);
msg.set_Body(_contents);
smtpClient.Send(msg);
Challenge Yourself!
Create a new form that shows a graph of the balance of a customer over time.
Create a new form that shows a graph of the balance of a customer over time.
Step by Step
1.
2.
3.
Solutions
Test Your Knowledge
1. How do you make CLR Interop assemblies visible to Microsoft Dynamics
AX 2009 X++ code?
MODEL ANSWER: You add references to the assembly files in the AOT,
under the References node.
3. What code do you use, to provide Code Access Security permission to a CLR
Interop class?
6. Which class contains multiple methods, using the kernel32.dll and user32.dll
objects to communicate with the Windows environment?
CHAPTER 6: LEDGER
Objectives
The objectives are:
Introduction
All Microsoft Dynamics AX 2009 modules interface with the General Ledger
module in some way; it is regarded as one of the most important modules. It is
also the module that has the least modifications made to it due to the
commonality in accounting procedures throughout the world.
There are two methods to consider when posting transactions to the ledger:
Since these procedures are used regularly but rarely modified, this lesson
examines how to use both methods, but does not go into details about how these
methods work.
Scenario
Isaac, the Systems Developer, is developing a process to import opening ledger
balances for a new company that is to be migrated to Microsoft Dynamics
AX2009. He has been asked to evaluate the best method to achieve this and to
write a program that can be used during the data migration.
LedgerVoucher
When using LedgerVoucher, remember that all vouchers in Microsoft Dynamics
AX 2009 must balance - an equal credit and debit side. All transactions on one
voucher must be posted on the same date. The idea is to build up a list of
vouchers to be posted and to build up a list of transactions within each voucher.
Once these lists are completed, the system posts them all to the General Ledger
module at once.
Posting (LedgerVoucher)
Voucher (LedgerVoucherObject)
o Trans (LedgerVoucherTransObject)
o Trans
Voucher
o Trans
o Trans
The LedgerVoucher class holds all the vouchers in temporary storage (a list
array) until the End method is called. The End method creates ledger transaction
records from the temporary postings.
Instantiation of LedgerVoucher
LedgerVoucher can be instantiated as follows:
LedgerVoucher::newLedgerPost(_detailSummary, _sysModule,
_voucherSeriesCode,
[_transactionLogType,
_transactionLogText,
_approveJournal,
_posting]);
Instantiation of LedgerVoucherObject
LedgerVoucherObject can be instantiated as follows:
LedgerVoucherObject::newVoucher(_voucher,
[_transDate,
_sysModule,
_ledgerTransType,
_correction,
_operationsTax,
_documentNum,
_documentDate,
_tmpVoucherMap,
_acknowledgementDate]);
ledgerVoucher.AddVoucher(ledgerVoucherObject);
Instantiation of LedgerVoucherTransObject
There are many other constructor methods for this class used in different
situations. The newCreateTrans() method is most commonly used.
LedgerVoucherTransObject::newCreateTrans(
_ledgerVoucherObject,
_ledgerPostingType,
_ledgerAccount,
_dimension,
_currencyCode,
_amountCur,
_sourceRecId,
[_LedgerQty,
_exchRate
_exchRateSecond,
_ExchRatesTriangulation,
_markBridging,
_projLedger,
_amountMST])
ledgerVoucherTransObject.parmTransTxt(_transTxt);
LedgerVoucher.end();
Final checks are made and the transactions are finalized. Also,
LedgerBalancesTrans and LedgerBalancesDimTrans are updated. These tables
store balances by period and by dimension.
Dimension dimension;
NumberSeq numSeq;
NumberSequenceCode NumberSequenceCode =
'Acco_18';
ledgerAccount accountNumPetty = '110180';
// Petty cash
ledgerAccount accountNumOffsetOffice =
'606300'; // Office Supplies
ledgerAccount accountNumOffsetPostage =
'606500'; // Postage
amountMST amountPetty = 55;
amountMST amountOffice = 50;
amountMST amountPostage = 5;
numSeq =
NumberSeq::newGetNumFromCode(NumberSequenceCode);
ttsbegin;
SysModule::Ledger,
LedgerParameters::numRefLedgerExchAdjVoucher().NumberSequen
ce) ;
ledgerVoucher.AddVoucher(LedgerVoucherObject::newVoucher(nu
mseq.num(),
today(),
Sysmodule::Ledger,
LedgerTransType::None));
ledgerVoucher.findLedgerVoucherObject(),
LedgerPostingType::LedgerJournal,
CompanyInfo::standardCurrency(),
-
amountPetty, // Amount
0, //
TableId
0); //
ReciID
ledgerVoucherTransObject.parmTransTxt("Petty cash
disbursement");
ledgerVoucher.addTrans(ledgerVoucherTransObject);
ledgerVoucher.findLedgerVoucherObject(),
LedgerPostingType::LedgerJournal,
accountNumOffsetOffice,
dimension,
CompanyInfo::standardCurrency(),
amountOffice, // Amount
0,
// TableId
0);
// ReciID
ledgerVoucherTransObject.parmTransTxt("Red stapler");
ledgerVoucher.addTrans(ledgerVoucherTransObject);
ledgerVoucher.findLedgerVoucherObject(),
LedgerPostingType::LedgerJournal,
accountNumOffsetPostage,
dimension,
CompanyInfo::standardCurrency(),
amountPostage,
0,
// TableId
0);
// ReciID
ledgerVoucherTransObject.parmTransTxt("Stamps");
ledgerVoucher.addTrans(ledgerVoucherTransObject);
ttsCommit;
}
LedgerJournal
Journals are the easiest way of posting in Microsoft Dynamics AX2009. Using
journals requires less work when grouping the transactions and vouchers
appropriately. When using a Journal lines table, fill it in with data and call upon
Microsoft Dynamics AX2009's method for posting the journal. All error
checking and posting are performed simultaneously without additional
programming. Journals also present the possibility of not posting immediately
but allowing the user to verify what is to be posted. This is often useful during
data migration.
Within these groups, there are many types that use the same tables, but differ
slightly in what information they require and the end result.
LedgerJournalName contains default data for a specific journal type. There can
be more than one LedgerJournalName per type.
LedgerJournalTable is the header record for the journal. There is one record per
journal.
ledgerJournalTable.JournalName =
SalesParameters::find().PayJournal;
Creating Lines
All transactions must be posted in the journal lines table. Fill the record with data
and then insert it. Two fields of interest when creating journal lines are:
LedgerJournalTrans.JournalNum which is mandatory, because transactions are
assigned to one of the journals, and LedgerJournalTrans.Voucher which is the
transaction voucher number.
Fill in other mandatory fields in the journal lines table as necessary, for example,
LedgerJournalTrans.CurrencyCode.
Each journal type requires different fields and field combinations to be correctly
entered. Any errors that show up while posting are displayed by validating a
journal through the journal lines form.
Posting Journals
After transactions are created in the journal, check and post the journal; use the
LedgerJournalCheckPost class. The easiest way is to create an instance of this
class using newLedgerJournalTable static method and then run the class.
ttsbegin;
numberseq =
NumberSeq::newGetVoucherFromCode(ledgerJournalName.VoucherS
eries);
ledgerJournalTrans.Voucher = numberseq.voucher();
ledgerJournalTrans.AccountNum = BankAccountID;
ledgerJournalTrans.AccountType =
LedgerJournalACType::Bank;
ledgerJournalTrans.AmountCurCredit = amountCur;
ledgerJournalTrans.TransDate = today();
ledgerJournalTrans.Txt = 'Room Stay';
ledgerJournalTrans.OffsetAccount = offsetAccount;
ledgerJournalTrans.OffsetAccountType =
LedgerJournalACType::Ledger;
ledgerJournalTrans.insert();
info(strfmt('Journal Id:
%1',ledgerJournalTable.JournalNum));
ttscommit;
}
Summary
This lesson introduces and explains two methods for posting transactions to the
ledger table. The two methods are:
2. The transaction log type and transaction log text are used in the ________
trail.
Challenge Yourself!
Step by Step
Payments are entered into the register by clicking Payment on the POS form
which opens the POSPayment form. In a live application this would also open the
cash drawer. In the next lesson you post the payments to the holding account and
the AR sub-ledger.
At any cash register, an End of Day routine counts the actual cash taken. This is
posted to the ledger, and any difference between the counted amount and the
recorded amount is posted to a specified ledger account. Write this routine.
Challenge Yourself!
Step by Step
1.
2.
3.
Solutions
Test Your Knowledge
1. Which class holds a list of transactions that are going to be posted for a
particular voucher?
2. The transaction log type and transaction log text are used in the ________
trail.
MODEL ANSWER: The transaction log type and transaction log text are
used in the audit trail.
CHAPTER 7: TRADE
Objectives
The objectives are:
Introduction
The most frequently modified modules in Microsoft Dynamics AX 2009 are the
Accounts Receivable (AR) and Accounts Payable (AP) modules. For instance,
the element that every company modifies is the printed sales invoice. This is
sometimes a rearrangement of the data available to the invoice, but is more often
a complete redesign that includes additional, company-specific data.
The trade modules revolve around buying and selling inventory, and issuing and
receiving payments. The AR and AP modules are similar in their structure
regarding both data dictionary and update procedures, and make good use of
table maps to reduce the amount of coding needed.
Demonstration - SalesLineType
ok = super();
ok = ok &&
this.validateWrite_server(_skipCreditLimitCheck);
return ok;
ok = super(_skipCreditLimitCheck);
if (salesLine.SalesQty > 0
&& salesLine.SkipUpdate == InterCompanySkipUpdate::No)
{
// Quantity of returned items orders must be
negative.
ok = checkFailed("@SYS53512");
}
After calling super() to provide validation for other line types, the method has
one additional check to ensure that the quantity of the return line is negative.
These inventType sub-class methods are often called from the system methods on
salesLine and salesTable. For example, SalesLine.Update(),calls
SalesLineType.Update(). This can be used when, for instance, creating sales
orders within code, meaning that instantiating SalesTableType or SalesLineType
is not necessary.
The examples below show how a sales order and sales order line are created
within code. The three table methods that are used: SalesTable.InitValue(),
SalesTable.insert(), and SalesLine.CreateLine(), show that they all use the
inventType sub-classes. Note that SalesLine.CreateLine() calls SalesLine.Insert()
which calls SalesLineType.Insert().
NumberSeq =
NumberSeq::newGetNumFromCode(SalesParameters::numRefSalesId
().numberSequence);
salesTable.SalesId = NumberSeq.num();
salesTable.initValue();
salesTable.CustAccount = _custAccount;
salesTable.initFromCustTable();
salesTable.insert();
}
salesLine.clear();
salesLine.SalesId = _salesId;
salesLine.ItemId = _itemId;
salesLine.createLine(NoYes::Yes, // Validate
NoYes::Yes, // initFromSalesTable
NoYes::Yes, // initFromInventTable
NoYes::Yes, // calcInventQty
NoYes::Yes, // searchMarkup
NoYes::Yes); // searchPrice
}
Although there are significant differences between what is posted, the procedures
to perform the update are similar, and therefore use a sub-class structure, based
on a class called formLetter (which extends RunBaseBatch).
Orders can be fully or partially updated, and can be updated one order at a time
or in a batch of orders. To handle this parm tables are used. For instance, Sales
order updates, use SalesParmUpdate, SalesParmTable, and SalesParmLine, with
one record in SalesParmUpdate per update, one record in SalesParmTable per
sales order, and one record in SalesParmLine per line to be updated.
When a user updates an order, the system populates these parm tables, and then
displays them for final adjustments before the posting takes place. They are
populated when the SalesFormLetter.dialog method is called - the
SalesFormLetter.ChooseLines() method uses the SalesUpdate query to select
appropriate salesLines records and populates SalesParmLine accordingly.
Once the parm tables are populated, they are displayed using the SalesEditLines
form. When the user makes alterations and clicks OK, the update occurs.
Demonstration - SalesFormLetter
query = this.queryBuild();
setprefix("@SYS25781");
while (query.next())
{
infoLogCounter = infolog.num();
infolog.updateViewSet(this);
if (printout == Printout::Current ||
this.proforma())
{
journalList = this.newJournalList();
}
salesParmTable =
query.get(tablenum(SalesParmTable));
if (salesParmTable.SalesId != salesTable.SalesId)
{
salesTable = salesParmTable.salesTable();
}
if (this.checkIfSalesOrderExist(salesTable))
{
try
{
if (batchHeader)
{
formLetterMultiThread =
FormLetterMultiThread::newFormLetter(this);
batchHeader.addRuntimeTask(formLetterMultiThread,this.parmC
urrentBatch().RecId);
batchHeader.addDependency(salesFormLetterEndMultiThread,for
mLetterMultiThread,BatchDependencyStatus::FinishedOrError);
}
else
{
this.createJournal();
}
}
SalesParmTable is retrieved from the query, and a check is made on the sales
order. SalesFormLetter.createJournal() is then called.
salesTotals = SalesTotals::construct(
salesParmTable,
salesParmUpdate.SpecQty,
salesParmUpdate.SumBy,
salesParmUpdate.ParmId,
salesParmUpdate.SumSalesId,
this.documentStatus());
this.initFromSalesTotals(salesTotals);
if (salesParmUpdate.Proforma)
this.insertProforma();
else
this.insertJournal();
numberSeq = this.allocateNumAndVoucher();
[number, voucher] = this.getNumAndVoucher();
if (this.updateNow())
{
this.postUpdate();
Posting number (e.g. invoice or delivery note) and voucher are created then
SalesFormLetter.UpdateNow() is called.
this.initJournal();
transactionTxt = this.initTransactionTxt();
this.initLedgerVoucher(transactionTxt);
this.initRecordListInventReportDimHistory();
salesLine.clear();
salesParmLine.clear();
recordListSalesParmLine.first(salesParmLine);
while (salesParmLine)
{
if (! this.checkDiscardLine())
{
inventMovement =
InventMovement::construct(salesLine);
this.updateInventory(inventMovement);
inventUpd_Physical =
InventUpd_Physical::newSalesPackingSlip(
_inventMovement,
salesParmLine,
number,
salesParmUpdate.ReduceOnHand);
inventUpd_Physical.updateNow(ledgerVoucher);
updateNow = -
inventUpd_Physical.updPhysicalUnit();
updateNowInvent = -inventUpd_Physical.updPhysical();
9. Return to SalesFormLetter_PackingSlip.UpdateNow():
if (salesParmLine.DeliverNow != updateNow ||
salesParmLine.InventNow != updateNowInvent)
{
salesParmLine =
SalesParmLine::findRecId(salesParmLine.RecId, true);
info(strfmt("@SYS26397",updateNow));
qtyReduced = true;
salesParmLine.RemainAfter +=
salesParmLine.DeliverNow - updateNow;
salesParmLine.setRemainAfterInvent();
salesParmLine.DeliverNow = updateNow;
salesParmLine.InventNow =
updateNowInvent;
salesParmLine.setLineAmount(salesLine);
salesParmLine.update();
}
10. If the quantity is reduced, the user is informed and the parm table
record is updated to ensure that later updates are correct.
this.writeJournalLine();
this.writeProjTrans(inventMovement);
salesLineType =
SalesLineType::construct(salesLine);
salesLineType.updateSalesLine(inventMovement.transIdSum());
...
salesLine.doUpdate();
12. The status of the SalesLine record is updated (which may also update
the SalesTable record):
if (qtyReduced)
{
salesTotals = SalesTotals::construct(salesTable,
salesParmUpdate.SpecQty, salesParmUpdate.SumBy,
salesParmUpdate.ParmId, salesParmUpdate.SumSalesId,
this.documentStatus());
salesTotals.calc();
this.tax(salesTotals.tax());
}
13. If the quantity is reduced, the totals are recalculated and the tax is
adjusted:
this.writeJournal();
this.writeRecordListInventReportDimHistory();
this.postMarkupTable();
this.updateSalesShippingStat(salesParmTable);
if (journalList.len() > 0)
{
if (printFormletter)
{
this.sendAsXML();
custPckSlpJour.printJournal(this, journalList);
}
salesFormLetter =
SalesFormLetter::construct(DocumentStatus::Invoice);
salesFormLetter.update(
_salesTable, //SalesTable record to be posted
systemDateGet(), //Transaction date
SalesUpdate::All, //Which qty should be used
AccountOrder::None,
NoYes::No, //Is document a proforma
NoYes::No); //Should document be printed
Posting Transactions
Customer and vendor transactions can be posted using a ledger journal. This can
be manually created or created within code, as with a normal daily journal.
ttsbegin;
numberSeq = NumberSeq::newGetVoucherFromCode(
NumberSequenceTable::find(_numberSequenceCode).NumberSequen
ce);
ledgerVoucher = LedgerVoucher::newLedgerPost(
DetailSummary::Detail,
SysModule::Cust,
NumberSequenceTable::find(_numberSequenceCode).NumberSequen
ce);
ledgerVoucherObject = LedgerVoucherObject::newVoucher(
numberSeq.voucher(),
SystemDateGet(),
SysModule::Cust,
LedgerTransType::Payment);
ledgerVoucher.AddVoucher(ledgerVoucherObject);
custVoucher = CustVendVoucher::construct(
SysModule::Cust,
ledgerVoucher,
_custAccount,
_amountMST,
CustTable::find(_custAccount).Currency,
LedgerTransTxt::CustPaymentCust,
CustTable::find(_custAccount).Dimension,
'',
LedgerPostingType::CustPayment);
custVoucher.parmTransTxt('Customer Transaction');
ledgerVoucherTransObject =
LedgerVoucherTransObject::newCreateTrans(
ledgerVoucherObject,
LedgerPostingType::CustPayment,
_ledgerAccount,
CustTable::find(_custAccount).Dimension,
CustTable::find(_custAccount).Currency,
-_amountMST,
0);
ledgerVoucherTransObject.parmTransTxt("Sales
Transaction");
ledgerVoucher.addTrans(ledgerVoucherTransObject);
custVoucher.post(CustTrans);
ledgerVoucher.end();
ttscommit;
}
Settlement
When customer or vendor invoices are paid the payment transactions and the
invoice transactions must be settled against each other to close the transaction.
The CustSettlement and VendSettlement tables hold each settlement that has
taken place and stores the recIds of the transactions that are settled against each
other.
Settlement takes place in the CustVendSettle class, with most of the work done in
CustVendSettle.SettleNow(). This method handles settlement using a FIFO
principle (oldest invoice gets settled first), and using transaction markings.
To mark transactions for settlement, use the SpecTrans table. This stores the
RecId's of the transactions to be settled, and the settle amount. The class
Specification can be used to handle creation of SpecTrans records and also to
handle marked transactions.
CustTrans _custTrans2)
CustTable custTable,
CustTable = CustTable::find(CustTrans1.AccountNum);
custTrans1.markForSettlement(custTrans2);
CustTrans::settleTransact(custTable);
Trade Agreement
Trade agreements are flexible in the way they can be set up. They are applicable
to:
Single items
Single customers
Groups of items
Groups of customers
All items
All customers
This means that a single item/customer combination can have multiple prices. To
set up complicated pricing structures, it is important to understand the logic
behind the calculation. Companies often have unique and imaginative pricing
structures, so the calculation is modified regularly.
A line discount
A multi-line discount
A total discount
Note that prices cannot be set up for groups of items or all items.
Demonstration - PriceDisc
This demonstration shows the calculation for the item price:
2. The item relation and account relation are set according to the item
code and account code. Since the system is calculating the price, the
itemCode = 1 or 2 is meaningless. Look in PriceDisc.FindLineDisc()
and note the item relation is set in the same way as the account
relation.
if (PriceDiscTable::activation(relation, accountCode,
itemCode, priceParameters))
{
if (PriceDisc::validateRelation(accountCode,
accountRelation) &&
PriceDisc::validateRelation(itemCode,
itemRelation ))
{
priceUnit = priceDiscTable.priceUnit();
price = priceDiscTable.price();
markup = priceDiscTable.markup();
deliveryDays = priceDiscTable.DeliveryTime;
calendarDays = priceDiscTable.CalendarDays;
actualPriceTable = priceDiscTable.data();
priceExist = true;
6. Data on the price is stored for later retrieval when the final price is
located.
if (! priceDiscTable.SearchAgain)
{
idx = 9;
break;
}
If the price set to not search for more prices, the loop ends.
Summary
In this lesson, the some of the most commonly used development concepts in the
Accounts Receivable and Accounts Payable modules have been introduced.
Concepts such as document updates, posting transactions, using settlements and
searching trade agreements should now be familiar, and developing
modifications with them is now possible.
3. What is the name of the class used when posting vendor transactions that
handle both posting to the AP sub-ledger and the AP ledger account?
Challenge Yourself!
Create a process that can post a payment transaction for the payment entered into
the POS screen.
1. Create a new class and retrieve the sales order id from the caller.
2. Create a new journal header.
3. Loop over all POSRegisterTrans records linked to the sales order,
that have not previously been posted to the ledger, create a journal
line for each.
4. The journal lines should post customer account, and offset to the
register holding account.
5. Flag the payment lines as having been posted to the register.
Step by Step
Challenge Yourself!
When the OK button on the payment lines screen is clicked, the payment posting
routine should be called. This should be followed by the standard invoice update.
1. Call the payment posting class and the invoice posting class when
the Post button is clicked.
2. Create a new class, POSPostInvoice that posts an invoice.
3. Instantiate SalesFormLetter, DocumentStatus::Invoice.
4. Call SalesFormLetter.Update().
L
Step by Step
Challenge Yourself!
When posting an invoice and payment from the POS, they should be settled
against each other. Ensure that this happens. This should happen whether
automatic settlement is enabled or not, and should settle against each other no
matter what other transactions are open for the customer.
1. Find the invoice and payment transactions and mark them against
each other for settlement using CustTrans.markForSettlement().
2. Post the settlement using CustTrans::SettleTransact().
Step by Step
1.
2.
3.
Solutions
Test Your Knowledge
1. True or False: SalesLineType is a sub-class of SalesTableType.
3. What is the name of the class used when posting vendor transactions that
handle both posting to the AP sub-ledger and the AP ledger account?
CHAPTER 8: INVENTORY
Objectives
The objectives are:
Introduction
Similar to Ledger, Inventory is closely linked to many modules within Microsoft
Dynamics AX and is a vital module to understand. Unlike the ledger module,
modifications to the inventory system can be significant with regard to the work
required. This lesson discusses both creating and posting inventory transactions,
and using the data correctly after it has been posted.
Scenario
Isaac, the Systems Developer, has been asked to add a field on the sales order
that shows the current on-hand inventory of an item, based on the visible
inventory dimensions. To be able to do this he must understand how the
inventory system works in Microsoft Dynamics AX.
Inventory Journals
Inventory Journals are similar to Ledger Journals and can be created in a similar
way. Application elements are similar in design but are physically different
elements.
Tables
In the inentory module, the tables used are InventJournalName,
InventJournalTable, and InventJournalTrans.
InventJournalName contains the default data for a specific journal type. There
can be more than one InventJournalName per type.
InventJournalTable is the header record for the journal. There is one record per
journal.
1. Create a journal table record or use an existing one. Set the journal
type according to the posting you require, such as profit\loss and
transfer.
2. Create lines for each transaction to be posted.
3. Post the journal. This checks the journal before posting. This step is
omitted if the journal is checked by the user before posting.
xInventJournalName = InventJournalName::find(
InventParameters::find().MovementJournalNameId);
InventJournalTable.InitFromInventJournalName(InventJournalN
ame);
InventJournalTable.insert();
Creating Lines
All transactions must be posted in the journal lines table. You must fill the record
with data and then insert it. There are two important fields to be filled every time
you insert a new record:
Other fields in the transaction table are mandatory according to the journal types
and should be filled as necessary.
While developing, test the code by letting it create a journal, and then manually
opening the journal in the journal forms. Run validate and post highlights errors.
Posting Journals
After transactions are created in the journal, check and post the journal using the
InventJournalCheckPost class. Create an instance of this class using
newJournalCheckPost static method and then run the class. Parameters for this
method are:
InventJournalCheckPost CheckPost;
CheckPost =
InventJournalCheckPost::newJournalCheckPost(
journalCheckPostType::Post, _InventJournalTable)
CheckPost.run();
Inventory Dimensions
Inventory dimensions are a powerful, versatile, and efficient way to track
variations in inventory movements. They define whether an item is tracked by:
Tables
Available tables are defined as follows:
InventDimSetup sets up each dimension group. There is one record per inventory
dimension per dimension group.
Classes
The available classes include:
InventDimGlobal is a global class that holds a cache of dimension field ids and
names. This is used mainly by InventDimSearch.
A static method on InventDimSearch is called. This method will set the flags for
each inventory dimension that financial inventory option checked.
_inventDimParm.data(classfactory.inventDimGlobal().dimSearc
hBase().financialInvent(_dimGroupId));
else
_inventDimParm.clear();
}
3. View InventDimSearchBase.financialInvent()
financialInvent is a map that contains the setup data for each group for the
financial inventory. The key is the group id, and the value is an InventDimParm
record holding the values for each inventory dimension. In this method, the map
is checked to see if the values have already been created for this group. If not,
then it will call the method to initialize these values.
4. View InventDimSeachBase.initInventDimCache()
if (_dimSearch.first(_dimGroupId))
do
{
5. Many setup options are examined. The following code shows where
the flags for the dimension being active and to post financial
inventory are examined, and if both are true, then the
InventDimParm flag for this dimension is set to Yes. Additionally
the field id which is used to refer to which dimension this is for is
stored in a container.
if (_dimSearch.dimActive() &&
_dimSearch.dimFinancialInvent())
{
inventDimParmFinancialInvent.(y) = NoYes::Yes;
conFinancialInventFields += _dimSearch.dimFieldId();
}
financialInvent.insert(_dimGroupId,
inventDimParmFinancialInvent);
Use an item with a dimension group containing dimensions set to Use in price
search, For sales prices. Also create sales prices for the different dimensions, as
shown Select main menu>inventory management>items>general tab
The Use in sales price search flag means that when looking for a price for the
item, the system:
When entered into sales lines, the price is calculated using the size set on the line:
switch (moduleType)
{
case ModuleInventPurchSales::Purch:
inventDimAllActivated.copyActivatePurchPriceAll(inventTable
.DimGroupId, inventDim);
break;
case ModuleInventPurchSales::Sales:
inventDimAllActivated.copyActivateSalesPriceAll(inventTable
.DimGroupId, inventDim);
break;
default:
inventDimAllActivated.copyItemDim(inventTable.DimGroupId,
inventDim);
}
container dimFields =
InventDimSearch::activateSalesPriceAllFields(_dimGroupId);
;
len = conlen(dimFields);
for (h=1;h<=len;h++)
{
x = conpeek(dimFields, h);
this.(x) = _fromInventDim.(x);
}
The static method to initialize or retrieve the cached InventDimParm record for
this group, that has the flags set for all dimensions that has the Sales Price option
set, is called. The return value is used to determine which fields to use from the
InventDim record that is referenced on the sales line.
inventDimItemDimActivated.copyItemDim(inventTable.DimGroupI
d, inventDimAllActivated);
Two InventDim records remain; one holding all values of dimensions only
applicable to the sales price search, and another holding values of item
dimensions applicable to the sales price search.
findAll =
!InventDim::isInventDimEqual(inventDimAllActivated,
inventDimItemDimActivated);
findItemDim =
!InventDim::isInventDimEqual(inventDimItemDimActivated,inve
ntDimNoneActivated);
inventDimAllActivated = findAll ?
InventDim::findDim(inventDimAllActivated) :
inventDimAllActivated;
inventDimItemDimActivated = findItemDim ?
InventDim::findDim(inventDimItemDimActivated) :
inventDimItemDimActivated;
this.findPriceAgreement(_priceGroupId,
InventDim::inventDimIdBlank()) ||
(_useItemPrice && this.findItemPrice()));
Search for a price with the inventory dimension id for the InventDimAllActivated
record. All inventory dimensions match if the found method returns true.
Then, search for a price with the item dimension. If a price is found, the method
returns true.
Then, search for a price with blank item dimensions. If a price is found, the
method returns true.
Finally, search for a price ignoring dimensions all together. If a price is found,
the method returns true.
InventSum
InventSum can be regarded as one of the most important tables in Microsoft
Dynamics AX. It is used to store current values of on-hand inventory. Any
function that creates an inventory transaction, including creating, updating or
deleting a sales line, purchase order line, inventory journal line or production
order, will cause an update to InventSum.
Due to the high number of processes that can update InventSum, optimistic
locking of InventSum is not appropriate. It is likely that a process that has
retrieved an InventSum record, is then going to update it. View the properties of
the table and note that the Occ property is set to No.
Tables
Tables that are associated with InventSum operations are as follows:
InventSum contains current inventory levels per item per inventory dimension
combination.
InventSumDelta contains information about on-hand changes that are not yet
committed to the database.
Classes
There are many classes used to retrieve data from InventSum. Some examples
are as follows:
Invent Macros
There are some useful macros available when using InventSum:
if (InventSum::mustInventTransBeUpdated(this))
{
inventSum.initFromInventTrans(this);
inventSum.updateInventTrans(this,NoYes::Yes);
}
if (plus)
inventSum.addInventTransOnSum(inventTrans);
else
inventSum.subInventTransOnSum(inventTrans);
switch(inventTrans.StatusIssue)
{
case StatusIssue::Sold:
this.PostedQty += inventTrans.Qty;
break;
case StatusIssue::Deducted:
this.Deducted -= inventTrans.Qty;
break;
case StatusIssue::Picked:
this.Picked -= inventTrans.Qty;
break;
case StatusIssue::ReservPhysical:
this.ReservPhysical-= inventTrans.Qty;
break;
case StatusIssue::ReservOrdered:
this.ReservOrdered-= inventTrans.Qty;
break;
case StatusIssue::OnOrder:
this.OnOrder -= inventTrans.Qty;
break;
case StatusIssue::QuotationIssue:
this.QuotationIssue-= inventTrans.Qty;
break;
default:
}
inventSumDelta.initFromInventTrans(inventTrans);
inventSumDelta.initFromInventSum(inventSum);
appl.inventUpdateOnhandGlobal().inventUpdateOnhand().addInv
entSumDelta(inventSumDelta,inventTrans);
The inventUpdateOnHand class is a class held in the global cache. This is due to
the frequency with which it is contructed. The
inventUpdateOnHand.inventUpdateOnHand() method sets the current ttsId on
the InventSumDelta record and then inserts the record in the database.
this.inventUpdateOnhandGlobal().ttsNotifyPreCommit();
This method is called during a ttsCommit by the system classes that commit a
transaction to the database.
inventUpdateOnhandMap.lookup(newExt).ttsNotifyPreCommit()
This finds the global instance of the class InventUpdateOnHand and calls the
ttsNotifyPreCommit() method.
where inventSum.ItemId ==
inventSumDelta.ItemId &&
inventSum.InventDimId ==
inventSumDelta.InventDimId
All records in InventSumDelta that were created in the current transaction and do
not have a related InventSum record, are retrieved. An InventSum record created
based on the InventSumDelta record and is added to a recordInsertList. When all
records have been added to the list, the records are committed to the database.
if (inventSumDeltaCnt == 1)
this.updateInventSumSimple();
else
this.updateInventSumAdvanced();
When building the SQL statement string, the system uses the field groups called
DeltaFields on InventSum and InventSumDelta to determine which fields need to
be updated. If you need to add more fields to InventSum, and therefore
InventSumDelta, you do not need to alter the SQL statement. Simply add the
fields to the field groups and the code will handle the update for you.
inventOnhand.parmInventDim(_inventDim);
inventOnhand.parmInventDimParm(_inventDimParm);
inventOnhand.parmItemId(_itemId);
return inventOnhand;
this.setInventSum();
return inventSum.ReservPhysical;
if (sumRead)
return;
this.findSumJoin();
sumRead = true;
if (inventDimId)
{
inventSum = InventSum::find(itemId,inventDimId);
}
else if (inventDimParm.ItemIdFlag)
{
inventSum = InventSum::findSum(itemId,
inventDimCriteria, inventDimParm, InventSumFields::All);
}
if (_inventDimParm.InventSerialIdFlag &&
_inventDimCriteria.InventSerialId)
{
switch (_sumFields)
{
The method selects InventSum, summing the relevant fields (financial, physical,
or all). The join to InventDim ensures all relevant records in InventSum are
summed.
Again, different select statements are used depending on what data is required. If
only the Pallet Id is required, the select statement is different from when any
other dimensions are required.
After the InventSum agregrated record has been calculated, the InventSumDelta
records need to be included. The addInventSumDelta() method is called.
this.addInventSumDelta();
if (!itemId ||
InventUpdateOnhandGlobal::mustAddInventSumDeltaOnhand(itemI
d))
inventSum.addInventSumDelta(this.findSumJoinDelta());
if (inventDimId)
{
inventSumDelta =
InventSumDelta::findSumDeltaDimId(itemId,inventDimId,Invent
SumFields::All);
}
else if (inventDimParm.ItemIdFlag)
{
inventSumDelta =
InventSumDelta::findSumDelta(itemId,inventDimCriteria,inven
tDimParm,InventSumFields::All);
}
Challenge Yourself!
Create a display method on the SalesLine data source to return the physcally
avaiable inventory based on the current inventory dimensions that are displayed.
Note the following
Step by Step
_salesLine.inventDim(),
element.inventDimSetupObject().parmDimParmVisibleGrid());
return inventOnHand.availPhysical();
}
InventMovement
Use the InventMovement class and subclasses to check and prepare data to update
inventory transactions. For instance, it is used to find if and where to post to
Ledger. It is similar to a data carrier used by other classes when updating
inventory data.
The following figure shows the hierarchy tree for the InventMovementClass
Any lines that can have inventory transactions attached, for example, SalesLines,
PurchLines, InventJournalTrans, can be used in the construct method of the class,
and the appropriate sub-class is instantiated accordingly. The sub-classes control
how the updates differ.
InventMovement movement =
InventMovement::construct(buffer);
InventMovement movement =
InventMovement::constructNoThrow(buffer,subType,childBuffer
);
if (!movement)
throw error("@SYS20765");
return movement;
When instantiating the class, you can throw an error if the class cannot be
instantiated. If you do no want to throw an error, call
InventMovement::ConstructNoThrow directly.
Note this method tests the table id of the record that it was called with and
instantiates the relevant sub-class of InventMovement. Some tables, such as
SalesPickingListJournalLine have their own methods written for instantiating
InventMovement. Others, such as InventJournalTrans, can instantiate different
sub-classes depending on values of certain fields, like
InventJournalTrans.JournalType. In this example the table is SalesLines, so
InventMov_Sales is instantiated.
qty = UnitConvert::qty(movement.transQtyUnit(),
movement.transUnitId(),
movement.inventTable().inventUnitId(),
movement.itemId()
);
qty =
decround(qty,InventTable::inventDecimals(movement.itemId())
);
movement.setTransQty(qty);
qty =
decround(movement.transQtyUnit(),Unit::decimals(movement.tr
ansUnitId()));
movement.setTransQtyUnit(qty);
Using this method enables the same methods to be used throughout the
application when creating and updating inventory transactions.
InventUpdate
InventUpdate is the class that creates and updates inventory transactions and is
always used in conjunction with InventMovement. The following figure shows
the hierarchy tree for InventMovement:
The class and subclasses are related to the base enums for inventory receipts and
issues.
The class and subclasses are instantiated directly, rather than using a constructor
on the super class. Some of the sub-classes should be instantiated using the new()
method, others have static methods that can be used in certain circumstances.
Once the class has been instantiated, calling updateNow() posts the required
update.
The sub classes are used for different types of required updates.
InventUpd_Estimated creates the initial transaction when, for example, a
salesLine is created. InventUpd_Physical posts the physical movement of the
inventory, in other words, the inventory is received or shipped.
InventUpd_Reservation reserves an outflow against an inflow.
inventUpd_Financial =
InventUpd_Financial::newSalesInvoice(_inventMovement,
ledgerVoucher,
number,
localSalesParmLine,
salesParmSubLine,
salesParmUpdate.ReduceOnHand);
inventUpd_Financial.updateNow();
localUpdateNow += -
inventUpd_Financial.updFinancialUnit();
localUpdateNowInvent += -
inventUpd_Financial.updFinancial();
localInvoiceUpdatedOnly += -
inventUpd_Financial.updPhysicalUnit();
if (physical || remainPhysical !=
movement.remainPhysical() ||
physicalUnit || remainPhysicalUnit !=
movement.remainPhysicalUnit())
{
if (financial > 0.0 && movement.mustBeReceived())
{
throw error("@SYS117599");
}
inventUpd_Physical =
InventUpd_Physical::newInventUpdFinancial(movement,
this,
physical,
physicalUnit,
Currency::mstAmount(this.parmCostAmountCur(),
this.parmCurrencyCode(),
ledgerVoucher.lastTransDate(),
this.parmExchRatesTriangulation(),
this.parmExchRate(),
this.parmExchRateSecondary()));
Calculate the cost value of the transaction and post to the general ledger:
costAmountMST =
movement.updateLedgerFinancial(ledgerVoucher, this);
if (financial < 0)
{
this.updateFinancialIssue(costAmountMST);
}
else
{
if (financial > 0)
{
this.updateFinancialReceipt(costAmountMST);
}
}
this.updateInventTransPosting(ledgerVoucher.lastTransDate()
, ledgerVoucher.lastVoucher());
this.updateInventTransPosting(ledgerVoucher.lastTransDate()
, ledgerVoucher.lastVoucher());
InventUpd_Reservation
To reserve incoming inventory against an outflow, use the InventUpd_Rservation
class:
InventQty _reserveQty)
InventUpd_Reservation reservation;
InventMovement movement;
movement = InventMovement::construct(_salesLine);
reservation =
InventUpd_Reservation::newMovement(movement, reserveQty,
true)
reservation.updateNow();
Summary
Similar to Ledger, Inventory is closely linked to many modules within Microsoft
Dynamics AX. Modifications to the inventory system can be significant with
regard to work required.
This lesson demonstrate both creating and posting inventory transactions, and
using the data correctly after it has been posted. In addition, this lesson
introduces some of the classes used to manipulate inventory data.
Challenge Yourself!
1. Write a routine that unreserves all open orders, and then reserve
according to the classification.
2. Reserving a negative quantity removes the reservation.
3. Use InventTransIdSum to find the existing reserved quantity.
4. Attempting to reserve the total quantity on a line reserves any that
are available to be reserved.
Step by Step
Challenge Yourself!
Use the SalesTable form as inspiration. Look for all references to
InventDimSetupObject.
Step by Step
Inspiration for the following may be drawn from the SalesTable form.
Challenge Yourself!
Step by Step
1.
2.
3.
Solutions
Test Your Knowledge
1. Is the InventDimParm table a temporary table?
CHAPTER 9: PRODUCTION
Objectives
The objectives are:
Introduction
Production orders in Microsoft Dynamics AX 2009 can have many components
and resources. One production order can be linked to many others through sub-
BOMs and reference production orders.
The Production module is designed to set up items that are produced, indicate
how they are produced, what components they are made up of, how long it takes,
and so on, and then to let the system calculate:
This is done by updating the production order status at each stage of the process.
The status update process is an important part of the production module and is
often modified to include parameters unique to a company's production line. The
key to modifying the update process is knowing the function of the many classes
involved.
ProdMulti
Production orders can be updated either one at a time, or multiple orders at once.
The RunBaseMultParm structure enables the use of different parm tables and
different updates to run, all using the same structure.
prodMultiCostEstimation =
ProdMultiCostEstimation::construct(args);
RunBaseMultiParm::initFromForm(prodMultiCostEstimation,args
);
FormDataSource fdS;
Common common;
ParmBuffer parmBuffer =
runBaseMultiParm.defaultParmBuffer();
ParmUpdate parmUpdate =
runBaseMultiParm.defaultParmUpdate();
;
RunBaseMultiParm::initParm(runBaseMultiParm); //sets
the parm id
runBaseMultiParm.insert(common,parmBuffer);
}
}
All selected records in the form data source are added to the appropriate parm
table. If the update has been called from the main menu, there is no data source
and no records are found.
if (! prodMultiCostEstimation.prompt())
return;
prodMultiCostEstimation.run();
The prompt displays the form where the user can change the settings to update
the order. There is also a function to select more records, which must be used
when the update is called from the main menu.
while (curParmCostEstimation)
{
try
{
prodTable =
this.initProdTable(curParmCostEstimation.ProdId);
prodTable.status().runCostEstimation(curParmCostEstimation,
false,prodPurch,this);
}
All records in the parm table are looped over. The ProdStatusType sub-class is
instantiated using the status of the prodTable record, then the update is called.
ProdStatusType
Production order updates are controlled using the ProdStatusType class and sub-
classes.
Each stage of the update process must take place. If the current status of an order
is Estimated, and you try to update it to Report as Finished, then it must be
Scheduled, Released, and Started before it can be reported as Finished.
Before each update is run it checks to see if the previous status has already been
processed. If not, then it runs that update. It also handles what updates can take
place to a production order. For instance, a production order with status Finished,
cannot be Estimated.
1. View ProdMultiReportFinish.Run():
try
{
this.initProdTable(prodParmReportFinished.ProdId).status().
runReportFinished(prodParmReportFinished,false,this,sysSign
);
}
2. View ProdStatusType_Released.runReportFinished():
if (!ask)
{
ProdUpdReportFinished::runPreviousJob(prodParmReportFinishe
d,_multi);
prodTable.type().runReportFinished(prodParmReportFinished,_
multi,_sysSign);
}
return true;
ask is set to true only when the method is called to ask whether the update is
allowed, not to run the update.
3. View ProdUpdReportFinished::RunPreviousJob:
ProdParmStartUp prodParmStartUp =
ProdUpdStartUp::initParmBufferFromRepFin(prodParmReportFini
shed);
;
prodParmStartUp.insert();
ProdTable::find(prodParmStartUp.ProdId).status().runStartUp
(prodParmStartUp,false,null,_multi);
if (!ask)
prodTable.type().runStartUp(prodParmStartUp,_multi);
return true;
Since the status previous to Started is released, and this order is already released,
there is no need to run the previous job.
ProdUpd
The update to the production order is taken handled by the ProdUpd class.
When a production order is updated, there are two events that can occur.
ProdUpdStartUp prodStartUp =
ProdUpdStartUp::newParmBuffer(prodParmStartUp);
;
prodStartUp.run();
try
{
ttsbegin;
this.setParameters();
if (! this.validate())
throw Exception::Error;
viewCacheProdRoute = null;
viewCacheProdRoute =
ProdRoute::viewCacheProdId(prodTable.ProdId,true);
this.updateProduction();
this.updateBOMConsumption();
prodTable.status().startUpUpdateRouteJobs(this);
this.updateRouteConsumption();
The production route is updated and the route journal is created and posted.
this.startupReferences();
this.updateJobJournal(ParmJobStatus::Executed);
TransactionLog::create(TransactionLogType::ProdStartUp,
strfmt("@SYS76498", prodTable.ProdId, "@SYS77138"));
ttscommit;
Any references (production orders created due to this production order) are also
updated, and the parm table record is updated to show it has been posted.
Scheduling
Production orders are scheduled using a sequence specified by the user according
to what is currently available with respect to material and work center capacity.
While scheduling a specific production, the system indicates separate operations
and, if job scheduling, separates the jobs. There are a number of factors that
determine when an operation or job can be scheduled. If limited material and
capacity are used, then all the materials and work centers must be available. The
scheduling direction and scheduling date must also be considered.
To handle all the factors involved in an efficient manner, the data is stored in
temporary tables and arrays. This temporary data is handled by classes that have
the postfix Data; for example, production orders to be scheduled are placed into
the WrkCtrMasterData data storage class. Each production order is split into
route operations and held in WrkCtrRouteData.
When all the temporary data has been created, the time to carry out each job or
operation can be calculated, which can be applied back up through the tree of
jobs, operations, routes, and productions to give starting and ending dates and
times.
setprefix(ProdMultiSchedulingJob::description());
wrkCtrParmSchedule =
WrkCtrParmSchedule::newProdParmScheduling(prodParmSchedulin
g);
wrkCtrMasterData = new WrkCtrMasterData_Prod();
wrkCtrScheduleJob = new
WrkCtrScheduleJobs_Detail(wrkCtrParmSchedule,wrkCtrMasterDa
ta);
super();
setprefix(#PrefixField(ProdParmScheduling,ProdId));
try
{
ttsbegin;
if (! this.validate())
throw Exception::Error;
wrkCtrScheduleJob.run();
try
{
ttsbegin;
masterData.load();
if (!this.runMaster())
{
4. View wrkCtrScheduleJobs.runMaster():
this.masterDirection(parmSchedule.schedDirection());
this.masterIteration(1);
this.setMasterStartDateTime();
The scheduling direction dictates the sequence that the production orders are
retrieved. Note that if references are not scheduled, only one production order is
in the temporary table.
setMasterStartDateTime initializes the scheduled start and end date and time to
the schedule date and time.
while (doFirst)
{
if (this.masterDirection() ==
SchedDirection::Forward)
doNext = masterData.last();
else
doNext = masterData.first();
while (doNext)
{
if (masterData.rec_Iteration() ==
this.masterIteration())
{
setprefix(masterData.prefixNumId());
if (! masterData.check())
throw error("@SYS18447");
routeData = masterData.newRouteData();
If the production order level is the level currently being scheduled, further checks
are made, wrkCtrRouteData is instantiated, and the class array variables are
populated with the data from ProdRoute records attached to the production order.
if (this.masterDirection() ==
SchedDirection::Forward)
{
this.masterSchedDate(masterData.rec_FromDate());
this.masterSchedTime(masterData.rec_FromTime());
this.routeSchedDate(masterData.rec_FromDate());
this.routeSchedTime(masterData.rec_FromTime());
}
else
{
this.masterSchedDate(masterData.rec_ToDate());
this.masterSchedTime(masterData.rec_ToTime());
this.routeSchedDate(masterData.rec_ToDate());
this.routeSchedTime(masterData.rec_ToTime());
}
The scheduling date on the master and route data is set according to the
scheduling direction.
if (!this.runRoute())
return false;
RunRoute is called.
while (doFirst)
{
if (this.routeDirection() ==
SchedDirection::Forward)
doNext = routeData.last();
else
doNext = routeData.first();
if (! this.runJobLink())
return false;
wrkCtrScheduleJob.runJobLink() is called.
jobLinkData.load();
JobLinkData.load() is called.
_calcTime =
_prodJobType.calcWrkCtrHours(masterData.bomCalcData(),
_prodRoute,
_prodRoute,
_prodRoute,
_prodRoute) * 3600;
_jobTime =
_prodJobType.calcJobSchedJobTime(_prodRouteJob,_prodRoute,_
calcTime);
_currentJobIdx =
jobData.insert(_prodRouteJob.WrkCtrId,
_prodRouteJob.PropertyId,
_currentLinkIdx,
this.rec_JobLastIdx(),
_prodRouteJob.JobType,
_prodRouteJob.OprPriority,
_prodRouteJob.RecId,
_prodRouteJob.JobId,
_prodRoute.WrkCtrLoadPct,
1,
_prodJobType.scheduleWorkTime(_prodRoute.routeGroup()),
_prodJobType.scheduleCapacity(_prodRoute.routeGroup()),
_prodRouteJob.Locked
);
8. Return to wrkCtrScheduleJobs.runJobLink():
oprNum = routeData.rec_OprNum();
while (oprNum)
{
if (jobLinkData.first(oprNum))
{
routeData.rec_FromDate(jobLinkData.rec_FromDate());
routeData.rec_FromTime(jobLinkData.rec_FromTime());
endDate = jobLinkData.rec_ToDate();
endTime = jobLinkData.rec_ToTime();
while (jobLinkData.next(oprNum))
{
if (endDate < jobLinkData.rec_ToDate()
||
(endDate == jobLinkData.rec_ToDate()
&& endTime < jobLinkData.rec_ToTime()))
{
endDate =
jobLinkData.rec_ToDate();
endTime =
jobLinkData.rec_ToTime();
}
}
routeData.rec_ToDate(jobLinkData.rec_ToDate());
routeData.rec_ToTime(jobLinkData.rec_ToTime());
routeData.rec_EndDate(endDate);
routeData.rec_EndTime(endTime);
Each operation in the route has the start and end date set according to start and
end date of the jobs attached to it.
if (this.routeSchedOk())
{
routeData.savePosition();
oprNum = routeData.rec_OprNum();
while (oprNum)
{
routeData.update();
Summary
Production orders in Microsoft Dynamics AX 2009 can have many components
and resources. The production module is designed to set up items that are
produced, indicate how they are produced, what components they are made up
of, how long it takes, and so on, and then to let the system perform the
calculations. Greater productivity can be achieved by automating these and other
business processes.
2. Which class controls whether a production order can be updated from one
status to another?
Challenge Yourself!
Add a free text field Special Instructions to the production order, which is
copied to the BOM journal and printed on the production order picking list.
Step by Step
Challenge Yourself!
Add the Special Instructions field to sales lines. Make sure this field is copied to
the production order when the production order is created both directly from the
sales line (sales table form > sales lines > inquiries > production), and through
master planning.
Step by Step
1.
2.
3.
Solutions
Test Your Knowledge
1. Which parm table is used when updating the status from Scheduled to
Released?
2. Which class controls whether a production order can be updated from one
status to another?
Introduction
The Project module is used to estimate and record costs and revenue transactions
against individual projects, which calculates running costs, total costs of the
project, and invoices customers for costs incurred.
This lesson describes how the project module is designed, how transactions are
created and posted, and how invoices are created.
Scenario
The Servicing department has asked Isaac, the System Developer, to make some
modifications to the project module. The project module in Contoso is used to
track time and costs used while installing and servicing home theatre equipment.
Some work is billable, other work is carried out under warranty.
Design
Transactions posted against projects can be one of five different types: Hour
(Employee), Cost, Revenue, Item, and On Account. Rather than use a Type field
on one table and many redundant fields, project transactions are recorded in five
tables.
Once transactions are posted against projects, an invoice proposal can be created.
The system creates a suggested invoice based on user defined criteria. Different
transactions are stored in five different tables. When transactions are invoiced,
they are stored in another set of five tables.
ProjTable: This is the main table for projects. Each project has one record in
ProjTable.
ProjTrans Class
The ProjTrans super class and its sub-classes control the creation and posting of
transactions. ProjTrans has a sub-class for each transaction type, and each sub-
classe has a sub-class for all the stages in a project.
Posting Transactions
When posting transactions against a project, journals are used. Cost transactions
use a ledger journal, item consumption transactions use an inventory journal, and
hours and revenue transactions use a project journal. On Account transactions are
created directly in the ProjOnAccTrans table. When invoiced, ledger transactions
are attached to the On Account transaction records.
Project journals are handled similar to inventory and ledger journals. You must
use a journal name from an existing ProjJournalName record to create a journal
header in ProjJournalTable, and then add lines in ProjJournalTrans. Use
ProjJournalCheckPost to post the journal.
All transaction types are created in a Proj...Trans table. When ledger or inventory
journals are posted and ProjId is marked on the line, they create ProjCostTrans
or ProjItemTrans records.
Invoice Proposal
Project invoices are created by using an invoice proposal. This proposal is
populated according to selections made by the user, which can be modified if
necessary and then posted.
Invoice proposals are created using the ProjInvoiceChoose super class and sub
classes. On account transactions use a specific sub-class.
this.progressInit("@SYS54552", progressTotal,
#AviFormLetter);
progress.setText("@SYS26577");
this.initQuery();
The initQuery() method updates the query according to selections made by the
user. If a transaction type is to be excluded the corresponding datasource is
disabled in the query. From and To dates specified by the user are also entered
into the query.
while (queryRun.next())
{
this.assignTables();
if(queryRun.changed(tablenum(ProjTable)))
{
pProjTable = queryRun.get(tablenum(ProjTable));
pProjInvoiceTable =
ProjInvoiceTable::find(pProjTable.ProjInvoiceProjId);
}
if (queryRevenue || querySubscription)
pProjRevenueTrans =
queryRun.get(tablenum(ProjRevenueTrans));
For each transaction type, if the record has changed, a method populates the
ProjProposal transaction tables.
if(this.parmProjEmplTrans() &&
this.parmProjEmplTrans().canBeInvoiced())
{
If a transaction record exists and has not been invoiced, the transaction can be
added to this proposal.
this.setProjProposalJour(this.parmProjEmplTrans().CurrencyI
d);
this.progressUpdate(strfmt("@SYS26810",this.parmProjEmplTra
ns().ProjId,this.parmProjEmplTrans().TransDate));
projProposalEmpl =
ProjProposalEmpl::initProposaleFromTrans(this.parmProjEmplT
rans());
projProposalEmpl.ProposalId =
this.parmProjProposalJour().ProposalId;
if
(CustTable::isCustDKPublic(pProjInvoiceTable.InvoiceAccount
) == NoYes::Yes && pProjInvoiceTable.eInvoiceLineSpec ==
NoYes::Yes)
{
projProposalEmpl.eInvoiceAccountCode =
pProjInvoiceTable.eInvoiceAccountCode;
}
if (projProposalEmpl.validateWrite())
{
projProposalEmpl.insert();
Invoice
Invoicing takes place in the ProjFormLetter class and the
ProjFormLetter_Invoice sub-class. The structure for these classes is similar to
the FormLetter classes used in Accounts Receivable (AR) and Accounts Payable
(AP).
if(projProposalJour)
{
try
{
if (batchHeader)
{
formLetterMultiThread =
FormLetterMultiThread::newFormLetter(this);
batchHeader.addRuntimeTask(formLetterMultiThread,this.parmC
urrentBatch().RecId);
batchHeader.addDependency(projFormLetterEndMultiThread,form
LetterMultiThread,BatchDependencyStatus::FinishedOrError);
}
else
{
this.createJournal();
}
projProposalJour.InvoiceDate =
projInvoiceParmTable.InvoiceDate;
projProposalTotals = new
ProjProposalTotals(projProposalJour, parmId);
projProposalTotals.calc();
this.tax(projProposalTotals.tax());
recordListProjProposalCost =
projProposalTotals.recordListProjProposalcost();
recordListProjProposalEmpl =
projProposalTotals.recordListProjProposalEmpl();
recordListProjProposalRevenue =
projProposalTotals.recordListProjProposalRevenue();
recordListProjProposalItem =
projProposalTotals.recordListProjProposalItem_Project();
recordListProjProposalOnAcc =
projProposalTotals.recordListProjProposalOnAcc();
Totals are calculated and the project transactions are put into lists which can be
used by the invoicing process.
if (proforma)
this.insertProforma();
else
this.insertJournal();
if (this.updateNow())
{
TransactionLog::create(this.transactionLogType(),this.tranS
actionLogTxt());
ttscommit;
this.createPayment();
}
The bulk of the work is done in updateNow(), which is overridden in the sub-
class.
this.initTransactionTxt(creditNote ?
LedgerTransTxt::ProjectCreditNoteLedger
:
LedgerTransTxt::ProjectInvoiceLedger, projProposalJour);
this.initLedgerVoucher();
this.initMarkup();
projProposalJour =
this.getProjProposalJour(projInvoiceParmTable, true);
Ledger voucher, transaction texts, and the invoice journal are initialized.
this.createProjInvoiceEmpl();
this.createProjInvoiceCost();
this.createProjInvoiceRevenue();
this.createProjInvoiceItem();
this.createProjInvoiceOnAcc();
this.createProjInvoiceSalesLine();
Invoice transactions are created and the necessary ledger and inventory postings
are handled.
projProposalJour.LineProperty =
ProjLinePropertyCode::Invoiced;
projProposalJour.LedgerVoucher =
projInvoiceJour.LedgerVoucher;
projProposalJour.SalesOrderbalance =
projInvoiceJour.SalesOrderbalance;
projProposalJour.update();
this.postMarkupTable();
this.postTax();
this.createCustTrans();
salesFormLetter.parmDeleteFullyInvoiced(true);
salesFormLetter.deleteFullyInvoiced();
this.postEndDisc();
this.postInvoiceRoundOff();
if (projInvoiceJour.CashDiscCode &&
TaxParameters::canApplyCashDiscOnInvoice_ES())
{
this.createCashDisc();
}
ledgerVoucher.end();
Summary
This lesson discusses how to design the project module, how to create and post
transactions, and how to create invoices.
2. In which class would you find a method that returns whether an employee
hours transaction has already been invoiced?
3. When posting a project invoice, which method calls the project totals
calculation method - projProposalTotals.calc()?
Challenge Yourself!
Add a WarrantyItemId field, type = itemId to the journal lines used for
employee time entry, and ensure this item id is also copied to the appropriate
project transactions when the journal is posted.
Step by Step
Challenge Yourself!
Enable an invoice proposal to be put on hold.
This requires a new field, OnHold, type = NoYes. If this field is set, then the
invoice cannot be posted, and a new invoice proposal can be created that may
include transactions that were included on the OnHold proposal.
Step by Step
1.
2.
3.
Solutions
Test Your Knowledge
1. True or False: A project is linked to the customer table through
ProjTable.CustAccount.
2. In which class would you find a method that returns whether an employee
hours transaction has already been invoiced?
3. When posting a project invoice, which method calls the project totals
calculation method - projProposalTotals.calc()?
Introduction
Workflow is a module in Microsoft Dynamics AX 2009, that allows flexible
task and approval routes for documents created by users. For example, a purchase
requisition may need to be approved by a number of different employees
according to the requisition's total amount, and each employee has to approve it
before the next employee in the approval route.
Scenario
Isaac, the systems developer, has been asked to create a new workflow that will
be used to approve a new sales order for a customer that has reached their credit
limit. The requirement is that when a new sales order is entered that takes the
customer over their credit limit, the sales order should be submitted to the
Accounts Receivable (AR) manager. They will either approve or deny the sales
order. Until it is approved, the sales order cannot be picked, packed or invoiced.
Workflow Installation
A number of the workflow system components are required to be installed before
you can begin to create and configure workflows in Microsoft Dynamics AX.
NOTE: This course does not cover the installation of the workflow system
components; however you need to be aware of the requirements. For more
information about workflow installation, refer to the Administrator Guide.
FIGURE 11.1
FIGURE 11.2
QueryName getQueryName()
{
return queryStr(SalesCreditLimitApproval);
}
Each outcome can trigger specific code by specifying a menu item for each item.
The Providers specify classes that enable rules to be defined for the workflow.
These providers are standard application classes but can be overridden and
modified, or other providers can be used in their place.
FIGURE 11.3
You are not required to do anything but set the workflow to either Approved or
Denied, therefore call the same class from two different menu items. The two
menu items simply allow you to use two different labels. In more complex
workflows it may be necessary to override or copy and modify this class rather
than use it directly.
FIGURE 11.4
You can specify conditions under which a workflow is eligible for submission.
One of these conditions must be that it has not already been submitted. To test
this condition, use a new field on the SalesTable table.
boolean canSubmitToWorkflow()
{
amountMST creditBalance;
custTable custTable;
;
if (!this.CreditLimitApprovalStatus ==
SalesCreditLimitApprovalStatus::NotSubmitted)
return false;
custTable = this.custTable_InvoiceAccount();
if (!custTable.CreditMax)
return false;
creditBalance = custTable.CreditMax -
custTable.balanceMST();
if (this.amountRemainSalesFinancial() +
this.amountRemainSalesPhysical() < creditBalance)
return false;
return true;
}
class SalesCreditLimitSubmit
{
}
workflowSubmitDialog.run();
if (workflowSubmitDialog.parmIsClosedOK())
{
recId = args.record().RecId;
SalesTable = args.record();
try
{
ttsbegin;
workflowCorrelationId =
Workflow::activateFromWorkflowTemplate(workflowTemplateName
,
recId,
note,
NoYes::No);
SalesTable.CreditLimitApprovalStatus =
SalesCreditLimitApprovalStatus::Submitted;
ttscommit;
}
catch(exception::Error)
{
info("Error on workflow activation.");
}
}
args.caller().updateWorkFlowControls();
}
SalesCreditLimitSubmit.submit(_args);
Event handlers are implemented by creating a class that implements one or more
of the EventHandler interfaces. The interfaces at the workflow level are as
follows:
Event Description
WorkflowStartedEventHandler This event raises when the
workflow instance starts.
WorkflowCompletedEventHandler This event raises when the
workflow instance ends after it
is completed.
WorkflowCanceledEventHandler This event raises when the
workflow instance ends after it
is canceled. Use this event
handler to perform any clean
up operations needed.
WorkflowConfigDataChangeEventHandler This event raises when the
workflow configuration data
changes. Use this event
handler to identify when a
configuration has changed. For
example, if you create an
association between the
application data and a
workflow configuration, this
event handler would raise if
the configuration was deleted
or updated.
ttsbegin;
SalesTable.CreditLimitApprovalStatus =
SalesCreditLimitApprovalStatus::NotSubmitted;
SalesTable.update();
ttscommit;
ttsbegin;
if (salesTable.CreditLimitApprovalStatus ==
SalesCreditLimitApprovalStatus::Submitted)
{
SalesTable.CreditLimitApprovalStatus =
SalesCreditLimitApprovalStatus::Approved;
SalesTable.update();
}
ttscommit;
ttsbegin;
SalesTable.CreditLimitApprovalStatus =
SalesCreditLimitApprovalStatus::Rejected;
SalesTable.update();
ttscommit;
ttsbegin;
SalesTable.CreditLimitApprovalStatus =
SalesCreditLimitApprovalStatus::Submitted;
SalesTable.update();
ttscommit;
Event Description
WorkflowElementStartedEventHandler This event raises when the
task or approval starts.
For approvals, you can use
this event to transition the
workflow document state
from Submitted to
PendingApproval.
WorkflowElementCanceledEventHandler This event raises when the
task or approval is canceled.
For approvals, you can use
this event to transition the
workflow document state
from the current state to
Canceled.
WorkflowElementCompletedEventHandler This event raises when the
task or approval is completed.
For approvals, you can use
this event to transition the
workflow document state
from PendingApproval to
Approved.
WorkflowElementReturnedEventHandler This event raises when the
task or approval is returned to
the originator.
For approvals, you can use
this event to transition the
workflow document state
from the current state to
RequestChange.
WorkflowElemChangeRequestedEventHandler This event raises when an
approver requests a change to
the task or approval.
For approvals, you can use
this event to transition the
workflow document state
from PendingApproval to
RequestChange.
{
}
ttsbegin;
SalesTable.CreditLimitApprovalStatus =
SalesCreditLimitApprovalStatus::Rejected;
SalesTable.update();
ttscommit;
Configure a Workflow
Now that you have created a template and enabled it on a form, you can
configure it for use.
1. Open the main menu and select Accounts Receivable > Setup >
Workflow configurations.
2. Click New.
3. Select Sales credit limit approval and click Create configuration.
4. Enter "Credit limit approval" as the name.
5. Click Create Instruction, enter "Please approve" and then click OK.
6. Click the Details tab, expand SalesCreditLineApproval, and click on
Step 1.
7. Under Step details, click on Assignment tab.
8. Click the Choose button.
9. Select User based, enter a user in the Select users field and then click
OK.
10. Close the approval form.
11. On the configuration form click Set as active.
12. Click the Overview tab.
13. Click Set as default.
1. Select a customer from the customer table and set a credit limit.
2. Create a new sales order and create lines such that the balance of the
customer plus the total amount on the lines is greater than the credit
limit.
3. The workflow submit button and dialog should appear.
4. Click the submit button and enter a comment.
5. Open the AOT.
6. Expand the Forms node.
7. Find the form Tutorial_WorkFlowProcessor.
8. Right-click on this form and select Open.
9. Click Start.
10. When the form says that it has zero records in queue, click Stop and
go back to the sales table form.
11. Select Actions > History. You will see that the document is waiting
for approval by the person you assigned to approve it.
12. Logon as the user who should approve the sales order
13. Open the sales order form.
14. Click the workflow Actions button and select Approve.
15. Open the Tutorial_WorkflowProcessor form again and click Start,
wait for it to complete and click Stop.
16. The workflow has now been approved.
Challenge Yourself!
Add conditions to the posting functions on the sales order form that will prevent
posting to picking, packing or invoicing until the workflow has been approved. If
the credit limit has not been reached, then the postings should be allowed.
Step by Step
boolean canPostCreditLimit()
{
amountMST creditBalance;
custTable custTable;
;
if (this.CreditLimitApprovalStatus ==
SalesCreditLimitApprovalStatus::Approved)
return true;
if (this.CreditLimitApprovalStatus ==
SalesCreditLimitApprovalStatus::Rejected
|| this.CreditLimitApprovalStatus ==
SalesCreditLimitApprovalStatus::Submitted)
return false;
custTable = this.custTable_InvoiceAccount();
if (!custTable.CreditMax)
return true;
creditBalance = custTable.CreditMax -
custTable.balanceMST();
if (this.amountRemainSalesFinancial() +
this.amountRemainSalesPhysical() < creditBalance)
return true;
return false;
boolean canPickingListBeUpdated()
{
......
ok = ok && salesTable.canPostCreditLimit();
return ok;
boolean canPackingslipBeUpdated()
{
......
ok = ok && salesTable.canPostCreditLimit();
return ok;
boolean canInvoiceBeUpdated()
{
......
ok = ok && salesTable.canPostCreditLimit();
return ok;
workflowSubmitDialog =
WorkflowSubmitDialog::construct(args.caller().getActiveWork
flowConfiguration());
workflowSubmitDialog.run();
The record is retrieved from the calling form and the recId is passed to the static
method Workflow::activateFromWorkflowTemplate(), which runs the submit
process.
ProjJournalTable = args.record();
// Get comments from the submit to workflow dialog.
_initialNote = workflowSubmitDialog.parmWorkflowComment();
try
{
ttsbegin;
tableId =
Workflow::getDocumentTableId(_workflowTemplateName);
configTable =
Workflow::findWorkflowConfigToActivateForTemplate(_workflow
TemplateName, _recId, tableId);
The tableId that the workflow is to be performed on is retrieved from the query
specified in the workflow document class.
The workFlowContext class holds all the relevant data for the workflow
submission. The SysWorkFlowEventDispatcher class creates records that will be
read by the Workflow Processor class to determine which actions should be
executed in the next step of the workflow.
workflowContext =
WorkflowContext::newRootWorkflowContext(curext(), tableId,
_recId, correlationId);
try
{
SysWorkflowEventDispatcher::onWorkflowSubmit(workflowContex
t, _submittingUser, configTable.ConfigurationId,
_initialNote, _activatingFromWeb);
}
ProjJournalTable.WorkFlowState = true;
WorkflowWorkItem::escalateWorkItem(SysWorkflowWorkItemConte
xt::newWorkflowWorkItemContextFromWorkItem(workflowWorkItem
Table));
cntWorkItems++;
}
try
{
workItemId =
SysWorkflowEventDispatcher::onWorkItemEscalation(_workItemC
ontext);
}
workItemTable =
WorkflowWorkItemTable::findPendingActivityInstanceId(_workI
temContext.parmWorkflowActivityInstanceKey().parmWorkflowAc
tivityInstanceId(), true);
The workItemTable record is retrieved. This is the next pending activity on the
workflow, based on the configuration.
switch (stepTable.EscalationType)
{
case WorkflowEscalationType::Action:
workItemInstanceId = workItemTable.Id;
workItemTable.Status =
SysWorkflowEventDispatcher::completeWorkItem(
_workItemContext,
workItemTable,
stepTable.EscalationAction,
workItemTable.UserId,
workflowTable.Originator, // always set the auto-
escalate user to the workflow originator
"@SYS110277");
Challenge Yourself!
When a workflow is rejected, it should be able to be resubmitted. Modify the
Submit to Workflow class so that it can resubmit the workflow after a rejection
Step by Step
if (_args.menuItemName() ==
menuitemactionstr(SalesCreditLimitSubmit))
{
SalesCreditLimitSubmit.submit(_args);
}
else
{
SalesCreditLimitSubmit.resubmit(_args);
}
WorkflowWorkItemActionType::Resubmit,
new
MenuFunction(menuitemactionstr(PurchReqReSubmit),
MenuItemType::Action));
workflowWorkItemActionDialog.run();
if (WorkflowWorkItemActionDialog.parmIsClosedOK())
{
_recId = args.record().RecId;
SalesTable = args.record();
// Get comments from the submit to workflow dialog.
_initialNote =
workflowWorkItemActionDialog.parmWorkflowComment();
try
{
ttsbegin;
WorkflowWorkItemActionManager::dispatchWorkItemAction(
args.caller().getActiveWorkflowWorkItem(),
_initialNote,
curUserId(),
WorkflowWorkItemActionType::Resubmit,
args.menuItemName(),
false);
SalesTable.CreditLimitApprovalStatus =
SalesCreditLimitApprovalStatus::Submitted;
ttscommit;
}
catch(exception::Error)
{
info("Error on workflow activation.");
}
}
args.caller().updateWorkFlowControls();
}
Summary
The workflow module is a highly configurable and flexible module. However, by
using Morph X and some standard code templates, it can be configured for any
part of the Microsoft Dynamics AX application.
This lesson explores some of the possibilities the workflow framework offers,
and explores some of the different ways it can be used to cover most workflow
requirements.
2. Which type of AOT element needs to be created to specify which tables will
be affected by a workflow?
( ) Extended data type
( ) Class
( ) Form
( ) Query
3. There are three types of providers that define what rules the workflow can
follow. What are they?
( ) Participant provider
( ) DueDate provider
( ) Hierarchy provider
( ) Internet provider
4. Which two properties on a form data source need to be modified to allow the
form to use a workflow?
( ) WorkflowTemplate
( ) WorkflowEnabled
( ) WorkflowDocument
( ) WorkflowDatasource
1.
2.
3.
Solutions
Test Your Knowledge
1. Which application element is used to define to which module a workflow is
applicable?
( ) Workflow template
() Workflow category
( ) A field in the workflow configuration
( ) SalesTable
2. Which type of AOT element needs to be created to specify which tables will
be affected by a workflow?
( ) Extended data type
( ) Class
( ) Form
() Query
3. There are three types of providers that define what rules the workflow can
follow. What are they?
() Participant provider
() DueDate provider
() Hierarchy provider
( ) Internet provider
4. Which two properties on a form data source need to be modified to allow the
form to use a workflow?
( ) WorkflowTemplate
() WorkflowEnabled
( ) WorkflowDocument
() WorkflowDatasource