Tradução automática. Os dados e os números são a fonte da verdade; a redação pode conter erros de tradução e alguns trechos podem continuar em inglês. A versão autoritativa são as páginas em inglês e em ucraniano. avise-nos se algum termo estiver impreciso

Metodologia

How a module is checked

Every module is installed into a clean database of the matching Odoo series, built from the official odoo:<series> image, with no demo data, using -i <module> --stop-after-init. The result is the process exit code and the log.

We install without demo data, on purpose

The exact flag is --without-demo=all, and it is not a detail of our setup: it is what a production database looks like. A module whose post_init_hook or data file looks up a record that its own manifest declares only under demo: installs fine for a developer with demo data loaded, and fails here. That failure is real — it is what the same module would do on a production install — so the verdict stays, and we do not re-run such a module with demo data to turn it green. What we do instead is name the reason in words on the module page, above the raw log, so nobody has to read a traceback to learn that the missing record is a demo record.

This is not an environment problem: that status is reserved for something missing from our image. Here nothing is missing from the image.

Statuses

EstadoSignificado
InstalaInstala
!Instala com avisosInstala com avisos
Bloqueado por uma dependênciaBloqueado por uma dependência
~Problema de ambienteProblema de ambiente
Erro de instalaçãoErro de instalação
Tempo esgotadoTempo esgotado

What we deliberately do NOT count as incompatibility

A failure caused by a missing external Python package or a missing system utility in the image is recorded as an environment problem and is not counted against the module. The same applies when the harness itself fails to expose the module to Odoo. This is deliberate: without that separation the statistics would be untrue, and an untrue index is worth nothing.

One correction made without re-running. On 30 August 2026 we fixed a rule in the classifier that swallowed the package name: Odoo writes «external dependency is not met: Python library not installed: transitions», and the rule captured only the first word after the colon, so the verdict read «Python». On the stable series 119 verdicts were renamed from the stored log, without installing anything again — the log and the run date did not change, only the name of what the log already said. Sixteen of them also moved from «missing Python package» to «missing system utility», which is a different environment problem, not a different verdict about the module. Rows renamed this way are marked in the dataset as not re-run.

What the percentages are measured against

Every published percentage names its own denominator, because a percentage without one can be moved simply by changing what you count. A module counts as runnable only if we can obtain it, it declares itself installable, and the series is one we actually run. Concretely, these are excluded from both the numerator and the denominator:

Coverage of the series as this page was generated, «tested / runnable»: 16.0 — 3.101/3.101 · 17.0 — 1.938/1.938 · 18.0 — 2.956/2.956 · 19.0 — 1.330/1.330. Fully covered: 16.0, 17.0, 18.0, 19.0.

So «96,5% install cleanly» — the share on the home page as this page was generated — is always written out in full: how many of how many tested, and separately how many are runnable out of the total. The figure in this sentence is computed together with the table, not typed next to it.

Why we raise the manifest version for the development line

Odoo refuses to install a module whose version in the manifest does not start with the platform’s own series. That is a string comparison, not a check of the code, and it happens before the platform reads a single line of the module:

def check_version(version, should_raise=True):
    serie = release.major_version          # '19.5' on the development line
    if version.startswith(serie + '.'):
        return True

There is a helper that prefixes the series automatically, but it has a length condition:

def adapt_version(version):
    ...
    if len(version_parts) <= 3 and not version.startswith(serie):
        return f"{serie}.{version}"      # prefix only for 2–3 part versions
    return version

In OCA the canonical version has five parts, so the prefix is never added. Measured on 25 August 2026: of the 1 226 modules on the 19.0 branch, all 1 226 declare a five-part version. Not one of them passes the gate. A run without raising the version would therefore produce 1 226 identical uninstallable lines — the same statement about the platform repeated 1 226 times, and nothing at all about any module.

So we raise one line. Raising the version is the first mechanical step of every port: a maintainer porting a module changes that line first, and always. We reproduce a state every port passes through — we do not invent one that never occurs. What changes and what does not:

Raising the version cures nothing and hides nothing — but without it the platform refuses before it reads the code.

Quantos módulos isto afeta de fato — uma contagem sem executar nada

Na 19.0, os direitos de acesso e as regras de registro são dois modelos separados: ir.model.access (declarado em ir_model.py) e ir.rule (em ir_rule.py). Nas séries seguintes ir_rule.py desaparece e surge ir_access.py com _name = 'ir.access', que substitui os dois. Não é um modelo renomeado: são dois modelos de segurança fundidos em um — e já está em saas-19.4, uma linha JÁ LANÇADA, não apenas na linha de desenvolvimento.

