Minimalistic EU VAT Configuration in Dynamics 365

Minimalistic EU VAT Configuration in Dynamics 365


Let me present my 4th iteration of the EU VAT setup in Dynamics. The below concise VAT configuration in Dynamics 365 for Finance has been tested over 3 years of my operations. It has been constantly updated with the changes in taxation. It covers intra-community import, export and import of services within the European Union and beyond, domestic supplies, business travel within the EU and abroad.

Before we begin with the setup let me explain some background facts and assumptions:

    • Any export of goods or services in or outside of the European Community is tax-free, but reported
    • An export of services is special: in accordance with Directive 2008/8/EC any service rendered for customers abroad is subject to reverse charge
    • In the case of an intra-community (IC) delivery, goods and services are presented separately on the EU Sales List
    • On the contrary, goods and services delivered in the home country are reported together and should not be distinguished
    • Intra-community deliveries are reported separately from the foreign trade with the so-called 3rd countries (i.e. countries which are not the 28 members of the EU, e.g. Norway or Switzerland). IC trade appears of the EU Sales list, while 3rd country trade does not. Deliveries of tangible goods are reported to the INTRASTAT in addition.
    • Most of the countries apply a full rate and several reduced rates. The reduced rates are there mostly for the basic consumer services and goods (Austria: 10%). They normally do not affect enterprises, until the employees start reporting travel expenses.
    • A semi-reduced “hotel” rate of 13% stands out in Austria, but also in Switzerland and France for accommodation. Update 2018: 13% were reverted back to 10% in Austria, but I am going to keep it in my scheme.
    • A grocery may have separate tax codes for an IC acquisition of Polish potatoes (self-assessed 10%) or French wines (self-assessed 13%). However, the reduced-% goods and services are rarely imported by manufacturing or professional service businesses, with the exception of foreign books and printed media (self-assessed 10%).
    • In Switzerland, the VAT receivable from investments is reported separately from the VAT received from current assets or services (de-ch: “Vorsteuer auf Investitionen und übrigem Betriebsaufwand” vs “Vorsteuer auf Material- und Dienstleistungsaufwand“) despite the same rate.
      Since 2019, a similar aspect has become valid in Austria too. The mandatory Chamber of Commerce contribution (de-at: “Kammerumlage 1” or KU1) is evaluated at 0,29% of the total VAT receivable excluding any investments i.e. assets acquired either domestically or abroad.
      The reduced VAT (foodstuffs, pharmaceuticals) may be neglected: milk is unlikely to become a long-term asset, unless condensed 😉
    • From the system point of view, there is a difference between a “zero rate” and “no rate”. A tax code with a 0,00 rate still logs the tax base for reporting, as long as a record in the tax Values table exists.
      From the fiscal standpoint, there is a difference between a “zero” an “no” rate too. For example, in the UK public transportation is taxable at 0% which is considered a tax rate of its own. In France, 0% is applied to newspapers. To enable a zero rate, just open the Values table in connection with the tax code, let the system create a record, leave the Value = 0,00000 and save the record.

VAT codes (en-us: Sales tax codes)

Tax code Name Rate AT Example
doF Domestic VAT, full 20% Sales or purchase of regular goods and services, e.g. raw materials, office consumables, car expenses (electric cars or ‘fiscal trucks’ only)
doA Domestic VAT from assets, full 20% Investments in long-term assets, i.e. machines, office equipment, electric car fleet
doH Reduced “hotel” rate 10% This reduced rate applies to accommodation, but not restaurant bills
doR Domestic VAT, reduced 10% Public transportation, taxi, and basic foodstuffs, including restaurant bills with non-alcoholic beverages, books and newspapers; in Germany it is slightly different: dishes served in a restaurant are charged with the full tax rate, while the ‘take away’ foodstuff is reduced
TF Tax free
Out of scope
0% Exempt international air and sea transportation, or services delivered by “non-genuine” tax exempt suppliers such as insurances, postal services (de-at: “unechte Steuerbefreiung“), but also the City tax (de-at: “Tourismusabgabe“, “Ortstaxe“) component on hotel bills. The base on purchases may still be reported to the authorities.
NR Not recoverable On a travel abroad, services consumed by the employee are taxable per se, and within the European Community the tax may even be recovered, but very few companies do so. The code is not reported to the authorities. Running expenses from traditional personal cars with internal combustion engines fall into this category too.
euF IC export
IC acquisition
Delivery of goods to another country of the European Community is tax free, but the value of the goods is reported to the tax authority.
On a regular import of goods from another member of the European Community the full domestic tax is levied; the tax is self-assessed with a zero effective tax load (what you pay is what you get recovered). This is achieved with the Use tax (en-us) i.e. Import VAT / purchase VAT (en-gb) setting in the VAT group.
euA IC asset export
IC asset acq.
It is highly likely, that the investments into long-term assets (machinery, other equipment) flow into another EU country. Less probably but also possible, is that used assets are sold to an EU neighbour.
euR Reduced IC export
IC acquisition
Delivery of goods to another country of the European Community is tax free, but the value of the goods is reported to the tax authority.
On goods that would be subject to the reduced tax rate domestically (e.g. books), the same reduced tax rate is applied when procuring such goods from another member of the EU; the tax is self-assessed with a zero effective tax load and the Use tax setting in the VAT group.
euS IC services export
IC services import
Delivery of services in another country of the European Community is tax free but reported.
The buyer, however, must obey the reverse charge principle. The setup for the reverse charge and the zero tax impact is similar to the above IC goods acquisition.
exF Export 3rd county
Import 3rd country
The export of goods is tax free in most of the countries, but the value has to be reported.
Tangible goods imported into the EU from a 3rd country is subject to an import tax, whose base is hard to calculate because it includes the portion of insurance and freight up until the border. Normally the import tax is calculated and paid by the customs broker; in the exotic import tax self-assessment mode (de-at: “Einfuhrumsatzsteuer geschuldet”) this exF tax code may be used to post the tax.
exS Services export 3rd
Services import 3rd
If the place of supply of services is outside of the EC, the export is tax-free but still have to be reported (as taxable elsewhere under the reverse charge regime).
The procurement of services outside of the EC is subject to a self-assessed reverse charge.

VAT groups (en-us: Sales tax groups)

In every business case in Dynamics 365 for Finance, the tax code is deducted by the system from an intersection between the customer/supplier VAT group and the product Item VAT group. Aside of the basic “F”, “S”, “TF” item VAT groups I recommend “H”, “Food” and “PubT” for the travel expense recording. The reason to separate the latter 2 groups is my reverence to companies recovering foreign taxes: tax rates for these categories vary across Europe.

It is a good practice to have a valid tax code, i.e. a valid Customer/Item group combination in every business case whenever it is present on the tax declaration or not. This is enforced by the tax parameter Check VAT groups. For example, on a business trip from the UK to Hungary the VAT may theoretically be recovered, but hardly any company does it. One way is omit the tax completely, but to stay compliant to the Check VAT groups setting, the tax code NR has been introduced. The respective VAT group AP-NR may be then assigned to the country HUN in the Travel and Expense module, and any foreign Hungarian VAT will be neglected yet formally recorded on the tax code NR.

Any business case we do not anticipate or consider a human mistake is going to result in a missing VAT code, e.g. an attempt to sell milk by a consulting company, or a foreign trade operation in a society without an EORI number. With the strong Check VAT groups setting such an attempt is going to result in an error and roll the transaction back, preventing havoc in the ledgers.

Customer or supplier VAT group Item VAT group “F” (full) Item VAT group “A” (assets) Item VAT group “S“ (services) Item VAT group “TF“ (tax free) Item VAT group “H” (hotel) Item VAT group “Food“ Item VAT group “PubT“ (public transp.)
AP-DO doF doA doF TF* doH doR doR
AP-EU euF** euA euS TF NR euR NR
AR-DO doF doA doF TF      
AR-EU euF euA euS TF      
AR-EX exF   exS TF      

*) Tax codes to be marked Exempt in the VAT group configuration are stroke through,

**) and those marked as a Use tax are in italic.


The above configuration is going to work well in the D-A-CH countries, in Scandinavia, in the UK, and even in Spain. Yet it doesn’t account for the “VAT on payment” aka Conditional tax common in France (de: “IST-Besteuerung“), and it will probably not work for countries with inflated reporting requirements (Italy).

Input validation and messaging in the Process Guide Framework

Input validation and messaging in the Process Guide Framework

A few words of introduction: Process Guide framework is THE framework for new Warehouse management mobile screens and menu items in “Dynamics 365 Supply Chain Management”:

I find that the topic of the validation of data entered or scanned by the user is very superficially covered. When you read the above shallow documentation you may think the ProcessGuideStep.isComplete() method is a good place to give an error, but it is NOT. It would be too late: by the time the execution reaches isComplete() the user input has already been processed, accepted and serialized in the session state aka “pass” WHSRFPassthrough.

I figured out that the true validation may be programmed at 2 places: in the process() method of a class derived from the WHSControl or in the ProcessGuideStep.validateControls() method. The 2 ways require slightly different techniques and have distinct flavours:


Inheriting the WHSControl class offers an object-oriented alternative to extending the WHSRFControlData class (this spaghetti code has been recently refactored and moved to WHSRFControlData.processLegacyControl). This technique is briefly described in the blog …/mfp/posts/extending-whs-adding-a-new-control-type .
There are pros and cons. The class is going to be used across the whole warehouse mobile app whenever you add a control with this name. This can be both good and bad:

  • The major disadvantage is that the WHSControl.process() is not triggered if the user has not entered anything into the field on the mobile device. E.g. one cannot check for an empty value, and this may lead to the “xxx is not found in the map” error message if one does not check existence of a parameter before fetching it from the “pass” WHSRFPassthrough.
  • The same validation logic is used everywhere. The code re-use is innate. This can be however too rough; for example, the below code snippet was written because the standard logic of the ProdID control was too broad: it was populating the item number and validating if it had a BOM.
  • The control is not fully aware of the context. It cannot reach the current Process Guide step object and call the Process Guide controller object back.
  • It is not fully aware of the surrounding controls either: the controls placed on the page after the current one have left no trace yet.
  • The control class can leverage the WHSRControlData instance with all its perks and shortcuts.

In WHSControl.process() you take the user entry from the parmData() and return a false result with the method:
#WHSRF [WhsControlFactory(#ProdIdLabel)]
public class WHSControlProdId extends WhsControl
public boolean process()
boolean ok = super();
ProdId prodId = this.parmData();
fieldValues.insert(ProcessGuideDataTypeNames::ProdId, prodId);
if (! ProdTable::exist(prodId))
ok ="@WAX1162"); // The production or batch order number is not valid
return ok;

Note that you have to care about parsing the entry and storing it in the fieldValues container. On an error, the processing stops, the page reloads and the error message(s) is shown:
WHS warning


The validation in the ProcessGuideStep class has a different quality:

  • One can check for empty (mandatory) data.
  • The logic can only be re-used in a menu item made in the Process Guide framework. Such menu items are too few. You re-use the logic by adding the whole step = screen to the process flow.
  • The step has a full access to the process controller and to the preceding step results.
  • All controls are validated at once and can be checked for mutual consistency.
  • The WHSRControlData instance with all its useful methods has already been disposed of by the time execution reaches the validateControls() method. You have to write most of the business logic from scratch.

The validateControls() method is executed before the fieldValues container is merged with the “pass” container (WHSRFPassthrough) and becomes available to the mobile application. This is why you take the user entry safely from the processingResult.fieldValues structure. If you encounter an error, you throw a warning:
class ProcessGuideItemConsumpPromptQtyStep extends ProcessGuideStep
protected void validateControls()
WhsrfPassthrough pass = controller.parmSessionState().parmPass();
qty = processingResult.fieldValues.lookup(ProcessGuideDataTypeNames::Qty);
if (qty * pass.parmQty() < 0)
throw warning("@SYS25506"); // Quantity may not change sign
protected ProcessGuidePageBuilderName pageBuilderName()
return classStr(ProcessGuideItemConsumpPromptQtyPageBuilder);

The warning thrown interrupts the flow, re-loads the page and presents the warning.

Success message

A related topic is an ability to present a success message to the user at the end of the process execution. You do it at the end of the doExecute() method in the process guide step:
protected void doExecute()
// do the work

This implementation is very limited, however: it cannot uptake a custom message and always shows the same text “Work Completed”.

The below snippet is smarter:
ProcessGuideMessageData messageData = ProcessGuideMessageData::construct();
messageData.message = strFmt("@SYS24802", journalId); // Journal cannot be posted because it contains errors.
messageData.level = WHSRFColorText::Warning;
navigationParametersFrom = navigationParametersFrom ? navigationParametersFrom : ProcessGuideNavigationParameters::construct();
navigationParametersFrom.messageData = messageData;

Unrelated conclusion: use the process guide framework and say goodbye to the WHSWorkExecute mess! 🙂

Get a cost center in D365FO

Get a cost center in D365FO

It is shocking to see over and over again functional consultants failing to create the Cost center dimension backed by the Organization units table (Organisation administration > Organisations > Operating units), choosing a custom dimension type instead and loosing the ability to specify an address, make a hierarchy of cost centers, specify a cost center manager etc.

This has also given a rise to excruciatingly dumb custom code snippets predating the dimension name CostCenter as if there were no British English or other languages.

With the proper setup and the proper coding this reduces to 3 essential lines:

static void main(Args _args)
DefaultDimensionView defaultDimensionView;
RefRecId testDefaultDimensionRecId =
(select firstonly DefaultDimension from ProdTable where ProdTable.ProdId == "P000173").DefaultDimension;
// Copy of the private method OMOperatingUnit::getDimensionViewId()
RefTableId getDimensionViewId(OMOperatingUnitType _omOperatingUnitType)
Dictionary dict = new Dictionary();
DictEnum dictEnum = new DictEnum(enumNum(OMOperatingUnitType));
return dict.tableName2Id(strFmt('DimAttribute%1', dictEnum.index2Symbol(_omOperatingUnitType)));

// Filter by the virtual backing entity table Id for cost centers
select defaultDimensionView
where defaultDimensionView.BackingEntityType == getDimensionViewId(OMOperatingUnitType::OMCostCenter)
&& defaultDimensionView.DefaultDimension == testDefaultDimensionRecId;