web
You’re offline. This is a read only version of the page.
close
Skip to main content

Announcements

News and Announcements icon
Community site session details

Community site session details

Session Id :
Finance | Project Operations, Human Resources, ...
Suggested Answer

extensibility of SalesFormLetter_Invoice

(2) ShareShare
ReportReport
Posted on by 31
Hello, 

I have a requirement to run my code after a sales order is invoiced, I have tried using SalesFormLetter_Invoice.afterOperationBody() this way
    protected void afterOperationBody()
    {
        CustInvoiceJour custInvoiceJour;
        ISTDParameters  parameters = ISTDParameters::find();

        next afterOperationBody();

        Set journalSet = Set::create(formletterOutputContract.parmAllJournalsPacked());
        SetEnumerator se = journalSet.getEnumerator();

        while (se.moveNext())
        {
            custInvoiceJour = se.current();

            if (ISTDParameters::isProduction() && !this.proforma() && custInvoiceJour && !formletterOutputContract.parmUpdateError())
            {
                ISTDProcessInvoice processISTDInvoice = new ISTDProcessInvoice();
                processISTDInvoice.parmCustInvoiceJour(custInvoiceJour);
                processISTDInvoice.run();
            }
        }
    }

in my class i call an API this way
 
/// <summary>
///    Contains the code that does the actual job of the class.
/// </summary>
private void processInvoice()
{
    System.Net.HttpWebRequest request;
    System.Net.WebResponse response;
    System.IO.Stream responseStream, requestStream;
    System.IO.StreamReader streamReader;
    System.Text.Encoding utf8;
    System.Byte[] bytes;
    System.Exception ex;
    Notes requestJson, responseJson, errorMessage;
    System.Net.WebException webException;
    ISTDInvoiceStatus invoiceStatus;
    ISTDGenerateInvoiceXML generateInvoiceXML;
    ISTDGenerateCreditNoteInvoiceXML generateCreditNoteInvoiceXML;
    ISTDLogTable logTable;

    try
    {
        System.Net.WebHeaderCollection httpHeader = new System.Net.WebHeaderCollection();
        new InteropPermission(InteropKind::ClrInterop).assert();

        request = System.Net.WebRequest::Create(parameters.URL);
        utf8 = System.Text.Encoding::get_UTF8();

        //fill log table
        logTable.clear();
        logTable.ProcessedDateTime = DateTimeUtil::utcNow();
        logTable.InvoiceId = custInvoiceJour.InvoiceId;
        logTable.InvoiceDate = custInvoiceJour.InvoiceDate;
        LogTable.custInvoiceJourRecId = custInvoiceJour.RecId;
        LogTable.InvoiceGUID = custInvoiceJour.ISTDGuid;

        if (custInvoiceJour.creditNote())
        {
            generateCreditNoteInvoiceXML = new ISTDGenerateCreditNoteInvoiceXML(custInvoiceJour);
            requestJson = generateCreditNoteInvoiceXML.returnXML();
        }
        else
        {
            generateInvoiceXML = new ISTDGenerateInvoiceXML(custInvoiceJour);
            requestJson = generateInvoiceXML.returnXML();
        }

        //fill request
        logTable.Request = requestJson;

        bytes = utf8.GetBytes(requestJson);
        httpHeader = request.Headers;

        httpHeader.Add('Client-Id', parameters.ClientId);
        httpHeader.Add('Secret-Key', parameters.SecretKey);

        request.set_Method('POST');
        request.set_Headers(httpHeader);
        request.ContentType = 'application/json';
        request.set_ContentLength(bytes.get_Length());
        requestStream = request.GetRequestStream();
        requestStream.Write(bytes, 0, bytes.get_Length());

        response = request.GetResponse();
        responseStream = response.GetResponseStream();
        streamReader = new System.IO.StreamReader(responseStream);
        responseJson = streamReader.ReadToEnd();

        logTable.Response = responseJson;
        logTable.IsProcessed = NoYes::Yes;

        invoiceStatus = ISTDInvoiceStatus::Sent;

        info('Invoice was sent successfully.');
    }
    catch (webException)
    {
        invoiceStatus = ISTDInvoiceStatus::Error;

        if (webException.get_Response() != null)
        {
            System.Net.HttpWebResponse httpWebResponse;
            Notes responseString;

            httpWebResponse = webException.get_Response() as System.Net.HttpWebResponse;
            responseStream = httpWebResponse.GetResponseStream();
            streamReader = new System.IO.StreamReader(responseStream);
            responseString = streamReader.ReadToEnd();

            logTable.Errors = responseString;
            logTable.IsProcessed = NoYes::No;

            streamReader.Close();
            responseStream.Close();
            httpWebResponse.Close();

            warning(strFmt('invoice %1 was not sent. Please check the log form for errors and try again.', custInvoiceJour.InvoiceId));
        }
        else
        {
            logTable.IsProcessed = NoYes::No;
            logTable.Errors = webException.get_Message();
        }

        //
        if (!logTable.Errors)
             logTable.Errors = webException.get_Message() + ' - block 1';
    }
    catch (Exception::Error)
    {
        logTable.IsProcessed = NoYes::No;
        logTable.Errors = con2Str(this.retreiveErrorsAndWarnings());
        invoiceStatus = ISTDInvoiceStatus::Error;
        this.clearInfologErrorAndWarnings();

        //
        if (!logTable.Errors)
             logTable.Errors = AifUtil::getClrErrorMessage() + ' - block 2';
    }
    catch (Exception::CLRError)
    {
        logTable.IsProcessed = NoYes::No;
        logTable.Errors = AifUtil::getClrErrorMessage();
        invoiceStatus = ISTDInvoiceStatus::Error;
    }
    catch
    {
        logTable.IsProcessed = NoYes::No;
        logTable.Errors = 'There was an unhandled error that occured. - ' + AifUtil::getClrErrorMessage();
        invoiceStatus = ISTDInvoiceStatus::Error;
    }
    finally
    {
        if (invoiceStatus == ISTDInvoiceStatus::Sent)
        {
            if (!custInvoiceJour.creditNote())
            {
                logTable.InvoiceAmount = generateInvoiceXML.parmInvoiceAmount();
            }
            else if (custInvoiceJour.creditNote())
            {
                logTable.InvoiceAmount = generateCreditNoteInvoiceXML.parmInvoiceAmount();
            }
        }

        if (invoiceStatus == ISTDInvoiceStatus::Error && !custInvoiceJour.creditNote())
        {
            ISTDLogLine logLine;

            delete_from logLine
                where logLine.custInvoiceJourRecId == custInvoiceJour.RecId;
        }
        
        ttsbegin;
        custInvoiceJour.selectForUpdate(true);
        custInvoiceJour.ISTDInvoiceStatus = invoiceStatus;
        custInvoiceJour.doUpdate();
        ttscommit;

        logTable.insert();
    }
}

