INTRODUCTION

Your Consultants Are Busy. Your Billing Says Otherwise.

Every professional services firm we've onboarded shares the same complaint: the team is working at full capacity, but revenue doesn't reflect it. Timesheets are submitted late (or not at all), project managers don't know who's available next week, and invoices go out weeks after the work was delivered — often missing billable hours that were never logged.

The gap between work performed and revenue captured is called revenue leakage, and in professional services it typically runs between 5% and 15% of total billable hours. For a 50-person consultancy billing at $150/hour, that's $1.5M–$4.5M in annual revenue that simply evaporates because the operational systems can't keep up with the delivery team.

Odoo 19 addresses this with a tightly integrated Project + Timesheet + Invoicing + Planning stack that eliminates the manual handoffs where billable hours get lost. This guide walks through the complete configuration — from setting up billing models (T&M, fixed-price, retainer) to building profitability dashboards that show margin by project, client, and consultant in real time.

01

How to Configure Time & Materials, Fixed-Price, and Retainer Billing in Odoo 19

Professional services firms rarely use a single billing model. The same company might bill implementation projects on T&M, support contracts on retainer, and compliance audits at a fixed price. Odoo 19 handles all three through the Service Invoicing Policy on each product, combined with the project's billing configuration.

Billing ModelInvoicing PolicyRevenue RecognitionBest For
Time & MaterialsBased on TimesheetsAs hours are loggedConsulting, development, ad-hoc work
Fixed PriceBased on MilestonesAt milestone completionAudits, implementations, deliverable-based
RetainerPrepaid (Deferred Revenue)As hours are consumedManaged services, ongoing support

Time & Materials Setup

For T&M billing, the service product must be configured with "Based on Timesheets" as the invoicing policy. When a consultant logs time against a task linked to a sales order, Odoo automatically creates a draft invoice line for that time at the rate defined on the sales order line.

Python — Service product for T&M billing
# In your custom module's data or migration script
product_tm_consulting = self.env['product.product'].create({
    'name': 'Senior Consultant — T&M',
    'type': 'service',
    'service_type': 'timesheet',
    'service_tracking': 'task_in_project',
    'invoice_policy': 'delivery',  # "Based on Timesheets"
    'list_price': 175.00,
    'uom_id': self.env.ref('uom.product_uom_hour').id,
    'project_id': False,  # Create new project per SO
    'property_account_income_id': account_revenue.id,
})

# Create a sale order with T&M line
sale_order = self.env['sale.order'].create({
    'partner_id': client.id,
    'order_line': [(0, 0, {
        'product_id': product_tm_consulting.id,
        'product_uom_qty': 0,  # No fixed qty for T&M
        'price_unit': 175.00,
    })],
})
sale_order.action_confirm()

The critical setting is product_uom_qty: 0 on the sale order line. This tells Odoo there's no fixed quantity — the delivered quantity will be driven entirely by timesheet entries. The invoice amount grows as hours are logged.

Fixed-Price with Milestones

For fixed-price engagements, you want to invoice at specific milestones rather than by hours logged. Odoo 19's milestone feature lets you define billing triggers on tasks:

Python — Fixed-price milestone billing
# Service product for fixed-price work
product_fixed = self.env['product.product'].create({
    'name': 'ERP Implementation — Fixed Price',
    'type': 'service',
    'service_type': 'milestones',
    'invoice_policy': 'delivery',
    'list_price': 45000.00,
    'uom_id': self.env.ref('uom.product_uom_unit').id,
})

# Create milestones on the project
project = self.env['project.project'].create({
    'name': 'Acme Corp ERP Implementation',
    'allow_milestones': True,
})

milestones = [
    ('Discovery & Gap Analysis', 30),   # 30% of total
    ('Configuration Complete', 30),       # 30%
    ('UAT Sign-Off', 25),                 # 25%
    ('Go-Live & Hypercare', 15),         # 15%
]

for name, pct in milestones:
    self.env['project.milestone'].create({
        'name': name,
        'project_id': project.id,
        'sale_line_id': so_line.id,  # Link to SO line
        'quantity_percentage': pct / 100,
    })

When a project manager marks a milestone as reached, Odoo makes that percentage of the sale order line's amount available for invoicing. This gives you clear billing triggers tied to deliverables rather than arbitrary calendar dates.

Retainer / Prepaid Hours

Retainers require a different approach: the client pays upfront, and hours are consumed against the prepaid balance. Odoo 19 handles this through prepaid service products with deferred revenue accounting. When the client purchases a 40-hour support block, the revenue is deferred. As consultants log time, the deferred revenue is recognized proportionally.

