Limpar contactos ou contas de atividade marketing falhadas

by Pedro Azevedo 11. December 2014 11:10

Boas pessoal,

Fui deparado com um problema que apareceu no fórum do Dynamics CRM em português. O objetivo do desafio passava por ter uma maneira mais expedita para eliminar\desativar contas ou contactos que tenham dado erro aquando de uma distribuição de uma campanha.

O primeiro desafio foi obter estes erros. Na interface gráfica os erros aparecem no seletor “Falhas”. Então a minha primeira tentativa foi através da interface REST e sobre a entidade CampaignActivity (Atividades de Campanha) mas sem sucesso porque aqui não aparecem os erros.

 

Depois de ver melhor a imagem reparei que a vista se chama “Vista associada de falhas de operações em massa”. Logo fui estudar a entidade BulkOperation mas aqui mais uma vez só encontrei a atividade chamada “Distribuição de Email” e não as atividades geradas. Depois de ver todas as relações da entidade, cheguei a entidade BulkOperationLog esta sim tem todas as atividades que foram geradas, as que correram bem e as que deram erro. Como não existe muita informação sobre esta entidade, executei uma chamada para ver o esquema desta entidade:

Esta entidade tem um campo o ErrorNumber que se tiver valor diferente de 0 é porque esta atividade deu erro. Infelizmente só tem mesmo o número do erro e não tem qualquer descrição. Bem este foi outro desafio. Eu já tinha escrito um post sobre erros e explicado como chegar a descrição. Para já vai ficar com o número do erro mas é meu objetivo de criar algo para me dar também a descrição do erro.

Tendo então descoberto qual a entidade que tinha a informação que necessitava e para além do erro ainda possuí qual o cliente\contacto através do campo RegardingObjectId. Podem ver a pré-visualização do resultado final:

Para listar esta informação utilizei um plugin jQuery chamado jqGrid e usei esta versão, já que a versão que tinha utilizado até aqui passou a ter uma licença paga. Para aqui também já evoluí este componente do jqGrid para facilitar listar entidades do CRM, mais tarde vou escrever um post sobre este componente.

Falando mais concretamente da implementação a primeira coisa foi colocar um botão ao nível da atividade de campanha. Botão este que irá chamar um recurso web html, este botão foi criado mais uma vez com recurso a ferramenta Ribbon Workbench:

Este botão vai chamar uma função Javascript onde é passado qual a campanha que estamos a falar neste momento e com isso mostrar todas as atividades que falharam:

function openWebResource(campaignId) {
    var customParameters = encodeURIComponent("?campaignId=" + campaignId);
    Xrm.Utility.openWebResource("new_failedcampaign/failedcampaign.html", customParameters, 300,300);
}

Este recurso web tem o plugin jqGrid e a sua inicialização, o método fetchGridData vai ser responsável por ir obter as atividades com falha e preencher a tabela.

$("#jqGrid").jqGrid({
        datatype: "local",
        height: '100%',
        colModel: [ {label: 'Id Registo', name: 'ObjectId', hidden: true},
                    {label: 'Tipo Registo', name: 'TypeRegister', width: 90},
                    {label: 'Nome do Registo', name: 'NomeRegisto', width: 200},
                    {label: 'Erro', name: 'ErrorNumber', width: 140 }
        ],
        viewrecords: true, // show the current page, data rang and total records on the toolbar
        caption: 'Carregar Atividades de Campanha Falhadas',
        pager: "#jqGridPager",
        gridComplete: initGrid
    });

    fetchGridData();
});

