We’re switching from Quickbooks to Manager.io but we need to continue to print checks for payments.
With Quickbooks we used pre-printed voucher checks.
The top section is the check itself.
The middle section is an information voucher with a perforation that folds behind and is sent with the check.
The bottom section has the same information voucher but is torn off at the perforation and is kept for our records.
The following theme html code works with the Quickbooks checks we’ve been using.
We’re posting it here in case it’s useful to others.
<!-- based on code from a post from laltomare on:
https://forum.manager.io/t/printing-cheques-from-manager/47445/6
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
@page {
size: 8.5in 11in;
margin: 0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 8.5in;
height: 11in;
font-family: 'Arial', 'Times New Roman', serif;
font-size: 12px;
position: relative;
margin: 0;
padding: 0;
background: white;
}
/* Hide any inherited Manager.io template elements */
header, #title, #business-logo, #recipient-info, #fields, #business-info,
#description, #table-headers, #table-rows, #custom-fields, #footers, #status, #qrcode {
display: none !important;
visibility: hidden !important;
}
.check-section {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3.5in;
}
.date-line {
position: absolute;
top: 0.77in;
right: 0.55in;
display: flex;
align-items: center;
}
.date-value {
// padding: 2px 8px;
min-width: 120px;
text-align: right;
font-weight: bold;
}
.amount-line {
position: absolute;
top: 1.30in;
left: 7.00in;
right: 0.40in;
text-align: left;
font-weight: bold;
font-size: 14px;
flex-shrink: 0;
display: flex;
}
.amount-dotted-line {
flex: 1;
border-top: 2px dotted #000;
margin-top: 7px;
margin-left: 5px;
min-width: 20px;
}
.payee-line {
position: absolute;
top: 1.33in;
left: 1.03in;
right: 1.80in;
display: flex;
align-items: flex-end;
}
.payee-text {
padding-right: 8px;
white-space: nowrap;
}
.payee-dotted-line {
flex: 1;
border-top: 2px dotted #000;
margin-bottom: 7px;
min-width: 20px;
}
.amount-words-line {
position: absolute;
top: 1.68in;
left: 0.50in;
right: 0.90in;
display: flex;
align-items: flex-end;
}
.amount-words-text {
padding-right: 8px;
white-space: nowrap;
}
.amount-words-dotted-line {
flex: 1;
border-top: 2px dotted #000;
margin-bottom: 7px;
min-width: 20px;
}
.recipient-addr-line {
position: absolute;
top: 2.05in;
left: 0.47in;
right: 0.90in;
display: flex;
align-items: baseline;
padding-bottom: 4px;
}
.recipient-addr-text {
padding-right: 8px;
}
.memo-line {
position: absolute;
top: 2.75in;
left: 0.74in;
right: 3.60in;
display: flex;
}
.memo-value {
display: inline-block;
min-width: 250px;
padding: 2px 8px;
}
.perforation {
position: absolute;
left: 15px;
right: 15px;
height: 0;
// border-top: 3px dashed #999;
}
.voucher-section {
position: absolute;
left: 0.25in;
right: 0.25in;
// background: #fafafa;
}
.voucher-1 {
top: 3.5in;
height: 3.50in;
// border-top: 1px solid #000;
// border-bottom: 1px solid #000;
}
.voucher-2 {
bottom: 0.48in;
height: 3.50in;
// border-top: 1px solid #000;
}
.voucher-content {
padding: 5px;
}
.voucher-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 5px;
margin-bottom: 5px;
}
.voucher-field {
display: flex;
}
.voucher-field label {
font-size: 9px;
color: #666;
text-transform: uppercase;
padding: 2px 2px;
// margin-bottom: 3px;
font-weight: bold;
}
.voucher-field .value {
border-bottom: 1px solid #ccc;
padding: 2px 2px;
font-family: 'Courier New', monospace;
font-size: 11px;
// min-height: 20px;
background: white;
}
.voucher-table {
width: 100%;
border-collapse: collapse;
font-size: 10px;
margin-bottom: 10px;
}
.voucher-table th,
.voucher-table td {
border: 1px solid #000;
padding: 2px 2px;
text-align: left;
}
.voucher-table th {
background: #e0e0e0;
font-weight: bold;
text-transform: uppercase;
font-size: 9px;
}
</style>
</head>
<body>
<!-- CHECK SECTION -->
<div class="check-section">
<!-- Date -->
<div class="date-line">
<span class="date-value" id="check-date"></span>
</div>
<div class="amount-line">
<span id="amount-numeric"></span>
<span class="amount-dotted-line"></span>
</div>
<!-- Payee Line -->
<div class="payee-line">
<span class="payee-text" id="payee-name"></span>
<span class="payee-dotted-line"></span>
</div>
<!-- Amount in Words -->
<div class="amount-words-line">
<span class="amount-words-text" id="amount-words"></span>
<span class="amount-words-dotted-line"></span>
</div>
<!-- Memo -->
<div class="memo-line">
<span class="memo-value" id="memo-text"></span>
</div>
<!-- Address -->
<div class="recipient-addr-line">
<span class="recipient-addr-text" id="recipient-addr"></span>
</div>
</div>
<div class="perforation" style="top: 3.5in;"></div>
<!-- VOUCHER 1 -->
<div class="voucher-section voucher-1">
<div class="voucher-content">
<div class="voucher-grid">
<div class="voucher-field">
<label>Date:</label>
<div class="value" id="voucher1-date"></div>
</div>
<div class="voucher-field">
<label>Check #:</label>
<div class="value" id="voucher1-number"></div>
</div>
<div class="voucher-field">
<label>Pay To:</label>
<div class="value" id="voucher1-payee"></div>
</div>
<div class="voucher-field">
<label>Amount:</label>
<div class="value" id="voucher1-amount"></div>
</div>
<div class="voucher-field" style="grid-column: 1 / -1;">
<label>Memo:</label>
<div class="value" id="voucher1-memo"></div>
</div>
</div>
<table class="voucher-table">
<thead>
<tr id="voucher1-table-headers"></tr>
</thead>
<tbody id="voucher1-rows">
<!-- Rows will be inserted here -->
</tbody>
</table>
</div>
</div>
<div class="perforation" style="bottom: 3.98in;"></div>
<!-- VOUCHER 2 -->
<div class="voucher-section voucher-2">
<div class="voucher-content">
<div class="voucher-grid">
<div class="voucher-field">
<label>Date:</label>
<div class="value" id="voucher2-date"></div>
</div>
<div class="voucher-field">
<label>Check #:</label>
<div class="value" id="voucher2-number"></div>
</div>
<div class="voucher-field">
<label>Pay To:</label>
<div class="value" id="voucher2-payee"></div>
</div>
<div class="voucher-field">
<label>Amount"</label>
<div class="value" id="voucher2-amount"></div>
</div>
<div class="voucher-field" style="grid-column: 1 / -1;">
<label>Memo:</label>
<div class="value" id="voucher2-memo"></div>
</div>
</div>
<table class="voucher-table">
<thead>
<tr id="voucher2-table-headers"></tr>
</thead>
<tbody id="voucher2-rows">
<!-- Rows will be inserted here -->
</tbody>
</table>
</div>
</div>
<script>
function sendResize() {
window.parent.postMessage({
type: "resize",
width: document.documentElement.scrollWidth + 1,
height: document.documentElement.scrollHeight + 1
}, "*");
}
window.addEventListener("message", (event) => {
if (event.source !== window.parent) return;
if (event.data.type !== 'context-response') return;
const data = event.data.body;
// Extract data using Manager.io's postMessage API
const recipient = data.recipient || {};
const fields = data.fields || [];
const totals = data.table?.totals || [];
const rows = data.table?.rows || [];
// Find date field
const dateField = fields.find(f =>
f.label && f.label.toLowerCase().includes('date')
);
const dateValue = dateField?.text || '';
// Find check number field
const numberField = fields.find(f =>
f.label && (f.label.toLowerCase().includes('number') ||
f.label.toLowerCase().includes('reference') ||
f.label.toLowerCase().includes('check'))
);
const numberValue = numberField?.text || '';
// Find amount from totals
const amountTotal = totals.find(t => t.key === 'Total') || totals[totals.length - 1];
const amountValue = amountTotal?.text || '';
// Populate CHECK section
document.getElementById('check-date').textContent = dateValue;
document.getElementById('payee-name').textContent = recipient.name || '';
// Remove $ from amount value if present, since $ is already in HTML
// let cleanAmount = amountValue.replace(/^\$/, '').replace(/^USD/, '').replace(/^\,/, '').trim();
let commaAmount = amountValue.replace('$', '');
let cleanAmount = commaAmount.replace(',', '');
document.getElementById('amount-numeric').textContent = commaAmount;
// recipient address
document.getElementById("recipient-addr").innerHTML = `${recipient.address ? recipient.address.replace(/\n/g, "<br>") : ""}`;
// document.getElementById("recipient-name").textContent = recipient.name || '';
// document.getElementById("recipient-addr").textContent = recipient.address || '';
document.getElementById('memo-text').textContent = data.description || '';
// Amount in words - convert numeric amount to words
document.getElementById('amount-words').textContent = numberToWords(cleanAmount);
// VOUCHER 1
// document.getElementById('voucher1-checknum').textContent = numberValue ? 'Check #' + numberValue : '';
document.getElementById('voucher1-date').textContent = dateValue;
document.getElementById('voucher1-number').textContent = numberValue;
document.getElementById('voucher1-payee').textContent = recipient.name || '';
document.getElementById('voucher1-amount').textContent = amountValue;
document.getElementById('voucher1-memo').textContent = data.description || '';
// TABLE HEADERS
// Build column headers dynamically based on data.table.columns
{
const headersRow = document.getElementById("voucher1-table-headers");
headersRow.innerHTML = "";
(data.table.columns || []).forEach(col => {
const th = document.createElement("th");
th.innerHTML = col.label; // Column header text
th.style.textAlign = col.align; // left, center, or right
// Column width options:
if (col.minWidth) {
// Minimum width column (typically for numbers)
th.style.whiteSpace = 'nowrap';
th.style.width = '1px';
}
else if (col.nowrap) {
// No-wrap column with fixed width
th.style.whiteSpace = 'nowrap';
th.style.width = '80px';
}
headersRow.appendChild(th);
});
// LINE ITEMS
// Populate table with line items (products, services, etc.)
const rowsBody = document.getElementById("voucher1-rows");
rowsBody.innerHTML = "";
(data.table.rows || []).forEach(row => {
const tr = document.createElement("tr");
tr.className = 'row'; // Apply row styling
// Create cells for each column
row.cells.forEach((cell, i) => {
var col = data.table.columns[i]; // Get column definition
const td = document.createElement("td");
// Convert newlines to HTML line breaks
td.innerHTML = (cell.text || "").split("\n").join("<br />");
// Apply column alignment
td.style.textAlign = col.align;
// Apply column width settings
if (col.minWidth) {
td.style.whiteSpace = 'nowrap';
td.style.width = '1px';
}
else if (col.nowrap) {
td.style.whiteSpace = 'nowrap';
td.style.width = '80px';
}
tr.appendChild(td);
});
rowsBody.appendChild(tr);
});
// MARK LAST ROW
// Add special styling to the last line item row
const vrows = rowsBody.querySelectorAll('tr.row');
if (vrows.length > 0) {
const lastRow = vrows[vrows.length - 1];
lastRow.classList.add('last-row'); // Adds bottom border and extra padding
}
// TOTALS SECTION
// Display subtotal, taxes, discounts, and grand total
(data.table.totals || []).forEach(total => {
const tr = document.createElement("tr");
// tr.className = 'total';
// Label cell (e.g., "Subtotal:", "Tax:", "Total:")
const tdLabel = document.createElement("td");
tdLabel.innerHTML = total.label;
tdLabel.style.textAlign = 'end';
tdLabel.colSpan = data.table.columns.length - 1; // Span all columns except last
// Value cell (the amount)
const tdValue = document.createElement("td");
tdValue.innerHTML = total.text; // Formatted amount
tdValue.style.textAlign = 'end';
tdValue.dataset.value = total.number; // Raw numeric value for calculations
// Bold formatting for important totals
if (total.emphasis) {
tdLabel.style.fontWeight = 'bold';
tdValue.style.fontWeight = 'bold';
}
tr.appendChild(tdLabel);
tr.appendChild(tdValue);
rowsBody.appendChild(tr);
});
}
// VOUCHER 2
// document.getElementById('voucher2-checknum').textContent = numberValue ? 'Check #' + numberValue : '';
document.getElementById('voucher2-date').textContent = dateValue;
document.getElementById('voucher2-number').textContent = numberValue;
document.getElementById('voucher2-payee').textContent = recipient.name || '';
document.getElementById('voucher2-amount').textContent = amountValue;
document.getElementById('voucher2-memo').textContent = data.description || '';
// TABLE HEADERS
// Build column headers dynamically based on data.table.columns
{
const headersRow = document.getElementById("voucher2-table-headers");
headersRow.innerHTML = "";
(data.table.columns || []).forEach(col => {
const th = document.createElement("th");
th.innerHTML = col.label; // Column header text
th.style.textAlign = col.align; // left, center, or right
// Column width options:
if (col.minWidth) {
// Minimum width column (typically for numbers)
th.style.whiteSpace = 'nowrap';
th.style.width = '1px';
}
else if (col.nowrap) {
// No-wrap column with fixed width
th.style.whiteSpace = 'nowrap';
th.style.width = '80px';
}
headersRow.appendChild(th);
});
// LINE ITEMS
// Populate table with line items (products, services, etc.)
const rowsBody = document.getElementById("voucher2-rows");
rowsBody.innerHTML = "";
(data.table.rows || []).forEach(row => {
const tr = document.createElement("tr");
tr.className = 'row'; // Apply row styling
// Create cells for each column
row.cells.forEach((cell, i) => {
var col = data.table.columns[i]; // Get column definition
const td = document.createElement("td");
// Convert newlines to HTML line breaks
td.innerHTML = (cell.text || "").split("\n").join("<br />");
// Apply column alignment
td.style.textAlign = col.align;
// Apply column width settings
if (col.minWidth) {
td.style.whiteSpace = 'nowrap';
td.style.width = '1px';
}
else if (col.nowrap) {
td.style.whiteSpace = 'nowrap';
td.style.width = '80px';
}
tr.appendChild(td);
});
rowsBody.appendChild(tr);
});
// MARK LAST ROW
// Add special styling to the last line item row
const vrows = rowsBody.querySelectorAll('tr.row');
if (vrows.length > 0) {
const lastRow = vrows[vrows.length - 1];
lastRow.classList.add('last-row'); // Adds bottom border and extra padding
}
// TOTALS SECTION
// Display subtotal, taxes, discounts, and grand total
(data.table.totals || []).forEach(total => {
const tr = document.createElement("tr");
// tr.className = 'total';
// Label cell (e.g., "Subtotal:", "Tax:", "Total:")
const tdLabel = document.createElement("td");
tdLabel.innerHTML = total.label;
tdLabel.style.textAlign = 'end';
tdLabel.colSpan = data.table.columns.length - 1; // Span all columns except last
// Value cell (the amount)
const tdValue = document.createElement("td");
tdValue.innerHTML = total.text; // Formatted amount
tdValue.style.textAlign = 'end';
tdValue.dataset.value = total.number; // Raw numeric value for calculations
// Bold formatting for important totals
if (total.emphasis) {
tdLabel.style.fontWeight = 'bold';
tdValue.style.fontWeight = 'bold';
}
tr.appendChild(tdLabel);
tr.appendChild(tdValue);
rowsBody.appendChild(tr);
});
}
sendResize();
}, false);
window.addEventListener("load", () => {
window.parent.postMessage({ type: "context-request" }, "*");
});
// Number to words converter function
function numberToWords(amount) {
// Parse amount string like "76.46"
// const match = amount.match(/^(\d+\.?\d*)$/);
// if (!match) return '!match' + match + amount; // Return as-is if not a number
const parts = amount.split('.');
const dollars = parseInt(parts[0]) || 0;
const cents = parts[1] ? parseInt(parts[1]) : 0;
const hundreds = dollars%1000;
const thousands = dollars/1000;
// return 'parts[0]=' + parts[0] + 'parts[1]=' + parts[1] + 'parts[2]=' + parts[2] + ' dollars=' + dollars + 'cents=' + cents;
// Convert dollars to words
const ones = ['', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine',
'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen',
'Seventeen', 'Eighteen', 'Nineteen'];
const tens = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety'];
function convertHundreds(num)
{
if (num === 0)
return '';
let words = '';
if (num >= 100)
{
// words += 'A' + num +' ';
words += ones[Math.floor(num / 100)] + ' Hundred ';
num %= 100;
}
if (num >= 20)
{
// words += 'B' + num +' ';
words += tens[Math.floor(num / 10)] + ' ';
num %= 10;
}
if (num > 0)
{
// words += 'C' + num +' ';
words += ones[num] + ' ';
}
return words.trim();
}
let result = '';
if (dollars >=1000)
{
result += convertHundreds(Math.trunc(dollars/1000)) + ' Thousand '
}
result += convertHundreds(dollars%1000);
// Add cents
result += ' and ' + cents.toString().padStart(2, '0') + '/100';
return result;
}
</script>
</body>
</html>