martedì 12 marzo 2024

AX 2012 - Job per scrivere i metodi di una classe in un unico file di testo

Questo semplice job che ho scritto può essere di aiuto quando si deve migrare il codice di una classe da ax 2012 a D365FFO. 

Come sappiamo in ax 2012 il sorgente di una classe viene mostrato "spezzettato" per singolo metodo cioè:



Mentre in D365 il sorgente è contenuto in un'unica schermata. Dobbiamo quindi aprire ciascun metodo ed incollarlo in Visual Studio.

Se la classe contiene molti metodi l'operazione può risultare pesante.

Il job prende come input il nome della classe e produce un unico file .txt con tutti i metodi, pronti per essere incollati in Visual studio:

 static void LIL_Ax2012To365ClassesSourceCode(Args _args)  
 {  
   SysDictClass      dictClass;  
   SysDictMethod      dictMethod;  
   int           i;  
   Source         Source;  
   TextIo         textIo;  
   Set           methodsSet;  
   SetEnumerator      methodsSetEnum;  
   str           filePath;  
   ClassId         classId;  
   str           addSpaceToNewLineRegex;  
   container        conFilter = ["TXT", "*.txt"];  
   #File  
   #xppTexts  
   
   classId = classNum(Tutorial_RunbaseBatch);  
   
   dictClass = new SysDictClass(classId);  
   
   for (i=1;i<=dictClass.objectMethodCnt();i++)  
   {  
     dictMethod = new SysDictMethod(UtilElementType::ClassInstanceMethod, dictClass.id(), dictClass.objectMethod(i));  
   
     if(dictMethod.name() == identifierStr(classDeclaration))  
     {  
       Source += dictMethod.getSource() + #newline;  
   
       Source = strRem(Source, "}");  
     }  
   }  
   
   methodsSet = dictClass.methods(true,true,false);  
   
   methodsSetEnum = methodsSet.getEnumerator();  
   
   methodsSetEnum.reset();  
   
   while(methodsSetEnum.moveNext())  
   {  
     dictMethod = methodsSetEnum.current();  
   
     Source += dictMethod.getSource();  
   
     Source += #newline + #newline;  
   }  
   
   source += "}";  
   
   filePath = WinAPI::getSaveFileName(  
     0,  
     conFilter,  
     "",  
     "@SYS56237",  
     ".txt",  
     dictClass.name());  
   
   textIo = new TextIo(filePath, #IO_WRITE);  
   
   textIo.write(Source);  
 }  

venerdì 24 novembre 2023

D365FFO - How to release to warehouse cross docking works even in case of warehouse "full reservation" release method

 In case in the warehouse setup we have the following parameters set 

-    Requirement for inventory reservation: Require full reservation

   Insufficient quantity response: Generate an error




It is still possible to release a sales order to warehouse even if there is no stock but at least a purchase order is marked against a sales order.

The crossdocking template is set in the following way:

-    Demand requirements set as Marking

-    Work template defined in the work template

-    Supply source defined as "Purchase order"

-    Location type: Location directives




We create the sales order as follows.


We create the purchase order directly from the sales order as follows.


In order to release the sales order to the warehouse it is required to launch the batch procedure as follows:

-    The value "Quantity to release": Reserved physically and cross dock



The sales order generates the following shipment.




The shipment has a planned cross docking defined as follows.

Instead if we use the fulfillment policy on the sales order header blocking the release to warehouse in case of missing items in the stock, the system is not able to skip the planned cross docking lines.


giovedì 23 novembre 2023

D365FFO - How to create a transfer order using the warehouse mobile app

Using the warehouse mobile app it is possible to generate a transfer order declaring the warehouse to we want to ship the goods and the license plate we want to move.

Before performing the transaction we have to set the following menu item.









Then we have to make sure that we have in stock (in the starting warehouse) the license plate that we want to move.



Then we access, in the warehouse mobile app, the menu item previously created.


We scan the destination warehouse.


We scan the license plate we want to move.


Once we complete the transfer order we can click on the button complete order underlined above.

Then we have to run the procedure "Process warehouse app events".

The transfer order will be then generated as follows.





martedì 21 novembre 2023

D365FFO - Register material consumption using warehouse mobile device

It is now possible to register the material consumption for production orders directly using the warehouse mobile device. In order to do it, the following feature "Register material consumption on the mobile app on a scale unit" must be switched on.



We create a production order, not setting any location directive for picking components or wave / work templates. At the end we release the production order not generating any work for picking goods, as reported below.


Then we can start the production order using the mobile app or the feature on the production order form (ensuring to not consume components while doing it).


After that we can access the warehouse mobile app feature related to the following menu item defined into the parameters.


Using the mobile app it is possible to generate automatically a picking list journal directly related to the production order.


After scanning the production order, it is required to scan the picking location as reported below.


Then it is required to scan all the other information:

- itemid

- batchid (if needed)

- serial number (if needed)

and to confirm the quantity to be picked as shown below.


Once completed the picking procedure the button Done must be clicked as reported below.


After clicking on the Done the system generates a picking journal posted against the production order scanned during the procedure.



lunedì 20 novembre 2023

D365FFO - How to backflush components using RAF from warehouse mobile device

In the warehouse mobile device app there is the option to report as finish (RAF from now on) the production orders using for instance the following setup in the menu item for mobile app.

While reporting as finished a production order it can be needed to consume all the production order components (backflush of components).

In order to do it the following parameters (Respect flushing principle during overpicking set to YES) must be set in the report as finished warehouse app menu item.


And the following must be set in the production order defaults form under the tab "Report as finished":

-    Picking list journal name

   Automatic bom consumption set to Flushing principle










Then on the Bom lines that we want to consume at the report as finished of the production orders we have to set as flushing principle "Finish" (as reported below).


Once we are reporting as finished a production order using the warehouse mobile app (as below)



We get the following journals posted on the production order related.


A picking list journal for consuming the components of the production order and a report as finished journal for producing the final item.




martedì 24 ottobre 2023

D365 FFO - Creare una form di selezione / deselezione

In questo post vediamo come creare una form selezione / deselezione, come quelle che c'è sui batch group per esempio:



La form inoltre avrà anche un campo per poter filtrare il nome degli elementi

Per creare queste form AX mette disposizione l'oggetto "SysListPanelRelationTableCallback"

Allo scopo occorrono 3 Tabelle:

1) Anagrafica dei gruppi (LILGroupTable)

