soli-pdf

Quote by trade section

Line items grouped into numbered sections, each with its own subtotal, and a dashed acceptance box.

Grouping is a repeat over sections with a data-bound table inside it: the inner table binds to the lines array of the current section, so no flattening is needed.

Nested repeat Subtotals Dashed box 1 page
Page 1
Page 1 of 1
render_quote_sections.rb ruby
require 'json'
require 'soli/pdf'

template = JSON.parse(File.read('quote_sections.template.json'))
data     = JSON.parse(File.read('quote_sections.data.json'))

pdf = Soli::PDF.render(
  template: template,
  data: data,
  title: 'Quote A-2025-0143'
)
File.binwrite('quote_sections.pdf', pdf)

Run it yourself

  1. Add the gem to your Gemfile — see Installation.
  2. Download quote_sections.template.json and quote_sections.data.json.
  3. Run ruby render_quote_sections.rb.
quote_sections.template.json json
{
  "fonts": ["titillium"],
  "options": { "header_height": 0 },
  "footer": [
    { "type": "paragraph", "value": "${company.name} · ${company.email} · Quote ${quote.number} · Page #PAGE# of #TOTAL_PAGE#", "options": { "alignment": "center", "fontSize": 7.5, "color": "6B7B85" } }
  ],
  "content": [
    {
      "type": "table",
      "header_columns": [
        {
          "content": [
            { "type": "text", "value": "${company.name}", "fontSize": 18, "fontWeight": "bold" },
            { "type": "text", "value": "${company.tagline}", "fontSize": 9 }
          ],
          "width": 300, "alignment": "left",
          "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" }
        },
        {
          "content": [
            { "type": "text", "value": "QUOTE", "fontSize": 24, "fontWeight": "bold" },
            { "type": "text", "value": "${quote.number}", "fontSize": 10 }
          ],
          "width": 181, "alignment": "right",
          "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" }
        }
      ],
      "rows": [],
      "options": { "header": { "fillColor": "0E4A5C", "textColor": "FFFFFF" }, "padding_x": 18, "padding_y": 16 }
    },
    {
      "type": "table",
      "header_columns": [
        {
          "text": "${quote.project}   ·   Prepared for ${customer.name}, ${customer.city}   ·   Valid until ${quote.valid_until}",
          "width": 481, "fontSize": 9, "fontWeight": "bold", "alignment": "center",
          "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" }
        }
      ],
      "rows": [],
      "options": { "header": { "fillColor": "EEF2F6", "textColor": "0E4A5C" }, "padding_x": 12, "padding_y": 8 }
    },
    { "type": "move", "y": 22 },

    {
      "type": "table",
      "header_columns": [
        { "text": "DESCRIPTION", "width": 271, "fontSize": 8, "fontWeight": "bold", "alignment": "left",   "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" }, "borderColor": "0E4A5C" },
        { "text": "QTY",         "width": 50,  "fontSize": 8, "fontWeight": "bold", "alignment": "center", "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" }, "borderColor": "0E4A5C" },
        { "text": "UNIT",        "width": 70,  "fontSize": 8, "fontWeight": "bold", "alignment": "right",  "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" }, "borderColor": "0E4A5C" },
        { "text": "AMOUNT",      "width": 90,  "fontSize": 8, "fontWeight": "bold", "alignment": "right",  "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" }, "borderColor": "0E4A5C" }
      ],
      "rows": [],
      "options": { "padding_x": 6, "padding_y": 6 }
    },

    { "type": "repeat", "data": "sections", "content": [
      { "type": "move", "y": 8 },
      {
        "type": "table",
        "header_columns": [
          { "text": "${title}", "width": 481, "fontSize": 9, "fontWeight": "bold", "alignment": "left",
            "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } }
        ],
        "rows": [],
        "options": { "header": { "fillColor": "EEF2F6", "textColor": "0E4A5C" }, "padding_x": 8, "padding_y": 5 }
      },
      {
        "type": "table",
        "data": "lines",
        "header_columns": [],
        "rows": [
          [
            { "text": "${name}",   "width": 271, "fontSize": 9.5, "alignment": "left",   "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } },
            { "text": "${qty}",    "width": 50,  "fontSize": 9.5, "alignment": "center", "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } },
            { "text": "${unit}",   "width": 70,  "fontSize": 9.5, "alignment": "right",  "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } },
            { "text": "${amount}", "width": 90,  "fontSize": 9.5, "alignment": "right",  "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } }
          ]
        ],
        "options": { "padding_x": 6, "padding_y": 3 }
      },
      {
        "type": "table",
        "header_columns": [],
        "rows": [
          [
            { "text": "${subtotal_label}", "width": 391, "fontSize": 9, "fontWeight": "bold", "alignment": "right", "borderSides": { "top": "true", "bottom": "false", "left": "false", "right": "false" }, "borderColor": "C7D5DC" },
            { "text": "${subtotal}",       "width": 90,  "fontSize": 9, "fontWeight": "bold", "alignment": "right", "borderSides": { "top": "true", "bottom": "false", "left": "false", "right": "false" }, "borderColor": "C7D5DC" }
          ]
        ],
        "options": { "padding_x": 6, "padding_y": 5 }
      }
    ]},

    { "type": "move", "y": 18 },
    {
      "type": "table",
      "header_columns": [],
      "rows": [
        [
          { "text": "", "width": 271, "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } },
          { "text": "Net total", "width": 130, "fontSize": 10, "alignment": "right", "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } },
          { "text": "${totals.net}", "width": 80, "fontSize": 10, "alignment": "right", "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } }
        ],
        [
          { "text": "", "width": 271, "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } },
          { "text": "VAT ${quote.vat_rate}", "width": 130, "fontSize": 10, "alignment": "right", "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } },
          { "text": "${totals.vat}", "width": 80, "fontSize": 10, "alignment": "right", "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } }
        ],
        [
          { "text": "", "width": 271, "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } },
          { "text": "Quoted total", "width": 130, "fontSize": 14, "fontWeight": "bold", "alignment": "right", "borderSides": { "top": "true", "bottom": "false", "left": "false", "right": "false" }, "borderColor": "0E4A5C" },
          { "text": "${totals.gross}", "width": 80, "fontSize": 14, "fontWeight": "bold", "alignment": "right", "borderSides": { "top": "true", "bottom": "false", "left": "false", "right": "false" }, "borderColor": "0E4A5C" }
        ]
      ],
      "options": { "padding_x": 6, "padding_y": 6 }
    },

    { "type": "move", "y": 22 },
    { "type": "box", "border": "0E4A5C", "borderWidth": 1, "dash": [4, 3], "padding": { "top": 13, "right": 16, "bottom": 14, "left": 16 }, "gap": 18, "content": [
      { "type": "paragraph", "value": "APPROVED FOR WORK", "options": { "fontSize": 9, "fontWeight": "bold", "color": "0E4A5C" } },
      { "type": "move", "y": 3 },
      { "type": "paragraph", "value": "Return one signed copy to accept this quote. Work is scheduled on receipt of the signed copy and the ${quote.deposit} deposit.", "options": { "fontSize": 8.5, "color": "51636D" } },
      { "type": "move", "y": 26 },
      {
        "type": "table",
        "header_columns": [],
        "rows": [
          [
            { "text": "Date", "width": 150, "fontSize": 8, "alignment": "left", "borderSides": { "top": "false", "bottom": "true", "left": "false", "right": "false" }, "borderColor": "0E4A5C" },
            { "text": "", "width": 24, "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" } },
            { "text": "Signature", "width": 275, "fontSize": 8, "alignment": "left", "borderSides": { "top": "false", "bottom": "true", "left": "false", "right": "false" }, "borderColor": "0E4A5C" }
          ]
        ],
        "options": { "padding_x": 0, "padding_y": 9 }
      }
    ]},

    { "type": "paragraph", "value": "${quote.note}", "options": { "fontSize": 8, "color": "6B7B85" } }
  ]
}
quote_sections.data.json json
{
  "data": {
    "company": {
      "name": "Steinberg Ausbau GmbH",
      "tagline": "Interior fit-out and building services",
      "email": "bau@steinberg-ausbau.de"
    },
    "customer": {
      "name": "Kaffeehaus Neumann",
      "city": "Berlin"
    },
    "quote": {
      "number": "A-2025-0143",
      "project": "Café fit-out, Oranienstraße 92",
      "valid_until": "31 Jan 2026",
      "vat_rate": "19%",
      "deposit": "30%",
      "note": "Prices assume the premises are vacant and accessible on weekdays between 07:00 and 17:00. Asbestos survey, if required, is quoted separately."
    },
    "sections": [
      {
        "title": "01   STRUCTURAL WORK",
        "lines": [
          { "name": "Demolition of partition wall, incl. disposal", "qty": "1", "unit": "850.00", "amount": "850.00" },
          { "name": "Floor levelling and screed", "qty": "24 m²", "unit": "23.00", "amount": "552.00" }
        ],
        "subtotal_label": "Subtotal, structural work",
        "subtotal": "1,402.00"
      },
      {
        "title": "02   PLUMBING",
        "lines": [
          { "name": "Hot and cold supply lines to counter", "qty": "1", "unit": "620.00", "amount": "620.00" },
          { "name": "Grease trap installation", "qty": "1", "unit": "480.00", "amount": "480.00" }
        ],
        "subtotal_label": "Subtotal, plumbing",
        "subtotal": "1,100.00"
      },
      {
        "title": "03   ELECTRICAL",
        "lines": [
          { "name": "Three-phase supply to counter", "qty": "1", "unit": "940.00", "amount": "940.00" },
          { "name": "Lighting circuit and fixtures", "qty": "18", "unit": "62.00", "amount": "1,116.00" }
        ],
        "subtotal_label": "Subtotal, electrical",
        "subtotal": "2,056.00"
      }
    ],
    "totals": {
      "net": "€4,558.00",
      "vat": "€866.02",
      "gross": "€5,424.02"
    }
  }
}