Plugins¶
Most of what oa shows is read-only and always there. Anything that needs
credentials, or that only some people want, ships as a plugin instead — so
it is installed rather than switched on, and absent installs cost nothing.
Installing¶
uv tool install "odoo-activity[odooly]" # one
uv tool install "odoo-activity[all]" # every bundled plugin
Installed is active for a plugin that opts into running by default — odooly does, so installing it is enough; a plugin added later stays off until named explicitly. Two flags narrow or extend that for a single run:
Both take a comma list or can be repeated. If the two disagree, disabling
wins. A name matching nothing installed stops oa with an error rather
than running on quietly — a typo should not look like a plugin that has
nothing to say.
odooly¶
The one bundled plugin today. It reads ~/odooly.ini at startup and matches
each database against it, which unlocks the actions that need a login:
- an
ODOOLYtag on every database row — green where an environment reaches it, red where none does - Database ▸ Toolbox — copy the
odoolycommand for this database, restore app icons - Create test job under Jobs, Send test mail under Mail
An action appears only when its plugin is active and has something to
offer for the highlighted row. Without a matching ~/odooly.ini section the
Toolbox says so rather than listing tools that would only fail.
Which Toolbox
Instance mode and database mode both have a Toolbox tab. odooly's rows
are on the database one — highlight the └── dbname row, not the
instance above it.
odooly always runs on your machine, even when you are watching a remote
host: it reaches the instance over the network using your own
~/odooly.ini, which the watched host neither has nor should be asked for.
Your project's own scripts¶
Some scripts only make sense for one customer project. Rather than packaging
one plugin per project, odooly's list is open: launch oa from a project
checkout and the Python files in its scripts/ directory are offered
alongside the packaged ones, for the databases that project's odooly env
matches.
~/code/demeter
├── scripts/
│ └── recompute_zones.py → "recompute zones (project)" in the Toolbox
└── ...
Nothing to install and nothing to register — the scripts live in the
project's own repository. Files starting with _ are skipped, so shared
helpers can sit next to the scripts that use them. They run under the same
interpreter as oa, so they can import the packaged helpers:
It has to be the directory you started oa from, not the instance's
directory on the server: odooly runs locally, so a script on the far end is
not somewhere it can reach.
pos¶
Reports on pos.config: whether each till is closed or has an open session
(and if so, its name and order count), when its latest session opened and
when its latest order was actually rung up — a session can open and close
with nothing sold on it, so the two dates can differ — the settings that
decide whether it can even reach a physical till — the IoT Box on/off
switch and its proxy_ip, cashdrawer/scale/printer-via-proxy — its payment
methods (named explicitly, v14+ only — older versions configure payment
through plain journals, which this doesn't try to name), and whether it's
set to wait for the terminal's own confirmation before moving on — per
payment method on v14+, or one flag for the whole till on the versions
before that, where it lived directly on pos.config.
Unlike odooly, pos is opt-in — most projects don't run Point of Sale, so it doesn't install or activate on its own:
It piggybacks entirely on odooly's own ~/odooly.ini matching (installing
pos pulls in the odooly extra too), so it needs the same section a
database's ODOOLY tag already points at. It also declares odooly as a
requirement (Plugin.requires), so naming just pos still runs odooly
alongside it — --enable-plugins=pos behaves like
--enable-plugins=odooly,pos, keeping the ODOOLY marker/Toolbox visible
too; --disable-plugins=odooly still overrides that if you really only
want the tab. Once enabled it adds one thing: a POS tab in database
mode, after Toolbox — an added tab reads as beyond the built-in set, not one
more of them. Reachable with [/] or a click, no letter shortcut. Without
a matching env it says so instead of showing an empty table, the same way
an unreachable Toolbox does.
A field this instance's version doesn't have (an OCA module not installed,
an older pos.config missing a newer field, pos.payment.method not
existing at all before v14 — v12/v13 configure payment through plain
journal_ids instead) shows as a blank cell rather than failing the whole
tab. Verified back to v12.
Writing one¶
Plugins are found through Python entry points, so a plugin is a package that registers itself. There are two kinds, and they ask different things of the app.
Contributors¶
A contributor adds to an instance the app already found. Every hook is optional — write only the ones you use.
from odoo_activity.plugins import Plugin
class DemeterPlugin(Plugin):
name = "demeter"
requires = ("odooly",) # pulled in automatically, see below
def marker(self, target): # a tag on the database row
def tools(self, mode, target): # Toolbox rows
def actions(self, tab, target): # buttons under a database tab
def hint(self, mode, target): # why the Toolbox is empty, if it is
def db_tab(self): # name of an extra database-mode tab, if any
def fetch_tab(self, tab, target): # that tab's rows
requires names other plugins this one needs to work fully — --enable-plugins=demeter
then runs odooly too, the way naming a package pulls in its own
dependencies. It's still overridable: --disable-plugins wins even over a
requirement pulled in this way. A name that isn't installed is just ignored,
not fatal.
target is the highlighted (instance, database) pair. A handler receives
the app — so it can confirm, prompt or notify — and returns the text to show
in the pane body, or None if it has already said its piece.
Managers¶
A manager decides what an instance is and how to reach it: how to find them, how to run a command against one, where its log comes from, how to read its config, how to connect to its databases, what start and stop mean.
from odoo_activity.managers.base import Manager
class PodmanManager(Manager):
name = "podman"
order = 45 # where its instances sit in the list
def instances(self, host): ...
def host_for(self, inst, host): ...
host_for is the one that matters. Return a Host that already routes to
where the instance lives — the docker manager returns one that prefixes
docker exec and reports is_local = False — and every shared probe works
against it unchanged. Override the rest only where that is not enough.
Entry points come from installed metadata
Not from the source tree. After adding or changing an entry point,
reinstall (uv sync, or uv tool install --force --editable .) —
otherwise the new code runs while discovering nothing at all.
A plugin that fails to import is skipped with a message rather than taking
the TUI down. That is load-bearing, not merely defensive: the odooly plugin
imports odooly, so oa installed without that extra simply has no odooly
plugin.