Pages

Saturday, November 30, 2013

AX Retail 2012 R2: POS Error when cancel customer order

Noted:  This customization is based on AX 2012 R2 + CU6 

I found a discussion on LinkedIn mentioning about "Cancel customer order error".  So, I try to research its solution and find it.  


Scenario: 

When cancel customer order, an error occurred about "Voucher do not balance

POSApp.CustomerOrderRecovery(): The order could not be saved at this time.
LSRetailPosis.TransactionServiceException: TS InvokeMethod threw FaultException<TrackedFault>: CancelCustomerOrder(). TrackedFault:                   Voucher XXXXXX, date MM/DD/YYYY, account 13XXXX--, transaction currency amount X.XX, accounting currency amount X.XX, reporting currency amount 0.00, currency USD, text Payment of cancellation charge for the order 012526


Voucher XXXXXX, date MM/DD/YYYY, account 40XXXX-------, transaction currency amount -X.XX, accounting currency amount -X.XX, reporting currency amount 0.00, currency USD, text Payment of cancellation charge for the order 012526
Voucher XXXXXX, date MM/DD/YYYY, account 13XXXX--, transaction currency amount X.XX, accounting currency amount X.XX, reporting currency amount 0.00, currency USD, text Refund for the order 012526
The transactions on voucher 80000011 do not balance as per 11/30/2013. (accounting currency: 2.61 - reporting currency: 0.00)

Posting has been canceled.


Order cancellation charge

Previous blog, I mentioned about "Default deposit percentage".  Here, "Cancellation charge percentage" and "Cancellation charge code" are also configured in "Retail parameters > Customer orders".  And Credit account is also required.   
   



I tried to create an customer order with paid deposit (Paid $5.00 bill, Change $0.22).  So, when cancel the order, cancellation charge will be $2.39 by default.




When we look into Voucher transactions (General ledger > Inquiries) by adding Payment reference as the sales order number, we will see transactions of the deposit (cash & change).  




So, the expectation of this blog is when cancel customer order, the order should be settled with the cancellation charge.  

Customize Real-time Service Class

Because this is about Real-time service, then we need to fix in Retail HQ.  Normally, the method which handles the cancellation is cancelCustomerOrder() in \Classes\RetailTransactionService.  But once I customize here, the changes doesn't affect to POS.  So, I have to create methods in \Classes\RetailTransactionServiceEx (Extension Real-time service class) instead.     

First, we need to prepare a class to be similar to the standard one; RetailTransactionService: -

  • classDeclaration - copy all variables from standard class
  • settleCancellation - copy it, let's rename it for making different look from standard.  I named it as pkaSettleCancellation  

Then, you can create a new method as below: -

