viernes, 31 de agosto de 2012

AX 2012 Post Purchase Order Intercompany Confirmation

I've been working with AX 2012, whoo-hoo!  Can't find anything in the new interface any more, they've moved a lot of the purchase/sales document processing code to run as a service and then I find myself helping junior developers with SSRS (reporting) without ever having touched the product.  All fun in my personal opinion, and am looking forward to playing some more with the product in the future.

Confirming a Purchase Order in X++ has already been done, but I thought I'd include the use case of when a client is marked as 'InterCompany', thereby the purchase orders are mimicked in another company but as a Sales Order for a supplier.  The issue is that once the sales order copy is created, it does not get confirmed as well (nor may it not accept the delivery note automatically once it is marked as dispatched).

Below is the code in it's simplest form.  Note: Ensure InterCompanyAutoCreateOrders and InterCompanyDirectDeliver are selected for the Sales Order.
static void tutorial_POConfirmJob(Args _args)
{    
    
    // Post Purch Confirmation, cross company 
    // --------------------------------------
    PurchFormLetter purchFormLetter;
    PurchTable      purchTable;
    ;
    changeCompany('qca')
    {
        purchTable      = purchTable::find('QCA-000041');
        purchFormLetter = PurchFormLetter::construct(DocumentStatus::PurchaseOrder);
        purchFormLetter.update(purchTable, strFmt('%1-%2', purchTable.PurchId, VendPurchOrderJour::numberOfPurchOrder(purchTable)+1));
    }
}

To plug the above into the existing framework try creating the following method in the SalesConfirmJournalPost class.
/// <summary>
///    Runs after posting a journal.
/// </summary>
public void postJournalPost()
{
    SetEnumerator   se = ordersPosted.getEnumerator();
    PurchFormLetter purchFormLetter;
    PurchTable      purchTable;
    
    ttsbegin;
    while (se.moveNext())
    {
        salesTable = SalesTable::find(se.current(),true);
        if (salesTable && strLen(salesTable.InterCompanyCompanyId) > 0
                    && strLen(salesTable.InterCompanyPurchId) > 0)
        {
            // Post Purch Confirmation, cross company.
            changeCompany(salesTable.InterCompanyCompanyId)
            {
                purchTable      = purchTable::find(salesTable.InterCompanyPurchId);
                if (purchTable
                        && purchTable.DocumentState < VersioningDocumentState::Confirmed)
                {
                    purchFormLetter = PurchFormLetter::construct(DocumentStatus::PurchaseOrder);
                    purchFormLetter.update(purchTable, 
                            strFmt('%1-%2', purchTable.PurchId, 
                              VendPurchOrderJour::numberOfPurchOrder(purchTable)+1));
                    
                    if (purchTable::find(salesTable.InterCompanyPurchId).DocumentState 
                                  == VersioningDocumentState::Confirmed)
                    { 
                        info(strfmt('Intercompany PO '%1' has also been Confirmed.', purchTable.PurchId));
                    }
                }
            }

        }
    }
    ttscommit;
}

Obviously, please test the code. Yours truly will not be held responsible for creating phantom stock in one of your companies!

viernes, 24 de agosto de 2012

Form Running Totals


I can think of two use cases when applying running totals to a grid on a form. Taking the LedgerTransAccount form (Account Transactions) as an example:

Running totals by transaction date

Add a real field to the grid, with a 'XXX_RunningBalanceMST' DataMethdod with the 'LedgerTrans' DataSource.
Add the following method to the LedgerTrans table:
public AmountMST XXX_RunningBalanceMST()
{
    LedgerTrans     ledgerTrans;
    ;
    select sum(AmountMST) from ledgerTrans
            where ledgerTrans.AccountNum  == this.AccountNum
               && ( (ledgerTrans.TransDate < this.TransDate)
                 || (ledgerTrans.TransDate == this.TransDate && ledgerTrans.RecId <= this.RecId) );
    return ledgerTrans.AmountMST;
}

Add the cache method to the form datasource (LedgerTrans) init() method, after the super(); call.
    ledgerTrans_ds.cacheAddMethod(tablemethodstr(LedgerTrans, XXX_RunningBalanceMST));

Running totals by user defined filters, dynalinks and ordenations:

This time add a disply method directly to the datasource.
//BP Deviation Documented
display AmountMST XXX_runningBalanceMST2(LedgerTrans _ledgerTrans)
{
    QueryRun    qr = new QueryRun(ledgerTrans_qr.query());
    LedgerTrans localLedgerTrans;
    AmountMST   amountMST;
    ;
    while (qr.next())
    {
        localLedgerTrans    = qr.get(tablenum(LedgerTrans));
        amountMST           += localLedgerTrans.AmountMST;
        if (localLedgerTrans.RecId == _ledgerTrans.RecId)
        {
            break;
        }
    }
    return amountMST;
}

