Metadata-Version: 2.5
Name: odoo-doctor
Version: 0.11.2
Summary: Unified health scoring for Odoo custom addons
Project-URL: Homepage, https://github.com/minhhq-a1/odoo-doctor
Project-URL: Changelog, https://github.com/minhhq-a1/odoo-doctor/blob/main/CHANGELOG.md
Project-URL: Bug Tracker, https://github.com/minhhq-a1/odoo-doctor/issues
Author: Odoo Doctor Contributors
License-Expression: MIT
License-File: LICENSE
Keywords: addons,health-check,lint,odoo,static-analysis
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Requires-Dist: lxml>=5.0
Requires-Dist: rich>=13.0
Requires-Dist: tomli>=2.0; python_version < '3.11'
Requires-Dist: typer>=0.16
Provides-Extra: dev
Requires-Dist: pygls<3,>=2.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: pyyaml>=6.0; extra == 'dev'
Provides-Extra: lsp
Requires-Dist: pygls<3,>=2.0; extra == 'lsp'
Description-Content-Type: text/markdown

# Odoo Doctor 🩺

**Unified health scoring for Odoo custom addons.**

Combines confidence-aware static analysis with optional external linters (Ruff, Pylint-Odoo) to produce a single **0–100 score per addon** — designed for CI pipelines and AI coding agents.

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

---

## Quick Start

```bash
# 1. Chạy ngay (không cần install)
pipx run odoo-doctor scan .

# 2. Install global
pip install odoo-doctor
odoo-doctor scan .

# 3. JSON output cho CI / agents
odoo-doctor scan . --json

# 4. Fail nếu score < 80
odoo-doctor scan . --min-score 80

# 5. Chỉ scan file đã thay đổi (PR review)
odoo-doctor scan . --diff main --json

# 6. Báo cáo HTML một file (mở bằng trình duyệt, đính kèm CI)
odoo-doctor scan . --html report.html

# 7. Upgrade readiness: how far is each module from Odoo 19?
odoo-doctor scan . --odoo-version 19.0 --upgrade-report

# 8. Reuse the last scan when nothing changed (`--cache`; only on repositories you trust:
#    a cache file committed to the repo can forge a hit)
odoo-doctor scan . --cache
```

**Upgrade readiness.** `--upgrade-report` (with `--odoo-version <target>`) groups the Upgrade
Safety findings by kind of change, counts them, labels each one mechanical (a pure rename, fixer
planned) or needs review, and scores every module 0-100. With `--json` it adds the top-level key
`upgrade_report`. With `--odoo-version X` every module is evaluated as targeting X regardless of its
manifest version; without it, rules follow each module's manifest version (a `17.0.x` module shows an
empty report, with a warning). See [docs/upgrade-report.md](docs/upgrade-report.md).

---

## What it checks