2) Anagrafica degli elementi che assegneremo di volta in volta ai gruppi (LILElementTable)

3) Tabella che contiene il legame (LILGroupElementTable), che sarà interemente gestita tramite l'oggetto "SysListPanelRelationTableCallback"

Ogni elemento inoltre potrà appartenere contemporaneamente a più di un gruppo alla volta.

Le tre tabelle saranno così fatte:




                                                 


Definiamo a questo punto una nuova form con il seguente design:



E che conterrà il seguente codice:
 [Form]  
 class LILGroupElementInfo extends FormRun  
 {  
   SysListPanelRelationTableCallback sysListPanel;  
   Num                groupId;  
   Description            elementNameFilter;  
   
   public void close()  
   {  
     sysListPanel.finalize();  
     super();  
   }  
   
   void init()  
   {  
     #ResAppl  
   
     sysListPanel = SysListPanelRelationTableCallback::newForm(element,  
                element.controlId(formControlStr(LILGroupElementInfo, ListPanelGroup)),  
                "@SYS113180", "@SYS78228", #ImageUser,  
                tablenum(LILGroupElementTable),  
                fieldnum(LILGroupElementTable, ElementId),  
                fieldnum(LILGroupElementTable, GroupId),  
                tablenum(LILElementTable),  
                fieldnum(LILElementTable, ElementId),  
                [fieldnum(LILElementTable, ElementId), fieldnum(LILGroupTable, Description)],  
                fieldNum(LILElementTable, Description), '',  
                identifierstr(Validate), '', '');  
     sysListPanel.parmDataRangeAsFilter(true);  
   
     super();  
   
     sysListPanel.init();  
   }  
   
   public edit str elementNameFilter(boolean _set, str _elementName)  
   {  
     if (_set && _elementName != elementNameFilter)  
     {  
       elementNameFilter = _elementName;  
     }  
   
     return elementNameFilter;  
   }  
   
   void tabChanged(int fromTab, int toTab)  
   {  
     #define.TabGroups(1)  
     #define.TabGroupElement(2)  
   
     groupId = LILGroupTable.GroupID;  
     switch (toTab)  
     {  
       case #TabGroups:  
         break;  
       case #TabGroupElement:  
         sysListPanel.parmDataRangeValue(elementNameFilter ? elementNameFilter : '*');  
         sysListPanel.parmRelationRangeValue(LILGroupTable.GroupID);  
         sysListPanel.parmRelationRangeRecId(LILGroupTable.RecId);  
         sysListPanel.fill();  
         break;  
     }  
   }  
   
   boolean validate(userId _userId, AddRemove _addRemove, SysListPanelRelationTable _listViewHandler)  
   {  
     return true;  
   }  
   
   [DataSource]  
   class LILGroupTable  
   {  
     public boolean validateWrite()  
     {  
       boolean ret;  
   
       str leftTrimedGroupId;  
       str rightTrimedGroupId;  
    
       leftTrimedGroupId = strltrim(LILGroupTable.GroupID);  
       rightTrimedGroupId = strrtrim(leftTrimedGroupId);  
   
       LILGroupTable.GroupID = rightTrimedGroupId;  
   
       ret = super();  
   
       return ret;  
     }  
   
     int active()  
     {  
       int ret;  
   
       ret = super();  
       element.elementNameFilter(true, '');  
       element.tabChanged(0, tab.tab());  
       return ret;  
     }  
   
   }  
   
   [Control("Tab")]  
   class Tab  
   {  
     void tabChanged(int fromTab, int toTab)  
     {  
       element.tabChanged(fromTab, toTab);  
       super(fromTab, toTab);  
     }  
   
   }  
   
   [Control("TabPage")]  
   class Groups  
   {  
     public boolean allowPageDeactivate()  
     {  
       boolean ret;  
   
       ret = super();  
   
       if (!LILGroupTable.RecId)  
       {  
         if (LILGroupTable_ds.validateWrite())  
           LILGroupTable.write();  
         else  
           return false;  
       }  
   
       return ret;  
     }  
   
   }  
   
   [Control("Button")]  
   class FilterButton  
   {  
     public void clicked()  
     {  
       super();  
   
       sysListPanel.parmDataRangeValue(elementNameFilter ? elementNameFilter : '*');  
       sysListPanel.fill(true);  
     }  
   
   }  
   
 }  

