A script to speed invoicing by email
November 22, 2011 at 8:38 AM by Dr. Drang
For years, I printed out my invoices on paper and sent them off to clients by snail mail. Now that my clients have all embraced interweb thing, I “print” my invoices as PDFs and email them with a brief message. The message is boilerplate, generated by a TextExpander snippet described in an earlier post.
Although the snippet saves time, pushing an invoice out the door is still a needlessly long series of steps:
- Generate one or more invoice PDFs on the accounting computer.
- Go back to my office and grab the PDFs over the local network.
- For each PDF:
- Open it in Preview so I can read the invoice number and amount.
- Start a new Mail message.
- Trigger the TextExpander snippet for the boilerplate.
- Fill in the blanks of the snippet.
- Send the email.
Did I forget to add the step where I attach the invoice PDF? Yeah, well sometimes I forget that step when I’m actually doing it, and then I have to send another email correcting the omission. And sometimes I have to fiddle with the window layout, because Preview gets covered and I can’t read the invoice number and amount. And, of course, sometimes I make a typo and have to go back and fix it.
Given that the PDF contains all the information I need to include in the boilerplate, it seemed like I should be able to better automate the whole procedure. Today I did.
The new system
My goal is to cut down the steps to these:
- Generate one or more invoice PDFs on the accounting computer.
- Go back to my office and grab the PDFs over the local network.
- Select the PDFs in the Finder.
- Invoke a script that creates an email for each invoice with both the boilerplate text and the attached PDF.
- Send the emails.
This may not seem like much reduction in work, but it avoids the problems of Preview getting covered by an overlapping window, typos in the boilerplate, and my forgetting to attach the PDF.
Xpdf
The first step is figuring out how to extract certain text items from the invoice PDF. Luckily, my Linux years got me familiar with the wonderful Xpdf utility by Derek Noonburg. It’s not Xpdf itself I need, but a utility that comes with it: pdftotext
.
My invoice PDFs have filenames like 6666-8888.pdf
, where 6666 is the project number and 8888 is the invoice number. When I run one of my invoices through
pdftotext -layout 6666-8888.pdf -
I get output that looks like this:
[header stuff that we don't care about]
Invoice for Services
November 21, 2011
Project: Kernighan Building Invoice number: 8888
Drang project number: 6666 Your file: 123456-89
Invoice Total: $5,432.10 Payment due: December 21, 2011
Detail of charges
Date Hours Charge Description
[more stuff we don't need to worry about here]
The -layout
option does its best to provide a nicely aligned ASCII representation of the PDF. The trailing hyphen in that command tells pdftotext
to send the results to standard output instead of a file.
With pdftotext
, I can put the content of a PDF in plain text form, which allows me to extract the information necessary to fill in the boilerplate fields.
Make Invoice Emails
Now it’s time to write the script that generates the emails from the selected PDFs. I call it “Make Invoice Emails,” and I keep it in ~/Library/Scripts/Applications/Finder
so FastScripts can run it when Finder is the active application. Nothing about the script is especially hard except for the usual annoyances that come with digging through an application’s AppleScript library to figure out the right wording for the properties and commands. In this case, I’m writing in Python with the appscript
library, so there’s an additional step to translate from AppleScript to Python.
Here’s the script:
python:
1: #!/usr/bin/python
2:
3: from subprocess import *
4: from appscript import *
5: from os import environ
6:
7: # Templates for the parts of the message.
8: sTemplate = '%s: Drang invoice %s'
9: bTemplate = '''Attached is Drang Engineering invoice %s for %s covering\
10: recent services on the above-referenced project. Payment is due %s.
11:
12: Thank you for using Drang. Please call if you have any questions or need\
13: further information.
14:
15: --
16: Dr. Drang
17: www.leancrew.com
18:
19: '''
20:
21: # Open the project list file and read it into a string.
22: pl = open(environ['HOME'] + "/Dropbox/pl").readlines()
23:
24: # Get the selected invoice PDFs.
25: pdfs = app(u'/System/Library/CoreServices/Finder.app').selection.get()
26:
27: # Make a new mail message for each invoice.
28: for f in pdfs:
29: # Get the POSIX path.
30: pdf = f.get(resulttype=k.alias).path
31:
32: # Use pdftotext from the xpdf project (http://foolabs.com/xpdf) to extract
33: # the text from the PDF as a list of lines.
34: pipe = Popen(['/usr/local/bin/pdftotext', '-layout', pdf, '-'], stdout=PIPE)
35: invLines = pipe.communicate()[0].split('\n')
36:
37: # Pluck out the project name, project number, invoice number, invoice amount,
38: # and due date.
39: for line in invLines:
40: if 'Project:' in line:
41: parts = line.split(':')
42: name = parts[1].split(' ')[0].strip()
43: invoice = parts[2].lstrip()
44: if 'project number:' in line:
45: number = line.split(':')[1].split()[0].lstrip()
46: if 'Invoice Total:' in line:
47: parts = line.split(':')
48: amount = parts[1].split()[0].strip()
49: due = parts[2].lstrip()
50:
51: # Get the email address of the client.
52: try:
53: client = [x.split('|')[2] for x in pl if number in x.split('|')[1] ][0]
54: ab = app('/Applications/Address Book.app')
55: contact = ab.people[its.name.contains(client)].get()[0]
56: email = contact.emails[its.label.contains('work')].get()[0].value()
57: except:
58: client = ''
59: email = ''
60:
61: # Construct the subject and body.
62: subject = sTemplate % (name, invoice)
63: body = bTemplate % (invoice, amount, due)
64:
65: # Create the mail message.
66: mail = app('/Applications/Mail.app')
67: mail.activate()
68: message = mail.make(new=k.outgoing_message,
69: with_properties={k.content: body, k.visible: True, k.subject: subject})
70: message.content.paragraphs[-1].after.make(new=k.attachment,
71: with_properties={k.file_name: pdf})
72: message.make(new=k.to_recipient, with_properties={k.name: client, k.address: email})
I hope the code is commented well enough to give a sense of what’s going on. A few additional bits of explanation are warranted, though.
Line 22 opens and reads in the project list file, ~/Dropbox/pl
. This is a plain text file with basic information about all my projects. Each line of the file represents a single project, with the project information formatted like this:
Angel Industries Boiler|2997|Eugene Johnson|File no. 02-445-EAJ|angel.boiler|14
The fields, separated by a pipe (|) character, are:
- Project name
- Project number
- Client name
- Client project number
- Subdirectory name
- Closed file box number
For what we’re doing here, we only care about the second and third fields.
In Lines 34-35, the script shells out via the subprocess
library to run pdftotext
on the current PDF. Lines 39-49 then do the dirty work of pulling the info we want out of the text. This is the reason I wrote this script in Python; the splitting and stripping done in these lines is just murder in AppleScript.
Line 53, which is called for each selected PDF, looks through list to find the project number and extracts the client name from that line. Lines 54-56 are an unfortunate example of the verbosity AppleScript forces on you, even when you’re using appscript
.
Finally, Lines 66-72 put the pieces together into an email message. Again, AppleScript makes this more wordy that it ought to be, but there’s no way around that.
When the script is done, Mail is the frontmost application, and there’s a filled-out message window up for each of the invoice PDFs. I can send the messages out as-is or add an extra note to a message if that’s warranted.
Finally
I like to send invoices out in batches, so the loop that constructs an email for each PDF in the selection should save me a bunch of time. I used it to send out two invoices today—a small batch for testing—and when the Mail windows popped up with everything filled out, attached, and ready to go, I couldn’t help but smile.
As is often the case with the scripts I present here, this one is too specialized for my needs and setup to be directly useful to anyone else. But I hope it gives you a hint of what you can do to make your workflows smoother.