giovedì 10 dicembre 2015

AX 2012 - R2 CU7 - Errore 'Incorrect syntax' su cancellazione schema

La procedura consigliata per i rilasci in produzione delle modifiche su AX 2012 prevede l'utilizzo di comandi che lavorano sul Model DataBase di AX. In particolare, al fine di limitare la durata del fermo in produzione, è prevista la creazione e l'importazione del modelstore su di uno schema temporaneo e poi l'inversione di questo schema con il 'dbo' una volta fermate le attività.

Con clienti che utilizzano la versione R2 CU7, a volte ho notato che nel fare questa operazione non riuscivo ad applicare il cambio di schema e non mi era più possibile eliminare lo schema temporaneo, nemmeno utilizzando un DROP da SQL. Approfondendo la cosa, mi sono accorto che ciò avveniva quando utilizzavo dei numeri all'inizio del nome dello schema (e.g. 20150101_TemporarySchema).

L'errore che ottenevo era il seguente (riporto solo la parte principale):

 Incorrect syntax near '20150101'.

Provando ad utilizzare apici (',"), parentesi ('[', '(' ) o altri caratteri speciali non riuscivo a risolvere il problema. Approfondendo allora il funzionamento dei comandi Management Shell e di AxUtil per la cancellazione dello schema ho visto che tutti richiamavano la stessa store procedure: XU_DropSchema .

All'interno di questa store procedure, nella parte finale viene richiamato il comando sql per il drop dello schema,

  1. SELECT @DropStatement = 'DROP SCHEMA ' + @schemaName

scritto così però non tiene in considerazione l'eventualità che vi siano numeri nella parte iniziale del nome e SQL Server non riesce ad interpretare correttamente il comando. Ho risolto il problema correggendo questa riga, aggiungendo le parentesi '[' per isolare il nome dello schema.

  1. SELECT @DropStatement = 'DROP SCHEMA [' + @schemaName + ']'

Posso confermare, per esperienza diretta, che la correzione risolve il problema. Inoltre aprendo la stessa store procedure in ambienti R3, ho visto che è stata riscritta e che non presenta più l'errore sulla parte finale. Non ho ancora verificato se il problema sussiste in kernel R2 CU8\CU9.

lunedì 16 novembre 2015

AX 2012 - Fare il merge di 2 ledger dimension

In questo post vediamo come effettuare il merge di 2 dimensioni finanziarie. Nel mio dovevo creare una nuova ledger dimension facendo il merge tra una dimensione che mi sono calcolato ( http://somarisuax.blogspot.it/2014/12/ax-2012-creare-una-ledgerdimension.html)



 e la dimensione di default nella mainAccountLegalEntity che nel mio caso aveva la dimensione "Commessa" fissa:


Basterà chiamare il metodo

 combinedLedgerDimension = DimensionDefaultingService::ServiceCreateLedgerDimension(ledgerDimension, mainAccountLegalEntity.DefaultDimension);  

combinedLedgerDimension contiene la dimensione combinata:



martedì 10 novembre 2015

AX 2012 - Query sql per estrarre il codice sorgente dal model

Ciao! con questa query sql potete estrarre il codice degli dal database model di Ax


 use "Model_db"  
 IF (OBJECT_ID('tempdb..#SOURCES') IS NULL)  
 begin  
         --drop table #SOURCES  
         SELECT  
                  b.Name  
                  ,b.ElementHandle  
                  ,coalesce(e.ElementTypeName, el.ElementTypeName) [Type]  
                  ,coalesce(pr.Name, b.Name) [Internal Name]  
                  ,parent.Name [Parent Name]  
                  ,d.Name [Layer]  
                  ,a.[LayerId]  
                  ,c.DisplayName [Model Display Name]  
                  ,c.Name [Model Name]  
                  ,a.[ModelId]  
                  ,a.[MODIFIEDDATETIME]  
                  ,a.[MODIFIEDBY]  
                  ,a.[CREATEDDATETIME]  
                  ,a.[CREATEDBY]  
                  ,cast(coalesce(sr.SourceText, sr1.SourceText) as nvarchar(max)) [Source]  
                 INTO #SOURCES   
         --select count(*)  
          FROM [ModelElementData] a   
                                 inner join [ModelElement] b  
                                        on     a.ElementHandle = b.ElementHandle  
                                 inner join [ModelManifest] c  
                                        on a.ModelId = c.ModelId  
                                 inner join [Layer] d  
                                        on a.LayerId = d.Id  
                                 left join [ModelElement] parent  
                                        on b.ParentHandle = parent.ElementHandle  
                                 left join [ModelElement] pr  
                                        on b.ElementHandle = pr.ParentHandle  
                                 left join [ElementTypes] e  
                                        on pr.ElementType = e.ElementType  
                                 left join [Sources] sr  
                                        on sr.SourceHandle = pr.ElementHandle   
                                 left join [ElementTypes] el  
                                        on b.ElementType = el.ElementType  
                                 left join [Sources] sr1  
                                        on sr1.SourceHandle = b.ElementHandle   
          where not b.Name is null  
                 --and  b.Name like '%InterfaceMacroservice%'   
                 --and coalesce(e.ElementTypeName, el.ElementTypeName) like '%Method%'  
                 --and cast(coalesce(sr.SourceText, sr1.SourceText) as nvarchar(max)) like '%InterfaceMacroService%'  
           --order by 1  
         SELECT TBL.name AS ObjName   
                  ,STAT.row_count AS StatRowCount   
                  ,STAT.used_page_count * 8 AS UsedSizeKB   
                  ,STAT.reserved_page_count * 8 AS RevervedSizeKB   
         FROM tempdb.sys.partitions AS PART   
                 INNER JOIN tempdb.sys.dm_db_partition_stats AS STAT   
                         ON PART.partition_id = STAT.partition_id   
                                 AND PART.partition_number = STAT.partition_number   
                 INNER JOIN tempdb.sys.tables AS TBL   
                         ON STAT.object_id = TBL.object_id   
                         --where TBL.name = '#SOURCES'  
         ORDER BY TBL.name;  
         CREATE INDEX IDX_NAME ON #SOURCES(Name)  
 end  
 --------------------------------------------------------------------  
 select top 10 *  
 from #SOURCES  
 where Name ='SalesFormLetter'  

Nella clausola Where dell'ultima riga potete fare i filtri sul nome dell'oggetto.

L'output è fatto così:


giovedì 5 novembre 2015

SSRS - Aggiungere TextBox HTML in un report

In questo post vediamo come aggiungere un controllo HTML ad un report SSRS:

Apriamo il design del report da Visual studio ed inseriamo una text box normale.

A questo punto selezioniamo il contenuto della cella così:


Ora nel form che si apre selezioniamo:


A questo punto possiamo inserire codice html nell'espressione della textbox:


Ci sono un numero limitato di tag che SSRS riesce ad interpretate. Trovate quì i dettagli:

https://msdn.microsoft.com/en-us/library/ff519562.aspx

venerdì 18 settembre 2015

AX 2009 - Esportazione degli elementi di un progetto in xpo

In questo post pubblico un piccolo job che dato il nome di un progetto shared genera su disco gli xpo di ogni oggetto salvando in un repository locale, generando anche la struttura delle sottocartelle in base al percorso dell'oggetto sull'AOT:

 static void DiabloGit(Args _args)  
 {  
   ProjName      projName;  
   ProjectListNode   list = infolog.projectRootNode().AOTfindChild("Shared");  
   TreeNodeIterator  ir = list.AOTiterator();  
   ProjectNode     pnProj;  
   ProjectNode     pn;  
   struct       prop;  
   str         strFieldName;  
   Commaio       file;  
   str         filename;  
   container      line,header;  
   Dialog       dialog;  
   DialogField     field;  
   #properties  
   
   void ExportTreeNodeasXpo(str _path, str _name, str _aotPath)  
   {  
     TreeNode      treeNode;  
     FileIoPermission  perm;  
     str         exportFile;  
     #define.ExportMode("w")  
     ;  
   
     if (!WinApi::pathExists(_path))  
     {  
       WinApi::createDirectoryPath(_path);  
     }  
   
     exportFile = _path + _name;  
   
     perm = new FileIoPermission(exportFile, #ExportMode);  
       
     if (perm == null)  
     {  
       return;  
     }  
       
     perm.assert();  
   
     _aotPath  = _aotPath+SubStr(_name ,1 , strLen(_name)-4);  
     treeNode = TreeNode::findNode(_aotPath);  
   
     if (treeNode != null)  
     {  
       treeNode.treeNodeExport(exportFile);  
     }  
   
     CodeAccessPermission::revertAssert();  
   }  
   
   void searchAllObj(projectNode rootNode)  
   {  
    #TreeNodeSysNodeType  
    TreeNode     childNode;  
    TreeNodeIterator   rootNodeIterator;  
    utilelements    result;  
    str        objPath, objName, aotPath;  
    int start;  
    int end;  
    str root;  
    ;  
   
    root = @'C:\Sources';  
   
    if (rootNode)  
    {  
     rootNodeIterator = rootNode.AOTiterator();  
     childNode = rootNodeIterator.next();  
     while (childnode)  
     {  
      if (childNode.AOTgetNodeType() == #NT_PROJECT_GROUP)  
       searchAllObj(childNode);  
      else  
      {  
       start  = Strlen(childNode.treeNodePath()) - strLen(childNode.treeNodeName());  
       end   = strLen(childNode.treeNodePath());  
       aotPath = subStr(childNode.treeNodePath(),1,start);  
       objPath = root+subStr(childNode.treeNodePath(),1,start);  
       objName = childNode.treeNodeName()+".xpo";  
   
       ExportTreeNodeasXpo(objPath, objName, aotPath);  
   
      }  
        
      childNode = rootNodeIterator.next();  
     }  
    }  
   }  
   ;  
   dialog = new Dialog();  
   dialog.addText("Select Project:");  
   field = dialog.addField(typeid(ProjName));  
   dialog.run();  
     
   if (dialog.closedOk())  
   {  
     projName = field.value();  
   }  
     
   pn = list.AOTfindChild(projName);  
     
   if (pn)  
   {  
    pnProj = pn.loadForInspection();  
    searchAllObj(pnProj);  
    pnproj.treeNodeRelease();  
   }  
   else  
    error("Projet not found");  
   
   info("Terminato!");  
 }  

Se Lanciato su un progettino così fatto:


L'output sarà:



Il job potrebbe essere usato come base di integrazione per sistemi di controllo di versione come TFS o GIT

lunedì 8 giugno 2015

AX 2012 - Lookup su tabella temporanea

In questo Post vogliamo creare una lookup avendo a disposizione come sorgente dati una tabella temporanea. Supponiamo di voler visualizzare i prima 10 clienti visualizzando codice, nome indirizzo e un campo contatore

Creiamo una nuova tabella chiamandola CustTableTmp, impostaimo la proprietà TableType = InMemory e creiamo 4 campi per contenere le informazioni. Creiamo ora un semplice form con un campo testo su cui andremo a fare l'override del metodo lookup. Creiamo un metodo a livello di form per popolare la tabella:
 public CustTableTmp fillTmpTable()  
 {  
   CustTable    custTable;  
   CustTableTmp  CustTableTmp;  
   int       i = 1;  
   while select custTable  
   {  
     if(i > 10)   
     {  
       break;  
     }  
     CustTableTmp.clear();  
     CustTableTmp.Counter        = i;  
     CustTableTmp.CustAccount      = custTable.AccountNum;  
     CustTableTmp.CustName        = custTable.name();  
     CustTableTmp.LogisticsAddressing  = custTable.address();  
     i++;  
     CustTableTmp.insert();  
   }  
   return CustTableTmp;  
 }  
a questo punto sul controllo implementiamo il metodo lookup:
 public void lookup()  
 {  
   CustTableTmp  CustTableTmp;  
   SysTableLookup sysTableLookup;   
   CustTableTmp  = element.fillTmpTable();  
   sysTableLookup = SysTableLookup::newParameters(tableNum(CustTableTmp),this,false);  
   sysTableLookup.addLookupField(fieldNum(CustTableTmp, Counter),False);  
   sysTableLookup.addLookupField(fieldNum(CustTableTmp, CustAccount),True);   
   sysTableLookup.addLookupField(fieldNum(CustTableTmp, CustName),false);   
   sysTableLookup.addLookupField(fieldNum(CustTableTmp, LogisticsAddressing),False);  
   sysTableLookup.parmTmpBuffer(CustTableTmp);  
   sysTableLookup.performFormLookup();  
 }  
Aprendo il form vediamo il risultato:

lunedì 2 febbraio 2015

AX 2012 - Cambiare report design dinamicamente

In questo articolo vediamo come cambiare design dinamicamente ai report standard che usano la "gestione stampa". Nel mio caso il report in oggetto è il DDT fornitori (PurchPackingSlip)
I report per cui è disponibile la "gestione stampa" sono memorizzati nella tabella PrintMgmtReportFormat.

Se per questi report proviamo ad impostare via codice  il report tramite il metodo parmReportName() verrà comunque stampato il design standard.

In questo esempio aggiungiamo un nuovo layout al report della fattura che verrà stampato solo se la company è italiana

Ecco quello che occorre fare:

1) Aggiungere al metodo populate() della tabella PrintMgmtReportFormat una nuova entry così

  case #isoIT:
            addAX(PrintMgmtDocumentType::SalesOrderInvoice, #isoIT);
            break;

che aggiunge un nuovo record nella tabella PrintMgmtReportFormat .  Verificare nella nuova riga che il flag "System" sia attivo, se non lo è attivarlo

2) Aggiungere alla classe PrintMgmtDocType metodo getDefaultReportFormat() una nuova entry così: (indivituate il blocco che riguarda la fattura):

if (SysCountryRegionCode::isLegalEntityInCountryRegion([#isoIT]))
{
                return ssrsReportStr(SalesInvoice, CustomLayout);
}   

Si consiglia ota di rigenerare la tabella PrintMgmtReportFormat  cancellando prima tutti i record
e poi abdare in

Account receivable -> setup -> form -> form setup

andare sul tab general e cliccare sul pulsante "Print management"

A questo link https://technet.microsoft.com/en-us/library/dd309660.aspx alla sezione "Specify conditional settings for an original or copy record" potete trovare una soluzione alternativa che non richiede codice, ma è necessario che l'utente in fase di stampa del report clicchi sulla voce "Usa gestione stampa":