| Rule | Tier | Category |
|------|------|----------|
| `raw-sql-string-interpolation` | P0 | Security |
| `eval-usage` | P0 | Security |
| `missing-access-csv` | P0 | Security |
| `unknown-model-in-access-csv` | P1 | Correctness |
| `duplicate-xml-id` | P1 | Correctness |
| `view-field-not-in-model` | P1 | Correctness |
| `button-method-not-found` | P1 | Correctness |
| `orphan-view` | P2 | Maintainability |
| `missing-xml-ref` | P1 | Correctness |
| `manifest-missing-dependency` | P1 | Module Hygiene |
| `manifest-license-incompatible` | P2 | Module Hygiene |
| `missing-external-dependency` | P2 | Module Hygiene |
| `manifest-missing-required-fields` | P2 | Module Hygiene |
| `search-in-loop` | P1 | Performance |
| `create-in-loop` | P1 | Performance |
| `write-in-loop` | P1 | Performance |
| `n-plus-one-read` | P1 | Performance |
| `public-controller-sudo-risk` | P1 | Security |
| `sudo-without-comment` | P1 | Security |
| `record-rule-without-domain` | P1 | Security |
| `unbounded-search` | P2 | Performance |
| `manifest-data-order-risk` | P2 | Module Hygiene |
| `override-missing-super` | P1 | Correctness |
| `compute-missing-depends` | P2 | Correctness |
| `field-no-string-on-required` | P2 | Maintainability |
| `missing-translation` | P2 | Maintainability |
| `vendored-python-code` | P3 | Maintainability |
| `missing-ondelete` | P1 | Data Integrity |
| `data-noupdate-risk` | P2 | Data Integrity |
| `deprecated-api-usage` | P1 | Upgrade Safety |
| `removed-model-still-referenced` | P1 | Upgrade Safety |
| `asset-bundle-missing` | P2 | Frontend |
| `expensive-nonstored-compute` | P2 | Performance |
| `monetary-missing-currency-field` | P1 | Correctness |
| `missing-multicompany-rule` | P1 | Security |
| `unsafe-template-render` | P1 | Security |
| `access-write-without-group` | P1 | Security |
| `dynamic-method-dispatch` | P1 | Security |
| `rpc-sudo-without-access-check` | P1 | Security |
| `sql-constraints-unsupported` | P1 | Upgrade Safety |
| `removed-orm-api-usage` | P1 | Upgrade Safety |
| `cron-removed-fields` | P1 | Upgrade Safety |
| `view-attrs-states-removed` | P1 | Upgrade Safety |
| `tls-verification-disabled` | P1 | Security |
| `csrf-disabled-authenticated-route` | P1 | Security |
| `manual-cr-commit` | P1 | Data Integrity |
| `deprecated-orm-api-usage` | P2 | Upgrade Safety |
| `http-request-without-timeout` | P2 | Performance |
| `view-tree-removed` | P1 | Upgrade Safety |
| `removed-core-field-usage` | P1 | Upgrade Safety |
| `search-view-group-attributes` | P1 | Upgrade Safety |
| `act-window-target-inline` | P1 | Upgrade Safety |
| `chatter-div-removed` | P1 | Upgrade Safety |
| `kanban-legacy-templates` | P1 | Upgrade Safety |
| `hardcoded-company-or-currency` | P2 | Correctness |

Plus Ruff and Pylint-Odoo findings when those tools are installed. Both run isolated from the scanned repository: Ruff runs with `--isolated` (its default rule set) and ignores the repository's `ruff.toml` / `[tool.ruff]`, so a hostile repo cannot hide findings with it. Pylint's configuration is ignored too, because Pylint runs `init-hook` code from it. Filter their findings with `[ignore] rules` in `odoo-doctor.toml`. In terminal and PR-comment text, a `::` that came from the repo (a file or module name) is shown with an invisible zero-width space between the colons, so copy-pasting such a name keeps that character.

The full, generated reference (55 rules, with before/after examples) is in
[`docs/rules.md`](docs/rules.md); every finding links to its entry. Disable a rule
with `odoo-doctor rules disable <rule-name>`; write your own with the stable
[plugin API](docs/custom-rules.md). What you can rely on across upgrades (CLI flags, exit
codes, JSON keys, rule IDs) is in the [stability contract](docs/stability.md).
Supported Python and Odoo versions and the release support policy are in
[`docs/support.md`](docs/support.md); scan times on Odoo 19 community are in
[`docs/performance.md`](docs/performance.md).

---

## Score explained

Each category score starts at 100 and loses points per **high-confidence**
finding, where each finding deducts `tier_impact × category_weight`
(defaults: Security/Correctness 1.5, Performance/Data Integrity/Upgrade Safety 1.0,
Module Hygiene 0.8, Maintainability/Frontend 0.5; override via `[category_weights]`). The overall score
blends only **in-scope** categories (those with at least one active rule):

```
category_score = max(0, 100 − Σ(tier_impact × category_weight))
overall        = 0.4 × min(in_scope_category_scores)
               + 0.6 × avg(in_scope_category_scores)
```

Tier impacts: P0 = 25, P1 = 10, P2 = 4, P3 = 1.

| Label | Range |
|-------|-------|
| Excellent | 90–100 |
| Good | 75–89 |
| Needs work | 50–74 |
| Critical | 0–49 |

Each finding deducts points by tier: **P0 = −25**, **P1 = −10**, **P2 = −4**, **P3 = −1**.  
Only `high` confidence findings count toward the score.

### Fix first

Every scan ranks the score-eligible findings of each module by **marginal score
gain per effort**, so you (or an agent) fix the right thing first. The terminal
prints the top 5 under *Fix first*; `--json` has the top 10 per module as
`modules.<name>.fix_priorities`:

```json
{"rank": 1, "rule": "duplicate-xml-id", "file_path": "...", "line": 79,
 "impact": 10.0, "effort": 1, "roi": 10.0, "projected_score": 52.1,
 "score_gain": 4.7, "fixable": false}
```

- `effort` is a coarse 1-3 estimate per rule (1 mechanical, 2 local code change,
  3 restructuring); auto-fixable rules count as 1.