and it works perfectly fine, although sometimes the invoice is not generated for some unknown reason since in my log table i can see the next number sequence of a sales invoice but its not actually used in the system so I'm assuming the invoice was not generated due to some errors, even though i made sure to use !formletterOutputContract.parmUpdateError() in my if statement but my class still gets called but the invoice is not generated, I have also tried using SalesInvoiceJournalPost.endUpdate(); but that didn't work since sometimes the API returns an exception and it would stop the sales order from being invoiced.

is there a place i can run my API after a sales order is completely invoiced with no errors so then it would be fine for me to call the API, Thanks a lot!
 
I have the same question (0)
  • Sohaib Cheema Profile Picture
    49,668 Super User 2026 Season 1 on at
    Use, SalesInvoiceJournalPost[Class].EndPost[Method] if you have sales invoices only
    Use, SalesInvoiceJournalPostProj[Class].EndPost[Method] if you have project invoices
    You also have option to use the base SalesInvoiceJournalPostBase[Class].[EndPost] 
  • Suggested answer
    Sagar121 Profile Picture
    1,187 Super User 2026 Season 1 on at
    Hi,
     
    As mentioned by Sohaib, EndPost is the perfect method to write the code.
     
    If you will go to SalesInvoiceJournalPostBase> end Post method then you can also see business event is also triggered inside this method when sales order is invoiced 
     
  • SH-03071114-0 Profile Picture
    31 on at
     
    Thanks for your reply. i tried using SalesInvoiceJournalPost[Class].EndPost[Method] but i when i threw an error it was't caught, unlike the method SalesFormLetter_Invoice.AfterOperationBody() when my API returns a web exception i was able to catch and so the invoice wouldnt be interrupted, is this even possible to achieve in SalesInvoiceJournalPost[Class].EndPost[Method]?
  • Sohaib Cheema Profile Picture
    49,668 Super User 2026 Season 1 on at
     
    What do you want?

    A) I do not care about API call failures; my invoice must never be affected because of an API call. If the API call is failing, the invoice must still go through regardless of the API failure.

    B) If my API fails, the invoice must also fail to post. Either both must go through or neither.
  • SH-03071114-0 Profile Picture
    31 on at
     Yes, the invoice should still go through whether the API fails to call or not so A
  • Martin Dráb Profile Picture
    239,684 Most Valuable Professional on at
    As I understand, you want to call a web service at some point after posting an invoice. It mustn't be in as transaction when the invoice is posted, because a failure in the code (e.g. the web service not being reachable) would prevent invoice posting completely.
     
    There are several possible solutions. For example, you could have a table working as a queue of messages to be sent. In the transaction, you'll write a record to this table. Later, e.g. by a periodic job, you can process records in the queue in a separate transaction.
     
    Business events do something similar.
     
    By the way, there are quite a few things you can improve in your code. For instance, you forgot to dispose resources created in the try block. You can access properties of .NET objects simply by name (e.g. request.Method = 'POST') instead of through get_* and set_* methods (such as request.set_Method('POST')). You can catch System.Exception object instead of catching Exception::CLRError and then getting the last exception by AifUtil::getClrErrorMessage(). The call of AifUtil::getClrErrorMessage() in the last catch block doesn't make sense to me - it'll never be an CLR exception, because they're already handled above. InteropPermission isn't needed. The block of code for Sent status belongs to the try block. It doesn't make sense in finally as the condition will never be met in case of an exception.
  • SH-03071114-0 Profile Picture
    31 on at

    Thanks for helping me improve my code as it really helps!, the API retruns a QR code in the response that i need to use to show inside the invoice printout, which is why im calling it right before the invoice is created, and thats why I can't run it later separately after the invoice is created as they are printing it right after it is invoiced.

    my issue here is that the API is getting called even though there was an issue with the invoice and all of these are different CustInvoiceJour records since im inserting the GUID right here,
     
    [ExtensionOf(tableStr(CustInvoiceJour))]
    final class ISTDCustInvoiceJour_Table_Extension
    {
        public void insert()
        {
            this.ISTDGuid = newGuid();
    
            next insert();
        }
    }
    I can't seem to grasp where the issue is and why my API is getting called even though i am making sure !formletterOutputContract.parmUpdateError() is returning true, could it be thats it's throwing an error somewhere else stopping the invoice from being posted, I read through formletter classes and it stated here that this method will be executed after all the main logic so im assuming if there was an error at any point before i am sending my API --formletterOutputContract.parmUpdateError() this would return true and therefore my API wouldn't be called.
     
        /// <summary>
        /// Executes the main logic for after the operation, before cleanup.
        /// </summary>
        protected void afterOperationBody()
        {
            this.afterOperationPrint();
        }
    these are all different records of CustInvoiceJour who have not been successfuly posted yet and still my API was still being called.
     
  • Sohaib Cheema Profile Picture
    49,668 Super User 2026 Season 1 on at
    Martin has provided you with a good solution. Keep your API call outside the transaction scope of the invoice posting. Who knows how a third-party API call might behave? It could be offline (as Martin said) or throw other exceptions. Put your API call requests in a separate queue (table), and let your custom batch job run very frequently (say, every minute). This way, you will have minimal impact on retrieving the QR code in the invoice data (CustInvoiceJour), but more control over the solution.
    If you are interested, I can also provide you with espnesive solutions using Service Bus (Azure); I have worked on very complex integrations. However, while staying within the app (D365 FO), your best option is to follow Martin's suggestion: keep the integration outside the invoice posting scope (ttsBegin/ttsCommit).
  • Martin Dráb Profile Picture
    239,684 Most Valuable Professional on at
    @SH-03071114-0 I failed to notice anything about QR in your code; it seems it only fetches some amounts. Anyway, you need to start paying attention to the database transaction; this is the key topic and you didn'tven mention it. You want to run your code somewhere between the end of the transaction that contains invoice posting, and report printing that's executed right after posting. Therefore you need to identify these two places to know where your logic may be put; you see that you weren't successful with trying to guess what the right place may be.
     
    You can find the transaction by carefully examining code, but a safer approach is checking the transaction level at runtime in debugger.
     
    If the goal is preparing data for a report, wouldn't it be more appropriate to forget invoice posting completely and rather do it at the same time when preparing other data for the report (in the RDP class)? As you see, knowing the actual business requirements is important for designing the solution correctly.

Under review

Thank you for your reply! To ensure a great experience for everyone, your content is awaiting approval by our Community Managers. Please check back later.

Helpful resources

Quick Links

Season of Sharing Community Challenge Launch!

Jump in, show your community spirit, and win prizes!

Women in Power Builds Momentum

Expanding mentorship, skilling, and AI innovation

Congratulations to the May Top 10 Community Leaders

These are the community rock stars!

Leaderboard > Finance | Project Operations, Human Resources, AX, GP, SL

#1
Abhilash Warrier Profile Picture

Abhilash Warrier 681 Super User 2026 Season 1

#2
André Arnaud de Calavon Profile Picture

André Arnaud de Cal... 598 Super User 2026 Season 1

#3
Giorgio Bonacorsi Profile Picture

Giorgio Bonacorsi 579

Last 30 days Overall leaderboard

Product updates

Dynamics 365 release plans