Austrian Peppol-UBL e-invoice

E-Invoice in D365
E-Invoice in D365

Austrian Peppol-UBL e-invoice

…in its version 304.13.25 does not work out of the box in Dynamics 365 for Finance, unfortunately. First and foremost, the public entity’s order number (the Customer requisition at the sales header order or in the project contract funding source details) is a must! As you specify the Customer requisition, a reference to the PO line number reference becomes mandatory, too. Yet this invoice line-level field is either malformed or missing. Moreover, Austria always requires an email address of the supplier (us) which is disabled or not working in all 4 standard ER formats. This can be the default e-mail address of the company in the legal entity settings.

As a good start, I recommend the excellent blog series E-Invoice in D365FO for Norway – Part 1 (Intro) – DynFOTech

The core configuration includes specifying the Endpoint ID of the supplier (ours) under Bank account / Routing number in the legal entity parameters, and providing the conversion of the units of measure into the UN standard, see the blog above.  For every unit of measure there must be an own Code ‘axis’ to compensate for the well known design flaw in the external code values cardinality in Dynamics 365 for SCM:

Then, to finally make it work, you have to extend all 4 standard electronic formats from Microsoft and make at least the following changes:

Format changes

Issue/Deficiency Path Peppol Sales invoice Peppol Project Invoice Peppol Sales Credit Note Peppol Project Credit Note
Mandatory Seller/ElectronicMail cac:AccountingSupplierParty/cac:Party/cbc:ElectronicMail must be enabled all the way down, and mapped like this: IF(Invoice.InvoiceBase.CompanyContact.ElectronicMail<>"", Invoice.InvoiceBase.CompanyContact.ElectronicMail, Invoice.InvoiceBase.CompanyInfo.Email) IF(ProjectInvoice.InvoiceBase.Contact.ElectronicMail<>"", ProjectInvoice.InvoiceBase.Contact.ElectronicMail, ProjectInvoice.InvoiceBase.CompanyInfo.Email) IF(Invoice.InvoiceBase.CompanyContact.ElectronicMail<>"", Invoice.InvoiceBase.CompanyContact.ElectronicMail, Invoice.InvoiceBase .CompanyInfo.Email) IF(ProjectInvoice.InvoiceBase.Contact.ElectronicMail<>"", ProjectInvoice.InvoiceBase.Contact.ElectronicMail, ProjectInvoice.InvoiceBase.CompanyInfo.EMail)
Wrong OrderLineRef.ID, namely taken from ProjTransId = alphanumeric instead of numeric Every of the five cac:InvoiceLine/cac:OrderLineReference/cbc:LineID must be unique and therefore enumerated. In the the Project Credit note the tags are missing completely and need to be added from scratch (5x) ('$ATLineNumberCollection'.Collect(1) + '$ATLineNumberCollection'.Sum(false))-1 ('$ATLineNumberCollection'.Collect(1) + '$ATLineNumberCollection'.Sum(false))-1
Missing PaymentMeans, i.e. our (beneficiary) bank account cac:PaymentMeans/cac:PayeeFinancialAccount/cbc:ID, schemeID and .../cac:FinancialInstitutionBranch/cbc:ID must be enabled all the way down and mapped ProjectInvoice.InvoiceBase.CompanyInfo.BankAccount.IBAN ProjectInvoice.InvoiceBase.CompanyInfo.BankAccount.SWIFT
Missing OrderReference (their PO number) cac:OrderReference/cbc:ID must be enabled Invoice.PurchaseOrder
Missing or wrongly formatted line quantity; the sign is properly displayed in Denmark only 🙂 cac:CreditNoteLine/cbc:CreditedQuantity must be enabled IF(OR(Invoice.InvoiceBase.CompanyInfo.PostalAddress.Country="DK", Invoice.InvoiceBase.CompanyInfo.PostalAddress.Country="AT"), -(@.LineBase.Quantity), @.LineBase.Quantity) -(@.LineBase.Quantity)
The ActualDeliveryDate ("Leistungsdatum") refers to the date or the corrected invoice. The data is not really known and not properly mapped, here is a stub: cac:Delivery/cbc:ActualDeliveryDate is completely missing and needs to be added first Invoice.InvoiceBase. ShipmentDate ProjectInvoice. InvoiceBase.VATRegisterDate
The root node bears no name, hence the format cannot be assigned a destination Call the root node "XMLHeader", for example X X

