Hire Odoo Developers in India
Hire Odoo developers in India who write custom modules the next major version will not destroy. The work is Python and PostgreSQL underneath, which widens the talent pool, but the conventions that keep an instance upgradeable are what we actually screen for.
Most people arrive at this page with one of two problems. Either they have an Odoo instance that somebody customised heavily two or three years ago and nobody left in the building understands it, or they are about to start customising and have been told the wrong things about how hard it is. Both problems come from the same place: Odoo is easy to change and difficult to change well, and the gap between those two only shows up at the next upgrade. What follows is what the job involves, what separates a developer who will leave you upgradeable from one who will not, and an honest account of how the work runs out of Mumbai when your business sits somewhere else entirely.
Why Odoo Hiring Does Not Look Like Other ERP Hiring
The word ERP puts most buyers into a particular mental model: certified consultants, functional analysts, a partner practice, a long implementation. Odoo breaks that model in a useful way and then quietly reintroduces some of it through the back door.
It is Python and PostgreSQL, and that changes the market
Odoo's server is Python. Its data lives in PostgreSQL, in tables you can open and read. Its business objects are declared as Python classes with typed field descriptors, and its screens are XML documents. There is no proprietary language to learn, no licensed development workbench, no transport request to raise before you can see your change. That single fact is why the hiring pool for Odoo is far wider than for the older enterprise suites: a competent backend engineer can read the source of the module they are extending on their first afternoon, which is not an option in most of this category.
It also means the role sits much closer to ordinary software engineering than the ERP label suggests. Version control, code review, automated tests, staging environments and deployment pipelines all apply, and a team that already has those habits from web work will find most of them transfer. If your search is really for a Python engineer who happens to work in an ERP, our Python developers in India page covers the wider language skill set that sits underneath this one.
Where strong Python engineers come unstuck
The language is never the problem. The framework is. Odoo has its own object model, its own inheritance mechanism that has nothing to do with Python's, an ORM whose semantics are unusual, a view layer defined in XML fragments that patch other XML fragments, a permission system split across access rights and record rules, and a large body of unwritten convention about how a module should be laid out. A developer who has shipped Django for eight years will write something that works within a week and something that is genuinely maintainable somewhere between two and four months later.
The failure is predictable. They write a plain SQL query because they can, and it bypasses record rules, so a user in one company sees another company's data. They override a method and forget to call the parent, and three unrelated features stop working. They add a computed field without thinking about what triggers recomputation, and a nightly job starts taking four hours. None of these are bad engineers. They are engineers applying reasonable instincts to a framework that punishes reasonable instincts.
What we sell here, stated plainly
We supply developers who write and maintain Odoo code. We do not run Odoo implementations, we do not do functional consulting, we do not design your chart of accounts or your warehouse process, and we do not describe ourselves as a certified Odoo practice. That work belongs to different people with different training, normally your own staff or the partner who installed the system. Our boundary is the code: modules, version migrations, integrations, printed documents, performance, and the care of whatever already exists. The same line is drawn on our ERP integration services in India page. Stating it out loud at the start costs far less than the meeting in month three where nobody can say which supplier was supposed to own a business rule.
The Module Model, and the One Mistake That Ends Upgrades
Everything in Odoo is a module, including Odoo. The sales app is a module. Accounting is a module. The web client is a module. Your customisation is a module too, and it has exactly the same powers as the ones that shipped with the product. Understanding that is the whole game.
What a module actually is
A directory with a manifest that declares a name, a version, a list of other modules it depends on, and the data files it loads. Inside, Python files declare models, XML files declare views and records, a CSV file grants access rights, and static assets go in their own tree. When the server starts with that module installed, it merges everything the module declares into a single in-memory registry. Your model extension and the original are the same object by the time a request is served.
The dependency list is doing more work than it looks. It sets load order, and load order decides which definition wins when two modules touch the same thing. A candidate who cannot explain why their module must depend on the one it patches will eventually produce a bug that only appears on a fresh database.
Classical inheritance: extending something that already exists
Declare a class with _inherit set to an existing model name and no _name, and you are not creating anything new. You are reopening the existing model and adding to it. New fields land on the same table. Methods you define with the same name as an existing one replace it, and calling super() decides whether the original behaviour still happens. This is the mechanism that lets you add a delivery priority to sale orders without owning sale orders, and it is the one you should reach for perhaps nine times in ten.
The discipline it demands is restraint in overrides. An override that calls the parent first, then does its own work, survives most upgrades. An override that reimplements the parent's logic inline because it was easier to copy than to extend is a landmine, because when the vendor fixes a bug in that method your copy never receives the fix.
Prototype inheritance: borrowing a shape for a new model
Set both _inherit and a different _name and you get a copy of the structure in a brand new model with its own table. Nothing links the two afterwards. This is right when you genuinely need a separate object that behaves similarly, and wrong far more often than it is used, because people reach for it when what they actually wanted was to extend. The tell is a new table that duplicates most of an existing one and then needs syncing back, which is a maintenance problem you invented for yourself.
Delegation inheritance: composition through a foreign key
The _inherits form, with the trailing s, is different again. It says this model contains a reference to another model, and the parent's fields should be readable and writable through the child as if they were its own. Odoo uses it internally where one concept wraps another, and it is the right answer when your object genuinely is a specialisation of an existing record that must keep its own identity. It is also the one that confuses people most in interviews, which makes it a useful question precisely because the answer reveals whether someone learned the framework or memorised a tutorial.
View inheritance, which follows the same idea in XML
Screens are patched, not replaced. You declare a view record that points at an existing view as its parent, then use an xpath expression to locate an element and say what happens there: insert after it, insert before it, put something inside it, replace it, or change its attributes. The result is that your change is a diff, not a fork, so when the vendor rearranges the rest of the form your patch usually still applies.
Usually. Anchor your xpath on something fragile, like a position index or a deeply nested chain of tags, and the next release moves it and your view fails to load. Anchor it on a field name and it survives almost anything short of that field being removed. This is a small habit with a very large effect on upgrade cost, and it is one of the fastest ways to judge how much production Odoo someone has actually done.
Why editing core is the decision you cannot undo cheaply
Nothing in the framework stops you opening a standard addon and changing it. It works immediately, it is fewer lines, and on a Friday afternoon it is genuinely tempting. What it does is take your instance off the upgrade path. From that moment the vendor's code and your code are the same code, so every future release is a manual three-way merge between what you changed, what they changed, and what you meant. Teams in this position stop upgrading, usually without ever deciding to, and two years later the instance is three versions behind with a list of security fixes it has not received.
A well-built custom module survives a version jump because it is a set of small declared changes against named things, sitting in a directory nobody upstream touches. When the new release lands, you reinstall it, find out which anchors moved and which methods changed shape, fix those, and move on. That is a task with an end. Merging a forked ERP is not.
The ORM Is Where Performance Is Won and Lost
Most complaints that reach us as "Odoo is slow" are not about Odoo. They are about a handful of ORM patterns that are comfortable to write and expensive to run, and they were written by people who never saw the SQL underneath.
Recordsets are the unit of work
In Odoo, self inside a model method is not one record. It is a set of records, possibly one, possibly forty thousand. Every method you write should assume the set could be large, and the operators the framework gives you for working on sets, mapped, filtered, sorted, are there because they operate on the batch rather than the item. Writing a loop that fetches a related record inside the loop turns one query into thousands, and this is the single most common performance defect we find in inherited code.
Prefetching softens this when you stay inside the framework's idioms. Read one field on one record of a set and the ORM will usually fetch that field for the whole set in a single query, because it assumes you are about to want the rest. Break out of the recordset, iterate with individual database reads, or force a flush in the middle of a loop, and you lose that behaviour without any warning that you did.
The environment carries more than you think
Every recordset carries an environment: the cursor, the user, the context and the company. That environment silently changes results. A method that behaves correctly for an administrator can return nothing for a warehouse operator because record rules filter what the same query returns. Wrapping the call in sudo makes the problem vanish and creates a far worse one, because now your code ignores the access model entirely and a bug becomes a data leak between companies.
We ask candidates when they would use sudo. The answer we like is narrow and specific: for a genuinely system-level operation such as writing a log or a sequence that an ordinary user must not be granted rights to, applied to the smallest possible call, never wrapped around a whole method because something was failing and it made the failure stop.
Computed fields, and the compute that ruins everything
A computed field is a Python method that fills a value. Left unstored, it is calculated every time it is read, which is cheap for one record on a form and ruinous for a thousand rows in a list. Marked as stored, it becomes a real column, and the framework recalculates it whenever one of its declared dependencies changes.
That dependency declaration is where systems die. Declare a stored total on the order that depends on a field of every line, and every line edit rewrites the order. Chain a second stored field on the customer that depends on all their orders, and now one line edit on one order touches the customer record, which triggers whatever depends on the customer. We have seen a single field change fan out into tens of thousands of writes because three developers each added a reasonable-looking stored compute over two years. The fix is usually to unstore the field and accept the read cost, or to narrow the dependency to the specific subfields that actually matter, or to compute nightly if the value does not need to be exact in real time.
Two related details separate people who have done this in anger from people who have read about it. Non-stored computed fields cannot be searched or sorted unless you supply a search method, which is why a filter suddenly stops working after someone unstores a field. And a compute that writes to other records rather than to its own is not a compute at all, it is a side effect in the wrong place, and it will fire at times you did not anticipate.
SQL constraints against Python constraints
You can enforce a rule in two places. An SQL constraint is declared on the model and becomes a real PostgreSQL constraint on the table. It is checked by the database, it cannot be bypassed by any code path including direct SQL, it costs nothing at runtime, and it is limited to what the database can express: uniqueness, a check on columns of the same row, not null.
A Python constraint is a method that runs on create and write, and it can express anything, including rules that involve other records or external state. It is also only enforced when writes go through the ORM, so a data import that goes around the framework will not honour it, and it costs a method call on every affected record.
The judgement is straightforward once stated. If the rule is a genuine invariant of the data, put it in the database, because a decade from now some script will write to that table and you want the constraint to hold anyway. If the rule is business policy that has exceptions and needs a readable error message, put it in Python. A candidate who reaches for Python every time has not thought about who else writes to their tables.
When to leave the ORM
Occasionally the right answer is a grouped read or raw SQL. Aggregations over large tables for a dashboard are the usual case, and the framework's grouped read exists precisely so you can get counts and sums without instantiating records. Below that, dropping to the cursor is legitimate for read-only reporting where the volume makes anything else absurd. What it is not legitimate for is writing, because you skip constraints, computed field recalculation, record rules and the audit trail in one move. Anyone who writes to Odoo tables with raw SQL should be able to explain, unprompted, exactly which of those four they just bypassed.
Views, Reports and the Parts Users Actually Touch
Backend developers who consider the interface somebody else's problem do badly in Odoo, because a large share of what users ask for is interface work and almost all of it is declared rather than coded.
The XML view layer
Forms, lists, kanban boards, search panels, pivots, graphs and calendars are all XML records in the database, loaded from files in a module. You describe what should appear and the client renders it. Conditional visibility, readonly and required states are expressed as domains evaluated against the record, which means a lot of behaviour that would be JavaScript in a normal application is a single attribute here.
The consequence for hiring is that an Odoo developer needs to be fluent in domains: the prefix notation with its explicit operators, the difference between a domain on a field and a domain applied by a record rule, and why a badly written one silently matches everything. Domain mistakes are common, quiet, and occasionally serious.
QWeb reports are a specialism, not a chore
Every printed document your business sends, the invoice, the delivery note, the purchase order, the picking label, is a QWeb template rendered to HTML and then converted to PDF. It sounds like a small job until the first real requirement arrives: a running total that continues across page breaks, a header block that repeats but a summary that must appear only on the final page, a fixed footer for statutory text, or a layout that has to line up with pre-printed stationery.
Odoo's PDF pipeline has long gone through an HTML-to-PDF engine, with page format controlled by paper format records rather than by CSS alone, and the practical result is that print behaviour does not always match what you see in a browser. Margins that look right on screen clip on paper. Table headers repeat or do not depending on how the markup nests. Getting a complex multi-page document to come out right is genuinely fiddly work, and it is worth asking directly whether a candidate has done it, because the person who has will describe the paper format record and the page-break problem without prompting, and the person who has not will say reports are easy.
OWL and the JavaScript side
Odoo's newer web client is built on its own component framework, with a declarative template syntax, a component lifecycle and hooks. If you need a one-off field widget, a custom kanban card, a dashboard with real interactivity or a point-of-sale screen change, that is where the work happens. It is real frontend engineering, and it is a different person from your backend module developer more often than not.
One caution worth pricing in before you commission anything ambitious: the client-side layer has been rewritten during Odoo's history, so JavaScript customisations tend to carry more upgrade risk than server-side ones. A custom widget is a fine thing to build when the requirement justifies it, but treat it as a component with a maintenance cost rather than a one-off.
The website and portal layer
Odoo also ships a public-facing side: the website builder, the ecommerce shop and the customer portal where partners see their own quotes, invoices and tickets. Controllers there are ordinary route handlers, and this is one place where security mistakes are expensive, because the code is exposed to the internet and the records it touches belong to specific customers. Any portal work needs an explicit check that the requested record belongs to the requesting partner, and that check is the first thing we look for when reviewing controller code.
What Actually Happens During an Odoo Upgrade?
This is the single most important question to ask any Odoo developer you are considering, and the one most likely to be answered vaguely. Ask it first.
The cadence, and why it matters to you
Odoo ships a major release annually. Each one brings new functionality and also moves things: fields renamed, models merged or split, methods changed, the web client adjusted, occasionally a whole app reorganised. Every release is supported for a defined window and then stops receiving fixes, and because the exact support period and the current version are things Odoo controls and adjusts, check their current policy rather than any figure quoted on a supplier's site.
The rhythm this produces is unfamiliar to buyers coming from older ERP suites, where a major upgrade might be a once-a-decade programme. Here it is a recurring, plannable piece of engineering work. That is better, provided you actually plan it.
The three separate jobs inside an upgrade
First, the database. Table structures, renamed columns and moved data have to be transformed to the shape the new version expects. Odoo runs a database upgrade service for this, and in the community ecosystem there is a long-standing open source project maintaining migration scripts for the same purpose. This part is largely mechanical and it is not where your budget goes.
Second, the code. Every custom module you own has to be made to run against the new addons. Fields it referenced may have moved, methods it overrode may have changed signature, xpath anchors may point at elements that no longer exist, and a widget it used may be gone. This is the part that takes real time, and the size of it is decided by choices made years earlier by whoever wrote those modules.
Third, behaviour. Standard functionality changes between releases, and a process your finance or warehouse team relies on may work differently even though nothing errors. Nobody finds this by reading release notes. You find it by running your actual month-end, your actual delivery flow and your actual reporting against a copy of your real data, with the people who do that work daily sitting in front of it.
What being three versions behind actually costs
You cannot skip. Getting from an old version to a current one means passing through each release in turn, with the custom modules fixed and tested at every step, because the migration scripts and the code changes are defined release by release. Three versions behind is therefore roughly three upgrades stacked, and the difficulty is not linear, because the changes compound and the person who wrote the original customisations has usually left.
There is a second cost that is easier to overlook. Once a version falls out of support it stops receiving fixes, which turns an ageing instance into a standing risk rather than an inconvenience. If you are in that position now, the first useful piece of work is not the upgrade itself. It is an inventory: every custom module, what it does, whether the business still uses it, and what depends on it. A surprising number of upgrade projects shrink by a third once someone establishes that four of the eleven custom modules have not been used since the original rollout.
How to make future upgrades cheaper starting now
Keep customisations in modules, never in core. Keep those modules in version control with your own history, not as files copied onto a server. Keep them small and single-purpose, so that when one breaks you can weigh fixing it against deleting it. Prefer configuration over code where the configuration exists, because configuration migrates and code does not. Anchor view patches on stable things. Write tests for the business rules that matter most, because they are what tells you the upgrade worked, and running your suite against the new version is far quicker than a manual pass across every screen. And keep a written record of which standard behaviour each module changes, so the person doing the upgrade in three years knows what to retest without reading every line.
Community or Enterprise, and What the Choice Locks In
This decision affects functionality, licensing and hosting all at once, and it is usually made before a developer is involved. It is worth understanding what it constrains.
The shape of the split
Community is the open source edition. Enterprise is a paid subscription layered on top, adding apps and features and support. The precise contents of each side move between releases as functionality shifts, so any list published by a supplier goes stale. Check Odoo's own current comparison, and if a particular capability is decisive for you, verify it in the version you would actually run rather than in a blog post about a previous one. We will not quote subscription figures here for the same reason.
What it means for the developer
Less than buyers expect. Modules are written the same way against both, the ORM is identical, view inheritance works the same, and the same development discipline applies. The differences that bite are practical ones: which standard modules exist to extend rather than build, whether certain apps are available to depend on at all, and licence compatibility for anything you intend to distribute. If your team is writing modules purely for internal use, that last point is a smaller question than the internet suggests, but it is still one to put to your own counsel rather than to a developer.
The community ecosystem is part of the calculus
A large body of community-maintained modules exists, organised by an association that publishes them per version, and it is often the quickest answer to a requirement that feels custom but is not. The engineering judgement is whether a given module is genuinely maintained for the version you are on, because an unmaintained third-party addon becomes your code at the next upgrade whether you wanted it or not. We treat a community module the same way as any other dependency: read it before adopting it, check its history, and be honest about who fixes it when it breaks.
Odoo Online, Odoo.sh or Self-Hosted: How Does That Change the Developer's Job?
More than any other infrastructure decision on this list. The hosting choice decides whether custom code is possible at all, how it gets deployed, and what a developer can see when something breaks.
Odoo Online
The fully managed option. Odoo runs it, upgrades it and keeps it patched, and in exchange you do not install custom modules. Customisation happens through configuration and the built-in customisation tools, which cover a genuine amount of ground for a straightforward business. If you are here, you may not need a developer at all yet, and it is worth being told that rather than sold something. The moment you require behaviour the configuration layer cannot express, the honest answer is that you have to move rather than that we can code around it.
Odoo.sh
The middle option, and the one that most changes how a developer works day to day. It is a managed platform built around Git: branches map to environments, pushing a branch triggers a build, and staging environments are created from a copy of production data so you test against something realistic. Custom modules live in your repository, which forces the version control discipline that self-hosted teams sometimes skip.
The trade is control. You get a defined amount of access to the underlying environment and no more, so system-level changes, unusual dependencies and certain debugging techniques are either awkward or unavailable. For most teams that is a good bargain, and it also means the branching model in your repository is not a detail; it is your deployment process. Agree it in the first week.
Self-hosted
Full control and full responsibility. You choose the PostgreSQL version and tuning, the worker configuration, the reverse proxy, the backup regime and the restore test that proves the backups work. You can attach a debugger to a running process, read the SQL log directly, and run whatever profiling you like, which makes serious performance work considerably easier. You also own patching, upgrades and the pager.
The mistake we see most in self-hosted instances is a single overloaded server with default worker settings, no separation between the process serving users and the process running scheduled jobs, and backups that have never been restored. If that describes your setup, the first fortnight of any engagement is better spent there than on features.
Getting Your Data In, and the Demo Data Trap
Data migration is where go-live dates slip, and the reasons are rarely technical brilliance. They are sequencing, identity and the state of the data you are migrating from.
External identifiers make imports repeatable
Odoo lets every imported record carry an external identifier, a stable key from your source system. Use it and an import becomes idempotent: run it again with corrected data and it updates the same records rather than creating a second set. Skip it and your second attempt at the customer file produces two of every customer, which someone then deduplicates by hand on a weekend. This is the cheapest habit in the entire migration and the one most often missed by people importing spreadsheets for the first time.
Order matters as much as identity. Partners before contacts, product categories before products, products before stock, chart of accounts before journals, journals before opening entries. Get the sequence wrong and you spend the day chasing relational errors that have nothing to do with the data itself.
The demo data trap
When a database is created, there is an option controlling whether sample data is loaded. It is convenient for evaluation, because you get demo customers, demo products and demo transactions to click around in. It is also close to impossible to remove cleanly later, because demo records are woven through the standard modules and referenced by each other.
The trap is not that people load it deliberately. It is that a prototype built to demonstrate a workflow gets approved, and then becomes production, because nobody wanted to redo two weeks of configuration. Six months later there are demo partners appearing in a customer report and nobody can explain them. The rule is simple and non-negotiable: the database that becomes production is created without demo data, from the start, and the prototype is thrown away. Any developer who shrugs at this has not lived with the consequences.
Opening balances and the accounting sequence
The other trap is accounting. The localisation and chart of accounts have to be right before transactions exist, because changing them afterwards means unwinding posted entries rather than editing a setting. Opening balances go in as journal entries that your accountant reconciles and signs off, not as an import into a table. And a migration cut-over date needs deciding early, because whether open invoices come across as full documents or as balances changes both the import and what your finance team can do in the first month.
If the source system also has to keep running in parallel or feed Odoo afterwards, that is an integration rather than a migration, and it needs its own design. The patterns for that live on our data integration services in India page, which covers reconciliation, idempotency and what to do when the two systems disagree.
How We Screen Odoo Developers
Odoo is easy to fake on a CV. Almost everyone in the market has touched it, and years of exposure tell you very little about whether someone leaves an instance better or worse than they found it. These four questions do most of the sorting, and you are welcome to use them yourself.
"Add a field to a standard screen without touching core. Talk me through it."
The answer should arrive quickly and in a specific order: a new module with a manifest declaring a dependency on the one being extended, a model class using classical inheritance to add the field, a view record inheriting the standard form with an xpath anchored on a nearby field, an access rights entry if a new model was involved, and the module installed and updated through the normal mechanism. Weak candidates describe the developer tools in the interface as their answer. Those tools have their place for configuration, but a candidate who has no code answer at all is a configurator, and that is a different hire.
"That module you just described. What happens to it at the next major version?"
This question separates people fast. A strong answer walks through the actual failure modes: the anchor element may have moved or been renamed, the method overridden may have changed signature, a field referenced may have gone, and the way to find out is to install against the new version and read the errors, then fix them one at a time. Better answers add what they did in advance to keep that list short. Weak answers say it should be fine, or that Odoo handles migration. Odoo migrates the database. Nobody but you migrates your code.
"A list view takes twelve seconds to load. How do you find out why?"
We want a method, not a guess. Turn on SQL logging and look at what the server actually runs. Count the queries: one slow query is an index or a join problem, three thousand fast ones is a loop or a non-stored computed field being calculated per row. Check whether the columns displayed include computed fields and whether they are stored. Check the ordering field and whether it is indexed or is a related field pulling in a join. Check the record rules that apply to the user reporting the problem, because a rule with a subquery in its domain will make the same view fast for an administrator and slow for everyone else. Then run explain on the worst query.
The bad answer is a guess presented as a diagnosis, most often more memory or more workers. Occasionally that is the fix. It is never the first step.
"Tell me about a customisation you argued against building."
This is the one we care about most, and it has no technical content at all. Every Odoo instance that became unmaintainable got there one reasonable request at a time. A developer worth hiring has said no, or at least pushed back hard, and can describe the reasoning: the standard behaviour already covered it once the process changed slightly, the requirement came from one person's habit rather than a business rule, the maintenance cost across future upgrades outweighed the benefit, or a configuration option did ninety per cent of it. A candidate who has never declined a request will build you exactly the instance you are trying to avoid.
Alongside these, we look at real code. Not a portfolio screenshot: an actual module, read together, with questions about why it is shaped that way. Communication gets assessed in the same session, because the developer will spend a large part of the engagement explaining trade-offs to people who are not engineers, and someone who cannot explain a stored computed field to a finance manager will cause you more trouble than a slightly weaker coder who can.
What Seniority Actually Means in Odoo Work
Years of experience map badly onto this skill, because someone can spend four years building the same three modules at an implementation shop. What follows is how we grade it, and it is worth matching the level to the job rather than defaulting to the most senior person available.
Junior
Comfortable in Python, has installed and configured Odoo, can add fields and simple views inside a module somebody else structured. Needs review on anything that overrides standard behaviour. Useful for well-defined tickets in an instance that already has a maintainer, and genuinely dangerous as the only developer on a production ERP, not because of ability but because nobody is checking the decisions that cause damage two years later.
Mid-level
Writes complete modules unsupervised. Knows the three inheritance forms and picks the right one. Understands stored against non-stored computes and thinks about recomputation before writing the decorator. Can build a QWeb report of moderate complexity, wire up an integration, debug the common performance problems, and has been through at least one version upgrade of code they wrote. This is the level most engagements actually need.
Senior
Has taken an instance through multiple upgrades and can estimate one credibly, which is a rare and valuable skill. Reads a slow system and finds the cause rather than the symptom. Designs the module structure for a body of customisation so that pieces can be removed later. Says no to requirements, in writing, with reasons. Comfortable in the frontend layer or honest about needing someone who is.
Lead or architect
Owns the shape of the whole customisation, decides what belongs in Odoo and what belongs in a service beside it, sets the branching and deployment model, and holds the line on upgradeability against commercial pressure to ship. On a single-instance engagement you may not need this person full time, but you need somebody making these calls, and if nobody is, the answer defaults to whoever is loudest that week.
One combination is worth naming because it is undersupplied. A developer who understands accounting properly, meaning journal entries, reconciliation, tax handling and period close, is markedly more useful than a stronger coder who does not, because most of what an ERP does eventually posts to a ledger. If your work is finance-heavy, screen for that explicitly and expect to wait longer.
Three Situations We Get Called Into
These are patterns we see repeatedly, described as composites rather than as any particular client. If one of them looks like your week, the shape of the fix is usually similar.
You inherited a heavily customised instance and nobody knows how it works
The original partner is gone, or the internal developer left, and what remains is an Odoo running the business with a custom addons directory that nobody has read. Sometimes the source is on the server and not in any repository. Occasionally the standard addons have been edited in place, which you discover by comparing them against a clean copy of the same version.
The instinct is to rewrite. The right first move is an inventory. Every custom module gets read and written up in a paragraph: what it changes, which standard behaviour it overrides, whether anyone still uses the feature. Put the whole custom directory under version control immediately, even as a single initial commit of unknown code, because until then every change is unrecoverable. Then check the standard addons against a pristine copy of the same release, because that comparison tells you whether you have a maintenance problem or a fork. From there the decisions are concrete rather than emotional: this module is used and worth keeping, this one has not fired since 2023 and can go, this one duplicates something the standard product now does natively. Most instances in this state come out lighter, and a rewrite that looked inevitable turns into a fortnight of deletion followed by an upgrade that is merely tedious.
Manufacturing or inventory has outgrown the standard flow
Odoo's inventory and manufacturing apps cover a lot, and then a real factory arrives with a genuine exception. A finished item that has to be traced by lot back to a specific raw batch for a regulatory audit. A subcontractor who receives components and returns assemblies, and needs stock visibility across the boundary. Routing that depends on a machine's current calibration state. Quality checks that must block a transfer rather than warn about it.
The mistake here is building the exception before exhausting the configuration, because stock is the part of Odoo where a badly written customisation does the most damage. Moves and quants are interlocked, and code that writes to them without going through the framework's own methods produces inventory that does not reconcile, which nobody notices until the year-end count. The right hire for this work has done stock before and treats it with more caution than sales or CRM. The right sequence is to model the exception in configuration first, prove it fails, then extend rather than replace.
Odoo has to talk to something else, and the sync keeps drifting
An ecommerce storefront, a warehouse management system, a payment provider or a legacy application has to exchange data with Odoo, and after a few months the two disagree. Orders are missing on one side, stock differs, a retry created duplicates.
Odoo exposes its model layer over remote procedure call interfaces, which is convenient and dangerous in equal measure: you get access to nearly everything, including operations that skip business logic if called carelessly. What actually fixes drift is design rather than protocol choice. Decide which system owns each field, not each record. Give every message a stable key so a retry updates instead of inserting. Write failures to somewhere a human looks rather than to a log nobody reads. Build the reconciliation report on day one, not after the first argument, because the question is always "which of these two is right" and without a report the answer is an opinion. The broader patterns are covered on our API integration services in India page.
How an India-Based Odoo Engagement Really Runs
Odoo work has a specific offshore characteristic that most development does not: the developer often needs a business person to answer a question before they can continue. That makes the time gap matter more here than on a typical backend project, so it is worth being exact about it.
The arithmetic, without the marketing
India stays on UTC+5:30 for all twelve months and never moves its clocks, so the distance between us changes only when your clocks change. Take a normal office day here, 09:30 in the morning through to 18:30 in the evening IST, and in UTC that becomes the window from 04:00 until 13:00. The rest is subtraction.
| Where you are | Your own office day, converted to UTC | Hours you share with us |
|---|---|---|
| London in winter (GMT) | 09:00 to 17:00 UTC | four |
| London in summer (BST) | 08:00 to 16:00 UTC | five |
| Sydney (AEST) | 23:00 to 07:00 UTC | three, right at the opening of our day |
| Auckland (NZST) | 21:00 to 05:00 UTC | one |
| New York and Toronto (EST) | 14:00 to 22:00 UTC | none |
| San Francisco (PST) | 17:00 to 01:00 UTC | none |
The bottom two rows are the ones most suppliers hurry past. Keep both sides on ordinary office hours and a North American business shares literally no working time with a normal Indian day, east coast or west. Anyone telling you differently has either moved the Indian day without mentioning it or is counting on you not doing the subtraction yourself.
Buying overlap, and what it costs somebody
Overlap with North America is created by pushing the Indian day later, and the person who pays is the engineer. A day of 13:30 to 22:30 IST lands as 08:00 to 17:00 UTC, which returns the whole Eastern morning to a New York or Toronto team. Reaching the West Coast properly requires an Indian day that runs past midnight, which nobody should call sustainable as a standing arrangement, and we will not.
For most North American clients the pattern that survives contact with reality is a partial shift. Moving our day to something like 12:00 until 21:00 IST recovers around an hour and a half of the Eastern morning, enough for one call a day plus live discussion of whatever is stuck, and it does not require anybody to work nights forever. Whichever pattern is chosen gets written down and treated as part of how the engagement operates, rather than being allowed to slide week by week. One more point on this. Continuous cover is never a free extra. Running a clock properly means people rostered onto shifts, a handover recorded at every boundary, and a second engineer who knows the instance well enough to act alone. All of it is scoped on purpose and paid for.
Written handover carries the engagement
When the shared window is short, whatever exists only inside somebody's recollection of a call is effectively gone. Reasons behind design decisions belong in the ticket or the pull request thread. At the close of each day here the engineer posts a short summary: what progressed, what is obstructed, and the precise questions that need answering before Mumbai starts again. Each of those questions carries the engineer's own recommended answer, so you can approve in one word instead of booking a meeting.
Odoo adds one wrinkle worth planning for. A lot of questions are functional, not technical: should this discount apply before or after tax, does this warehouse allow negative stock, who is allowed to confirm a purchase over a threshold. If the only person who can answer those is in your timezone and busy, the developer stalls. The practical fix is a single named business owner on your side and a standing rule that unanswered questions get an assumption written down and implemented, flagged for review rather than left waiting.
Working safely on a system the business is using
Unlike a greenfield product, an Odoo instance is usually live while you are changing it, and a mistake shows up as an invoice a customer receives. So the mechanics are stricter than on ordinary development work. Changes go to a staging environment restored from real data, get tried by the people who do that job daily, and only then move to production. Deployments happen at an agreed time, never into a month-end close. Anything that touches accounting or stock gets a second pair of eyes before merge, on principle rather than by request. And restoring a backup is something you have practised, not something you are about to attempt for the first time under pressure.
The Parts That Go Wrong, and What Is Actually Done About Them
Offshore engagements fail in a fairly small number of predictable ways. Writing them down before you sign anything is more use to you than working them out yourself during the fourth month.
Quality drifting where you cannot see it
The lever is your review process, not anyone's promise. Work arrives as small pull requests into your repository, under your branch rules, with your people reviewing anything that touches accounting, stock or a shared model. For Odoo specifically, the review checklist that matters is short and non-negotiable: no edits to standard addons, no raw SQL writes, every override calls its parent unless there is a written reason, every new model has access rights declared, and every stored compute has its dependency list justified in the description. If you do not have a pipeline that can enforce a red build, building one is the first week's work rather than an afterthought.
Ownership of the code
What is written for you is yours. Intellectual property assignment, confidentiality and data handling obligations are agreed before any code exists, and the terms are negotiated rather than presented as fixed. Practically, the custom addons directory lives in your repository and the engineers work inside your accounts, so there is never a private copy of your business logic sitting on a machine you cannot see. Where personal data is involved, and in an ERP it usually is, bring your own counsel's view on your obligations rather than taking a supplier's summary of them.
Access to a system that holds everything
An ERP database contains your customers, your prices, your margins and your payroll if the module is installed. Access should be named and individual rather than shared, scoped to what the person is actually doing, and revoked the day they roll off. Most development needs no production access at all: a staging environment restored from an anonymised or reduced copy is enough for the majority of tickets, and the exceptions should be deliberate and time-boxed. No certification is claimed here that we do not hold. The commitment is narrower and more useful than a badge: engineers operate inside the controls your organisation already runs, and when a ticket appears to need broader access than the work justifies, you get told rather than asked.
When the person is not the right fit
Sometimes a placement does not work, and it is much better to say so in week two than in month five. Raise it early and we would rather hear it directly. How a swap is handled commercially gets settled in the paperwork between us, and no figure for it is going to be invented on a marketing page. What we can say now, ahead of any paperwork: raising it gets you a different Odoo developer inside 48 hours, and the seat is filled from a bench of candidates instead of the first name we happened to send. What matters technically is whether a departure is survivable, and that comes down to whether the work was committed continuously, reviewed and documented, or whether it lived in one person's head and one long branch. That is precisely why the written module inventory is not optional busywork.
The costs that never make it into the comparison
Ramp-up on an existing Odoo instance is slower than on a typical codebase, because the developer has to learn your business rules as well as your code, and those rules are frequently undocumented. Expect one to three weeks before real output, longer if the customisations are dense. Your own staff will spend time answering functional questions, and that time is a genuine cost that rarely appears on anyone's estimate. On top of that comes the friction the time gap adds to every decision, plus the fact that version upgrades are a recurring line in your budget rather than a single event. None of that is an argument against hiring offshore. It is an argument for pricing the first month realistically instead of assuming full output from the opening Monday.
Ways to Work With Us
An embedded developer
A single Odoo developer joins your team, takes work from your backlog, appears at your standup during the agreed window and raises pull requests in the repository you own. It fits organisations that already have someone technical making the calls and simply need more hands, and it leaves you deciding directly what gets built and what gets refused.
A small team with a lead
Two or three developers plus someone who owns coordination and the written handover, which fits an upgrade programme or the handover of a whole instance rather than a stream of tickets. Your people deal with the lead rather than with three individuals, which keeps the coordination load on your side roughly constant even as the group gets bigger.
A scoped piece of work
One outcome, one finish line: document and stabilise an instance you inherited, carry a set of custom modules across a single version, build a named integration, or fix a system that has slowed to a crawl. Most clients start here for a good reason. The piece is small enough that you can form a view of how we work before committing to anything longer.
Not sure yet which of the three fits? Our broader guide to hiring developers in India sets out how the roles and the levels compare.
What Our Odoo Developers Work With
Frequently Asked Questions
Can I just hire a good Python developer and let them learn Odoo?
You can, and for a long engagement it sometimes works out cheaper than waiting for a specialist. Budget for it properly though. The language is not the obstacle; the obstacle is a framework with its own inheritance rules, its own ORM semantics, its own XML view layer and a set of conventions that are enforced by convention rather than by the interpreter. A strong Python engineer usually needs a few months of supervised work before their modules stop causing upgrade pain.
How do you check that a candidate will not edit Odoo's own source?
We give them a change request that is easiest to satisfy by editing an addon in place, and watch what they reach for. The answer we want is a new module that declares a dependency, extends the model with _inherit, and patches the view with an xpath. If they open the standard addon and start typing, that tells us the next upgrade will be paid for twice, once by them and once by whoever inherits the instance.
What actually breaks when we move to a newer Odoo version?
Rarely the database, usually the code. Fields get renamed or dropped, methods change signature, the XML element your xpath anchors to disappears, a helper you overrode is gone, and a widget your form used no longer exists. The database side is largely mechanical. Reworking every custom module against the new addons, then retesting the business processes that run through them, is the part that takes real weeks.
Should we be on Community or Enterprise?
It depends on which apps you need and whether you are willing to build or source the gaps yourself. Community is open source and extensible in every direction; Enterprise is a paid subscription that adds functionality on top. What sits on which side moves between releases, so check Odoo's current comparison rather than trusting any page including this one. From a developer's point of view the mechanics of writing a module are the same either way.
Our Odoo has become slow. Is that hosting or is that our code?
In our experience it is usually a stored computed field with an over-broad depends, a loop that browses one record at a time, an unindexed field being sorted on, or a record rule whose domain drags a join into every query. Bigger hardware moves the pain later without removing it. The first hour of the diagnosis should be spent reading the SQL the server emits, not resizing the instance.
How much working time will we actually share with an Odoo team in India?
India sits on UTC+5:30 permanently, with no seasonal clock change at either end of the year. Our office day converts to the window running from 04:00 to 13:00 UTC. Measured against that, London gets four hours in winter and five in summer, Sydney around three, Auckland about one, while New York, Toronto and San Francisco get nothing whatsoever on standard hours. Shared time with North America exists only if the Indian day is deliberately moved later, which is a decision taken with you up front.
Who owns the custom modules your developers write for us?
You do. Assignment of intellectual property, confidentiality and data handling sit in the agreement before the first commit, and the terms are settled with you rather than handed to you. In practice the code lives in your repository from day one, so your custom addons directory is never something you have to ask us for. If you are running Community and want to publish anything back, that is your decision to make.
How quickly can an Odoo developer start on our instance?
We keep engineers available rather than starting a recruitment cycle after your brief arrives, so a shortlist reaches you inside 48 hours and work can begin within 7 days of you choosing someone. Hiring the same profile directly in India means sourcing, interview rounds, an offer and then a notice period, which is how a search that starts in one quarter lands a person in the next.