Most support billing on Odoo starts with a spreadsheet somewhere. A tab with client names, prepaid hour balances, and a running total that someone keeps current — until they forget, until two people update it simultaneously, or until a client asks for a full breakdown and you are staring at cell history.

Odoo can replace this entirely. Sales Orders handle the prepaid billing. The Project module handles ticket tracking and time logging. The balance is live and auditable at all times.

There is one catch. Standard Odoo CE has a behavior in its billing engine that silently corrupts your audit trail the moment you add a new batch of hours to a client's account — retroactively reassigning all past timesheet entries to the new Sales Order as if the old one never happened. This guide covers how to close that gap, and how to build the full workflow around it: ticket intake, time tracking, client portal access, and balance monitoring.

Architecture: The Wallet Model

The model rests on three standard Odoo relationships:

  1. A confirmed, upfront-invoiced Sales Order represents the prepaid batch — the wallet. Each line on the SO is a product called "Support Hour" at a fixed quantity.
  2. The Project's Sales Order Item field (sale_line_id) points at the active SO line.
  3. Every timesheet entry logged against a task in that project increments qty_delivered on the SO line.

The balance is product_uom_qty − qty_delivered — hours sold minus hours delivered. No custom fields. No scripts. Odoo tracks it natively as long as the SO line is wired correctly.

+---------------------------------+
|  Sales Order (SO)               |  <- The Wallet
|  Product: Support Hour          |
|  Qty: 8.0  -  invoiced upfront  |
+----------------+----------------+
                 | Project > Settings > Sales Order Item
                 v
+---------------------------------+
|  Project: [Client] - Support    |  <- The Delivery Vehicle
|  Issues / Tasks                 |
+----------------+----------------+
                 | Timesheets increment qty_delivered
                 v
+---------------------------------+
|  account.analytic.line          |  <- Hours logged here
|  (timesheet entries)            |     reduce the SO balance
+---------------------------------+

Three StrictBase modules complete the picture:

Module Purpose
strictbase_task_sol_lock Prevents historical timesheet entries from being retroactively reassigned when a batch is reloaded. Without this, switching the SO line on a task silently corrupts all past data.
strictbase_support_project_overview Adds a backend reporting view with live wallet balances across all support clients, sorted by remaining hours. Includes alert filters for low and negative balances.
strictbase_project_portal_readonly Adds a strict read-only client portal mode. Clients land on a Support Overview page showing tickets, time spent, and remaining hours. No task editing.
Part 1

One-Time Setup

These steps establish the foundational elements. You do this once per Odoo instance.

Step 1: Install Core Odoo Modules

Ensure the following applications are installed: Sales, Invoicing, Project, Timesheets.

Step 2: Install Automation Rules

The Automation Rules module (base_automation) is hidden in Odoo CE behind the Apps filter. To find it:

  1. Activate Developer Mode: Settings → Activate the developer mode.
  2. Go to Apps.
  3. In the search bar, click the × next to the "Apps" filter to remove it.
  4. Search for base_automation and install the module named Automation Rules.

From the CLI:

./odoo-bin -c odoo.conf -d your_db -i base_automation --stop-after-init

Step 3: Install the Three StrictBase Modules

Install order matters

Install strictbase_task_sol_lock before any batch reloads take place. Once a reload has been done without the module, historical timesheet entries on those tasks may already be on the wrong SO line. The module cannot retroactively correct past corruption — it only prevents future occurrences.

./odoo-bin -c odoo.conf -d your_db \
  -i strictbase_task_sol_lock,strictbase_support_project_overview,strictbase_project_portal_readonly \
  --stop-after-init

Step 4: Configure the Service Product

This product represents the prepaid bucket of time sold to clients.

  • Go to Sales → Products → Products → New.
  • Name: Support Hour
  • Product Type: Service
  • Invoicing Policy: Prepaid/Fixed Price — bills upfront, not based on delivered quantity.
  • Create on Order: Nothing — the Sales Order itself is the wallet; no dummy task needed per sale.
  • Unit of Measure: Hours
  • Set your hourly rate in Sales Price.

Step 5: Create a Project Template

Standardise the project structure so every new client project starts identically.

  • Go to Project → Projects → New.
  • Name: _Template_Client_Support
  • In project settings, enable Timesheets and Billable.
  • Create task stages: New, Triaged, In Progress, Waiting Client, Done.
  • Set Visibility to All internal users and invited portal users.
  • In project settings, rename "Tasks" to Issues.
  • Tag the template with Support Tickets (see Step 6). Duplicated client projects inherit this tag, which is how strictbase_support_project_overview identifies support projects. The is_template flag prevents the template itself from appearing in the reload overview.

Step 6: Define Project Tags

Go to Project → Configuration → Tags and create:

  • Support Tickets — required by strictbase_support_project_overview to detect support projects. Apply this to every real client support project.
  • Bug
  • Feature Request
  • Maintenance

Issue tags (Bug, Feature Request, Maintenance) are classification only — they do not affect billing.

Step 7: Configure the Auto-Close Automation Rule

