Skip to main content

How to get FinOps Azure storage blob connection string

· One min read
Max Nguyen
FinOps Ranger

In Dynamics 365 for Finance and Operations Azure storage is used to

And more!

If you are using tier 1 Cloud-Hosted environment, you can find the azure storage from the Azure resource group in Azure portal.

For tier 1, UAT tier 2, and PROD Tier 3, you can create a runnable class and execute to get the Azure storage connection string.

using Microsoft.Dynamics.Clx.ServicesWrapper;
class GetAzureBlob
{
public static void main(Args _args)
{
info('AzureStorageConnectionString = ' + CloudInfrastructure::GetCsuStorageConnectionString());
info('ClientCertificateThumbprint = ' + CloudInfrastructure::GetCsuClientCertificateThumbprint());
}
}

Once you have the connection string, you can connect it to MS Azure Storage Explorer, Power Automate, or consuming in any framwork/language for integration/backup purposes.

How to skip standard code in Dynamics 365 finance and operations

· 2 min read
Max Nguyen
FinOps Ranger
caution

Disclaimers: Try at your own risk, this is consider as a bug and it will be fixed soon.

According to this MS article about Chain Of Command https://docs.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/extensibility/method-wrapping-coc#wrapper-methods-must-always-call-next

"Wrapper methods must always call next Wrapper methods in an extension class must always call next, so that the next method in the chain and, finally, the original implementation are always called. This restriction helps guarantee that every method in the chain contributes to the result. In the current implementation of this restriction, the call to next must be in the first-level statements in the method body."

So as described in the article, the Chain Of Command in Dynamics 365 finance and operations is used to add the logic before or after a method of a class is invoked and the original method will always be executed due to the compulsory of the next statement. When doing some implementations, we found out that there is a tricky way to completely ignore the execution of the original method of the class that is extended.

Please see below for more details: For example, I created a project and add a new runnable class named Alex:

class TestClass
{
public static void main(Args _args)
{
throw error ("Hello World");
}
}

When this class runs, a Hello World info message will be displayed in the FinOps UI. Then I used chain of command to create Alex_Extension class that extends this class and "override" the main method of the Alex's class by putting the next call into another method and never call it like this:

[ExtensionOf(classStr(TestClass))]
final class Alex_Extension
{
public static void main(Args _args)
{
//Implement something here
info ("Ok");
void ignore()
{
next main(_args);
}
}
}

When I run the AlexExtension, the original main method of the Alex class will be ignored and only the additional block of code that I wrote in the AlexExtension class will be executed, so only the OK info will appear.

Now we will try with a sample scenario from D365 Finance and Operation. In General Ledger Module, we have General journals form

Image

When you click the button for Journal Print, the menu item will be triggered and point to the controller name: LedgerJournalController with LedgerJournalController class and the report will be generated.

I will use COC to “override” the main method of LedgerJournalController class by applying the method above:

[ExtensionOf(classStr(LedgerJournalController))]
final class Alex_LedgerJournalController_Extension
{
public static void main(Args _args)
{
info("ok");
void ignore_first()
{
next main(_args);
}
}
}

Using this I will ignore the the main method of LedgerJournalController class and instead of generate the report, the result will be only the OK info line in the extension class:

Image

This is a little bit tricky because we still call next in our extension class as per the requirement of COC, but as we never call the method that "wrap" the next call so it will not be executed. You can try this to simply ignore or rewrite a method of a class in FinOps.

Thank you for reading.

Set up a VHD for finance and operations apps

· 5 min read
Max Nguyen
FinOps Ranger

1. Download Dynamics 365 finance and operations VHD files

  • Go to the LCS main page and select Shared asset library or go to Shared Asset Library.

  • Select the asset type Downloadable VHD.

  • Find the VHD you are looking for based on the desired Finance and Operation version. The VHD is divided into multiple file parts that you need to download. For example, the asset files that start with "VHD - 10.0.5" are the different files you need in order to install version 10.0.5.

  • Download all files (parts) associated with the desired VHD to a local folder.

  • After the download is complete, run the executable file that you downloaded, accept the software license agreement, and choose a file path to extract the VHD to.

  • This creates a local VHD file that you can use to run a local virtual machine.

  • Sign in to the VM by using the following credentials:

    • User name: Administrator
    • Password: pass@word1

Create Purchase Orders - Confirm - Product receipt - Post using X++ in Dynamics 365 Finance & Operations

· 2 min read
Max Nguyen
FinOps Ranger

The script will work for Dynamics 365 Finance & Operations version

class MaxGeneratePO
{
public static void main(Args _args)
{
int i = 0; // number of purchase orders
NumberSeq numberSeq;
PurchTable purchTable;
PurchLine purchLine;
InventDim inventDim;

while (i <= 3)
{
ttsBegin;
MaxGeneratePO createPO = new MaxGeneratePO();
numberSeq = NumberSeq::newGetNum(PurchParameters::numRefPurchId());
numberSeq.used();
purchTable.PurchId = numberSeq.num();
purchTable.initValue();
purchTable.initFromVendTable(VendTable::find('US-101'));

if (!purchTable.validateWrite())
{
throw Exception::Error;
}

purchTable.insert();
inventDim.clear();
purchLine.clear();
purchLine.initValue();

purchLine.PurchId = purchTable.PurchId;
purchLine.ItemId = 'D0002';
inventDim.InventSiteId = "1";
inventDim.InventLocationId = "11";
purchLine.InventDimId=InventDim::findOrCreate(inventDim).inventDimId ;
purchLine.createLine(true, true, true, true, true, true);

purchLine.PurchQty = 5;
purchLine.PurchUnit = "ea";
purchLine.PurchPrice = createPO.randomAmount(); // get random amount nubmer
purchLine.LineAmount = purchLine.calcLineAmount();
purchLine.update();

//PO confirm
PurchFormLetter purchFormLetter;
PurchFormLetter purchFormLetterPack;
purchFormLetter = PurchFormLetter::construct(DocumentStatus::PurchaseOrder);
purchFormLetter.update(purchTable,
strFmt("ConNum_%1", purchTable.PurchId),
systemDateGet(),
PurchUpdate::All,
AccountOrder::None,
NoYes::No,
NoYes::no);
//Product receipt
createPO.proceed(purchTable.PurchId, purchLine.ItemId,purchLine.InventDimId,purchLine.PurchQty,strFmt("RptNum_%1", purchTable.PurchId));
//Post PO
createPO.postPOInvoice(purchTable.PurchId, strFmt("RptNum_%1", purchTable.PurchId));

info(strFmt("Purchase order '%1' has been created", purchTable.PurchId));
ttsCommit;
i++;
}
}

public boolean proceed(PurchId _purchId, ItemId _itemId,inventDimId _inventDimId, PurchQty _qty, PackingSlipId _productReceiptNumber)
{
return
this.generateProductReceipt(_purchId, this.addToPurchLineList(_purchId, _itemId, _inventDimId, _qty), _productReceiptNumber);
}

public boolean generateProductReceipt(PurchId _purchId, List _purchLineList, PackingSlipId _productReceiptNumber)
{
boolean ret = true;
PurchFormLetter purchFromLetter;
PurchTable purchTable = PurchTable::find(_purchId);

try
{
ttsbegin;
purchFromLetter = PurchFormLetter::construct(DocumentStatus::PackingSlip);
purchFromLetter.createFromLines(true);
purchFromLetter.parmLineList(_purchLineList.pack());
purchFromLetter.update(purchTable, _productReceiptNumber,
DateTimeUtil::getToday(DateTimeUtil::getUserPreferredTimeZone()),
PurchUpdate::All);
ttscommit;
}
catch
{
ret = false;
}
return ret;
}

public List addToPurchLineList(PurchId _purchId, ItemId _itemId,inventDimId _inventDimId, PurchQty _qty)
{
List purchLineList = new List(Types::Record);
PurchLine purchLine = PurchLine::findItemIdInventDimId(_purchId, _itemId, _inventDimId);

if(purchLine && _qty > 0)
{
purchLine.PurchReceivedNow = _qty;
purchline.modifiedField(fieldNum(PurchLine, PurchReceivedNow));
purchLineList.addEnd(purchLine);
}
return purchLineList;
}

public void postPOInvoice(PurchId purchId, PackingSlipId packingSlipId)
{
TmpFrmVirtual tmpFrmVirtualVend;
PurchFormLetter_Invoice purchFormLetter;
VendPackingSlipJour vendPackingSlipJour;
SysQueryRun chooseLinesQuery;
SysQueryRun chooseLinesPendingInvoiceQuery;
container conTmpFrmVirtual;
List selectedList = new List(Types::Record);

select firstonly vendPackingSlipJour
where vendPackingSlipJour.PurchId == purchId
&& vendPackingSlipJour.PackingSlipId == packingSlipId;

if (vendPackingSlipJour)
{
tmpFrmVirtualVend.clear();
tmpFrmVirtualVend.TableNum = vendPackingSlipJour.TableId;
tmpFrmVirtualVend.RecordNo = vendPackingSlipJour.RecId;
tmpFrmVirtualVend.NoYes = NoYes::Yes;
tmpFrmVirtualVend.Id = vendPackingSlipJour.PurchId;
tmpFrmVirtualVend.insert();
}

chooseLinesQuery = new SysQueryRun(queryStr(PurchUpdate));
chooseLinesQuery.query().addDataSource(tableNum(VendInvoiceInfoTable)).enabled(false);

// chooseLinesPendingInvoiceQuery needs to be initialized, although it will not be used
chooseLinesPendingInvoiceQuery = new SysQueryRun(queryStr(PurchUpdatePendingInvoice));
chooseLinesPendingInvoiceQuery.query().dataSourceTable(tableNum(PurchTable)).addRange(fieldNum(PurchTable,PurchId)).value(queryValue(''));

purchFormLetter = PurchFormLetter::construct(DocumentStatus::Invoice);
purchFormLetter.chooseLinesQuery (chooseLinesQuery);
purchFormLetter.parmQueryChooseLinesPendingInvoice(chooseLinesPendingInvoiceQuery);
purchFormLetter.purchTable (PurchTable::find(PurchId));
purchFormLetter.transDate (systemDateGet());
purchFormLetter.parmParmTableNum (strFmt("%1",packingSlipId)); //This is invoice number
purchFormLetter.printFormLetter (NoYes::No);
purchFormLetter.sumBy (AccountOrder::Auto);
purchFormLetter.specQty (PurchUpdate::PackingSlip);

while select tmpFrmVirtualVend
{
selectedList.addEnd(tmpFrmVirtualVend);
conTmpFrmVirtual = selectedList.pack();
}
purchFormLetter.selectFromJournal(conTmpFrmVirtual);
purchFormLetter.reArrangeNow(true);
purchFormLetter.run();
}

public int randomAmount()
{
RandomGenerate randomGenerate;
randomGenerate = RandomGenerate::construct();
randomGenerate.parmSeed(new Random().nextInt());
return RandomGenerate.randomInt(100, 800);
}

}

Thank you for reading.