This is definately the slowest procedure.  I'd avoid applying this modification to be honest.
We would have to implement our own caching on the results, using a map, with RecId as our key and the result as the value.  Initialise/empty the map in the executeQuery() method and check if the _ledgerTrans.RecId key exists in the map before launching into the loop in the method.  The optimisation is undertaken here.

EDIT: Includes user filters on the form columns:-
//BP Deviation Documented
display AmountMST XXX_runningBalanceMST(LedgerTrans _trans)
{
    LedgerTrans localLedgerTrans;
    AmountMST   amountMST;
    ;
    localLedgerTrans    = this.getFirst();
    while (localLedgerTrans)
    {
        amountMST           += localLedgerTrans.AmountMST;
        if (localLedgerTrans.RecId == _trans.RecId)
        {
            break;
        }
        localLedgerTrans    = this.getNext();
    }
    return amountMST;
}
Thanks to Jan B. Kjeldsen for the heads-up.

martes, 21 de agosto de 2012

That company container you have always been looking for


// Inner function to return a list of non virtual companies
// --------------------------------------------------------
container companyContainer(NoYes _includeVirtual = NoYes::No)
{
    DataArea            dataarea;
    container           retVal;
    ;
    while select id, isVirtual from dataarea index hint id
    {
        if (_includeVirtual == NoYes::No && dataarea.isVirtual == NoYes::Yes)
        {
            continue;
        }
        retVal += dataarea.id;
    }
    return retVal;
}

I have used the above inner function in two Jobs now, placed in the function declaration section.  Example usage:
static void tutorial_Job_companyCurrency(Args _args)
{
    container               conComps,
                            conComp;
    Counter                 cntComps;
    DataAreaId              dataareaId;
    CurrencyCode            currencyCompany;

    container companyContainer(NoYes _includeVirtual = NoYes::No)
    {
        DataArea            dataarea;
        container           retVal;
        ;
        while select id, isVirtual from dataarea index hint id
        {
            if (_includeVirtual == NoYes::No && dataarea.isVirtual == NoYes::Yes)
            {
                continue;
            }
            retVal += dataarea.id;
        }
        return retVal;
    }
    ;

    conComps        = companyContainer();

    for (cntComps=1; cntComps <= conlen(conComps); cntComps++)
    {
        dataareaId  = conpeek(conComps, cntComps);
        conComp     = [dataareaId];

        currencyCompany     = (select crosscompany : conComp CompanyInfo).CurrencyCode;
        warning(strfmt("Company: %1; Currency: %2", dataareaId, currencyCompany));
    }
}

Finally, below is an example when the customers table is shared across companies, using virtual companies in the query.
    DataArea            dataArea;
    VirtualDataAreaList virtualDataAreaList, 
                        virtualDataAreaListNow;
    ;
    select virtualDataAreaListNow
        where virtualDataAreaListNow.id == curext();

    while select dataArea
        where dataArea.isVirtual == false
//            && dataArea.Id != curext()
        join virtualDataAreaList
            where virtualDataAreaList.id == dataArea.id
            &&    virtualDataAreaList.virtualDataArea == virtualDataAreaListNow.virtualDataArea
    {
        changecompany(dataArea.Id)
        {
            info(strFmt('%1: %2', dataArea.Id, CustTable::find('CL000000').openBalanceMST()));
        }
    }

Let's put that in a tidy container:
static void BigJob(Args _args)
    container                   conCompanys;

    container companyContainer()
    {
        DataArea            dataarea;
        container           retVal;
        VirtualDataAreaList virtualDataAreaList,
                            virtualDataAreaListNow;
        ;
        select virtualDataAreaListNow
            where virtualDataAreaListNow.id == curext();

        while select dataArea
            where dataArea.isVirtual == false
            join virtualDataAreaList
                where virtualDataAreaList.id == dataArea.id
                &&    virtualDataAreaList.virtualDataArea == virtualDataAreaListNow.virtualDataArea
        {
            retVal += dataarea.id;
        }
        return retVal;
    }
    ;
    conCompanys = companyContainer();
    // Do work
}

martes, 14 de agosto de 2012

Decimal Number Formatting

Well I can't explain why but we kept losing decimal places when converting a real to a string.  The below function does *not* truncate the real value to two decimal places.  Note that the function is located in a utility class, and will execute on the server.
public static server str getExchRateCalc(real _fixedExchRate)
{
;
    new InteropPermission(InteropKind::ClrInterop).assert();
    return System.String::Format("{0,0:G}",(_fixedExchRate));
}