Mixed Billing on One Project

A common scenario: the implementation phase is fixed-price, but change requests during the project are billed T&M. Odoo supports this by linking multiple sale order lines (each with a different billing policy) to the same project. Assign T&M tasks to the T&M SO line and milestone tasks to the fixed-price line. The invoicing engine handles both correctly.

02

Timesheet Entry, Approval Workflows, and Billing Rate Overrides in Odoo 19

Timesheets are the revenue engine of a professional services firm. Every hour that isn't logged is an hour that can't be billed. Odoo 19's timesheet module has improved significantly — with grid view for weekly entry, timer-based tracking, manager approval workflows, and automatic billing rate computation.

Configuring the Approval Workflow

Enable timesheet approval under Timesheets → Configuration → Settings → Timesheet Approval. Once enabled, submitted timesheets require manager validation before they can be invoiced. This prevents billing errors from reaching the client.

Python — Automated timesheet reminder via scheduled action
from odoo import models, fields, api
from datetime import timedelta

class TimesheetReminder(models.TransientModel):
    _name = 'hr.timesheet.reminder'
    _description = 'Weekly Timesheet Reminder'

    @api.model
    def _send_missing_timesheet_reminders(self):
        """Cron job: remind employees who logged < 32h this week."""
        today = fields.Date.today()
        week_start = today - timedelta(days=today.weekday())

        employees = self.env['hr.employee'].search([
            ('company_id', '=', self.env.company.id),
            ('employee_type', '=', 'employee'),
        ])

        for emp in employees:
            logged = sum(
                self.env['account.analytic.line'].search([
                    ('employee_id', '=', emp.id),
                    ('date', '>=', week_start),
                    ('date', '<=', today),
                    ('project_id', '!=', False),
                ]).mapped('unit_amount')
            )

            if logged < 32.0:  # Less than 4 days logged
                template = self.env.ref(
                    'my_module.email_timesheet_reminder'
                )
                template.send_mail(emp.id, force_send=True)

        return True

Billing Rate Overrides by Employee or Role

Not every consultant bills at the same rate. A senior architect at $250/hour and a junior analyst at $95/hour may both work on the same project. Odoo 19 supports employee-level billing rate overrides through the timesheet_cost field and sale order line pricing. Configure rate tables under Project → Configuration → Settings → Pricing to set different rates per employee, per project, or per task type.

Rate StrategyConfigurationUse Case
Flat project rateSingle SO line, one priceSmall projects, blended rates
Per-role rateMultiple SO lines by seniorityEnterprise clients, rate card contracts
Per-employee rateEmployee mapping on projectStaff augmentation, named resources
Timer vs. Manual Entry

Odoo 19's built-in timer lets consultants start/stop tracking from the task view. In our experience, timer-based tracking captures 15–20% more billable hours than manual end-of-day entry. The reason is simple: when you fill in timesheets from memory at 5 PM, you forget the 30-minute client call and the 45-minute code review. Timers capture it as it happens.

03

Resource Planning and Allocation: Scheduling Consultants Across Projects in Odoo 19

The Planning module is where professional services firms gain the most operational visibility. It answers the question every delivery manager asks on Monday morning: "Who is available, and where should I put them?"

Odoo 19's Planning module provides a Gantt-style view of resource allocation across projects, with drag-and-drop scheduling, conflict detection, and capacity forecasting. Shifts (allocations) are tied to projects and tasks, so when a consultant is assigned to a project phase, it shows up in their personal schedule and the project's resource plan simultaneously.

Python — Automated resource allocation from sale order confirmation
from odoo import models, api
from datetime import timedelta

class SaleOrder(models.Model):
    _inherit = 'sale.order'

    def action_confirm(self):
        res = super().action_confirm()
        self._create_planning_shifts()
        return res

    def _create_planning_shifts(self):
        """Auto-create planning shifts when an SO is confirmed."""
        Planning = self.env['planning.slot']

        for line in self.order_line.filtered(
            lambda l: l.product_id.service_tracking
                      in ('task_in_project', 'project_only')
        ):
            project = line.project_id or line.task_id.project_id
            if not project:
                continue

            # Calculate shift duration from estimated hours
            hours = line.product_uom_qty
            days_needed = max(1, int(hours / 8))

            Planning.create({
                'project_id': project.id,
                'task_id': line.task_id.id if line.task_id else False,
                'resource_id': False,  # Unassigned — PM allocates
                'allocated_hours': hours,
                'start_datetime': fields.Datetime.now(),
                'end_datetime': fields.Datetime.now()
                    + timedelta(days=days_needed),
                'role_id': self._get_default_role(line).id,
                'state': 'draft',
            })