- `projected_score` is the module score after fixing this finding *and every one
  ranked before it*; `score_gain` is the change this step made. A category that is
  already at 0 shows `+0.0` until enough of its findings are fixed — the weakest
  category carries the `0.4 × min` term, so it is ranked first.

---

## Configuration

```bash
odoo-doctor init   # creates odoo-doctor.toml
```

```toml
[odoo-doctor]
odoo_version = "17.0"
addons_paths = ["."]
odoo_source_path = "/path/to/odoo/source"
capabilities = ["enterprise", "owl"]
min_score = 75

[adapters]
ruff = true
pylint_odoo = false

[severity]
"search-in-loop" = "warning"

[ignore]
rules = []
files = ["**/migrations/**"]
modules = []

[category_weights]
Security = 1.5

[surfaces.pr_comment]
min_confidence = "all"
categories = []

[surfaces.ci_failure]
min_confidence = "high"
categories = []

[surfaces.terminal]
min_confidence = "medium"   # "low" shows everything
```

---

## CI Integration

### GitHub Actions

The easiest way to integrate Odoo Doctor into GitHub Actions is using our official composite action. See `.github/workflows/odoo-doctor.example.yml` for a full example.

```yaml
- name: Odoo Doctor Scan
  uses: minhhq-a1/odoo-doctor@v0.11.2
  with:
    fail-on: warning
    min-score: 75
    diff-base: main
    pr-comment: true
    paths: "."
```

If you prefer `pip install`, you can run it directly:

```yaml
- name: Odoo Doctor (pip)
  run: |
    pip install odoo-doctor
    odoo-doctor scan . --format github --min-score 75 --fail-on error
```

### GitLab CI

`ci-templates/gitlab-ci.yml` defines an `odoo-doctor` job. Include it (pin a release tag
instead of `main` once you depend on it) and override what you need:

```yaml
include:
  - remote: "https://raw.githubusercontent.com/minhhq-a1/odoo-doctor/main/ci-templates/gitlab-ci.yml"

variables:
  ODOO_DOCTOR_ODOO_VERSION: "17.0"
  ODOO_DOCTOR_FAIL_ON: "warning"
  ODOO_DOCTOR_MIN_SCORE: "75"
```

Merge request pipelines scan only the files the MR changed (against the merge base, so a
target branch that moved on does not leak into the scan); default-branch pipelines scan
everything. The job keeps `odoo-doctor-report.json` as an artifact. Other variables:
`ODOO_DOCTOR_VERSION` (pin a release), `ODOO_DOCTOR_PATHS`, `ODOO_DOCTOR_ADVISORY=true`
(report only, never fail).

### Bitbucket Pipelines

Copy `ci-templates/bitbucket-pipelines.yml` into your `bitbucket-pipelines.yml` (Bitbucket has
no remote include). It runs on every pull request, scans only the changed files and keeps
`odoo-doctor-report.json` as an artifact. The same `ODOO_DOCTOR_*` names as above are read from
repository variables; the file's header lists them and shows how to also scan your main branch.

### SARIF & Baseline Mode

For GitHub Code Scanning and IDE integration:
```bash
odoo-doctor scan . --format sarif > results.sarif
# Then upload via github/codeql-action/upload-sarif
```

To capture current debt and block only new findings in CI:
```bash
odoo-doctor scan . --write-baseline .odoo-doctor-baseline.json
# Commit the baseline, then in CI:
odoo-doctor scan . --baseline .odoo-doctor-baseline.json --fail-on warning
```

New baselines use format v3 and identify findings by paths relative to the scan root,
so the same baseline works in another checkout or CI runner. Use the same scan root
layout when writing and applying it. Existing v1/v2 baselines are still accepted at
their original checkout path; regenerate them with `--write-baseline` for portability.

### CI/PR Surfaces

- **`--show-low`**: The terminal hides low-confidence findings by default (`[surfaces.terminal]`) and prints how many it hid; `--show-low` lists them all. Only the terminal output changes; JSON, SARIF, HTML, `--fail-on` and scores do not.
- **`--format github`**: Emits GitHub Actions annotations inline.
- **`--score-delta <base-ref>`**: Opt-in PR score delta. It does a worktree-isolated second scan and needs git history (`fetch-depth: 0` in Actions).
- **Sticky PR comment**: Posted/updated via `gh` when `--format github` runs in a PR with a valid `GH_TOKEN`. Idempotent via a hidden marker. Only `--format github` posts it; `--no-pr-comment` (or `pr-comment: false` on the Action) turns it off.

