Fixed Kanbans in Dynamics 365 SCM with Warehouse Management

Small consumable items (Kanban)
Small consumable items (Kanban)

Fixed Kanbans in Dynamics 365 SCM with Warehouse Management

In a warehouse-managed environment, supplying production resources with small quantities of material is a common requirement. One possible solution is a Kanban flow, where the operator uses one container while the second container is replenished by the warehouse team. Once the first container is empty, it is returned to the replenishment cycle, ensuring that material is available at the resource without requiring the operator to create a warehouse request manually.

In Dynamics 365 Supply Chain Management, this process can be implemented using fixed Kanban rules in combination with Warehouse Management (WHS). The destination is defined by the activity in the production flow, whereas the source location in the main warehouse can be controlled by WHS location directives.

Configuration: Fixed Kanban Replenishment with WHS

  1. Navigate to Production control > Setup > Lean production flow > Value streams and create a value stream for the material supply process. The value stream provides the organisational context for the production flow and its activities. After that create a generic logistic Production flow model.
  2. Create the production flow with 1 transfer activity: Lean production flow > Production flows, associated with the value stream. Add a Version.
  3. Under the Activities button, use Create new plan activity, where in the wizard you create the transfer activity: Activity type = Transfer. The work cells are irrelevant; mark both Update on hand on receipt and Update on hand on pick.
  4. Choose the same “From” and “To” locations, namely the location of the resource: the activity represents the destination of the material flow. The actual source location can be determined separately by the WHS location directive.
  5. Enter something into the time, finish creating the activity, but then enter the Details mode and enable Auto complete by put line! This setting allows the transfer activity to be completed automatically with the last put step on the mobile device in the warehouse.
  6. Activate the flow version.
  7. Create the fixed Kanban rule: navigate to Product information management > Lean manufacturing > Kanban rules and create a rule of type Withdrawal.
  8. The same screw will be at more locations, potentially in containers of different sizes. Every Item-Location combination requires an own Kanban rule with a specific quantity.
  9. Set Replenishment strategy = Fixed. Select the transfer activity created in the previous steps as the First plan activity. This automatically populates the Receipt location of the resource. Set Register as empty = Manual.
  10. Specify the product and the Default quantity. This quantity defines how much material is requested when an empty container triggers replenishment i.e. the size of the box.
  11. Set the Fixed kanban quantity = 2 for the two-container replenishment cycle, or 1 if the replenishment is fast enough for a one-container system.
  12. At the bottom of the screen, you may pre-create the Kanban cards with the button + Add. Each becomes a unique barcode and a unique – and different from the barcode card number – Kanban number.
  13. Configure warehouse work classes and the work template:  under Warehouse management > Setup > Work, create the work classes required for Kanban picking and putting, then a work template for the Kanban picking work order type with the following sequence:
  14. Under Warehouse management > Setup > Location directives, configure a directive for the Kanban picking work type. Use the directive query and actions to determine the source location from which the material is picked. This is where the actual warehouse source is controlled, independently of the destination defined in the production flow.
  15. No separate put-away location directive nor a Kanban put away work template is required for this scenario because the destination is given by the transfer activity.
  16. Under Warehouse management > Setup > Waves > Wave templates, configure the template used to process Kanban replenishment demand. Enable automatic wave creation and automatic release of work. Configure the remaining wave settings according to the intended processing flow.
  17. Test the process: scan a Kanban card on the Transfer Kanban board (Production control > Kanban > Kanban board for transfer jobs) to make it empty.
  18. The system creates the replenishment demand and processes the corresponding wave to generate warehouse work. The warehouse worker executes the picking and put operations, replenishing the container with the fixed quantity defined in the Kanban rule.
  19. After the put operation, the material is available at the resource location, and Auto complete by put line allows the transfer activity to be completed through that process: the Kanban resets and becomes Completed automatically, awaiting the new cycle.

The fixed Kanban card cycle

