Template Components

Options for Business Documents

Template options for invoices, statements, credit notes, delivery notes, and remittances. Pin a closing block to the foot of the last page, drop empty values from a strip, print comment lines as text, leave out empty brackets, line up a wrapped card's values, and name and total each dispatch record in its own currency.

These options come from the shipped Business Central templates, and any template can use them. Each one is opt-in: a template without it renders as before.

OptionWhere it goesWhat it does
pinToPageBottomA section's propsDraws the document's last block at the foot of the page.
segmentSeparatorA text node's propsLeaves the empty parts out of a strip of several values.
commentLineKey and commentLineTypeKeyA table's columnManifestPrints a comment line as its text, with no zeros or empty cells.
omitEmptyBracketsA table, hierarchical_table, or card's propsLeaves out a bracket whose tokens have no value, such as () after AMOUNT.
equalizeTitles in section columnsA layout_group's propsLines up a card wrapped in a section with the cards beside it.
recordLabeltemplateSettings.distributionNames each dispatch record for the approver.
batchSummary forEach and currencyFieldtemplateSettings.distribution.batchSummaryTotals a dispatch per currency.

The browser preview does not pin or equalize

The Designer's browser preview applies segmentSeparator, omitEmptyBrackets, comment lines, and hideUnlessData. It does not pin a block to the page foot (pinToPageBottom) or line up titles (equalizeTitles): both happen only in the server render. Check them in a rendered PDF.


pinToPageBottom: the last block at the foot of the last page

What it does. A kept-together section is drawn at the foot of the page it lands on. When it fits under the content already on that page, it stays there and the page's spare room goes above it. When it does not fit, it moves whole to a new page, to the foot of that page. It is never split.

Use it for a document's closing block that belongs at the bottom of the last page, such as an account statement's period line, dashed rule, and remittance slip. Do not use it for a block that should simply follow the content, such as payment methods or notes: keep those together with keepTogether: "prefer" on a transparent section instead.

Property. "pinToPageBottom": true in the props of a section, together with keepTogether (true or "prefer"). The section must be the template's last top-level section: a direct child of the root, followed by nothing but the header and the footer. It may be transparent.

{
  "id": "node_remittance_group",
  "type": "section",
  "props": { "title": "", "transparent": true, "keepTogether": "prefer", "pinToPageBottom": true },
  "children": [
    { "id": "node_slip_rule", "type": "spacer_separator", "props": { "lineStyle": "dashed", "spaceAbove": 8, "spaceBelow": 8 } },
    {
      "id": "node_remittance_slip",
      "type": "section",
      "props": { "title": "REMITTANCE", "keepTogether": true },
      "children": [
        { "id": "node_slip_text", "type": "text", "props": { "text": "Account {{accountNumber}}  •  Amount due {{balanceDue}}" } }
      ]
    }
  ]
}

The shipped account-statement-standard template ends this way.

Accessibility. Only the block's position changes. With Section 508 tagging on, the tag tree, roles, and reading order are the same as without the pin, and the pin adds no element and no artifact. A transparent section adds no element of its own.

Validation. pinToPageBottom anywhere but the last top-level section is a template validation error, TEMPLATE_PIN_TO_PAGE_BOTTOM_MISPLACED: the Designer's check reports it, and template validation refuses the template with 400 and a configuration report naming the code. A render is not refused for it. "pinToPageBottom": false is no pin and is not checked. See Error Responses and Codes.

Limits.

  • Without keepTogether, nothing is pinned: the section flows where it is.
  • Anything after a pinned block starts on a new page, which is why only the last top-level section may carry it.
  • A "prefer" block taller than a page is laid out across pages from where it is, and is not pinned. A keepTogether: true block taller than a page fails the render, as any keepTogether: true block does; use "prefer".
  • A statement with one remittance slip per currency keeps every slip in the one pinned group, so with about 9 currencies the group is taller than a page and flows instead.
  • The browser preview does not pin: it shows the block where the content ends.

segmentSeparator: a strip of values drops the empty ones

What it does. The text is split on the separator. A part whose {{tokens}} all have no value is left out with its separator, and when no part is left, the line is left out. No value means missing, null, text of spaces only, or an empty list; 0 and false are values. A part with no token, such as Applied, always stays.