### CI failure policy

`--fail-on <severity>` only counts findings admitted by `[surfaces.ci_failure]`,
which defaults to **P0/P1 at high confidence**: style/advisory (P2/P3) and
low-confidence findings are reported but never fail a build. Adjust it:

```toml
[surfaces.ci_failure]
tiers = ["P0", "P1", "P2"]   # [] = every tier
min_confidence = "high"
```

### Score history & badge

Track the score over time and publish a badge without any server (see
[`docs/score-history.md`](docs/score-history.md)):

```bash
odoo-doctor scan . --history .odoo-doctor/history.jsonl --badge badge.svg
odoo-doctor history show .odoo-doctor/history.jsonl --max-drop 3   # exit 2 on regression
odoo-doctor history import history.jsonl old-report.json           # pre-0.4.0 reports
```

### HTML report

Write one self-contained HTML file (no server, no network request) to open in a browser or keep
as a CI artifact (see [`docs/html-report.md`](docs/html-report.md)):

```bash
odoo-doctor scan . --html report.html
```

### pre-commit

```yaml
# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: odoo-doctor
        name: Odoo Doctor
        language: system
        entry: odoo-doctor scan --diff HEAD --fail-on error
        pass_filenames: false
        types: [python]
```

---

## Agent Usage

Odoo Doctor is designed for AI coding agents. Install the SKILL.md files:

```bash
odoo-doctor install   # installs to .odoo-doctor/skills/
```

Then in your agent workflow:

```bash
# After editing Odoo code
odoo-doctor scan . --diff main --json

# Fix P0/P1 findings with confidence: "high"
# Re-scan to verify fixes
odoo-doctor scan . --diff main --json
```

Use `odoo-doctor rules explain <rule-name>` to understand any finding (description, why, fix, examples and a docs link).

Use `odoo-doctor rules stats` to see which rules your team suppresses most (inline `# odoo-doctor: disable`, `[ignore] rules`, `[severity] = "off"`). Rules where most findings are suppressed are flagged as noisy, with a suggestion to lower their severity.

**In your editor (experimental).** `uv tool install 'odoo-doctor[lsp]'` (or `pipx install`; Python 3.10+) adds `odoo-doctor lsp`, a language server that shows the same findings as diagnostics and offers quick fixes (apply the auto-fix, or disable the rule on a line, in a file or in `odoo-doctor.toml`). The VS Code extension is on the [Marketplace](https://marketplace.visualstudio.com/items?itemName=MinhHong.odoo-doctor) (`MinhHong.odoo-doctor`, preview; source in [`editors/vscode`](editors/vscode)); setup for VS Code, Neovim and Helix is in [`docs/lsp.md`](docs/lsp.md).

---

## Generating stubs for your Odoo version

Bundled stubs cover **17.0, 18.0, 19.0** (core models only).  
For full accuracy, generate from source or a live instance:

```bash
# From Odoo source checkout
python -m odoo_doctor.graph.stubs.build_stubs source \
  --odoo-path /path/to/odoo \
  --version 17.0

# From a live Odoo instance (no source needed)
python -m odoo_doctor.graph.stubs.build_stubs rpc \
  --rpc-url http://localhost:8069 \
  --rpc-db mydb \
  --rpc-password admin \
  --version 17.0
```

The generated JSON is written to `src/odoo_doctor/graph/stubs/data/<version>.json`  
(or `--output <path>` for a custom location).

---

## Inline suppression

```python
x = self.env.cr.execute(f"SELECT ...")  # odoo-doctor: disable=raw-sql-string-interpolation
```

```xml
<record id="my_record" model="ir.ui.view">  <!-- odoo-doctor: disable=duplicate-xml-id -->
```

---

## Exit codes

| Code | Meaning |
|------|---------|
| `0` | Clean — no triggered thresholds |
| `1` | Findings at or above `--fail-on` severity |
| `2` | One or more modules score below `--min-score` |
| `3` | Invalid argument, out-of-range `--min-score`, or git/ref failure |

`odoo-doctor history show --max-drop N` also exits `2` when the score regressed.

---

## Development

```bash
git clone https://github.com/minhhq-a1/odoo-doctor
cd odoo-doctor
pip install -e ".[dev]"
pytest                    # the full suite
pytest --cov=odoo_doctor  # with coverage
```