To summarise:

  1. An empty container triggers replenishment: when a container is empty, the operator scans its Kanban card on the transfer-job Kanban board in empty-container mode. This triggers the replenishment process for that card.
  2. The system creates the replenishment demand: the Kanban is activated, and a Kanban wave is generated. Wave processing creates the warehouse work required to pick the fixed quantity and deliver it to the destination associated with the production-flow activity.
  3. The warehouse replenishes the container: the warehouse worker executes the picking and put operations. The source location is determined by the WHS location directive, while the destination is defined by the production-flow activity. The quantity to be replenished comes from the fixed Kanban rule.
  4. The replenished container returns to the resource: once the warehouse work is completed, the container is available for use at the resource. The other container can continue to supply material while this replenishment takes place.
  5. The cycle repeats: when the next container becomes empty, its Kanban card triggers another replenishment cycle. The circulating cards allow the process to repeat without creating a new Kanban rule for each replenishment.

Fixed Kanban or Min/Max replenishment?

For this process, the fixed Kanban rule provides a direct connection between the production resource, the container and the replenishment quantity. The empty Kanban card is the trigger, and the Kanban rule determines the quantity to be replenished. The WHS module then generates and lets execute the warehouse work, including the determination of the source location. The machine operator is in control.

Min/Max replenishment addresses a slighty different requirement. It is a warehouse replenishment mechanism based on inventory thresholds by location (!), whereas the fixed Kanban process is driven by the consumption and replenishment cycle of a defined container. The Dynamics 365 system periodically controls the min/max levels and hopefully reacts on time, but only as well, as good the quantities in the BOM are, and as timely the workers record material consumption.

Advanced query syntax in Dynamics 365 Finance: an example with joined tables

Advanced query syntax in Dynamics 365 Finance: an example with joined tables

Microsoft documents the syntax for advanced queries in Dynamics 365 Finance here: Advanced filtering and query syntax – Microsoft Learn. The documentation contains an example of a query that uses fields from two data sources:

((AccountNum LIKE "US*") && (DirPartyTable.Name LIKE "Cont*"))

This is useful, but it does not really help you construct more complex queries when the required fields are several joins away from the primary data source. This was also the subject of a question in the Dynamics 365 Community: Seeking “Finance and Operations query syntax” examples from “Advanced filter or sort”.

In reality, the Microsoft’s example is even incorrect. When the query is entered in the Advanced filter or sort dialog, the system may not use the table name exactly as shown in the documentation. In the example above, the system-generated data source name can require a “_1” suffix, for example: DirPartyTable_1.Name rather than DirPartyTable.Name.

Let’s have a look at a more complex example, where customers should be filtered by the customer group, unless they are from specific “countries” (French overseas territories, to be exact). The customer group is available directly from the “CustTable”, but the country/region is stored in the address. The required tables are therefore: Customers → Parties → Addresses

The required joins can be created directly in the Dynamics’ Query: on the Joins tab, select + Add table join and enable Show details. Scroll down to n:1 Global address book and select it. Then add another nested join to the Global address book and select n:1 Addresses.

The resulting structure is:

CustTable

    └── n:1 Global address book

            └── n:1 Addresses

It is important to use Show details and be able to select the n:1 relationships: there are also 1:n relationships in the list, but they return an empty dataset.

The range can then reference fields from both CustTable and LogisticsPostalAddress:

((((LogisticsPostalAddress_1.CountryRegionId = "REU") || (LogisticsPostalAddress_1.CountryRegionId = "GLP")|| (LogisticsPostalAddress_1.CountryRegionId = "MTQ"))
&& (CustTable_1.CustGroup LIKE "C-EX-NOEU*"))
|| (CustTable_1.CustGroup LIKE "*DO*"))

This means:
((Country = REU, GLP or MTQ) AND Customer group = C-EX-NOEU*)
OR
Customer group contains DO

The same approach can be used for considerably more complex filters: fields from the primary table can be combined with fields from joined tables, and the &&, || and LIKE operators can be used to build the required expression.

Important notice: if you used a field in the body of an advanced query, do not use the same field in another range line of the Advanced filter or sort dialog. Unlike regular queries, these conditions will result in an AND clause, not in an OR clause as usual. In other words, if you added a line with an additional condition Customer group = “EX*”, then the query is not going to return any records at all, because a customer may not have a group like “*DO*” and “EX*” at the same time, unless there is an “EX-DO*” group.

ZUGFeRD export: common modifications

ZUGFeRD export: common modifications

Reflect header charges (markups)