This rule ensures that dragging a task to the Done column actually closes it (sets its internal state to Done, which is what the client portal uses to distinguish current from past tickets).

  • Go to Settings → Technical → Automation → Automation Rules → New.
  • Name: Project: Auto-Close Task in Done Stage
  • Model: Task (project.task)
  • Trigger: Stage is set to → select your Done stage(s)
  • Actions To Do: Add an action → Update Record → Field: State (Internal State) → Value: Done
  • Save.
Part 2

Client Onboarding

Step 1: Create the Project

  • Duplicate _Template_Client_Support and rename to [Client Name] - Support.
  • Set the Customer field on the project.

Step 2: Create Contacts and Grant Portal Access

For each person at the client who needs visibility into tickets:

  1. Go to Contacts, open the client company, and create a child contact. Fill in at minimum: Name, Email, and confirm it is linked to the correct company.
  2. Open the contact → Action → Grant Portal Access. Odoo sends the standard invitation email. The contact sets their password via the invite link.
UX note

For read-only support users, strictbase_project_portal_readonly hides the Name field on the signup form — it is not relevant in this flow and caused confusion in practice. The invite link is what activates the account.

Step 3: Share the Project

  • Open the client support project → Share Project.
  • Add the contact and set Access Mode to Read-only (all tasks).

This mode is added by strictbase_project_portal_readonly. It gives the contact access to all tasks in the project — including closed ones — enforced at the record-rule level, not just the frontend. After login, the contact is redirected to the Support Overview portal page rather than the generic Odoo portal.

To give multiple contacts access to the same project, repeat the share step for each. Multiple read-only collaborators on the same project are fully supported.

Step 4: Initialise the First Batch

  1. Create a new Sales Order for the client.
  2. Add a line: Support Hour, quantity = agreed hours (e.g., 8.0), unit price = your rate.
  3. Confirm and invoice the SO.
  4. Go to Project → Settings on the client's support project.
  5. Set Sales Order Item to the new SO line.
  6. Save.

From this point, every timesheet entry on any task in that project deducts from the wallet.

Migrating an Existing Client from a Spreadsheet

If the client has an existing prepaid balance tracked elsewhere, maintain audit continuity with a zero-price migration order:

  1. Add a final freeze note to the spreadsheet in bold red (e.g., Continued in Odoo as of [date]). Export a PDF copy and attach it to the project description in Odoo (Project → Description → Upload a file).
  2. Create a new Sales Order: Support Hour, quantity = remaining hours from the spreadsheet, unit price 0.00. Confirm — do not invoice.
  3. Set the project's Sales Order Item to this migration SO line.

Odoo now starts counting down from the migrated balance. The zero-price SO preserves the audit trail without triggering a billing event.

Part 3

The Batch Reload Problem

When a client's prepaid hours run out, you sell them a new batch and point the project at the new Sales Order. The intent is clear: only future timesheet entries should count against the new batch. Historical entries stay on the old SO.

Odoo CE does not honour this intent out of the box.

What Odoo Actually Does

The so_line field on timesheet entries (account.analytic.line) is a stored computed field:

# sale_timesheet/models/hr_timesheet.py
@api.depends('task_id.sale_line_id', 'project_id.sale_line_id',
             'employee_id', 'project_id.allow_billable')
def _compute_so_line(self):
    for timesheet in self.filtered(
        lambda t: not t.is_so_line_edited and t._is_not_billed()
    ):
        timesheet.so_line = (
            timesheet.project_id.allow_billable
            and timesheet._timesheet_determine_sale_line()
        )

When task.sale_line_id changes, the ORM dependency system marks every linked timesheet entry as stale and fires _compute_so_line() on all of them. The compute skips only two categories:

  • is_so_line_edited = True — entries where a user manually overrode the SO line.
  • _is_not_billed() returns False — entries with an active invoice (timesheet_invoice_id set).

In the prepaid wallet model, the Sales Order is invoiced upfront as a lump sum. Individual timesheet entries are never individually invoiced — timesheet_invoice_id stays empty on every entry, permanently. So _is_not_billed() always returns True for all of them, and the guard never fires.

The result: the moment you update the open tasks to point at the new SO, every historical timesheet entry — including those logged months ago against the old batch — gets silently reassigned to the new one. The old SO balance changes retroactively. There is no warning. There is no log entry. The corruption is silent.

How strictbase_task_sol_lock Fixes It

project.task.write() is overridden. When sale_line_id is in the write values and the task already has an SO line set (a switch, not an initial assignment), the module:

  1. Finds all timesheet entries on the affected tasks where is_so_line_edited = False — exactly the set the compute would touch.
  2. Sets is_so_line_edited = True on those entries before calling super().
  3. Calls super().write(vals). The ORM fires the compute, which now skips every locked entry.

Timesheet entries created after the switch start with is_so_line_edited = False and pick up the new SO line normally via the compute. No process change required. The fix is invisible in normal use.

The is_so_line_edited flag already existed in Odoo for this exact purpose (manual per-entry overrides). The module uses it as designed — it just sets it preemptively, before the compute runs.

