27 Nisan 2021 Salı

AX 2012 - SFTP Download

 Bir müşterimizin SFTP download yaptığı veri sağlayıcı SFTP servisini yenilemiş. Mevcut kod 2007'den beri güncellenmeyen Tamir.SharpSSH.DLL'i kullanıyordu. Malesef DLL yeni servisle çalışmadı, "Tamir.SharpSsh.jsch.JSchException: Algorithm negotiation fail" hatası verdi. Hatayla ilgili internette bulduğum çözümlerden hiçbiri malesef çalışmadı. 

Forum sayfalarında yaptığım araştırmadan SSH.NET'in tavsiye edildiğini gördüm. SSH.NET'in DLL'sini direk AX içinden referanslara ekleyip kullanmaya kalktığımda malesef download metodunda opsiyonel olan parametrenin X++'a gelirken zorunlu olduğunu ve daha da kötüsü bu paremetrenin X++ tarafından desteklenmeyen Action<ulong> türü olduğunu gördüm. Önce bir blogda gördüğüm çözümü denedim, metodu overload etmeyi öneriyordu, SSH.NET open source bir library olduğu için yapılabilir bir çözüm gibi geldi, ancak derlemede bir takım hatalar aldım ve hatalarla uğraşmaktansa SSH.NET DLL'yi kullanan bir DLL yazmaya karar verdim. Bu hem daha kolay olacaktı, hem de SSH.NET'in yeni bir versiyonu çıktığında güncellemek için tekrar kod düzenleme ve derleme gerekmeyecekti.

Aşağıdaki C# kodunu Dynamics 365 F&O için yazılmış bir blog sayfasındaki kodu biraz değiştirerek yazdım:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Renci.SshNet;
using System.IO;

namespace SSHNet2
{
    public class SSHNet2
    {
        public SftpClient sftpClient;
        public void OpenSFTPConnection(string host, int port, string username, string password)
        {
            if (this.sftpClient == null)
            {
                this.sftpClient = new Renci.SshNet.SftpClient(host, port, username, password);
            }
            if (!this.sftpClient.IsConnected)
            {
                this.sftpClient.Connect();
            }
        }
        public List<string> GetDirectories(string path)
        {
            return this.sftpClient.ListDirectory(path).Select(n => n.Name).ToList();
        }
        public void MoveFile(string sourcePath, string destinationPath, bool isPosix)
        {
            this.sftpClient.RenameFile(sourcePath, destinationPath, isPosix);
        }

         public void DeleteFile(string sourcePath)
        {
            this.sftpClient.DeleteFile(sourcePath);
        }
        public Stream DownloadFile(string sourcePath)
        {
            var memoryStream = new MemoryStream();
            this.sftpClient.DownloadFile(sourcePath, memoryStream);
            memoryStream.Position = 0;
            return memoryStream;
        }
    }
}

Oluşturduğum DLL'yi ve SSH.NET'in kendi DLL'sini AX klasörlerine kopyalayıp aşağıdaki kodla SFTP sorunumu çözmüş oldum:


System.IO.StreamReader          streamReader; 

System.IO.StreamWriter          streamWriter; 

 SSHNet2.SSHNet2                 sftp = new SSHNet2.SSHNet2();

System.IO.Stream                stream;

 sftp.OpenSFTPConnection(_RemoteAddress,any2int(_RemotePort), _RemoteUser, _RemotePassword);

            stream = sftp.DownloadFile(_RemoteFileName);

streamReader = new System.IO.StreamReader(stream);

streamWriter = new System.IO.StreamWriter("c:\test\test.txt");

streamWriter.Write(_StreamReader.ReadToEnd());

streamWriter.Flush();

streamWriter.Close();

streamReader.Close();      



9 Mart 2021 Salı