The workflow above creates unassigned planning shifts when a sale order is confirmed. The project manager then drags these into specific consultant calendars using the Planning Gantt view. This ensures no confirmed project falls through the cracks — every sale immediately creates a resource demand that needs to be fulfilled.

Capacity vs. Demand Dashboard

Navigate to Planning → Planning by Resource to see each consultant's allocation. Odoo 19 color-codes capacity: green (< 80% allocated), yellow (80–100%), red (> 100% overbooked). A delivery manager can spot overallocation instantly and redistribute work before it causes burnout or deadline slippage.

ConsultantThis WeekNext WeekWeek +2Status
Sarah K. (Sr. Consultant)40h / 40h32h / 40h16h / 40hBench risk in 2 weeks
Marco R. (Technical Lead)44h / 40h40h / 40h40h / 40hOverbooked
Leila M. (Jr. Analyst)24h / 40h8h / 40h0h / 40hAvailable
Planning vs. Timesheet: Different Purposes

Planning shows where people should be working. Timesheets show where they actually worked. Comparing the two surfaces a critical metric: schedule adherence. If a consultant was planned for 30 hours on Project A but timesheets show 18 hours on Project A and 12 hours on "internal meetings," you have a delivery risk that won't show up in either system alone.

04

Expense Management: From Receipt to Client Reinvoicing in Odoo 19

Professional services projects often include reimbursable expenses — travel, software licenses, subcontractor fees. Odoo 19 connects expenses to projects and sales orders so that client-billable expenses flow directly into the project invoice alongside timesheet-based charges.

The configuration chain is: Expense Product → Analytic Account (project) → Sale Order Line → Invoice. When a consultant submits a travel expense tagged to a project, and that project is linked to a sale order with a "reinvoice expenses" line, Odoo automatically adds the expense to the next invoice at cost or with a markup.

Reinvoicing PolicyConfigurationInvoice Amount
At costexpense_policy = 'cost'Exact amount paid
At sales priceexpense_policy = 'sales_price'Product list price (markup built in)
No reinvoicingexpense_policy = 'no'Internal cost only

Enable OCR receipt scanning under Expenses settings. Odoo 19's built-in OCR extracts the vendor, amount, date, and category from uploaded receipt photos — reducing manual data entry by 80%. The consultant snaps a photo of a dinner receipt, Odoo pre-fills the expense form, and the consultant just needs to tag the project and submit.

Approval Workflow for Expenses

Configure multi-level expense approval under Expenses → Configuration → Settings. For professional services, we recommend a two-tier flow: expenses under $500 require only the project manager's approval, while expenses above $500 also require finance team sign-off. This balances speed (consultants aren't blocked on small travel claims) with control (large expenses get financial oversight).

Once an expense report is approved, it creates a journal entry that debits the appropriate analytic account (the project). If the expense product is configured for reinvoicing, Odoo adds the amount to the project's "Costs to Re-invoice" section, making it available on the next client invoice. The entire chain — receipt upload to client billing — happens without a single spreadsheet.

Mileage and Per-Diem Expenses

For consultants who travel frequently, configure mileage and per-diem expense products with fixed unit costs. Instead of uploading individual gas receipts, the consultant enters the distance traveled, and Odoo calculates the reimbursable amount at your company's rate (e.g., $0.67/km). This dramatically reduces expense report volume while maintaining accurate project costing.

05

Client Portal: Giving Clients Real-Time Project Visibility Without Email Chains

The number one cause of client dissatisfaction in professional services isn't poor work — it's lack of visibility. Clients want to know: How many hours have been used? Are we on track? What's the remaining budget? Without a portal, they ask via email, the PM compiles a spreadsheet, and the answer arrives 48 hours later.

Odoo 19's customer portal gives clients self-service access to:

  • Project tasks — status, assignee, and deadlines for every deliverable.
  • Timesheet entries — who worked on what, when, and for how long (if you enable timesheet visibility on the project).
  • Invoices and payments — current billing status and payment history.
  • Support tickets — if using Helpdesk, clients can submit and track tickets directly.
  • Documents — shared project files and deliverables.

To enable timesheet visibility for portal users, navigate to Project → Configuration → Settings and enable "Timesheets on Customer Portal". Then on each project, toggle the "Timesheets" portal visibility option. Clients will see a summary of hours logged per task without needing to request a status report.

Portal Permissions Are Granular

You don't have to show everything. Some firms show task status but hide timesheet details. Others show invoices but not individual time entries. Configure portal visibility per project — an implementation project might have full transparency while an internal R&D project shows only milestones. The settings are on the project form under the "Settings" tab.