The Full Reload Procedure

Prerequisite

strictbase_task_sol_lock must be installed before performing a reload. See Part 1, Step 3.

Step 1: Freeze and Audit the Old Batch

Check the old Sales Order (e.g., SO001). Note the final balance including any over-delivery. Example: 10.0 hours delivered against 8.0 hours sold = −2.0 hours. This debt carries forward to the new batch.

Step 2: Sell and Invoice the New Batch

Create a new Sales Order (e.g., SO055): Support Hour, quantity = agreed hours, confirm and invoice.

Step 3: Carry Over Any Debt

If the old batch ended in a negative balance, deduct the debt from the new batch immediately — before any new work is logged against it.

  • Create or open a dedicated [Adjustment] Balance carry-over task on the project.
  • Add a timesheet line on this task:
    • Description: Carry-over of 2.0 over-delivered hours from previous batch (SO001)
    • Duration: 2.0 (the over-delivered amount)
    • Sales Order Item: the new batch (SO055)

The new batch now starts at 6.0 hours remaining (8.0 sold − 2.0 carry-over). The old batch is closed at its actual delivered figure.

Step 4: Update Project Settings

Go to Project → Settings on the client's project. Change Sales Order Item to SO055. Save. New tasks created from this point will automatically pick up SO055.

Step 5: Migrate Open Tasks

Existing open tasks still point at SO001. Work logged against them today should come out of the new wallet.

  1. Switch to the project's List View.
  2. Filter by Is Open to hide Done tasks.
  3. Select all open tasks.
  4. Action → Update Sales Order Item → select SO055.

At the moment this write is executed, strictbase_task_sol_lock intercepts the call: all existing timesheet entries on the affected tasks are locked to their current SO line before the switch takes effect. SO001 is frozen at its final balance and will not change. SO055 starts at the carry-over amount and counts down from new entries only.

Inspecting individual timesheet entries

To see or correct which SO line a specific timesheet entry is attributed to: open the task → Timesheets tab → click the ⚙ (optional columns) icon at the right of the table header → enable SO Item. The column is hidden by default.

Part 4

Daily Workflow

Creating a Ticket Manually

  1. Go to Project → Tasks → New.
  2. Set Project to the client's support project.
  3. Enter a clear title (e.g., Fix login error on mobile).
  4. Set Assignee and Tags (Bug / Feature Request / Maintenance).
  5. The Sales Order Item field is automatically filled from the project's active batch — no manual selection needed.

Ticket Intake via Email

To have client emails automatically create tasks, configure an email alias on the project: Project → Settings → Email → Create tasks by sending email to [alias].

Emails sent to that address create tasks in the project's inbox. A project manager reviews the queue to check assignee, tags, and priority before the ticket enters active work.

Logging Time

  • Open the task → TimesheetsAdd a line.
  • Set Duration. Minimum: 0.25 hours (15 minutes) — this reflects the context-switching overhead inherent in support work.
  • Odoo increments qty_delivered on the linked SO line immediately. The balance updates in real time.

Internal Notes vs. Client Messages

  • Log note — internal only. Use for team communication and @-mentions. Not visible to portal users.
  • Send message — visible to the client via the portal. Use for status updates and questions directed at the client.

Closing a Ticket

Drag the task to the Done column. The Auto-Close automation rule (Part 1, Step 7) fires and sets the internal state to Done. The task moves from "Current Tickets" to "Past Tickets" in the client portal automatically.

Part 5

Visibility and Monitoring

Client Portal: Support Overview

Portal users shared with Read-only access bypass the generic Odoo portal and land directly on the Support Overview page (strictbase_project_portal_readonly).

The page shows, per project:

  • Remaining support hours — prominently displayed, derived from the active SO line
  • Current tickets table: ticket name (linked), stage, time spent
  • Past tickets table: same structure

If the user has read access to multiple support projects, a project selector appears at the top.

Clicking any ticket opens a read-only detail view showing the ticket description and the full work log (date, description, hours per entry).

An Export to Excel button downloads an .xls file containing the overview summary and the full ticket/worklog breakdown across all shared projects.

Backend: Support Reload Overview

Project → Reporting → Support Reload Overview (strictbase_support_project_overview) gives an operational view across all active support clients.

The list is scoped to projects tagged Support Tickets and sorted by remaining hours ascending — the projects closest to running out appear first.

Columns: Project, Customer, Active SO Item, Ordered, Delivered, Remaining. The Remaining column is colour-coded:

  • Red — balance is at or below zero. The client is in debt. A new batch is overdue.
  • Yellow — fewer than 4 hours remaining. Reload is imminent.

Three search filters are added to the standard project search:

  • Support Ticket Projects — all projects tagged Support Tickets
  • Low Support Balance — remaining hours below 4
  • Needs Reload — remaining hours at or below zero

A weekly check of this view is enough to catch negative balances before they go unnoticed. The Needs Reload filter makes the list actionable: every project that appears there needs a new SO created and the reload procedure from Part 3 run before the next timesheet entry is logged.