Introduction
Custom Odoo development often begins with a small requirement. A business needs an additional approval rule, a new field, a specialized report or integration with another system. The first version of the module may be easy to understand because only one developer and a limited amount of code are involved.
The challenge appears after months or years of development. More features are added, business rules change and additional developers modify the same codebase. Without coding standards and automated testing, a small Odoo customization can gradually become difficult to understand, risky to deploy and expensive to upgrade.
Odoo's own coding guidelines state that proper code improves readability, simplifies maintenance, reduces complexity and makes applications more reliable. The guidelines also recommend a predictable module structure with separate directories for models, controllers, views, tests and other module components.
For businesses maintaining several Odoo custom modules, clean code should therefore be supported by a continuous integration strategy. Every meaningful change should be reviewed and tested before reaching production so problems are discovered during development rather than by employees using the live ERP.
Why Clean Odoo Codebases Become Harder to Maintain
Custom Odoo modules rarely remain unchanged. A sales approval module may later require multi-company support. An inventory customization may gain barcode functionality while an external connector may need new authentication or mapping rules.
If each developer solves the immediate problem without considering module structure, dependencies and test coverage, technical debt begins accumulating.
Common warning signs include:
- Large Python files containing unrelated models
- Business rules repeated across several methods
- Hard-coded database IDs or company-specific values
- Unnecessary module dependencies
- Copied standard Odoo methods
- Large copied XML views
- Security rules without automated validation
- No tests for business-critical workflows
- Production deployments performed manually
- Bugs discovered only after code reaches users
The cost appears most clearly when a module needs to be changed or upgraded. Developers spend more time understanding existing code than implementing the actual requirement.
Follow a Predictable Odoo Module Structure
Odoo recommends organizing modules into well-defined directories. Standard areas include models/, controllers/, views/, static/ and tests/ while optional directories can contain wizards, reports and additional resources.
A custom module might therefore contain:
custom_sale_approval/ ├── __init__.py ├── __manifest__.py ├── models/ │ ├── __init__.py │ └── sale_order.py ├── security/ │ ├── ir.model.access.csv │ └── sale_approval_security.xml ├── views/ │ └── sale_order_views.xml ├── data/ │ └── approval_data.xml └── tests/ ├── __init__.py └── test_sale_approval.py
Odoo's guidelines also recommend placing inherited models in appropriately named files so developers can quickly understand which standard model is being modified.
Predictable structure becomes increasingly valuable when a company maintains dozens of custom apps.
Keep Modules Focused on One Business Responsibility
A clean codebase should avoid one large module containing every company customization.
For example, a module named company_custom may initially contain one sales field but eventually include accounting reports, inventory approvals, purchase logic and API integrations. That architecture increases coupling.
A better structure may separate responsibilities into modules such as:
- custom_sale_approval
- custom_stock_validation
- custom_purchase_request
- custom_account_report
- custom_shipping_connector
Each module should declare only the dependencies required for its own functionality.
This modular architecture makes testing more focused because a failure in the shipping connector does not automatically require developers to inspect unrelated accounting customization.
Follow Odoo Coding Guidelines Consistently
Clean code is not simply code that runs without errors.
Odoo publishes development guidelines covering Python conventions, imports, XML identifiers, file organization, JavaScript and module structure. The guidelines recommend designing code so it remains extendable and warn developers against unnecessary transaction commits inside normal business logic.
A development standard should therefore cover areas such as:
| Development Area | Recommended Practice |
|---|---|
| Models | Separate major models logically |
| Inherited Models | Keep them in clearly named files |
| Views | Extend existing views instead of copying them |
| XML IDs | Use consistent descriptive names |
| Dependencies | Declare only required modules |
| Security | Maintain ACLs and record rules in module files |
| Tests | Store tests with the module |
| JavaScript | Follow Odoo's current frontend conventions |
| Configuration | Avoid hard-coded environment-specific values |
The objective is not formatting code simply for appearance. Consistency reduces the amount of time another developer needs to understand the module.
Keep Business Logic Out of Views
Critical business rules should remain in backend code rather than being implemented only through UI behavior.
For example, suppose orders above a defined amount require manager approval. Hiding the Confirm button from some users does not guarantee that the approval rule is enforced because transactions may also be triggered through APIs, automated actions or other server-side methods.
The validation should therefore be enforced through the model layer.
Views can control how information is presented but backend logic should determine whether the operation is valid.
This also makes testing easier because automated Python tests can call the business methods directly without depending entirely on a browser interface.
Treat Security as Testable Business Logic
Custom module testing should include security.
Odoo uses model-level access rights and record rules to control access to business data. Access rights determine whether a user can create, read, write or delete records while record rules restrict which specific records are available to the user.
A custom module may work correctly for an Administrator while exposing information incorrectly to a normal employee.
Testing should therefore verify scenarios such as:
- Standard user can read permitted records
- Standard user cannot edit protected records
- Manager can approve eligible transactions
- Users cannot see records belonging to restricted companies
- Portal users cannot access internal models
- Restricted fields remain inaccessible
Security regression tests are particularly valuable because a future developer may change a group or record rule without realizing how many workflows depend on it.
Add Python Tests to Every Business-Critical Module
Odoo supports Python tests through its testing framework. Custom tests are placed inside a module's tests package and test filenames should normally start with test_.
A simple approval test could look like:
from odoo.tests.common import TransactionCase class TestSaleApproval(TransactionCase): def test_order_requires_approval(self): order = self.env["sale.order"].create({ "partner_id": self.env.ref("base.res_partner_1").id, }) # Add test-specific data and execute business method here. self.assertTrue(order.exists())
The exact test should validate the real business rule rather than only checking whether a record can be created.
Useful tests may cover calculations, validations, workflow transitions, generated records and error conditions.
Test Business Outcomes Instead of Individual Lines of Code
A test suite should represent important business behavior. Suppose a company develops a custom purchase approval module. Testing only whether the approval_state field exists provides very little protection.
More valuable scenarios include:
- Request below threshold can proceed normally
- Request above threshold requires approval
- Unauthorized employee cannot approve it
- Authorized manager can approve it
- Rejected request cannot generate a purchase order
- Approved request creates the expected downstream transaction
This creates meaningful regression protection.
If a future code change accidentally bypasses an approval condition, the CI pipeline can detect the problem before production deployment.
Understand at_install and post_install Tests
Odoo tests use tags to determine when they should run.
Tests derived from Odoo's testing classes are normally tagged with standard and at_install by default. at_install tests run after the corresponding module is installed. Tests can instead be marked post_install and -at_install when they should run after the complete module installation process.
For example:
from odoo.tests import tagged from odoo.tests.common import TransactionCase @tagged("post_install", "-at_install") class TestFullBusinessScenario(TransactionCase): def test_complete_scenario(self): pass
The correct tag depends on the behavior being validated.
Tests that should verify the module immediately after installation are good candidates for at_install while broader integration behavior may require post_install.
Use Test Tags to Keep CI Efficient
Large Odoo databases can contain hundreds or thousands of tests.
Running every possible test after every tiny change may slow development unnecessarily. Odoo allows tests to be filtered through --test-tags using tags, module names, classes and individual test methods.
Examples include:
odoo-bin --test-tags /custom_sale_approval
or a more focused test:
odoo-bin --test-tags /custom_sale_approval:TestSaleApproval
Teams can also define categories such as:
- standard
- integration
- slow
- post_install
- external
Fast module tests can run on every commit while heavier integration tests can run before a release.
Include JavaScript and Browser-Level Testing Where Needed
Python tests cannot validate everything.
Custom Odoo modules may include JavaScript components, POS extensions, website interactions or complex client actions. Odoo's testing framework supports Python tests, JavaScript unit tests and tours that simulate interactions between the browser and backend.
A useful testing strategy may therefore include:
| Test Type | Best Use |
|---|---|
| Python Tests | ORM and business logic |
| Security Tests | ACLs, record rules and permissions |
| JavaScript Unit Tests | Frontend components |
| Tours | End-to-end browser workflows |
| Integration Tests | Interaction between modules |
| UAT | Real business-user validation |
Not every module requires every type of test.
A backend accounting calculation may require extensive Python tests but little browser testing while a POS customization may require significantly more frontend validation.
Use Git as the Source of Truth
Continuous integration requires disciplined source control.
Every production customization should exist in a version-controlled repository rather than being modified directly on the live server.
Useful Git practices include:
- One feature or fix per branch
- Small reviewable commits
- Meaningful commit messages
- Pull-request review before merge
- No production-only uncommitted files
- Versioned configuration where practical
- Protected production branches
- Clear release tags for important deployments
A Git history should allow developers to answer who changed a business rule, why it changed and which release introduced it. This becomes invaluable when investigating regressions.
Use Odoo.sh Development Builds for Automated Testing
Odoo.sh integrates directly with GitHub and provides a continuous integration environment for customized Odoo projects. Development branches create fresh databases, load demo data and run unit tests by default when new commits create builds.
A development build fails when the associated tests fail which gives developers immediate feedback before the branch reaches production.
Odoo.sh builds can appear as successful, successful with warnings or failed depending on what happens during build creation.
This makes development branches useful for catching:
- Module installation errors
- Broken dependencies
- XML errors
- Python exceptions
- Failed unit tests
- Incorrect initialization logic
The development environment should be treated as a technical quality gate rather than merely a temporary demo database.
Use Staging for Real Production Data Validation
Passing unit tests does not prove that a customization works against years of existing business data.
Odoo.sh staging branches use neutralized copies of the production database. Outgoing emails are intercepted while payment providers and shipping connectors are placed into test-oriented conditions. This allows teams to test changes against realistic production records without altering the live database.
An important detail is that Odoo.sh does not run the normal unit test suite automatically on staging branches. Development builds are therefore used for automated tests while staging is primarily intended for validation against production-like data.
A strong release should use both layers.
Do Not Depend on Production to Run Your Tests
Production is the worst place to discover a regression.
Odoo.sh does not run unit tests on production updates because doing so would increase production unavailability. If a production update cannot load successfully, Odoo.sh can revert to the previous successful revision and roll the database back to its previous state.
That protection is useful but it should not replace pre-deployment testing.
The intended quality control should happen before production through development tests, staging validation and business-user acceptance.
Rollback capability limits damage. It does not make risky deployment practices acceptable.
Add Static Checks and Linters Before Odoo Tests
Not every error needs a full Odoo database to detect it. Static checks can identify common development problems much earlier. Odoo's coding guidance explicitly recommends using linters as part of development quality practices.
Teams may automatically review:
- Python formatting and common mistakes
- XML validity
- JavaScript syntax
- Manifest structure
- Missing imports
- Unused code
- Dependency declarations
- Naming conventions
These checks are fast enough to run before more expensive database installation and integration tests. The exact tools can vary according to the development team's standards.
Test Module Installation and Upgrade Separately
A module can install successfully on a new database while failing when an existing database is upgraded.
This happens because production systems already contain records, previous field definitions and historical configuration.
CI should therefore test both:
Fresh Installation
and:
Module Upgrade
Odoo tests can be executed during installation or update using --test-enable and --test-tags.
For long-lived custom apps, upgrade testing becomes especially important when models, fields or XML data are changed. Historical business records should remain valid after the module is updated.
Include External Integrations in the Test Strategy
Custom Odoo modules often connect with payment providers, logistics systems, CRMs or marketplaces. A connector may pass all internal Python tests while failing because an API payload changed.
Integration tests should verify:
- Authentication
- Request mapping
- Response mapping
- Error handling
- Duplicate-event handling
- Webhooks
- Scheduled synchronization
- Timeouts
- Retry behavior
External sandbox environments should be used where available.
Browseinfo's current Odoo migration-testing guidance similarly emphasizes revalidating custom modules and third-party integrations because framework or dependency changes can affect previously working custom functionality.
Measure Test Coverage by Business Risk
A large number of tests does not automatically create a strong test suite. A module containing 100 tests for simple field defaults but no test for a business-critical invoice calculation still leaves the organization exposed.
Testing priority should reflect business impact.
| Risk Level | Example | Testing Priority |
|---|---|---|
| Critical | Accounting posting | Very High |
| Critical | Inventory valuation | Very High |
| High | Approval workflow | High |
| High | External payment API | High |
| Medium | Custom report filter | Medium |
| Low | Cosmetic label change | Low |
The objective is not achieving a vanity test count. It is preventing the failures that would damage business operations.
Review Performance as Part of CI Quality
Correct code can still be inefficient.
A custom method that performs hundreds of repeated ORM queries may return the correct result but gradually slow the ERP as data volume increases.
Performance-sensitive customizations should be tested with realistic record volumes. Developers should review batch operations, computed fields, search patterns and loops that generate repeated queries.
Odoo's coding guidance encourages maintainable patterns while Odoo.sh also provides profiling tools that can produce flame graphs showing how workers spend execution time.
Performance should therefore be treated as a code-quality concern rather than only a server-sizing problem.
Make Upgrades Part of the Development Standard
Clean code becomes most valuable when a new Odoo version arrives.
Custom modules may need changes because standard models, methods, XML views or frontend components have evolved. Poorly structured modules make this analysis much slower.
Browseinfo's recent custom-module upgrade guidance recommends reviewing manifests, Python logic, XML views, security, controllers, reports, static assets and integrations before migrating custom applications to a newer Odoo version.
Upgrade-safe development should therefore include:
- Minimal core coupling
- Supported model inheritance
- Supported view inheritance
- Explicit dependencies
- Automated business tests
- Security tests
- Documented integrations
- Migration logic for structural changes
Testing should be considered part of module design from the first version rather than added only when an upgrade begins.
How Browseinfo Can Help Maintain Clean Odoo Codebases
Browseinfo's current custom-module development guidance recommends building custom functionality as separate modules instead of modifying Odoo core and includes testing, staging deployment and ongoing maintenance as part of the development lifecycle.
For organizations with large custom repositories, a code-quality engagement can include:
- Custom module architecture review
- Dependency analysis
- Coding-standard implementation
- Automated Python tests
- Security testing
- Integration testing
- Odoo.sh CI setup
- Staging validation
- Upgrade compatibility review
- Performance profiling
- Refactoring legacy modules
- Regression-test development
Browseinfo's current upgrade guidance also emphasizes automated tests and staging environments as important controls when refactoring proprietary extensions into upgrade-safe modules.
The objective should be to make every future change easier to understand, test and deploy.
Common Odoo CI and Testing Mistakes
One common mistake is writing tests only after a serious production failure. At that point the business has already paid the cost of missing regression protection.
Another mistake is testing exclusively as Administrator. Real users operate under ACLs and record rules so permission scenarios need their own validation.
Other common problems include:
- Running only fresh-install tests
- Ignoring existing production data
- Skipping integration failure scenarios
- Deploying directly from developer machines
- Maintaining production-only code changes
- Treating staging as optional
- Writing tests that do not verify actual business outcomes
- Disabling failing tests instead of fixing the cause
Continuous integration provides value only when failing checks are treated as deployment blockers rather than inconvenient warnings.
Frequently Asked Questions
1. Does Odoo support automated tests for custom modules?
Yes. Odoo provides Python tests for backend business logic, JavaScript tests for frontend functionality and tours for integration or browser-level scenarios.
2. Where should custom Odoo tests be stored?
Tests should normally be stored in a module's tests sub-package with test files named using the test_ prefix and imported through tests/__init__.py.
3. What is the difference between at_install and post_install?
at_install tests normally execute after the relevant module is installed. post_install tests are designed to run after all modules have been installed and are commonly paired with -at_install.
4. Does Odoo.sh automatically run tests?
Odoo.sh development builds create fresh databases with demo data and run unit tests by default. Staging branches use production database copies but do not automatically run the normal unit test suite.
5. Should every Odoo customization have automated tests?
Business-critical rules should have automated regression coverage wherever practical. Minor visual changes may require less testing than financial calculations, inventory rules or integrations.
6. Can Odoo tests verify security rules?
Yes. Test cases can run operations under different users and verify that access rights, record rules and protected business operations behave as expected.
7. Why is staging still needed if all automated tests pass?
Automated tests validate defined scenarios while staging verifies the customization against realistic production data, configuration and user workflows. Both address different types of risk.
8. How does automated testing help Odoo upgrades?
A regression suite allows developers to confirm whether existing custom workflows continue producing expected results after framework changes or module refactoring. This reduces the amount of upgrade validation that depends entirely on manual testing.
Conclusion
Clean Odoo development is not achieved by formatting Python files once before deployment. It requires a development system that keeps the codebase understandable as requirements change.
A strong custom module should have a clear responsibility, limited dependencies, predictable file organization and supported Odoo extension patterns. Security rules should be explicit while important business behavior should be protected by automated tests.
Continuous integration then turns those standards into repeatable controls.
Odoo's testing framework can validate Python business logic, frontend behavior and integration scenarios while test tags allow developers to control when different suites run. Odoo.sh adds development builds that automatically execute unit tests and staging branches that validate new code against production-like data.
The strongest teams do not wait for the next major Odoo upgrade to discover whether their modules are maintainable. They test every important change as part of normal development.
When Odoo coding standards, modular architecture, Git version control, automated testing, Odoo.sh continuous integration and staging validation work together, custom development becomes easier to maintain and significantly safer to deploy.
That is the real purpose of a clean Odoo codebase: not simply making code look organized but ensuring every future developer can change it with confidence without turning routine customization into a production risk.