Create an OCA-style module
1. Decide where it lives
- A first-party module for your own project → your project's custom addons
path (e.g.
odoo/custom/src/private in Doodba-based projects).
- A module meant to eventually live in an OCA (or other) repo → create it
inside that repo's own checkout, and register it wherever your project's
addon-path mechanism expects it (e.g.
addons.yaml in Doodba-based
projects).
2. Scaffold
odoo-bin scaffold <module_name> <path>
This is Odoo's own scaffolding command — it gives you the base skeleton
(__init__.py, __manifest__.py, models/, views/, security/, demo/,
tests/). Many project setups wrap it (e.g. Doodba's invoke scaffold,
usually run inside a container) — use your project's wrapper if it has one,
the result is the same. Treat the output as a starting point, not the final
shape — fix it up per the checklist below.
3. Manifest (__manifest__.py)
A standard OCA .pylintrc (loading the pylint_odoo plugin) enforces:
license key is required, and must be one of: AGPL-3, GPL-2,
GPL-2 or any later version, GPL-3, GPL-3 or any later version, LGPL-3,
OPL-1, OEEL-1.
description and active keys are deprecated — don't add them
(description goes in readme/DESCRIPTION.md/.rst, see below; use
auto_install for install-state logic, not active).
- If the repo's
.pylintrc sets manifest-required-authors (or the pre-16
manifest_required_authors), that author string is required in
author — typically "<author>, Odoo Community Association (OCA)" for a
module meant for an OCA repo.
version must follow Odoo's X.Y.Z.W.V manifest-version format and start
with the target Odoo series (e.g. 18.0.1.0.0).
- On Odoo ≥16 additionally:
development_status must be one of the allowed
OCA values (Alpha/Beta/Production/Stable/Mature), and maintainers
should be a list of GitHub handles.
- Other standard OCA keys worth setting even where lint doesn't force them:
summary (one-liner), category, website (repo/addon URL),
external_dependencies ({"python": [...], "bin": [...]}), application
(only True for a real top-level app, not a technical/integration module).
4. README, not description
Never hand-write README.rst — OCA's oca-gen-addon-readme tool (from
OCA/maintainer-tools, usually
wired as a pre-commit hook) generates it from fragment files under
<module>/readme/. Recognized fragment names, in the order they appear in
the generated README:
DESCRIPTION — what the module does (the only one effectively required —
an empty README fails the missing-readme pylint-odoo check).
CONTEXT — use cases/context, optional.
INSTALL — extra install steps beyond normal dependency install, optional.
CONFIGURE — configuration steps, optional.
USAGE — how to use it, optional but recommended for anything with a
non-obvious UI flow.
DEVELOP — notes for future contributors, optional.
ROADMAP — known issues/planned work, optional.
HISTORY — changelog, optional (some OCA repos wire a towncrier/
newsfragments automation for this instead of hand-editing it).
CONTRIBUTORS — one line per contributor, optional.
CREDITS — funders/sponsors, optional.
Fragments can be written as .rst or .md — oca-gen-addon-readme
auto-detects the format and can auto-convert one to the other. Check the
target repo's own .pre-commit-config.yaml for a
--convert-fragments-to-markdown flag before picking one; don't assume —
plenty of OCA repos still use .rst.
Module icons (static/description/icon.png) can be generated with
oca-gen-addon-icon, also from maintainer-tools — reuse the standard OCA
icon unless the module needs a custom one.
5. Code structure
- One model per file, file name matches the model's main purpose
(
models/<model_name>.py), each file imported from models/__init__.py.
- Every override of
create/write/unlink/compute/search/inverse
methods must call super() (method-required-super) and compute/inverse/search
methods must actually be wired to the right @api.depends/@api.depends_context
decorators (method-compute, method-inverse, method-search checks).
- No
eval/exec on untrusted input (eval-used, eval-referenced).
- No raw string-interpolated SQL (
sql-injection — parametrize cr.execute calls).
- On Odoo ≥16: don't put wizards inside
models/ (no-wizard-in-models), don't
write to fields inside a @api.depends compute (no-write-in-compute), and any
outbound HTTP call needs an explicit timeout= (external-request-timeout).
- Don't set manifest/field values equal to their default —
OCA/odoo-pre-commit-hooks' oca-checks-odoo-module auto-fixes (and will
just strip) redundant entries like "installable": True or "data": []
(manifest-superfluous-key) and a field's string= when it only restates
the Title-Case of the field name (field-string-redundant) — don't add
either in the first place.
- Don't leave an unused
_logger = logging.getLogger(__name__) in a model
(unused-logger) — only declare it where actually used.
6. Security
security/ir.model.access.csv — every new model needs at least one access
line; see the odoo-module-security skill for the full checklist.
- Add
security/<module>_security.xml for record rules/groups if the module
needs access control beyond basic CRUD.
7. Tests
Add a tests/ package (__init__.py importing test_* modules) — see the
odoo-module-test skill for conventions, then run it using your project's
Odoo test runner (e.g. invoke test -m <module_name> in Doodba-based
projects, or odoo-bin -i <module_name> --test-enable --test-tags /<module_name> --stop-after-init otherwise) before considering the module
done.
8. Finish
pre-commit run -a
(or your project's wrapper for it, e.g. Doodba's invoke lint) — this runs
the full OCA pre-commit suite, not just pylint-odoo: ruff/black+isort+
flake8 (formatting), prettier (XML/JS/etc.),
OCA/odoo-pre-commit-hooks' oca-checks-odoo-module (structural OCA checks)
and oca-checks-po --fix (translation file hygiene), and — if configured —
oca-gen-addon-readme (regenerates README.rst/README.html from the
fragments in step 4). Fix everything it reports before calling the module
done — this is the same check that runs in CI.
If the module lives in an upstream OCA repo checkout, also make sure it's
registered wherever your project's addon-path mechanism expects (e.g.
addons.yaml in Doodba-based projects) so it actually gets installed.
1---2name: odoo-module-create3description: Scaffold a new Odoo module that follows OCA conventions, then bring it up to full OCA shape (manifest, readme fragments, security, tests) and pass standard OCA lint checks. Use when asked to create, add, or scaffold a new Odoo module/addon.4---56# Create an OCA-style module78## 1. Decide where it lives910- A first-party module for your own project → your project's custom addons11 path (e.g. `odoo/custom/src/private` in Doodba-based projects).12- A module meant to eventually live in an OCA (or other) repo → create it13 inside that repo's own checkout, and register it wherever your project's14 addon-path mechanism expects it (e.g. `addons.yaml` in Doodba-based15 projects).1617## 2. Scaffold1819```20odoo-bin scaffold <module_name> <path>21```2223This is Odoo's own scaffolding command — it gives you the base skeleton24(`__init__.py`, `__manifest__.py`, `models/`, `views/`, `security/`, `demo/`,25`tests/`). Many project setups wrap it (e.g. Doodba's `invoke scaffold`,26usually run inside a container) — use your project's wrapper if it has one,27the result is the same. Treat the output as a starting point, not the final28shape — fix it up per the checklist below.2930## 3. Manifest (`__manifest__.py`)3132A standard OCA `.pylintrc` (loading the `pylint_odoo` plugin) enforces:3334- `license` key is **required**, and must be one of: `AGPL-3`, `GPL-2`,35 `GPL-2 or any later version`, `GPL-3`, `GPL-3 or any later version`, `LGPL-3`,36 `OPL-1`, `OEEL-1`.37- `description` and `active` keys are **deprecated** — don't add them38 (description goes in `readme/DESCRIPTION.md`/`.rst`, see below; use39 `auto_install` for install-state logic, not `active`).40- If the repo's `.pylintrc` sets `manifest-required-authors` (or the pre-1641 `manifest_required_authors`), that author string is **required** in42 `author` — typically `"<author>, Odoo Community Association (OCA)"` for a43 module meant for an OCA repo.44- `version` must follow Odoo's `X.Y.Z.W.V` manifest-version format and start45 with the target Odoo series (e.g. `18.0.1.0.0`).46- On Odoo ≥16 additionally: `development_status` must be one of the allowed47 OCA values (`Alpha`/`Beta`/`Production/Stable`/`Mature`), and `maintainers`48 should be a list of GitHub handles.49- Other standard OCA keys worth setting even where lint doesn't force them:50 `summary` (one-liner), `category`, `website` (repo/addon URL),51 `external_dependencies` (`{"python": [...], "bin": [...]}`), `application`52 (only `True` for a real top-level app, not a technical/integration module).5354## 4. README, not `description`5556Never hand-write `README.rst` — OCA's `oca-gen-addon-readme` tool (from57[`OCA/maintainer-tools`](https://github.com/OCA/maintainer-tools), usually58wired as a pre-commit hook) generates it from fragment files under59`<module>/readme/`. Recognized fragment names, in the order they appear in60the generated README:6162- `DESCRIPTION` — what the module does (the only one effectively required —63 an empty README fails the `missing-readme` pylint-odoo check).64- `CONTEXT` — use cases/context, optional.65- `INSTALL` — extra install steps beyond normal dependency install, optional.66- `CONFIGURE` — configuration steps, optional.67- `USAGE` — how to use it, optional but recommended for anything with a68 non-obvious UI flow.69- `DEVELOP` — notes for future contributors, optional.70- `ROADMAP` — known issues/planned work, optional.71- `HISTORY` — changelog, optional (some OCA repos wire a towncrier/72 `newsfragments` automation for this instead of hand-editing it).73- `CONTRIBUTORS` — one line per contributor, optional.74- `CREDITS` — funders/sponsors, optional.7576Fragments can be written as `.rst` or `.md` — `oca-gen-addon-readme`77auto-detects the format and can auto-convert one to the other. **Check the78target repo's own `.pre-commit-config.yaml`** for a79`--convert-fragments-to-markdown` flag before picking one; don't assume —80plenty of OCA repos still use `.rst`.8182Module icons (`static/description/icon.png`) can be generated with83`oca-gen-addon-icon`, also from `maintainer-tools` — reuse the standard OCA84icon unless the module needs a custom one.8586## 5. Code structure8788- One model per file, file name matches the model's main purpose89 (`models/<model_name>.py`), each file imported from `models/__init__.py`.90- Every override of `create`/`write`/`unlink`/`compute`/`search`/`inverse`91 methods must call `super()` (`method-required-super`) and compute/inverse/search92 methods must actually be wired to the right `@api.depends`/`@api.depends_context`93 decorators (`method-compute`, `method-inverse`, `method-search` checks).94- No `eval`/`exec` on untrusted input (`eval-used`, `eval-referenced`).95- No raw string-interpolated SQL (`sql-injection` — parametrize `cr.execute` calls).96- On Odoo ≥16: don't put wizards inside `models/` (`no-wizard-in-models`), don't97 write to fields inside a `@api.depends` compute (`no-write-in-compute`), and any98 outbound HTTP call needs an explicit `timeout=` (`external-request-timeout`).99- Don't set manifest/field values equal to their default —100 `OCA/odoo-pre-commit-hooks`' `oca-checks-odoo-module` auto-fixes (and will101 just strip) redundant entries like `"installable": True` or `"data": []`102 (`manifest-superfluous-key`) and a field's `string=` when it only restates103 the Title-Case of the field name (`field-string-redundant`) — don't add104 either in the first place.105- Don't leave an unused `_logger = logging.getLogger(__name__)` in a model106 (`unused-logger`) — only declare it where actually used.107108## 6. Security109110- `security/ir.model.access.csv` — every new model needs at least one access111 line; see the `odoo-module-security` skill for the full checklist.112- Add `security/<module>_security.xml` for record rules/groups if the module113 needs access control beyond basic CRUD.114115## 7. Tests116117Add a `tests/` package (`__init__.py` importing `test_*` modules) — see the118`odoo-module-test` skill for conventions, then run it using your project's119Odoo test runner (e.g. `invoke test -m <module_name>` in Doodba-based120projects, or `odoo-bin -i <module_name> --test-enable --test-tags121/<module_name> --stop-after-init` otherwise) before considering the module122done.123124## 8. Finish125126```127pre-commit run -a128```129130(or your project's wrapper for it, e.g. Doodba's `invoke lint`) — this runs131the full OCA pre-commit suite, not just `pylint-odoo`: `ruff`/`black`+`isort`+132`flake8` (formatting), `prettier` (XML/JS/etc.),133`OCA/odoo-pre-commit-hooks`' `oca-checks-odoo-module` (structural OCA checks)134and `oca-checks-po --fix` (translation file hygiene), and — if configured —135`oca-gen-addon-readme` (regenerates `README.rst`/`README.html` from the136fragments in step 4). Fix everything it reports before calling the module137done — this is the same check that runs in CI.138139If the module lives in an upstream OCA repo checkout, also make sure it's140registered wherever your project's addon-path mechanism expects (e.g.141`addons.yaml` in Doodba-based projects) so it actually gets installed.