function fetchGridData() {
    var campaignActivityId;
    if (location.search != "") {
        vals = decodeURIComponent(location.search).split("=");
        campaignActivityId = vals[2].toString().split(',');
    }

    var gridArrayData = [];
    var odataquery = Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc";
    odataquery += "/CampaignActivitySet(guid'" + campaignActivityId + "')";
    odataquery += "?$expand=CampaignActivity_BulkOperations";
    odataquery += "&$select=CampaignActivity_BulkOperations/ActivityId";

    $.getJSON(odataquery, function(data) {
        if(data.d.CampaignActivity_BulkOperations) {
            var bulkOperations = data.d.CampaignActivity_BulkOperations.results;
            for (var idxBulkOper = 0; idxBulkOper < bulkOperations.length; ++idxBulkOper) {
                var activityId = bulkOperations[idxBulkOper].ActivityId;

                var odataquery2 = Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc";
                odataquery2 += "/BulkOperationSet(guid'" + activityId + "')";
                odataquery2 += "?$expand=BulkOperation_logs";
                odataquery2 += "&$select=BulkOperation_logs/ErrorNumber,BulkOperation_logs/RegardingObjectId";

                $.getJSON(odataquery2, function(data) {
                    var bulkLogs = data.d.BulkOperation_logs.results;

                    for (var i = 0; i < bulkLogs.length; i++) {
                        gridArrayData.push({
                            ObjectId: bulkLogs[i].RegardingObjectId.Id,
                            TypeRegister: bulkLogs[i].RegardingObjectId.LogicalName,
                            NomeRegisto: bulkLogs[i].RegardingObjectId.Name,
                            ErrorNumber: bulkLogs[i].ErrorNumber
                        });
                    };

                    $("#jqGrid").jqGrid('setGridParam', { data: gridArrayData});
                    $("#jqGrid").trigger('reloadGrid');
                });
            }
        }
    });
}

Quero realçar o seguinte código:

var odataquery = Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc";
odataquery += "/CampaignActivitySet(guid'" + campaignActivityId + "')";
odataquery += "?$expand=CampaignActivity_BulkOperations";
odataquery += "&$select=CampaignActivity_BulkOperations/ActivityId";

Este código vai obter todas as atividade de campanha e sobre cada uma obtenho os erros que aconteceram, como se pode ver a seguir:

var odataquery2 = Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc";
odataquery2 += "/BulkOperationSet(guid'" + activityId + "')";
odataquery2 += "?$expand=BulkOperation_logs";
odataquery2 += "&$select=BulkOperation_logs/ErrorNumber,BulkOperation_logs/RegardingObjectId";

Sobre o plugin jqGrid usei a extensão Context Menu para poder realizar ações sobre os registos listados, entre eles o inativar e o apagar. Para inativar usei a mensagem SetStateRequest, vejam aqui a função que utilizei:

function SetStateRequest(active, entity) {
    var grid = $("#jqGrid");
    var rowKey = grid.getGridParam("selrow");
    var rowData = grid.getRowData(rowKey);
    var entityId = rowData.ObjectId;

    var state, status;
    if(active){
        state = 0;
        status = 1;
    }
    else{
        state = 1;
        status = 2;
    }

   var requestMain = ""
   requestMain += "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">";
   requestMain += "  <s:Body>";
   requestMain += "    <Execute xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">";
   requestMain += "      <request i:type=\"b:SetStateRequest\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\" xmlns:b=\"http://schemas.microsoft.com/crm/2011/Contracts\">";
   requestMain += "        <a:Parameters xmlns:c=\"http://schemas.datacontract.org/2004/07/System.Collections.Generic\">";
   requestMain += "          <a:KeyValuePairOfstringanyType>";
   requestMain += "            <c:key>EntityMoniker</c:key>";
   requestMain += "            <c:value i:type=\"a:EntityReference\">";
   requestMain += "              <a:Id>" + entityId + "</a:Id>";
   requestMain += "              <a:LogicalName>" + entity + "</a:LogicalName>";
   requestMain += "              <a:Name i:nil=\"true\" />";
   requestMain += "            </c:value>";
   requestMain += "          </a:KeyValuePairOfstringanyType>";
   requestMain += "          <a:KeyValuePairOfstringanyType>";
   requestMain += "            <c:key>State</c:key>";
   requestMain += "            <c:value i:type=\"a:OptionSetValue\">";
   requestMain += "              <a:Value>" + state + "</a:Value>";
   requestMain += "            </c:value>";
   requestMain += "          </a:KeyValuePairOfstringanyType>";
   requestMain += "          <a:KeyValuePairOfstringanyType>";
   requestMain += "            <c:key>Status</c:key>";
   requestMain += "            <c:value i:type=\"a:OptionSetValue\">";
   requestMain += "              <a:Value>" + status + "</a:Value>";
   requestMain += "            </c:value>";
   requestMain += "          </a:KeyValuePairOfstringanyType>";
   requestMain += "        </a:Parameters>";
   requestMain += "        <a:RequestId i:nil=\"true\" />";
   requestMain += "        <a:RequestName>SetState</a:RequestName>";
   requestMain += "      </request>";
   requestMain += "    </Execute>";
   requestMain += "  </s:Body>";
   requestMain += "</s:Envelope>";
   var req = new XMLHttpRequest();
   req.open("POST", _getServerUrl(), true)
   // Responses will return XML. It isn't possible to return JSON.
   req.setRequestHeader("Accept", "application/xml, text/xml, */*");
   req.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
   req.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");
   var successCallback = null;
   var errorCallback = null;
   req.onreadystatechange = function () { SetStateResponse(req, successCallback, errorCallback); };
   req.send(requestMain);
}

