martedì 12 novembre 2013

AX 2012 - stampa report su PDF

Questo job stampa il report lettere di sollecito (CustCollectionJour) SSRS su file pdf. Il job è preso da quì apportando qualche piccola modifica.

 static void printCollecionLetterPDF(Args _args)  
 {  
   Args                args = new Args();  
   SRSPrintDestinationSettings     printJobSettings = new SRSPrintDestinationSettings();  
   CustCollectionJourController    custCollectionJourController;  
   CustCollectionJourContract     custCollectionJourContract;  
   SrsReportRunImpl          srsReportRun;  
   SrsPrintMgmtExecutionInfo      executionInfo = new SrsPrintMgmtExecutionInfo();  
   FileIOPermission          fileIOPermission;  
   CustCollectionLetterJour      custCollectionLetterJour;  
   Filename              fileName = @'C:\temp\custCollectionLetter.pdf';  
   ;  
   select firstOnly custCollectionLetterJour;  
   args.record(custCollectionLetterJour);  
   CustCollectionJourController = new CustCollectionJourController();  
   CustCollectionJourController.parmReportName(ssrsReportStr(CustCollectionJour, Report));  
   CustCollectionJourContract = CustCollectionJourController.parmReportContract().parmRdpContract();  
   CustCollectionJourContract.parmRecordId(custCollectionLetterJour.RecId);  
   CustCollectionJourController.parmArgs(args);  
   srsReportRun = CustCollectionJourController.parmReportRun() as SrsReportRunImpl;  
   CustCollectionJourController.parmReportRun(srsReportRun);  
   CustCollectionJourController.parmReportContract().parmPrintSettings().printMediumType(SRSPrintMediumType::File);  
   CustCollectionJourController.parmReportContract().parmPrintSettings().overwriteFile(true);  
   CustCollectionJourController.parmReportContract().parmPrintSettings().fileFormat(SRSReportFileFormat::PDF);  
   fileIOPermission = new FileIOPermission(fileName, 'rw');  
   fileIOPermission.assert();  
   CustCollectionJourController.parmReportContract().parmPrintSettings().fileName(fileName);
   //la riga di codice sottostante salva il report nei file temporanei dell'utente, non ho ben capito a cosa serve
   //ma sembra sia obbligatorio altrimenti la stampa solleva un errore
   executionInfo.parmOriginalDestinationFileName(WinApi::getTempPath()+conPeek(Global::fileNameSplit(fileName),2)+".pdf");  
   CustCollectionJourController.parmReportContract().parmReportExecutionInfo(executionInfo);  
   CustCollectionJourController.runReport();

   CodeAccessPermission::revertAssert(); 
 }  

lunedì 14 ottobre 2013

AX 2012 - Form di lookup che ritornano più valori

Per far sì che un form di lookup restituisca più di un valore al chiamante dobbiamo fare l'override del metodo closeSelect() nel form di lookup.

Allo scopo creiamo una tabella (e relativa form) principale che chiameremo "LILMainTable" con i campi codice e descrizione. Creiamo poi una tabella  (e relariva form) per la lookup che chiameremo  "LILLookUpTable" aventi sempre due campi codice e descrizione.

Facciamo l'override del metodo lookup sul campo del datasource "LILMainTable" così:

 public void lookup(FormControl _formControl, str _filterStr)  
 {  
   Args args = new Args();  
   FormRun lookUpForm;  
   ;  
     
   args.name(formstr(LILLookUpTable));  
   args.caller(element);  
   lookUpForm = new FormRun(args);  
   lookUpForm.init();  
   this.performFormLookup(lookUpForm, _formControl);  
 }  

A questo punto nella form di lookup  "LILLookUpTable" facciamo l'override del metodo closeSelect:

 public void closeSelect(str _selectString)  
 {  
    LILMainTable    mainTable;
    FormRun formRun;
    
    super(_selectString);
    
    formRun = element.args().caller();

    //recupero il buffer in cui sono posizionato al momento della chiamata al form di lookup  
    mainTable = formRun.dataSource().cursor();
   
   
    mainTable.Code   		   = LILLookUpTable.Code; //LILLookUpTable è il nome del datasource della form di lookup contenente i valori correnti selezionati
    mainTable.Description      = LILLookUpTable.Description;
 
    //refresh del datasource di origine per vedere i valori aggiornati  
    formRun.dataSource().refresh();  
 }  

lunedì 23 settembre 2013

AX 2012 - Jobs

In questo post pubblico due job che ho scritto per un cliente e che possono risultare utili:

Il primo job prende come input il nome di una tabella e salva su file CSV la lista dei campi e le principali prorietà ("Field Name","Label","Help text", "Base type", "EDT") più lunghezza della stringa se il campo è di tipo testo:

 static void CRTableInfoExport(Args _args)  
 {  
   int     i,fieldId;  
   DictTable  DictTable;  
   DictField  DictField;  
   TableId   TableId;  
   str     strFieldName;  
   Commaio   file;  
   container  line,header;  
   str     filename;  
   #File  
   ;  
   TableId = tableNum(CustTable); //inserici il nome della tabella  
   DictTable = new DictTable(TableId);  
   filename = @'C:\Temp\'+tableId2name(TableId)+'.csv'; //percorso  
   file = new Commaio(filename , 'W');  
   file.outFieldDelimiter(';');  
   if(DictTable)  
   {  
     header = ["Field Name","Label","Help text", "Base type", "EDT", "String lenght"];  
     file.writeExp(header);  
     for (i=1; i <= DictTable.fieldCnt(); i++)  
     {  
       fieldId     = DictTable.fieldCnt2Id(i);  
       DictField    = new DictField(TableId, fieldId);  
       strFieldName  = (DictField ? DictField.name() : "");  
       line = [strFieldName,DictField.label(),DictField.help(),strFmt("%1",DictField.baseType())];  
       if(extendedTypeId2name(DictField.typeId()))  
       {  
         line    += extendedTypeId2name(DictField.typeId());  
       }  
       else  
         line    += enumId2Name(DictField.enumId());  
       if(strFmt("%1",DictField.baseType()) == "String")  
         line    += DictField.stringLen();  
       else  
         line    += '';  
       file.writeExp(line);  
     }  
   }  
   info("Terminato");  
 }  

