Build FreshBooks

KINDAreplaces $23/mosaves $276/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimePython 3.12 with SQLiteMoneyInteger minor units throughout, never floating pointNumberingA gapless sequence allocated in a transactionDocumentsHTML rendered to PDF, stored immutably once sentEmailA transactional provider, with every send recorded

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DB_PATHrequired./data/books.dbClients, invoices, payments. Financial records, so back them up properly.
DOCUMENTS_DIRrequired./data/invoicesIssued PDFs. Part of the record and part of the backup.
INVOICE_NUMBER_FORMATrequiredINV-{year}-{seq:04d}Your numbering scheme. Agree it with your accountant before the first invoice.
NEXT_INVOICE_SEQrequired1Where the sequence continues from. Set it to follow your existing records.
DEFAULT_PAYMENT_TERMS_DAYSrequired14Default due date offset. Overridable per client.
EMAIL_SERVERsecretrequiredsmtp://user:pass@smtp.provider.com:587Transactional provider credentials.
EMAIL_FROMrequiredinvoices@example.comOn the verified sending domain, or delivery will suffer.

The build, in order

  1. Money and numbering, before anything else

    Amounts are exact and invoice numbers are gapless.

    1. Storage and arithmetic. Formatting happens at display and nowhere else.

      Files src/money.py

    2. Sequence and insert in one transaction. Test by forcing a failure mid-creation.

      Files src/numbering.py

    3. One 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 passes
    watch 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.
  2. Clients, projects and billable time

    Time you tracked becomes lines on an invoice without being entered twice.

    1. Rate on the project, currency on the client, terms driving the due date.

      Files src/clients.pysrc/time.py

    2. In the same transaction. Anything else double bills eventually.

      Files src/invoice.py

    3. Locked, or edits recorded against the affected invoice. Silent edits break the audit trail.

      Files src/time.py

    done when · tick each as it passes
    watch 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.
  3. Issuing, which makes a document permanent

    A sent invoice is fixed, and corrections happen the way accounting expects.

    1. State transition, with no update path for issued invoices anywhere in the code.

      Files src/invoice.py

    2. The document the client received. The hash proves it has not changed.

      Files src/render.py

    3. Their 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 passes
    watch 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.
  4. Sending, and knowing it was sent

    The client receives the invoice, and you can prove when.

    1. The DNS records your provider specifies. Invoices in a spam folder are unpaid invoices.

    2. Attempt, outcome and timestamp. This is the answer when a client says it never arrived.

      Files src/send.py

    3. Against the invoice, visibly. A bounce is information you must act on.

      Files src/webhooks.py

    done when · tick each as it passes
    watch 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.
  5. Payments, including the awkward ones

    A single transfer covering three invoices records correctly, and so does a partial payment.

    1. A payment is a real event; allocations distribute it. This is what matches the bank statement.

      Files src/payments.py

    2. From allocations and credit notes. A settable state field will disagree with the arithmetic eventually.

      Files src/invoice.py

    3. Credit held for the client, partial payment with a correct remainder.

      Files src/payments.py

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more finance & accounting.