HVAC contractor, southern Ontario (3-4 technicians, one dispatch office)
Scheduling, Field Work Orders, and QuickBooks Desktop for an HVAC Contractor
Work orders priced by hand on paper, a schedule with nothing in it to catch a double booking, on-call hours typed onto a timesheet every week by the office, and a bookkeeper retyping every invoice into QuickBooks Desktop off that paper trail. The crew had also killed one GPS rollout already.
What we built
A dispatch board, a phone flow for technicians where location is asked for before it is ever taken, a pricing-review queue for the office, and a QuickBooks Desktop integration over the Web Connector with cent-match and no-double-invoice guards. QuickBooks stays the ledger of record.
What the record shows
Still pre-launch as of the last recorded build activity. Every work order gets its own number, even when two people finish jobs in the same second. Overtime follows Ontario's ESA rules without anyone doing the math by hand, and a job can move from intake through QuickBooks Desktop without a person retyping a figure along the way. No production usage data exists yet, so read the status note below.
The client’s own words, unedited
This build came out of one email. The client runs three to four technicians out of a single dispatch office in southern Ontario, and he wrote down how his own process worked, in his own words. That email became the requirements brief:
“Right now, I get all of the supplier invoices, add a markup and add to our physical WO’s and then [the bookkeeper] types up in QuickBooks.”
Every invoice got typed twice. Once from the supplier invoice onto a paper work order, then again from that paper work order into QuickBooks. Thirteen concrete pain points came out of that email and a short follow-up thread. In his order:
- No mandatory fields on new customers. Records went in however whoever was typing felt like it. He wanted hard-required fields, email, bill-to, site address, phone, “so it is filled out properly.”
- No schedule-conflict warning. Technicians got assigned from memory, and nothing in the system checked whether that technician was already booked.
- No pricing structure on the work order. Labour, materials, service charge and parking never got itemised, so the bookkeeper rebuilt the total off the paper afterwards.
- On-call hours added by hand. Somebody in the office typed 3 extra hours onto the on-call technician’s timesheet, every single week of the year.
- No aggregate view of technician hours, vacation, or overtime across any stretch of time.
- Quarterly maintenance tracked with no automatic prompting. Nothing told the office a recurring job had come due. It rode on somebody remembering.
- No centralized customer or site record, and no way to tap an address in the field and get driving directions.
- No live technician status. Once a job went out, the office had no idea whether the technician had accepted it, was on the way, was running late, or had turned it down.
- No structured overtime calculation or disclosure. The 1.5x multiplier got worked out by hand, and never showed up clearly for the customer signing the work order. He asked for a visible marker, something like “3*”, so the multiplier reads plainly on the signed document. Weekend hours counted as overtime too, with no automatic rule behind them.
- A previous GPS-tracking attempt was a deal-breaker. His words: “we would need to discuss the GPS tracking as this was the deal breaker last time.” That one sentence shaped a whole part of the build. More on it below.
- Parking reimbursement was informal, with technicians paying out of pocket and the office paying them back on trust, with nothing to check it against.
- No customer-facing sign-off record with photos, and no reliable emailed copy of a completed work order.
- No warranty-call notification loop. On manufacturer warranty work, somebody had to update the manufacturer by hand on scheduling, parts and completion. This one arrived two weeks into the build as a follow-up ask rather than in the original brief.
Every one of those thirteen traces back to a nameable moment in a specific person’s workday, written down by a dispatch office that got asked what was broken. Nothing on the list was invented by us, which makes it a better spec than most feature-request lists we get handed.
What got built
The dispatch board
The office works off a technician-by-day board. One job card per work order, native drag-and-drop to reschedule, and a confirmation step before a date or a technician assignment changes. Drop a technician into a window he’s already booked for and the board warns you, then lets you do it anyway. The client wanted the conflict flagged, with the dispatcher still making the judgment call.
Customer intake now demands the fields he asked for: email, bill-to, site address, phone. A “due this week” filter pulls up recurring and quarterly maintenance jobs on its own, so those stop depending on someone’s memory. The board polls for status updates every five seconds. A full push or websocket layer would be more machinery than a four-person crew watching one screen needs, and five seconds answers “is this live enough” for them.
The technician’s phone
Every work order takes its number automatically, from a counter that never hands two jobs the same one. The old paper numbering left that to chance, and a duplicate number meant nobody could tell which job was which. Even if the office and a technician close out two different jobs in the same second, each still gets its own number.
On the technician’s own phone, the job page carries four status buttons: Accepted, On My Way, Delayed, Rejected. The office sees the change close to real time, which is pain point 8 answered directly. Clock in and clock out are tracked per visit, with a manual override in 0.25-hour increments for the times the system’s read needs a human correction. Photos come off the camera or out of the photo library. An iPhone photo arrives in HEIC, and the server converts it to a downscaled JPEG on its own, because HEIC files don’t display everywhere and no technician should have to know what HEIC is. Tap the site address and Google Maps opens.
Parking got built as a headline feature, because the client said plainly it should never sit behind a feature flag. The technician photographs the receipt and enters an amount, which routes to an office approval queue before it can land on an invoice. Jobs in the downtown core get a “nearest parking lot” lookup that reads a public municipal parking-occupancy open-data set, bundled at build time and refreshed out of band, plus a one-time office-side geocoding pass on each job site. Nothing about that lookup phones anywhere at runtime. It’s a static dataset doing a static job, with no live GPS and no payment integration behind it.
Overtime works itself out under two rules that never stack: a flat 1.5x on Saturday and Sunday, and the 44-hour weekly threshold from Ontario’s Employment Standards Act. Only one of them ever applies. The multiplier and the reason for it, “weekend” or “after reaching 44 hours in the week”, render as plain readable text on the signed document, where a customer can see it without hunting for a footnote. That logic holds up on the awkward cases too, like a shift that crosses the 44-hour line halfway through a visit, so the number on the page a customer signs is still right.
The customer signs on the technician’s own device with a signature pad. After that signature, the hours and the overtime wording on that document are frozen. A correction made later in the same week’s timesheet cannot creep back into a page a customer has already put their name on.
Location, done the way the client asked for it
Pain point 10 gets its own section, because it’s the clearest case in this build of taking a stated objection at face value. The client had tried GPS tracking before with a different vendor, it burned crew trust badly, and he named it “the deal breaker last time” before any scoping conversation had started.
The system asks for location and only ever takes it after the technician has said yes. Two things happen after that. A one-shot GPS stamp fires when he taps something deliberate (On My Way, clock in, clock out, sign-off), and coarse, foreground-only pings land roughly every 10-15 minutes while the job page is open in front of him. Lock the phone and it stops. Close the page and there is nothing running behind it. Part of that is also an honest technical limit of a browser-based app, and we’d rather say so than dress the limit up as a privacy feature.
Office review before anything becomes an invoice
Every completed job lands in a pricing-review queue before anyone can turn it into an invoice, which is how the client described the process himself: “Admin will review, esp. for typos and to add pricing.” Four pricing fields sit at the customer level and can be overridden per customer: labour rate, minimum call hours (default 2), materials markup percentage and service charge rate. That covers his ask for pricing he can program customer by customer instead of one flat rate across the whole book. A “No Cost” toggle handles jobs done free of charge, and it still produces a printable work order.
Office staff see pricing and technicians never do, kept apart by a strict two-role permission model. Every technician-facing page and PDF gets checked against that rule, so a dollar sign, a rate figure or a markup figure can’t quietly slip through onto a screen a technician can see.
One PDF and HTML rendering template serves the on-screen document, the printed PDF and the emailed customer copy. With one code path behind all three, the screen and the printout can’t drift apart over the years.
A later addendum wired up three automatic emails to a manufacturer contact on warranty service calls: scheduled, parts needed, complete. That was the client’s follow-up ask, and it runs off a shared manufacturer directory the office maintains once and reuses across every job.
The QuickBooks Desktop integration, as the centrepiece
Plenty of vendors hear “the client is still on QuickBooks Desktop” and start quoting a migration to something cloud-based. That was never on the table. He runs QuickBooks Desktop on his own Windows PC and he is staying there, so the integration got built to sit alongside that ledger.
QuickBooks Desktop is the single ledger of record. The web application keeps no invoice table and no payment table of its own. It builds a reviewed, priced work order, hands it over, and QuickBooks assigns the real invoice number. An earlier plan did keep a local invoice record alongside QuickBooks’ own, and we deleted it, because two ledgers is one too many and the second one always ends up wrong.
The connection runs through QuickBooks’ own Web Connector, the SOAP/qbXML polling mechanism Intuit ships for exactly this. The desktop edition has no cloud API to talk to, so that’s the road in.
Three safety rails sit on the money path:
- No double-invoicing. The system refuses an invoice-creation request outright when that job already carries a QuickBooks transaction ID.
- Cent-match reconciliation. The computed total gets checked against what QuickBooks reports back before anything is stored. One cent of daylight between them and the operation aborts.
- Non-destructive customer sync. Customer records come in from the real company file. We create them and update them, and we never delete one.
Ontario HST (13%) and the ESA’s 44-hour overtime threshold sit in the code as hard-coded constants. The client works one province and one jurisdiction, so that was the right call here, and the build says in writing that both would have to become real configuration the day he takes work outside Ontario.
The economics, honestly
The whole point of building and hosting this was to dodge an ongoing per-seat subscription, on infrastructure the operator already owned rather than anything new and billed. As of 2026-08-28, here is what a comparable off-the-shelf product costs at roughly five seats (three to four technicians plus one office user), from published and third-party-reported pricing:
| Product | Approximate monthly cost, ~5 seats |
|---|---|
| Jobber (Connect tier, annual billing) | ~$99-149/month |
| Housecall Pro (Essentials tier, up to 5 users) | ~$149-189/month |
| FieldPulse | ~$300-600/month (quote-based, third-party estimate) |
| Zuper | ~$300-600/month (quote-based, third-party estimate) |
| ServiceTitan | ~$1,000-2,000+/month before add-ons, plus an additional implementation fee and a multi-year contract reviewers commonly describe as substantial (specific figures not independently verified) |
The Jobber and Housecall Pro figures come off their own public pricing pages. FieldPulse, Zuper and ServiceTitan publish nothing, so those three are third-party-reported estimates with no vendor confirmation behind them. Weigh them accordingly if you’re out collecting quotes yourself.
This system’s marginal hosting cost rounds to zero. It shares an existing always-free-tier cloud server the operator already runs for other client work, under an enforced CPU and memory cap. No per-seat licence exists at all, so a fifth or sixth technician costs whatever that person’s own phone costs. An earlier, since-superseded hosting plan (a small dedicated cloud server) was costed at roughly $6-10/month before the zero-cost shared-server approach replaced it. Even that fallback sat an order of magnitude under any SaaS competitor’s price, which is the only reason it’s worth a mention. Treat the number as history, since it describes a plan that was abandoned.
None of that makes a custom build the right call for every shop, and the project’s own research says so out loud: Jobber is honestly the better product for a shop this size on pure feature-for-dollar terms. Custom won here on four business rules a generic product doesn’t hand you out of the box. Overtime that calculates itself and never stacks, with a multiplier the customer can read on the page they sign. GPS the technician opts into. True per-customer pricing without buying a rules-engine tier. And a link into the accounting software already sitting on the desk, with no switch to a cloud accounting product.
The trade-offs cut the other way too. A custom build has no vendor support line and no bench of engineers behind it beyond the person who wrote it, so every bug is this team’s bug, where a SaaS product spreads that risk over thousands of customers. Free per seat still costs money. Somebody designed, built, tested and now maintains this thing, and that bill arrived once instead of every month. A single self-hosted server, replication and all, is a smaller blast radius than a large SaaS vendor’s infrastructure, which is exactly why the backup and restore discipline above earns its keep. It’s the answer to the strongest argument a “just use a SaaS” case would make for itself.
Where this stands today
As of its last recorded activity, this project was still in demo and pre-launch build. Everything above describes what got built and demoed. No measured outcomes from live use exist yet, so treat what follows as two separate lists: what you can trust today, and what you can’t.
Working in demo: a job can move from intake through assignment, the technician accepting it, clock in and out, a photo, sign-off, office review and pricing, all the way to the queue for QuickBooks, without a person retyping a number along the way. The system renders a real PDF, and came back clean after a full data-volume wipe and restore, with the database and the photos intact. Work-order numbering held up when two jobs closed out at the same instant, and the overtime math held up against Ontario’s own ESA rules, including a shift that crosses the 44-hour line halfway through a visit.
Not yet proven: actual time saved per invoice cycle, any drop in billing errors, whether the office or the crew took to it day to day, and any dollar figure tied to real use. That’s because the system was running on seeded demonstration data, against a QuickBooks Desktop connection nobody had yet pointed at the real company file’s tax codes and edition. Whether the client’s actual overtime policy matches what was built, flat weekend 1.5x plus the ESA 44-hour floor, was never confirmed with the office before this build reached its last recorded state. Treat that one as unconfirmed until someone checks the current status directly.
This case study describes work built and demoed against a client’s own written pain points. No result gets claimed here that can’t be backed up. Once the system goes live and gathers real usage, that will be a second case study rather than a quiet edit to this one.
Show us the paperwork that slows the job down.
Send a rate card, work order, or tender package. We'll tell you if this kind of build fits before either of us wastes time.
Send a sample documentRelated pages
The service behind this build, the trade it serves, and another job like it.
All case studiesWorkflow Automation for Busy Shops
Built around your current tools, one owned workflow can connect field work, scheduling, invoices, and the records your office needs.
Service
HVAC Contractors
For HVAC shops that already use QuickBooks Desktop, paper work orders and an office calendar. We build the missing field workflow around them.
Industry
Report Writing Corpus Built for an Arborist and Landscaping Firm
How an arborist and landscaping firm turned past arborist reports into a writing corpus, then turned their conclusions into guidelines for new drafts.
Built for an arborist and landscaping firm
Landscaping Tender Pipeline for Provincial and Municipal Bids
How an arborist and landscaping firm turns a provincial or municipal tender into a priced, cited quote draft that a person certifies before it goes out.
Built for an arborist and landscaping firm
Field Service ERP for a Fire-Safety Inspection Contractor: Paper Books to a 42,000-File Digital Archive
Years of finished work orders, invoices and quotes sat scattered across filing cabinets, a shared drive of scanned photos, and two Excel workbooks. Nobody could open one client's file and see the whole history in one place. The receivables sheet alone ran north of $1.7 million across roughly 490 rows, with duplicate and hidden rows in it that the client's own staff had stopped trusting.
Case study