September 8, 2023
by Matt Raines

Looking back to 2013

« 2011 | 2012 | 2013 | 2014 »

Danny Alexander and Lorely Burt visiting the Prater Raines stand at conference

FIM Capital (then IOMA FIM), the last of the Assanka customers, chose us to take over support of their fully featured investment management system which we continue to develop to this day. We added additional support around busy quarter end dates and a fully featured helpdesk for problem reporting, initially by importing from Assanka’s in-house system into Trac and later a fresh import into hosted Gitlab, which we now use.

With the CIPR we focused on new server hardware and improvements to their members’ only Continuous Professional Development portal, Ladder, including rebuilding the activity search using faceted Elasticsearch, and migrating their proprietary blog aggregator site The Conversation to WordPress.

Prater Raines website in 2013We moved our accounts software from desktop Sage, which we were running on a shared PC over a VPN, to FreeAgent this year. I already had some experience with the site at PHP London and it’s a really good match for our business, neither too simple nor too complicated. The API means we can automate invoicing from various sources including Nominet and Gandi renewals and our own website platforms. We send automated reminders for overdue payments by email and PDFs which are printed and posted by Viapost (now cloudPOST). All of this reduced our outstanding debt massively and I enjoyed the process so much I gave my only ever talk at PHP London about it, to the puzzled and angry faces of my peers expecting a talk about some technical topic, not sending post and generating invoices.

Alliance Party website in 2013But the most exciting thing by far was that Tim got a machine that you put a cheque in, and it goes WHIRRR, and it reads the account number and sort code off the cheque. We still use this to this day to reconcile payments from customers more rapidly and print stubs to attach to our paying in book. Once we’ve seen one cheque from a client, we know who the next cheque is from. You’d be amazed how many people don’t tell you who they are. Doesn’t help with bank transfers where the customers use our name as the reference, as if the main thing we needed to know to reconcile a payment was who we are. With several hundred payments a month all for the same amount, this was becoming quite a time-consuming process that needed streamlining.

Part of an SRB cover sheetManually tagging imported documents from Stone Rowe Brewer’s document storage system took an increasing amount of support time, so we worked to improve the legibility of the cover sheets. Covers are printed and attached to documents before they are scanned and OCRed allowing them to be associated with the correct entries in the system when imported. We switched to the OCR-B font for key information, printed ID numbers at various horizontal and vertical positions in the page to reduce problems with striping from the printers, added checksums and a cleaner barcode, and improved the import logic. It was an interesting project and it continues to work well to this day.

A rebrand for The Alliance Party of Northern Ireland and the addition of online donations to our Liberal Democrat platform completed a busy year’s work.

Tools of the trade

Location, location, location

The year in tech

Sample code


$version = "4";

require_once $_SERVER["DOCUMENT_ROOT"]."/lib/inc/global";

// Check permissions, error if none

