# Odoo 19 to 20 Sales migration: a guide for custom modules

> A release that reshapes the invoicing and re-invoicing plumbing around Sales, retires several sales-team KPI and template shortcuts, and reworks a number of order-level actions, so most custom-module breakage will cluster around billing logic and stored computed fields rather than the UI.

Odoo 20 was released on 24 September 2026. This guide covers Odoo 19 → 20 Sales, **Community Edition**, built from the released version; Enterprise modules are not included in these counts. It counts 1,252 verified changes across 25 core modules.

Start by mapping where your module touches the money side of Sales: anything that reads timesheet-to-invoice links, sales-team revenue targets, re-invoicing policy on products, or the order-level 'update prices/taxes' buttons is the most likely to break. Once you know those touchpoints, work outward to the smaller field renames and helper removals, which are usually a quick find-and-replace.

A fair number of the changes are in invoicing and computed-value territory, so give your compute methods and any stored fields that depended on the removed ones a close read before trusting a silent upgrade.

## What needs the closest look

### Timesheet lines no longer carry their invoice link

*Removed field — `account.analytic.line.timesheet_invoice_id`*

The direct pointer from an analytic/timesheet line back to the invoice it generated has been dropped. If your module reads this to trace which invoice billed a timesheet, or filters/groups on it, you'll need to derive that relationship another way through the sale order line or move it depended on.

Present in Odoo 19, **removed in 20**:

```python
timesheet_invoice_id = fields.Many2one('account.move', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet", index='btree_not_null')
```

### Billable-type classification on timesheets is gone

*Removed field — `account.analytic.line.timesheet_invoice_type`*

The stored selection that labelled a timesheet line by its billing nature has been removed along with its compute. Any reports, filters or automations keying off this 'billable type' will need to be reworked, since the value is no longer maintained on the line.

Present in Odoo 19, **removed in 20**:

```python
timesheet_invoice_type = fields.Selection(TIMESHEET_INVOICE_TYPES, string="Billable Type",
            compute='_compute_timesheet_invoice_type', compute_sudo=True, store=True, readonly=True)
```

### Sales-team 'invoiced this month' figure removed

*Removed field — `crm.team.invoiced`*

The computed monthly invoiced revenue used on the sales team kanban is gone. If your module surfaced this KPI or built dashboards on it, recompute it yourself from the invoices rather than reading the field.

Present in Odoo 19, **removed in 20**:

```python
invoiced = fields.Float(
        compute='_compute_invoiced',
        string='Invoiced This Month', readonly=True,
        help="Invoice revenue for the current month. This is the amount the sales "
                "channel has invoiced this month. It is used to compute the progression ratio "
                "of the current and target revenue on the kanban view.")
```

### Sales-team invoicing target removed

*Removed field — `crm.team.invoiced_target`*

The manual monthly revenue target that paired with the invoiced figure has also been dropped. Custom views or progression-ratio logic that referenced the target will need to store and read that goal from your own field.

Present in Odoo 19, **removed in 20**:

```python
invoiced_target = fields.Float(
        string='Invoicing Target',
        help="Revenue Target for the current month (untaxed total of paid invoices).")
```

### The 'product is in this sale order' catalog flag is gone

*Removed field — `product.product.product_catalog_product_is_in_sale_order`*

The helper boolean (with its search method) that told the product catalog whether a product already appeared on the current order has been removed. If you reused it to drive catalog UI or domains, replace it with your own check against the order's lines.

Present in Odoo 19, **removed in 20**:

```python
product_catalog_product_is_in_sale_order = fields.Boolean(
        compute='_compute_product_is_in_sale_order',
        search='_search_product_is_in_sale_order',
    )
```

### Re-invoice cost policy moved off this field

*Removed field — `product.template.expense_policy`*

The 'Re-Invoice Costs' selection on the product template has been removed as part of reshaping how expense/cost re-invoicing is configured. Anywhere your module reads or writes this policy needs revisiting against the new re-invoicing setup rather than this field.

Present in Odoo 19, **removed in 20**:

```python
expense_policy = fields.Selection(
        selection=[
            ('no', "No"),
            ('cost', "At cost"),
            ('sales_price', "Sales price"),
        ],
        string="Re-Invoice Costs", default='no',
        compute='_compute_expense_policy', store=True, readonly=False,
        help="Validated expenses, vendor bills, or stock pickings (set up to track costs) can be invoiced to the customer at either cost or sales price.")
```

### Auto-purchase-on-sale service flag removed

*Removed field — `product.template.service_to_purchase`*

The company-dependent 'Subcontract Service' boolean that triggered an automatic RfQ when the product was sold is gone. If your logic branched on this to create purchase requests, you'll need to hook into the current subcontracting/purchase flow instead of this flag.

Present in Odoo 19, **removed in 20**:

```python
service_to_purchase = fields.Boolean(
        "Subcontract Service", company_dependent=True, copy=False,
        help="If ticked, each time you sell this product through a SO, a RfQ is automatically created to buy the product. Tip: don't forget to set a vendor on the product.")
```

## Also worth checking

**Invoicing and re-invoicing configuration.** Several settings and shortcuts around billing defaults have been retired. The company-level default sale order template and its settings counterpart, the automatic-invoice toggle, and the default invoice policy have all been removed from the configuration models, so any module that read these to seed new orders or invoices needs to source those defaults elsewhere. In the same spirit, the product template's re-invoicing visibility helper has been renamed from the old expense-policy name to a re-invoice-policy name — update any view attribute or override that referenced the old one. Sales order templates also lost their custom create override, so if you relied on template-creation side effects, verify your own create logic still fires.

**Sales-team revenue tracking.** Alongside the removed invoiced and target fields, the method that updated the invoicing target on the sales team is gone; drop any calls to it and manage targets through your own field if you still need them.

**Order and line actions.** The order-level actions to recompute prices and to recompute taxes have both been removed, so buttons or server actions in your module that called them will error — rebuild those recalculations against the current order recompute flow. On the line side, the undiscounted amount field, the translated product name field, and the helper that walked up to a line's parent section have all disappeared; anywhere you displayed the pre-discount total, showed a translated name, or grouped lines under their section header will need a new approach.

**Project and partner touchpoints.** The project method that assembled sale-item data for the project view has been removed, so custom project-to-sales widgets should be re-examined. The partner-side helper that decided whether the VAT number could still be edited is also gone; if you gated VAT editing on it, replace that guard with your own check.

Beyond these, 480 smaller internal changes exist — only relevant if you override Odoo's private helpers or compute methods.

## Your migration checklist

1. Grep your codebase for the removed billing fields — the timesheet invoice link and billable type, the sales-team invoiced and target fields, the product re-invoice and subcontract-service flags — and list every read, write, view and domain that touches them.
2. Fix the configuration defaults first: replace uses of the removed company sale order template, automatic-invoice toggle and default invoice policy with the new re-invoicing setup, and rename the product template's re-invoice-policy visibility reference.
3. Rework any code calling the removed order actions (update prices, update taxes) and the removed sales-team target update method against the current recompute flow.
4. Repair order-line usages of the undiscounted amount, translated product name and parent-section helper, providing your own computations where needed.
5. Revisit project and partner integrations that used the removed sale-items data method and the VAT-edit guard.
6. Run your full test suite on Odoo 20 and exercise every affected workflow end to end before go-live.