D365 F&O - Form kontrolüne sağ mouse popup menü eklemek

 Bu konuda hem AX 2009 hem AX 2012'de çalışan bir örnek paylaşımı yapmıştım 2015 yılında. 365'de yapı biraz değişmiş, bu sebeple artık o kod çalışmıyor. Microsoft öncesi-sonrası diye bu konuyu anlatan bir paylaşım yapmış, ben de oradan yararlandım. Daha önce tek metodda halledilen iş iki metoda çıkmış.

 Aşağıdaki constantları class declaration'a tanımlıyoruz. Constant kullanımı tabii ki zorunlu değil ama okunurluğu arttırıyor:

public const int listCredit = 1;
public const int listJournal = 2;

Bu metodu popup çalışmasını istediğimiz kontrole koyuyoruz. Bu metod popup menüyü çalıştırıyor. Aşağıdaki örnekte iki popup bar var:

        public str getContextMenuOptions()
        {
            str ret;
            ContextMenu menu = new ContextMenu();
            List menuOptions = new List(Types::Class);
            ContextMenuOption option;
            
            option = ContextMenuOption::Create("Credit number", listCredit);
            menuOptions.addEnd(option);

            option = ContextMenuOption::Create("Journal number", listJournal);
            menuOptions.addEnd(option);

            menu.ContextMenuOptions(menuOptions);
            return menu.Serialize();
        }

 


 Bu metod da popup menüde seçim yapılınca çalışıyor:

 public void selectedMenuOption(int selectedOption)
        {
            switch (selectedOption)
            {
                case -1:
                    break;
                case listCredit:

                    info("Bar 1 seçildi!..");
                    break;
                case listJournal:
                    info("Bar 2 seçildi!..");
                    break;
            }
        }


Benim yaptığım denemelerde popup menü bazen görünmedi. Böyle bir duruma 2012 veya 2009'da hiç rastlamamıştım. Sebebini bulamadım. Popup menünün görünmediği durumlarda debug yaptığımda getContextMenuOptions metodunun tetiklenmediğini gördüm.

4 Mart 2021 Perşembe

AX 2012/365 F&O - Satıcı/müşteri hareket kapatma

  public static void settlement(CustVendTransOpen _trans1,CustVendTransOpen _trans2,
        CustVendACType _type = CustVendACType::Vend)
    {
        CustVendOpenTransManager manager;

        if (_type == CustVendACType::Vend)
            manager = CustVendOpenTransManager::construct(VendTable::find(_trans1.AccountNum));
        else
            manager = CustVendOpenTransManager::construct(CustTable::find(_trans1.AccountNum));
        manager.updateTransMarked(_trans1, true);
        manager.updateTransMarked(_trans2, true);
        manager.settleMarkedTrans();
    }

26 Ocak 2021 Salı

AX 2012 - Args ile filtre göndermek

Args ile filtreniz sadece bir kayıttan ibaretse args.record() kullanarak filtreleyebilirsiniz, record metodu ile birden fazla kayıt gönderemezsiniz. Bunun için InitialQueryParameter sınıfını kullanabilirsiniz: 

            Args                 args = new Args();
            MenuFunction         MenuFunction = new MenuFunction('LedgerJournalTable', MenuItemType::Display);
            Query                q = new Query();
            QueryBuildDataSource QBDS = q.addDataSource(tableNum(LedgerJournalTable));

            if (ABCRCreditTable.AccualJournalNum != "")
                QBDS.addRange(fieldNum(LedgerJournalTable,JournalNum)).value(ABCRCreditTable.AccualJournalNum);
            if (ABCRCreditTable.AccualJournalNum2 != "")
                QBDS.addRange(fieldNum(LedgerJournalTable,JournalNum)).value(ABCRCreditTable.AccualJournalNum2);

            args.initialQuery(InitialQueryParameter::createByQuery(q));
            args.caller(this);

            MenuFunction.run(args);  


3 Ekim 2020 Cumartesi

AX 2012 - Axapta ile Rest Api kullanımı