The standard sales invoice in D365 only shows the total of all miscellaneous charges (markups), but not the single markup transactions. No information about header charges is disclosed – unless we are in Finland: the Finnish localisation ensures disclosure of detailed charges on invoices, which is why the invoice data provider collects markup details only when the ISO country code is “FI”. For all other countries, this logic is skipped in standard D365.

Data for the ZUGFeRD e-invoice is calculated in D365 by the same Data Provider as with the original SSRS sales invoice: SalesInvoiceDP. Without extending the DP or introducing alternative data sources, Electronic Reporting alone cannot retrieve missing charge details. The challenge and a possible no-code solution is best described in the article “ER: Add charge details to the Sales invoice report – CleverAX” written by a fellow ER expert.

An alternative is a small X++ customisation:

				
					[ExtensionOf(classStr(SalesInvoiceDPBase))]
final class SalesInvoiceDPBase_Extension
{
    #ISOCountryRegionCodes
    public void createData()
    {
        next createData();
        if (isoCountryCode != #isoFI)
        {
            this.insertMarkupSpec(custInvoiceJour);
        }
    }
}
				
			

This extension effectively enables header charge extraction for all countries; in the Lasernet application, this behaviour is activated by the feature “SalesInvoice.Report creates temporary MarkupTmpTrans_FI“. In the Docentric ecosystem, you need the above customisation plus another one.    

If no detailed charge records are available in the data model, our e-invoice falls back to displaying the total:

      <ram:SpecifiedTradeAllowanceCharge>
        <ram:ChargeIndicator>
          <udt:Indicator>true</udt:Indicator>
        </ram:ChargeIndicator>
        <ram:ActualAmount>123.99</ram:ActualAmount>
        <ram:Reason>Summe der Zuschläge</ram:Reason>
        <ram:CategoryTradeTax>
          <ram:TypeCode>VAT</ram:TypeCode>
          <ram:CategoryCode>S</ram:CategoryCode>
          <ram:RateApplicablePercent>19</ram:RateApplicablePercent>
        </ram:CategoryTradeTax>
      </ram:SpecifiedTradeAllowanceCharge>

With the above customisation in place, the XML output expands to include individual charge entries:

      <ram:SpecifiedTradeAllowanceCharge>
        <ram:ChargeIndicator>
          <udt:Indicator>true</udt:Indicator>
        </ram:ChargeIndicator>
        <ram:ActualAmount>99.99</ram:ActualAmount>
        <ram:Reason>FREIGHT</ram:Reason>
        <ram:CategoryTradeTax>
          <ram:TypeCode>VAT</ram:TypeCode>
          <ram:CategoryCode>S</ram:CategoryCode>
          <ram:RateApplicablePercent>19</ram:RateApplicablePercent>
        </ram:CategoryTradeTax>
      </ram:SpecifiedTradeAllowanceCharge>
      <ram:SpecifiedTradeAllowanceCharge>
        <ram:ChargeIndicator>
          <udt:Indicator>true</udt:Indicator>
        </ram:ChargeIndicator>
        <ram:ActualAmount>24.0</ram:ActualAmount>
        <ram:Reason>HANDLING</ram:Reason>
        <ram:CategoryTradeTax>
          <ram:TypeCode>VAT</ram:TypeCode>
          <ram:CategoryCode>S</ram:CategoryCode>
          <ram:RateApplicablePercent>19</ram:RateApplicablePercent>
        </ram:CategoryTradeTax>
      </ram:SpecifiedTradeAllowanceCharge>

Reflect line charges

The above customisation to Dynamics 365 for Finance only solves header-level charges. Even in Finland, the SalesInvoiceTmp record representing the surcharge does not have a reference to the invoice line: the line-level charges are never disclosed in standard D365.

Fortunately, the Retail Call Center (“MCR”) configuration key (it is usually active) helps: the related MCRMarkupCalculatedAmount field keeps the total of all line charges which affect the invoice amount. This data source is leveraged in our ZUGFeRD configuration to reflect the sum of all charges per line:

      <ram:SpecifiedTradeAllowanceCharge>
        <ram:ChargeIndicator>
          <udt:Indicator>true</udt:Indicator>
        </ram:ChargeIndicator>
        <ram:ActualAmount>12.3</ram:ActualAmount>
        <ram:Reason>Belastung</ram:Reason>
      </ram:SpecifiedTradeAllowanceCharge>

If a detailed list of line-level charges broken down by code/description is required, then you need an adaptation of the ZUGFeRD ER report and mapping. Refer to the the Docentric’s article “Add Charges to Sales Invoice Lines through ER” first.

If you do not have the Docentric code installed, the following X++ snippet must be deployed, plus an extension of the SalesInvoiceTmp table with the field JourTransRecId_DR, contact us for more details:

				
					[ExtensionOf(classStr(SalesInvoiceDP))]
final class SalesInvoiceDP_Extension
{
    protected void populateSalesInvoiceTmp(CustInvoiceJour _custInvoiceJour, CustInvoiceTrans _custInvoiceTrans, TaxSpec _taxSpec,
        CustPaymSchedLine _custPaymSchedLine, CustTrans _prepaymentCustTrans, TaxTrans _prepaymentTaxTrans)
    {
        next populateSalesInvoiceTmp(_custInvoiceJour,
            _custInvoiceTrans,
            _taxSpec,
            _custPaymSchedLine,
            _prepaymentCustTrans,
            _prepaymentTaxTrans);

        salesInvoiceTmp.JourTransRecId_DR = _custInvoiceTrans.RecId;
    }
}
				
			

Derive a custom mapping from the “Invoice model mapping (ZUGFeRD)“, and derive a custom format from the “ZUGFeRD Sales invoice“.

As suggested by Sanja Kolundzija, add $SalesInvoiceTmp_Lines.$CustInvoiceTransFromTableDef asTables.CustInvoiceTransTable.'findRecId()'(@.JourTransRecId_DR) to the derived Mapping,

then add a nested record list

$CustInvoiceTransFromTableDef.$MarkupTransWithFilter as FILTER(Tables.MarkupTransRecords, AND(Tables.MarkupTransRecords.TransTableId=TABLENAME2ID(“CustInvoiceTrans”),Tables.MarkupTransRecords.TransRecId=@.RecId))

The InvoiceLines.MarkupTransaction list in not bound in the Microsoft’s base mapping, you have to bind it as shown:

Additionally, you have to set the MCRMarkupCalculatedAmount element to zero to avoid doubling of line charge amounts: Charge(InvoiceLines.MCRMarkupCalculatedAmount) = @.MCRMarkupCalculatedAmount*0.0

In the derived “ZUGFeRD Sales invoice“, add a new <ram:SpecifiedTradeAllowanceCharge> at the line and bind it  toInvoiceLines.MarkupTransaction as shown below:

This is not all. In accordance with the ZUGFeRD specification, this markup amount is separated from the header level charges, and it increases the total line amount before VAT <ram:LineTotalAmount>. Dynamics 365 in contrary adds all header and line charges and prints them at the and of the invoice. To satisfy the CII scheme, the line charges must be subtracted from the total charge amount given by Dynamics.

In our ZUGFeRD configuration, a collection named $SumMCRMarkupCalculatedAmount2 is used to hold the total of all line charges across all invoice lines. This is why $SumMCRMarkupCalculatedAmount2.Collect() wraps and adds to an internal counter the amounts @.’$CUSTOM_SumOfLineMarkups’, which is a shortcut for the totals of all markup transactions per line: @.’$CUSTOM_SumOfLineMarkups’ = @.’$CUSTOM_MarkupTransGrpLines’.aggregated.SumChargeCalculatedValue

The @.'$CUSTOM_MarkupTransGrpLines' is a Group By element.

Technical debt of customised SSRS Invoices

This chapter concerns Dynamics users who utilised SSRS in the past, or intend to further use their SSRS invoices with the Dynamity Group’s SSRS-to-ZUGFeRD convertor.

In these installations, we often see the SSRS Report Data Provider (RDP) customised to a certain extent to make the printed invoice show information that the standard invoice layout did not expose conveniently. For example, the abovementioned markup/charge details was a common requirement, there the charges started appearing among the invoice lines and not in a separate section as intended by Microsoft; namely, the typical legacy approach was to inject additional records into SalesInvoiceTmp that behaved like invoice lines from the SSRS report’s perspective. This made it easy for the report designer to render the information in the same repeating line section as normal sales lines. Such customisations could also carry other display-only information that was useful on the printed invoice.

This creates a problem when the same SalesInvoiceTmp dataset is reused as the source for ZUGFeRD/Factur-X:

  • SSRS asks essentially: “What should I display on the printed invoice?”
  • ZUGFeRD/ER asks: “What are the actual commercial invoice lines and their tax/amount semantics?”

With the RDP customised, those are not necessarily the same. Consequently, a record that was perfectly useful as a display-only pseudo-line in an SSRS report can look like a malformed invoice line to CII/ZUGFeRD. It may have an amount or description but lack the quantity, item, tax code or other information expected for a genuine IncludedSupplyChainTradeLineItem.

To resolve this abiguity, the SalesInvoiceTmp data had to be analysed first, the malformed or unintended records identified, and then…

  1. either the upstream/customised D365 logic corrected,
  2. or the rogue SalesInvoiceTmp filtered out so that ZUGFeRD receives a clean dataset,
  3. or the ER binding made more tolerant to these additional records.

The options 2 and 3 are elaborated below on two real-world examples.

Case 2: remove the pseudo-lines from the lines list. In the “Invoice model mapping” there is already a filter of invoice lines to separate true invoice lines from markup records. The latter bear no quantity:

$SalesInvoiceTmp_Lines as WHERE('$SalesInvoiceTmp', '$SalesInvoiceTmp'.Qty <> 0)

This is exactly the place where you can start adding criteria to filter out other pseudo-lines.

Case 3: the developers added records to SalesInvoiceTmp representing order-level surcharges. Unlike the previous cases, we decided not to remove them from SalesInvoiceTmp. They were intended to remain visible in the electronic invoice as IncludedSupplyChainTradeLineItem lines.

The problem was that these pseudo-lines did not contain the same tax information as regular invoice lines. In particular, the expected TaxWriteCode was not available to the ER data source, resulting in an empty value being passed to the tax-category mapping.

As a solution, where no Print code/tax code was available, the tax category was derived from the first relevant tax transaction, based on the assumption that the invoice contains either zero-rated tax or the applicable standard/reduced German VAT rates, but not a mixture of zero and taxable rates.

A second adjustment was required because the surcharge records were effectively represented twice in the generated XML: once as invoice lines and again as charges. The pseudo-charge lines therefore had to be excluded from the charge calculation so that ChargeTotalAmount remained zero.

Here again, the collection $SumMCRMarkupCalculatedAmount2 was leveraged. When looping through invoice lines and encountering a line representing a charge, the charge amount was recoded to get subtracted at the end. This was achieved with the following dirty trick:
IF(@.TaxWriteCode="",
model.InvoiceBase.’$FirstTaxTrans’.’$TaxWriteCode2Category’&

LEFT(TEXT(model.InvoiceBase.’$SumMCRMarkupCalculatedAmount2′.Collect(@.LineBase.Amount)*0),0),

@.’$TaxWriteCode2Category’)

The amount of the charge has nothing to do with the VAT category, but at that tag the charge amount was evaluated, added to the collection, then reduced to nil with LEFT(TEXT(…),0) = “” .

Multiple bank accounts

 

Case 1: Bank account by currency

Methods of payment in Dynamics 365 help distinguish between a standard bank transfer and SEPA direct debit, but every Method of payment also points to a specific bank account of ours:

Subsequently, different bank accounts may be assigned to a customer by means of the payment method. The user can specify the default method of payment for the customer, and even amend it on the sales order or sales quotation if needed.

The invoice layout may use the selected method of payment to print the corresponding IBAN, BIC and bank address. This keeps the selection of the bank account within the standard payment method setup and allows the invoice to reflect the payment arrangements agreed for the individual transaction.

To make the XML and PDF aware of the custom bank, a series of changes is needed in a derived Model mapping to destination (ZUGFeRD):