Para apagar o registo usei a interface REST:

function DeleteEntity(accountId, entity) {
    var serverUrl = Xrm.Page.context.getClientUrl();
    var ODATA_ENDPOINT = "/XRMServices/2011/OrganizationData.svc/" + entity + "Set";
    var ODataPath = serverUrl + ODATA_ENDPOINT;
    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        datatype: "json",
        url: ODataPath + "(guid'" + accountId + "')",
        beforeSend: function (XMLHttpRequest) {
            XMLHttpRequest.setRequestHeader("Accept", "application/json");
            XMLHttpRequest.setRequestHeader("X-HTTP-Method", "DELETE");
        },
        error: function (xmlHttpRequest, textStatus, errorThrown) {
            alert("Status: " + textStatus + "; ErrorThrown: " + errorThrown);
        }
    });
} 

Reparem que para apagar temos que especificar o header “X-HTTP-Method” como delete.

Nesta solução a grande dificuldade foi mesmo a obtenção do tipo de erros referentes a esta comunicação o resto são coisas que já tínhamos visto anteriormente. Deixo-vos o ficheiro HTML do recurso web que usei, bem como a solução específica para o Dynamics CRM 2015. O meu objetivo é tornar esta solução num género de AddOn tanto para qualquer versão do CRM.

 

Até a próxima.

Tags: , , , , , , ,

Obter dados de um SubGrid

by Pedro Azevedo 8. August 2013 01:12

Boas pessoal,

Tinha um requisito que era calcular o valor médio de uma lista. Neste caso vou demonstrar através da lista de oportunidades em que o objectivo é calcular a média do valor dos produtos inseridos.

Bom a minha primeira solução foi obter os registos directamente a grid, já que lá estavam os dados, assim fiz então baseei-me em alguns blogs ora vejam:

http://lakshmanindian.wordpress.com/2012/05/25/retrieve-subgrid-rows-in-crm-2011-using-jscript/

http://stackoverflow.com/questions/17651752/how-to-retrieve-all-record-ids-of-a-sub-grid-in-crm-2011-using-javascript

http://crmbusiness.wordpress.com/2011/05/19/crm-2011-javascript-and-subgrids-code-example/

Mas sempre tive problemas:

  • ·         Com o ir buscar a grid, com o código pré e após UR12
  • ·         Como ir buscar todos os elementos da grid, pois alguns dos métodos só vão buscar os registos que estão visíveis.
  • ·         Ter os campos necessários na lista

Então passei a enveredar por outra solução que é realizar uma query odata para obter todos os registos e apenas os campos que necessito. Tenho um overhead de obter todos os dados novamente, mas tenho uma solução que sei que vai perdurar no tempo pois qualquer mudança na forma de obter os registos de uma subgrid vou ficar imune.

