Escape
HomeTemplatesPurchase order

Purchase order template for JavaScript

A purchase order is an invoice read from the other side of the trade. Same parties, swapped. Same line items, same totals. The obligation runs the other way. That symmetry is worth noticing. It’s what a declarative reporting engine actually buys you: you don’t wire up a purchase order generator and then an invoice generator. You write documents in one vocabulary, and a purchase order is one of the things you can say in it.

The template

Compare this to the invoice definition. You will see the same document doing different work — the same header splits, the same table shape, the same totals block. What changed is who appears where, and what the footer commits to:

purchase-order.report.json
{
  "data": "$.order.lines[*]",
  "aggregates": {
    "subtotal": "sum:[email protected] * @.unitPrice",
    "vat": "sum:[email protected] * @.unitPrice * @.vatRate / 100"
  },
  "style": { "family": "sans", "size": 10 },
  "header": [
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "Purchase order No. {{ $.input.order.number }}", "width": 60,
          "style": { "family": "serif", "size": 19, "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": [{ "value": "Raised " }, { "value": "{{ $.input.order.raised }}" }],
          "style": { "size": 8, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.order.reference }}", "width": 60,
          "style": { "size": 8, "color": "#989a97" } },
        { "type": "text", "value": [{ "value": "Required by " }, { "value": "{{ $.input.order.requiredBy }}" }],
          "style": { "size": 8, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceBefore": 34 },
      "slots": [
        { "type": "text", "value": "Ordered by", "width": 50,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Supplier",
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.order.buyer.name }}", "width": 50,
          "style": { "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": "{{ $.input.order.supplier.name }}",
          "style": { "bold": true, "color": "#1e1f22" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.order.buyer.street }}", "width": 50,
          "style": { "color": "#5e6062" } },
        { "type": "text", "value": "{{ $.input.order.supplier.street }}",
          "style": { "color": "#5e6062" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.order.buyer.city }}", "width": 50,
          "style": { "color": "#5e6062" } },
        { "type": "text", "value": "{{ $.input.order.supplier.city }}",
          "style": { "color": "#5e6062" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.order.buyer.country }}", "width": 50,
          "style": { "color": "#5e6062" } },
        { "type": "text", "value": "{{ $.input.order.supplier.country }}",
          "style": { "color": "#5e6062" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceAfter": 40 },
      "slots": [
        { "type": "text", "value": "VAT {{ $.input.order.buyer.vatId }}", "width": 50,
          "style": { "size": 8, "color": "#989a97" } },
        { "type": "text", "value": "VAT {{ $.input.order.supplier.vatId }}",
          "style": { "size": 8, "color": "#989a97" } }
      ]
    },
    {
      "type": "split",
      "visible": "=$.input.order.shipTo != null",
      "slots": [
        { "type": "text", "value": "Deliver to", "width": 22,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "{{ $.input.order.shipTo.name }} · {{ $.input.order.shipTo.street }} · {{ $.input.order.shipTo.city }}",
          "style": { "size": 8, "color": "#5e6062" } }
      ]
    }
  ],
  "detail": {
    "header": {
      "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97",
        "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
    },
    "row": { "style": { "color": "#5e6062" } },
    "columns": [
      { "header": "Code", "value": "{{ @.code }}", "width": 13,
        "style": { "family": "mono", "size": 8, "color": "#989a97" } },
      { "header": "Item", "value": "{{ @.item }}", "width": 37 },
      { "header": { "value": "Qty", "style": { "align": "right" } }, "value": "{{ @.qty }} {{ @.unit }}", "width": 12,
        "style": { "align": "right" } },
      { "header": { "value": "Unit price", "style": { "align": "right" } }, "value": "{{ @.unitPrice }}", "width": 17,
        "style": { "align": "right", "format": "currency" } },
      { "header": { "value": "Amount", "style": { "align": "right" } }, "value": "{{ round(@.qty * @.unitPrice, 2) }}", "width": 21,
        "style": { "align": "right", "format": "currency" } }
    ],
    "total": {
      "style": { "color": "#5e6062", "align": "right", "paddingTop": 0, "paddingBottom": 0 },
      "rows": [
        {
          "cells": [
            { "value": "Subtotal", "span": 4 },
            { "value": "{{ round($.subtotal, 2) }}", "style": { "format": "currency" } }
          ]
        },
        {
          "cells": [
            { "value": "VAT 21%", "span": 4 },
            { "value": "{{ round($.vat, 2) }}", "style": { "format": "currency" } }
          ]
        },
        {
          "style": { "bold": true, "color": "#1e1f22", "paddingTop": 5,
            "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
          "cells": [
            { "value": "Order total", "span": 4 },
            { "value": "{{ round($.subtotal + $.vat, 2) }}", "style": { "format": "currency" } }
          ]
        }
      ]
    }
  },
  "footer": [
    {
      "type": "split",
      "style": { "spaceBefore": 10 },
      "slots": [
        { "type": "text", "value": "Delivery", "width": 22,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "{{ $.input.order.delivery }}",
          "style": { "size": 8, "color": "#5e6062" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "Terms", "width": 22,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "{{ $.input.order.terms }}",
          "style": { "size": 8, "color": "#5e6062" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "Authorised by", "width": 22,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "{{ $.input.order.authorisedBy.name }}, {{ $.input.order.authorisedBy.role }}",
          "style": { "size": 8, "color": "#5e6062" } }
      ]
    }
  ]
}

One thing here the invoice doesn’t need. A purchase order often ships somewhere other than the address that raised it, and just as often doesn’t — so the “Deliver to” line declares "visible": "=$.input.order.shipTo != null". When your data carries a delivery address the line renders. When it doesn’t, the line drops out instead of rendering blank. That’s a property of the definition, not a branch in your code.

The totals are detail.total, a block of rows in the table itself. Each figure lands in the column it belongs to instead of in widths you match by eye. What the rows share is the block’s own style. A row states only what differs.

A definition declares amounts rather than formatting them. The cell interpolates the bare number and "format": "currency" says how to show it, from the locale and currency you hand quario(). The order total wraps its sum in round(…, 2). The sample’s subtotal plus VAT computes to 3003.2200000000003, which a page presents as €3,003.22 but a workbook would store as is. Dates work the same way. The Raised and Required by lines use styled runs — a value written as a list — so the date stands in its own run. The line’s "format": "date" then presents it as 18 Aug 2026 under en-IE and leaves the label alone. The template registers no function for it.

The data

This is the sample the preview below renders. $.order.lines[*] selects the rows the table repeats over, and $.input reads everything else — both parties, the dates, the terms:

purchase-order.data.json
{
  "order": {
    "number": "PO-2026-0117",
    "raised": "2026-08-18",
    "requiredBy": "2026-09-08",
    "reference": "Job 2026-1042",
    "buyer": {
      "name": "Quire Press Ltd.",
      "street": "Binnenkant 42",
      "city": "1011 BM Amsterdam",
      "country": "Netherlands",
      "vatId": "NL8123.45.678.B01"
    },
    "supplier": {
      "name": "Papierfabriek Deckle B.V.",
      "street": "Hemkade 18",
      "city": "1506 PR Zaandam",
      "country": "Netherlands",
      "vatId": "NL8099.12.345.B01"
    },
    "shipTo": {
      "name": "Quire Press — bindery",
      "street": "Kattenburgergracht 5",
      "city": "1018 KK Amsterdam"
    },
    "lines": [
      {
        "code": "MU-090-SW",
        "item": "Munken Pure 90gsm, 70×100",
        "qty": 24,
        "unit": "reams",
        "unitPrice": 62.5,
        "vatRate": 21
      },
      {
        "code": "BC-QT-NAT",
        "item": "Bookcloth, natural quarter",
        "qty": 40,
        "unit": "m",
        "unitPrice": 11.8,
        "vatRate": 21
      },
      {
        "code": "GB-2MM",
        "item": "Greyboard 2mm, 100×140",
        "qty": 300,
        "unit": "sheets",
        "unitPrice": 1.35,
        "vatRate": 21
      },
      {
        "code": "HB-IND",
        "item": "Headbands, indigo",
        "qty": 250,
        "unit": "m",
        "unitPrice": 0.42,
        "vatRate": 21
      }
    ],
    "delivery": "Delivered duty paid, bindery entrance, weekdays 08:00–16:00",
    "terms": "Payment within 30 days of delivery",
    "authorisedBy": { "name": "R. Kaats", "role": "Production" }
  }
}

Delete the shipTo object and re-render: the “Deliver to” line disappears and nothing else moves.

What it renders

This isn’t a screenshot. quario compiles the definition and data above, and @quario/viewer renders them in your browser, right here. The PDF button hands you the same order as real @quario/pdf bytes, one A4 page, no browser rendering involved. The DOCX button beside it hands the same definition to @quario/docx, where Word paginates and resolves the faces itself:

Rendering live with @quario/viewerA4

Use it

shell
npm install quario @quario/html
app.js
import { quario } from 'quario'
import { html } from '@quario/html'
import purchaseOrder from './purchase-order.report.json' with { type: 'json' }

const q = quario({ locale: 'en-IE', currency: 'EUR' })   // what `format` presents through
const report = q.report(purchaseOrder)

const data = await getOrder(id)                      // your data
const page = await report.render(html(), data)       // ready to display or print

Show page in the browser. Or render the same compiled report straight to a PDF to send to the supplier and keep for the file — no browser involved:

pdf.js
import { writeFileSync } from 'node:fs'
import { pdf } from '@quario/pdf'

const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)

writeFileSync('purchase-order.pdf', bytes)

See JSON to PDF for both PDF routes side by side.

Make it yours

The obvious edits are the approval ones, and they’re all data. A second signature line for orders over a threshold is another footer item with a visible expression on it. A cost-centre or budget code is a field in your data and a slot in the header. A delivery window per line is one more column. And if your suppliers span VAT rates, the single VAT 21% line becomes one conditional aggregate per rate — the receipt does exactly that for a mixed basket. None of that’s layout work — it’s editing a JSON file that your reviewers can read in a pull request. The same definition renders identically in Node, the browser, and at the edge. It renders without eval, so supplier-supplied item names never become code. Security →

FAQ

Can I generate the purchase order as a PDF?

Yes, and it’s usually the format you want here. A purchase order is a document you send to a supplier and keep for the file. @quario/pdf renders this definition straight to bytes you can attach and archive, with no browser anywhere in the path. That matters when a background job raises the order rather than someone sitting at a screen. Full example →

Can I export the purchase order to Excel?

Yes, and for an order it’s often the more useful half. A supplier who has to price or pick a parts list wants the codes and quantities in cells, not in a PDF. @quario/xlsx renders this same definition to a workbook, with nothing to change. The amount columns declare "format": "currency" rather than baking a formatted string into the cell. Each one arrives as a real number the supplier can re-price and total. Full example →

Can I use this purchase order template commercially?

The definition is yours — the report definitions, data, and output you create with quario belong to you. quario is free to evaluate with no time limit. Committing real spend to real suppliers is past evaluation, so that needs a seat for each developer working with it. Deployment is then unlimited and royalty-free. Pricing →

Can one definition serve both my purchase orders and my invoices?

It can, but it’s usually the wrong trade. The two documents share a shape, not a purpose. A purchase order commits you to spend and carries an authorisation. An invoice demands payment and carries a bank line. Keeping them as two definitions means each says one thing, and the cost of that’s a file, not a subsystem.

Where are the other templates?

Browse the template gallery → — invoices, receipts, quotes, and more.

Fold your data into this order.

npm install quario
Getting started
© 2026 quario · KvK 61815977