Why LLM subagents, not regex, for deduplicating legacy item codes

You bring a 20-year-old product master into Odoo, and you find the same item filed under three or four codes. My migration had roughly 25% of legacy codes as duplicates. The natural reflex is to write a regex. I did. It caught the easy ones and missed the ones that cost real time downstream. The approach that worked was purpose-built LLM subagents running category by category. Why do regex rules fall apart on real item codes? Regex is exact. It matches strings you can describe precisely. Legacy item codes are not precise. They are the residue of years of manual entry: truncations, spelling variants, swapped words, and category words that drifted out of a line name. A rule that matches “Mandarins 5kg” does not match “Mand 5kg” or “5kg Mandarin” or “Mandarin (5kg)” depending on which operator typed it and which year. Each exception needs a new rule, and each new rule needs a new test. You are not fighting one dirty field. You are fighting a thousand small human decisions. ...

2026-09-11 · 4 min · Leo Larkin

Odoo 19 XML-RPC product import: ptav resolution, step by step

You import a product master into Odoo 19, and the pieces do not line up the way the docs say. I put a full fresh-produce catalogue onto a self-hosted Odoo 19.0 Community Edition over the external XML-RPC API. The mechanics are specific and unforgiving. Here is the sequence that worked, template through variant, with the ptav resolution that makes or breaks it. How do you authenticate to Odoo 19 over the external API? Use XML-RPC, not JSON-RPC session auth. On self-hosted 19 CE, API keys authenticate over XML-RPC only. ...

2026-09-10 · 4 min · Leo Larkin

Odoo 19 CE or SaaS: what a distribution SME should run

You pick Odoo, then you hit the fork. Community Edition on a server you control, or the hosted SaaS. Both are “Odoo 19” but they behave differently, and the choice decides how you do access control, product variants, and automation. I run both daily: 19 Community Edition on a small VPS and the hosted SaaS, for a fresh-produce distributor with 7 branches plus a transit warehouse. Do CE and SaaS ship the same modules? Broadly, yes at the surface. Core ERP is the same menu: sales, purchases, inventory, accounting, CRM. If you need the standard screens, CE and SaaS feel identical. The difference is not the feature list. It is what you are allowed to control underneath. That is where the real decisions sit. ...

2026-09-09 · 4 min · Leo Larkin

Dynamic product variants in Odoo 19: importing a produce catalogue without the cartesian explosion

A fresh-produce catalogue kills naive Odoo setups. One apple template with brand, grade, size, and country attributes becomes hundreds of combinations. Multiply across 18 categories and you get a product table with more dead variants than live ones. Here’s the pipeline I use, verified on a real import. The template decision Template carries variety and weight. Brand, grade, size, and country are attributes. Example: “Driscoll raspberries 170g” is a template; the country of origin is an attribute on the variant. Naming rules matter more than they sound: the origin code leads the template name when the notes call for it (KR peaches, AU apples), family names stay out of variant names when the category already says it. ...

2026-09-08 · 3 min · Leo Larkin

How 3-way match works at invoice validation in Odoo 19 CE

Every distribution business I work with pays the same invoice checking tax: does this bill match what we ordered, and did it actually arrive? Odoo 19 CE has a place for that check, and it runs when you validate the bill. This is how it works and how to make it strict with a server action. What is the 3-way match in Odoo 19 CE? Three-way matching compares three documents: the purchase order, the vendor bill, and the goods received. Each represents one side of the deal. The order says what you wanted. The receipt says what came in. The bill says what the vendor charges. ...

2026-09-08 · 4 min · Leo Larkin

How I made this blog readable by AI assistants (and why that's just good SEO now)

A growing share of “who should I hire for X” questions get answered by chatbots before anyone sees a search result. This post documents the exact setup this blog uses so an AI assistant can find, parse, and quote it. Nothing here is a trick; it’s making your content the most quotable source on the topic. 1. llms.txt A plaintext file at /llms.txt that states what the site is, who wrote it, and which pages matter. No HTML to parse. AI crawlers (Claude, ChatGPT, Perplexity) read it; humans can too. Mine lists the services, the facts an assistant is welcome to quote, and direct links. Harmless if a crawler ignores it. ...

2026-09-08 · 2 min · Leo Larkin

Inter-company stock transfers in Odoo 19: the route collision that eats your sales orders

Inter-company stock transfers look like a routing problem and turn out to be a route-collision problem. Symptom: you confirm an inter-company PO and the procurement group produces moves pointed at view locations, so nothing ever lands. The diagnosis Transfer routes with pick/pack rules attached hijack sale-order procurement. The pull rules on those routes fire for the inter-company flow and push moves toward warehouse view locations, where they dead-end. The fix is to strip the routes down to two steps: ship and receive. Picks stay manual, created by the warehouse team, not generated by rules. ...

2026-09-08 · 2 min · Leo Larkin

One warehouse per user in Odoo 19 Community: the ir.access method

A 7-branch distribution business wants each branch manager to see only their own warehouse. In Odoo 19 Community this is solvable without record rules, which matters because SaaS won’t let you create them. Why groups leak Odoo access rights are additive. Every group a user belongs to contributes its full set of rights, and the union applies. Put your branch manager in a “Westport only” group and leave them in anything else with unscoped access to stock.quant, and they see everything. The union always wins. ...

2026-09-08 · 2 min · Leo Larkin

Running AI agents against Odoo 19 with XML-RPC and JSON-RPC

I run autonomous AI agents against live Odoo databases daily: data reconciliation, scheduled reporting, migration verification. This is the pattern that keeps it safe. The connection Odoo 19 speaks XML-RPC and JSON-RPC. Generate an API key per user (Preferences → API Keys), then authenticate with username plus key instead of password. The agent stores the key, not the password, and revocation is one click. import xmlrpc.client url = "https://odoo.example.com" db = "production" username = "agent@company.com" key = "API_KEY" common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common") uid = common.authenticate(db, username, key, {}) models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object") models.execute_kw(db, uid, key, "res.partner", "search_count", [[["customer_rank", ">", 0]]]) JSON-RPC works the same way with the /json/1 endpoint and the API key in the password field. I use XML-RPC for scripts (stdlib only, no dependencies) and JSON-RPC when the agent already speaks HTTP. ...

2026-09-08 · 2 min · Leo Larkin