Odoo module upgrades, done for you
Migrating a one-click confirm-invoice-deliver button from Odoo 14 to 18
A pattern we've migrated many times, anonymized and simplified: a custom module adds one button to the sale order. Click it and the order is confirmed, the invoice is created and posted, and the products ship from inventory. Maybe 80 lines of Python and a view inherit — and a four-version jump breaks it in three separate places.
The Odoo 14 code
def action_quick_process(self):
self.ensure_one()
self.action_confirm()
invoice = self._create_invoices()
invoice.action_post()
for picking in self.picking_ids:
for line in picking.move_line_ids:
line.qty_done = line.product_uom_qty
res = picking.button_validate()
if isinstance(res, dict) and res.get("res_model") == "stock.immediate.transfer":
self.env["stock.immediate.transfer"].browse(res["res_id"]).process()
And the button in the form view:
<button name="action_quick_process" string="Confirm + Invoice + Ship"
type="object" states="draft,sent"/>
What broke on the way to 18
- Odoo 17 — the view:
states="draft,sent"was removed. The button becomesinvisible="state not in ('draft', 'sent')". One attribute, but Odoo refuses to load the whole view until it's fixed. - Odoo 17 — the inventory API: the biggest chunk.
qty_doneon move lines was renamed, quantities are now driven throughstock.move.quantityplus a newpickedflag, and thestock.immediate.transferwizard that the old code juggled was removed entirely. Half the method body is calling APIs that no longer exist. - Odoo 17 — the state machine:
sale.orderlost itsdonestate (locking moved to a separatelockedfield). This module only gated ondraft/sent, so it survived — but any check againststate == 'done'would have failed silently.
The Odoo 18 version
def action_quick_process(self):
self.ensure_one()
self.action_confirm()
invoice = self._create_invoices()
invoice.action_post()
for picking in self.picking_ids.filtered(
lambda p: p.state not in ("done", "cancel")
):
picking.move_ids.picked = True
picking.button_validate()
Shorter than the original — the wizard juggling is gone because Odoo removed the wizard. The subtle part is behavioral: on 14 the immediate-transfer wizard forced full quantities; on 18 you must decide what a partial reservation means for a one-click flow (ship what's reserved and backorder the rest, or block). We flag decisions like that in the changelog we hand back instead of guessing silently.
How we verified it
Installed on a clean Odoo 18, then ran the flow end to end: draft order → click → order confirmed, posted invoice exists, delivery in done state, stock levels reduced. That's the test that matters for a button like this — not just "the module installs."
Try the working result before you pay a cent. No signup to get started.
Common questions
Is a module like this the $50 or $250 tier?
The code itself is simple — pure Python and XML, no JavaScript. What tips it to the $250 tier is the four-version jump: crossing that many version boundaries prices as complex because each one adds its own breaking changes. The same button moving one or two versions would be $50.
What about the invoice API — did _create_invoices survive to 18?
Yes. _create_invoices() and action_post() are stable across 14–18. On this pattern the breakage concentrates in the stock layer and the view syntax.