Quantos módulos isto afeta pode ser contado sem executar nada: um nome de arquivo, ou o valor de model=, é um nome de modelo — não há um terceiro estado. Contado sobre o instantâneo do pool de 2026-09-10, 1.321 módulos do branch 19.0:

O que o módulo carregaMódulos
ir.model.access.csv277
só registros model="ir.rule"25
os dois137
nenhum dos dois882
afeta o novo ir.access439 de 1.321 (33,2%)

Este é o único número aqui que não depende de nenhuma das nossas execuções: ele se reproduz em qualquer checkout da OCA pela mesma contagem. Um módulo falha mesmo que nunca mencione esses nomes no seu código Python — por isso é preciso procurar nos dados, não no código.

What came of it: 1.290 modules

The denominator is a dated pool snapshot. 2026-09-10 06:16 UTC: 1.290 runnable modules of the 19.0 branch — the very set the run was made over. The branch meanwhile grows: at 2026-09-16 03:25 UTC it holds 1.330 such modules. 40 modules are not in the snapshot and are therefore not measured in this section — no number above accounts for them. They are absent from the pool directory, so the next pool build picks them up.

442 (34.3%) start on the development line with no change to the code beyond the version string. 833 fail because of changes in the platform itself: the same code and the same tree sha give ok on 19.0, so the difference cannot be charged to the module.

15 we could not attribute to the platform. Our rule demotes a failure to the platform only when the same code and the same tree sha gave ok on 19.0, and not one of these 15 has such a reference run: 13 never got installed on 19.0 at all — a Python package missing from our image; 1 fails there on a binary we do not ship; 1 fails on 19.0 too. So 14 of the 15 are unattributed because of a gap in our test stand, not because of anything the author did, and in 14 of the 15 the file that broke belongs to a dependency, not to the module. The one genuine own-fault case is field_vector (sql_error in its own hooks.py, and the same way on 16.0, 18.0, 19.0). The named list is in devline.csv (attributed_status = fail).

The commonest cause, named and counted. Of 848 that did not pass, model_gone accounts for 521 and access_model for 37: together 558 of 848 (65.8%). Both causes are the same merge of ir.model.access and ir.rule into the new ir.access, seen from two sides: the first catches a reference to the model that is gone, the second an access file with nowhere left to go.

Separately — whose file failed, by the path in the log. The addons pool is flat, so the first path segment after /mnt/pool is a module name, and the owner of the file is readable from the log itself. Of 848 failures, 46 have no culprit file by nature: a dependency missing from the addons path, or a declared Python package that is absent — the failure happens while checking the manifest, before any file is read. “Whose file” does not apply to them, so they are not in the denominator.

That leaves 802 failures that do have a file. Of these, 346 are in a file of the module itself, 394 in a file belonging to another module — meaning the verdict is about a dependency, not about the page you are reading. A further 62 (7.7%) are left with no file named, and the log level is not the reason: the set was run at --log-level=info.

At INFO Odoo does print a line reading odoo.modules.loading: loading <file>, and taking it at face value would be tempting — it would attribute several dozen more failures at a stroke. We do not take it that way: the line names the last file announced, not the one that failed. When the failure happens after the data has been loaded, it points at an entirely unrelated file — a fresh instance of the very error this section guards against, only wearing the look of a measurement. So the line counts only together with a traceback frame proving the failure occurred inside loading a file (load_data, convert_file, convert_csv_import, convert_xml_import). Second safeguard: the line must appear in the log excerpt printed on the module page — an attribution drawn from text the reader cannot see is not one they can check.

Those 62 are almost entirely two causes: access_model — 31 of 37, registry — 29 of 38 (60 of 62 together). Both strike after the data is loaded, so a file name is there in the log while whose file it is does not follow from it. They are not spread across the other two categories — they stand on their own, because we did not measure it.

Per module, with the file path and the raw pre-attribution raw_status/raw_causedevline.csv.

How to check it yourself

Every result on the development line carries the sha256 of the original __manifest__.py, the version string before and after, and the sha256 of the module tree without the manifest — so «everything else is byte for byte the same» is verifiable rather than promised. The gate itself takes one command:

docker run --rm --entrypoint python3 <image> -c "
from odoo.modules.module import adapt_version, check_version
import odoo.release as r
print('release.major_version =', r.major_version)
for v in ('19.0.1.0.0','19.5.1.0.0','1.0'):
    print(repr(v), '-> adapt', adapt_version(v),
          '| check_version', check_version(v, should_raise=False))"

