ZUGFeRD 2.2 import for Dynamics 365 for Finance

ZUGFeRD 2.2 import for Dynamics 365 for Finance

ZUGFeRD (Zentraler User Guide des Forums elektronische Rechnung Deutschland) is an e-invoicing format combining a human-readable PDF/A-3 and an embedded XML file using the CII (Cross-Industry Invoice) syntax. The CII syntax may also be used in the xRechnung, which is a pure XML version of the German e-Invoice. From 2025, the ability to receive and process electronic invoices becomes mandatory for B2B transactions in Germany.

Microsoft’s current solution in Dynamics 365 for Finance provides support specifically for the UBL (Universal Business Language) format of the xRechnung: Import vendor electronic invoices – Finance | Dynamics 365 | Microsoft Learn. It enables the import of vendor invoices received in this format, mapping them as pending vendor invoices in D365. The invoice may be uploaded directly into the system or placed in a SharePoint folder for unattended processing.

We are happy to announce the release of the CII (Cross-Industry Invoice) dialect of the xRechnung / ZUGFeRD electronic invoice. This solution is implemented via Electronic Reporting (ER) configurations, which means it does not require any customisations and can be easily installed across all active versions of D365. It sits on top of the standard Invoice model v280, Vendor Invoice Mapping to Destination v280.46, side by side with the configuration Vendor Invoice Import v280.7.

The functionality aligns with the standard Microsoft UBL import, utilizing the key data elements for processing and ensuring PO matching:

  • Header: Invoice ID, Invoice date, Due date, Vendor ID, Currency code, and a header-level note.
  • Lines: Line number, Item ID, Quantity, Sales unit of measure, Sales price, Discount (percentage discounts are converted to absolute values as the XML schema doesn’t support percentages), Price unit, Name, Description, and Line amount (excluding VAT).

The solution supports the ZUGFeRD 2.2/Factur-X 1.0 profile EN 16931, also known as the “COMFORT” profile (being also a subset of the EXTENDED profile), which meets all tax law requirements and must be accepted by customers unless explicitly denied.

Price

The solution is offered at a fixed fee per end customer / D365 tenant:
Version Price Deliverables Notes
Light €890 The Vendor invoice import CII Electronic Reporting configuration This option does not require any X++ customisations or 3rd party software and can be installed and used right away.
Light+ €1200 Same as the Light version but with 5 hours of prepaid support and installation  

Your can acquire the solution on the official Microsoft AppSource marketplace: ZUGFeRD CII import (microsoft.com)

 

Setup and use

 
  1. Follow the steps outlined in the Microsoft’s guidance Import vendor electronic invoices – Finance | Dynamics 365 | Microsoft Learn:
  2. Import the same three configurations from Microsoft (Prerequisites);
  3. Import the configuration Vendor invoice import CII.xml provided to you;
  4. Open Accounts payable > Setup > Accounts payable parameters; on the Electronic documents tab, select the imported Vendor invoice import CII format.
  5. To run either one of the formats – UBL or CII – by choice, you may download and import a complimentary X++ extension with an additional parameter CII vendor invoice and an own menu item underneath the standard one: VendImportInvoice_CII.axpp. In the batch execution mode this does not matter, because each of the formats becomes its own Electronic source with a set of SharePoint folders etc.
  6. Prepare the master data: make sure the vendors are stored with proper VAT numbers in Dynamics 365 for Finance.
  7. Make sure that the vendor’s external item ID mapping exists. Any Buyer Assigned ID (our item number) potentially sent by the vendor is neglected by the Microsoft’s mapping, and the conversion to the internal Item ID is always handled via the external item IDs. Under these conditions, the import works as expected and the PO lines may be matched.
  8. Make sure that the units of measure are properly mapped to the UNECE Recommendation No. 20 list called “Codes for Units of Measure Used in International Trade”. E.q. the UNECE unit H87 must be mapped to either PCS or EA or STK.
  9. Perform an interactive test upload by using the function Accounts payable > Periodic tasks > Import vendor invoices (or Import CII vendor invoices if you installed and configured the above extension from step 5). Beware: contrary to the UBL format, the CII (“COMFORT”) invoice does not include any line-by-line reference to the purchase order lines. Just the PO number is sent in the header. We assume that the numeration of the invoice lines mostly follows the PO lines numeration. If not, the standard mapping will fall back to the matching by the line purchase price.
  10. Configure the batch mode with Electronic sources as outlined in the guidance at step 1.

Useful links

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;
    }
}