venerdì 28 marzo 2014

AX 2012 - Lookup su dialog SSRS: Utilizzo classe UI Builder

In moltissimi casi può sorgere la necessità di aggiungere un parametro che esegua una lookup su un campo di una tabella nella finestra di dialog di un report.

Per prima cosa occorre modificare (o creare in caso non esista) la classe UI builder del report. Questa classe è necessaria quando si desidera personalizzare la dialog di un report.
In sintesi ci consente di aggiungere controlli (visibilità, abilitazione...) e lookup personalizzati ai parametri impostati nella classe Data contract del report stesso. Si presuppone la creazione quindi del metodo parm nella data contract del campo che vogliamo "lookupare".

La classe builder che andremo a creare (se non esiste già) deve innanzitutto estendere SrsReportDataContractUIBuilder. Vediamo, con un banalissimo esempio, la creazione di questa classe per un report che ha come parametro il codice cliente. Si vuole visualizzare una lookup nella SalesTable di tutti gli ordini di quel cliente

 class SimpleDemoUIBuilder extends SrsReportDataContractUIBuilder  
 {  
   DialogField dialogCustAccount;
   DialogField dialogSalesOrders;  
   SimpleDemoContract contract;  
 }  

Può essere utile effettuare l'override del metodo build per impostare la data contract e il dialog field dichiarati nella class declaration...

 public void build()  
 {  
   contract = this.dataContractObject();  
   dialogCustAccount = this.addDialogField(methodStr(SimpleDemoContract,parmCustAccount),contract);
 }  

...piuttosto che eseguire tutto nel metodo postBuild:

 public void postBuild()  
 {  
   DialogField             dialogCostingVersionId;  
   SimpleDemoContract      contract;
  
   super();
  
   contract= this.dataContractObject();  
   dialogCustAccount = this.addDialogField(methodStr(SimpleDemoContract,parmCustAccount),contract);
   dialogSalesOrders = this.addDialogField(methodStr(SimpleDemoContract,parmSalesOrders),contract);   
   .....
   dialogSalesOrders.registerOverrideMethod(methodStr(FormStringControl, lookup), methodStr(SimpleDemoUIBuilder, salesOrdersLookup), this);
  
 }  

dove l'ultima riga servirà proprio a far si che la dialog esegua il metodo di lookup sottostante:

  private void salesOrdersLookup(FormStringControl _salesOrdersLookup)  
 {  
      Query query = new Query();  
      QueryBuildDataSource qbds_SalesTable;  
      SysTableLookup sysTableLookup;  
    
      if (_salesOrdersLookup != null)
      {
                 sysTableLookup = SysTableLookup::newParameters(tableNum(SalesTable), _salesOrdersLookup);  
                 qbds_SalesTable = query.addDataSource(tableNum(SalesTable)); 
                 sysTableLookup.addLookupfield(fieldnum(SalesTable, SalesId), true);
     
                 qbds_SalesTable.addRange(fieldNum(SalesTable,CustAccount)).value(queryValue(contract.parmCustAccount());
                 sysTableLookup.parmQuery(query);  
                 // Perform the lookup  
                 sysTableLookup.performFormLookup();  
      }
 }    

Dove si effettuerà il range con il metodo parm (che in questo caso funge da getter), creato nella data contract.

lunedì 20 gennaio 2014

AX 2012 - SSRS valore assente per un nuovo parametro

Quando aggiungiamo un parametro al form di lancio dei report in AX 2012, può capitare che questo valore al lancio sia nullo, ma se non gestiamo questa eventualità (impostando le proprietà adeguate lato Visual Studio) allora avremo il seguente messaggio di errore:

The 'ParameterName' parameter is missing a value


In questo caso impostare le giuste proprietà in Visual Studio:

Allow Blank       True
Nullable            True
 nel parametro avente il problema (ed eseguire il deploy) potrebbe non bastare!

In questo caso occorre agire come brillantemente segnalato in questo blog, dove segnala i vari metodi per far "sentire" questa modifica:
blog con i metodi

Method I.
Force the update of the properties by renaming the parameter
1. Open the report for editing in Visual Studio.
2. Expand the Parameters node and rename the affected parameter to ParameterName1.
3. Set Allow Blank and Nullable to True if not already set.
4. Deploy the report.
5. Rename the parameter back to ParameterName.
6. Deploy the report.

Method II.
Recreate the parameter in Visual Studio and set the properties as required before deploying the report
1. In AOT, expand the SSRS Reports node, expand the Reports node and locate the report.
2. Right click on the report and select Delete. This will remove all customizations to the report from the current model/layer so make sure you create a backup if you think you might want to return to them.
3. Right click on the report and select Deploy element. You should now be at the point where you did not receive this error.
4. Open the report for editing in Visual Studio and refresh the dataset. The parameter will be created under theParameters node. Do not deploy the report at this point.
5. Set Allow Blank and Nullable to True.
6. Deploy the report.

Method III.
Modify the properties of the parameter editing the report in the Reporting Services Report Manager

1. Open the report for editing in Visual Studio.
2. Expand the Parameters node and locate the affected parameter.
3. Set Allow Blank and Nullable to True if not already set.
4. Deploy the report.
5. Open Reporting Services Report Manager, in the Parameters properties page of the report, verify that Has defaultand Null check boxes are selected for the affected parameter.
6. Press Apply to save any changes.

venerdì 3 gennaio 2014

AX 2012 - Default dimension

In questo post vediamo come gestire le dimensioni finanziare nelle anagrafiche cliente/fornitore. Il job sottostante scrive nella defualt dimension "CLIENTE" il valore del custAccount:

 static void CreateDefaultDimension(Args _args)  
 {  
   DimensionAttributeValueSetStorage  valueSetStorage = new DimensionAttributeValueSetStorage();  
   DimensionDefault          result;  
   CustTable        custTable = CustTable::find('‪‪‪I01-000001',true);  
   int           i;  
   DimensionAttribute   dimensionAttribute;  
   DimensionAttributeValue dimensionAttributeValue;  
   container        conAttr = ["CLIENTE"]; //nome della dimensione  
   container        conValue = ["I01-000001"]; //valore da inserire  
   str           dimValue;  
   /* per evitare l'hard code 'CLIENTE' possiamo cercare nella tabella dimensionAttribute per   
   backEntityType che è un table num. Nel caso cliente tableNum sarà = tableNum(DimAttributeCustTable)  
   mentre nel caso fornitori tableNum(DimAttributeVendTable)*/  
   for (i = 1; i <= conLen(conAttr); i++)  
   {  
     dimensionAttribute = dimensionAttribute::findByName(conPeek(conAttr,i));  
     if (dimensionAttribute.RecId == 0)  
     {  
       continue;  
     }  
     dimValue = conPeek(conValue,i);  
     if (dimValue != "")  
     {  
       dimensionAttributeValue =  
           dimensionAttributeValue::findByDimensionAttributeAndValue(dimensionAttribute,dimValue,false,true);  
       valueSetStorage.addItem(dimensionAttributeValue);  
     }  
   }  
   result = valueSetStorage.save();  
   ttsBegin;  
   custTable.DefaultDimension = result;  
   custTable.doUpdate();  
   ttsCommit;  
 }  
Prima dell'esecuzione del job:


Dopo l'esecuzione:


Una diversa soluzione, un pò più complessa l'ho trovata quì

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"