With regards to the collection $ATLineNumberCollection for the enumeration of invoice lines, refer to respective blog below.

Useful links

 

D365 Mass de-reservation utility

Prehistoric art at the Niedersachsenhaus lodge, Austria

D365 Mass de-reservation utility

Cancel BOM reservation in D365FO

In the Production control module in Dynamics 365 for SCM there is an automatic BOM reservation routine. For example,  set the reservation on Estimation in the module parameters, run the materials Estimation for all open orders, done. If you did it by mistake, it is you who are done, because in the case of a just-in-time supply and a long production order backlog, the reservation is going to quickly deplete the stock, preventing any future urgent order from starting.

Since the reservation can only be cancelled production order by production order, BOM line by BOM line, then in a medium sized plant it can take a few days of work. In Dynamics 365 Business Central aka “Navision” the state of affairs is similar.

The below is a programmatic solution, a runnable class for mass de-reservation. You may place it on a D365 menu, or just run the following command in the browser:  https://xxxprod.operations.dynamics.com/?mi=SysClassRunner&cls=InventDeReserve

The user may select the type of the order to cancel the reservation for, and add additional criteria such as the warehouse, expected date of the order et cetera. Set the safeguard parameter Cancel reservation to Yes, then run with OK. Upon execution, the system is going to show a number of success messages like “Reference: Production line, Number: B000021 Item number: 205026, Lot ID: 002516 Reservation has been removed“.

You may copy or download the X++ source code here: InventDeReserve.axpp

				
					class InventDeReserve extends RunBaseBatch implements BatchRetryable
{
    QueryRun        queryRun;
    boolean         dereserveNow = false;
    DialogField     dlgDereserveNow;

    #define.CurrentVersion(1)
    #localmacro.CurrentList
        dereserveNow
    #endmacro

