...feel the spirit of Microsoft Dynamics AX RSS 2.0
 Monday, February 07, 2011

Das Speichern eines Datensatzes in einem Tabellen-Feld ist an sich kein Problem.
Aber leider ist dies eine der nicht so oft verwendeten Sachen in der Dynamics AX Entwicklung und wird deshalb immer mal gerne wieder vergessen. :-)

Wir haben also eine Tabelle mit einem Feld, welches einen Container als Datentyp hat.
Weiterhin haben wir einen Datensatz welchen wir in diesem Feld speichern wollen.
Wie bekommen wir nun den Datensatz in dem Feld gespeichert? Ein direkte Zuweisung für hierbei leider zu einem Fehler.

Für diese Operationen stellt der Dynamics AX Standard folgende Funktionen zur Verfügung:

  • buf2con(common buffer)
  • con2buf(container c, common buffer)

Schreiben des Datensatzes in das Tabellenfeld

Die Funktion “buf2Con” wandelt eine Datensatz in einen Container, welcher entsprechend in dem Tabellen-Feld gespeichert werden kann.

Lesen des Datensatzes aus dem Tabellenfeld

Die Funktion “con2buf” wandelt einen Container (genauer dessen Inhalt) wieder zu einem Datensatz um.

Allerdings sollte bei der Verwendung dieser beiden Funktionen immer bedacht werden, dass Änderungen an Tabellen, von denen Datensätze auf diese Art gespeichert werden zu Problemen führen können.

Monday, February 07, 2011 7:59:20 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

 Monday, August 16, 2010

Wie im Blog des Dynamics AX Sustained Engineering Teams zu lesen ist, wird der neue SharePoint Server, Microsoft SharePoint 2010, nun auch von Dynamics AX 2009 SP1 unterstützt.

Es wird allerdings empfohlen, dass Hotfix Rollup 5 zu installieren.

Genauere Details können dem Blog des Dynamics AX Sustained Engineering Teams oder im “Solutions Monkey” Blog entnommen werden:

Artikel im Blog des Dynamics AX Sustained Engineering Teams

Artikel im “Solutions Monkey” Blog

Monday, August 16, 2010 8:25:44 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  |  | 

 Thursday, August 05, 2010

Mit Microsoft Dynamics AX 2009 wird für die Erstellung neuer dokumentenbasierter AIF-Services ein Tool/Wizard mitgeliefert, der so genannte “Assistent für AIF-Dokumentendienste”, welcher den Entwickler bei der Erstellung eines neuen AIF-Services unterstützt.

Wie dieser Wizard verwendet wird, kann zum Beispiel in der MSDN nachgelesen werden:

How to: Create a Service Using the AIF Document Service Wizard
Walkthrough: Creating a Service Using the AIF Document Service Wizard

In den meisten Fällen funktioniert dieser Wizard auch sehr gut und bietet somit eine echte Entwicklungsunterstützung.
Es gibt allerdings auch Fälle in denen der Wizard auf einen Fehler läuft, welche auf den ersten Blick sehr schwer zu verstehen sind.

Ein Beispiel hierfür sind Tabellen, die ein Feld beinhalten welches als Datentyp den Extended Data Type “InventDimId” definiert hat.
Wird beim Erstellen des Query-Objekts für den AIF-Service nicht darauf geachtet, dass auch die Tabelle  InventDim mit entsprechender Relation in die Query aufgenommen wird, erzeugt der Wizard eine Fehlermeldung.

Wird folgende Query für den neuen AIF-Service angelegt und diese für die Generierung des AIF-Service verwendet

AxdQueryNotWorking

erzeugt der Assistent für AIF-Dokumentendienste folgenden Fehler:

AifWizardError

Um diesem Problem aus dem Weg zu gehen, muss die Tabelle “InventDim” mit in die Query aufgenommen werden, selbst wenn diese für den Dokumentkontext nicht von Bedeutung ist.

AxdQueryWorking

Anmerkung:
An dieser Stelle soll auch auf den Punkt hingewiesen werden, dass der Assistent für AIF-Dokumentendienste keinen vollständig funktionsfähigen AIF-Service erstellt.
Es wird eher mehr das Grundgerüst des AIF-Service angelegt und der Entwickler muss nach Ausführung des Wizards unter Anderem noch die benötigte Business-Logik ergänzen um die gewünschte Funktionalität bereit stellen zu können.

Thursday, August 05, 2010 8:30:32 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  |  |  |  | 

 Friday, June 11, 2010

Ein großer Vorteil des Application Integration Frameworks (AIF) gegenüber “selbst geschriebene” Schnittstellen ist es, dass man sich über Dinge wie Datentyp-Mapping keine Gedanken machen muss.
Das Application Integration Framework verfügt über die entsprechende Logik, um alle Dynamic AX Datentypen automatisch in den jeweils gültigen XSD-Datentyp zu “mappen” (oder umgekehrt).

Zeiten, in denen sich der Entwickler zum Beispiel Gedanken machen musste, wie viele Nachkommastellen eine Zahl haben darf, oder welches Zeichen als Dezimaltrennzeichen verwendet werden muss, sind somit vorbei.
Da alle Daten die aus Dynamics AX exportiert oder nach Dynamics AX importiert werden, in einem XSD-Schema konformen XML-Dokument “transportiert” werden, und das AIF entsprechendes Mapping bereits stellt, geschieht das Datentyp-Mapping automatisch.

Allerdings kann auf Seiten der Anwendung, welche über das Application Integration Framework (AIF) angebunden werden soll, ein wenig “Verwirrung” entstehen.
Durch die von Programmiersprache zu Programmiersprache durchaus unterschiedlichen Datentypen kann es vorkommen, dass Dynamics AX Datentypen nicht in dem erwarteten Datentyp der anderen Programmiersprache erscheinen.
Dies ist allerding kein “wirkliches” Problem des Application Integration Frameworks (AIF), sondern eher eine Frage, welche Datentypen eine Programmiersprache bereit stellt und wie diese in XSD-Datentypen “gemappt” werden.

Ein gutes Beispiel hierfür sind die Dynamics AX Datentypen “Date”, “Time” und “DateTime” (inklusive aller von diesen Basisdatentypen abgeleiteten EDT’s).

Ohne genauere Betrachtung liegt die Annahme nahe, dass ein DateTime Datentyp von Dynamics AX in einen DateTime Datentyp von z.B. C# “gemappt” wird.
Dies ist allerdings nicht richtig. Da nicht direkt zwischen Dynamics AX Datentyp und C# Datentyp gemappt wird, sondern immer von/zu einem XSD-Datentyp gemappt wird, wird in C# eine neue Klasse hierfür erzeugt.

Ein wenig schwieriger wird es bei den beiden Dynamics AX Datentypen “Date” und “Time”. Für diese Datentypen wird z.B. in C# kein direkt vergleichbarer Datentyp bereit gestellt.
Diese Datentypen werden jeweils als C# DateTime Datentypen gemappt.

Das Mapping der Datentypen geschieht wie folgt:

Dynamics AX

XSD Schema

