What happens when an order is confirmed
One real session, Odoo 18 core — 8 400 files, 600-odd modules. Every answer below is what the agent actually got back, including the move that would have given a wrong answer. Under each move: the same move done by hand.
The question
Section titled “The question”I am about to change what happens when a sales order is confirmed. Which modules hook into confirmation, and what do they do there — so I know what I am stepping on?
Move 1 — who overrides it, on this model
Section titled “Move 1 — who overrides it, on this model”Ask: who defines action_confirm on sale.order — the question narrowed to the core tree,
so that the answer is about Odoo and not about anyone’s customisations.
Back: nine definitions — sale itself, then sale_management, sale_crm, sale_loyalty,
event_sale, event_booth_sale, sale_gelato, delivery_mondialrelay, l10n_it_edi_doi —
all on one model, no ambiguity. The fifteen other methods called action_confirm on other
models — manufacturing orders, pickings, repairs, lunch — are not among the definitions: the
graph knows which model each one sits on. The same answer also lists forty-nine places that
call the name, and there the graph is honest about its limit: a call site does not say which
model it targets, so each one is tagged — same model, a model that defines its own, or
undetermined — and you read the tag, not the name.
By hand: grep -rn "def action_confirm" — twenty-four lines. Which nine are on sale.order
you find out by opening twenty-four files.
Move 2 — what actually happens, in plain words
Section titled “Move 2 — what actually happens, in plain words”Ask: when a sale order is confirmed, what side effects happen — deliveries, mail, invoices, opportunities, registrations?
Back: fifteen hits. The expected ones — sale_crm updates the opportunity’s revenue,
event_sale reschedules registration mails, sale_management sends the quotation template’s
mail, payment.transaction confirms on payment. And one the agent had not asked for:
sale_purchase generates purchase orders on confirm — in _action_confirm, with an
underscore, not in the method from move 1. Deliveries, the most visible consequence of
confirming an order, did not appear at all: sale_stock does not override the public name.
What to make of that: meaning search found the second hook here by luck of one hit. The move
that finds it every time is cheaper still — read the twenty lines of the base method the graph
had already pointed at. Line 1174 delegates everything to _action_confirm.
By hand: you would have to already know about the underscore, or read the base method first.
Move 3 — the second hook
Section titled “Move 3 — the second hook”Ask: who defines _action_confirm on sale.order.
Back: seven, and what each one generates:
| Module | Generates | Relative to super |
|---|---|---|
sale_stock |
deliveries — _action_launch_stock_rule |
before |
sale_project |
project and tasks | before |
sale_purchase |
purchase orders for services | after |
repair |
repair orders | after |
website_sale_slides |
course enrolments | after |
delivery |
pickup address from the carrier | — |
sale |
nothing: an empty method, the extension point | — |
So the honest answer is sixteen modules on two hooks, not nine on one. The first list, taken alone, would have left out the warehouse, purchasing and projects — the three side effects a developer is most likely to break. And the order matters: two of them run before the base confirmation, three after.
By hand: another eleven-line grep, and the same reading problem — plus the before/after question, which no grep answers.
Move 4 — who triggers it without a person
Section titled “Move 4 — who triggers it without a person”No new question. The answer to move 1 already carried every call site of the name, each one
tagged. Most are defines_its_own — manufacturing, pickings, repairs calling their own method
of the same name — and drop out at once. A dozen are undetermined, and here the tag means
exactly what it says: this answer does not know. Opening those lines — one sed each — sorts
them: three are namesakes on other models (event booths, resource bookings, lunch), six are real
doors into sale.order:
| Door | Where |
|---|---|
| the form button — two of them, one for a sent quotation and one for a draft | sale/views/sale_order_views.xml:278, :281 |
| payment confirmed by the provider | sale/models/payment_transaction.py:124 |
| pay-on-collection pickup | website_sale_collect/models/payment_transaction.py:17 |
| an order of free event tickets | website_event_sale/controllers/main.py:81 |
| a free booth reservation | website_event_booth_sale/controllers/event_booth.py:38 |
| the point of sale | pos_sale/models/pos_order.py:54 |
What the doors show about mail. The send_email condition from the base method splits them:
the payment door and the pickup door set it and the customer gets a confirmation; the button,
the free ticket, the free booth and the point of sale do not. Next to the free-ticket call the
core author left # tde notsure: email sending ?. (Move 5 will revise this count.)
By hand: grep for .action_confirm( returns thirty-seven files, most of them calling the
method on other models. The six that matter are mixed in with the rest, and nothing marks
which is which.
Move 5 — where “complete” stops
Section titled “Move 5 — where “complete” stops”The limit, stated before it bites: the graph’s list is complete for what it promises — call sites of this name, as code on disk. That is not the same as “every way an order gets confirmed”. Two things live outside it.
Ask, as a literal text match: the name action_confirm as a string, in data files.
Back: <function model="sale.order" name="action_confirm"> in the demo data of five
modules, and a server action in lunch whose body is the string records.action_confirm().
Worth nothing in demo data — but this is exactly the mechanism that runs automations in a
production database, and to the graph a name stored as a value is invisible by construction.
Ask the graph once more: who calls _validate_order, the helper that confirms and sets
send_email itself.
Back: two callers the previous list could not contain, because they call a different name —
the customer signing the quotation in the portal (sale/controllers/portal.py:300) and the
web-shop checkout (website_sale/controllers/main.py:1972). The two busiest doors of all. And
sale_loyalty overrides the helper to auto-invoice a zero-amount order with a reward.
The corrected count: eight doors, not six. Four of them send the confirmation mail — payment,
pickup, portal signature, web-shop checkout; four do not — the form button, free tickets, free
booths, the point of sale. And sale_management covers the button only conditionally: the mail
goes out only when the order was built from a quotation template that carries one.
By hand: this move is by hand — a literal grep over *.xml and *.py. The difference is
knowing to make it: the graph says what it covers, so the agent knows what it does not.
Move 6 — read two places
Section titled “Move 6 — read two places”No tool for this one. The agent opened sale/models/sale_order.py at the two lines the graph
had named. _action_confirm turned out to be an empty method whose docstring says extend me
when confirmation should generate other documents — the second hook is the designed one, not
an accident. And the customer’s confirmation mail goes out only when send_email is set in
context — set by whoever knows a customer is on the other end: the portal, the web shop, the
payment; the back-office button does not. And set means scheduled: sale_async_emails
overrides the sending and, when its config parameter is on, parks the template and wakes a
cron instead — whether the mail leaves now or later is a setting, not the code path. Two
places, not thirty-five — because the earlier moves said which two, and that the list was
complete for what it covered.
Move 7 — the map
Section titled “Move 7 — the map”With six moves’ worth of parts on the table, the last move is to put them together — and the picture says something none of the parts did.
The mechanism has two floors, on purpose. The public action_confirm owns the transition
and the policy: validity checks, subscribing the customer, state = 'sale', locking, mail.
The underscored _action_confirm owns one thing — generating documents — and nothing else.
The rule of placement reads off who sits where: generate a document, extend the lower floor;
add a policy or a consequence outside sales, extend the upper. Control enters through eight
doors and then runs as one flow; it only fans out at the bottom, into documents. Notification
is a separate flow, steered by a value in context rather than by a call. And there is no way
to refuse a confirmation except to raise and let the transaction roll back.
What the map shows that the list did not. The lower floor is extended by seven modules on
confirm. Its mirror, _action_cancel, is extended by five. sale_project and
website_sale_slides are missing from the mirror: confirmation creates the project, the tasks
and the course enrolment; cancellation does not remove them. An observation from two lists of
definitions, not yet a verdict — the cancel bodies were not read — but the kind of thing you
only see from above.
Blind spots, named. send_email is a value, not a symbol: the graph answers zero on it, and
zero looks clean. Automations in a production database store code as records and live in no
tree at all. Anything that leaves the transaction before a late raise — an external call, a
synchronous send — does not roll back with it.
By hand: there is no by-hand for this one either. It is what the agent writes after the other six, and it is the part a developer actually keeps.
What to take from this
Section titled “What to take from this”No single move answered the question — the answer came out of five different kinds of move, each picking up where the previous one stopped. What made that possible was not that any tool was clever, but that each one said plainly what it had covered and what it had not: the graph knows names, not values; meaning search finds the likely place, not every place; a literal grep finds strings the graph cannot see. Knowing the edge of each tool is what told the agent which tool to reach for next.
The developer ends up knowing which modules they will touch, which doors lead into the code they are changing, and which two places are worth reading — instead of a list of thirty-five files and an afternoon.