Build FreshBooks
KINDAreplaces $23/mosaves $276/yrback to the verdict
Clients, projects and tracked time turning into invoices with a gapless sequence, PDFs emailed and recorded, payments applied against invoices with partial payment handled properly, credit notes for corrections, and exports an accountant can reconcile.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the application, the PDF rendering and the reports.
Get it python.org or your package manager, then a virtual environment.
Verify
python3 --version prints 3.12 or higher - installfree
Why Invoices are documents someone else keeps, and they must render identically every time.
Get it A headless browser print path or a dedicated renderer. Whichever you choose, confirm two renders of the same invoice are byte-identical.
Verify
Two renders of one invoice produce identical files - API keyfree tiers cover a sole trader's volume
Why An invoice in a spam folder is an unpaid invoice, and domain verification is what prevents that.
Get it Create an account, add the DNS records they specify, take the credentials. Verification takes hours, so start early.
Verify
An invoice email arrives at three providers and is not in spam - decideusually free, and it saves more than it costs
Why Numbering, credit note handling and what they need in an export are all things they have opinions about, and matching those opinions now is cheaper than reformatting a year of records later.
Get it Ask what format they want, whether your numbering scheme is acceptable, and what they need to see for a correction.
Verify
The export format has been agreed with them - have readyfree
Why Numbering must continue from where you are rather than restarting, and importing history is what makes the reports meaningful.
Get it Whatever you have: a spreadsheet, another tool's export, or PDFs. Enter at least the numbers and totals so the sequence continues correctly.
Verify
The next issued invoice continues your existing sequence
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- clients(id, name, email, address, currency, payment_terms_days, notes) - projects(id, client_id, name, rate_minor, billable, active) - time_entries(id, project_id, started_at, minutes, description, invoiced_on_invoice_id) - invoices(id, number, client_id, issued_on, due_on, currency, state, subtotal_minor, tax_minor, total_minor, pdf_path, pdf_hash) - invoice_lines(id, invoice_id, description, quantity, unit_minor, tax_rate_bp, amount_minor) · tax_rate_bp is a rate someone entered, in basis points - payments(id, client_id, received_on, amount_minor, method, reference), allocations(payment_id, invoice_id, amount_minor) - credit_notes(id, number, invoice_id, issued_on, amount_minor, reason) - A payment is allocated to invoices rather than belonging to one, so a single transfer covering three invoices records correctly.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DB_PATH | required | ./data/books.db | Clients, invoices, payments. Financial records, so back them up properly. |
DOCUMENTS_DIR | required | ./data/invoices | Issued PDFs. Part of the record and part of the backup. |
INVOICE_NUMBER_FORMAT | required | INV-{year}-{seq:04d} | Your numbering scheme. Agree it with your accountant before the first invoice. |
NEXT_INVOICE_SEQ | required | 1 | Where the sequence continues from. Set it to follow your existing records. |
DEFAULT_PAYMENT_TERMS_DAYS | required | 14 | Default due date offset. Overridable per client. |
EMAIL_SERVERsecret | required | smtp://user:pass@smtp.provider.com:587 | Transactional provider credentials. |
EMAIL_FROM | required | invoices@example.com | On the verified sending domain, or delivery will suffer. |
The build, in order
Money and numbering, before anything else
Amounts are exact and invoice numbers are gapless.
Storage and arithmetic. Formatting happens at display and nowhere else.
Files
src/money.pySequence and insert in one transaction. Test by forcing a failure mid-creation.
Files
src/numbering.pyOne multi-line invoice, computed by hand, asserted in a test. This is the arithmetic everything rests on.
Files
tests/test_totals.py
done when · tick each as it passeswatch out- Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
- Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
Clients, projects and billable time
Time you tracked becomes lines on an invoice without being entered twice.
Rate on the project, currency on the client, terms driving the due date.
Files
src/clients.pysrc/time.pyIn the same transaction. Anything else double bills eventually.
Files
src/invoice.pyLocked, or edits recorded against the affected invoice. Silent edits break the audit trail.
Files
src/time.py
done when · tick each as it passeswatch out- Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
Issuing, which makes a document permanent
A sent invoice is fixed, and corrections happen the way accounting expects.
State transition, with no update path for issued invoices anywhere in the code.
Files
src/invoice.pyThe document the client received. The hash proves it has not changed.
Files
src/render.pyTheir own sequence, referencing the invoice, reducing the balance. Easy enough that nobody wants to edit instead.
Files
src/credit_notes.py
done when · tick each as it passeswatch out- Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
Sending, and knowing it was sent
The client receives the invoice, and you can prove when.
The DNS records your provider specifies. Invoices in a spam folder are unpaid invoices.
Attempt, outcome and timestamp. This is the answer when a client says it never arrived.
Files
src/send.pyAgainst the invoice, visibly. A bounce is information you must act on.
Files
src/webhooks.py
done when · tick each as it passeswatch out- An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
Payments, including the awkward ones
A single transfer covering three invoices records correctly, and so does a partial payment.
A payment is a real event; allocations distribute it. This is what matches the bank statement.
Files
src/payments.pyFrom allocations and credit notes. A settable state field will disagree with the arithmetic eventually.
Files
src/invoice.pyCredit held for the client, partial payment with a correct remainder.
Files
src/payments.py
done when · tick each as it passeswatch out- Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
Reports, export and the accountant's questionsproduct builder
You can answer what you invoiced, what you were paid and what is outstanding, and hand it over.
Invoiced, paid, outstanding, aged. Verify one month by hand.
Files
src/reports.pyCSV plus PDFs. An accountant should not need this application.
Files
src/export.pyDatabase and documents together. Verify hashes after restore.
Files
scripts/backup.shREADME.md
done when · tick each as it passeswatch out- Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches.
That is the whole plan for FreshBooks. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Tax calculation or determination.
- Online payment collection.
- Bank feed imports and reconciliation.
- Full accounting and financial statements.
- payment collection
- bank imports
- accounting reports
- client portal
- mobile apps
- tax workflows
- support
- Recurring invoices, which are a template plus a schedule once issuing is correct
- Payment links through a processor, added as its own phase with webhook reconciliation
- Expense recording with receipts attached, which your accountant will ask for next
Need the files? The project pack on the verdict page hands your agent the whole brief · more finance & accounting.