The Challenge: Canadian Business Invoicing Done Right
As a consultant working with Canadian businesses, I needed a professional invoice and timesheet system that respected GST and HST, tracked SOW and PO references, and produced sharp PDFs ready for clients. I did not want another subscription or a brittle spreadsheet. I chose Django, and I let AI coding assistants accelerate the work.
This post shows exactly what we built, how AI helped, what still required human judgment, and the prompts that saved hours.
Note: this post shares an engineering approach, not tax advice. Keep your accountant close.
What We Built
A production Django application that delivers:
- Professional invoice generation with automatic sequential numbering
- Canadian tax compliance with GST and HST, 5 percent default and configurable
- Timesheet management with stat holiday support
- Customer and company management with simple CRM features
- PDF generation that looks clean in print and on screen
- Email integration for direct invoice delivery with CC support
- Banking details on invoices, including institution, transit, and account numbers
- SOW and PO tracking on customers and invoices
- Docker deployment for a reliable production setup
- Comprehensive testing with 32 plus unit tests that catch edge cases
The system runs in production today and sends real invoices to real clients.
The AI Assisted Development Journey
Phase 1: Foundation and Core Models
I began with clear, constrained prompts that described the data model and tax behavior.
Prompt
Create a Django project for invoice generation with these models:
Company, Customer, Product, Invoice, OrderLine.
Companies include addresses and contacts.
Invoices have line items with quantities and unit prices.
Prompt
Add GST/HST tax calculation to invoices. The tax rate is configurable
in GlobalSettings and defaults to 5 percent. Use Python's Decimal
for all financial calculations to avoid floating point errors.
This produced a working base. Real needs pushed me to add Canadian specifics.
Prompt
Add fields to Company for Canadian banking:
bank_institution_number (3 digits), bank_transit_number (5 digits),
bank_account_number, and gst_number for GST/HST registration.
Prompt
Add SOW and PO fields to Company and Invoice as optional
CharFields with max length 100.
Key learning: precise requirements reduce refactoring. Asking for Decimal up front prevented subtle rounding bugs.
Phase 2: PDF Generation and Styling
I chose WeasyPrint for HTML to PDF.
Prompt
Set up WeasyPrint. Create a view that renders
invoices/templates/invoices/invoice_pdf.html to PDF and saves it
to Invoice.pdf_document.
Prompt
Design a professional invoice template with:
logo at top, billing and customer details, invoice number and dates,
line items with product, quantity, price, and totals,
subtotal, discount, GST/HST, and grand total, banking information at bottom.
Use a clean, Canadian business style.
The first draft lacked contrast. I iterated.
Prompt
Make the table easier to read.
Add zebra striping, bold headers with a dark background,
clear spacing between sections, and a bordered totals box.
Pro tip: for anything visual, iterate in short loops. Establish layout first, then refine spacing, then refine type, then contrast.
Phase 3: Timesheet Functionality
I needed monthly timesheets, calendar view, stat holidays, and totals.
Prompt
Create Timesheet and TimesheetEntry.
Timesheet: customer, month, year, hourly_rate.
TimesheetEntry: timesheet, date, hours (decimal), is_stat_holiday.
Validate that entries fall inside the timesheet month.
Prompt
Create a timesheet PDF template similar to the invoice template.
Show a calendar grid with dates, hours, and stat holiday markers.
Calculate total hours and total amount due at the bottom.
A pleasant surprise came from the assistant.
Unexpected win: it suggested Django calendar utilities for a clean calendar grid. That simplified the implementation and reduced custom date math.
Phase 4: Comprehensive Testing
Tests are tedious to write, yet they save your future self. This is where AI shined.
Prompt
Write unit tests for Invoice:
creation with automatic numbering, GST/HST accuracy,
discounts, zero value invoices, fractional cent rounding.
Use Django's test framework and separate modules.
Prompt
Write tax calculation edge case tests:
half cent cases such as 10.005, pennies, large amounts,
0 percent tax, 100 percent discount.
Use Decimal and quantize to 2 decimal places.
Prompt
Write tests for timesheets:
hours math, stat holiday marking, month validation,
rate calculations, and partial hours 0.5 and 0.25.
Result: 1,348 lines of tests across 32 plus cases. The suite caught several corner cases before production.
Phase 5: Email Integration and Deployment
Email needed to send PDFs with a clear message and CC support.
Prompt
Create an email utility that takes an Invoice,
renders a concise email body, attaches the PDF,
sends to the customer's contact, CCs a secondary email if set,
and updates a MailInfo status. Use Django's email framework.
For production I containerized the service.
Prompt
Create a Dockerfile that uses Python 3.13, installs dependencies with Poetry,
includes the SQLite database for dev, runs migrations on startup,
uses Gunicorn for production, and serves static files with WhiteNoise.
Add docker-compose.yml.
Critical issue discovered: dates displayed as 2,025 instead of 2025. Django thousand separators applied to year numbers.
Fix
Set USE_THOUSAND_SEPARATOR = False in settings.py.
Explain that internationalization number formatting applies separators
to all numbers, including years in certain contexts.
Key Technical Decisions
Use Decimal for Money
Floating point math creates penny level errors. Decimal avoids this.
from decimal import Decimal, ROUND_HALF_UP
TWO_PLACES = Decimal("0.01")
def money(x: Decimal) -> Decimal:
return x.quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
subtotal = money(sum(line.total for line in invoice.lines()))
gst_rate = Decimal("0.05") # configurable default
tax = money(subtotal * gst_rate)
total = subtotal + tax
Prompt that helped
Explain why to use Decimal for currency.
Show a calculation that fails with float and succeeds with Decimal.
Automatic Unit Price from Product
Avoid retyping prices. Let OrderLine inherit the price, but allow overrides.
def save(self, *args, **kwargs):
if not self.unit_price and self.product:
self.unit_price = self.product.price
super().save(*args, **kwargs)
Test Safe Invoice Numbering
Sequential numbers should not cause flaky tests. I moved tests into a separate range.
def increase_last_number(self):
import sys
if 'test' in sys.argv or 'pytest' in sys.modules:
if self.last_number < 9000:
self.last_number = 9000
self.last_number += 1
self.save()
return self.last_number
Users add several items fast with formsets.
from django.forms import inlineformset_factory
OrderLineFormSet = inlineformset_factory(
Invoice, OrderLine,
form=OrderLineForm,
extra=10,
can_delete=True
)
Prompts That Saved Hours
The Audit Prompt
Prompt
Review Invoice.calculate_totals for bugs.
Check order of operations, Decimal usage, edge cases,
and any division by zero. Suggest improvements and write tests.
This caught a 100 percent discount case that still charged tax. The fix set the subtotal after discount to zero, then short circuited tax.
The Production Ready Prompt
Prompt
Review the Django app for production readiness.
Check security settings and secrets, common indexes,
static file configuration, email error handling,
and migration safety. Produce a deployment checklist.
I added indexes, fixed ALLOWED_HOSTS, hardened settings with environment variables, and confirmed WhiteNoise and Gunicorn behavior.
The Test Organization Prompt
Prompt
Split invoices/tests.py into:
test_models.py, test_forms.py,
test_invoice_calculations.py, test_timesheets.py.
Keep the same coverage, improve maintainability.
The suite became easier to read and run.
Lessons Learned
What Worked Well
- Specific prompts created better code and fewer revisions.
- Iterative refinement raised quality without churn.
- Asking for explanations taught me new patterns while shipping.
- Test generation fit AI perfectly, especially for edge cases.
- Code review prompts surfaced bugs I would have missed.
What Required Human Judgment
- Canadian business rules and SOW and PO behavior.
- UX decisions such as layout, wording, and approvals.
- Architecture choices such as Django, WeasyPrint, and containerization.
- Security and privacy tradeoffs.
- Deployment decisions and backup plans.
Pitfalls to Avoid
- Accepting the first answer without inspection.
- Skipping edge cases in tests.
- Pasting code you do not fully understand.
- Ignoring warnings in explanations.
- Over reliance on AI at the expense of domain understanding.
Results
Metric |
Value |
Time |
about six weeks of evenings and weekends |
Solo estimate without AI |
three to four months for an experienced Django developer |
Lines of code |
about 2,000 application, 1,350 tests, 800 templates |
Production |
running today with Docker |
Test coverage |
32 plus comprehensive tests |
Cost savings |
replaced a 20 dollar per month subscription |
Conclusion
AI did not write the entire system, or this blog post, it just sped up the tedious parts, helped me think, and caught bugs through test generation and code review prompts. I set the business rules and architecture, and I made the tradeoffs. Together we shipped a reliable, Canada aware invoicing stack in a reasonable timeline.