If you bill services by milestone in Odoo, this guide builds a report that shows how much each project should become invoiceable, month by month, based on each milestone's deadline. Standard Odoo has no such report, and a milestone stores a percentage rather than an amount. The fix is one stored computed field on the milestone that multiplies its percentage by the value of the linked sales order line, followed by a pivot of that field by deadline month.
The guide is written for finance and operations users, with one short line of formula for whoever creates the field.
Before you start
- Version: the steps were verified on Odoo 19, and the parts they depend on were checked again on Odoo 20.
- Edition: the amount field works in Community and Enterprise. The pivot menu uses Studio, Odoo's built-in customization tool, which is part of Enterprise.
- Apps: Sales and Project. When both are installed, Odoo adds the link between them automatically.
- Access: an administrator account. Creating fields needs administration rights, and the Studio button only appears for users with Settings administrator rights.
- Milestones turned on: in the Project app, open a project's form and tick Milestones on its Settings tab.
- Terms: a milestone is a checkpoint inside a project with a deadline, which can be marked Reached. A Sales Order Item is one line of a sales order. Developer mode is Odoo's setting that shows technical menus and options to administrators.
Why the amount is not there already
When a service product uses the invoicing policy Based on Milestones, each milestone in the project is linked to a Sales Order Item and carries a Quantity (%). That percentage is the share of the line that counts as delivered once the milestone is marked Reached. Odoo adds up the percentages of the reached milestones, multiplies by the ordered quantity, and that delivered quantity becomes invoiceable on the order.
The milestone therefore knows its deadline and its share, but not its value in money. A pivot view, which is Odoo's table for summing figures by rows and columns, can add up a field but cannot multiply two fields while it groups. You need a field that already holds the amount.
Two details about the source numbers matter:
- Quantity (%) is stored as a fraction. The screen shows 25%, but the stored value is 0.25. Multiply by it directly and do not divide by 100.
- The line Subtotal excludes tax and reflects any line discount. It is also in the currency of that sales order.
Add the expected amount field
A computed field is a field Odoo fills in by itself from a formula. Making it stored means the result is saved, so it can be filtered, grouped and summed like any other field. Nothing needs to be installed.
- Open the Settings app, scroll to the bottom and click Activate the developer mode. The page reloads with technical menus available.
- Go to Settings > Technical > Database Structure > Fields and click New. An empty field form opens.
- Set the Field Name to something starting with x_, for example x_expected_amount. Odoo rejects custom field names that do not start with x_.
- Set the Field Label to Expected Amount, the Model to Project Milestone, and the Field Type to Float.
- On the Properties tab, check that Stored is ticked. It is ticked by default.
- On the Compute tab, enter these Dependencies: quantity_percentage, sale_line_id.price_subtotal
- In the Compute box, enter one line: for record in self: record['x_expected_amount'] = record.quantity_percentage * record.sale_line_id.price_subtotal
- Save.
On save, Odoo calculates the field for every existing milestone, so past and open milestones are filled straight away. After that, the Dependencies make Odoo recalculate a milestone whenever its percentage changes or the subtotal of its linked line changes, for example after a price or quantity edit on the order.
Build the pivot by deadline month
Standard Odoo gives milestones list, kanban and form views only, with no pivot or graph view, in Community or Enterprise. No menu opens them directly either: you reach them from inside each project. Studio can add both.
- Open the app where the report should live, such as Sales, and click the Toggle Studio button in the top bar. Studio opens on that app.
- Click Edit Menu, then New Menu. Name it, for example Expected Milestone Invoicing, choose Existing Model, and select Project Milestone.
- In Studio's Views tab for that menu, activate the Pivot view, then close Studio.
- Open the new menu and switch to the pivot. Set Project as the rows, Deadline grouped by Month as the columns, and Expected Amount as the measure.
- Open the search options and add a Custom Filter on Reached, so that only milestones not yet reached are counted. Save it as a favourite so the view opens filtered.
To put the result on a dashboard, click Insert in Spreadsheet from the pivot view and choose a new or existing spreadsheet.
Check that it worked
- In the Project app, open a project's card menu and, under View, click Milestones. For a billable project, the list shows each milestone's Sales Order Item and Quantity (%). Pick one that is not yet reached and note its percentage.
- Click View Sales Order on that milestone and note the Subtotal of the linked line.
- In the pivot, the milestone's project and deadline month should include that Subtotal multiplied by the percentage. For a 25% milestone on a line of 10,000, that is 2,500.
- Mark the milestone Reached and reload the pivot. Its amount should leave the report.
Spreadsheet formulas versus a stored field
A spreadsheet can produce the same figure without a custom field: insert a list of milestones, add a formula column that multiplies the percentage by the line amount, and summarise by month. The list reloads live data each time the spreadsheet opens. However, an inserted list only fetches as many records as there are rows that reference it, so milestones added later fall outside it until someone extends it, and the monthly totals miss them.
The stored field has no such limit. Every milestone carries its amount, whenever it was created, so for day-to-day use it is the safer choice.
Pitfalls to check before you rely on it
- Mixed currencies are summed together. The field takes each line's subtotal in that order's currency, and a Float field carries no currency. If you sell in more than one currency, a saved view per currency, using a custom filter on the currency of the Sales Order Item, should keep the totals apart.
- Amounts are before tax. The figure matches the untaxed value of the invoice line, not the total the customer pays.
- Milestones without a Sales Order Item show 0. With no linked line there is nothing to multiply. Link them or filter them out.
- Percentages above 100% are not blocked by the field. The milestone form shows a percentage below 0% or above 100% in red, but the field still calculates whatever is entered.
- Milestones with no deadline sit outside the months. The pivot groups them under an empty value, so they are missing from the monthly columns until a date is set.
Steps verified on Odoo 19. Menus and labels can differ on other versions.
Common questions
Does Odoo have a standard report of milestones to invoice by month?
No. In standard Odoo 19 and 20, project milestones have only list, kanban and form views, with no pivot or graph view and no menu of their own. A pivot needs a custom menu and view, which Studio in Odoo Enterprise can add.
Why can't an Odoo pivot multiply a milestone's percentage by the sales order line amount?
A pivot view sums or averages fields that already exist on the records it groups; it does not calculate the product of two fields. The amount has to be stored on the milestone first, which a stored computed field does.
How does Odoo store a milestone's Quantity (%)?
As a fraction. Odoo shows 25% on screen but stores 0.25, so multiplying it by the sales order line Subtotal gives the right amount without dividing by 100.
Does a computed milestone amount update when the sales order changes?
Yes, if the field is stored and its Dependencies list quantity_percentage and sale_line_id.price_subtotal. Odoo then recalculates a milestone when its percentage or the linked line's Subtotal changes.
Is the sales order line Subtotal in Odoo before or after tax?
Before tax. The Subtotal on a sales order line excludes tax, reflects any line discount, and is in the currency of that order.
Talk to the team.
If you are setting this up, or Odoo is not behaving the way this guide describes, book a 30-minute call. We will look at your configuration and tell you what it needs. Calgary studio, in-house team, no offshore handoffs.
Book the call →