D365 Finance – Erro na validação do Azure KeyVault

A partir da PU48 ou PU49, não sei qual exatamente, o código fonte da aplicação possui uma regra utilizando RegEx para validar o valor do campo Secret dos parametros do Azure KeyVault.

Então se você está encontrando algum problema na validação do seu certificado no formulário do Azure KeyVault, certifique-se que o campo secret dê Match com a expressão regular acima.

É aqui que o RegEx não dá Match: Método GetValue da classe KeyVaultClient:

Encontrei esse erro porque o campo Secret do AKV estava com o valor Vault:///TICATICA, sendo que o correto seria vault:///TICATICA, já que o RegEx é case sensitive.

Problema reproduzido num ambiente com as seguintes versões:

D365 Finance – Erro RetailServer em deploys de PU – Dev sandbox

Quando se deparar com o erro no script de RetailServer no deploy de pacote de Service Update (Platform Update), uma solução aplicável é fazer o seguinte:

“Under the deployable package folder, find the following SQL script <ServiceVolue>:\DeployablePackages\<PackageGUID>\RetailServer\Scripts\DropAllRetialChannelDbObjects.sql”

Executar o script SQL acima enquanto o ambiente está no status de “Failed”. Após a execução, retomar o deploy.

Gerando Hash SHA-1 a partir de uma String

Olá,

Algumas customizações referentes a Nota Fiscal Eletrônica de Serviços requerem que seja criada um hash SHA-1 em uma tag Assinatura.

Da primeira vez que tive que passar por esta situação foi bem complicado e levei muitas horas para fazer um algoritmo muito complexo… mas recentemente tive a oportunidade de colaborar neste assunto novamente e conseguimos chegar a um algoritmo muito mais simples.

Segue abaixo job que exemplifica o mesmo:

 

static void SHA_1()
{
System.Security.Cryptography.SHA1CryptoServiceProvider hash;
System.Text.ASCIIEncoding encoder;
System.Byte[] combined;
System.Byte byte;
str Sha1, input;
int i, arrayLength;
container cont;

;
new InteropPermission(InteropKind::ClrInterop).assert();

input = “00000317330NF 00000003866320090905T NN000000000001686000000000000000082997990008764130000102”;

hash = new System.Security.Cryptography.SHA1CryptoServiceProvider();
encoder = new System.Text.ASCIIEncoding();
combined = encoder.GetBytes(input);

hash.ComputeHash(combined);

sha1 = System.BitConverter::ToString(hash.get_Hash());

info(stralpha(sha1));
}

 

Espero que ajude e poupe um bom esforço e tempo…!!!

Créditos de colaboração ao sr. Hilquias Ferreira, futuro escritor deste blog =)

Trabalhando com datas no Microsoft Dynamics Ax

Muitas vezes precisamos trabalhar com datas no Ax.

Existem algumas funções que faciliam muito este trabalho.

Dica: O comando “Shift+F4” abre a lista de funções do Ax

Fiz um job exemplificando a utilização destas:

 

static void dateJob(Args _args)
{
Date birthday = 311\2013;
;
//Ano a partir da data
info(strfmt(“Ano: %1”, year(birthday)));

//Mês a partir da data
info(strfmt(“Mês (numeral): %1”, mthofyr(birthday)));

//Nome do mês
info(strfmt(“Mês (extenso): %1”, mthname(mthOfyr(birthday))));

//Próximo mês
info(strfmt(“Próximo mês: %1”, mthName(mthofyr(nextmth(birthday)))));

//Semana a partir da data
info(strfmt(“Semana: %1”, wkofyr(birthday)));

//Dia a partir da data
info(strFmt(“Dia: %1”, dayofmth(birthday)));

//Dia da semana (numeral)
info(strfmt(“Dia da semana (numeral): %1”, dayofwk(birthday)));

//Dia da semana (extenso)
info(strfmt(“Dia da semana (extenso): %1”, dayname(dayofwk(birthday))));

//Dias passados desde o início do ano
info(strfmt(“Dia do ano (conta quantos dias passaram): %1”, dayofyr(birthday)));
}

 

A saída deste job será a seguinte:

Imagem

Como pegar o valor anterior do campo durante uma modificação

As vezes, durante o desenvolvimento, é necessário verificar o valor anterior de um registro na tabela.

A primeira opção que muitos de nós pensamos seria declarar uma variável para guardar o valor, modifica-lo e depois fazer a comparação, porém as tabelas do Ax possuem um método que verifica o valor “commitado” do Table Buffer.

Este método é o orig().

Segue exemplo:

static void origMethod(Args _args)

{
CustTable ct;
;

select ct where ct.AccountNum == “000003”;

ct.AccountNum = “Modificado”;

info(strfmt(“Valor atual: %1, valor anterior: %2”,
ct.AccountNum,
ct.orig().AccountNum));
}