release.major_version = 19.5
'19.0.1.0.0' -> adapt 19.0.1.0.0 | check_version False
'19.5.1.0.0' -> adapt 19.5.1.0.0 | check_version True
'1.0'        -> adapt 19.5.1.0   | check_version True

A module that honestly wrote 19.0.1.0.0 is rejected; one that wrote 1.0 passes. That is the gate, and it is about a string.

What a development-line result does and does not say

It says: the module’s code runs on the development line after a mechanical version raise — or fails, with the error shown. It does not say the module is compatible with Odoo 20.0, and it does not say the module installs on Odoo 20.0: on a stock platform it will not, until its maintainer raises the version.

The thirty-one modules the development line does not cover

The denominator is 1.290, not 1.321. Thirty-one modules on the 19.0 branch declare 'installable': False, and the same thirty-one still declare an 18.0.* version — the two sets coincide exactly. They sit in three repositories (storage 19, queue 6, rest-framework 6); all thirty-one also exist on the 18.0 branch, and not one is byte-identical to its 18.0 copy — the 18.0 line kept moving after the branch was opened.

We do not run them, for the same reason that lets us run the rest. Raising the version is one line. Making these thirty-one install would mean changing a second one — installable — and that line is the maintainer’s own statement that the module is not ready. Overriding it would not reproduce a step of a port; it would contradict the module. Left as they are, all thirty-one return the same uninstallable for a reason already known, so the run would carry no information either way. They are outside every percentage on this site, on the stock series too.

The platform is a moving target, so each result names it: the image tag, the run date and the platform version. The image is built from a dated nightly package (odoo_19.5a1.20260821_all.deb from nightly.odoo.com/master/nightly/deb/), which is what identifies the build — a nightly .deb carries no odoo/odoo commit.

Limits

We publish the fact of a run, with its date and log — not a rating of a vendor.

Onde erramos