06

Profitability Analysis and Revenue Forecasting for Services Firms in Odoo 19

Revenue is vanity, margin is sanity. A project that bills $200,000 but consumes $190,000 in consultant time and expenses isn't a success — it's a warning sign. Odoo 19's Project Profitability view aggregates revenue (invoiced + uninvoiced timesheets) against costs (employee cost rate x hours + expenses) to show margin at the project, client, and portfolio level.

Navigate to Project → All Projects and click the Profitability button on any project. Odoo displays:

  • Revenues — invoiced amount, amount to invoice (uninvoiced timesheets), and other revenue.
  • Costs — employee timesheet costs (internal rate x hours), expense costs, purchase order costs, and other costs.
  • Margin — revenue minus costs, shown as both an absolute amount and a percentage.
Python — Custom profitability report by client
from odoo import models, fields, api

class ClientProfitabilityReport(models.TransientModel):
    _name = 'ps.client.profitability.report'
    _description = 'Client Profitability Report'

    date_from = fields.Date(required=True)
    date_to = fields.Date(required=True)
    partner_ids = fields.Many2many('res.partner')

    def action_generate(self):
        domain = [
            ('date', '>=', self.date_from),
            ('date', '<=', self.date_to),
        ]
        if self.partner_ids:
            domain.append(
                ('partner_id', 'in', self.partner_ids.ids)
            )

        timesheets = self.env['account.analytic.line'].search(
            domain + [('project_id', '!=', False)]
        )

        report_data = {}
        for ts in timesheets:
            partner = ts.project_id.partner_id
            if not partner:
                continue
            key = partner.id
            if key not in report_data:
                report_data[key] = {
                    'partner': partner.name,
                    'hours': 0.0,
                    'revenue': 0.0,
                    'cost': 0.0,
                }
            report_data[key]['hours'] += ts.unit_amount
            report_data[key]['revenue'] += (
                ts.unit_amount * ts.so_line.price_unit
                if ts.so_line else 0.0
            )
            report_data[key]['cost'] += (
                ts.unit_amount * ts.employee_id.timesheet_cost
            )

        # Calculate margin for each client
        for data in report_data.values():
            data['margin'] = data['revenue'] - data['cost']
            data['margin_pct'] = (
                (data['margin'] / data['revenue'] * 100)
                if data['revenue'] > 0 else 0.0
            )

        return report_data

Forecasting Revenue from the Pipeline

Combine CRM pipeline data with resource planning to forecast revenue. Every qualified opportunity with an estimated value and expected close date represents future resource demand. When that opportunity converts to a sale order, the planning shifts created at confirmation (from section 03) immediately show the impact on capacity. This creates a closed-loop forecast: sales pipeline → resource demand → capacity constraint → hiring decision.

Key metrics to track on your executive dashboard:

  • Utilization rate — billable hours / total available hours. Target: 70–80%.
  • Realization rate — invoiced revenue / (billable hours x standard rate). Measures discounting and write-offs.
  • Revenue per FTE — total revenue / headcount. Industry benchmark: $150K–$250K for mid-market firms.
  • Backlog — contracted but undelivered revenue. Shows how many weeks of work are secured.
Python — Utilization rate computation via scheduled action
class UtilizationCompute(models.Model):
    _inherit = 'hr.employee'

    utilization_rate = fields.Float(
        string='Utilization Rate (%)',
        compute='_compute_utilization',
        store=True,
    )

    @api.depends('timesheet_ids', 'resource_calendar_id')
    def _compute_utilization(self):
        today = fields.Date.today()
        month_start = today.replace(day=1)

        for emp in self:
            # Total available hours from work schedule
            calendar = emp.resource_calendar_id
            if not calendar:
                emp.utilization_rate = 0.0
                continue

            available = calendar.get_work_hours_count(
                datetime.combine(month_start, time.min),
                datetime.combine(today, time.max),
                resource=emp.resource_id,
            )

            # Billable hours from timesheets
            billable = sum(
                self.env['account.analytic.line'].search([
                    ('employee_id', '=', emp.id),
                    ('date', '>=', month_start),
                    ('date', '<=', today),
                    ('project_id.allow_timesheets', '=', True),
                    ('so_line', '!=', False),  # Linked to SO = billable
                ]).mapped('unit_amount')
            )

            emp.utilization_rate = (
                (billable / available * 100)
                if available > 0 else 0.0
            )

This computed field gives you a live utilization percentage on every employee record. Build a Pivot or Dashboard view on hr.employee grouped by department to see which teams are running hot and which have capacity. Combine this with the Planning module's forward-looking allocation data to distinguish between "utilized now" and "booked for next month."