O info irá exibir a seguinte mensagem:

infolog

 

Erro nos índices das tabelas – Recriar SqlDictionary

Depois de bem mais de um ano sem fazer uma única postagem, venho postar algo de interesse para quem está passando por um problema chatíssimo: índices de campos.

Este script recria os registros dos Ids da tabela desejada que não se encontram na SqlDictionary.

Quem já teve a experiência de participar de uma migração, por exemplo, deve ter se deparado com este problema.

#macrolib.DictField
// This type is used instead of Types::Int64 for fields of type
// RecId/RefRecId/createdTransactionId/modifiedTransactionId
#define.RecIdBaseType   (49)
// For nonsystem fields of type UtcDateTime an additional field is created
// that holds the actual time zone in which the value has been set
#define.TZIDsuffix      ('_TZID')          

SqlDictionary   sqlDict;
SysdictType     dictType;
DictTable       dictTable;
DictField       dictField;
ArrayIdx        arrIdx;
Counter         numOfSqlFields; // number of records created in SqlDictionary
fieldName       fieldName;
fieldId         fieldId;
tableId         tableId = tablenum(TheTable2Fix);  // TARGET

boolean processTableField(
    DictField   _dictField,
    ArrayIdx    _arrIdx,
    boolean     _isTzIdField = false
    )
{
    ArrayIdx    dictArrIdx;
    str         infoName;       // this field name is for messages only
    FieldName   sqlName;
    boolean     ret;
    ;
    if (_isTzIdField)
    {
        if (    _dictField.baseType()   != Types::UtcDateTime
            ||  _dictField.id()         == fieldnum(Common, createdDateTime)
            ||  _dictField.id()         == fieldnum(Common, modifiedDateTime)
           )
        {
            throw error(Error::wrongUseOfFunction(funcname()));
        }
        dictArrIdx  = _dictField.arraySize() + _arrIdx;
        sqlName     = _dictField.dateTimeTimeZoneRuleFieldName(_arrIdx - 1);
        infoName    = _dictField.name() + #TZIDsuffix;
    }
    else
    {
        dictArrIdx  = _arrIdx;
        sqlName     = _dictField.name(DbBackend::Sql, _arrIdx);
        infoName    = _dictField.name();
    }
    select firstonly sqlDict
        where   sqlDict.tabId   == _dictField.tableid()
            &&  sqlDict.fieldId == _dictField.id()
            &&  sqlDict.array   == dictArrIdx
                ;
    if (!sqlDict)
    {
        sqlDict.clear();
        sqlDict.initValue();
        sqlDict.tabId           = _dictField.tableid();
        sqlDict.fieldId         = _dictField.id();
        sqlDict.array           = dictArrIdx;
        sqlDict.name            = strupr(_dictField.name(DbBackend::Native, _arrIdx));
        sqlDict.sqlName         = sqlName;
        dictType                = new SysDictType(_dictField.typeId());
        if (_isTzIdField)
        {
            sqlDict.fieldType   = Types::Integer;
        }
        else
        if (        _dictField.id()     == fieldnum(Common, RecId)
            ||      _dictField.id()     == fieldnum(Common, createdTransactionId)
            ||      _dictField.id()     == fieldnum(Common, modifiedTransactionId)
            ||      _dictField.typeId() == extendedtypenum(RecId)
            ||      _dictField.typeId() == extendedtypenum(RefRecId)
            ||  (   dictType
                &&  dictType.isExtending(extendedtypenum(RecId))
                )
           )
        {
            // This type is used instead of Types::Int64 for fields of type
            // RecId/RefRecId/createdTransactionId/modifiedTransactionId
            sqlDict.fieldType   = #RecIdBaseType;
        }
        else
        {
            sqlDict.fieldType   = _dictField.baseType();
        }
        sqlDict.strSize         = _dictField.stringLen();
        sqlDict.shadow          = bitTest(_dictField.flags(), #DBF_SHADOW);
        sqlDict.rightJustify    = bitTest(_dictField.flags(), #DBF_RIGHT);
        sqlDict.flags           = sqlDict.shadow;   // not _dictField.flags() at all!
        sqlDict.nullable        =   _dictField.baseType() == Types::Container
                                ||  _dictField.baseType() == Types::VarString
                                    ;
        if (sqlDict.validateWrite())
        {
            sqlDict.insert();
            ret = true;
            info(strfmt(@"Created record for field %1.%2%3",
                        dictTable.name(), infoName,
                        _dictField.arraySize() > 1 ? strfmt(@"[%1]", _arrIdx) : ''));
            // for all nonsystem UtcDateTime fields we also create a related TZID-field
            if (   !_isTzIdField
                &&  _dictField.baseType()   == Types::UtcDateTime
                &&  _dictField.id()         != fieldnum(Common, createdDateTime)
                &&  _dictField.id()         != fieldnum(Common, modifiedDateTime)
               )
            {
                processTableField(_dictField, _arrIdx, true);
            }
        }
        else
        {
            ret = checkFailed(strfmt(@"%1 record for %2.%3 was not created",
                                     tablestr(SqlDictionary), dictTable.name(), infoName));
        }
    }
    return ret;
}
;
dictTable = new DictTable(tableId);
if (!dictTable)
{
    throw error(strfmt(@"Failed to create %1 for '%2' (%3)",
                classstr(DictTable), tableid2name(tableId), tableId));
}
if (dictTable.isSystemTable())
{
    throw error(strfmt(@"'%1' is a system table, no way...", dictTable.name()));
}
if (!dictTable.isSql())
{
    throw error(strfmt(@"Table '%1' should not be in DB", dictTable.name()));
}
for (fieldId = dictTable.fieldNext(0); fieldId; fieldId = dictTable.fieldNext(fieldId))
{
    dictField = dictTable.fieldObject(fieldId);
    if (dictField && dictField.isSql())
    {
        fieldName = dictField.name();
        for (arrIdx = 1; arrIdx <= dictField.arraySize(); arrIdx++)
        {
            numOfSqlFields++;
            processTableField(dictField, arrIdx);
        }
    }
}
select firstonly sqlDict
    where   sqlDict.tabId   == tableId
        &&  sqlDict.fieldId == 0
            ;
if (!sqlDict)
{
    sqlDict.clear();
    sqlDict.initValue();
    sqlDict.tabId       = tableId;
    sqlDict.name        = strupr(dictTable.name());
    sqlDict.sqlName     = dictTable.name(DbBackend::Sql);
    sqlDict.strSize     = numOfSqlFields;       // for the table "header" - num of fields 
    sqlDict.flags       = dictTable.isView();   // that's the way it is
    sqlDict.insert();
    info(strfmt(@"Created record for table %1", dictTable.name()));
}

Créditos: http://gl00mie.blogspot.com.br/

 

Gráficos!!!!

Bomm diaaa a todos. . .

Hoje vamos clarear um pouco a visão sobre gráficos no AX. Apenas um início para ter uma noção desta funcionalidade que é bem útil para visualização mais rápida de alguns indicadores.

Utilize o Form tutorial do AX para ter noção de como é o gráfico

  1. Abra a OAT
  2. Exploda o nó “Forms”
  3. Abra o formulário “Tutorial_Form_Graph”

Neste exemplo vamos utilizar o gráfico com o conceito de tabela temporária que foi explicado no post anterior.

Portanto, depois de criar a tabela, inserir os dados através do método CreateTableTemp(), vamos fazer carregamento do gráfico na tela.

São necessários mais 2 métodos alem do método de inserção de dados na tabela temporária.

Lembrando que no ClassDeclaration temos que descalarar uma variável do tipo “GRAPHICS” pois esta é a classe que carrega todas as cores, estilos, formas, grades e tudo que existe num gráfico. (Para saber mais, exploda o nó Classes na OAT e veja a classe GRAPHICS).

Bom, vamos começar do zero.

  • Criar a tabela temporária (post Anterior explicando o processo).
  • Criar um Novo Form
  • Arraste sua tabela para o DataSource do Form.
  • No Design crie um group com os campos que você deseja que apareça no gráfico.
  • No Design crie um novo Group do tipo ACTIVEX e selecione a opção “ChartObject”.
  • No design crie mais um group do tipo STATICTEXT e na propriedade AutoDeclaration Marque “YES”

Agora vamos para os métodos de criação do gráfico.

init();

√ Chama os métodos para carregar os dados na sua tabela temporária e chama o método para carregar os dados no gráfico e mostrar na tela. O “Description.text()” que você pode perceber no final do método é o Group do tipo STATICTEXT que foi falar no início do post, nele você pode escrever uma breve descrição da funcionalidade do gráfico.

updateGraph();

√ Instancia um objeto do tipo GRAPHICS. A linha “graphics.parmActiveX(graphControl);” é referente ao group do tipo ACTIVEX que foi falado também no início do post e “graphControl é o nome do group no design. Os outros são apenas para colocar títilo no gráfico, no eixo X e Y. O group ActiveX é pra ser do tipo “ChartObject” que é onde você estipula de o gráfico é 3D ou não, basta alternar o parametro para true ou false e observar a diferença ao abrir o gráfico. Logo abaixo, segue um while selec apenas para carregar os dados no grafico pelo método “loadData”. Depois basta chamar o método ShowGraph para que apareça na tela.

Bom, isso é o básico para se ter um gráfico.

Os dois métodos seguintes são apenas para fazer a atualização do gráfico sempre que for alterado algum registro na tabela.

Métodos Write e Delete.

Lembrando que são métodos de substituição e o código dentro deles é apenas:

element.updateGraph();

Apenas de grande, acredito que ficou fácil e compreensível.

Qualquer dúvida, crítica ou sugestão, deixe seu comentário.

Obrigado!

Tabela temporária

Boa Tarde,

Vamos falar um pouco sobre tabelas temporárias.

Para criar uma tabela temporária é bem simples, basta criar uma tabela comum na OAT e na propriedade “Temporary” deixar marcada a opção “YES“.

Pronto, está criada.

Geralmente utilizamos tabelas temporárias para fazer ações que não é necessário ficar com os dados no banco de dados, apenas carregamos os dados na tabela temporáriamente, ou seja,  quando for feita determinada transação ou coisa do tipo.

Portanto para inserir registros na tabela temporária é necessário utilizar um método por exemplo “CreateInventVend_W” seguindo o exemplo da figura acima.

Dentro desse método, você insere os registros que quiser, conforme figura exemplo abaixo.

Suponhamos que você colocou os EDT´s (ItemNum, Name e Qty) que são responsáveis por armazenar o código, nome e quantidade de um produto.

Após isso se você fizer um SELECT na tabela, os dados que foram inseridos serão encontrados, porém por ela ser uma tabela temporária se você abrí-la pela AOT não vai constar nenhum dado.

Obs: Por ser uma tabela temporária, ela existe apenas na OAT, portanto você não vai encontrar ela no SQLServer.

Tree Form Control

Tree é um controle de formulário que exibe em formato de árvore uma hierarquia de informações, tornando assim a visualização de informações muito mais clara e intuitiva para o usuário final. Como padrão no Ax, temos o form “Exibição da organização”, no módulo de RH.

Exemplo de tree no Ax

O tree possui algumas características:

  • Os nós da árvore são sempre criados via código, não por drag-and-drop;
  • A lógica de construção da árvore deve ser colocada em uma classe nova, para certificar que ela possa ser reutilizada e não interferir no código do form.

Demonstração

Para demonstrar a utilização deste controle, utilizarei o formulário “BudgetModel”.

Antes de criar a árvore, temos que criar a classe que a construirá. Na AOT, crie uma classe e a nomeie “BudgetModelTree”.
Em sua class declaration, declare:

Crie os seguintes métodos:

New:

constructor:

createNode:

BuildTree:

Agora com a classe criada, podemos inserir o controle no form.
Abra o formulário na AOT e crie uma nova TabPage. Nomeie-a TabTree. Nesta tabpage, adicione o controle Tree. Nomeie-o ModelTree, mude as propriedades Height e Width para Column height e column width.

Declare a classe no ClassDeclaration do form;


Sobrescreva o método init do form;


Na tabpage, sobrescreva o método pageActivated

Se tudo deu certo e sua tabela possuia dados, a tab deverá parecer com a imagem abaixo:

Resumo:
Para separar a árvore do resto do form, criamos uma nova TabPage, onde colocamos o TreeControl.
Os nós sempre são gerados por código;
A lógica de construção da árvore fica em uma classe;
Além dos métodos new() e construct(), a classe possui dois métodos que realmente geram a árvore:
createNode(), um método recursivo que gera os “galhos” do nó;
buildTree(), que constrói a árvore. Primeiro, deleta todos os nós e trava a atualização da árvore. Depois, adiciona os nós-pai e chamando o método acima. Após inserir todos os nós, destrava a árvore.
A geração da hierarquia de informações pode ser demorada, por isso só a criamos quando necessário, ou seja, quando a tabpage é aberta. Esse é o motivo de termos utilizado o método “pageActivated()”.

SCRIPTS no Ax.

Bom pessoal, vou colocar aqui alguma coisa sobre scripts, na maioria das vezes são úteis durante o desenvolvimento, pois podemos padronizar alguns comentários de código e coisas do tipo.

Os scripts geralmente são utilizados em métodos para comentar/descomentar, colocar uma , tag inicial entre outros.
Para utilizar um script basta clicar com o botão direito do mouse sobre o code editor e escolher a opção “Scripts” e depois o script que você deseja utilizar.

Para tentar entender melhor vou explicar o script de comentário de código.

Se quiser acompanhar e tentar intender melhor algum outro script padrão do AX o local aonde eles ficam é:
AOT\Classe>>EditorScript –> La dentro ficam todos os scripts do AX.

A figura abaixo explica o script de comentário:

É possível criarmos o nosso próprio Script, porém vou deixar para o próximo post.

Obrigado e até mais. . .