Um índice que nunca errou é um índice que ninguém verificou. Cada defeito abaixo esteve em produção, a maioria visível de fora, e cada linha diz o que agora o torna impossível. Os mais recentes primeiro em igual peso; nada aqui é editado depois, apenas acrescentado.

  1. 2026-08-25 Our GoAccess traffic report was served publicly at /stats.html — 784 KB containing 382 unique visitor IP addresses (personal data of third parties under the GDPR), plus a map of our own infrastructure: 404s, paths, user agents, volumes. No noindex. It was public from 21 August 12:08 to 25 August 09:48.

    The report was written straight into the public site root, and nobody was looking at what lived there. It was found by external measurement, not by us.

    O que não podemos provar: Access logs only reach back to 22 August 01:16, so roughly thirteen hours of that window are not covered and we cannot say what happened in them. In the logs we do have there are six requests to the page, all six from the person who was auditing us; no crawler, and it does not appear in site:.

    Agora impossível: The report lives only in a private channel; IPs are anonymised at capture (--anonymize-ip); /stats.html returns 404 by an explicit rule; and check_site_root() fails the build on any file in the site root that the generator did not put there.

  2. 2026-08-26 All 774 published log excerpts opened on a failure of our own image (a .pth crash of typing_extensions at interpreter start) instead of on the failure they were evidence for, because the excerpt window was anchored on the first Traceback in the log. For 389 of them — every run whose verdict was «the model does not exist in this version» — the KeyError that proves that verdict was not in the published excerpt at all. Two numbers, because they measure different things: 774 excerpts were anchored in the wrong place, and 389 verdicts are known to have been published with no proof under them.

    The verdicts themselves were correct. That is what makes it worse than a wrong verdict: what we sell is the evidence, and the evidence under a right answer was somebody else’s.

    O que não podemos provar: In the other 214 runs classified as an XML error, 98 excerpts did contain the root line; for the rest the truncated window does not show it, so we cannot say per-run which were affected.

    Agora impossível: The classifier returns the lines it derived the cause from; the excerpt builder raises if they are not inside the window; and the exporter refuses to publish a verdict whose excerpt lacks them. On 27 August this same invariant caught a second, opposite defect of ours — 50 runs where the window was anchored past the proof — which is what it is for.

  3. 2026-08-24 llms.txt — the file language models read — publicly stated «Tested: 1» while 9 067 module-series pairs had been tested. The front page said «Ported 19.0→master 0.1%» and «master shipped 11 months ago».

    ORDER BY series sorts as text, so master landed after 19.0 and became the «newest» series — and almost every number on the front page is counted from the newest one. master is not an OCA branch at all; it is the key of an image.

    Agora impossível: Series are filtered and ordered numerically, one filter feeding the site, llms.txt and status.json alike, plus an assertion that the newest series is 19.0 while no 20.0 branch exists.

  4. 2026-08-26 The classifier blamed a platform change on «an error in the module’s XML». At least 96 of 214 runs carried the wrong cause — 81 about ir.rule, 15 about ir.model.access.

    The views_xml rule was tried before model_gone, and Odoo wraps the KeyError that names the missing model inside a ParseError — so both lines are always in the log and the wrapper matched first. «At least», because in the remaining 118 the truncated excerpt does not show the root, which is the defect above.

    Agora impossível: Root before wrapper is no longer a convention about rule order but an assertion: if the log contains KeyError: '<a.dotted.model>', the cause cannot be an XML, assets or access error, and the classifier raises rather than publish one.

  5. 2026-08-25 www.allservices.one served the whole site instead of redirecting — including its own robots.txt and sitemap.xml, so every page existed at two addresses at once.

    The redirect block was never written; the host simply fell through to the main one.

    Agora impossível: www answers 301 to the apex, checked by name after every config change.

  6. 2026-08-20 Of the twelve warn verdicts we had that day, eleven were ours, not the modules’. Exactly one survived checking.

    Two independent causes. A batch shares one log, and the classifier ran once for the whole batch — so a warning belonging to one module was written onto seven others from three different repositories. And four more were a CPython DeprecationWarning about a third-party SWIG library the module merely imports, which said nothing about its code but read on the page as a reproach to its author.

    Agora impossível: A batch that produces a warn is split exactly like a batch that fails, and a warning counts only if the same line names code inside Odoo. Since 25 August the whole development-line pass runs one module per batch, so every module has its own log.

  7. 2026-08-30 For 24 hours the site published nothing. Every hourly build failed on a privacy check that had found something real — a third-party maintainer’s address in our own visitor report — and the check could not stop it: the files, that address included, had already been written and served the whole time. The line the build printed said “publication stopped”.

    The build wrote straight into the directory the web server serves, so writing WAS publishing and every check ran after it. That made all of them decorative, not just this one. The address itself came back to us through our own leak: URLs we had published before removing them stayed in crawler databases, the crawlers kept requesting them, and the requests landed in the access log the report is built from. Nobody noticed for a day, because no unit signalled failure anywhere.

    Agora impossível: The build goes to a directory nothing serves, every check runs against it, and only then is the live symlink switched by an atomic rename. A failed check now leaves yesterday’s site up, which is the intended state. Separately: the dead URL form answers 301 instead of 200, raw logs have a named retention limit, a failed unit sends mail, and a second watchdog checks the published page’s freshness over HTTP rather than the unit’s exit code.

  8. 2026-08-27 The development-line set could have been declared complete while nineteen modules had never been run at all.

    «Complete» compared what we had run against the branch, but runs go against a pool built earlier. Modules added to the branch after the pool was built were in neither the queue nor the results, so the counter would have reached zero on its own and published the set as full.

    Agora impossível: Publication additionally requires that the number of runnable modules in the pool equal the number actually run, and both denominators are published side by side in status.json, each with its own date.

  9. 2026-08-26 The development-line pool was not a snapshot: seventeen modules were run against code that had already been replaced under them, mid-pass.

    The pool is symlinks into a git checkout, and the nightly sync does git reset --hard on that checkout. A result attributed to code that was no longer there cannot be proved either way.

    Agora impossível: The tree sha of every module is recorded when the pool is built, the runner compares before each run and refuses on a mismatch, and the sync will not fetch while the queue is not provably empty.

  10. 2026-08-26 The commit recorded next to each result was the one the harvester had seen in the remote repository, not the one that was actually in the container.

    The field said something other than what the page claimed it said — the same class as a verdict with somebody else’s log, only quieter.

    Agora impossível: The sha is read from the checkout that was mounted, at the moment of the run.

  11. 2026-08-21 Cleanup of the one-shot run databases had never once worked: ten orphaned databases, 270 MB. Before the fix it removed exactly one of nine.

    Two independent causes in the same script. The age of a database was measured by the directory’s mtime, which a checkpoint or autovacuum updates, so nothing ever looked old enough. And the DROP loop read the same stdin the database list was being piped through, so it consumed its own input after the first line.

    Agora impossível: Age is read from the database’s own files, and the loop no longer shares stdin. It now clears everything orphaned in one pass.

  12. 2026-08-24 /feed/master.xml survived the cleanup that removed master from the list of series: an empty Atom feed that still announced a series and could still be subscribed to.

    The cleanup walked only index.html files, and the call it used to delete them does nothing at all when pointed at a file rather than a directory — silently, because errors were being ignored.

    Agora impossível: Cleanup covers feeds as well as pages, and it refuses to delete anything at all if more than 2% of pages or more than 100 fall under the deletion.

  13. 2026-09-02 The front page and /blocking/ published two different numbers for the same statement — how many 18.0 modules have no 19.0 version. The front page said 1 834, /blocking/ said 1 821, and both pages carried the same «Data updated» line.

    Two units for one question. The front page counted (repository, module) pairs, so a module OCA moved to a different repository between 18.0 and 19.0 looked unported; /blocking/ counted module names, which is the right unit — the question is whether the module exists on 19.0, not whether it stayed where it was. The difference was exactly the 13 modules that moved, eleven of them the base_tier_validation family extracted from server-ux into its own repository.

    O que não podemos provar: We cannot say how long the two numbers stood apart: neither page records its own history, and both were rebuilt hourly. The pair counting is as old as the front page.

    Agora impossível: Both pages take the figure from one function; a build check fails the publication if the two ever differ again. The correct number is the lower one: 1 821, and the ported share rose from 36.9% to 37.3% accordingly — a correction that makes us look better, which is exactly why it needed a recount rather than a rewrite.

  14. 2026-09-02 Every English module page printed the failure cause in Ukrainian«Помилка SQL при установці» where it should have read «SQL error while installing». Systemic, not one page: it affected every verdict carrying a cause.

    The classifier writes the human-readable cause into the database already rendered in one language, and the page printed the stored string as it was.

    Agora impossível: The page renders the cause per language from the stored one, and a build check fails if a classifier label has no English counterpart — so a new rule cannot quietly put Ukrainian back onto English pages.

  15. 2026-09-02 For five days the static ir.access breakdown on this page was counted over a pool snapshot that no longer existed. The pool was rebuilt on 30 August (1 261 modules); the file the page reads was left from 27 August (1 245). So the one number here that depends on none of our runs stood next to run figures measured on a different snapshot.

    The script that writes that file was called by nobody. It was run by hand once, and its promise — «the same date as the rest of the set» — held only for as long as somebody remembered to repeat it.

    O que não podemos provar: The snapshot date was printed next to the number the whole time, so a reader could see which snapshot it described. We cannot show that anyone did.

    Agora impossível: poolsnap.py rewrites the breakdown in the same run that replaces the snapshot, so the two dates cannot drift apart; a failure there names itself in the log instead of leaving the old file silently in place.