07

3 Configuration Mistakes That Cause Revenue Leakage in Odoo Professional Services

1

Timesheets Not Linked to a Sale Order Line

Consultants log time against a project, the project manager sees the hours, but when it's time to invoice — the hours don't appear on the invoice. The root cause: the project task isn't linked to a sale order line. This happens when tasks are created manually on the project instead of being auto-generated from the sale order confirmation. The timesheets exist, but Odoo's invoicing engine doesn't know they're billable.

Our Fix

Always create projects and tasks from the sale order, not manually. Set service_tracking = 'task_in_project' on your service products so that confirming the SO automatically generates the project and tasks with the correct SO line linkage. For ad-hoc tasks, use the "Create on Tasks" mapping on the project to assign them to an existing SO line.

2

Employee Cost Rate Set to Zero (or Never Set)

The profitability report shows 100% margin on every project. That sounds great until you realize the employee cost field is $0 for every consultant. Odoo calculates project cost as hours x employee_cost_rate. If the cost rate was never configured, every project looks infinitely profitable — and your margin reporting is useless. You'll make staffing decisions based on fictional data.

Our Fix

Set the Timesheet Cost field on every employee record (HR → Employees → employee form → Settings tab). This should reflect the fully-loaded cost: salary + benefits + overhead, divided by available hours per year. For a consultant earning $120K/year with 30% overhead, that's approximately ($120K x 1.3) / 1,800 hours = $86.67/hour.

3

Retainer Hours Consumed but Deferred Revenue Not Recognized

A client buys a 100-hour prepaid block. Your team logs 40 hours against it. But on the P&L, all 100 hours of revenue appear as recognized income in the month of sale. The issue: deferred revenue recognition wasn't configured on the retainer product. This creates artificially high revenue in the sale month and artificially low revenue in delivery months — distorting monthly financial reporting and potentially causing compliance issues under ASC 606 / IFRS 15.

Our Fix

On the retainer service product, set the Deferred Revenue Account (under the Accounting tab). Then configure a Revenue Recognition rule that ties recognition to timesheet consumption. As consultants log hours against the retainer, revenue moves from the deferred account to the income account proportionally. This matches revenue to the period in which the service was delivered.

BUSINESS ROI

What Integrated Project Billing Saves Your Services Firm

The financial impact of connecting timesheets, billing, and resource planning in a single system:

12%Revenue Recovery

Linking timesheets directly to sale orders eliminates the "forgot to log it" and "task wasn't billable" leakage. Firms typically recover 8–15% of previously lost billable hours.

3 daysFaster Invoice Cycle

Invoices generated from approved timesheets in two clicks instead of compiled from spreadsheets over a week. Faster billing means faster payment — improving cash flow by 15–20 days on average.

85%Utilization Visibility

Real-time resource planning shows who's available before the PM sends a Slack message asking. Bench time becomes visible and actionable, reducing unbillable gaps between projects.

The compounding effect is what matters most. When timesheets are submitted on time because reminders are automated, invoices go out faster because they're generated from approved timesheets, and resource gaps are filled sooner because planning shows availability in real time — the entire revenue cycle accelerates. One firm we migrated to this setup reduced their average days-sales-outstanding (DSO) from 52 days to 31 days within two quarters.

SEO NOTES

Optimization Metadata

Meta Desc

Configure Odoo 19 for professional services: T&M, fixed-price, and retainer billing. Timesheet approval, resource planning, expense reinvoicing, and profitability analysis.

H2 Keywords

1. "How to Configure Time & Materials, Fixed-Price, and Retainer Billing in Odoo 19"
2. "Timesheet Entry, Approval Workflows, and Billing Rate Overrides in Odoo 19"
3. "Resource Planning and Allocation: Scheduling Consultants Across Projects in Odoo 19"
4. "3 Configuration Mistakes That Cause Revenue Leakage in Odoo Professional Services"

Stop Leaving Revenue on the Table

Every billable hour that goes unlogged, every invoice that ships late, every project where margin is unknown until the post-mortem — these are symptoms of disconnected systems. Spreadsheets for planning, a separate tool for timesheets, and manual invoice compilation create gaps where revenue disappears.

If you're running a professional services firm on Odoo 19 (or considering it), we can help. We configure the full Project + Timesheet + Invoicing + Planning stack, set up billing models that match your contracts, build profitability dashboards for your leadership team, and train your consultants on timesheet workflows that actually get adopted. The result is a system where every hour worked flows automatically to an invoice.

Book a Free Services Operations Assessment