Il risultato sarà il seguente:





Se vogliamo creare una form di selezione / deselezione più semplice con un solo tab, l'oggetto da usare è il SysListPanel. Supponiamo di voler aggiungere ai parametri fornitori una tab dove poter selezionare / deselezionare le delivery mode:
 

Occorre:

1) Creare una tabella che conterrà i valori selezionati che chiameremo LILDlvModeAssociation così fatta:


2) Estendere la classe sysPanel così:

 public final class LILSysListPanel_VendDlvModeAssociation extends SysListPanel  
 {  
   public void addData(container _data)  
   {  
     int                 i;  
     LILDlvModeAssociation        relation;  
     DlvMode               dlvMode;  
     
     for (i = 1; i <= conlen(_data); i++)  
     {  
       relation.DlvModeRecId       = conpeek(_data, i);  
       relation.DlvModeId         = DlvMode::findRecId(relation.DlvModeRecId).Code;  
     
       relation.insert();  
      }  
   }  
   
   public container getData()  
   {  
     container                selectedData;  
     container                availableData;  
     LILDlvModeAssociation          relation;  
     DlvMode                 dlvMode;  
     
     while select TableId from relation  
       join RecId, Code from dlvMode  
       where dlvMode.RecId == relation.DlvModeRecId  
     {  
       selectedData += [[dlvMode.RecId, dlvMode.Code]];  
     }  
     
     while select RecId, Code from dlvMode  
       order by Code  
       notexists join relation  
         where relation.DlvModeRecId == dlvMode.recId   
     {  
       availableData += [[dlvMode.RecId,dlvMode.Code]];  
     }  
     
     return [availableData, selectedData];  
   }  
   
   public int numOfColumns()  
   {  
     return 2;  
   }  
   
   public void removeData(container _data)  
   {  
     int                 i;  
     RefRecId      dlvModeRecId;  
     LILDlvModeAssociation relation;  
     
     for (i = 1; i <= conlen(_data); i++)  
     {  
       ttsBegin;  
       dlvModeRecId = conpeek(_data, i);  
       delete_from relation  
         where relation.DlvModeRecId    == dlvModeRecId;  
       ttsCommit;  
     }  
   }  
   
   public static LILSysListPanel_VendDlvModeAssociation construct()  
   {  
     return new LILSysListPanel_VendDlvModeAssociation();  
   }  
   
   internal static SysListPanel newForm(FormRun _formRun,  
                     int   _parentId)  
   {  
     LILSysListPanel_VendDlvModeAssociation  sysListPanel = LILSysListPanel_VendDlvModeAssociation::construct();  
     
     sysListPanel.parmFormRun(_formRun);  
     sysListPanel.parmParentId(_parentId);  
     sysListPanel.parmCaptionSelected("@SYS113180");  
     sysListPanel.parmCaptionAvailable("@SYS179894");  
     sysListPanel.parmHasAddAllButton(true);  
     sysListPanel.parmHasRemoveAllButton(true);  
     sysListPanel.parmHasUpDownButton(true);  
     sysListPanel.build();  
     
     return sysListPanel;  
   }  
   
   protected FormListSort sortMethod()  
   {  
     return FormListSort::NoSort;  
   }  
   
 }  