// Get parameters from URL and validate
$params = Common::geturlparams();
preg_match("/^(.*)\.pdf$/", $params[0], $matches);
$docid = $matches[1];
if (!is_numeric($docid)) {
  trigger_error("Expected numeric input: none supplied", E_USER_ERROR);
if (strlen($docid) > 12) {
    "Cannot generate coversheets for documents with ID numbers longer than 12 digits as "
    . "the ean-13 barcode format doesn't support such long numbers", E_USER_ERROR);

// Get from cache if available
$coversheetdocdir = docdir($docid, true);
if (!is_dir($coversheetdocdir)) {
$coversheetfilename = $docid."_coversheet4.pdf";
$coversheetfilenametosend = "srbcoversheet".$docid.".pdf";
$coversheetfilepath = $coversheetdocdir."/".$coversheetfilename;
if (file_exists($coversheetfilepath)) {
  output_cached_pdf($coversheetfilepath, $coversheetfilenametosend);

// Include PDF library
error_reporting(error_reporting() ^ E_DEPRECATED);

// Include barcode-generating libraries
$barcodelibrarydir = $_SERVER["DOCUMENT_ROOT"]."/lib/inc/barcodegen1d/class";

// Check that the requested document exists and get some basic data about it
$doc = $db->queryRow("SELECT title, doctype FROM documents WHERE id = %d", $docid);
if (empty($doc)) {
  $page->redirectAndAlert("The requested document was not found", "error", "/library/search");

/* Generate barcode
   see */

// Define barcode font and colours
$font = new BCGFont($barcodelibrarydir."/font/Arial.ttf", 18);
$color_black = new BCGColor(0, 0, 0);
$color_white = new BCGColor(255, 255, 255);

// Set up the barcode
$code = new BCGean13();
$barcodestr = str_pad($docid, 12, 0, STR_PAD_LEFT);
$checksum = $code->getChecksum();

// Draw the barcode and save it to disk
$barcodetmpdir = $_SERVER["DOCUMENT_ROOT"]."/lib/tmp/barcodes";
if (!is_dir($barcodetmpdir) or !is_writable($barcodetmpdir)) {
  trigger_error("Temporary barcode directory does not exist or is not writable", E_USER_ERROR);
$barcodetmpfile = $barcodetmpdir."/".$barcodestr.".png";
$drawing = new BCGdrawing($barcodetmpfile, $color_white);

$pdf = new FPDF("P", "mm", "A4");

$pdf->SetDisplayMode("fullpage", "continuous");
$pdf->SetAuthor("Stone Rowe Brewer");
$pdf->SetCreator("Prater Raines Ltd");
$pdf->SetSubject("SRB Intranet File Document Cover Sheet version $version");
$pdf->SetMargins(15, 20);
$pdf->AddFont("OCRB", "", "ocrb.php");

$pdf->SetFont("OCRB", "", 26);
$pdf->Cell(0, 0, "SRB Intranet Document v$version", 0, 0, "L");

$pdf->SetFont("Times", "B", 20);
$pdf->MultiCell(0, 8, $doc["title"], "TB", "C");

$pdf->SetFont("Times", "B", 10);
$pdf->Cell(0, 7,
  to_pdf_charset("DO NOT REUSE THIS COVERSHEET"), 0, 0, "L");
$pdf->Cell(0, 7,
  to_pdf_charset("Create a different Intranet ID for each related document"), 0, 0, "R");

if (!empty($doc["doctype"])) {
  add_meta($pdf, "Document Type", $doc["doctype"]);
$metadatares = $db->query(
  "SELECT mf.label, mf.type, md.value FROM metafields mf "
  . "JOIN metadata md ON "
  . "WHERE md.docid = %d ORDER BY md.metafieldid, md.sortindex", $docid);
while ($metadatarow = $db->getRow($metadatares)) {
  // Date fields should be formatted as dates
  if ($metadatarow["type"] == "date") {
    $value = date("d/m/Y", $metadatarow["value"]);
  } else {
    $value = $metadatarow["value"];
  // Add the value to the page
  add_meta($pdf, $metadatarow["label"], $value);
$tags = $db->querySingle(
  "SELECT GROUP_CONCAT(tag SEPARATOR ', ') FROM doctags dt "
  . "JOIN tags t ON WHERE dt.docid=%d", $docid);
if (!empty($tags)) {
  add_meta($pdf, "Tags", $tags);

$ref = "id#$docid$checksum";
$pdf->SetFont("OCRB", "", 20);
$pdf->Cell(0, 0, trim(str_repeat("$ref ", 3)), 0, 0, "L");
$pdf->Cell(0, 0, trim(str_repeat("$ref ", 3)), 0, 0, "C");
$pdf->Cell(0, 0, trim(str_repeat("$ref ", 3)), 0, 0, "R");

$pdf->Image($barcodetmpfile, 55, 195, 94);

$pdf->SetFont("OCRB", "", 26);
$pdf->Cell(0, 0, "SRB Intranet Document v$version", 0, 0, "R");

// Delete the temporary barcode file

// Send the PDF to the browser
$pdf->Output($coversheetfilepath, "F");
output_cached_pdf($coversheetfilepath, $coversheetfilenametosend);

function to_pdf_charset($string) {
  return iconv("UTF-8", "ISO-8859-1//TRANSLIT", $string);

function add_meta(&$pdf, $label, $value) {
  $pdf->SetFont("Times", "I", 14);
  $pdf->Cell(50, 7, to_pdf_charset("$label:"), 0, 0, "R");
  $pdf->SetFont("Times", "", 14);
  $pdf->MultiCell(0, 7, to_pdf_charset($value), 0, "L");

 * Get a PDF from disk, output it to the browser, and exit the script
 * @param string $filepath       path to the cached PDF
 * @param string $filenametosend filename with which the output file will be sent to the user
 * @return void
function output_cached_pdf($filepath, $filenametosend) {
  header("Expires: ".date("r", time()));
  header("Cache-Control: max-age=0, public");
  header("Content-type: application/pdf");
  header("Content-length: ".filesize($filepath));
  header("Content-Disposition: attachment; filename=".$filenametosend);