  • at the $SalesInvoiceHeaderFooterTmp data source, define a calculated field  $CUSTOM_SalesTable as  FILTER(ZUGFERD.SalesTable, ZUGFERD.SalesTable.SalesId = @.SalesId). You can take it from the CustInvoiceJour, but it should work with a proforma, too; hence SalesTable.
  • at $CUSTOM_SalesTable, define  $CUSTOM_PaymModeBankTable as FIRSTORNULL(@.’>Relations’.CustPaymModeTable).’bankAccountTable()’.
  • bind the new data source to the nodes of InvoiceBase.CompanyInfo.BankAccount according to following pattern, with a fallback condition: IBAN = IF(‘$SalesInvoiceHeaderFooterTmp’.’$CUSTOM_SalesTable’.’$CUSTOM_PaymModeBankTable’.AccountNum = “”, ‘$SalesInvoiceHeaderFooterTmp’.Iban, ‘$SalesInvoiceHeaderFooterTmp’.’$CUSTOM_SalesTable’.’$CUSTOM_PaymModeBankTable’.IBAN).

As a result, the bank info from the Method of payment (if available) will be printed on and in the invoice instead of the default one.

Case 2: Free choice of accounts for credit transfer 

In addition to selecting a bank account based on the invoice currency, some businesses want to offer customers a choice of bank accounts for incoming payments. For example, an invoice may list a primary account and an alternative account, allowing the customer to choose where to make the credit transfer.

In Dynamics 365 Finance, the existing InvoiceBase.CompanyInfo.CurrencyBankAccount node in the Electronic Reporting invoice model can be mis-used to provide the bank account details. The CurrencyBankAccount  was made for currency specific bank account handling in the Eastern European localisations, where a separate company bank account can be selected for invoices issued in a currency other than the accounting currency.

We may reuse this existing node to supply additional creditor bank accounts. This allows us to generate two ram:SpecifiedTradeSettlementPaymentMeans blocks, each identifying a credit transfer and its corresponding creditor account, including IBAN, account name and BIC, without introducing a new data structure into the ER model. This requires the invoice mapping (see above) and its underlying $SalesInvoiceHeaderFooterTmp data source to support the additional account(s).

Important: according to the ZUGFeRD specification, repeating ram:SpecifiedTradeSettlementPaymentMeans for multiple creditor bank accounts is supported by the EXTENDED profile only! All repeated blocks must use the same payment means code; this mechanism is intended for multiple accounts for credit transfers, not for mixing different payment methods. Copy and paste this parent tag as a whole and set the bindings accordingly:

One important exception concerns SEPA direct debit. When ShowSepaNotification trigger is enabled in the model due to the method of payment, the primary mapping uses payment means code 59 (SEPA direct debit) instead of the credit transfer code and shows data from the SEPA DD mandate. In this case, the secondary bank accounts must be suppressed. Otherwise, the XML would present alternative creditor accounts as though they were available for the same direct debit transaction, although the customer does not choose the account to debit from a list of transfer destinations.

Therefore, only activate the alternative bank tag conditionally: Enabled = AND(NOT(model.InvoiceBase.ShowSepaNotification), AND(model.InvoiceBase.RemainAmountToBePaid<>0.0, model.InvoiceBase.CustomerBankAccount.IBAN<>””))

Avoiding conflicts with customer-specific Invoice model mappings

Dynamics 365 Finance can have multiple Electronic Reporting (ER) model mappings for the same data model, but marking more than one as the default creates ambiguity. This can happen when a customer already uses a customised Invoice model mapping for the Italian FatturaPA electronic invoicing format, or Belgian UBL. Activating the ZUGFeRD mapping as another Default can then cause errors when processing invoices.

The solution is to create a separate chain of derived ER configurations for ZUGFeRD. This leaves the existing e-invoice configurations intact, while allowing customer-specific ZUGFeRD changes without modifying Microsoft’s original configurations.

