このドキュメントはまだご利用の言語に翻訳されていないため、英語で表示しています。製品の画面は翻訳済みです。

← All recipes

Invoice

Build an invoice whose line items come from your data and whose total is computed from it. This is where you learn repeating tables, currency formatting, summing, and running headers with page numbers.

このテンプレートを取り込む

What this teaches

  • node-label
  • node-table
  • table-repeat
  • expr-format-currency
  • expr-array
  • page-header
  • page-footer
  • style-font-cjk
  • style-align
  • api-sync
  • api-template-id
  • node-autoheight

Building it in the editor

  1. Create a template and open the editor. Leave the paper at A4 portrait with the default margins.
  2. Add a label and write {{ data.title }} in its text. Raise the size to 26 and the weight to 700 so it reads as a title. Choose Noto Sans CJK KR — this is a place where non-Latin text can appear.
  3. Add two more labels: the recipient ({{ data.customer }}) and the document number (No. {{ data.no }}). Push the number to the right with box alignment.
  4. Add a table. At the top of the table properties choose the repeating mode and set the array path to data.items.
  5. Give it three columns. Set heading and cell template to Item/{{ item.name }}, Qty/{{ item.qty }}, Amount/{{ format(item.amount, "currency", "USD", "en-US") }}. Right-align the numeric columns — numbers only read well when their digits line up.
  6. Below the table put a total label with {{ format(sum(data.items, "amount"), "currency", "USD", "en-US") }}. Computing the total instead of accepting it as data means it can never disagree with the line items.
  7. When the table grows the total must move with it. Everything below the table sits in the flow region, and the orange guide in the editor shows exactly how far that region reaches.
  8. In the Data tab replace the JSON with real values and check the preview. The generated sample puts strings where numbers go, so currency shows as NaN — put real numbers in to see digits and overflow properly.
  9. Save. Saving creates a version, and the template can then be called from the API by template_id.

Building it with the schema and API

  1. A document is a flat array of nodes. The title, recipient and number are labels; the line items are a single table.
  2. A table becomes a repeating table purely by having props.bind. When it is set, rows and cells are unused and columns defines the shape.
  3. Setting h to 0 means automatic height. On a repeating table it becomes the reserved height, and anything below is pushed down when the table grows past it.
  4. page.header and page.footer repeat on every page. Only the footer can use [[page]] and [[pages]].
  5. For a saved template send template_id instead of template. Sending both is rejected — we cannot tell which one you meant.

The finished document

The JSON below is exactly the document those two routes produce. Our tests validate it and check that it renders without warnings, so you can paste it and use it as is.