Independentemente da forma como vou buscar os produtos da oportunidade terei que adicionar um Recurso Web em que neste caso terá apenas uma caixa de texto. Aqui podia não ter optado por um Recurso Web e ter colocado num campo que criasse como atributo da oportunidade. O Recurso Web permite ter campos dinâmicos, no requisito que referi tinha que realizar várias análises de qual o produto com maior valor, qual o produto com mais unidades, qual o produto que demorava mais a instalar. Por essa razão e porque o vosso caso pode ser semelhante vou manter a solução de um Recurso Web. Podem ver neste artigo um pouco mais sobre Recurso Web.

Vamos então adicionar um Recurso Web a nossa solução:

E colocar o seguinte código:

<HTML><HEAD><BASE>
<META charset=utf-8></HEAD>
<BODY contentEditable=true><INPUT id=quantity name=quantity>

<SCRIPT type=text/javascript src="ret_jquery"></SCRIPT>
<SCRIPT src="ClientGlobalContext.js.aspx"></SCRIPT>
<SCRIPT type=text/javascript>
  $(document).ready(function(){
    var entityId = window.parent.Xrm.Page.data.entity.getId();
    var serverUrl = window.location.protocol + "//" + window.location.host + "/" + Xrm.Page.context.getOrgUniqueName();
    var ODATA_ENDPOINT = "/XRMServices/2011/OrganizationData.svc";

    var odataSetName = "OpportunitySet";
    var relName = "/product_opportunities";

    var odataSelect = serverUrl + ODATA_ENDPOINT + "/" + odataSetName + "(guid'" + entityId.substr(1,36) + "')" + relName;

    return $.ajax({
       type: "GET",
       contentType: "application/json; charset=utf-8",
       datatype: "json",
       url: odataSelect,
       beforeSend: function (XMLHttpRequest) { XMLHttpRequest.setRequestHeader("Accept", "application/json"); },
       success: function (data, textStatus, XmlHttpRequest){
              RetrieveMultipleEntities(data.d.results);
       },
       error: function (XmlHttpRequest, textStatus, errorThrown) { alert('TextStatus:' + textStatus + "\nerror:" + errorThrown); }
    });
  });

  function RetrieveMultipleEntities(ManyEntities)
  {
    var total = 0;
    for( i=0; i< ManyEntities.length; i++){
      var Entity = ManyEntities[i];
      total = total + parseFloat(Entity.BaseAmount.Value)
    }
    $('#quantity').val(total/ManyEntities.length);
  }
</SCRIPT>

<P><FONT size=2 face=Tahoma></FONT> </P></BODY></HTML>

E o nosso desenvolvimento acabou, basta adicionarmos este Recurso Web dentro do formulário (que vamos fazer mais tarde) e este código funciona as mil maravilhas. Vamos explorar alguns pormenores que apesar de já estar explicado em outros artigos que escrevi, nunca é demais referir:

·         Este código pressupõe termos adicionado um Recurso Web com a biblioteca jQuery, vejam aqui como referencio:

<SCRIPT type=text/javascript src="ret_jquery"></SCRIPT>

·         A referência a biblioteca para podermos aceder ao contexto da entidade:

<SCRIPT src="ClientGlobalContext.js.aspx"></SCRIPT>

·         A construção do URL para obter o endpoint OData:

var serverUrl = window.location.protocol + "//" + window.location.host + "/" + Xrm.Page.context.getOrgUniqueName();
var ODATA_ENDPOINT = "/XRMServices/2011/OrganizationData.svc";

Vejam a utilização do Xrm.Page.context para obter o contexto, que sem a referenciação a biblioteca anterior não funcionava. O resultado da variável deve ficar algo do género:

http://<server>/<organization>/XRMServices/2011/OrganizationData.svc

·         Por último a obtenção do GUID do registo onde estamos:

var entityId = window.parent.Xrm.Page.data.entity.getId();

Vejam como eu referencio o Xrm.Page para poder aceder por exemplo aos campos do formulário.