With "Status: {{status}} • Ref: {{invoiceNumber}}" and no status, the line prints Ref: 103033, not Status: • Ref: 103033. Use it on every reference strip of several values. For a line of one value, use hideUnlessData on its path instead.

Property. "segmentSeparator" in the props of a text node, set to the exact separator the text uses. It is matched as written, spaces included.

{
  "id": "node_ops_strip_text",
  "type": "text",
  "props": {
    "text": "Status: {{status}}  •  Ref: {{invoiceNumber}}",
    "align": "center",
    "fontSize": 7,
    "segmentSeparator": "  •  "
  }
}

Accessibility. The line is the same one paragraph, with the shorter text. A line with no part left is not drawn and has no tag.

Limits.

  • text nodes only. A card field, a header title or caption, and a footer are not split.
  • Inside a _repeat, the tokens are read against the entry. A part that reads a top-level key through repeatInheritsData counts as empty and is dropped.
  • A pipe is read past: in {{periodStart|date}}, the path before the pipe decides.

commentLineKey and commentLineTypeKey: comment lines print their text

What it does. A line table decides, line by line, whether a line is a comment. A comment line prints as one row across the table holding only its text values, each its own paragraph, with no zeros, no empty cells, and no row shade.

  1. Type first. When the manifest names commentLineTypeKey and the line has a value there, "comment" (any case, spaces trimmed) makes it a comment line, whatever its item number or amounts. Any other type, such as "Item" or "Resource", makes it an item line, even with no number and zeros.
  2. Otherwise, with no type or a blank one (Business Central's blank line type), the line is a comment when it has no value at commentLineKey, no amount other than zero in any number or currency column, and some text.
  3. A line that carries a nested list of its own is never a comment line, so nothing under it is lost.

Use it on a document's line table where comment lines, a description with no item and no amounts, arrive among the item lines. Do not use it on a table whose blank lines must still show their zeros.

Property. In the line table's columnManifest: "commentLineTypeKey", the key of the line type, and "commentLineKey", the key of the item number column.

"columnManifest": {
  "maxCols": 6,
  "commentLineKey": "no",
  "commentLineTypeKey": "type",
  "columns": [
    { "key": "no", "label": "ITEM NO.", "displayOrder": 1, "primary": true, "align": "left", "format": "text", "width": "*" },
    { "key": "qty", "label": "QTY", "displayOrder": 2, "primary": true, "align": "center", "format": "number", "width": 40 },
    { "key": "amount", "label": "AMOUNT ({{currency}})", "displayOrder": 3, "primary": true, "align": "right", "format": "currency", "width": 85 },
    { "key": "description", "label": "", "displayOrder": 4, "primary": false, "align": "left", "format": "text", "layout": { "span": "full", "showTitle": false } }
  ],
  "overflowStrategy": { "mode": "additionalRow", "columnsPerRow": 1, "indent": 6, "columnGap": 0, "borderStyle": "none", "fontSizeAdjustment": 0 }
}

The shipped invoice, credit note, and delivery note set both keys; the delivery note's item column is sku.

Accessibility. A comment line is its own TR holding one TD that spans the table's columns (ColSpan), with a P for each text value. It is never a cell of the line above, and never a row of empty or zero cells. Item lines are tagged as before.

Limits.

  • The table must go through the column manifest renderer: at least one column with an explicit width, an overflow column (primary: false), a conditional style, or borderStyle: "minimal".
  • A typed comment line's amounts are not printed. Business Central's comment lines carry none.
  • A comment row leaves out the number and currency columns and the commentLineKey column. Any other column with a value, such as text or a date, prints as its own paragraph.

omitEmptyBrackets: no empty brackets in a label

What it does. In the node's column and field labels, a bracketed part whose {{tokens}} all have no value is left out, with the space before it. "AMOUNT ({{currency}})" prints AMOUNT when the request sends no currency, and AMOUNT (USD) when it does.

Use it on every table or card whose labels carry a token in brackets, usually the currency.

Property. "omitEmptyBrackets": true in the props of the table, hierarchical_table, or card whose labels hold the brackets. It covers columnManifest.columns[].label, columns[].label, and fields[].label.

{
  "id": "node_totals_card",
  "type": "card",
  "props": {
    "title": "INVOICE TOTALS",
    "showHeader": true,
    "labelPosition": "left",
    "labelWidth": 145,
    "valueAlign": "right",
    "emphasizeLastRow": true,
    "omitEmptyBrackets": true,
    "fields": [
      { "key": "subtotal", "label": "Subtotal", "value": "{{subtotal}}", "format": "currency" },
      { "key": "totalAmountDue", "label": "TOTAL AMOUNT DUE ({{currency}})", "value": "{{totalAmountDue}}", "format": "currency" }
    ]
  }
}

Accessibility. The label's text is shorter. Its role, a TH or a card label, and the reading order do not change.

Limits.

  • A bracket goes only when every token in it is empty: "({{a}} / {{b}})" stays when either has a value. A bracket of plain text, such as "(excl. tax)", always stays.
  • Titles, text nodes, and header and footer text are not changed.
  • Nested brackets are not supported: only a bracket with no bracket inside it is read.
  • Inside a _repeat, the tokens are read against the entry.

equalizeTitles in section columns: the first card's title counts

What it does. In a row with equalizeTitles, a section column's title is its title, else its first visible text line, else its first card's title. So a card wrapped in a section lines up with the cards beside it.

Use it when one card in a row of peer cards must be able to drop out of the row. A card in a layout_group takes no visibleWhen, so wrap it in a section and give the section the visibleWhen. A column hidden by visibleWhen gives its width to the others. Do not rely on it for a section that has its own title or a heading text line: those count first, so the card's title is not the one lined up.

Property. "equalizeTitles": true on the layout_group, as for any row of peer columns. The section column needs no prop. Put the visibleWhen in the section's expressions.

{
  "id": "node_notes_contact_group",
  "type": "layout_group",
  "props": {
    "equalizeTitles": true,
    "columns": [{ "width": 44 }, { "width": "star" }, { "width": "star" }],
    "columnGap": 12
  },
  "children": [
    {
      "id": "node_payment_info_col",
      "type": "section",
      "props": { "title": "", "showHeader": false, "borderWidth": 0, "padding": 0 },
      "expressions": { "visibleWhen": "paymentInfo.bankName || paymentInfo.accountName || paymentInfo.accountNo" },
      "children": [
        { "id": "node_payment_info_card", "type": "card", "props": { "title": "PAYMENT INFORMATION", "showHeader": true, "fields": [{ "key": "bank", "label": "Bank", "value": "{{paymentInfo.bankName}}" }] } }
      ]
    },
    { "id": "node_notes_card", "type": "card", "props": { "title": "IMPORTANT NOTES", "showHeader": true, "fields": [{ "key": "notes", "label": "", "value": "{{notes}}" }] } },
    { "id": "node_contact_card", "type": "card", "props": { "title": "CONTACT US", "showHeader": true, "fields": [{ "key": "phone", "label": "Phone", "value": "{{company.phone}}" }] } }
  ]
}

This is the shipped vend-remit-standard template's bottom row: without bank details the first column is left out, and the other two share the row.

Accessibility. Unchanged: the row has the same tag tree with and without equalizeTitles. Each card's title is still its heading, then its table, and the wrapper section adds no element.

Limits.

  • Only the first visible card in the section counts.
  • The browser preview does not line up titles: the second layout pass runs in the server render only.

recordLabel: how a dispatch record is named

What it does. On the Dispatch intake receipt, the Details cell of a record with no issue reads the template's recordLabel, filled in from that record, for example 20000 · Fabrikam, Inc. | $4,410.30. Without it, the cell lists the record's validationFields as key: value pairs. A record with issues lists its issues either way. In Dispatch Review, each record with an issue is headed by its label too, after its record number.

  • Tokens read the record, such as {{vendor.name}}.
  • A pipe formats the value with the request's settings: |currency, |number, or |date. A token with no pipe prints the value as sent.
  • Money is in the record's own currency when recordLabelCurrency names where it is (a path holding a three-letter code), else in the request's currency. The locale is always the request's.
  • A part between " · " or " | " whose tokens all have no value is left out with its separator, as segmentSeparator leaves one out.

Use it on any template that dispatches, so an approver reading the receipt recognizes each record: who it is for and how much. It names the record and checks nothing: the fields every record must have stay in validationFields.

Property. "recordLabel" and, when each record carries its own currency, "recordLabelCurrency", in templateSettings.distribution.

"distribution": {
  "enabled": true,
  "validationFields": ["remittance.number", "vendor.name"],
  "recordLabel": "{{vendor.accountNo}} · {{vendor.name}} | {{remittance.totalPayment|currency}}",
  "recordLabelCurrency": "vendor.currency"
}

This is from the shipped vend-remit-standard template. account-statement-standard sets "recordLabel": "{{accountNumber}} · {{customer.companyName}} | Balance due {{balanceDue|currency}}", with recordLabelForEach (see below).

Accessibility. The documents do not change. The label is the text of the receipt's Details cell, which is tagged as before.

Limits.

  • One amount per label, unless the template names recordLabelForEach.
  • An email address in the label is masked, on the receipt and in Dispatch Review, as in every Details cell.
  • The label holds an amount, so it is never put in the approval email. See Totals stay in the tenant.
  • A dispatch stored before the template set recordLabel, or a template without it, is shown by its validationFields.

Totals and record labels per currency

What it does. A dispatch whose records are in different currencies gets its total per currency, never added across currency codes, and each record's label can list one amount per currency.

  • batchSummary.currencyField is where a record's currency code is: a path holding a three-letter code. The figure is grouped by that code. A record with no code counts in the request's currency.
  • batchSummary.forEach is a list in each record whose entries each carry the figure and their own currency, such as a statement's currencies[]. While the list has entries, the record's own figure at its root is not counted, so a first currency that Business Central also copies to the root is not counted twice. A record without the list counts at its root.
  • With more than one currency, the total lists each currency's figure joined with " · ", such as US$200.00 · $210.00, each in its own currency and the request's locale. The dispatch record keeps them in byCurrency. With one currency, nothing changes: the figure and its text are the ones a dispatch always had.
  • The numeric check on the summary field follows forEach: each entry must hold a number, or the record gets the warning NON_NUMERIC_SUMMARY_FIELD.
  • recordLabelForEach, with recordLabelCurrency read on each entry, fills each |currency token of recordLabel once per entry, in the entry's currency, joined with " · ". A record without the list is labelled as before, in the request's currency.

Where the totals show. On the intake receipt's dispatch total, and in Dispatch Review and on the dispatch's record in SharePoint, one line per currency. They are never in the approval email. See Totals stay in the tenant.

Use these keys on a template whose dispatches can hold more than one currency, such as the account statement (each customer's currencies[]) or the vendor remittance (each vendor's currency). A template whose records are all in the request's currency needs none of them.

Property. In templateSettings.distribution:

"batchSummary": {
  "field": "balanceDue",
  "operation": "sum",
  "format": "currency",
  "label": "Batch total",
  "forEach": "currencies",
  "currencyField": "currency"
},
"recordLabel": "{{accountNumber}} · {{customer.companyName}} | Balance due {{balanceDue|currency}}",
"recordLabelForEach": "currencies",
"recordLabelCurrency": "currency"

This is the shipped account-statement-standard template. vend-remit-standard sets "currencyField": "vendor.currency" in its batchSummary, with no forEach, and its recordLabelCurrency is vendor.currency.

With the request's locale en-CA, a statement dispatch with one customer in US dollars and one in Canadian dollars shows the dispatch total US$200.00 · $210.00. A customer billed in both is labelled 50000 · Contoso Ltd. | Balance due US$130.50 · $210.00.

Accessibility. The documents do not change. The receipt's dispatch total is the same text line, longer.

Limits.

  • The approval email carries no total and no amount.
  • A list of summaries groups each one on its own.
  • Business Central 1.0.0.3 does not yet send statements in more than one currency: see Customer Statements and Currencies.

  • Card: equalizeTitles, and the title each column type contributes
  • Section: keepTogether and pinToPageBottom
  • Table: the column manifest
  • REFRACT Dispatch: the dispatch summary, record labels, and approval

On this page