Two Odoo 19 modules automate the same sales order workflow with identical features — three per-warehouse switches behind the same Confirm button, on both. What separates them only shows up when a step fails, and only one module tells you what happened.
Key takeaways:
- Feature parity: both modules add the same three per-warehouse switches behind the same Confirm button.
- The real gap is failure handling: the generated module rolls back cleanly, names the failed step, and logs it where the rollback can't erase it — the vendor module has none.
- "Nothing to invoice" is a legitimate configuration, not an error — the generated module treats it that way; the vendor module blocks confirmation.
- The vendor module has ten Odoo releases of field exposure and a fifth of the code; the generated module ships 12 tests, demo data and a documented limitations section instead.
- Every difference traces back to two paragraphs in the written specification, not to code quality.
One module is sale_order_automation from Craftsync Technologies — more than 8,000 downloads, listed on the Apps Store from Odoo 10 to 19. The other, sale_warehouse_auto_fulfillment, we generated from a written specification with OdoMate, our AI Odoo module generator. No developer wrote its models, tests, or error handling — and it's the point of this post.
For once the checklist is a tie, so it comes down to what each module does when a step fails — and every difference there was written down in the specification before a line of code existed.
Scope and evidence: this compares OdoMate sale_warehouse_auto_fulfillment v19.0.1.0.0 with Craftsync sale_order_automation v19.0.1.1, current as of 25 August 2026. Every claim about Craftsync's code comes from reading the actual v19.0.1.1 package downloaded from the Apps Store — not a live head-to-head test. OdoMate is our own product.
Why does Odoo need four manual clicks to ship and invoice an order?
A salesperson confirms an order. Odoo stops. The delivery is sitting in Ready, and the invoice doesn't exist yet.
Somebody opens the transfer and clicks Validate, goes back to the order and clicks Create Invoice, opens the draft and clicks Confirm — three clicks and two screen changes, on every order, forever. For a warehouse that ships everything it sells, out of stock it trusts, that's ceremony.
Both modules remove it the same way. Three per-warehouse switches, and the Confirm button your team already uses does the rest. No new menu, no new screen, no new state.
We picked this problem on purpose — it's a harder test than the discount comparison, where the generated module came out ahead on four checklist features. Here there's nothing to come out ahead on: the features are a tie, and that's where a comparison actually gets interesting.
Does Odoo 19 already automate this without any module?
Worth knowing before you install anything, because for some readers the answer is "install neither."
Odoo 19 does ship automatic invoicing — it creates the invoice, posts it and emails it — but only when an online payment goes through. You'll find it as Invoice automatically on payment in the eCommerce settings (the code is public). There is no native path for an order a salesperson confirms by hand — and products invoiced on delivered quantities are explicitly excluded from it too, online payment or not, which matters later when this piece gets to the "nothing to invoice" case.
Automatic delivery validation has no native path at all. The immediate-transfer wizard that used to cover it existed in Odoo 16.0 and was gone by the 17.0 release — it isn't in Odoo 19's stock/wizard/ directory either.
So the gap both modules fill is real. If your orders arrive through eCommerce and are paid online, Odoo already covers you — and emails the invoice, which neither module does. Check that setting first.
The feature list is a tie
User-facing capabilities — what a buyer reads, what the store page shows, what a demo demonstrates:
| Odoo 19 as it comes | Craftsync, 8,000+ downloads | Generated by OdoMate | |
|---|---|---|---|
| Per-warehouse switch: validate the delivery on confirm | ⬜ | ✅ | ✅ |
| Per-warehouse switch: create the customer invoice | ⚠️ on payment only | ✅ | ✅ |
| Per-warehouse switch: post the invoice | ⚠️ on payment only | ✅ | ✅ |
| Runs behind the existing Confirm button, no new screen | ➖ | ✅ | ✅ |
| Emails the invoice after posting | ✅ | ⬜ | ⬜ |
✅ yes · ⚠️ only under a condition · ⬜ no · ➖ not applicable
On every row here, the two modules are identical. This is the tie.
Failure handling & evidence — the same feature working badly, working well, or not shipping at all:
| Craftsync | Generated by OdoMate | |
|---|---|---|
| Any error handling around the chain | ⬜ | ✅ |
| Failure message naming the step that broke | ⬜ | ✅ |
| Failure recorded where the rollback can't erase it | ⬜ | ✅ |
| "Nothing to invoice" skipped instead of blocking | ⬜ | ✅ |
| Backorder creation explicitly suppressed | ⬜ | ✅ |
| Automated tests | ⬜ | 12 |
| Demo data you can click through | ⬜ | 6 quotations |
| README with a written limitations section | ⬜ | ✅ |
Nothing here is a feature a store listing would advertise. It's what happens once the happy path ends.
What did the specification ask for that no feature list shows?
The specification this module was generated from ships inside it as SPEC.md — most of it is what you'd expect: three fields, their defaults, where they go on the form.
Then there's this, verbatim:
Failure handling (real errors only — delivery validation error, invoice creation error, invoice posting error): the entire automation, including the order's state change to confirmed, runs inside one database savepoint. On any real error: roll back the savepoint completely (order reverts to quotation, no delivery, no invoice — nothing half-done), write a failure record to
ir.loggingvia an independent cursor (so it survives the rollback), then raise a blockingUserErrornaming the stage that failed and the underlying reason (e.g. "Invoice could not be posted: customer has no receivable account") instead of the raw traceback.
And this:
"Nothing to invoice" is not a failure. If step 3 produces zero invoiceable lines (e.g. a delivered-quantity-policy product with
auto_shipoff), invoicing is skipped silently, a note is added to the order's chatter, and the confirmation proceeds/succeeds normally.
Neither paragraph describes a feature — nobody puts "handles its own errors" on a store page. But those two paragraphs are the entire difference between these two modules, and the generator built both of them.
What came back for the first one is a message a salesperson can act on:
Automatic fulfillment failed for S00042 on warehouse Main Warehouse.
Stage: automatic invoice posting
Reason: The customer has no receivable account.
Nothing was kept: the order stays a quotation, no delivery was validated
and no invoice was created. Fix the reason above and confirm the order
again, or turn the automation off on the warehouse to confirm it manually.Three things matter there: it names which step broke, it gives the underlying reason instead of a traceback, and it states what was and wasn't kept — "did that half-work?" is the first question anyone asks and the hardest to answer from a stack trace.
That write goes through a separate database cursor so it survives the rollback — without it, a failed automated invoice would leave no trace, and the order would just look like a quotation nobody confirmed.
Where do the two modules actually diverge?
Error handling. sale_order_automation has none — no try, no except, no raise anywhere in its 44 lines, and the one import that hints at it (from odoo import ... exceptions) is never used. When a step fails, whatever Odoo raised is what the salesperson sees.
Backorders. The generated module passes skip_backorder=True, so a full-quantity delivery goes straight to Done. Craftsync's passes nothing, so Odoo can hand back a backorder wizard — a dialog nobody's there to answer, whose return value the module discards before forcing the transfer done anyway.
Confirming several orders at once. Their loop reads self.picking_ids instead of order.picking_ids, inside a loop that's already iterating orders — select ten quotations and hit Confirm, and every order's transfers get processed once per order in the batch. The generated module handles each order once, though it wraps the whole selection in a single savepoint, so one failure rolls back all ten. That's a real trade-off, not a win; it's below in the limitations.
What ships in the box?
The generated module comes with 12 automated tests, covering the defaults, each switch in isolation, shipping into negative stock, the silent skip, both rollback paths and warehouse isolation — readable before you install anything. Craftsync's module has no tests/ directory.
It also ships six demo quotations you can click Confirm on to watch a specific behaviour, and a README whose Known limitations section says, among other things, that invoices are posted and never emailed, and that automation history lives in ir.logging only.
None of that is code quality — it's whether you can evaluate the module before it touches your database, and re-check it after every point release.
The part you can check yourself
Here's the claim easiest to doubt: this is the generated module, not one somebody then tidied up.
The repository is public, so this isn't something you have to take our word for. The module landed in commit 3b7066e, straight out of generation. The code that does the actual work — models/ and tests/ — is unchanged since.1 What's been added on top since then is publication housekeeping: a longer store description, the banner and screenshots, and (as of 17 August) removing a generator artefact that shouldn't have shipped in the first place.
The screenshots on the listing page are from the live demo deployment the platform spun up during generation, not from a staged environment we assembled afterwards.
What does Craftsync's module have that ours doesn't?
Ten Odoo releases of exposure. Their module is listed for every series from Odoo 10 to 19, downloaded more than 8,000 times. Downloads aren't deployments, but exposure at that scale surfaces problems no test suite predicts — ours is a first release with a handful of downloads and no field history. If that counts for more than anything above, that's defensible, and we won't argue you out of it.
Being a fifth of the size. 44 lines of Python in models/ against our 224 — same directory, same count, both sides.2 Less code is less to audit, and for a single-warehouse shop confirming orders one at a time with ordinary stocked products, those extra 180 lines buy edge-case handling that shop may never reach.
We also found one thing that isn't a trade-off, just the ordinary cost of keeping a module alive across ten releases: it still passes default_immediate_transfer=True, a context key for a wizard Odoo removed in 17.0, which does nothing in 19. A module generated against today's Odoo starts from what the platform does now — the same reason it once refused to duplicate a report Odoo already had — an advantage it gets for free and can't take credit for.
What doesn't this comparison prove?
- No test run, no live head-to-head. 12 tests ship and are readable, but nobody ran them for this comparison, and neither module was tested head-to-head on a live instance — every claim here comes from reading both modules' source and Odoo 19's, as of 25 August 2026, at v19.0.1.0.0 against v19.0.1.1.
- Batch confirmation is all-or-nothing. One savepoint wraps the whole selection — confirm ten orders, have the tenth fail, and all ten roll back. For some teams that's correct behaviour, for others it's infuriating.
- Generated code still needs human review before production. Both modules can drive your stock negative by design — that's a call for a reviewer to make, not inherit.
- One module, one problem. This is a small, well-scoped sales workflow. It says nothing about heavy accounting logic or an external integration.
What we actually take from this
The last comparison we ran was won on features. This one couldn't be — the feature lists were identical before we started, which is the more honest test, because it removes the possibility of winning by simply asking for more.
What was left was the failure path, and the failure path was in the specification: the savepoint, the message naming the stage, the log write that survives the rollback, the decision that "nothing to invoice" is a normal outcome rather than an error. Somebody had to think about what should happen on a bad day and write it down in plain sentences — everything after that was generation.
That's the part worth taking away. The generator didn't out-engineer a vendor. It built what the specification said, including the paragraphs about failure most specifications never contain.
Try it on your own problem
Read the specification and the module it produced, side by side, or see how this fits into what we've found generating Odoo modules with AI throughout 2026. Then bring a requirement of your own — ideally one where you already know what should happen when it goes wrong. Inspect the module, then request early beta access — we review requests on a rolling basis and onboard in small cohorts.
FAQ
How do I make Odoo validate the delivery and post the invoice when a sales order is confirmed?
Odoo 19 has no native setting for it on manually confirmed orders. Native automatic invoicing exists but only fires when an online payment completes, and native automatic delivery validation doesn't exist at all — the wizard for it was removed at the 17.0 release. Options include a hand-written server action through Automation Rules, a third-party workflow module, or one with per-warehouse switches built for exactly this — two that do are sale_order_automation from Craftsync Technologies and sale_warehouse_auto_fulfillment from OdoMate, both compared in this piece.
Can a module generated from a specification match one from an established vendor? On this problem the feature lists came out identical. The generated module goes further on error handling, the "nothing to invoice" case, backorder suppression and what ships alongside the code. The vendor module has more than 8,000 downloads and ten Odoo releases of exposure that the generated one doesn't.
Was the generated module edited by hand before it was published?
No. The models/ and tests/ directories are byte-identical between the commit the generator produced and the branch published to the Apps Store. Only the manifest summary changed, plus the store banner, screenshots and the specification file added for publication.
What happens if one of the automated steps fails?
The whole run is inside one database savepoint: the order reverts to a quotation, nothing is validated or invoiced, and the salesperson gets a message naming which step failed and why. The failure is also written to ir.logging through a separate transaction, so the record survives the rollback.
Will either module ship stock the warehouse doesn't have? Yes, both. Neither checks availability before validating the delivery, so automatic shipping can drive quantities negative. The generated module states this in its specification, README and a test. Only enable it on warehouses whose stock figures you trust.
Footnotes
-
git diff 3b7066e origin/19.0 -- sale_warehouse_auto_fulfillment/models sale_warehouse_auto_fulfillment/testsagainst a fresh clone of the repository returns nothing — every line of logic and every test is byte-for-byte what the generator produced. ↩ -
Both counts are the Python lines in each module's
models/directory only,wc -l, nothing else — not the same metric Apps Store's own listing page uses for its maintenance-pricing calculator, which currently shows 173 and 53 for the two modules respectively. ↩