3) Create l'estensione della VendParameters aggiungendo un nuovo tab. Il nuovo tab và laciato vuoto perchè sarà poi gestito dalla classe che abbiamo creato al passo 2


4)Fare la CoC sulla form VendParameters:

 [ExtensionOf(formStr(VendParameters))]  
 final class LILVendParameters_Form_Extension  
 {  
   public LILSysListPanel_VendDlvModeAssociation    sysListPanelDlvMode;  
   
   public LILSysListPanel_VendDlvModeAssociation parmSysListPanel(LILSysListPanel_VendDlvModeAssociation _sysListPanelDlvMode = sysListPanelDlvMode)  
   {  
     sysListPanelDlvMode = _sysListPanelDlvMode;  
     return sysListPanelDlvMode;  
   }  
   
   public void init()  
   {  
     next init();  
   
     this.initListPanels();  
   }  
   
   protected void initListPanels()  
   {  
     sysListPanelDlvMode.init();  
     sysListPanelDlvMode.fill();  
   }  
   
 }  

NB: Se stiamo aggiunendo il controllo in extension come in questo caso, come riportato quì:

L'inizializzazione della classe LILSysListPanel_VendDlvModeAssociation và fatta sottoscrivendo l'evento "Initializing" altrimenti il controllo non viene visualizzato correttamente nella form. Questo vale anche per "SysListPanelRelationTableCallback" usato in precedenza. Si veda come esempio la classe standard  SimulationJournalGroupForm_ApplicationSuite_Extension

5) Creiamo la classe EventHandler:

 class LILVendParametersFormEH  
 {  
   [FormEventHandler(formStr(VendParameters), FormEventType::Initializing)]  
   public static void VendParameters_OnInitializing(xFormRun sender, FormEventArgs e)  
   {  
     FormRun fr = sender as FormRun;  
     LILSysListPanel_VendDlvModeAssociation sysListPanelDlvMode = LILSysListPanel_VendDlvModeAssociation::newForm(fr, fr.controlId(formControlStr(VendParameters, LILDlvMode)));  
          
     fr.parmSysListPanel(sysListPanelDlvMode);  
   }  
 }  

mercoledì 27 settembre 2023

D365FFO - Messaggio di errore: "Stopped (error): X++ Exception: The text associated with this work item cannot be found in the assignee’s language"


Eseguire i seguenti passaggi:

  1. Richiamare il Workflow;
  2. Andare alla versione attiva del flusso e controllare che la configurazione del flusso di lavoro disponga delle traduzioni nelle lingue utilizzate dagli utenti come lingue predefinite;
  3. Controllare che la lingua predefinita degli utenti del flusso sia la stessa in uso dal workflow.