Deferred revenue in foreign currency: closing the gap in D365

Deferred revenue in foreign currency: closing the gap in D365

The gap

In Microsoft Dynamics 365 for Finance, Deferred revenue in Subscription Billing works reliably as long as you stay within a single currency context. Once foreign currencies are involved, a design flaw starts to show its effects: the system recognises deferred revenue exclusively in accounting currency. The original transaction currency is disregarded, and the exchange rate used at invoice posting is not retained in the deferral logic.

This is consistent behaviour Revenue <-> COGS, but it creates a persistent issue: accounts stop balancing in currency, and this is even carried forward from year to year, at every FY closing.

At invoice posting, the exchange rate is effectively fixed, and from there, the deferral engine operates purely in accounting currency. This becomes visible when the deferred balance is fully recognised. In accounting currency, the balance reaches zero as expected. In transaction currency, however, it does not. Residual amounts remain not because of revaluation or fluctuating exchange rates, but because the postings were never aligned in the first place.

From a reconciliation point of view, this is difficult to justify. An account that is technically cleared still shows values in another currency view.

It is worth noting that this behaviour has been raised repeatedly: many customers and partners complained, the “Idea” Deferral schedule recognition to include transaction currency is at the top of complaints about the Subscription billing module.

Customisation: store the historical exchange rate, fix the recognition journal

Rather than working around the issue, the approach taken here was more straightforward: the Subscription Billing deferral logic was adjusted so that revenue recognition is posted in the original transaction currency, using the exchange rate from the invoice; in other words, the system was made to behave as one would expect.

Technically, this requires intervening in the deferral posting process. First, we add 3 fields – currency, amount, and rate to the Deferrals schedule table, and populate them:

				
					[ExtensionOf(classStr(SubBillDeferralScheduleCreate))]
final class SubBillDeferralScheduleCreate_Extension
{
    public static void initDeferralScheduleTable(
        SubBillDeferralAmountSource _subBillDeferralAmountSource,
        SubBillDeferralTransactionLineDeferral _deferralTable,
        SubBillDeferralScheduleTable _schedTable,
        boolean _usingShortTerm)
    {
        next initDeferralScheduleTable(_subBillDeferralAmountSource, _deferralTable, _schedTable, _usingShortTerm);
        if (_subBillDeferralAmountSource == SubBillDeferralAmountSource::DeferralAmount)
        {
            _schedTable.SubBillDeferralAmountCur = _deferralTable.SubBillDeferralAmountCur;
            _schedTable.ExchRate = _deferralTable.ExchRate;
            _schedTable.CurrencyCode = _deferralTable.CurrencyCode;
        }
    }
}
				
			

The form Subscription billing > Revenue and expense deferrals > Deferral schedules > All deferral schedules has been extended to reflect the new fields, too: 

Next, we make sure that the 3 values are carried through correctly as the invoice is posted and the final schedule lines are re-initialised from the posted invoice line:

				
					[ExtensionOf(tableStr(SubBillDeferralTransactionLineDeferral))]
final class SubBillDeferralTransactionLineDeferral_Extension
{
    public void updateFromTransaction()
    {
        next updateFromTransaction();

        if (! this.TransRecId)
        {
            return;
        }
        switch (this.SubBillDeferralSourceRecType)
        {
            case SubBillDeferralSourceRecType::SalesLine:
                if (this.SubBillDeferralTransactionType != SubBillDeferralTransactionType::SalesOrder)
                {
                    // Not supported
                    return;
                }
                SalesLine salesLine = SalesLine::findRecId(this.TransRecId);
                this.SubBillDeferralAmountCur = salesLine.calcGrossAmountExclTax(this.Qty);
                SalesFixedExchRate fixedExchRate = salesLine.salesTable().fixedExchRate();
                if (fixedExchRate != 0.0)
                {
                    this.ExchRate = fixedExchRate;
                }
                else
                {
                    ExchangeRateHelper helper = ExchangeRateHelper::construct();
                    helper.parmFromCurrency(salesLine.CurrencyCode);
                    helper.parmExchangeDate(this.TransDate);
                    helper.parmToCurrency(Ledger::accountingCurrency());
                    this.ExchRate = helper.getExchangeRate1();
                }
                this.CurrencyCode = salesLine.CurrencyCode;
                break;

            case SubBillDeferralSourceRecType::CustInvoiceTrans:
                CustInvoiceTrans custInvoiceTrans = CustInvoiceTrans::findRecId(this.TransRecId);
                this.SubBillDeferralAmountCur = custInvoiceTrans.LineAmount + custInvoiceTrans.SumLineDisc;
                this.ExchRate = custInvoiceTrans.exchRate();
                this.CurrencyCode = custInvoiceTrans.CurrencyCode;
                break;

            default:
                return;
        }

        this.SubBillDeferralAmountCur = abs(this.SubBillDeferralAmountCur);
    }
}
				
			

At last, these stored rates are used in the monthly revenue recognition (do not Summarise the journal lines!). This is the hardest part, because the class SubBillDeferralLedgerJournalCreate  is nearly impossible to extend in the D365 FO standard, as the developers either “privatised” or “internalised” all methods for no obvious reason. All.. but one. I’ll leave this adventurous Chinese hack uncommented:

				
					[ExtensionOf(tableStr(LedgerParameters))]
final class LedgerParameters_Extension
{
    public static boolean isChineseVoucher_CN()
    {
        boolean ret = next isChineseVoucher_CN();
        if (ret)
        {
            return ret;
        }

        str txt = con2Str(xSession::xppCallStack());
        if (strscan(txt, "SubBillDeferralLedgerJournalCreate", 1, 1000) > 0  &&
            strscan(txt, "assignChineseVoucherFromDefaultType_CN", 1, 1000) == 0)
        {
            ret = true;
        }
        return ret;
    }

}
				
			

The hack then ultimately triggers the actual update of the general journal line with the proper amount and rate:

				
					[ExtensionOf(classStr(SubBillDeferralLedgerJournalCreate))]
final class SubBillDeferralLedgerJournalCreate_Extension
{
    protected void assignChineseVoucherFromDefaultType_CN(LedgerJournalTrans _ledgerJournalTrans)
    {
        next assignChineseVoucherFromDefaultType_CN(_ledgerJournalTrans);

        SubBillDeferralScheduleNumber   schedNumber = subStr(_ledgerJournalTrans.Txt, 1, strFind(_ledgerJournalTrans.Txt, " ", 1, 20)-1);
        if (! schedNumber)
        {
            return;
        }
        SubBillDeferralScheduleTable deferralScheduleTable;
        deferralScheduleTable = SubBillDeferralScheduleTable::find(schedNumber);
        if (! schedNumber)
        {
            return;
        }
        if (deferralScheduleTable.ExchRate == 0.00 || deferralScheduleTable.ExchRate == 100.00)
        {
            return;
        }

        _ledgerJournalTrans.CurrencyCode = deferralScheduleTable.CurrencyCode;

        if (_ledgerJournalTrans.AmountCurDebit)
        {
            _ledgerJournalTrans.AmountCurDebit = CurrencyExchangeHelper::curAmount(
                _ledgerJournalTrans.AmountCurDebit,
                deferralScheduleTable.CurrencyCode,
                deferralScheduleTable.TransDate,
                UnknownNoYes::Unknown,
                deferralScheduleTable.ExchRate);
        }
        else
        {
            _ledgerJournalTrans.AmountCurCredit = CurrencyExchangeHelper::curAmount(
                _ledgerJournalTrans.AmountCurCredit,
                deferralScheduleTable.CurrencyCode,
                deferralScheduleTable.TransDate,
                UnknownNoYes::Unknown,
                deferralScheduleTable.ExchRate);
        }
        _ledgerJournalTrans.ExchRate = deferralScheduleTable.ExchRate;
    }
}
				
			

The result

…is simple: balances clear cleanly not only in accounting currency, but also in transaction currency. There are no residual amounts, no inconsistencies, and no need for explanations during reconciliation. A long-standing and widely discussed gap is effectively closed.

You may download the source code here: SubBillDeferral_with2currencies

Dynamics 365 Intercompany Cost+ pricing

Dynamics 365 Intercompany Cost+ pricing

As highlighted 10 years ago by M. Aamer in his article Inter-company goods trading (sales price equal to cost price) – Microsoft Dynamics AX 2012 – Microsoft Dynamics 365 Blog, there is a parameter Unit price equal to cost price in the intercompany settings in Dynamics 365 for SCM, which enforces the intercompany purchase price at the cost price in the intercompany selling company.

The system tries to update the Sales order – Purchase order pair on 3 occasions:

  • ICO sales order line creation;
  • reservation of the goods in the supplier company;
  • release (!) to the warehouse in that company i.e., just before picking.

The resulting sales price is the current moving average stock price. D365 traverses the inventory transactions, and if no price is known at the given inventory dimensions, it defaults to the price at the item master (this may be relevant in the engineer-to-order scenario, where the ultimate cost price becomes known just shortly before the shipping).

As noted many years ago by my humble self 😉 , the pure cost price is normally not allowed by accounting standards: the “arm’s length” principle must be respected in transfer pricing between companies within a group i.e., there must be a markup on top. The Intercompany percent charge was developed recently by Microsoft to comply: Set up charges on intercompany orders – Supply Chain Management | Dynamics 365 | Microsoft Learn.

At some locations, the misc. charge is not good enough, as the auditors favour an “opaque” sales price with an embedded markup percentage. This may in principle be achieved with a negative line discount to the sales price = cost price.  Normally, negative discounts are frowned upon by D365: “Field 'Discount percentage 1'(= -15,00) can only contain positive numbers.“, but the trick described in the blog “Overwrite a read-only configuration in D365FO” will let you import such a trade agreement.

At the recipient’s side, at the ICO Purchase order, the accounting distribution will be split into the base price (= ICO cost) and the [negative] discount. To satisfy the request for one single SO revenue GL transaction and one single PO change-in-stocks GL transaction, a customization was made out of pure desperation. The markup percentage is stored in a custom field Intercompany percent at the seller’s Item group, and quietly added to the net sales price in the following routine (you may download the source code here: ICOCostPlus.axpp):

				
					[ExtensionOf(classStr(SalesLineType))]
final class SalesLineType_ICOCostPlus_Extension
{
    public CostPrice interCompanyCalcSalesPrice()
    {
        SalesPrice          salesPrice = next interCompanyCalcSalesPrice();

        if (! salesLine.InterCompanyInventTransId ||
            salesLine.SalesQty < 0 ||
            ! salesLine.isStocked())
        {
            return salesPrice;
        }
        if (! salesTable.interCompanyEndpointActionPolicy().UnitPriceEqualsCostPrice)
        {
            return salesPrice;
        }

        InventItemGroup itemGroup = InventItemGroupItem::findByItemIdLegalEntity(salesLine.ItemId, salesLine.DataAreaId).itemGroup();

        if (itemGroup.ICOSalesPercentMarkup != 0)
        {
            salesPrice += CurrencyExchangeHelper::price(salesPrice * itemGroup.ICOSalesPercentMarkup/100, salesLine.CurrencyCode);
        }

        return salesPrice;
    }

}

				
			

Accrued revenue (Revenue recognition)

Accrued revenue (Revenue recognition)

Introduction

Let’s build upon the yesterday’s business case. A guarantee, a maintenance, an insurance, a rental service is backed by a contract and span over a period of time, as opposed to a single delivery of goods. If the service is pre-paid at the beginning of the contract period, then the revenue should be allocated over time. This is known as

  • EN: Accrued revenue or deferred revenue/income
  • DE: Passive Rechnungsabgrenzung
  • RU: Доходы будущих периодов
  • FR: Régulation passif

Setup

Once the Revenue recognition module is activated, there is a bit of configuration to be done.

First of all, a general journal (General ledger > Journal setup > Journal names or Revenue recognition > Setup > Journal names) of the novel type Revenue recognition must be created. It is used to preview the periodic (monthly) revenue recognition before posting. The journal name is then selected as a default Revenue recognition journal name in General ledger parameters (this seems to be an innovation relative to the original Armanino module).

Next, the Item group of the service must be provided (Revenue recognition > Setup > Inventory and product setup > Posting) at least one additional account: Deferred revenue. It is a liability (we owe something to the customer for a service not yet rendered in full), a passive balance account.

In the case of a back-to-back contract if we are reselling a 3rd party [financial] service to the customer, the service has a cost price. It must be spread over time along with the revenue. This can be reflected by buying the service item ‘to the stock’ and treating it as a tangible item. The sales order produces COGS, and to accrue this expense we may need a separate account Deferred cost of goods sold. It is an asset (the services supplier owes something to us), an active balance account.

Finally, a Revenue recognition > Setup > Revenue schedules must be created:
Revenue schedule

  • Occurrences: the number of periods to allocate;
  • Recognition basis: the revenue may be allocated to calendar periods equally (1/12, 1/12, 1/12, 1/12, …) or in the proportion of the days in the month (31/365, 28/365, 31/365, 30/365…), or non-uniformly (then the allocation percentages are entered by the user in the Revenue schedule details);
  • Recognition convention: when to start recognizing. Sadly, if you choose Actual start date, and the service was billed on the 3rd etc. of the month, the revenue is going to be recognized on every 3rd on the following months;
  • Auto hold: any new revenue recognition records are put on hold and must be manually unlocked before taken over into the revenue GL journal;
  • Automatic contract terms: apparently, this option did not exist in the original Armanino ISV module. It pre-populates the start and the end date of contract in the sales order line, based on the number of occurrences and the length of the period.

If the service always brings about deferred revenue, the revenue schedule may be assigned to the Released product on the Revenue recognition tab:
Default revenue schedule
The selected revenue schedule is then automatically applied to every new sales order line with this item. It works even with bundle components. Furthermore, a default Revenue schedule may be assigned to the whole item group to pre-populate the items.

Process

The service item is added to a service item. Pay attention to the Revenue schedule column far to the right:
Sales order line contract terms
It is pre-populated with the default Revenue schedule from the item master, or it may be amended / entered ad-hoc by the user. Next, the begin and the end of the contract must be specified in the Contract terms field group. With the Automatic contract terms on, the terms are evaluated automatically, starting from the day of today.

The allocation may be pre-viewed: check the Expected revenue recognition schedule on the Manage button ribbon. The expected revenue is persistent and rebuilt on every sales order confirmation.

Now deliver and bill the sales order. The revenue (and COGS) are posted not into the P&L but the BS accounts chosen before. Any due revenue from the current and the past periods (imagine the service was billed in the middle of the contractual period) is “caught up” with the first periodical revenue recognition.
Deferred revenue voucher
A Revenue recognition schedule has been set up for the sales order (Manage ribbon or the Revenue recognition > Inquiries and reports > Revenue recognition inquiry). The financial dimensions in the schedule are retrieved from the sales order header and lines. The percentages are taken from the revenue schedule details.

Once a month the user should check the Unprocessed revenue recognition schedules (workspace Revenue recognition > Workspaces > Revenue management) and apply the action Create journal:
Revenue recognition workspace: Create journal
The system is going to pick up any outstanding revenue records up to the As of date (i.e. catching up the revenue if the service was billed in the middle of the contractual period) and produce a GL journal with both the revenue and the COGS.

This journal (Revenue recognition > Journal entries > Revenue recognition journals) is left for approval and posting by the accounting department.

Conclusion

In the past, one of the few options to get an Accrued revenue was to use the straight-line revenue recognition mode in the Project accounting module, but the Revenue recognition module now offers a better, user-friendly solution that supports sales orders.
In the future, this offer may be rounded up by the Deferrals, providing a comparable user experience for the Accrued expenses.