Il secondo job analizza un progetto shared e per ogni elemento del progetto salva su file cvs il tipo di elemento, il nome, il livello più basso, quello più alto, l'user che ha creato l'oggetto, la data di creazione, l'eventuale user che ha apportato la modificato e la data di modifica:

 static void CRProjectScan(Args _args)  
 {  
   ProjName        projName;// = "projectName";  
   ProjectListNode   list = infolog.projectRootNode().AOTfindChild("Shared");  
   TreeNodeIterator    ir = list.AOTiterator();  
   ProjectNode      pnProj;  
   ProjectNode      pn;// = list.AOTfindChild(projName);  
   struct         prop;  
   str     strFieldName;  
   Commaio   file;  
   str     filename;  
   container  line,header;  
   Dialog   dialog;  
   DialogField field;  
   #properties  
   utilelements getElementSysInfo (Description nodeType, Description NodeName)  
   {  
     utilelementtype uet;  
     utilelements sue;  
     ;  
     switch(nodeType)  
     {  
       case "BaseEnums":  
         uet = utilelementtype::Enum;  
       break;  
       case "Tables":  
         uet = utilelementtype::Table;  
       break;  
       case "ConfigurationKeys":  
         uet = utilelementtype::ConfigurationKey;  
       break;  
       case "Maps":  
         uet = utilelementtype::TableMap;  
       break;  
       case "ExtendedDataTypes":  
         uet = utilelementtype::ExtendedType;  
       break;  
       case "Views":  
         uet = utilelementtype::ViewQuery;  
       break;  
       case "Queries":  
         uet = utilelementtype::Query;  
       break;  
       case "Classes":  
         uet = utilelementtype::Class;  
       break;  
       case "Forms":  
         uet = utilelementtype::Form;  
       break;  
       case "Reports":  
         uet = utilelementtype::Report;  
       break;  
       case "Jobs":  
         uet = utilelementtype::Job;  
       break;  
     }  
     select firstonly sue where sue.recordType == uet && sue.name == NodeName;  
     return sue;  
   }  
   void searchAllObj(projectNode rootNode)  
   {  
     #TreeNodeSysNodeType  
     TreeNode          childNode;  
     TreeNodeIterator      rootNodeIterator;  
     utilelements        result;  
     ;  
     if (rootNode)  
     {  
       rootNodeIterator = rootNode.AOTiterator();  
       childNode = rootNodeIterator.next();  
       while (childnode)  
       {  
         if (childNode.AOTgetNodeType() == #NT_PROJECT_GROUP)  
           searchAllObj(childNode);  
         else  
         {  
           result = getElementSysinfo(rootNode.AOTname(),childNode.AOTname());  
           line = [rootNode.AOTname(),childNode.AOTname(),enum2str(result.utilLevel),enum2str(childNode.applObjectLayer()), result.createdBy, result.createdDateTime, result.modifiedBy, result.modifiedDateTime];  
           file.writeExp(line);  
         }  
         childNode = rootNodeIterator.next();  
       }  
     }  
   }  
   ;  
   dialog = new Dialog();  
   dialog.addText("Select Project:");  
   field = dialog.addField(typeid(ProjName));  
   dialog.run();  
   if (dialog.closedOk())  
   {  
     //info(field.value());  
     projName = field.value();  
   }  
   pn = list.AOTfindChild(projName);  
   if (pn)  
   {  
     filename = WinAPI::getSaveFileName(0, ['TEXT FILE', '*' + '.CSV'], @'C:\', 'Save file as','',projName,0);//@'C:\Users\axservice\Desktop\'+tableId2name(TableId)+'.csv'; //percorso  
     file = new Commaio(filename , 'W');  
     file.outFieldDelimiter(';');  
     header = ["Group", "Name", "Lower Level","Highest level", "CreatedBy", "CreatedDateTime", "ModifiedBy", "ModifieddateTime"];  
     file.writeExp(header);  
     // info(strFmt("Projet %1:", projName));  
     pnProj = pn.loadForInspection();  
     searchAllObj(pnProj);  
     pnproj.treeNodeRelease();  
   }  
   else  
     error("Projet not found");  
 }  

lunedì 16 settembre 2013

AX 2012 - Global cache

AX 2012 mette a disposizione un potente strumento per memorizzare dei valori e recuperarli quando necessario: la Global chace. Può essere considerato come una speciè di "mappa" in cui posso memorizzare un valore associato ad una chiave e recuperare il valore tramite la chiave:

per inserire un valore nella global cache:

  globalCache.set(str owner , anytype key , anytype value );   

owner e key possono essere, liberi. Tipicamente si usa come owner lo username

es:

 SysGlobalCache globalCache = Appl.globalCache();  
 ;  
 globalCache.set(curUserId(), 4, "Somari su AX");   

per recuperare un valore:

 value = globalCache.get(str owner , anytype key);  

es:

  SysGlobalCache globalCache = Appl.globalCache();  
  str s;  
  ;  
  s = globalCache.get(curUserId(), 4); //s conterrà la stringa "Somari su AX"  

venerdì 2 agosto 2013

AX 2012 - Creazione servizio AIF e consumo tramite C#

In questo articolo vediamo come creare un servizio AIF, esporlo e consumarlo tramite un'applicazione C#, il servizio legge un file CSV e scrive i dati un una tabella. L'articolo è tratto dal white paper Microsoft Dynamics AX 2012 Services.pdf. La tabella di destinazione contterrà due campi, un campo itemid ed un campo itemname Iniziamo creando la classe datacontract del nostro servizio, che chiameremo CRImportaDataContract che si occuperà di fare il parm del filepath:

 [DataContractAttribute('Import')] 
 public class CRImportDataContract 
 { 
   Filename  file; 
 } 

 [DataMemberAttribute('File')] 

 public Filename parmFilename(Filename _file = file) 
 { 
   ; 
   file = _file; 
   return file; 
 } 

 private static CRImportDataContract construct() 
 { 
   return new CRImportDataContract(); 
 } 

 public static CRImportDataContract newFromFile(Filename _file) 
 { 
   CRImportDataContract contract = CRImportDataContract::construct(); 
   ; 

   contract.parmFilename(_file); 
   return contract; 

 } 



A questo punto creimano la classe del servizio che chiameremo CRImportService, settando poi la proprieta RunOn su "Server". La classe contiene il metodo che leggendo il file scrive i dati nella tabella.

 public class CRImportService  
 {  
 }  
 [SysEntryPointAttribute(true)]  
 public CRImportDataContract getFileName(Filename _file)  
 {  
   CRImportDataContract contract;  
   ;  
   contract = CRImportDataContract::newFromFile(_file);  
   return contract;  
 }  
 [SysEntryPointAttribute(true)]  
 public boolean insertIntoTable(Filename _file)  
 {  
   TextIo          inFile;  
   container        line;  
   Counter         records;  
   //SysOperationProgress   simpleProgress;  
   container        fileContainer;  
   Counter         loopCounter;  
   CRTestTable       CRTestTable;  
   boolean         ret;  
   str           filename;  
   #OCCRetryCount  
   #AviFiles  
   filename = _file;  
   try  
   {  
    //Caricamento file per righe, ogni riga è memorizzata in un container  
    inFile = new TextIo(filename, 'r');  
    inFile.inRecordDelimiter('\n');  
    inFile.inFieldDelimiter(';');  
    while (inFile.status() == IO_Status::OK)  
    {  
     fileContainer += [infile.read()];  
    }  
    inFile = null;  
   }  
   catch  
   {  
    //throw error(strFmt("@SYS18678", filename));  
    return false;  
   }  
   //simpleProgress = SysOperationProgress::newGeneral(#aviUpdate, "Importazione...", conLen(fileContainer));  
   ttsBegin;  
   records = 0;  
   //il ciclo parte da 2 perchè si suppone che la prima riga sia di intestazione  
   for (loopCounter = 2; loopCounter <= conLen(fileContainer) - 1 ; loopCounter++)  
   {  
    CRTestTable.clear();  
    //simpleProgress.incCount();  
    try  
    {  
     line = conPeek(fileContainer, loopCounter); // lettura della riga i-esima  
     CRTestTable.ItemId      = conPeek(line, 1);  
     CRTestTable.ItemName     = conPeek(line, 2);  
     CRTestTable.insert();  
     records++;  
     //simpleprogress.setText(strfmt("@SYS76835", loopCounter, CRTestTable.RecId));  
    }  
    catch (Exception::Deadlock)  
    {  
     if (xSession::currentRetryCount() < #RetryNum)  
     {  
      retry;  
     }  
      else  
        return false;  
    }  
   }  
   ttsCommit;  
   //info(strFmt("Inseriti %1 record", records));  
  return true;  
 }  

Dobbiamo ora deployare il servizio:
  1. Tasto destro sul nodo service group dell' AOT, nuovo service group
  2. Nelle proprietà immettere il nome del servizio per es ImportService
  3. Tasto desto sul nodo service group dell'AOT,  new service node reference
  4. Impostare il valore della proprietà Service col valore ImportService
  5. Tasto desto sul nodo service group a cliccare su Deploy service group
Se tutto è andato a buon fine andando in System administration | setup | Service and Application Integration Framework | Inbound ports dovremmo avere il nostro servizio pronto per essere utilizzato



Scriviamo ora una piccola applicazione consolo C# che cunsuma il servizio. Aprire visual studio creare una nuova console application. Click col desto su service reference -> add service reference, Nella finestra che si apre inseriamo nel campo address l'indirizzoil valore del campo URI WSDL che troviamo nelle inboud ports di AX e clicchiamo su Go.


 Ora il servizio è pronto per essere riahiamato via C#:

 string theFile = "C:\\Users\\AXService\\Desktop\\test2.csv";  
 bool insert;  
 ImportServiceClient theService = new ImportServiceClient();  
 CallContext theContext = new CallContext();  
 theContext.Company = "CEU";  
 theContext.Language = "it";  
 theContext.LogonAsUser = "AXService";  
 insert = theService.insertIntoTable(theContext, theFile);  
 Console.WriteLine("Terminato!");     
 Console.Read();  

lunedì 15 aprile 2013

AX 2009 - Duplicazione dati multiriga su form grid

Molte volte capita di dover duplicare i dati presenti su una o più righe di una griglia appartenente ad un form. Generalmente all griglia è associato un datasource specificato nel form.

Per prima cosa possiamo creare un bottone, che una volta schiacciato si assicurerà di effettuare l'operazione di duplica. Per permettere questa operazione in multiriga è necessario impostare la proprietà "MultiSelect" del bottone a "Yes".

Dopodichè basterà inserire questa logica (ne esistono comunque diverse varianti) nel metodo "clicked" del bottone stesso:

   DataSource  dataSourceLocale;  
   DataSource  dataSourceNuovo;  
   MultiSelectionHelper selection = MultiSelectionHelper::construct();  
   super();
  
   selection.parmDatasource(DataSource_ds);  
   dataSourceLocale = selection.getFirst();  

   while (DSLunchOrderslocal.RecId != 0)  
   {  
       dataSourceNuovo.field1 = dataSourceLocale.field1;  
       dataSourceNuovo.field2 = dataSourceLocale.field2;  
       ....  
       dataSourceNuovo.insert();  
       dataSourceLocale = selection.getNext();  
   }  
   DataSource_ds.executeQuery();  
    

Come si vede dal codice basta dichiarare due buffer dello stesso tipo della tabella che è impostata nella griglia, e un oggetto "MultiSelectionHelper" che ci aiuterà a scorrere le righe selezionate.

Il primo buffer (dataSourceLocale) serve per raccogliere i record che mano a mano vogliamo copiare, ovvero ogni singola riga, e il secondo serve per l'inserimento nel database.

Dopo aver inizializzato l'oggetto "MultiSelectionHelper", lo si parametrizza con i dati selezionati dal form e lo si inizia a scorrere partendo dal primo record con "getFirst()" parametrizzando il primo buffer. All'interno del ciclo si parametrizza il secondo buffer con gli stessi valori del primo che viene via via ciclato con "getNext()".

Infine per visualizzare le nuove righe nel form basta effettuare il metodo "executeQuery()" del datasource dello stesso.

martedì 9 aprile 2013

AX 2009 - Lookup su tabelle e campi

In questo post vediamo come creare un un semplice form con due campi: il primo campo apre una lookup su tutte le tabelle, il secondo apre una lookup su tutti i campi della tabella selezionata. Il nostro form dovrà contenere due controlli di ti po string che chiameremo rispettivamente "Tables" in autodeclaration e "Fields". Effettuiamo l'override del metodo lookup sul campo tables:

 static void lookup(FormStringControl _ctrl)  
 {  
   SysTableLookup       sysTableLookup =  
   SysTableLookup::newParameters(tablenum(UtilidElements), _ctrl);  
   Query            query = new Query();  
   QueryBuildDataSource    queryBuildDataSource;  
   QueryBuildRange       nameQBR, typeQBR;  
   ;  
   sysTableLookup.addLookupfield(fieldnum(UtilidElements, Name));  
   sysTableLookup.addLookupfield(fieldnum(UtilidElements, Id));  
   queryBuildDataSource = query.addDataSource(tablenum(UtilidElements));  
    
   typeQBR = queryBuildDataSource.addRange(fieldnum(UtilidElements, recordType));  
   typeQBR.value(SysQuery::value(UtilElementType::Table));  
   sysTableLookup.parmQuery(query);  
   sysTableLookup.performFormLookup();  
 }  

A livello di class declaration dichiariamo una variabile che conterrà l'id della tabella selezionata:

 public class FormRun extends ObjectRun  
 {  
   TableId TableId;  
 }  

Questa variabile verrà settata nel modified field del controllo "Tables"

 public boolean modified()  
 {  
   boolean ret;  
   ret = super();  
   TableId = TableName2Id(this.text());  
   return ret;  
 }  

Scriviamo a questo punto il metodo lookup del controllo "Fields"

 public void lookup()  
 {  
   SysTableLookup       sysTableLookup =  
   SysTableLookup::newParameters(tablenum(UtilidElements), this);  
   Query            query = new Query();  
   QueryBuildDataSource    queryBuildDataSource;  
   QueryBuildRange       nameQBR, typeQBR;  
   ;  
   sysTableLookup.addLookupfield(fieldnum(UtilidElements, Name));  
   sysTableLookup.addLookupfield(fieldnum(UtilidElements, Id));  
   queryBuildDataSource = query.addDataSource(tablenum(UtilidElements));  
   nameQBR = queryBuildDataSource.addRange(fieldnum(UtilidElements,  
   ParentId));  
   nameQBR.value(queryValue(TableId));  
   typeQBR = queryBuildDataSource.addRange(fieldnum(UtilidElements, recordType));  
   typeQBR.value(SysQuery::value(UtilElementType::TableField));  
   sysTableLookup.parmQuery(query);  
   sysTableLookup.performFormLookup();  
 }  

Possiamo verificare il funzionamento del form selezionando "CustTable"



lunedì 25 marzo 2013

AX 2012 - Importazione dati da file CSV / EXCEL

In questo post vediamo come importare dati da un file .CSV in AX, inserendo i dati in un tabella custom chiamata "MyTable" avente due campi "Field1" e "Field2" di tipo stringa. Il metodo sottostante effettua l'operazione leggendo i dati dal file C:\TEMP\test.csv

 static void CSVLoadData(Args _args)  
 {  
   TextIo             inFile;  
   container            line;  
   Counter             records;  
   SysOperationProgress      simpleProgress;  
   container            fileContainer;  
   Counter             loopCounter;  
   Mytable             Mytable;  
   CustTable            CustTable;  
   InventTable           InventTable;  
   str               filename;  
   #OCCRetryCount  
   #AviFiles  
   #File 
   
   if (curExt() != 'DAT')
   {
		throw error("This script must run in the DAT company!");
   } 
   
   filename = WinAPI::getOpenFileName(0,
                                   [WinAPI::fileType(#csv),#AllFilesName + #csv],
                                   @'C:\users\',
                                   "@SYS53008"
                                   );
                                   
   if(!filename)
   {
      return;
   }                               
                                   
   try  
   {  
     //Caricamento file per righe, ogni riga è memorizzata in un container  
     inFile = new TextIo(filename, 'r');  
     inFile.inRecordDelimiter('\n');  
     inFile.inFieldDelimiter(';');  
     while (inFile.status() == IO_Status::OK)  
     {  
       fileContainer += [infile.read()];  
     }  
     inFile = null;  
   }  
   catch  
   {  
     throw error(strFmt("@SYS18678", filename));  
   }  
   simpleProgress = SysOperationProgress::newGeneral(#aviUpdate, "Importazione...", conLen(fileContainer));  
   ttsBegin;  
   records = 0;  
   //il ciclo parte da 2 perchè si suppone che la prima riga sia di intestazione  
   for (loopCounter = 2; loopCounter <= conLen(fileContainer) - 1 ; loopCounter++)  
   {  
     Mytable.clear();  
     simpleProgress.incCount();  
     try  
     {  
       line = conPeek(fileContainer, loopCounter); // lettura della riga i-esima  
       Mytable.Field1       = conPeek(line, 1);  
       Mytable.Field2       = conPeek(line, 2);  
       Mytable.insert();  
       records++;  
       simpleprogress.setText(strfmt("@SYS76835", loopCounter, Mytable.RecId));  
     }  
     catch (Exception::Deadlock)  
     {  
       if (xSession::currentRetryCount() < #RetryNum)  
       {  
         retry;  
       }  
     }  
   }  
   ttsCommit;  
   info(strFmt("Inseriti %1 record", records));  
 }  

Per leggere i dati da EXCEL possiamo invece usare il seguente  job:

 static void EXCELLoadData(Args _args)  
 {  
   SysExcelApplication     application;  
   SysExcelWorkbooks      workbooks;  
   SysExcelWorkbook      workbook;  
   SysExcelWorksheets     worksheets;  
   SysExcelWorksheet      worksheet;  
   SysExcelCells        cells;  
   COMVariantType       type;  
   Name            name;  
   FileName          filename;  
   int             row;  
   container          TableDataContainer,  
                 line;  
   SysOperationProgress    simpleProgress;  
   Counter           BLrecords,ITrecords,ITRecordsNotFound;  
   Counter           loopCounter;  
   InventTable         inventTable;  
   ItemId           itemId;  
   boolean           previewMode = true; //EXECUTION MODE  
   #AviFiles  
   #File
   
    str COMVariant2Str(COMVariant _cv, int _decimals = 0, int _characters = 0, int _separator1 = 0, int _separator2 = 0)
    {
        switch (_cv.variantType())
        {
            case (COMVariantType::VT_BSTR):
                return _cv.bStr();

            case (COMVariantType::VT_R4):
                return num2str(_cv.float(),_characters,_decimals,_separator1,_separator2);

            case (COMVariantType::VT_R8):
                return num2str(_cv.double(),_characters,_decimals,_separator1,_separator2);

            case (COMVariantType::VT_DECIMAL):
                return num2str(_cv.decimal(),_characters,_decimals,_separator1,_separator2);

            case (COMVariantType::VT_DATE):
                return date2str(_cv.date(),123,-1,-1,-1,-1,-1);

            case (COMVariantType::VT_EMPTY):
                return "";

            default:
                throw error(strfmt("@SYS26908", _cv.variantType()));
        }
    return "";
    }
   
   ;  
     
   application = SysExcelApplication::construct();  
   workbooks = application.workbooks();  
   
   filename = WinAPI::getOpenFileName(0,  
                   [WinAPI::fileType(#xlsx),#AllFilesName + #xlsx],  
                   strFmt(@'C:\users\%1\Desktop',WinApi::getUserName()),  
                   "@SYS53008"  
                   );  
   try  
   {  
     workbooks.open(filename);  
   }  
   catch (Exception::Error)  
   {  
     application.quit();  
   
     throw error("@SYS19358");  
   }  
   
   workbook = workbooks.item(1);  
   worksheets = workbook.worksheets();  
   worksheet = worksheets.itemFromNum(1);  
   cells = worksheet.cells();  
   row = 1;  
   
   //read data  
   do  
   {  
     row++;  
       
     line = [cells.item(row, 1).value().bStr()
     		 ,COMVariant2Str(cells.item(row, 2).value())
             ,COMVariant2Str(cells.item(row, 10).value(),-1,-1,-1,-1)];
             
     TableDataContainer += [line];  
     type = cells.item(row+1, 1).value().variantType();  
   
   }  
   while (type != COMVariantType::VT_EMPTY);  
   
   application.quit();  
 }  

giovedì 14 marzo 2013

AX 2012 - Number Sequence Framework

In questo post vedremo il funzionamento delle sequenza numeriche in AX 2012 creando una nuova sequenza per un modulo custom chiamato FCM con la relativa tabella dei parametri. La fonte di questo articolo è:
http://msdn.microsoft.com/en-us/library/aa608474.aspx Rispetto all'originale ho approfondito alcuni aspetti che risultavano un pò vaghi.

1) Creazione EDT: creiamo un unovo EDT per la nostra sequenza, unica accortezza che il nostro EDT dovrè assere di tipo "String". Rinominiamo l'EDT in "NSid", impostiamo una lunghezza (per es 15) assegnamo la label "Test sequenza numerica" e salviamo.

2) Creazione Tabella parametri: La tabella dovrò contenere ALMENO il campo chiave con il nostro EDT appena creato. Chiamiamo la tabella "NSParameters" e aggiungiamoci il campo. Implementiamo poi il classico metodo "find()" e facciamo l'ovverride dei metodi delete() e update() rispettivamente:

 void delete()  
 {  
   throw error("@SYS23721");  
 }  

 void update()  
 {  
   super();  
   flush NSParameters;  
 }  

ed i metodi per la gestione della sequenza:

 client server static NumberSequenceReference numRefNSIdNum()  
 {  
   NumberSeqScope scope = NumberSeqScopeFactory::createDataAreaScope(curext());  
   return NumberSeqReference::findReference(extendedtypenum(NSId), scope);  
 }  

 public NumberSeqModule numberSeqModule()  
 {  
   return NumberSeqModule::FCM;  
 }  

3)Modifica base enum: modificare l'enumerato "NumberSeqModule" aggiungendo il un nuovo valore che chiameremo FCM

4) Creazione nuova classe : La classe servirà a gestire la sequenza:

 //Classe per il setup delle sequenza numerica  
 class NumberSeqModuleFacilityManagement extends NumberSeqApplicationModule  
 {  
 }  

implementando i metodi:

 public NumberSeqModule numberSeqModule()  
 {  
   return NumberSeqModule::FCM;  
 }  

 void loadModule()  
 {  
   NumberSeqDatatype datatype = NumberSeqDatatype::construct();  
   datatype.parmDatatypeId(extendedTypeNum(NSid));  
   datatype.parmReferenceHelp("Riferimento unico all'elemento della sequenza");  
   datatype.parmWizardIsContinuous(false);  
   datatype.parmWizardIsManual(NoYes::No);  
   datatype.parmWizardIsChangeDownAllowed(NoYes::No);  
   datatype.parmWizardIsChangeUpAllowed(NoYes::No);  
   datatype.parmSortField(1);  
   datatype.parmWizardHighest(999999);  
   datatype.addParameterType(NumberSeqParameterType::DataArea, true, false);  
   this.create(datatype);  
 }  

5) Creazione form: creiamo il form per la nostra tabella dei paramtri. Come suggeriscono le BP di miscosoft e bene crere un nuovo tab con la relativa griglia che conterrà la Sequenza per il nostro modulo. Al form dovrà anche essere aggiunto il datasource della tabella NumberSequenceReference con le seguenti proprietà:



il form dovrà contenere i seguenti metodi:

 public class FormRun extends ObjectRun  
 {  
   boolean          runExecuteDirect;  
   TmpIdRef          tmpIdRef;  
   NumberSeqScope       scope;  
   NumberSeqApplicationModule numberSeqApplicationModule;  
   container         numberSequenceModules;  
 }
 
 public void init()  
 {  
   this.numberSeqPreInit();  
   super();  
   this.numberSeqPostInit();  
 } 
 
 void numberSeqPostInit()  
 {  
   numberSequenceReference_ds.object(fieldNum(NumberSequenceReference, AllowSameAs)).visible(numberSeqApplicationModule.sameAsActive());  
   referenceSameAsLabel.visible(numberSeqApplicationModule.sameAsActive());  
 } 
 
 void numberSeqPreInit()  
 {  
   runExecuteDirect  = false;  
   numberSequenceModules = [NumberSeqModule::FCM, NumberSeqModule::FCM];  
   numberSeqApplicationModule = new NumberSeqModuleCustomer();  
   scope = NumberSeqScopeFactory::createDataAreaScope();  
   NumberSeqApplicationModule::createReferencesMulti(numberSequenceModules, scope);  
   tmpIdRef.setTmpData(NumberSequenceReference::configurationKeyTableMulti(numberSequenceModules));  
 }  

Implementiamo i seguenti metodi nel datasource  "NumberSequenceReference" :

 void removeFilter()  
 {  
   runExecuteDirect = false;  
   numbersequenceReference_ds.executeQuery();  
 }
 
 void executeQuery()  
 {  
   if (runExecuteDirect)  
   {  
     super();  
   }  
   else  
   {  
     runExecuteDirect = true;  
     this.queryRun(NumberSeqReference::buildQueryRunMulti(numberSequenceReference,  
                                tmpIdRef,  
                                numberSequenceTable,  
                                numberSequenceModules,  
                                scope));  
     numbersequenceReference_ds.research();  
   }  
 } 
 
 int active()  
 {  
   int ret;  
   ret = super();  
   buttonNumberSequenceGroup.enabled(numberSequenceReference.groupEnabled());  
   return ret;  
 }  

Nel nodo design , nella griglia relativa al datasource "NumberSequenceReference" aggiungiamo 6 controlli.
Possiamo direttamente copiare questi controlli  dal tab numberSeq del form "CustParameters":


5 nella griglia:
- referenceLabel
- NumberSequenceReference_NumberSequenceId
- taxBookSectionId
- NumberSequenceReference_AllowSameAs
- referenceSameAsLabel

1 fuori:
- referenceHelp

Consigliamo a questo punto un riavvio dell'AOS

6) Job Caricamento: Creiamo un job per il refresh delle sequenze numeriche di tutti i moduli:

 static void NumberSeqLoadAll(Args _args)  
 {  
   NumberSeqApplicationModule::loadAll();  
 }  

7) Il Wizard:

  1. Andare in Organization administration > Common > Number sequences > Number sequences.
  2. Click Generate -> Set up number sequences wizard.
  3. Completare il wizard per creare la nuova sequenza per il modulo custom
Se tutto è andato a buon fine se apriamo la lookup sul campo "Codice sequenza numerica" dovremo poter vedere la nuova sequenza con la descrizione equivalente al campo Label del nostro EDT:


Possiamo creare ora un job per iniziare a generare numeri con la sequenza numeri impostata nella tabella dei parametri:

 static void testNSId(Args _args)  
 {;  
   Info(NumberSeq::newGetNum(NSParameters::numRefNSIdNum()).num());  
 }  

8) Creazione form di utilizzo: creiamo un nuovo form in modo tale che ad ogni nuovo record creato venga automaticamente assegnato il numero di sequenza. Il form dovrà avere come datasource una nuova tablella (che chiameremo NSUsage) con almeno un campo di tipo NSid (cioè la stesso EDT creato al punto 1).
Metodi del from:

 public class FormRun extends ObjectRun  
 {  
   NumberSeqFormHandler numberSeqFormHandler;  
 } 
 
 NumberSeqFormHandler numberSeqFormHandler()  
 {  
   if (!numberSeqFormHandler)  
   {  
     numberSeqFormHandler = NumberSeqFormHandler::newForm(  
       NSParameters::numRefNSIdNum().NumberSequenceId,  
        element,  
        NSUsage_ds,  
        fieldnum(NSUsage, NSId));  
   }  
   return numberSeqFormHandler;  
 }  


il datasource dovrà contenere i seguenti 3 metodi:

 public void write()  
 {  
   super();  
   element.numberSeqFormHandler().formMethodDataSourceWrite();  
 } 
 
 public void delete()  
 {  
   element.numberSeqFormHandler().formMethodDataSourceDelete();  
   super();  
 } 
 
 public void create(boolean _append = false)  
 {  
   element.numberSeqFormHandler().formMethodDataSourceCreatePre();  
   super(_append);  
   element.numberSeqFormHandler().formMethodDataSourceCreate();  
 }  

Salviamo il form. A questo punto ogni volta che clicchiamo su nuovo, il campo "Test sequenza numerica" sarà automaticamente popolato con il numero della sequenza numerica selezionata nella tabella dei parametri:



A questo link potete trovare l' XPO del progetto 

venerdì 8 marzo 2013

AX 2012 - Breve introduzione ai report

AX 4.0 - Reset generatore label

In questo breve post vediamo come risolvere un bug presente in AX 4.0 riguardo la generazione delle etichette. In quella versione il generatore degli id delle etichette era gestito da un componente esterno. A volte capita che quando si tenta di creare una nuova etichette il generatore non riesca a recuperare l'ID corretto, assegnando il valore 0 alla nuova etichetta:


Per ovviare a ciò dobbiamo andare in:


strumenti -> strumenti di sviluppo-> controllodi versione->impostazioni->impostazioni di sistema 
cliccare applica e ok

per resettare il componente. Possiamo a questo punto creare correttamente nuove etichette:



martedì 15 gennaio 2013

AX 2009 - Creare un form di lookup

Con questo post ho colto l'occasione per documentare una modifica abbastanza frequente ed utile, almeno per la mia esperienza. Ho ripreso un post già esistente (http://axaptapedia.com/Lookup_Form) che però faceva riferimento a più versioni di AX e che in parte era ormai obsoleto su AX2009. Con l'occasione ho tradotto il post in italiano e riadattato il contenuto al pieno funzionamento sulla versione AX2009, per le versioni precedenti rimando la lettura al post originale di cui ho esso il link.

Nell'esmpio sotto vedremo come creare una lookup per un'ipotetica tabella "Table" con campo chiave "Id". Il campo "Id" dovrà essere una stringa.
Come prima cosa dovremo creare un form molto semplice che conterrà nella maggior parte dei casi solo una griglia contenente i campi che vogliamo mostrare. Nella figura sotto è mostrato come impostare alcune proprietà del datasource:


Dovremo poi impostare come di seguito alcune proprietà nel design del form:


Assiamiamo che il nostro form di lookup abbia un control di tipo grid, e che in questo sia presente un control che punta al campo chiave che vogliamo utilizzare. Tale campo per comodità lo rinominiamo come "Table_Id" e impostiamo la proprietà 'AutoDeclaration' dello stesso a true.

Andremo adesso a modificare alcuni metodi del nostro form.

Override del metodo 'init' del form, in questo modo andremo ad impostare quel'è il controllo che sarà il risultato della nostra selezione.

 public void init()  
 {  
   ;  
   super();  
   element.selectMode(Table_Id);  
 }  

Override del metodo 'executeQuery' del datasource 'Table' del nostro form (ci permetterà di impostare il valore selezionato nel controllo chiamante come filtro della nostra form di lookup):

 public void executeQuery()  
 {  
   FormStringControl  callerControl  = SysTableLookup::getCallerStringControl(element.args());  
   ;  
   super();  
   Table_ds.findValue(fieldnum(Table,Id),callerControl.text());  
 }  

e del metodo 'init' del nostro dataSource (dove possiamo impostare la nostra query custom, definendo filtri, ordinamenti, ecc...):

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

Se vogliamo inoltre rendere disponibili le wildcard (eg. "abc*" )per il filtro su stringhe, come concesso a standard da Ax, dovremo effettuare l'override del metodo 'run' del nostro form di lookup come segue:

 public void run()  
 {  
   FormStringControl  callerControl  = SysTableLookup::getCallerStringControl(element.args());  
   Boolean       filterLookup  = false;  
   ;  
   // if lookup was called with filter, then supress autoSearch  
   if (callerControl.text() && callerControl.hasChanged())  
   {  
     filterLookup = true;  
     Table_ds.autoSearch(false);  
   }  
   super();  
   // after call of super filter search manually by applying past filter  
   if (filterLookup)  
   {  
     Table_ds.research();  
     Table_ds.filter(fieldnum(Table, Id),callerControl.text());  
   }  
 }  

Se vogliamo che il form appena creato diventi il default per un determinato Extended Data Type dovremo semplicemente impostare il nome del nostro form nella proprietà 'FormHelp' del EDT.

Se, in alternativa, vogliamo utilizzare la nostra lookup in un controllo specifico, lasciando di default la lookup standard di AX, dovremo andar a customizzare il metodo lookup del nostro chiamante.
Definito per comodità il controllo chiamate come "CallerTable_CallerId" e impostata la proprietà 'AutoDeclaration' dello stesso a true, dovremo andare a modificare il metodo lookup del campo ad esso associato come segue. Nell'esempio la tabella chiamante sarà "CallerTable", il campo "CallerId" e datasource "CallerTable_DS".

 public void lookup(FormControl _formControl, str _filterStr)  
 {  
   Args args = new Args();  
   FormRun itemLookUp;  
   ;  
   args.name(formstr(CustomLookupForm));  
   args.caller(CallerTable_CallerId);  
   itemLookUp = new FormRun(args);  
   itemLookUp.init();  
   this.performFormLookup(itemLookUp, CallerTable_CallerId);  
 }  

 public void performFormLookup(FormRun _form, FormControl _formControl)  
 {  
   super(_form, _formControl);  
 }  

Avrete in questo modo un form di lookup custom perfettamente funzionante.