EXAMPLE
{
  "v": 1,
  "unit": "px",
  "page": {
    "size": "a4",
    "orientation": "portrait",
    "margin": {
      "t": 48,
      "r": 48,
      "b": 56,
      "l": 48
    },
    "header": "{{ data.company }}",
    "footer": "{{ data.company }} · [[page]] / [[pages]]"
  },
  "fonts": [],
  "styles": [
    {
      "id": "s_h1",
      "fontFamily": "Liberation Sans",
      "fontSize": 26,
      "fontWeight": 700,
      "italic": false,
      "color": "#111111",
      "bg": "transparent",
      "align": "left",
      "valign": "top",
      "lineHeight": 1.3,
      "border": "none",
      "radius": 0,
      "opacity": 1
    },
    {
      "id": "s_body",
      "fontFamily": "Liberation Sans",
      "fontSize": 13,
      "fontWeight": 400,
      "italic": false,
      "color": "#333333",
      "bg": "transparent",
      "align": "left",
      "valign": "top",
      "lineHeight": 1.5,
      "border": "none",
      "radius": 0,
      "opacity": 1
    },
    {
      "id": "s_right",
      "fontFamily": "Liberation Sans",
      "fontSize": 13,
      "fontWeight": 400,
      "italic": false,
      "color": "#333333",
      "bg": "transparent",
      "align": "right",
      "valign": "top",
      "lineHeight": 1.5,
      "border": "none",
      "radius": 0,
      "opacity": 1
    },
    {
      "id": "s_tbl",
      "fontFamily": "Liberation Sans",
      "fontSize": 12,
      "fontWeight": 400,
      "italic": false,
      "color": "#222222",
      "bg": "transparent",
      "align": "left",
      "valign": "middle",
      "lineHeight": 1.4,
      "border": "none",
      "radius": 0,
      "opacity": 1
    },
    {
      "id": "s_total",
      "fontFamily": "Liberation Sans",
      "fontSize": 18,
      "fontWeight": 700,
      "italic": false,
      "color": "#111111",
      "bg": "transparent",
      "align": "right",
      "valign": "top",
      "lineHeight": 1.5,
      "border": "none",
      "radius": 0,
      "opacity": 1
    }
  ],
  "nodes": [
    {
      "id": "n_title",
      "parent": "",
      "order": 0,
      "type": "label",
      "x": 0,
      "y": 0,
      "w": 380,
      "h": 34,
      "show": "",
      "styleId": "s_h1",
      "props": {
        "text": "{{ data.title }}",
        "src": "",
        "fit": "",
        "thickness": 0,
        "stroke": "",
        "fill": "",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 0,
        "padX": 0,
        "align": "",
        "valign": "",
        "wrap": "",
        "bind": "",
        "headerHeight": 0,
        "rowHeight": 0,
        "max": 0,
        "columns": [],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Title"
    },
    {
      "id": "n_to",
      "parent": "",
      "order": 1,
      "type": "label",
      "x": 0,
      "y": 44,
      "w": 380,
      "h": 0,
      "show": "",
      "styleId": "s_body",
      "props": {
        "text": "To {{ data.customer }}",
        "src": "",
        "fit": "",
        "thickness": 0,
        "stroke": "",
        "fill": "",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 0,
        "padX": 0,
        "align": "",
        "valign": "",
        "wrap": "",
        "bind": "",
        "headerHeight": 0,
        "rowHeight": 0,
        "max": 0,
        "columns": [],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Bill to"
    },
    {
      "id": "n_no",
      "parent": "",
      "order": 2,
      "type": "label",
      "x": 420,
      "y": 6,
      "w": 277,
      "h": 20,
      "show": "",
      "styleId": "s_right",
      "props": {
        "text": "No. {{ data.no }}",
        "src": "",
        "fit": "",
        "thickness": 0,
        "stroke": "",
        "fill": "",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 0,
        "padX": 0,
        "align": "",
        "valign": "",
        "wrap": "",
        "bind": "",
        "headerHeight": 0,
        "rowHeight": 0,
        "max": 0,
        "columns": [],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Number"
    },
    {
      "id": "n_items",
      "parent": "",
      "order": 3,
      "type": "table",
      "x": 0,
      "y": 96,
      "w": 697,
      "h": 160,
      "show": "",
      "styleId": "s_tbl",
      "props": {
        "text": "",
        "src": "",
        "fit": "",
        "thickness": 1,
        "stroke": "#c9ced6",
        "fill": "#f3f5f7",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 6,
        "padX": 8,
        "align": "",
        "valign": "middle",
        "wrap": "",
        "bind": "data.items",
        "headerHeight": 30,
        "rowHeight": 26,
        "max": 200,
        "columns": [
          {
            "header": "Item",
            "cell": "{{ item.name }}",
            "width": 397,
            "align": "left"
          },
          {
            "header": "Qty",
            "cell": "{{ item.qty }}",
            "width": 100,
            "align": "right"
          },
          {
            "header": "Amount",
            "cell": "{{ format(item.amount, \"currency\", \"USD\", \"en-US\") }}",
            "width": 200,
            "align": "right"
          }
        ],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Line items"
    },
    {
      "id": "n_total",
      "parent": "",
      "order": 4,
      "type": "label",
      "x": 0,
      "y": 268,
      "w": 697,
      "h": 26,
      "show": "",
      "styleId": "s_total",
      "props": {
        "text": "Total {{ format(sum(data.items, \"amount\"), \"currency\", \"USD\", \"en-US\") }}",
        "src": "",
        "fit": "",
        "thickness": 0,
        "stroke": "",
        "fill": "",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 0,
        "padX": 0,
        "align": "",
        "valign": "",
        "wrap": "",
        "bind": "",
        "headerHeight": 0,
        "rowHeight": 0,
        "max": 0,
        "columns": [],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Total"
    }
  ]
}

The request body. Put the document above into template and the data below into data.

REQUEST
{
  "template": "(the document above)",
  "data": {
    "company": "brewmypdf",
    "title": "Invoice",
    "customer": "Hanbit Corp.",
    "no": "INV-2026-0042",
    "items": [
      {
        "name": "Brand design",
        "qty": 1,
        "amount": 2400
      },
      {
        "name": "Website build",
        "qty": 1,
        "amount": 1500
      },
      {
        "name": "Maintenance (3 months)",
        "qty": 3,
        "amount": 280
      }
    ]
  }
}