Se tivéssemos optado por não utilizar um Recurso Web o código era semelhante, não teríamos que referenciar as duas bibliotecas Javascript (apesar de termos que incluir a biblioteca jQuery na solução) e na obtenção do id do registo podemos retirar a parte do window.parent.

Como tinha referenciado agora basta adicionarmos o Recurso Web ao formulário: 

 

Vejam o resultado final:

 

Até a próxima.

Tags: , , , ,

Biblioteca XrmServiceToolkit

by Pedro Azevedo 5. January 2013 23:17

Boas pessoal,

Hoje queria ressalvar um grande projeto oferecido a comunidade. É a biblioteca XrmServiceToolkit que podemos fazer download daqui: https://xrmservicetoolkit.codeplex.com/.

Esta biblioteca que tem um ficheiro único XrmServiceToolkit.js e basta colocar como Recurso Web. Nela temos funções genéricas, SOAP ou REST e que esconde toda a construção destas chamadas e que ajuda muito especialmente em requisições SOAP. Existe também os chamados métodos extendidos que permite por exemplo trabalhar com OptionSet dependentes.

Temos a possibilidade de fazermos estas chamadas síncronas ou assíncronas. Para além de suportar qualquer deploy do Dynamics CRM 2011 (On-Prem; IFD e Online)

Em resumo um espectáculo e mais qualquer alteração por exemplo na questão de suportar outros browsers e não podermos usar esta declaração var xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"), esta biblioteca foi logo atualizada por isso só mudou um sitio.

 

Esta biblioteca depende de outras duas bibliotecas jQuery e JSON. Vocês podem ver as versões necessárias no site que referi.

Exemplo funções genéricas:

XrmServiceToolkit.Common.enableField(fieldName);

Exemplo Actualizar REST:

var account = {};
account.Name = "My Name";
account.Address1_City = "Lisboa";
account.Address1_Line1 = "Minha Rua";
account.EMailAddress1 = "psa@psazevedo.com";

XrmServiceToolkit.Rest.Update(
	accountId,
	account,
	"AccountSet",
	function () {
		alert("Foi atualizado");
	},
	function (error) {
		alert("ups... Erro")
	},
	false
);


Exemplo Retorno Múltiplo em REST:

XrmServiceToolkit.Rest.RetrieveMultiple(
    "ContactSet",
    "$select=ContactId, FullName",
    function (results) {
        alert(results.length);
        var fullname = results[0].FullName;
        alert(fullname);
    },
    function (error) {
        alert(error)
    },
    function onComplete() {
        alert('completed')
    },
    false
);

Exemplo Criação em SOAP:

var createContact = new XrmServiceToolkit.Soap.BusinessEntity("contact");
createContact.attributes["firstname"] = "Pedro";
createContact.attributes["lastname"] = "Azevedo";
createContact.attributes["gendercode"] = { value: 2, type: "OptionSetValue" };
createContact.attributes["birthdate"] = birthDate;
createContact.attributes["donotemail"] = true;
createContact.attributes["donotphone"] = false;
createContact.attributes["parentcustomerid"] = { id: accountId, logicalName: "account", type: "EntityReference" };
contactId = XrmServiceToolkit.Soap.Create(createContact);

Exemplo Pesquisa pelo ID:

var contactid = Xrm.Page.getAttribute("primarycontactid").getValue()[0].id;
var cols = ["firstname", "lastname", "middlename", "familystatuscode"];
var retrievedContact = XrmServiceToolkit.Soap.Retrieve("contact", contactid, cols);
alert(retrievedContact.attributes['lastname'].value);

Aproveitem estas benesses que a comunidade nos dá. Futuramente irei falar de outras bilbliotecas\ferramentas para nos auxiliar no dia-a-dia. Um bem haja ao Jaimie.

Até a próxima

Tags: , , , ,

About

Muito bem casado, Pai babado e um gosto muito grande pela tecnologia.

Tenho um lema "Sharing is Learning"

Mais aqui -> http://www.psazevedo.com

Month List