Çalıştığım AX partner AGC Yazılım'daki bir müşterimizin hizmet aldığı servis Rest Api kullanıyordu. Forumlarda AX 2012 için yazılmış değişik örnekler buldum fakat hiçbirini çalıştırmayı başaramadım. O noktada baştan başlamaya karar verdim. 

Önce bilgisayarıma Postman'ın ücretsiz versiyonunu yükledim. Postman ile müşterinin servisini kullanmayı başardım. 

Daha sonra Postman'ın C# RestSharp kütüphanesi kodu üretme özelliğiyle C# koduna sahip oldum:





Bu kodu AX ile deploy edilebilecek bir koda çevirmeden önce projeme RestSharp kütüphanesini import etmem gerekiyordu. Visual Studio 2013 Package Manager Console ile aşağıdaki komutu yazıp  import etmeye çalıştığımda

Install-Package RestSharp 





"The underlying connection was closed: An unexpected error occurred on a send." hatası aldım. Hatanın çözümünü bir forum sayfasında buldum. Aşağıdaki kodu bir .REG dosyasına yazıp çalıştırınca artık Nuget pakedi yüklenebiliyordu:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v4.0.30319]
"SchUseStrongCrypto"=dword:00000001

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v4.0.30319]
"SchUseStrongCrypto"=dword:00000001


Ancak bu sefer de son RestSharp versiyonunun Visual Studio 2013 ile çalışmadığını gördüm:





Bir başka forum sayfasından Visual Studio 2013 ile çalışan son RestSharp versiyonunu öğrendim ve import edebildim:

Install-Package RestSharp -Version 103.1.0


Artık çalışan ve AX içinde deploy edebildiğim bir kodum vardı:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RestSharp;

namespace Ingenico
{
    public class Ingenico
    {
        public string StartDate { get; set; }
        public string EndDate { get; set; }
        public string Body { get; set; }
        public string Idx { get; set; }
        public string getPaymentSummaryReport()
        {
            var client = new RestClient("https://XXXXXXXXXXXXXXX/GetPaymentSummaryReport");
            client.Timeout = -1;
            var request = new RestRequest(Method.POST);
            request.AddHeader("Operator", "XXXXXXX");
            request.AddHeader("Authorization", "Basic XXXXXXX");
            request.AddHeader("Content-Type", "application/json");
            Body = "{\r\n\"ReportId\": "+Idx+",\r\n\"SerialList\": [\r\n\"\"\r\n],\r\n\"StartDate\": \""+StartDate+
            "\",\r\n\"EndDate\": \""+EndDate+
            "\",\r\n\"ExternalField\": \"\",\r\n\"ReceiptNo\": 0,\r\n\"ZNo\": 0,\r\n\"GetSummary\": true,\r\n\"SaleFlags\": 0,\r\n\"DeviceGroupName\": \"\"\r\n}\r\n       \r\n";
            request.AddParameter("application/json",Body, ParameterType.RequestBody);
            IRestResponse response = client.Execute(request);
            return response.Content;
        }
    }
}


Kodu deploy edip AX içinde çalıştırmaya kalktığımda proje klasöründeki RestSharp.DLL ve RestSharp.XML dosyalarını da aradığını ve AX'ın bunları bin klasörüne kopyalamadığını gördüm. Bu dosyaları server ve client bin klasörlerine kopyaladım:



Artık servise erişebiliyordum:

    Ingenico.Ingenico o = new Ingenico.Ingenico();
    str a;

    try
    {
        o.set_StartDate("2020-10-01");
        o.set_EndDate("2020-10-01");
        o.set_Idx("111111111111");
        a = o.getPaymentSummaryReport();

    info(a);


Gelen Json stringi çözümleyebilmek için de yine bir blogdan faydalandım.

 

class AGCingenico
{
    Array           jsonArray;
 
}

Önce bu metod ile json string çözümleniyor:

void parseJson(str  _json)

{
   Array           jsonArray;
   container       con;

   int             jsonSubstrNo, i;

   TextBuffer      textBuffer;

   Array           returnArray = new Array(Types::Class);

   #Define.LBracket('{')

   #Define.RBracket('}')
    str st;
    int fnd;



    fnd = strScan(_json,"]",1,strLen(_json));

    st="{"+subStr(_json,fnd+2,strlen(_json) - fnd -1);
    con+=st;



   // extract the json substrings and add them to a Array

   textBuffer = new TextBuffer();

   textBuffer.setText(_json);

   while (textBuffer.nextToken(0, #LBracket+#RBracket))

   {

       if ((jsonSubstrNo mod 2) > 0)
       {
           st =#LBracket + textBuffer.token() + #RBracket;
           con += st;
       }
       jsonSubstrNo++;

   }


   for (i = 1; i <= conLen(con); i++)

   {

       returnArray.value(i, RetailCommonWebAPI::getMapFromJsonString(conPeek(con, i)));

   }

   jsonArray = returnArray;

}

Daha sonra bu metod ile tabloya atıyorum:

void jsonToTable()
{
   MapIterator                 mi;
   Map                         data;
   int                         i;
   str                         value;
   

   for (i = 1; i <= jsonArray.lastIndex(); i++)
   {
       data = jsonArray.value(i);
       mi = new MapIterator(data);
       tmpTable.clear();

       while (mi.more())
       {
           value = data.lookup(mi.key());
     
           switch (mi.key())
           {
               case "ErrCode":
                this.parmErrCode(str2int(value));
                break;
               case "ErrDesc":
                this.parmErrStr(value);
                break;
               case "Amount":
                tmpTable.Amount = str2num_RU(value);
                break;
               case "AmountCredit":
                tmpTable.AmountCredit = str2num_RU(value);
                break;

...

...

...


Bütün iş bitti derken son bir sürprizle daha karşılaştım. Client tarafında düzgün çalışan kod CIL içinde tip dönüştürmeyle ilgili bir hata veriyordu. Bunun çözümünü de yine bir forum sayfasından buldum. Martin Drab çözümü nokta atışı veriyordu. RetailCommonWebAPI sınıfında aşağıdaki düzeltmeyi yaptım ve başka hata kalmamıştı:

//AGC memre 29.9.20
//private static Map getMap(CLRObject _dict)
private static Map getMap(System.Collections.IDictionary _dict)
//http://dev.goshoom.net/en/2016/01/x-to-cil-object-must-implement-iconvertible/#comment-135186
//AGC memre
{


Sonuç olarak AX 2012 ile Rest Api kullanımı her ne kadar kolay olsa da ortamı uyarlayana kadar çeşitli pürüzler çıkıyor, neyse ki forumlar ve bloglar var. :)


4 Eylül 2020 Cuma

AX 2012 - Workflow formu açılırken hata

  Bir müşterimizde Workflow formu açılmıyordu. Üstüne üstlük verdiği hata mesajı çok da açıklayıcı değildi:





Event log incelemesi yaptığımda hatayla ilgili bir log kaydı bulamadım. İnternette tavsiye edilen çözümlerin tümünü sırayla denedim, ancak hiçbiri işe yaramadı:

-Incremental CIL

-Full CL

-XppIL klasörünü silip AOS restart

-Full compile ve Full CIL

-Client reinstall


Bir blogda WorkflowEditorHost formunda try/catch bloğunda CRL hataları için iç hata yakalaması yapılmadığından bahsediliyordu. Bloggerın yaptığı gibi koda ekleme yapmaya karar verdim ve aşağıdaki kırmızı işaretli satırları ekledim:

 

private void build()
{
    #Admin
    #AOT
     //AGC memre
    #OCCRetryCount
    System.Exception ex;
    //AGC memre



    WorkflowVersionTable        versionTable;
    WorkflowTypeName            templateName;
    str                         domainUser;
    UserInfo                    userInfo;
    NumberSeq                   num;
    SysInfoAction_MenuFunction  sysInfoAction;
    TreeNode                    treeNode;
    SysDictWorkflowType            workflowType;
    int                         classId;

    num = NumberSeq::newGetNum(SysWorkflowParameters::numRefSequenceId(), false, true);
    if (num == null)
    {
        sysInfoAction = SysInfoAction_MenuFunction::newMenuItem(menuitemDisplayStr(SystemParameters), MenuItemType::Display);
        throw error("@SYS108268", '', sysInfoAction);
    }

    versionTable = element.args().record();

    //BP Deviation Documented
    select userInfo where userInfo.Id == curUserId();
    domainUser = userInfo.NetworkDomain + '\\' + userInfo.NetworkAlias;

    if (element.args().parmEnumType() == enumNum(WorkflowConfigurationActionType) &&
        element.args().parmEnum() == WorkflowConfigurationActionType::New)
    {
        templateName = element.args().parm();
        treeNode = TreeNode::findNode(#WorkflowTypesPath + #AOTDelimiter + templateName);

        if (treeNode)
        {
            workflowType = new SysDictWorkflowType(templateName);
            classId = className2Id(workflowType.document());
            if(classId == 0)
            {
                throw error("@SYS108554" + '. ' + "@SYS113219");
            }

            try
            {
                workflowConfiguration = Microsoft.Dynamics.AX.Framework.Workflow.Model.WorkflowModel::
                                            Create(templateName, curext(), curUserId(), domainUser);
            }
            catch (Exception::CLRError)
            {
            //AGC memre
            ex = ClrInterop::getLastException();
            if (ex != null)
            {
                ex = ex.get_InnerException();
                if (ex != null)
                {
                    error(ex.ToString());
                }
                else
                    error(AifUtil::getClrErrorMessage());
            }
            else
                    error(AifUtil::getClrErrorMessage());
            //AGC memre           
               
throw error("@SYS327400");
            }
        }
        else
        {
            throw error(strFmt("@SYS106830", templateName));
        }
    }
    else
    {
        try
        {
            workflowConfiguration = Microsoft.Dynamics.AX.Framework.Workflow.Model.WorkflowModel::Create(versionTable.ConfigurationId, curext(), domainUser);
        }
        catch (Exception::CLRError)
        {
            //AGC memre
            ex = ClrInterop::getLastException();
            if (ex != null)
            {
                ex = ex.get_InnerException();
                if (ex != null)
                {
                    error(ex.ToString());
                }
                else
                    error(AifUtil::getClrErrorMessage());
            }
            else
                    error(AifUtil::getClrErrorMessage());
            //AGC memre           
           
throw error("@SYS327400");
        }
    }

}





public void run()
{
    //AGC memre
    #OCCRetryCount
    System.Exception ex;
    //AGC memre

    try
    {
        modelEditorControl.Load(workflowConfiguration, userSettings);
        workflowEditorPane.initializeActionPane();
        if (element.args().openMode() == OpenMode::View)
        {
            isReadOnly = true;
            modelEditorControl.set_IsReadOnly(true);
            modifyElementButtonGroup.caption("@SYS322934");
            modifyWorkflow.caption("@SYS322935");
            viewToolboxButton.visible(false);
        }
    }
    catch (Exception::CLRError)
    {
    //AGC memre
        ex = ClrInterop::getLastException();
        if (ex != null)
        {
            ex = ex.get_InnerException();
            if (ex != null)
            {
                error(ex.ToString());
            }
            else
                error(AifUtil::getClrErrorMessage());
        }
        else
                error(AifUtil::getClrErrorMessage());
    //AGC memre
        throw error("@SYS327400");

    }

    super();
}

 

Artık hata mesajını açıkça alabiliyordum:

 



Inbound portlara baktım, Windows güvenlik duvarını inceledim, Ax32Serv.exe.config dosyasını inceledim, hiçbirinde sorun görünmüyordu. Client configuration tool bir türlü WCF refresh yapamıyordu:

 


 

Her ne kadar bilgisayarda tek bir AOS olsa da Instance Name yazmayı denedim:



Sorun çözülmüştü: