Wednesday, February 23, 2011

Save an Microsoft Dynamics AX 2009 report to a PDF file (Second Part) - Save the file to a network location

In this part of the article we'll continue to build our functionality to save an Axapta report to a PDF format, then save this file to a network location, and then send it as an attachment in outlook.

The firt part of this series in here http://axwonders.blogspot.com/2011/02/save-microsoft-dynamics-ax-2009-report.html

In the prior article I created a job to save an Axapta report to PDF into a local drive ... C:\. In this article, the code from the last article is integrated within the Run() method of a class I created called SalesConfirmReportEmail.

The SalesConfirmReportEmail class gets executed when a a user wants to send and post an Order Acknowledgement on a Sales Order. I will not discuss how the code works from the moment the user clicks ok in the SalesEditLines form, but I will point out the path that the code follows:

So, the user opens the Sales Order form, finds a record, goes to Posting > Acknowledgement , does whatever he/she needs to do and clicks OK.

In my implementation, the user can choose the output for the order confirmation report. This is, in the DocDestination drop down list he/she can choose Preview, Print, and/or Email. In this case the user wants to print the report.



As soon as the user clicks OK, the code goes through the following:
  • Classes\FormMenuButton\Clicked
  • Classes\FormFunctionButtonCOntrol\Clicked
  • Classes\SalesFormLetter\Main
  • Classes\SalesFormLetter\MainOnServer
  • Classes\SalesFormLetter\Run
  • Classes\SalesFormLetter_Confirm\printJournal
  • Data Dictionary\Tables\CustConfirmJour\Methods\printJournal
  • Classes\SalesConfirmReportEmail\postingPrint
  • Classes\SalesConfirmReportEmail\run
  • Classes\SysInetMail\SendMailAttach
  • Classes\SysINetOutlook\AddAttachment
The following is the code of CustConfirmJour class, which is where I call the SalesConfirmReportEmail class

void printJournal(SalesFormLetter   salesFormLetter = null,
                  RecordSortedList  journalList     = null)
{
    Args            parameters = new Args();
    MenuFunction    salesConfirmMenu;
    ;

                              
    if(salesFormLetter && salesFormLetter.SalesParmUpdate().DocDestination == glPrintType::Printer)
    {
        salesConfirmMenu = new MenuFunction(menuItemoutputStr(SalesConfirmation), MenuItemType::Output);

        parameters.caller(salesFormLetter);
        if (journalList)
        {
            parameters.object(journalList);
        }
        else
        {
            parameters.record(this);
        }
        salesConfirmMenu.run(parameters);
    }
    else
    {
        //Calling my report confirmation class
       //Notice that I'm passing the salesformletter class and journal list
        SalesConfirmReportEmail::postingPrint(salesFormLetter,journalList);
    }
}

The following is the code for the SalesConfirmReportEmail class. It is going to be long.

class SalesConfirmReportEmail
{
    RecordSortedList        journalList;
    glPrintType             printType;
    PrintJobSettings        printJobSetting;
    FileName                fileName;
    FilePath                filePath;
   
    //Earias - 2/22/2011
    SalesTable              salesTable;
    CustConfirmJour         custConfirmJour;
    DirPartyId              dirPartyId;
    SalesQuotationTable     salesQuotationTable;
    Filename                prtFile;
    Filepath                prtFilePath;
    Email                   email;
    Email                   CCEmail;
    Telefax                 fax;
    SysINetMail_GLBTS       mail;
    str                     _mailBody;
}

Email CCEMail(SalesTable    _salesTable)
{
    Email   _ccEmail;
    EmplTable   _emplTableOutside;
    EmplTable   _emplTableInside;
    EmplId      _emplIdInside;
    ;

    _emplIdInside = EmplTable::userId2EmplId(_salesTable.SalesResponsible);

    if (_salesTable.SalesTaker == _emplIdInside)
        _ccEmail = Empltable::find(_salesTable.SalesTaker).email();     //outside salesperson set with emplId
    else
    {
        _ccEmail = Empltable::find(_salesTable.SalesTaker).Email();     //outside salesperson set with emplId

        if (!_ccEmail)
            _ccEmail = Empltable::find(_emplIdInside).Email(); //inside salesperson set with UserId
        else
            _ccEmail += "; " + Empltable::find(_emplIdInside).Email(); //inside salesperson
    }

    return _ccEmail;
}

public FileName parmFileName(FileName _fileName = fileName)
{
    ;
    fileName = _fileName;

    return fileName;
}


glPrintType parmglPrintType(glPrintType _printType = printType)
{
    ;
    printType = _printType;
    return printType;
}

void parmJournalList(Common _common)
{
    ;
    journalList = FormLetter::createJournalListCopy(_common);

}

Container parmPrintJobSettings(PrintJobSettings _printJobSetting = printJobSetting)
{

    ;
    return printJobSetting.packPrintJobSettings();
}

void printJournal(CustConfirmJour _custConfirmJour)
{
     Args            parameters = new Args();
    MenuFunction     salesConfirmMenu;

    ;
    salesConfirmMenu = new MenuFunction(menuItemoutputStr(SalesConfirmation),MenuItemType::Output);
    parameters.caller(this);
    parameters.record(_custConfirmJour);

    salesConfirmMenu.run(parameters);

}

void setJournalList(RecordSortedList _journalList)
{
    journalList = _journalList;
}

static void main (Args _args)
{

    SalesConfirmReportEmail thisClass = new SalesConfirmReportEmail();
  
    ;
    breakpoint;
    if(!_args.caller())
    {
        throw error("Invalid calling location");
    }
   
    thisClass.parmglPrintType(_args.parmEnum());
    thisClass.parmJournalList(_args.record());
    thisClass.run();
}

static void postingPrint(SalesFormLetter      salesFormLetter,
                         RecordSortedList     journalList)
{
    SalesConfirmReportEmail thisClass = new SalesConfirmReportEmail();
    ;
    thisClass.parmglPrintType(salesFormLetter.SalesParmUpdate().DocDestination);
    thisClass.setJournalList(journalList);
    thisclass.run();
}

static glPrintType reportPrintType()
{
    glPrintType printType;
    ;
    return printType;
}

public DirPartyId GetContactEmailFromDirParty(SalesTable _salesTable)
{
    ;
   
    dirPartyId = CustTable::find(_salesTable.CustAccount, false).PartyId;
    return dirPartyId;
}


Now, the Run()  Method is very long. Here there are a few things that I want to explain before I go on. In this method I'm saving my Axapta report in a Network folder.

Yo handle this I did not want to have a constant string where the netwrok address is assigned. The reason is that I also needed to specify an alternative address in case the Network address was not available. So, if the \\NetworkLocation.com\MyFolder is not available, then we will save the file to a temp folder in the user's machine.

For this I created a table names DocuParameters as Shown below:




Then, I'm getting the PrimaryFileOutputPath from this table, if this is not Available, then I'm getting the AlternativeFileOutputPath. I guess there are a  100 ways to do this. I just choose to do it like this as it is more centralize for my purposes.

Also, after the RUN method code, I will add a method that gets the email of an Global Address Book record or PartyID.

NOTE: In the Run() method below I'm using settings that you might not need.

void run()
{
    SalesId             Id;
    int                 i;
    str                 prtFileExt = '.pdf';
    SalesFormLetter     salesFormLetter = SalesFormLetter::construct(DocumentStatus::Confirmation, false);
    PrintJobSettings    printJobSettings = new PrintJobSettings();
    Args                args = new Args();
    DirPartyId          _partyId

    ;

    if(printType == glPrintType::Email || printType == glPrintType::Fax || printType == glPrintType::Printer)
    {
        //This is where I store the Network Address
         prtFilepath = DocUParameters::find().PrimaryFileOutputPath;
         if ( !WinApi::fileExists(prtFilePath) )
         {
            prtFilePath =  DocUParameters::find().AlternateFileOutputPath;
         }
         //Setting the print parameters to file and PDF
         printJobSettings.setTarget(PrintMedium::File);
         printJobSettings.format(PrintFormat::PDF);
    }
    else
    {
        if(printType == glPrintType::Preview)
        {
            printJobSetting = new PrintJobSettings();
           // printJobSetting.unpackPrinterSettings(salesFormLetter::getPrinterSettingsFormletter(DocumentStatus::Invoice));
            printJobSetting.setTarget(PrintMedium::Screen);
        }
    }

    if(journalList)
    {
        if(journalList.first(CustConfirmJour))
        {
            Do
            {
                if(! printType == glPrintType::Preview)
                {
                    prtFile = custConfirmJour.SalesId;
                    printJobSettings.fileName(prtFilePath + prtFile + prtFileExt);
                    salesFormLetter.updatePrinterSettingsFormLetter(printJobSettings.packPrintJobSettings());

                    select firstOnly custConfirmJour
                       where custConfirmJour.salesid == prtFile;

                    args.record(custConfirmJour);
                    args.caller(salesFormLetter);

                    new MenuFunction(menuitemoutputstr(SalesConfirmation), MenuItemType::Output).run(args);
                    //Here we set the file name to the file name and the extension (.pdf)
                    prtFile = prtFile + prtFileExt;
                }
                else if(printType == glPrintType::Preview)
                {
                    printJobSetting = new PrintJobSettings();
                    this.printJournal(CustConfirmJour);
                    return;
                }

                salesTable = CustConfirmJour.salesTable();
               
                if(this.parmglPrintType() == glPrintType::Email)
                {
                    _partyId = this.GetContactEmailFromDirParty(salesTable);
                    info(_partyId);
                    //Here I call a custom method to resolve the email from a Party ID
                    email = salesQuotationTable.contactEmail(_partyId, EmailTypes::OrderAcknowledgement);
                    info(email);
                    if(!email)
                        email = ContactPerson::find(salesTable.QuoteContactPersonId, false).Email;

                    if(!email)
                    {
                        email = CustTable::find(custConfirmJour.OrderAccount, false).Email;

                        //Here I set a custom error method when contacts don't have emails
                       //Look for this code below after this method
                        salesTable.SetContactEmailErrorMessage(prtFile, prtFilePath, _partyId);
                        return;
                    }

                    mail = new SysINetMail_GLBTS();
                    mail.sendMailAttach_OLD(email, CCEmail, 'Ref.: '+ CustConfirmJour.CustomerRef +'// '+ 'Acknowledgement ' + CustConfirmJour.SalesId + '','', true, prtfilePath + prtfile,  prtfile);
                    WinApi::deleteFile(prtfilePath + prtfile);
                }
               
                if ( this.parmglPrintType() == glPrintType::Fax )
                {
                    fax =this.parseFaxNumber( ContactPerson::find(salesTable.quoteContactPersonId,false).TeleFax);
                    if(!fax)                       
                        fax =this.parseFaxNumber(CustTable::find(custConfirmJour.OrderAccount,false).Telefax);

                    email = fax + '@j2send.com';
                }

            }while(journalList.next(custConfirmJour));
        }
    }
}

This method is in my SalesQuotationTable

Email contactEmail(DirPartyId partyId, EmailTypes type, QuotationId quoteId = '')
{
    Email                       email;
    ;
    //Get Email based on DirPartyId
    email = AGO_Utilities::GetContactEmailFromPartyID(partyId, type, quoteId);

    return email;
}

AGO_Utilities::GetContactEmailFromPartyID is a class method - see it below


public static Email GetContactEmailFromPartyID(DirPartyId partyId, EmailTypes type, QuotationId quoteId = '')
{
    DirPartyRelationship        dirPartyRelationship;
    Boolean                     breakProcess = false;
    DirPartyId                  childPartyId;
    ContactPerson               contactPerson;
    ContactPersonId             contactPersonId;
    Email                       email = '';
    ;

    while select dirPartyRelationship where dirPartyRelationship.ParentPartyId == partyId
    {
        childPartyId = dirPartyRelationship.ChildPartyId;
        if(childPartyId)
        {
            switch(type)
            {
                case EmailTypes::SalesQuotes:
                    //Check if sales quote has a contact to it
                    contactPersonId = SalesQuotationTable::find(quoteId).ContactPersonId;
                    if(contactPersonId)
                        select * from contactPerson where contactPerson.ContactPersonId == contactPersonId;
                    else
                        breakProcess = true;
                    break;
                case EmailTypes::Invoice:
                    select * from contactPerson where contactPerson.PartyId == childPartyId &&
                               contactPerson.EmailType_Invoice == NoYes::Yes;
                    break;
                case EmailTypes::OrderAcknowledgement:
                    select * from contactPerson where contactPerson.PartyId == childPartyId &&
                               contactPerson.EmailType_OrderAcknowledgement == NoYes::Yes;
                    break;
                case EmailTypes::ShippingMarks:
                    select * from contactPerson where contactPerson.PartyId == childPartyId &&
                               contactPerson.EmailType_ShippingMarks == NoYes::Yes;
                    break;
                case EmailTypes::ARStatement:
                    select * from contactPerson where contactPerson.PartyId == childPartyId &&
                               contactPerson.EmailType_ARStatement == NoYes::Yes;
                    break;
            }

            if(contactPerson.Email || contactPerson.Email2)
            {
                if(!contactPerson.Email)
                    email =  contactPerson.Email2;
                else
                    email = contactPerson.Email;

                break;
            }

            if(breakProcess)
                break;
        }
    }

    return email;
}

The next method seats in the Sales Table sets  an error message when the contact does not have an email. It creates a Info log where a user can double click on the message and go directly to a record in the Contacts Table

void SetContactEmailErrorMessage(FileName fileName, FilePath filePath, DirPartyId partyId)
{
    ContactPersonId contactPersonId;
    SalesQuotationTable smmQuotationTable;
    SysInfoAction_FormRun   SysInfoAction;
    ;
    contactPersonId = smmQuotationTable.GetContactIdFromPartyID(partyId);
    WinApi::deleteFile(filePath+fileName);
    if(contactPersonId || contactPersonId != '')
    {
        SysInfoAction = SysInfoAction_FormRun::newFormnameDesc(FormStr(ContactPersonLookup), "@AIC586");
        info('@AIC583',"", SysInfoAction_TableField::newBuffer(ContactPerson::find(contactPersonId)));
    }
    else
        Box::info('@AIC584');
}

15 comments:

  1. How do I save multiple Sales invoices to the same pdf file? I process hundreds on invoices on weekly basis and need a way to save them in one pdf file instead of saving them one by one. is there a way to do so? thank you

    ReplyDelete
  2. Hi,

    You can't just save a bunch of sales orders on the same PDF without a customization.

    First, it is important to remember that you can have several sales order (for the same invoice account) given one single invoice number. So, let's say you have customer account 123456789 (same invoice account for this example). You create 6 sales orders for the customer 123456789. When posting (SalesEditLine Form), you can go to the "Other Tab" and choose "Invoice Account" under the "Summary Update for" drop down list. When this is done, then you can have many sales orders in one PDF.

    Now, if you want to have different sales orders on the same PDF, you will need to customize your system as this is not possible given the way AX groups the sales order data.

    I hope this helps

    ReplyDelete
  3. Thank you so much for taking the time to look into this.

    ReplyDelete
  4. Not a problem. I hope you can solve your problem.

    So, just to clarify and wrap-up. You can have many sales orders in one PDF for the same customer (or invoice account).

    You cannot, however, do this for different sales orders from different customers (different invoice account). In this case, AX will create one invoice for each invoice account.

    ReplyDelete
  5. Hi,
    Please help me out in finding the solution to my problem. This is very
    urgent.
    I am creating a AX 2009 SSRS report. In the report I have two
    parameters, based on the first parameter which is a drop down , the
    second
    parameter needs to be filtered with specific values. For eg AccountNum
    is the
    first parameter which displays all the CustomerId. Based on the
    AccountNum selected, the second parameter should display the
    AppointmentNumber. Can you
    let me know how to achieve this scenario. Is this scenario possible in
    AX
    2009 SSRS?
    Thanks & regards,
    Ankita

    ReplyDelete
  6. Ankita,

    You can do this taking a few different approaches. The one I recommend if you are new to SSRS and AX is the following:

    1- Create a DataSet in Visual Studio that is linked to a query in AX that brings all the customer id's. This query in AX already exists, so you don't have to build anything.

    2- For the second parameter, you will have to write a data method in Visual Studio that calls a method (and/or custom class) in AX. So, for this you will have to create a tmpTable in AX that will be used to store the results of what the info related to a customer id.

    Then, I would recommend creating a class and 1 static method. The static will have the name of the tmpTable, so you will return this table back to visual studio.

    So you will write public static tmpTable_AppoinemntNumbers(CustNum _custNum)

    The method will receive the customer id as a parameter and with this you can create a while loop that will insert the values into your tmpTable, then in Visual Studio, your data method will have to return a data table, so it will be like this

    public DataTable getAppointments (string custNum)

    Then, you will have to call your new class and assign it to the data table from C# and then set it back to your appointment parameter value in your SSRS report.

    Let me know if you have any questions.

    ReplyDelete
  7. Hi,

    Thanks for your reply.
    I have never create any data method so please help me in making it and please tell me the process step wise so that i will be able to solve my issue.

    Thanks in advance.

    ReplyDelete
    Replies
    1. Hi,

      Please look at the following links, they will help you understand ow to create a report for AX in visual studio, and create a report data method.

      Introduction to Reporting with Visual Studio 2008
      http://msdn.microsoft.com/en-us/library/cc622374(v=ax.50).aspx

      Report Data Method Overview
      http://msdn.microsoft.com/en-us/library/cc587341(v=ax.50).aspx

      I think it would be a good idea for you to take a few hours and read through this material. Also, if yopu can, try to follow a walkthrough exercise and you'll get it right away.

      I hope this helps!

      Delete
    2. Hi,

      I am trying to learn that how to create data methods and already seeing the same links which you have told but if you will help me out in writing data methods then it will be benefited for me because till now i haven't create it.

      I also want to know that where I will create a class and 1 static method (in ax or in Visual Studio). And i didn't get this statement - "The method will receive the customer id as a parameter and with this you can create a while loop that will insert the values into your tmpTable, then in Visual Studio, your data method will have to return a data table, so it will be like this

      public DataTable getAppointments (string custNum)

      Then, you will have to call your new class and assign it to the data table from C# and then set it back to your appointment parameter value in your SSRS report".

      Please elaborate that how to perform the steps mentioned by you in the previous post.

      Delete
    3. Ankita,

      I'm sorry you didn't get what you were expecting for the links. It seems that you are new to this and it can be frustrated when we cannot find the exact answer we are looking for.

      Please follow and execute each of the following exercises. I'm sure you will be able to get what you need from them as they cover everything you need to accomplish.

      http://www.youtube.com/watch?v=JVNisza0BQI

      http://www.youtube.com/watch?v=JVNisza0BQI

      http://msdn.microsoft.com/en-us/library/cc622728(v=ax.50).aspx

      http://msdn.microsoft.com/en-us/library/cc636713(v=ax.50).aspx

      http://msdn.microsoft.com/en-us/library/cc639019(v=ax.50).aspx

      I hope this helps!

      Delete
  8. Hi,
    I am using AX2012 and I am trying to send invoice directly to customers via email. When the invoice is attached to the email it should bear the name as Customer name - Invoice no. How to automate this when we post the invoice. What changes should i make to your code given above to achieve it. I will be saving files locally and not on any network.

    Thanks you have already done a very job keep up the good work.

    ReplyDelete
    Replies
    1. Hi!

      If I'm not mistaken, the email invoice function comes with AX 2012.

      Let's take the following from MSDN:


      Send e-mail

      "Select this check box to send the invoice for a sales order to the customer as an email attachment after the invoice is posted. Attachments are sent as PDF and XML files. This check box is available only if you select the Enable CFD (electronic invoices) check box in the Electronic invoice parameters form."

      The above instructions comes from here http://msdn.microsoft.com/en-us/subscriptions/aa550287.aspx

      Just click the link and search for the word "Email"

      Now, if you still want to write your own code (Which I would not recommend with AX 2012), please take a look at the following code and see what serves your purpose.

      Thanks for reading my blog and let me know if you have any questions.

      Args args=new Args();
      SalesFormLetter salesFormLetter;
      SysMailer mailer;
      PrintJobSettings printJobSettings;
      str _fileName;
      InteropPermission permission;
      FileIOPermission dirPermission, filePermission;
      Set permissionSet;
      ;
      args.record(_custInvoiceJour);
      _fileName=strfmt('%1\\%2.pdf',filepath,_custInvoiceJour.InvoiceId);
      if(_custInvoiceJour)
      {
      salesFormLetter = SalesFormLetter::construct(DocumentStatus::Invoice,false);
      printJobSettings = new printJobSettings();
      printJobSettings.setTarget(PrintMedium::File);
      printJobSettings.format(PrintFormat::PDF_EMBED_FONTS);
      printJobSettings.fileName(_filename);
      salesFormletter.updatePrinterSettingsFormLetter(printJobSettings.packPrintJobSettings());
      _custInvoiceJour.printJournal(SalesFormletter);
      {
      permission = new InteropPermission(InteropKind::ComInterop);
      dirPermission = new FileIOPermission(_fileName,'W');
      permissionSet = new Set(Types::Class);
      permissionSet.add(permission);
      permissionSet.add(dirPermission);
      CodeAccessPermission::assertMultiple(permissionSet);
      mailer = New SysMailer();
      mailer.fromAddress('abc@xyz.com');
      mailer.subject(strfmt("Invoice %1,date %2", _custInvoiceJour.invoiceId,_custInvoiceJour.InvoiceDate));
      if(email) //<--dialog field
      {
      mailer.tos().appendAddress(email);
      }
      if(!email)
      {
      mailer.tos().appendAddress(CustTable::find(_custInvoiceJour.InvoiceAccount).Email);
      }
      mailer.htmlBody("Please see attached");
      mailer.SMTPRelayServer( SysEmailParameters::find().SMTPRelayServerName,
      SysEmailParameters::find().SMTPPortNumber,
      SysEmailParameters::find().SMTPUserName,
      SysEmailParameters::find().SMTPPassword,
      SysEmailParameters::find().NTLM);
      mailer.attachments().add(_fileName);
      mailer.sendMail();
      }
      CodeAccessPermission::revertAssert();
      }

      Good Luck!

      Delete
  9. thank for post the links..
    and
    i need AIF&EP Fundamentals ple send blog names or creation steps post here...ple

    ReplyDelete
  10. Nice one . U r doing well job.

    This is axapta ERP blog for Technical and functional fields and includes Microsoft Dynamics Axapta tutorials and Dynamics Axapta Coverage. This blog also contains x++ code help for Ax developer and solution of technical and functional daily issues. This blog is specific for Microsoft dynamics programming. Enterprise portal, SharePoint services, business connectors and Enterprise Resource Planning applications and sql database.It will help to get Microsoft Business Solutions.


    axapta ERP blog

    ReplyDelete
  11. hi
    Can u please help me out this issue ax 2009 ,
    i want to send one Report pdf file on email thorugh batch process ...For this run time i am generating the report reportrun.run and saveing in one folder , my issue was when i am runing the batch (normal process) report is generating as 38 Kb it is working file , when i attached to batch process it is giveing me report 1 kb it is not generating may i no the reason.

    ReplyDelete

Thank you for your thoughts. Your comment will appear in my blog shortly after review.

Have a great day!