Your COGS Line Is Wrong — And It's Inflating Your Margins
Your COGS Line Is Wrong — And It's Inflating Your Margins
Here is what I found in 2019 on a 14-truck residential HVAC shop in the mid-Atlantic: the owner had been losing money on every install job for approximately three years. Not bleeding out — losing quietly, a few points at a time, in a way that never triggered an alarm because the service side was profitable enough to carry it. The P&L showed install gross margin around 22%. The actual number, once we reclassified the labor correctly, was closer to 4%.
The owner was not reckless. He had simply never had a reason to question where his job costs were landing on the chart of accounts, and neither had his bookkeeper.
That is a cost classification problem, not a pricing problem. Of the fourteen acquisitions I touched at Atlantic Comfort Partners, eleven came in with a COGS line that overstated margin — sometimes by four points, sometimes by twelve.
What "Cost of Goods Sold" Actually Means for a Contractor
Cost of goods sold, in a contracting context, is the total direct cost of producing a specific unit of revenue: field labor with full burden, materials at landed cost, and subcontracted work tied to the job. That is the list.
Most small-shop P&Ls run longer because they have absorbed indirect costs — truck operating expense, dispatcher time, shop rent allocation — into COGS rather than overhead. That compression matters. Gross margin and contribution margin are different numbers, and they produce different pricing decisions. Gross margin is revenue minus direct COGS. Contribution margin is what remains after subtracting variable costs, which may not match your COGS depending on how you have classified fixed versus variable expenses.
A shop running 38% gross margin might be running 24% contribution margin if fixed costs have been absorbed into COGS. Pricing off the wrong number means every job estimate starts with a built-in error.
At Atlantic Comfort Partners, the COGS line was always the first thing the ops team touched in a recast. Every acquisition target went through it. The sellers were not lying. In my experience, they were using a chart of accounts configured by a CPA whose primary objective was a clean tax return, not job-level visibility — and the two structures are not the same thing.
The acquirer recast it because the acquirer was pricing a business. You should recast it because you are pricing jobs.
The Labor Misclassification Problem
The most common version of this is also the least visible. A field tech installs a condenser, logs his hours, and the hours get coded to the job. But coded how?
In about half the shops I have audited — I am currently at thirty-one engagements since going independent in 2021 — the time entry lands on a materials line because the job is built around the equipment and the tech's time reads as an accessory to it. Sometimes install labor gets absorbed into the equipment line entirely, one combined figure for "equipment and installation," and the labor portion disappears as a discrete number.
Callback hours are the quieter problem. A tech spends two hours on a warranty callback. The job is closed, the invoice is paid, there is nowhere obvious to put the hours. They vanish into payroll expense on the P&L as general overhead. The original job's margin calculation never absorbed the cost of fixing it. The job looked fine. The job was not fine.
Here is where the calculation breaks. Say a shop does a $9,800 equipment replacement. The tech and helper spend nine hours on the install. The price book says 31% gross margin, or $3,038. But if those nine hours were coded to materials rather than labor, the gross margin calculation never compared actual labor cost against the labor revenue embedded in that ticket. The owner sees the number he expects. The number does not tell him whether he made money on install labor specifically, or whether he is consistently underestimating install time on that equipment category.
When labor is misclassified, the markup math feels right and the margin math is invisible. You are pricing the next job off data that was wrong on the last one.
The Overhead That Hid Inside Your COGS Line
The second way this goes wrong runs the opposite direction. Instead of direct costs bleeding into the wrong category, indirect costs end up inside COGS. That makes company-level gross margin look lower than it actually is, while simultaneously making it impossible to see what each job cost.
Truck operating cost is where I spend the most time. Most shops use a national average for vehicle cost per mile or per hour. That number does not belong on individual job tickets as a materials line. It belongs in overhead, calculated annually as total fleet cost — insurance, fuel, maintenance, registration, depreciation or lease payment — divided by projected billable hours across the fleet. That produces a per-billable-hour vehicle burden rate that feeds your cost-of-doing-business calculation, which is the number that has to be in every price before you touch margin.
The math: if your five trucks cost $147,000 per year fully loaded and your fleet generates 8,400 projected billable hours, your vehicle burden rate is roughly $17.50 per billable hour. That number belongs in your overhead pool. I find it coded directly to individual job records about half the time.
Dispatcher time, shop rent, and warehouse labor if you carry inventory are the same category. Indirect costs belong in SG&A or overhead allocation, separated from the direct cost of producing a specific job. When they sit inside COGS, they compress apparent gross margin without giving you any job-level information. When they sit in overhead and get allocated correctly, they produce a per-billable-hour cost burden you can actually use.
Better Software Won't Fix a Classification Problem
When a shop owner tells me the P&L feels wrong and they need better reporting, the next sentence is usually about software. The instinct is understandable and almost always wrong.
I will say plainly what I said in a longer piece on ServiceTitan: for shops under 12 trucks, the reporting suite is unused. Not because it is bad — it is genuinely capable — but because nobody on the team has the time to configure the reports correctly. The implementation consultant optimized for a working demo, not for the shop's specific job costing logic. If the COGS categories are misconfigured going in, the job margin reports coming out are wrong with more confidence and more detail than before. The software scales the problem.
The flat-rate pricing book issue is related. The book produces predictable revenue per ticket — that part is real. The margin per ticket is not predictable, because the labor and overhead assumptions baked into the pricing were built on the vendor's cost model, not yours. I have opinions about the major vendors by name, but the structural problem is the same across all of them: if your burden rates differ, if your truck cost per billable hour differs, if your call volume doesn't match the book's assumption about calls per tech per day, the number in the book is someone else's number. You are pricing off a fiction that is more legible than your own, which makes it feel more trustworthy.
The fix is definitional. It happens in your chart of accounts before any software touches it.
How to Rebuild the Line — What Actually Belongs Where
Here is the classification structure I use when rebuilding a residential HVAC shop's job costing from scratch.
Direct COGS:
- Field labor at fully burdened cost. FICA runs 7.65%. Workers' compensation in HVAC is among the higher-risk classifications — pull your actual rate from your premium invoice, not an estimate. Add health insurance at whatever the shop contributes, and accrued PTO. In the shops I work with, the base wage alone understates actual direct labor cost by 20 to 30 points once burden is fully accounted for. This is where the margin problem most often originates.
- Materials at landed cost — invoice price plus freight and any warehouse labor directly tied to that material.
- Subcontracted labor tied to a specific job.
Overhead (allocated via a burden rate, not coded directly to jobs):
- Vehicle operating cost per billable hour.
- Dispatcher and CSR time allocated to scheduling and job management.
- Shop and warehouse rent, prorated across job volume.
- Small tool and consumable allowances.
SG&A (period expenses, not job costs):
- Owner's W-2 wages at market rate for their working role.
- Marketing spend.
- Software subscriptions.
- Training and licensing costs.
- Administrative staff whose time doesn't touch field operations.
I ran service calls at Caldera Heating and Cooling in Sunnyvale, and before that at Bayview Mechanical. The commercial division at Caldera had job costing discipline because the general contractors required certified payroll and job cost reporting on every project — pull the documentation or the retention check doesn't come. Residential shops don't have that external pressure. The owners I work with are not behind because they failed to pay attention. They are behind because the commercial job-cost requirement never landed on their side of the industry.
What to Do Before You Price Another Job This Week
One. Pull three completed jobs from the last 90 days — one service call, one equipment replacement, one new installation. The misclassification pattern is usually different across job types. Go line by line through each job record and reclassify every cost against the structure above. You are not restating your financials. You are finding where the miscoding happens and how consistently it repeats. Three jobs will show you the pattern.
Two. Inside those same job records, compare labor hours logged to hours billed. A tech logs 3.5 hours on-site. The invoice shows 2.0 hours billed because the price book caps labor at a flat rate for that repair category. The difference disappears as absorbed cost, never surfacing as a variance. If that gap averages one hour across 12 service calls per day, you are absorbing roughly $28,000 to $35,000 per year in unrecovered labor at typical burden rates for this market — it doesn't appear anywhere on the P&L as a line item, which is exactly why it persists.
Three. Calculate your actual burden rate for one technician. Base wage plus FICA, workers' comp at your actual premium rate, health contribution, accrued PTO. Divide by projected billable hours for the year — not total hours, billable hours. Compare to what your price book assumes. In my engagements, fewer than one in five shops finds those numbers within 10% of each other. If yours don't match, every job priced using the book carries a built-in error equal to the difference.
Four. Do the overhead math. Total indirect costs for last year, divided by last year's actual billable hours. Add that overhead burden per billable hour to your burdened labor rate. That combined number is what an hour of field time actually costs before you touch margin.
When you have those four numbers in front of you, look at your service call rate and your install pricing. You will either confirm the pricing is sound, or you will see exactly where the gap is.
FAQ
If my accountant set up my chart of accounts, why would it be wrong for job costing?
Your accountant built it to produce an accurate tax return. That is a different document with a different purpose than a job-cost report. The structure that satisfies the IRS often lumps operational costs in ways that obscure job-level profitability — and that is a predictable outcome of a general-practice CPA optimizing for the objective they were hired to meet. You need a second classification layer organized around how you produce revenue, not how you report income. Most shops I work with run these in parallel without touching the tax return at all.
What's the difference between job-level gross margin and company gross margin?
Job-level gross margin tells you whether individual jobs are priced correctly relative to their direct costs. It drives pricing decisions and job-mix decisions. Company gross margin is what remains after all direct costs are subtracted from total revenue, and it tells you whether the business model works at scale. A shop can have strong job-level margins and weak company gross margin if overhead is too high. It can also show apparently strong company gross margin while specific job types quietly lose money. You need both views because they answer different questions.
My flat-rate book bundles labor and materials into one price — how do I separate them for COGS?
Start from your own cost structure. For any flat-rate ticket, you know what materials you used at landed cost, and you know the actual hours the tech spent. Price the materials first. Calculate the labor cost at your fully burdened rate for actual hours. The difference between those two figures and the total ticket price is your gross margin on that job. If your actual costs exceed the book's implied assumptions, the book is underpricing your work.
How do I handle a tech's time when the day is split between billable calls, drive time, a shop meeting, and a warranty callback?
Each category gets its own time code, tracked daily. Billable hours are direct COGS. Non-billable drive time feeds your vehicle burden calculation in overhead. Shop meetings and training are period expenses in SG&A. Warranty callback hours are the most important to track — code them back to the original job if your system allows it, or at minimum run them as a discrete warranty labor category. If warranty labor never appears as its own number, you are systematically understating the cost of the work that generated it.
Do I have to restructure my chart of accounts, or can I run this separately?
Run it separately first. Build a management P&L in a spreadsheet using the correct classification structure, fed by the same underlying numbers your bookkeeper already captures. Two or three quarters of that, and you will know whether the classification logic is stable enough to be worth restructuring the chart of accounts with your bookkeeper. The tax return and the management accounts serve different purposes. They can coexist indefinitely.
Enjoyed this article?
Get articles like this in your inbox every Monday. Free, no spam.