  1. Create and activate a customer-specific ER provider (Organisation administration > Electronic reporting > Configuration provider table).
  2. On Organisation administration > Workspaces > Electronic reporting, use the the 3 dots … and Set active to use the provider for all new derived configurations. 
  3. Open Organisation administration > Electronic reporting > Configurations and select the Invoice model.
  4. Create a derived model with +Create configuration. Derive CUSTOM Invoice model from the standard Invoice model, version 323. Change status  to Complete. 
  5. Create a derived model mapping. Derive CUSTOM Invoice model mapping (ZUGFeRD) from Invoice model mapping (ZUGFeRD) and select CUSTOM Invoice model as its Target model. This operation may take quite some time. Ultimately, Change status to Complete (you may need to Skip validation at configuration’s status change under User parameters).
  6. De-select Default for model mapping at ER-Consult’s Invoice model mapping (ZUGFeRD) if it was selected earlier, and restore the original default mapping.
  7. Select ZUGFeRD Sales invoice. Derive CUSTOM ZUGFeRD Sales invoice from ZUGFeRD Sales invoice and configure it to use Data model = CUSTOM Invoice model. “Complete” the version. 
  8. Re-import the Application specific parameters for the new CUSTOM ZUGFeRD Sales invoice and set their status to Completed. 
  9. Configure Print management: under AR > Setup > Forms > Form setup > Print management, select CUSTOM ZUGFeRD Sales invoice as the report format for Customer invoices.
  10. Test.

The derived configurations remain separate from the original configurations. The Default for model mapping flag does not need to be enabled on the new mapping, because it targets the dedicated CUSTOM Invoice model rather than the original Invoice model.

For the general installation and setup of ZUGFeRD, including the Electronic Reporting destinations and application-specific parameters, refer to the ER-Consult ZUGFeRD installation guide.

Extensibility of the Heavy version

To implement a custom invoice design with a corporate logo, a certain colour palette or fonts, extend the ER configuration “ZUGFeRD Sales invoice” as any other configurable business document. Derive an own configuration, then extract, amend and replace the PDF template attached to the ER configuration version.
 

Please bear in mind that PDF forms do not contain dynamic, repeatable ranges or cells (refer to Filling PDF Forms with Electronic Reporting in Dynamics 365 for Finance for more details). This is the reason why the number of invoice lines is limited by the PDF template; change the constants Paginator.#LinesMax, #LinesOnFirstPage and #LinesOnNextPages in accordance with the template to let Dynamics control the number of pages.

 

The “output actions” i.e. different transmission options are extendable; new output actions may be offered upon request at a discounted price. You may even add your own class ERFileOutputActionXXX_ZUGFeRD. If inherited from the ERFileOutputActionPersist_ZUGFeRD, it will commit the synchronous transmission, setting the SentElectronically flag at the invoice in the process. The interface ERIDocuManagementAttachable_ZUGFeRD interacts with the Archive electronic reporting destination, replacing the file to attach to the ER job record.

The method sendFile() must be implemented. It receives a memory stream with the hybrid invoice and transmits it to the final destination.

The method makeVisitor() must be implemented, too. Its result – a ERFileOutputActionXmlVisitor object – subscribes to the post-processor to extract information from the XML. By default, it receives the file name of the PDF. The input parameter of the method andVisitNode() is the name of a tag in the CII invoice (without the “ram” or “rsm” schema prefix). For example, if instantiated with return visitor.andVisitNode(“ExchangedDocument”), the Visitor object will also receive the invoice number.

				
					/// <summary>
/// Send the resulting PDF to a browser window interactively
/// </summary>
[ERFileOutputAttribute_ZUGFeRD(SRSPrintMediumType::Screen)]
class ERFileOutputActionScreen_ZUGFeRD extends ERFileOutputAction_ZUGFeRD
{
    ERFileOutputActionXmlVisitor_ZUGFeRD visitor;
    /// <summary>
    /// The screen destination does not need to extract any contact info
    /// </summary>
    /// <returns>An empty visitor class</returns>
    public ERFileOutputActionXmlVisitor_ZUGFeRD makeVisitor()
    {
        visitor = new ERFileOutputActionXmlVisitor_ZUGFeRD();
        return visitor;
    }

    /// <summary>
    /// Display  the PDF file formed by ERFileDestinationPostProcessor_ZUGFeRD on the screen 
    /// </summary>
    /// <param name = "_outputPDF">Stream with a PDF file, with a CII XML attached</param>
    /// <returns>Success or failure to display</returns>
    public boolean sendFile(System.IO.MemoryStream _outputPDF)
    {
        Browser br = new Browser();
        
        str downloadUrl = File::SendFileToTempStore(_outputPDF,
                                                    ERConstants_ZUGFeRD::ZUGFeRD_Filename,
                                                    classstr(FileUploadTemporaryStorageStrategy),
                                                    true);
        if (downloadUrl != '')
        {
            br.navigate(downloadUrl, true, false);
        }
        else
        {
            return checkFailed("@ApplicationPlatform:DownloadFailed");
        }
        return true;
    }

}