/// <summary>
/// Cancels the sales order.
/// </summary>
/// <param name="custOrderXmlStr">
/// XML string having the header, line details and charges, payments associated with the sales order to be canceled.
/// </param>
/// <returns>
/// A container having the status of order cancellation.
/// </returns>
public static container pkaCancelCustomerOrder(str custOrderXmlStr)
{
    int         i;
    str         error;
    boolean     success = false;
    SalesTable  salesTable;
    SalesLine   salesLine;
    MarkupTrans markupTrans;
    SalesId     salesId;
    MarkupCode  chargeCode;
    Voucher     voucher;
    Amount      amount,chargeCodeAmount, totalChargeCodeAmount;
    TaxGroup   taxGroup;
    TaxItemGroup taxItemGroup;
    TaxAmountCur    exclusiveTaxAmountCur = 0;
    TaxUncommitted  taxUncommitted;
    XmlDocument custOrderXml;
    XmlElement  xmlRoot;
    XmlElement  xmlRecord;
    XmlNodeList xmlRecordList;
    Counter     infologline    = infolog.num();
    LedgerJournalCheckPost  ledgerJournalCheckPost;
    LedgerJournalName  ledgerJournalName;
    LedgerJournalTable ledgerJournalTable;
    LedgerJournalTrans ledgerJournalTrans;
    AmountCur   refundAmount;
    MarkupTable markupTable;
    int fromLine;
    // ---> Phannasri, 2013.11.30
     Amount      pkaAmountChange;
    // <--- Phannasri, 2013.11.30

    // <GIN>
    RetailStoreId               storeId;
    TaxTable                    taxTable;
    boolean                     isIndia = SysCountryRegionCode::isLegalEntityInCountryRegion([#isoIN]);
    // </GIN>

    CurrencyCode currencyCode = CompanyInfo::standardCurrency();
    // <GEERU>
    boolean countryRegion_W = SysCountryRegionCode::isLegalEntityInCountryRegion(#easternEuropeAllandRU);
    // </GEERU>

    custOrderXml    = new XmlDocument();
    custOrderXml.loadXml(custOrderXmlStr);

    xmlRoot = custOrderXml.documentElement().getNamedElement('Id');
    salesId = xmlRoot.text();

    xmlRoot = custOrderXml.documentElement().getNamedElement('CurrencyCode');
    if(xmlRoot.text())
    {
        currencyCode = xmlRoot.text();
    }

    ttsbegin;
    salesTable = SalesTable::find(salesId);
    try
    {
        fromLine = Global::infologLine();

        // Cancelling the order
        if(salesTable.DocumentStatus == DocumentStatus::None)
        {
            while select forupdate salesLine
                where salesLine.SalesId == salesId
            {
                salesLine.RemainSalesPhysical = 0.0;
                salesLine.RemainInventPhysical = 0.0;
                salesLine.update();
            }
        }
        else
        {
            throw error('Order cannot be cancelled at this time from POS');
        }

        //creation of charge codes
        xmlRoot = custOrderXml.documentElement().getNamedElement('Charges');
        xmlRecordList = xmlRoot.childNodes();
        if(xmlRecordList)
        {
            for(i = 0; i < xmlRecordList.length(); i++)
            {
                xmlRecord = xmlRecordList.item(i);
                chargeCode = xmlRecord.getAttribute('Code');
                chargeCodeAmount = str2num(xmlRecord.getAttribute('Amount'));
                totalChargeCodeAmount += chargeCodeAmount;

                taxGroup = xmlRecord.getAttribute('TaxGroup');
                taxItemGroup = xmlRecord.getAttribute('TaxItemGroup');

                markupTrans.clear();
                markupTrans.MarkupCode = chargeCode;        //Set markup code, then call initFromSalesTable, then initValue to ensure tax groups are initialized correctly
                markupTrans.initFromSalesTable(salesTable);
                markupTrans.initValue();
                markupTrans.Value = chargeCodeAmount;
                markupTrans.CurrencyCode = currencyCode;
                markupTrans.TaxGroup = taxGroup;
                markupTrans.TaxItemGroup = taxItemGroup;
                markupTrans.insert();
            }
        }

        while select Prepayment, AmountCurCredit, PaymReference from ledgerJournalTrans
                where ledgerJournalTrans.PaymReference == salesId
                    && ledgerJournalTrans.Prepayment == NoYes::Yes
        {
            // ---> Phannasri, 2013.11.30
            pkaAmountChange += ledgerJournalTrans.AmountCurDebit;
            // <--- Phannasri, 2013.11.30
            amount += ledgerJournalTrans.AmountCurCredit;
        }

        // ---> Phannasri, 2013.11.30
        // In case, there is change amount for prepayment, consider it
        //refundAmount = amount - totalChargeCodeAmount;
        if (pkaAmountChange)
        {
            refundAmount = amount - pkaAmountChange - totalChargeCodeAmount;
        }
        else
        {
            refundAmount = amount - totalChargeCodeAmount;
        }
        // ---> Phannasri, 2013.11.30

        //Creation of payment journal
        if(amount)
        {
            select firstonly JournalType, JournalName, OffsetLedgerDimension, OffsetAccountType from ledgerJournalName where ledgerJournalName.JournalType == LedgerJournalType::CustPayment;

            ledgerJournalTable.clear();
            ledgerJournalTable.JournalName = ledgerJournalName.JournalName;
            ledgerJournalTable.initFromLedgerJournalName(ledgerJournalName.JournalName);
            ledgerJournalTable.Name = strFmt("@RET4505",salesTable.SalesId);
            ledgerJournalTable.OffsetLedgerDimension = ledgerJournalName.OffsetLedgerDimension;
            ledgerJournalTable.OffsetAccountType = ledgerJournalName.OffsetAccountType;
            ledgerJournalTable.CurrencyCode = currencyCode;
            ledgerJournalTable.insert();

            // Reset InclTax to match the SalesOrder because .Insert() forces it to match the value from LedgerJournalName
            ledgerJournalTable.LedgerJournalInclTax = salesTable.InclTax;
            ledgerJournalTable.update();

            // Use a common Voucher number for all payment entries
            voucher = NumberSeq::newGetNum(CustParameters::numRefCustPaymVoucher()).num();

            for(i = 0; i < 2; i++)
            {
                ledgerJournalTrans.clear();
                ledgerJournalTrans.initValue();
                ledgerJournalTrans.JournalNum           = ledgerJournalTable.JournalNum;
                ledgerJournalTrans.LineNum              = LedgerJournalTrans::lastLineNum(ledgerJournalTrans.JournalNum) + 1;
                ledgerJournalTrans.AccountType          = LedgerJournalACType::Cust;
                ledgerJournalTrans.parmAccount(salesTable.CustAccount);
                ledgerJournalTrans.DefaultDimension     = ledgerJournalTable.DefaultDimension;
                ledgerJournalTrans.initFromCustTable(CustTable::find(salesTable.CustAccount));
                ledgerJournalTrans.CurrencyCode         = currencyCode;
                ledgerJournalTrans.ExchRate             = Currency::exchRate(ledgerJournalTrans.CurrencyCode);
                ledgerJournalTrans.TransDate            = systemDateGet();
                ledgerJournalTrans.PaymReference        = salesTable.SalesId;
                ledgerJournalTrans.Prepayment           = NoYes::No;
                ledgerJournalTrans.Voucher              = voucher;
                ledgerJournalTrans.TransactionType      = LedgerTransType::Sales;

                if(i == 0 && totalChargeCodeAmount)
                {
                    //Insert the charge first, the uncommitted taxes are calculated on .Insert()
                    markupTable = MarkupTable::find(MarkupModuleType::Cust, chargeCode);
                    ledgerJournalTrans.OffsetAccountType        = LedgerJournalACType::Ledger;
                    ledgerJournalTrans.OffsetLedgerDimension    = DimensionDefaultingService::serviceCreateLedgerDimension(markupTable.VendorLedgerDimension);
                    ledgerJournalTrans.AmountCurDebit           = Currency::amount(totalChargeCodeAmount, currencyCode);
                    ledgerJournalTrans.TaxGroup                 = taxGroup;
                    ledgerJournalTrans.TaxItemGroup             = taxItemGroup;
                    // Payment of cancellation charge for the order %1
                    ledgerJournalTrans.Txt                      = strFmt("@RET260962", salesTable.SalesId);
                    ledgerJournalTrans.insert();


                    //After .Insert, read back the uncommitted exclusive taxes, as these need to be excluded from the refundAmount
                    if(!ledgerJournalTable.LedgerJournalInclTax)
                    {
                        select sum(SourceRegulateAmountCur) from taxUncommitted
                            where taxUncommitted.SourceTableId == ledgerJournalTrans.TableId
                                && taxUncommitted.SourceRecId == ledgerJournalTrans.Recid
                            ;
                        exclusiveTaxAmountCur = TaxUncommitted.SourceRegulateAmountCur;
                    }

                }
                // ---> Phannasri, 2013.11.30
                //else if(i == 1 && refundAmount)
                else if(i == 1 && refundAmount && exclusiveTaxAmountCur)
                // <--- Phannasri, 2013.11.30
                {
                    // Refund for the order %1
                    ledgerJournalTrans.Txt              = strFmt("@RET260964", salesTable.SalesId);
                    // ---> Phannasri, 2013.11.30 
                    markupTable = MarkupTable::find(MarkupModuleType::Cust, chargeCode);
                    ledgerJournalTrans.OffsetAccountType        = LedgerJournalACType::Ledger;
                    ledgerJournalTrans.OffsetLedgerDimension    = DimensionDefaultingService::serviceCreateLedgerDimension(markupTable.VendorLedgerDimension);
                    // <--- Phannasri, 2013.11.30 
                    ledgerJournalTrans.AmountCurDebit   = Currency::amount(refundAmount - abs(exclusiveTaxAmountCur), currencyCode);
                    ledgerJournalTrans.insert();
                }
            }
            ledgerJournalCheckPost = LedgerJournalCheckPost::newRBOLedgerJournalTable(ledgerJournalTable,NoYes::Yes,NoYes::No);
            ledgerJournalCheckPost.run();

            RetailTransactionServiceEx::pkaSettleCancellation(salesTable, voucher);
        }
        error = "";
        success = true;
        ttscommit;
    }
    catch
    {
        ttsabort;
        error = RetailTransactionService::getInfologMessages(fromLine);
        RetailTracer::Error('RetailTransactionServiceEx', funcName(), error);
        success = false;
    }
    return [success, error];
} 

Originally, I copy the method from standard one and customize some parts (noticing from red texts).  Something like, there is a bug about "Uncommitted exclusive taxes".  So, I check if no taxes, skip posting its transaction.  


Retail POS Customization

In POS, we customize class SalesOrder.cs in "Retail SDK\POS Plug-ins\Services\SalesOrder". Search for the method "CancelCustomerOrder", change code to call the new extension method using InvokeExtension instead. 

                // ---> Phannasri, 2013.11.30
                //containerArray = Application.TransactionServices.Invoke("CancelCustomerOrder", xmlString);
                containerArray = Application.TransactionServices.InvokeExtension("pkaCancelCustomerOrder", xmlString);

                // ---> Phannasri, 2013.11.30

Then, compile SalesOrder.dll and replace it in Retail POS services directory (C:\Program Files (x86)\Microsoft Dynamics AX\60\Retail POS\Services). 

This time, you can successfully cancel the order.  Once look into the voucher transaction, you will see that the order is settled with cancellation charge.   



However, this is just a temporary solution.  Let's hope that Microsoft will give us a hotfix for this issue soon.  ;-)  


8 comments:

  1. Hi Phannasri,

    Really thanks for your work on this issue, but i found that it throws error if the cancellation charge is set to zero. Do you have any idea on it?

    thank you

    BR
    KS

    ReplyDelete
    Replies
    1. Sorry for late reply, I didn't touch Retail module for a few month cuz having HR projects. I will have a look this and let you know.

      Delete
    2. First, thanks for your work;

      We have tried this solution but we still get an error...
      hope you can help us.

      The error thrown when calling the method on the POS is:
      TSInvokeExtensionMethos threw FaultException : pkaCancelCustomerOrder(). TrackedFault: Error when calling method pkaCancelCustomerOrder

      Thank you in advance.

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. EPOS all in one solution for your takeaway business. all about order management system.
    EPOS allowing you to take action quickly and effortlessly.
    takeaway epos
    epos software for takeaway delivery
    website: intelepos.com
    Email: info@intelepos.com

    ReplyDelete
  4. Great stuff, will looking forward to epos software some more postings....

    ReplyDelete
  5. Amazing information. Are you looking for EPOS System provider in UK, If yes,visit our website.

    ReplyDelete
  6. Fantastic post. epos software Lots of useful information here. thank you in your effort!

    ReplyDelete