    protected void deReserveByQuery()
    {
        while (queryRun.next())
        {
            InventTrans inventTrans = queryRun.get(tableNum(InventTrans));
            if (inventTrans.StatusIssue != StatusIssue::ReservPhysical && inventTrans.StatusIssue != StatusIssue::ReservOrdered)
            {
                continue;
            }
            InventDim       inventDim = queryRun.get(tableNum(InventDim));
            InventDimParm   inventDimParm;
            inventDimParm.initFromInventDim(inventDim);

            ttsbegin;

            InventUpd_Reservation::newParameters(InventMovement::construct(inventTrans),
                // At these specific inventory dimensions only
                inventDim, inventDimParm, InventDimFixedClass::inventDimParm2InventDimFixed(inventDimParm),
                -inventTrans.Qty,
                false, // Do not allow auto reserve dim
                true /*Show info*/).updateNow();            

            ttscommit;
        }
    }
    public void run()
    {
        #OCCRetryCount

        if (! dereserveNow)
        {
            info("@SYS52714"); // Nothing to do
            return;
        }

        try
        {            
            this.deReserveByQuery();
        }
        catch (Exception::Deadlock)
        {
            retry;
        }
        catch (Exception::UpdateConflict)
        {
            if (appl.ttsLevel() == 0)
            {
                if (xSession::currentRetryCount() >= #RetryNum)
                {
                    throw Exception::UpdateConflictNotRecovered;
                }
                else
                {
                    retry;
                }
            }
            else
            {
                throw Exception::UpdateConflict;
            }
        }
    }
    public container pack()
    {
        return [#CurrentVersion, #CurrentList, queryRun.pack()];
    }
    public boolean unpack(container _packedClass)
    {
        Version     version = RunBase::getVersion(_packedClass);
        container   packedQueryRun;
        
        switch (version)
        {
            case #CurrentVersion:
                [version, #CurrentList, packedQueryRun] = _packedClass;
                queryRun = new QueryRun(packedQueryRun);
                break;
            default:
                return false;
        }

        return true;
    }
    public Object dialog()
    {
        DialogRunbase       dialog = super();
        
        dlgDereserveNow = dialog.addFieldValue(extendedTypeStr(NoYesId), dereserveNow, "@SYS50399" /* Cancel reservation */);

        return dialog;
    }
    public boolean getFromDialog()
    {
        dereserveNow = dlgDereserveNow.value();

        return super();
    }
    static void main(Args _args)
    {
        InventDeReserve    inventDeReserve = new InventDeReserve();
        if (inventDeReserve.prompt())
        {
            inventDeReserve.runOperation();
        }
    }
    public void initParmDefault()
    {
        super();
        queryRun = new QueryRun(new Query(queryStr(InventDeReserve)));
    }
    QueryRun queryRun()
    {
        return queryRun;
    }
    public boolean showQueryValues()
    {
        return true;
    }
    static ClassDescription description()
    {
        return "@SYS50399"; // Cancel reservation
    }
    public final boolean isRetryable()
    {
        return true;
    }
    public boolean canGoBatchJournal()
    {
        return true;
    }
    public boolean canRunInNewSession()
    {
        return false;
    }
}

				
			

Print a custom product label: a Template solution in Process Guide Framework, with detours

Print Product Label
Print Product Label

Print a custom product label: a Template solution in Process Guide Framework, with detours

I wanted to have a template code for a new Warehouse Management App menu item in Dynamics 365 for SCM. The guidance …/warehousing/process-guide-framework is good, but a few aspects are missing there. This sample takes an Item ID (released product), looks for a custom label for products, deducts a default printer and sends a requested number of product labels to the printer. It does not require any parameters but the mobile device menu item setup to perform this job, which is my credo: the less configuration I have, the less conversations with the customer must be led, and the less documentation must be written. As we all agree, writing documentation – including this one – is utterly boring.

ProcessGuideController and WHSWorkExecuteMode

The below mobile device menu item is an indirect one, i.e. it is not based on any warehouse work. A new menu item class is instantiated in connection with a new WHSWorkExecuteMode enumeration element, while an indirect menu item is driven by an Activity code in Warehouse management > Setup > Mobile device , which is a different enumeration: WHSWorkActivity:

This is why you have to extend each of the enums with a new element of the same name (here: WHSWorkExecuteMode::PrintProductLabel = WHSWorkActivity::PrintProductLabel). One is converted into another; you do not have to explicitly program the mapping anymore: it is enough for the elements to have precisely the same name.

The code in the controller is a no-brainer:

				
					[WHSWorkExecuteMode(WHSWorkExecuteMode::PrintProductLabel)]
public class ProcessGuideProductLabelController extends ProcessGuideController
{
    protected final ProcessGuideStepName initialStepName()
    {
        return classStr(ProcessGuideProductLabelItemIdStep);
    }

    protected ProcessGuideNavigationRoute initializeNavigationRoute()
    {
        ProcessGuideNavigationRoute navigationRoute = new ProcessGuideNavigationRoute();

        navigationRoute.addFollowingStep(classStr(ProcessGuideProductLabelItemIdStep), classStr(ProcessGuideProductLabelNoOfLabelsStep));
        navigationRoute.addFollowingStep(classStr(ProcessGuideProductLabelNoOfLabelsStep), classStr(ProcessGuideProductLabelItemIdStep));

        return navigationRoute;
    }
}
				
			

For the new process guide flow to come up, do not forget to SysFlushAOD: Refresh SysExtension cache in D365FO. For the Use process guide slider to automatically be set at the mobile device menu item (see above), we often use the following table extension:

				
					[ExtensionOf(tableStr(WHSRFMenuItemTable))]
internal final class WHSRFMenuItemTable_Extension
{ 
    protected boolean workActivityMustUseProcessGuideFramework()
    {
        boolean ret = next workActivityMustUseProcessGuideFramework();
        ret = ret || (this.WorkActivity == WHSWorkActivity::PrintProductLabel);
        return ret;
    }
}
				
			

Declarations to support detours

The menu item must be able not only to be called from the main mobile device menu, but also receive parameters from other menus / processes. It should be able to uptake the Item ID and print a product label right away. This is called a detour:  Configure detours for steps in mobile device menu items – Supply Chain Management | Dynamics 365 | Microsoft Learn.
We have to declare all possible steps and fields of the new menu item with a descendant of WHSMobileAppFlow:

				
					[WHSWorkExecuteMode(WHSWorkExecuteMode::PrintProductLabel)]
public final class WHSMobileAppFlowProductLabel extends WHSMobileAppFlow
{
    protected void initValues()
    {
        this.addStep(WHSMobileAppStepIds::ItemId);
        this.addStep(WHSMobileAppStepIds::WaveLblQty);

        this.addAvailableField(extendedTypeNum(ItemId));
        this.addAvailableField(extendedTypeNum(NumberOfLabels));
    }
}
				
			

Once declared and compiled, refresh the SysExtension cache again, open Warehouse management > Setup > Mobile device > Mobile device steps and use Create default setup there.

I also have a unique field “NumberOfLabels” to prompt the number of label copies. It must be declared with a class, a descendant of WHSField. It does not need a WHSControl descendant (see also my Input validation and messaging in the Process Guide Framework), because its behaviour is quite standard.

 

				
					[WHSFieldEDT(extendedTypeStr(NumberOfLabels))]
public class WHSFieldNumberOfLabels extends WHSField
{
    private const WHSFieldName             Name        = "@WAX:NumberOfLabels";
    private const WHSFieldDisplayPriority  Priority    = 10;
    private const WHSFieldDisplayPriority  SubPriority = 90;
    private const WHSFieldInputMode        InputMode   = WHSFieldInputMode::Manual;
    private const WHSFieldInputType        InputType   = WHSFieldInputType::Numeric;

    protected void initValues()
    {
        this.defaultName        = Name;
        this.defaultPriority    = Priority;
        this.defaultSubPriority = SubPriority;
        this.defaultInputMode   = InputMode;
        this.defaultInputType   = InputType;
    }
}
				
			

The extendedTypeNum property of the controls on “pages” (see below) must match exactly the above declaration. Once programmed, use the button Create default setup in Warehouse management > Setup > Mobile device > Warehouse app field names. The new field should appear in the list and can be used as a novel parameter in “Select fields to send” in a detour.  

Step 1: Screen to prompt the product number

The below 2 classes are trivial and I’ll keep them uncommented:

				
					[ProcessGuidePageBuilderName(classStr(ProcessGuideProductLabelItemIdPageBuilder))]
public class ProcessGuideProductLabelItemIdPageBuilder extends ProcessGuidePageBuilder
{
    protected final void addDataControls(ProcessGuidePage _page)
    {
        _page.addTextBox(ProcessGuideDataTypeNames::ItemId,
                         "@SYS14428",
                         extendedTypeNum(ItemId),
                         true,
                         controller.parmSessionState().parmPass().lookupStr(ProcessGuideDataTypeNames::ItemId));
    }
    protected final void addActionControls(ProcessGuidePage _page)
    {
        #ProcessGuideActionNames
        _page.addButton(step.createAction(#ActionOK), true);
        _page.addButton(step.createAction(#ActionCancelExitProcess));
    }
}

[ProcessGuideStepName(classStr(ProcessGuideProductLabelItemIdStep))]
public class ProcessGuideProductLabelItemIdStep extends ProcessGuideStep
{
    protected final ProcessGuidePageBuilderName pageBuilderName()
    {
        return classStr(ProcessGuideProductLabelItemIdPageBuilder);
    }
    protected final boolean isComplete()
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        return (pass.lookup(ProcessGuideDataTypeNames::ItemId) != "");
    }
}
				
			

Step 2: Screen to prompt the number of copies, then print

The next 2 classes are more complex.

Microsoft developers tend to design an additional step (here it would be the 3rd) which executes the core business logic, but a mobile app step comes with a UI – an additional screen looking like a confirmation. Yet every new screen is one click more for the worker to do, and we can initiate the printing right after the number of copies prompt.

It is essential to have super()  in front of the .doExecute() method, because it updates the “pass” with the latest user interaction (i.e. saves the “NumberOfLabels” in the session state).

				
					[ProcessGuidePageBuilderName(classStr(ProcessGuideProductLabelNoOfLabelsPageBuilder))]
public class ProcessGuideProductLabelNoOfLabelsPageBuilder extends ProcessGuidePageBuilder
{
    protected final void addDataControls(ProcessGuidePage _page)
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();

        if (! pass.exists(ProcessGuideDataTypeNames::NumberOfLabels))
        {
            pass.insert(ProcessGuideDataTypeNames::NumberOfLabels, 1);
        }
        _page.addTextBox(
            ProcessGuideDataTypeNames::NumberOfLabels,
            "@WAX:NumberOfLabels",
            extendedTypeNum(NumberOfLabels),
            true,
            WhsWorkExecuteDisplay::num2StrDisplay(pass.lookupNum(ProcessGuideDataTypeNames::NumberOfLabels)));

        _page.addLabel(
            ProcessGuideDataTypeNames::ItemInfo,
            InventProcessGuideInquiryItemHelper::generateItemInformation(pass.lookupStr(ProcessGuideDataTypeNames::ItemId), InventDim::findOrCreateBlank()),
            extendedTypeNum(WHSRFItemInformation));   
    }

    protected final void addActionControls(ProcessGuidePage _page)
    {
        #ProcessGuideActionNames
        _page.addButton(step.createAction(#ActionOK), true);
        _page.addButton(step.createAction(#ActionCancelExitProcess));
    }
}

				
			
				
					[ProcessGuideStepName(classStr(ProcessGuideProductLabelNoOfLabelsStep))]
public class ProcessGuideProductLabelNoOfLabelsStep extends ProcessGuideStep
{
    protected final ProcessGuidePageBuilderName pageBuilderName()
    {
        return classStr(ProcessGuideProductLabelNoOfLabelsPageBuilder);
    }

    protected void doExecute()
    {
        super(); // process the controls

        // Identify the custom product label
        WHSLabelLayoutDataSource    labelDS;
        WHSLabelLayout              labelLayout;
        select firstonly labelLayout 
            where labelLayout.LayoutType == WHSLabelLayoutType::CustomLabel
        exists join labelDS
            where labelDS.CustomLabelRootDataSourceTable == tableStr(InventTable)
               && labelDS.LabelLayoutDataSourceId == labelLayout.LabelLayoutDataSource;
        if (! labelLayout)
        {
            return; 
        }        

        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        if (this.printCustomLabel(pass.lookupStr(ProcessGuideDataTypeNames::ItemId),
                                labelLayout,
                                WHSLabelPrinterSelector::construct()
                                    .withUserId(pass.parmUserId())
                                    .withWarehouseId(pass.parmInventLocationId())
                                    .selectPrinterForPrinterStockType(""),
                                pass.lookupNum(ProcessGuideDataTypeNames::NumberOfLabels)) > 0)
        {
            this.addReprintLabelProcessCompletionMessage();
        }
        
        this.passReset();
    }

    private int printCustomLabel(ItemId _itemId, WHSLabelLayout _labelLayout, WHSPrinterName _printerName, NumberOfLabels _noOfLabels)
    {
        InventTable     record = InventTable::find(_itemId);
        int             labelCount;
                        
        // Do not use a service to avoid a disruptive info message
        using (var batchPrintingContext = WhsBatchedDocumentRoutingContext::construct())
        {
            while (labelCount < _noOfLabels)
            {
                labelCount += WHSCustomLabelPrintCommandGenerator::printLabelsForRecord(_labelLayout, _printerName, record.RecId);
            }
            batchPrintingContext.printBatchedLabels();
        }
        return labelCount;
    }

    protected void passReset()
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        pass.remove(ProcessGuideDataTypeNames::ItemId);
        pass.remove(ProcessGuideDataTypeNames::NumberOfLabels);
    }

    private void addReprintLabelProcessCompletionMessage()
    {
        ProcessGuideMessageData messageData = ProcessGuideMessageData::construct();
        messageData.message = "@WAX3181";
        messageData.level = WHSRFColorText::Success;

        navigationParametersFrom = ProcessGuideNavigationParameters::construct();
        navigationParametersFrom.messageData = messageData;
    }
}
				
			

Source code

You may download the example here: ProcessGuideProductLabel.axpp