.NET (C#)

Date

xs:date System.DateTime

Time

xs:time System.DateTime

DateTime

xs:dateTime new class
i.e. “AxdType_DateTime”


Da die beiden Dynamics AX Datentypen “Date” und “Time” in den C# Datentyp “DateTime” gemappt werden, kann an dieser Stelle leider ein kleines Problem entstehen.
In C# ist nun leider nicht mehr zu erkennen, um was für einen Dynamics AX Datentyp es sich z.B. bei einem Feld handelt, und ob nun ein Datum oder eine Zeit in diesem enthalten ist.

Oftmals entsteht diese Problem dadurch nicht, dass der jeweilige Business-Kontext die Datentypverwendung entsprechend einschränkt und es somit teilweise egal ist ob nun in ein Dynamics AX Date oder Time gemappt wird.
Ist es aber erforderlich zu wissen, um ob ein Feld nun den Dynamics AX Datentyp Date oder Time hat, kann der generierte Code der Proxyklasse Aufschluss geben (oder das XSD-Schema).

Durch die Angabe eines Serialisierungs-Attributes wird bestimmt, welcher “Teil” des DateTime Datentyps verwendet wird.
Für ein Feld, welches in einen Dynamics AX Date Datentyp gemappt wird, wird nur der “Datumsteil” in das XML-Dokument serialisiert.
Entsprechendes geschieht für einen Dynamics AX Time Datentyp.

Mapping eines C# Datetime Datentyps in einen Dynamics AX Date Datentyp (generierter Code der Proxyklasse):

   1:  [System.Xml.Serialization.XmlElementAttribute(DataType="date", IsNullable=true, Order=54)]
   2:  public System.Nullable<System.DateTime> MyDateField {
   3:      get {
   4:          return this.myDateFieldField;
   5:      }
   6:      set {
   7:          this.myDateFieldField = value;
   8:          this.RaisePropertyChanged("MyDateField");
   9:      }
  10:  }

Mapping eines C# Datetime Datentyps in einen Dynamics AX Time Datentyp (generierter Code der Proxyklasse):

   1:  [System.Xml.Serialization.XmlElementAttribute(DataType="time", IsNullable=true, Order=56)]
   2:  public System.Nullable<System.DateTime> MyTimeField {
   3:      get {
   4:          return this.myTimeFieldField;
   5:      }
   6:      set {
   7:          this.myTimeFieldField = value;
   8:          this.RaisePropertyChanged("MyTimeField");
   9:      }
  10:  }

Wie durch den generierten Code der Proxyklasse ersichtlich wird, wird nur der jeweils benötigte “Teil” eines C# Datetime Datentyps serialisiert/deserialisiert und somit verwendet.

Für einen Dynamics AX DateTime Datentyp wird bei Erstellung des Proxys eine neue Klasse generiert. Somit kann der Dynamics AX Datentyp hierbei immer eindeutig identifiziert werden.

   1:  [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "2.0.50727.4016")]
   2:  [System.SerializableAttribute()]
   3:  [System.Diagnostics.DebuggerStepThroughAttribute()]
   4:  [System.ComponentModel.DesignerCategoryAttribute("code")]
   5:  [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/dynamics/2008/01/documents/Customer")]
   6:  public partial class AxdType_DateTime : object, System.ComponentModel.INotifyPropertyChanged {
   7:          
   8:      private System.DateTime localDateTimeField;
   9:          
  10:      private bool localDateTimeFieldSpecified;
  11:          
  12:      private AxdEnum_Timezone timezoneField;
  13:          
  14:      private bool timezoneFieldSpecified;
  15:          
  16:      private System.DateTime valueField;
  17:         
  18:      [System.Xml.Serialization.XmlAttributeAttribute()]
  19:      public System.DateTime localDateTime ...  
  20:      
  21:      [System.Xml.Serialization.XmlIgnoreAttribute()]
  22:      public bool localDateTimeSpecified...   
  23:      
  24:      [System.Xml.Serialization.XmlAttributeAttribute()]
  25:      public AxdEnum_Timezone timezone...
  26:          
  27:      [System.Xml.Serialization.XmlIgnoreAttribute()]
  28:      public bool timezoneSpecified...
  29:         
  30:      [System.Xml.Serialization.XmlTextAttribute()]
  31:      public System.DateTime Value {
  32:          
  33:      public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
  34:          
  35:      protected void RaisePropertyChanged(string propertyName)...
  36:  }

Dieses Verhalten ist nicht nur mit C# zu beobachten. Auch JAVA oder andere Programmiersprachen verhalten sich ähnlich und muss entsprechend berücksichtigt werden.

Friday, June 11, 2010 8:33:13 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  |  |  |  |  |  | 

 Friday, June 04, 2010

Macros werden innerhalb von Dynamics AX z.B. für die Best-Practice konforme Verwendung von festen Zeichenfolgen innerhalb des X++ Quellcodes verwendet.
An vielen Stellen im System finden sich Quellcodezeilen wie diese:

#define.MySimpleMacro('The string value')

Dies ist die am meist verwendete Art der Macrodefinition innerhalb von Dynamics AX.
So weit, so gut.

Es gibt allerdings Konstellationen von “Werten”, welche bei dieser Art der Macrodefinition zu einer Fehlermeldung beim speichern führen.
Es bei dieser Art der Macrodefinition z.B. nicht möglich, eine schließende Klammer als Macrowert zu definieren.
Dies ist auch so im MSDN dokumentiert: http://msdn.microsoft.com/en-us/library/cc197110.aspx

Gerade bei der Verwendung von Regular Expressions (Regex) kann dies zu regelmäßiger Verwirrung führen, da man gerne die weiteren Macrodefinitions-Möglichkeiten vergisst und oder diese nicht so präsent sind.

Die Syntax

#define.Macroname(Wert)

sollte eigentlich nur verwendet werden um (einfache) Konstanten innerhalb des Quellcodes zu definieren.
Für alle anderen Fälle und wenn ein Macro mit mehr als einer Zeile benötigt wird, sollte folgende Syntax zu Definition eines Macros verwendet werden:

#localmacro.AnExample

// Some statements or text

#endmacro

Die Verwendung von #localmacro anstelle eines "(einfachen) #define erlaubt es nun auch mit schließenden Klammern als Macrowert zu arbeiten oder sogar ganze SQL oder X++ Codeblöcke zu verwenden.
Eine genauere Beschreibung von Dynamics AX Macros (deren Möglichkeiten und Einsatzgebiete) ist im MSDN dokumentiert:

Macros in X++

Friday, June 04, 2010 6:10:52 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  | 

 Thursday, March 25, 2010

Im Microsoft Downloadcenter ist ein Whitepaper verfügbar, welches bei der Konfiguration einer Microsoft Dynamics AX Datenbank unterstützen soll.

Das Dokument kann als eine Art “Checkliste” angesehen werden, nach welcher einzelne Konfigurationspunkte abgearbeitet werden können.

Das Whitepaper kann über folgenden Link bezogen werden:
http://www.microsoft.com/downloads/details.aspx?FamilyID=ab4cd401-b366-4c1c-9a73-88c945ae8191&displaylang=en

Thursday, March 25, 2010 10:01:51 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  |  | 

 Saturday, February 27, 2010

Beim Entwickeln und/oder Testen von AIF-Service, welche als Webservice bereit gestellt werden, entstehen oftmals (mindestens) 2 Fragestellungen.

  1. Wie kann die Nachricht betrachtet werden, welche zwischen den System über den Webservice ausgetauscht wird?
  2. Wie lässt sich der Webservice-Aufruf debuggen?

Leider werden diese Fragen bei einer Suche im Internet oft mit “Geht nicht” beantwortet.
Dies ist so allerdings nicht richtig.

Es ist z.B. möglich, für alle Webserviceaufrufe eine Tracing-File zu erzeugen, welches u.A. auch die Nachricht protokolliert, die von oder zu Dynamics AX geschickt wurde.
Um das Tracing zu aktivieren müssen nur entsprechende Einstellungen in der web.config des Webservices vorgenommen werden.

Wie dies im Detail funktioniert beschreibt dieses kleine How-To des Microsoft Dynamics Developer Centers im MSDN.
http://msdn.microsoft.com/en-us/library/cc967372.aspx

Es ist ebenfalls möglich, die Webserviceaufrufe zu debuggen.

Allerdings müssten hierfür einige Schritte beachtet werden, damit das Debuggen von Webserviceaufrufen auch für X++ Code funktioniert.
Eine detaillierte Anleitung hierzu ist am Ende des Whitepapers “Tips for Creating Services in Microsoft Dynamics AX 2009” zu finden.
http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=90388b14-fb8c-4633-a255-28ff7146c5b2

Saturday, February 27, 2010 4:21:20 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  |  | 

 Wednesday, February 03, 2010

Um Datensätze in Microsoft Dynamics AX, welche z.B. auf einer Maske angezeigt werden, entsprechend seiner Anforderungen einzuschränken (zu filtern) muss die Query (Abfrageobjekt) durch Erstellung von Ranges (Abfrageeinschränkungsobjekt) entsprechend “manipuliert” werden.
Hierfür wird z.B. die Query einer Maskendatenquell (DataSource) verwendet und für diese eine neue Range definiert:

Beispiel:

public void init()
{
    QueryBuildRange     range;
    ;
    super();
    
    range = CustTable_ds.query().dataSourceTable(tablenum(CustTable)).addRange(fieldnum(CustTable, AccountNum));
    range.value("1101");
}

Im Dynamics AX Standard kann ähnlicher X++ Quelltext in vielen Masken gefunden werden, da dies der “Standard-Weg” zum Einschränken von Datensatzabfragen bei Masken oder auch Reports ist.
Ebenso wird dieses Vorgehen auch in den Schulungsunterlagen, in der Entwicklerhilfe und anderen Stellen beschrieben.

Leider hat dieses Vorgehen eine kleine aber teilweise sehr störende Beschränkung.
Über diesen Weg ist es nicht möglich, alle Abfrageeinschränkungen welche durch X++ Quellcode “gesetzt” wurden und Einschränkungen, welche durch einen Benutzer mittels der Standardfilterfunktion von Dynamics AX definiert wurden, zu berücksichtigen. Beim einer Datenaktualisierung (Aufruf von DataSource.executeQuery) gehen die von einem Benutzer definierten Abfrageeinschränkungen verloren.

Dies ist darin begründet, dass es nicht nur ein DataSoucre.query-Objekt, sondern auch ein DataSource.queryrun().query-Objekt gibt.
Diese beiden “Query-Objekte” sind jeweils unterschiedliche Objekte, bzw. Objektinstanzen.

Das DataSource.query-Objekt ist das “Basisabfrageobjekt”, welches durch einen Benutzer, mittels der Filterfunktionalität des Standards, nicht verändert werden kann (nur durch X++ Code).
Alle durch den Benutzer vorgenommenen Änderungen an der “Basisabfrage” werden in dem Query-Objekt von DataSource-queryrun() “gespeichert”.
Dies kann unter Anderem durch Betrachtung des SQL-Statements, welches durch ein Query-Objekt bereit gestellt wird nachgewiesen werden.

Beispiel:

Aufruf einer Maske mit einer durch X++ Code modifizierten Abfrage (Query).
Anmerkung: Beim Aufruf der Maske wird in der “Init-Methode” eine Range (CustGroup = “10”) gesetzt.

range = CustTable_ds.query().dataSourceTable(tablenum(CustTable)).addRange(fieldnum(CustTable, CustGroup));
range.value("10");

QueryVSQueryRunQuery_1

Durch den Benutzer wird nun mittels der Dynamics AX Standard-Filterfunktion die Abfrage bzw. deren Einschränkung(en) angepasst/verändert.

QueryVSQueryRunQuery_2

Hierdurch ist zu beobachten, das sich zwar das SQL-Statement des DataSource.queryrun().query-Objekts, aber nicht das SQL-Statement des DataSoucre.query-Objekts ändert.
Da bei einem Aufruf von DataSource.executeQuery allerdings immer das DataSource.query-Objekt verwendet wird, gehen die durch den Benutzer gewählten Abfrageeinschränkungen verloren.

Wie ist es nun aber möglich, die von einem Benutzer gewählten Abfrageeinschränkungen/Filtereinstellung doch zu berücksichtigen?

Da alle Abfrageeinschränkungen, welche von einem Benutzer gewählt wurden, in dem Query-Objekt von DataSource.queryrun() “gespeichert” werden und somit auch im X++ Code zur Verfügung stehen ist dies recht einfach.
Es muss einfach das Query-Objekt von DataSource.queryrun() genommen werden, um die gewünschten Ranges ergenzt werden und schließlich dass Query-Objekt der DataSource überschrieben werden.

Beispiel:

Basis ist eine einfach Maske, welche alle Kundendatensätze anzeigt.

ShowQuery_1

Dieser Maske/Abfrage wird nun durch die Standard-Filterfunktion (Benutzerfilter) eine neue Abfrageeinschränkung hinzugefügt (Kundennummer = 1101 und 2001).

ShowQuery_2

Wie zuvor beschrieben, wird nun eine neue Abfrageeinschränkung mit X++ Code auf dem Query-Objekt von DataSource.queryrun “gesetzt” und das Query-Objekt der DataSource mit diesem überschrieben.

void clicked()
{
    Query               query;
    QueryBuildRange     range;
    ;
    super();
 
    query = CustTable_ds.queryRun().query();
 
    range = query.dataSourceTable(tablenum(CustTable)).addRange(fieldnum(CustTable, CustGroup));
    range.value("10");
 
    CustTable_ds.query(query);
 
    CustTable_ds.executeQuery();
}

Dies hat zur Folge, dass die durch den Benutzer gewählten Abfrageeinschränkungen, wie zu sehen, weiterhin berücksichtigt werden.

AddRange_2

Alternativ zur Verwendung der Abfrageeinschränkung (Range) “direkt” über das Query-Objekt kann auch mit einem oder mehreren Filtern gearbeitet werden.
Diese unterliegen im Gegensatz zu den Query-Objekt aber einigen Einschränkungen, sodass diese nicht in jeder Situation verwendet werden können.

Der folgende X++ Code zeigt, wie ein Filter gesetzt werden kann.

void clicked()
{
    Query               query;
    QueryBuildRange     range;
    ;
    super();
 
    CustTable_ds.filter(fieldnum(CustTable, CustGroup), "10");
}

Wird eine Abfrage auf diese Art und Weise eingeschränkt, ist der Aufruf von DataSource.executeQuery() unnötig, da die Datenaktualisierung bereits im Hintergrund durch den Filter-Aufruf durchgeführt wird.
Filter einer DataSource funktionieren vom Prinzip her wie die Standardfilter, welche durch einen Benutzer in Dynamics AX gesetzt werden können.
Dies hat zur Folge, dass sich diese ebenfalls nur auf das Query-Objekt von DataSource.queryrun() auswirken und somit DataSource.query nicht beeinflussen.

Add_Filter

Mit der Methode DataSource.removeFilter können die gesetzten Filter wieder gelöscht werden.
Leider werden hierdurch alle gesetzten Filter gelöscht, sodass nach diesem Aufruf unter Umständen einige bereits gesetzte Filter erneut gesetzt werden müssen, um das gewünschte Abfrageergebnis zu erhalten.

Welche der gezeigten Methoden, zum Einschränken von Abfragen, aber nun der beste oder bessere Weg ist, muss von Fall zu Fall entschieden werden.

Wednesday, February 03, 2010 8:49:23 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

 Monday, January 25, 2010

Oftmals besteht die Anforderung, über einen Zeitgesteuerten Job (Batchjob), das Generieren von statistischen Berichten, welche in einer Datei gespeichert werden soll, zu automatisieren.

Microsoft Dynamics AX stellt hierfür die Möglichkeit bereit, jeden Bericht mithilfe der Stapelverarbeitung zu einem definierten Zeitpunkt zu generieren und in einer Datei, z.B. in einem Netzwerklaufwerk, bereit zu stellen.
Soweit stellt dies kein Problem dar, da über den Standard von Dynamics AX diese Anforderung ohne weiteres erfüllt werden kann.

Leider wird hierbei oft vergessen, dass  der entsprechende Batchserver (AOS) so konfiguriert werden muss, dass dieser das “Drucken auf dem Server” zulassen muss.
Dies ist eine Einstellungsoption des Serverkonfigurations-Utilities.

Weiterhin sollte bei der Angabe der Datei bzw. des Speicherortes der Datei immer ein UNC-Pfad verwendet werden, da die eigentlich Ausführung des Berichtes und somit auch die Erstellung der Datei über das Benutzerkonto des AOS-Dienstes geschieht.

Dies bedingt auch, dass entsprechende Berechtigungen für das Dienstkonto des Batchservers (AOS) für das freigegebene Verzeichnis vergeben werden müssen, damit die Datei und somit der Bericht erfolgreich erstellt werden kann.

Eine weiterführende Beschreibung hierzu ist auch im EMEA Dynamics AX Support Blog zu finden.

Monday, January 25, 2010 9:04:57 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  |  | 

 Sunday, January 24, 2010

In Microsoft Dynamics AX wird das Args-Objekt dazu verwendet, Informationen z.B. an eine aufzurufende Maske oder Klasse zu übergeben.

Mittels des Args-Objektes ist es z.B. möglich, den auf einer Maske ausgewählten Datensatz an die Aufzurufende (Unter)Maske zu übergeben, um mit diesem die Darstellung und/oder Funktionen der Maske anzupassen.
Oft wird dieses Vorgehen dazu verwendet, Abfragen (Queries) entsprechend einzugrenzen, damit nur relevante Informationen verarbeitet werden.
Eine beispielhafte Anforderung hierfür könnte sein, alle Aufträge des zuvor ausgewählten Kunden in einer neuen Maske anzuzeigen.

Manchmal ist aber notwendig, nicht nur den Aufrufer (oder den gewählten Datensatz), sondern auch dessen Aufrufer zu kennen, um bestimme Funktionalitäten erstellen oder implementieren zu können.
Herbei kann es sein, dass der ausgewählter Datensatz über mehrere Aufrufebenen übergeben werden muss und der direkte Aufrufer dennoch bekannt sein muss.

GetTheCallerOfTheCaller_Schema

Nehmen wir an, es existiert eine Hauptmaske, auf der ein Kundendatensatz ausgewählten werden kann. Auf einer weiteren Maske (1. Maske), sollen nun alle Aufträge des ausgewählten Kunden angezeigt werden. Diese Maske soll über die Hauptmaske aufgerufen werden. Über eine 2. Maske, welche über die 1. Maske aufgerufen werden soll, sollen die Adressdaten des Kunden angezeigt werden.

GetTheCallerOfTheCaller2

Um nun die benötigten Information, ausgewählter Kunden in der Hauptmaske, auf der 2. Maske zur Verfügung zu haben, muss über das FormRun-Objekt der 1. Maske der Aufrufer (Caller) dieser Maske bestimmt werden.

Beispiel (Init-Methode der 2. Maske):

public void init()
{
    Object          callerDataSource;
    FormRun         callerFormRun;
    common          callerRecord;
    common          callerRecordOfCallerRecord;
    ;
    super();
 
    if(element.args() && element.args().dataset())
    {
        callerRecord = element.args().record();
        callerDataSource = callerRecord.dataSource();
        callerFormRun = element.args().caller();
 
        callerRecordOfCallerRecord = callerFormRun.args().record();
 
        CtrlCallerTable.text(tableid2name(callerRecord.TableId));
        CtrlCallerOdCallerTable.text(tableid2name(callerRecordOfCallerRecord.TableId));
    }
}

 

Sicherlicht läßt sich das in dem Beispiel beschriebene Verhalten auch anders (oder eleganter) Lösen. Dieses Beispiel wurde nur gewählt, um den Ablauf oder die notwendigen Schritte zu demonstrieren, wie Aufrufer über mehrere Ebenen bestimmt werden können.

Sunday, January 24, 2010 8:00:59 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

 Friday, January 22, 2010

Notizen, Dokumente oder Dateien werden in Dynamics AX mithilfe des “Dokumentenmanagement-Systems” verwaltet.

Zu jedem beliebigen Datensatz einer beliebigen Tabelle (z.B. CustTable -> Debitoren) können beliebig viele Notizen oder Dokumente hinterlegt werden.
Per Benutzeroberfläche kann die entsprechende Funktionalität über die Menüleiste der Masken aufgerufen werden.

Das folgende Beispiel zeigt wie dies auch per Programmcode erfolgen kann:

static void AKU_CreateDocuRefNote(Args _args)
{
    CustTable       custTable;
    DocuRef         docuRef;
    DocuType        docuType;
    ;
    custTable = CustTable::find("1101");
    docuType  = DocuType::find("Note");
    
    if(custTable && docuType)
    {
        docuRef.initValue();
        
        docuRef.RefTableId   = custTable.TableId;
        docuRef.RefRecId     = custTable.RecId;
        docuRef.RefCompanyId = custTable.dataAreaId;
 
        docuRef.TypeId       = docuType.TypeId;
        
        docuRef.Restriction  = DocuRestriction::External;
        
        docuRef.Name         = "Name der Notiz";
        docuRef.Notes        = "Text (Inhalt) der Notiz";
        docuRef.insert();
    }
}
Friday, January 22, 2010 7:29:40 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

 Wednesday, December 30, 2009

Für Debitoren (Kunden) kann in Microsoft Dynamics AX ein Kreditlimit vergeben/eingestellt werden.

In der Auftragsmaske wird bei Anlage eines Auftrag (in Abhängigkeit der Kreditlimit-Einstellungen) das verbleibende Kreditlimit berechnet und der Auftragswert gegen dieses geprüft.
Bei Überschreitung des Kreditlimits wird eine entsprechende Warnung oder ein entsprechender Fehler ausgegeben.

Die Funktion zur Überprüfung des Kreditlimits kann auch manuell, durch  eine entsprechende Funktion auf der Auftragsmaske, aufgerufen werden.

Soll das verbleibende Kreditlimit mit X++ Code berechnet werden, muss leider eine “Kleinigkeit” beachtet werden, die so auf den ersten Blick nicht immer ersichtlich ist bzw. für Verwirrung sorgen kann.

Die Berechnung des verfügbaren Kreditrahmens oder des verbleibenden Kreditlimits ist durch die alleinige Angabe eines Debitors nicht möglich.
Es muss immer ein entsprechender Auftrag “vorhanden” sein um diese auszuführen zu können.

Berechtigterweise stellt sich die Frage, wie das verbleibende Kreditlimit eines Debitors berechnet werden kann, wenn keine “Beziehung” zu einem Auftrag besteht bzw. wenn kein Auftrag angegeben werden kann.
Hierfür muss ein kleiner “Trick” angewendet werden, der nichts anderes macht, als einen neuen “SalesTable” Datensatz zu initialisieren, diesen aber nicht zu speichern.
Ist der “SalesTable” Datensatz initialisiert kann mithilfe der beiden Klassen “SalesTotals” und “CustCreditLimit” der verfügbare Kreditrahmen berechnet werden.

Beispiel:

    CustTable               custTable = CustTable::find("1101");
    CustCreditLimit         custCreditLimit;
    SalesTotals             salesTotals;
    SalesTable              salesTable;
    AmountMST               balanceEstimate;
    AmountMst               creditRemain;
    ;
    salesTable.CustAccount  = custTable.AccountNum;
    salesTable.initFromCustTable();
 
    salesTotals = SalesTotals::construct(salesTable);
    salesTotals.calc();
 
    custCreditLimit = CustCreditLimit::construct(salesTable);
    balanceEstimate = custCreditLimit.balanceEstimate();
    if(custCreditLimit.useEstimated())
    {
        creditRemain -= balanceEstimate;
    }
 
    creditRemain += conpeek(salesTotals.displayFieldsCurrency(CustTable.Currency), TradeTotals::posFreeValue());
 
 
    info(strfmt("Verbleibendes Kreditlimit: %1", creditRemain));
Wednesday, December 30, 2009 11:16:18 AM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  | 

 Saturday, October 24, 2009

In Microsoft Dynamics AX beziehen Masken ihre Daten, oder anders gesagt die Daten welche sie anzeigen, über so genannte DataSources.
In einer DataSource sind somit die aktuellen auf der Maske angezeigten Daten (lokal) gespeichert.

Zugriff auf den jeweils aktuell ausgewählten Datensatz erhält man üblicherweise über genau diese DataSource.
Der ausgewählte Datensatz kann unter anderem auch, z.B. durch ein MenuItem(Button), an eine Funktion oder andere Maske übergeben werden.
Hierfür muss nur die Eigenschaft “DataSource”, in diesem Beispiel des MenuItem(Button), entsprechend eingestellt sein.

Ist keine DataSource in den Eigenschaften hinterlegt wird immer die erste DataSource der Query verwendet bzw. dessen aktiver Datensatz übergeben.
In der Regel ist dies die DataSource, welche beim Erstellen der Maske als erstes hinzugefügt oder erstellt wurde (Ausnahme ist hier eine eventuelle Manipulation der Query per Programmcode).

Dieses Vorgehen ist für 90% aller Fälle das wohl am besten geeignete Vorgehen und wird in dieser Weise auch vom Dynamics AX Standard verwendet.
Leider gibt es Anwendungsfälle, bei denen diese “starre Verbindung” von DataSource und z.B. Button oder MenuItem nicht funktioniert, beziehungsweise nicht zum gewünschten Ergebnis führt.

Angenommen man hat eine Maske mit zwei DataSources (CustTable und SalesTable), deren Daten über zwei Grids angezeigt werden, sowie einen Button, welcher eine Operation mit dem zuletzt ausgewählten Datensatz (unabhängig von der DataSource) durchführen soll.
Wenn ein Datensatz der DataSource “CustTable” selektiert wurde, soll dieser verarbeitet werden.
Ist zuletzt ein Datensatz der DataSource “SalesTable” selektiert wurden, soll die Operation mit diesem Datensatz erfolgen.

Maske

Bei dieser Anforderung ergibt sich das Problem, dass die Standardvorgehensweise zur Abfrage des selektierten Datensatzes nicht funktioniert, da hierfür eine der DataSources “direkt” angesprochen werden muss.
Welche DataSource nun aber die “aktive” ist, lässt sich leider nicht ermitteln, da standardmäßig jede DataSource einen “aktiven” Datensatz hat und somit eine Unterscheidung, ob der Aufruf für die “CustTable” oder “SalesTable” erfolgen soll, nicht möglich ist.

Über die Methode “docCursor” der Klasse FormRun bietet sich eine zweite Möglichkeit, den aktiven (ausgewählten) Datensatz zu ermitteln.
Dieses Vorgehen wird z.B. vom Dokumentenmangement (Form “DocuView”) verwendet, um den zuletzt gewählten Datensatz zu ermitteln und somit die dem Datensatz zugeordneten Dokumente anzuzeigen.

Leider scheidet dieser Weg ebenfalls aus, da ein “Click” auf den Button zur Folge hat, dass der jeweils aktive Datensatz der “Button-DataSource” durch die Methode “docCursor” zurück geben wird.
Dies ist soweit auch logisch, da ein Button immer einer DataSource zugeordnet ist (entweder über Angabe in der entsprechenden Eigenschaft des Buttons oder, wenn nicht festgelegt, die erste DataSource der Query).

Wie ist es nun aber möglich, dennoch den zuletzt selektierten (ausgewählten) Datensatz zu ermitteln, wenn die im Standard verwendeten Wege nicht funktionieren?

Um das gewünschte Ziel zu erfüllen (bestimmen, welcher der zuletzt selektierte Datensatz ist) muss eine kleine funktionale Erweiterung der “Info” Klasse durchgeführt werden.

Zuerst müssen in der “classDeclaration” der Klasse “Info” zwei neue Variablen/Buffer zum Speichern des selektierten Datensatzes erstellt werden.

final class Info extends xInfo
{
    #SysTaskRecorderMacro
 
    ObjectIdent         docuView;
    ObjectIdent         lastActivatedForm;
 
    ...
 
    // New code -->     
    Common                  lastSelectedRecord;
    Common                  selectedRecord;
    //New code <--
 
    #Define.CurrentVersion(1)
}
 

Nun müssen noch einige neue Methoden für die Klasse “Info” erstellt werden, damit die Variablen/Buffer geschrieben, abgefragt und gelöscht werden können.

private void setLastSelectedRecord(FormRun _formRun)
{
    ;
    if(_formRun.docCursor())
    {
        if(lastSelectedRecord)
        {
            lastSelectedRecord = selectedRecord;
        }
        else
        {
            //Only get the record data, not the cursor
            lastSelectedRecord = _formRun.docCursor().data();
        }
 
        //Only get the record data, not the cursor
        selectedRecord = _formRun.docCursor().data();
    }
}
 
private void clearLastSelectedRecord()
{
    ;
    lastSelectedRecord.clear();
    selectedRecord.clear();
}
 
common lastSelectedRecord()
{
    ;
    return lastSelectedRecord;
}

 

Zum Schluss müssen diese neu erstellten Methoden noch entsprechend in der Methode “formNotify” aufgerufen werden.

void formNotify(FormRun formRun,FormNotify event)
{
    switch (event)
    {
        case FormNotify::Activate:
            this.activate(formRun);
            if (docu)
                docu.reSearch(formRun);
            //New code -->
            this.setLastSelectedRecord(formRun);
            //New code <--
            break;
        case FormNotify::DeActivate:
            break;
        case FormNotify::Open:
            this.open(formRun);
            if (docu)
                docu.set(formRun);
            break;
        case FormNotify::Close:
            this.close(formRun);
            if (docu)
                docu.clear(formRun);
            //New code -->
            this.clearLastSelectedRecord();
            //New code <--
            break;
        case FormNotify::RecordChange:
            if (docu)
                docu.reSearch(formRun);
            //New code -->
            this.setLastSelectedRecord(formRun);
            //New code <--
 
            if (formRun.isWorkflowEnabled())
            {
                // only refresh controls if current ds equals workflow data source
                if (formRun.objectSet().name() == formRun.workflowDataSource().name())
                    formRun.updateWorkflowControls();
            }
 
            break;
        case FormNotify::NoteClicked:
            if (docu)
                docu.note(formRun);
            break;
    }
}
Durch diese kleine Codeänderung kann nun der zuletzt ausgewählte Datensatz, unabhängig von einer DataSource, abgefragt werden.
void clicked()
{
    Common      currentRecord;
    DictTable   dictTable;
    ;
    super();
    //Get the last selected  record
    currentRecord = infolog.lastSelectedRecord();
 
    dictTable = new DictTable(currentRecord.TableId);
 
    setPrefix(tableid2name(currentRecord.TableId));
    info(strfmt("%1 - %2", currentRecord.(dictTable.titleField1()), currentRecord.(dictTable.titleField2())));
}

Bezogen auf die zuvor beschrieben Anforderung könnte das Ergebnis so aussehen.
 

Maske_CustTable  Maske_SalesTable

Saturday, October 24, 2009 5:45:25 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

 Monday, October 19, 2009

In Microsoft Dynamics AX existiert ein Feature, um Aufgaben (Jobs), welche durch entsprechende Klassen bereit gestellt werden, zu planen und zu einem geplanten Zeitpunkt auszuführen.
Dies wird in Dynamics AX als Stapelverarbeitung (Batch-Framework) bezeichnet.

Jeder Stapelverarbeitungsauftrag verfügt über einen Status, der angibt, in welchem “Zustand” sich der jeweilige Stapelverarbeitungsauftrag befindet.

BatchJobs

Über die Funktion, “Funktionen –> Status ändern” kann dieser Status durch den Benutzer geändert werden.

BatchJobs_statusaendern

Beim Auswählen des “neuen” Status ist leider ein wenig Vorsicht geboten, da bei einem falschen Klick der gesamte Stapelverarbeitungsauftrag unbrauchbar gemacht werden kann.
Drückt man zufällig nicht auf einen der durch die Maske angebotenen Werte, so wird der Status der Stapelverarbeitungsauftrags gelöscht.

BatchJobs_OhneStatus

Das unschöne hierbei ist, dass dieses “Status löschen “nicht mehr rückgängig gemacht werden kann (jedenfalls nicht durch die Dynamics AX Masken).
Bei dem Versuch, wieder einen korrekten Status zu vergeben (ebenfalls über die Funktion “Status ändern”) wird leider nicht der gewünschte Status gesetzt, sondern eine Fehlermeldung ausgegeben.

Fehlermeldung_BatchStatus

Die einzige Möglichkeit, wieder eine korrekten Status zu setzten, besteht leider darin, einen kleinen Job zu schreiben (mit X++), welcher den Status per Programmcode ändert.

Ist gerade kein Entwickler “zur Hand”, besteht nur die Möglichkeit, den Stapelverarbeitungsauftrag zu löschen und erneut anzulegen (dies kann aber von Fall zu Fall sehr aufwändig sein).

Monday, October 19, 2009 7:41:39 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

 Thursday, October 01, 2009

Wie bereits an mehreren Stellen angekündigt, hat Microsoft das HotFix Rollup 3 für Microsoft Dynamics AX 2009 (RTM und SP1) veröffentlicht.

Alle Microsoft Dynamics AX Kunden und Partner können das Hotfix Rollup über das Customer Source bzw. Partner Source beziehen.

HotFix Rollup 3 für Dynamics AX 2009 RTM (KB974407)

HotFix Rollup 3 für Dynamics AX 2009 SP1 (KB974409)

Thursday, October 01, 2009 7:48:49 AM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

 Thursday, September 17, 2009

In mehreren Artikeln wurde bereits beschrieben, wie LookupForms erstellt werden müssen, um alle Funktionen bereit zu stellen, die auch durch einen Standard-Lookup bereit gestellt werden.

Ein guter Artikel ist zum Beispiel auf Axaptapedia zu finden:
http://axaptapedia.com/Lookup_Form

Leider wurde in diesem Artikel auf eine Kleinigkeit nicht hingewiesen, die allerdings für sehr viel Verwirrung sorgen kann.

Um beim Öffnen des Lookups den bereits eingetragenen Wert zu selektieren (in dem Control der aufrufenden Maske), müssen wie in dem Artikel beschrieben, die Methoden „executeQuery“ und „init“ der DataSource der Lookup-Maske überschrieben werden.

Beispiel:

public void executeQuery()
{
    FormStringControl   callerControl   = SysTableLookup::getCallerStringControl(element.args());
    ;
    super();
 
    xyz_ds.findValue(fieldnum(xyz,id),callerControl.text());
}

public void init()
{
    Query                q = new Query();
    QueryBuildDataSource qbds;
    ;
    super();
 
    qbds = q.addDataSource(tablenum(xyz));
    qbds.orderMode(OrderMode::OrderBy);
    qbds.addSortField(fieldNum(xyz,some_other_field));
    this.query(q);
}

Es wird auch beschrieben, dass in der Methode “init” der Datasource eigene Ranges oder Sortings definiert werden können.
Dies ist soweit auch richtig, allerdings mit einer Ausnahme.

Wird auf dem Feld, welches bei dem Aufruf von „Datasource.findValue“ in der Methode „init“ angegeben wurde (sollte auch immer das Feld sein, dessen Wert durch den Lookup ausgewählt wird), eine Range definiert, so funktioniert die Selektion des zuvor gewählten Wertes nicht mehr und es wird immer der erste Wert im Lookup selektiert bzw. ausgewählt.

Beispiel:

public void executeQuery()
{
    FormStringControl   callerControl   = SysTableLookup::getCallerStringControl(element.args());
    ;
    super();
 
    xyz_ds.findValue(fieldnum(xyz,id),callerControl.text());
}

public void init()
{
    Query                q = new Query();
    QueryBuildDataSource qbds;
    QueryBuildRange      range;
    ;
    super();
 
    qbds = q.addDataSource(tablenum(xyz));
    qbds.orderMode(OrderMode::OrderBy);
    qbds.addSortField(fieldNum(xyz,some_other_field));
    range = qbds.addRange(fieldnum(xyz,id));
    range.value(SysQuery::valueNot(<someValue>));
    this.query(q);
}

Dieses Verhalten lässt sich allerdings umgehen, wenn anstelle des Aufrufs von “DataSource.findValue” in der „ExecuteQuery“ Methode der DataSource der Aufruf von „DataSoucre.findRecord“ verwendet wird.
Hierfür muss aber der entsprechende Datensatz des zuvor oder bereits ausgewählten Wertes ermittelt werden um diesen beim Aufruf von „DataSource.findRecord“ als Parameter zu übergeben.

Beispiel:

public void executeQuery()
{
    FormStringControl   callerControl;
    Xyz                 xyzRecord;
    ;
    callerControl = SysTableLookup::getCallerStringControl(element.args());
    xyzRecord = Xyz::find(callerControl.text());
 
    super();
 
    xyz_ds.findRecord(xyzRecord);
}

public void init()
{
    Query                q = new Query();
    QueryBuildDataSource qbds;
    QueryBuildRange      range;
    ; 
    super();
 
    qbds = q.addDataSource(tablenum(xyz));
    qbds.orderMode(OrderMode::OrderBy);
    qbds.addSortField(fieldNum(xyz,some_other_field));
    range = qbds.addRange(fieldnum(xyz,id));
    range.value(SysQuery::valueNot(<someValue>));
    this.query(q);
}

Es muss also darauf geachtet werden, ob eine Einschränkung (Range) auf dem „ID-Feld“ benötigt wird oder nicht.

Wird keine Einschränkung benötigt, kann, wie in dem Artikel auf Axaptapedia beschrieben, mit „DataSource.findValue“ gearbeitet werden um den entsprechenden Datensatz zu selektieren.
Wird aber eine solche Einschränkung benötigt, muss mit „DataSoucre.findRecord“ gearbeitet werden.

Thursday, September 17, 2009 9:48:11 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

 Saturday, September 12, 2009

Wird Microsoft Dynamics AX 2009 mit Windows Server 2008 und SQL Server 2008 installiert, kann es zu einem Problem bei Bereitstellen der ODC-Dateien kommen.

Nach dem Aufruf der Funktion „ODC-Dateien bereitstellen“ meldet Dynamics AX 2009 einen Fehler im Infolog.

ODC_Error

Diese Fehlermeldung wird ebenfalls im Ereignislog von Windows protokolliert.

Laut einem Artikel im EMEA Dynamics AX Support Blog ist hierfür ein Hot Fix erhältlich.
https://blogs.msdn.com/emeadaxsupport/archive/2009/04/23/unable-to-deploy-odc-files-to-enterprise-portal-even-after-installing-hotfix-kb960158.aspx

Wurde allerdings schon das Hot Fix Rollup 2 für Dynamics AX 2009 (SP1) installiert, wird das beschriebene Hot Fix nicht mehr benötigt.

Um nun die ODC-Dateien erfolgreich bereitstellen zu können muss wie folgt vorgegangen werden:

  • Das bereits installierte Enterprise-Portal muss aktualisiert werden
    (Verwaltung/Einstellungen/Internet/Enterprise Portal/Bereitstellungen verwalten/ Button „Aktualisieren“ wählen)
  • Nun (sicherheitshalber) den IIS neu starten, z.B. durch Aufruf von „iisreset“
  • Anschließend können die ODC-Dateien bereit gestellt werden.
    (Verwaltung/Einstellungen/Unternehmensanalyse/OLAP/Olap-Verwaltung/ Button „ODC-Dateien bereitstellen“ wählen)

Ob das Bereitstellen der ODC-Dateien funktioniert hat, kann überprüft werden, indem man kontrolliert, ob die entsprechende Bibliothek im SharePoint die ODC Dateien enthält.
http://<servername>/websites/DynamicsAx/Data%20Connections/Forms/AllItems.aspx

Saturday, September 12, 2009 2:11:03 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  | 

 Thursday, September 03, 2009

Ab sofort führt mein Arbeitgeber, die Firma AX Solutions GmbH, einmal in der Woche (mittwochs 15:30 - 16:30 Uhr) eine kostenlose „Experten-Sprechstunde“ durch.

Diese „Sprechstunde“ richtet sich an Microsoft Dynamics AX Bestandskunden sowie Interessenten die Antworten auf bisher unbeantwortete Fragen oder einen Rat zu einer speziellen Problemstellung suchen.

Unter anderem werde auch ich diese Sprechstunden abhalten und versuchen nach besten Wissen und Gewissen Rat zu geben. Sicherlich wird sich nicht jede Problemstellung Ad-hoc lösen lassen. Ich bin aber sicher, dass der gemeinsame Dialog zumindest Lösungsoptionen aufzeigen wird.

Guter Rat ist Sprichwörtlich teuer. Diesmal aber nicht, denn die Dynamics AX Sprechstunde ist kostenlos.
Auf die sonst übliche Praxisgebühr in Höhe von 10,- EUR pro Quartal wird verzichtet. :-)

Da die Sprechstunden in einem 1:1 Gespräch durchgeführt werden, und möglichst vielen Ratsuchenden die Möglichkeit gegeben werden soll diese zu nutzen, ist jede Sprechstunde auf 60 Minuten begrenzt.

Ratsuchende bzw. Interessierte möchte ich bitte, sich unter folgendem Link: (http://www.ax-solutions.de/kontaktformular.html) mit dem Stichwort „Dynamics AX – Sprechstunde“ anzumelden.

Weitere Informationen können über die  Webseite der AX Solutions GmbH bezogen werden.

Thursday, September 03, 2009 3:06:30 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  | 

 Thursday, July 30, 2009

Wie schon die erste Auflage des Buches „Inside Dynamics AX“ ist dieses Buch eine sehr gute Ergänzung zu den von Microsoft angebotenen Schulungsunterlagen (Development 1-4).

Angefangen bei der Architektur, der Entwicklungsumgebung und –Tools, bis hin zu Code Upgrades beschreibt dieses Buch alle Themen die für einen AX Entwickler von Bedeutung sind.

Nicht nur alle neuen Features von Dynamics AX 2009, z.B. Dynamics AX Reporting Services oder Workflows, sondern auch ältere Features wie z.B. das Application Integration Framework (AIF), werden wesentlich detaillierter beschrieben als an anderen Stellen.

Leider gibt es auch Bereiche, die nicht so detailliert besprochen werden bzw. wo einige Fragen nicht gänzlich beantwortet werden.
Ein Beispiel hierfür ist die .NET Integration. Zwar wird der Business Connector ausreichend beschrieben, aber das Thema CLR-Interoperability wird leider nur sehr knapp behandelt.

Einige Kapitel wurden im Vergleich zu der ersten Auflage des Buches gänzlich überarbeitet.
Beispielhaft sei das Kapitel über Form Customizations genannt, welches komplett neu geschrieben wurde.

Leider hat dies auch zur Folge, dass einige sehr gut Beschriebene Themen, wie Beispielweise das dynamische Anpassungen von Masken mit X++,
jetzt nicht mehr behandelt werden.

Was dieses Buch aber nicht beschreibt oder behandelt, sind die Klassen, Tabellen, API‘s, etc. des Microsoft Dynamics AX Standards.
Dies würde allerdings auch den Rahmen des Buches mehr als sprengen.

In der Gesamtbetrachtung ist die neue Auflage von Inside Microsoft Dynamics AX eins der besten technischen Bücher über Microsoft Dynamics AX.
Kein Buch geht soweit in die Tiefe wie dieses. Egal ob Anfänger oder erfahrener Entwickler, für jeden ist etwas dabei.

Auch wer schon die erste Auflage von Inside Microsoft Dynamics AX gelesen hat, wird viele neue Themen finden.

Thursday, July 30, 2009 1:28:50 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

Wird die Eigenschaft(Property) “AllowEditOnCreate” eines Tabellenfeldes auf den Wert “No” gesetzt, ist es nicht möglich,
Werte für dieses Tabellenfeld über das AIF (Application Integration Framework) zu schreiben (Insert-Operation).

Alle Tabellenfelder, welche diese Eigenschaft auf “No” gesetzt haben, werden durch das AIF automatisch auf deren Default-Wert gesetzt und jegliche Wert der AIF Nachricht werden ignoriert.
Dies hat zur Folge, dass wenn das Tabellenfeld kein Enum ist, das Tabellenfeld immer leer ist.

Da dieser Automatismus schon vor Ausführung der AX<Table> Klasse greift, der Wert also schon beim Ausführen der entsprechenden Parm-Methode “leer” ist,
kann dieses Verhalten ohne Änderung der AIF-Basis Klassen nicht geändert werden.

Thursday, July 30, 2009 12:09:08 AM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  | 

 Wednesday, July 29, 2009

Egal ob Anfänger oder schon erfahrener Dynamics AX Entwickler.
Von Zeit zu Zeit tauchen immer die gleichen Fragen und/oder Problemstellungen auf.

Was immer mal wieder in Foren oder Newsgroups zu lesen ist, ist die Frage, wie die Berechnung des Artikelpreises für eine Debitor mit X++ Code durchgeführt werden kann.
Der Standard von Microsoft Dynamics AX bringt zwar eine Maske zur “Preisabfrage” mit sich, welche diese Problemstellung eigentlich schon löst, allerdings ist es manchmal
notwendig, die Preisberechnung z.B. in einer Klasse durchzuführen, um mit dem Ergebnis weitere Operationen durchführen zu können.

Maske zur Preisberechnung

Da in Dynamics AX Preise für einen Artikel nicht nur “direkt” im Artikelstamm, sondern auch in den Handelsvereinbarungen, hinterlegt werden können, stellt der Standard von
Dynamics AX die Klasse “PriceDisc” zur Verfügung, welche alle Möglichkeiten der Preispflege berücksichtig.
Unter Anderem werden auch Rabatte bzw. Zuschläge der Handelsvereinbarungen durch diese Klasse berücksichtigt.

Die Klasse “PriceDisc” stellt die statische Methode “actualSalesPriceDisc” bereit. Mit dieser können z.B. der Verkaufspreis pro Preiseinheit, der Rabatt oder die Zuschläge
berechnet und/oder ermittelt werden.
Durch eine zweite statische Methode “price2Amount” lässt sich anschließend der Nettobetrag eines Artikels ausrechnen.

Beispiel:

CustTable               custTable = CustTable::find("5010");
InventTable             inventTable = InventTable::find("1000");
Qty                     qty = 1.00;
 
SalesPrice              salesPrice; // Verkaufspreis je Preiseinheit
PriceMarkup             salesMarkup; //Preis sonst. Zuschläge
PriceUnit               priceUnit; // Preiseinheit
SalesLineDisc           salesLineDisc; // Rabatt
SalesLinePercent        salesLinePercent;
DiscPct                 percent1;
DiscPct                 percent2;
PriceDiscTable          actualPrice; // Gefundener Datensatz der Handelsvereinbarung (Verkaufspreis)
PriceDiscTable          actualDisc; // Gefundener Datensatz der Handelsvereinbarung (Rabatte)
AmountCur               lineAmount; // Nettobetrag
;
[salesPrice, salesMarkup, priceUnit,
 salesLineDisc, salesLinePercent, percent1,
 percent2, actualPrice, actualDisc] = PriceDisc::actualSalesPriceDisc(custTable, inventTable, qty);
 
lineAmount = PriceDisc::price2Amount(salesPrice, priceUnit, salesLineDisc, qty,
                                     qty, salesMarkup, salesLinePercent,
                                     custTable.Currency, 0);
Wednesday, July 29, 2009 5:48:40 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  | 

 Thursday, June 18, 2009

Die neue oder zweite Auflage von "Inside Mircosoft Dynamics AX" ist vor wenigen Tage erschienen.

Inside Mircosoft Dynamics AX 2009

Wie auch schon die erste Auflage des Buches, welche auf der Version 4.0 von Microsoft Dynamics AX basierte, ist diese Buch in erster Linie für Entwickler gedacht.
Die aktuelle Auflage umfasst gut 100 Seiten mehr als die erste Auflage und ist leider auch im Preis etwas teurer.

Weitere Informationen über den Inhalt können z.B. auf den Seiten von Amazon entnommen werden.

Es ist zu hoffen, dass sich diese Auflage auf gleichem Level wie die erste Auflage bewegt und somit zu einem "Must-Have" oder "Must-Read" für AX Entwickler wird.

 

Thursday, June 18, 2009 7:56:05 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [1] - Trackback
 |  |  | 

 Saturday, April 04, 2009

In dem Artikel "Anzeige von Lagerdimensionen auf Masken" wurde bereits gezeigt, was zu tun ist, um die Anzeige von Lagerdimensionen auf Masken dynamisch anpassen zu können bzw. das Standardverhalten für die Anzeige von Lagerdimensionen zu implementieren.

Manchmal soll eine ähnliche Funktionalität auch für Berichte bereit gestellt werden, um zum Beispiel vor der Berichtserstellung auswählen zu können, welche Lagerdimensionen auf dem Bericht(Report) angedruckt werden.
Auch hierfür sind im Dynamics AX Standard die entsprechenden Funktionalitäten (oder besser Klassen) bereits vorhanden, sodass diese nur verwendet werden müssen.

Als Ausgangsbasis für den Bericht dient ebenfalls die Tabelle „AKU_DemoTable“.

Der Bericht soll nun, die in dieser Tabelle gespeicherten Datensätze andrucken/ausgeben.

Wie bei Masken, muss auch für einen Bericht, die Query entsprechend um die Tabelle InventDim ergänzt werden.
Hierbei ist zu beachten, dass die Eigenschaften (Properties) „FetchMode“ auf „1:1“ und „Relations“ auf „Yes“ gesetzt werden.

Als nächstes muss nun, ebenfalls analog zu dem Vorgehen bei Masken, die Feldgruppe „InventoryDimensions“ in den Designzweig des Reports aufgenommen werden.
Beispielhaft wird diese in einem Body-Element erstellt.

Nun müssen noch einige Anpassungen an den Methoden des Berichts durchgeführt werden, damit das gewünschte Ergebnis erreicht werden kann.
Bezogen auf die Möglichkeit, die zu druckenden Lagerdimensionen bestimmen zu können, müssen die Methoden „classDeclaration“, „run“, „dialog“ und „getFromDialog“ wie folgt überschrieben werden.
Auch das Überschreiben der Methoden „pack“ und „unpack“ ist hilfreich (für die Lagerdimensionsanzeige nicht zwingend erforderlich), da über diese die Speicherung der „Nutzungsdaten“ realisiert wird.

public class ReportRun extends ObjectRun
{
   InventDimParm inventDimParm;
   DialogRunbase dialog;
   DialogGroup dialogInventoryDimensions;

   #define.CurrentVersion(1)
   #localmacro.CurrentList
      inventDimParm
   #endmacro
}

void updateDesign()
{
   ;
   InventDimCtrl::updateReportVisible(element, inventDimParm);
}

public void run()
{
   ;
   this.updateDesign();
   super();
}

public Object dialog(Object _dialog)
{
   ;
   dialog = _dialog;
   dialogInventoryDimensions = inventDimParm.addFieldsToDialog(dialog,"@SYS53654",true, false, "@SYS102592");
   return dialog;
}

public boolean getFromDialog()
{
   ;
   inventDimParm.getFromDialog(dialog, dialogInventoryDimensions);
   return true;
}

public container pack()
{
   return [#CurrentVersion, #CurrentList];
}

public boolean unpack(container packedClass)
{
   Version version = RunBase::getVersion(packedClass);
   ;
   switch(version)
   {
      case #CurrentVersion:
         [version,#CurrentList] = packedClass;
         break;
      default:
         return false;
   }
   return true;
}

Wird nun der Bericht geöffnet, zum Beispiel über ein MenuItem, kann in einem Dialog ausgewählt werden, welche Lagerdimensionen auf dem Report angedruckt werden sollen.

Der ausgegebene Bericht(Report) sieht, unter Berücksichtigung der im Dialog gewählten Einstellungen, wie folgt aus.

Das vorgestellt Bespiel steht hier als Download bereit um die einzelnen Schritte genau ansehen/nachvollziehen zu können.

AKU_Demo_InventDimRep.rar (1,99 KB)
Saturday, April 04, 2009 6:24:53 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  |  | 

 Wednesday, March 11, 2009

Leider scheint es ein Problem mit duplizierten Tabellen in Dynamics AX 2009 zu geben.
Unter gewissen "Umständen" ist es möglich, Quelltext der in einem andern Layer (z.B. CUS oder BUS) geschrieben wurde, in den SYS Layer zu "verschieben".

Dieser Verhalten ist sehr unschön, da viele Entwickler, z.B. für Testzwecke, mal ein Objekt duplizieren, neuen Quelltext testen und anschließen diesen auf das orginale Objekt übernehmen oder verschieben. Leider taucht genau an dieser Stelle das Problem auf (es kann sein, dass dieses "Verschieben" den Quelltext in den SYS Layer schreibt).

Das genaue Verhalten ist in einem Video von "elranu" auf YouTube beschrieben (Link zum Video):

In den Newsgroups ist auch ein entsprechender Thread zu finden: Ax 2009 bug in Sys Layer

Wednesday, March 11, 2009 8:22:18 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  | 

 Friday, February 06, 2009

Wer schon mit dem AIF in der Version 4.0 von Microsoft Dynamics AX gearbeitet hat wird sich daran erinnern, dass ein Debuggen des Quellcodes, welcher durch das AIF ausgeführt wird, nur möglich ist, wenn hierfür eine kleine Codeanpassung in den Klassen "AifInboundProcessingService" und "AifOutboundProcessingService" vorgenommen wird.
Das genaue Vorgehen für die Version 4.0 von Dynamics AX ist in diesem Artikel beschrieben.

Für Dynamics AX 2009 kann diese Quellcodeänderung allerdings nicht so ohne weiteres angewendet werden, da für Dynamics AX 2009 einige Features ergänzt wurden (z.B. paralelle Verarbeitung von AIF Nachrichten) und somit der Quellcode der beiden Klassen einige Abweichungen zu dem der Version 4.0 hat.

Debuggen von ausgehenden Nachrichten

Um das Debuggen von ausgehenden Nachrichten zu ermöglichen, muss die Methode "runAsWrapper" der Klasse "AifOutboundProcessingService" angepasst werden.
Der Aufruf von "runAS" (Zeile 22) muss durch "AifOutboundProcessingService::processAsUser(messageIdContainer)" ersetzt werden.

...
try

{
   // runAs currentUser and process all messages in the container.
   new RunAsPermission(runAsUserId).assert();

   // AKU, Enable Debuging for outbound messages - START -->
   // Do not use in production system!!!
   // BP deviation documented
   //runas(runAsUserId,
   // classnum(AifOutboundProcessingService),
   // staticmethodstr(AifOutboundProcessingService, processAsUser),
   // messageIdContainer,
   // runAsCompany);
   AifOutboundProcessingService::processAsUser(messageIdContainer);
   // AKU, Enable Debuging for ourbound message - END -->

   // Revert the permission
   CodeAccessPermission::revertAssert();
}
...

Debuggen von eingehenden Nachrichten

Um das Debuggen von eingehenden Nachrichten zu ermöglichen, muss die Methode "runAsWrapper" der Klasse "AifInboundProcessingService" angepasst werden.
Der Aufruf von "runAS" (Teile 24) muss durch "AifInboundProcessingService::processAsUser(messageIdContainer)" ersetzt werden.

...
try
{
   // Convert to Axapta UserId
   axaptaUserId = AifEndpointUser::getAxaptaUser(runAsUserId).Id;

   new RunAsPermission(axaptaUserId).assert();

   // AKU, Enable Debuging - START -->
   // Do not use in production system!!!
   // BP deviation documented
   //runas(axaptaUserId,
   // classnum(AifInboundProcessingService),
   // staticmethodstr(AifInboundProcessingService, processAsUser),
   // messageIdContainer);
   AifInboundProcessingService::processAsUser(messageIdContainer);
   // AKU, Enable Debuging - END -->

   CodeAccessPermission::revertAssert();
}
...

Für beide Quellcodeänderung sollte noch erwähnt werden, dass diese in einem Produktivsystem nicht durchgeführt werden sollten, da dies Auswirkungen auf die Verarbeitung der Stapelprozesse des AIF's haben könnte.

Friday, February 06, 2009 11:29:08 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  | 

 Tuesday, January 27, 2009

Das Application Integration Framework von Dynamics AX basiert auf Dokumenten (Axd<Document> Klassen).
Eigene Dokumente lassen sich reicht einfach per Hand oder mit Hilfe des Dokumenten-Wizards erstellen.
In der Version 2009 von Dynamics AX erstellt dieser Wizard auch gleichzeitig den benötigenten Service (WCF-Webservice) und andere benötigte Elemente wie (Serviceklassen und Macros).
Welche Schritte hierfür benötigt werden ist zum Beispiel im Microsoft Dynamics AX Developer Center beschrieben.

Ein kleines Problem entsteht, wenn das neue Dokument, genauer gesagt die Elemente oder Objekte des Dokuments, in einem Layer (zum Beispiel CUS-Layer) entwickelt wird und später, aus welchen Gründen auch immer, alle Objekte des Dokuments (Query, Ax<Table> Class, Axd<Document> Class) in einen anderen Layer (zum Beispiel VAR-Layer) verschoben werden.

Nach der "Verschiebung" des neuen Dokuments in einen anderen Entwicklungslayer werden durch die AIF-Konfigurationsmasken (siehe Maske Dienstleistungen) allerdings keine Operationen (Insert, Update, Delete, FindKey, etc.) mehr angezeigt.
Auch an anderen Stellen, wie zum Beispiel der Endpunktkonfiguration, sind die entsprechenden Operationen nicht mehr auswählbar oder vorhanden.

Der Grund hierfür liegt in der ClassId der Serviceklasse des neuen Dokuments. Dieser, wie auch jedem anderen Objekt, wird beim Import in einen anderen Layer eine neue ID zugewiesen, wenn nicht explizit angegeben wurde, dass der Export und Import mit ID's erfolgen soll. So kann es sein, dass die Klasse mit der ID 40001 nach dem Import in einen anderen Layer die ID 300001 zugeordnet ist.

Da wärend der Konfiguration des AIF's der AOT nach Dokumenten/Services durchsucht wird und für jedes Dokument bzw. jeden Service ein Datensatz in der Tabelle "AifService" sowie ein bis mehrere Datensätze in der Tabelle "AifAction" erzeugt wird, welche alle eine Referenz auf die ClassId der Serviceklasse enthalten, kommt es jetzt zu dem genannten Problem.
Der Id 40001 ist nun keine Klasse oder noch schlimmer eine andere Klasse zugewiesen. Auch eine "Aktualisierung" dieser Datensätze über die Aktualisierungsfunktion, welche auf der Maske Dienstleistungen bereit gestellt wird, führt nicht zum gewünschten Erfolg.

Genau in dieser Funktion scheint sich ein kleiner Fehler eingeschlichen zu haben. Dort wird zwar eine Aktulisierung der ClassId für die Datensätze der Tabelle "AifService", aber nicht für die Datensätze der Tabelle "AifAction", durchgeführt.

Um diese Verhalten zu reproduzieren, muss folgendes gemacht werden:

  1. Erstellen eines neuen AIF Dokuments bzw. AIF Services.
  2. Über die Maske Dienstleistungen, zu finden unter "Grundeinstellungen -> Einstellungen -> Application Integration Framework -> Dienstleistungen", Funktion "Aktualisieren" das neue Dokument / den neuen Service "aktivieren".
  3. Über den Button "Servicearbeitsgänge" können nun alle Operationen welche durch das Dokument / den Service bereit gestellt werden eingesehen werden.
  4. Verschieben aller Elemente des Dokuments / des Services in einen anderen Layer.
  5. Schritt 2 erneut druchführen.
  6. Über den Button "Servicearbeitsgänge" werden keine Operation mehr angezeigt.

Um dieses Problem zu lösen, gibt 2 Möglichkeiten.

Entweder manuelles Löschen alle zu diesem Dokument/Service gehörigen Datensätze der Tabelle "AifAction" oder aber man ergänzt die Methode "registerOperations" der Klasse "AifServiceGenerationManager" um ein wenig X++ Code (bei Zeile 43) welcher nicht nur den Namen der Operation aktualisiert sondern auch die ClassId.
Da dieser Code sehr einfach ist verzichte ich an dieser Stelle auf ein Beispiel.

Leider tritt dieses Problem auch mit Dynamics AX 2009 Service Pack 1 auf.

Tuesday, January 27, 2009 7:46:19 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  |  |  |  |  | 

 Saturday, August 30, 2008

Alle frei erhältlichen Dokumente über Microsoft Dynamixs AX 2009 stehen jetzt über Windows Live SkyDrive für jedermann frei zur Verfügung.

Vielen Dank an Arijit Basu, der sich die Arbeit gemacht hat, diese dort zum downlaod zur Verfügung zu stelllen. 

Mehr Informationen hierzu gibt es im Blog von Arijit Basu.

Hier der "direkt" Link zu den Dokumenten: AX 2009 Documents

Saturday, August 30, 2008 1:53:37 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 |  | 



Translate
Über/Kontakt

     







© Copyright 2012 Axel Kühn
Sign In
Subscribe this blog
Blogroll
 Arijit Basu
 Axapta Blog
Blog around Microsoft Business Solutions Axapta by Helmut Wimmer
 BlaBlubBlog
Der Blog von Kai Gloth
 Dave Bowles
 Dick Wenning
Ax(apta) start pages
 Fred Shen
 Harish Mohanbabu
 jinx´s AX Blog
Everything about Microsoft Dynamcis AX
 Lars Keller
All about .NET, VSTS, VSTO and more
 Max Belugin
 TaReMoTi Blog
Der Blog von Karsten Döring
Archiv
<February 2012>
SunMonTueWedThuFriSat
2930311234
567891011
12131415161718
19202122232425
26272829123
45678910
Statistik
Gesamte Posts: 137
Dieses Jahr: 0
Dieser Monat: 0
Diese Woche: 0
Kommentare: 49





All Content © 2012, Axel Kühn
DasBlog theme 'Business' created by Christoph De Baene (delarou)