Result looks wrong?

Tell us — we will show the full log or run the module again. This is not a formality: of the twelve warn verdicts we had on 20 August 2026, eleven were ours and not the modules’: exactly one survived being checked. The three fail verdicts of that day were genuine. They were found only because someone opened the log — see the defect log.

The main channel is a GitHub issue: the history is public, and it shows that we answer. Every module page carries a link that pre-fills the module, series, run date and verdict. By mail: hello@allservices.one.

Being told, without being tracked

Two channels, and the difference between them is deliberate. The Atom feed is anonymous: no address, no registration, every event in the index. The alert list is the opposite trade — you give an address and hear only about the series or the modules you named. The feed is not going away, and the 20.0 tracker carries the feed alone.

What the list stores is the address, a random token, what you picked, and the timestamps. Not your IP, not your user agent, not where you came from. Confirmation and unsubscribe links carry the token, never the address: an address in a URL ends up in a web-server log, and that is not a hypothetical — a stranger’s address, put into a URL by us in August, came back through our own log and stopped the site build for twenty-four hours. Both links open a page with a button rather than acting on the click, because mail clients pre-fetch links, and a subscription confirmed by a scanner is not a subscription. Unsubscribing deletes the row; it does not set a flag.

Maintainer, Module Health Index

Dados atualizados em 2026-09-16 20:00 UTC · publicamos o log de instalação com data, não uma avaliação do fornecedor · O CSV e o JSON são abertos

Dúvidas ou um resultado errado: abrir uma issue ou hello@allservices.one

Projeto independente. Sem vínculo com a Odoo S.A. nem com a Odoo Community Association.