an abundance of strawberries

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 becomes invisible="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_done on move lines was renamed, quantities are now driven through stock.move.quantity plus a new picked flag, and the stock.immediate.transfer wizard 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.order lost its done state (locking moved to a separate locked field). This module only gated on draft/sent, so it survived — but any check against state == '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.