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
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
- Add the gem to your Gemfile — see Installation.
- Download quote_sections.template.json and quote_sections.data.json.
- 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"
}
}
}
More in Business documents