From c2124392e80f1e86990ab57e024b6dcffb11e1a8 Mon Sep 17 00:00:00 2001
From: Don Kendall
Date: Tue, 19 May 2026 20:42:05 -0400
Subject: [PATCH 1/4] feat(farm_onboarding): systray bell + post_init hook
autostart
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
First UX-refinement slice. Surfaces the onboarding wizard the
moment a farm user signs in instead of waiting for them to find
the menu item.
What's wired:
- `farm_pack` gains a `post_init_hook` that seeds one
`farm.onboarding.session` per (internal-farm-user, company)
on install. Existing installs upgrade and immediately surface
the bell; new users created later pick up a session lazily via
`get_or_create_for_current_user`.
- `farm_onboarding` ships its first OWL component:
`FarmOnboardingSystray`. A leaf icon + red badge dot appears in
the navbar when the current user has a pending session; clicking
it opens the wizard via the existing `action_open_for_current_user`
server action. Hidden entirely when no session is pending — non-
farm users see nothing.
- `farm.onboarding.session.count_pending_for_current_user` is a
new read-only RPC endpoint the bell calls on mount. Kept separate
from `get_or_create_for_current_user` so the systray mount path
cannot accidentally create session rows on every page load.
Step 1 of the UX-refinement plan. Step 2 (the design pass that
gives this bell + the wizard their distinctive look) builds on the
assets pipeline this PR sets up.
Versions bumped: farm_pack 19.0.1.0.0 → 19.0.1.1.0,
farm_onboarding 19.0.1.0.0 → 19.0.1.1.0.
---
farm_onboarding/__manifest__.py | 8 ++-
.../models/farm_onboarding_session.py | 19 ++++++
.../static/src/js/onboarding_systray.esm.js | 56 +++++++++++++++++
.../static/src/xml/onboarding_systray.xml | 28 +++++++++
.../tests/test_farm_onboarding_session.py | 17 ++++++
farm_pack/__init__.py | 1 +
farm_pack/__manifest__.py | 5 +-
farm_pack/hooks.py | 47 ++++++++++++++
farm_pack/tests/__init__.py | 1 +
farm_pack/tests/test_post_init_hook.py | 61 +++++++++++++++++++
10 files changed, 241 insertions(+), 2 deletions(-)
create mode 100644 farm_onboarding/static/src/js/onboarding_systray.esm.js
create mode 100644 farm_onboarding/static/src/xml/onboarding_systray.xml
create mode 100644 farm_pack/hooks.py
create mode 100644 farm_pack/tests/__init__.py
create mode 100644 farm_pack/tests/test_post_init_hook.py
diff --git a/farm_onboarding/__manifest__.py b/farm_onboarding/__manifest__.py
index eeb5990..a3884d4 100644
--- a/farm_onboarding/__manifest__.py
+++ b/farm_onboarding/__manifest__.py
@@ -1,7 +1,7 @@
{
"name": "Farm Onboarding",
"summary": "60-second first-run wizard for new farm-pack installs",
- "version": "19.0.1.0.0",
+ "version": "19.0.1.1.0",
"license": "AGPL-3",
"author": "Ledo Enterprises, Odoo Community Association (OCA)",
"website": "https://github.com/ledoent/farm-pack",
@@ -16,6 +16,12 @@
"views/farm_enterprise_type_views.xml",
"views/farm_onboarding_menu.xml",
],
+ "assets": {
+ "web.assets_backend": [
+ "farm_onboarding/static/src/js/onboarding_systray.esm.js",
+ "farm_onboarding/static/src/xml/onboarding_systray.xml",
+ ],
+ },
"demo": [],
"installable": True,
"application": False,
diff --git a/farm_onboarding/models/farm_onboarding_session.py b/farm_onboarding/models/farm_onboarding_session.py
index 49100fa..f58b431 100644
--- a/farm_onboarding/models/farm_onboarding_session.py
+++ b/farm_onboarding/models/farm_onboarding_session.py
@@ -131,6 +131,25 @@ def get_or_create_for_current_user(self):
)
return session
+ @api.model
+ def count_pending_for_current_user(self):
+ """Lightweight RPC endpoint for the systray bell.
+
+ Returns the number of unfinished onboarding sessions for the
+ current (user, company). The bell uses this to decide whether
+ to render the pulsing dot. Kept separate from
+ `get_or_create_for_current_user` so the systray's mount path
+ is read-only — we do NOT want every page load creating session
+ rows.
+ """
+ return self.search_count(
+ [
+ ("company_id", "=", self.env.company.id),
+ ("user_id", "=", self.env.user.id),
+ ("state", "!=", "done"),
+ ]
+ )
+
def action_open_for_current_user(self):
session = self.get_or_create_for_current_user()
return {
diff --git a/farm_onboarding/static/src/js/onboarding_systray.esm.js b/farm_onboarding/static/src/js/onboarding_systray.esm.js
new file mode 100644
index 0000000..d04314f
--- /dev/null
+++ b/farm_onboarding/static/src/js/onboarding_systray.esm.js
@@ -0,0 +1,56 @@
+/** @odoo-module **/
+
+import {Component, onWillStart, useState} from "@odoo/owl";
+import {registry} from "@web/core/registry";
+import {useService} from "@web/core/utils/hooks";
+
+/**
+ * Systray bell that surfaces unfinished farm-onboarding sessions.
+ *
+ * Hidden when the current user has no pending session (count == 0),
+ * so non-farm users see nothing. When pending, a pulsing dot draws
+ * the eye; click opens the wizard via the existing
+ * `action_open_for_current_user` server action.
+ *
+ * Deliberately lightweight — one `search_count` RPC on mount, no
+ * polling. The bell hides immediately on click (optimistic); if the
+ * user backs out of the wizard without finishing, the next page
+ * reload will surface it again.
+ */
+export class FarmOnboardingSystray extends Component {
+ static template = "farm_onboarding.SystrayBell";
+ static props = {};
+
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+ this.state = useState({pending: 0, loaded: false});
+
+ onWillStart(async () => {
+ this.state.pending = await this.orm.call(
+ "farm.onboarding.session",
+ "count_pending_for_current_user",
+ []
+ );
+ this.state.loaded = true;
+ });
+ }
+
+ async openWizard() {
+ const action = await this.orm.call(
+ "farm.onboarding.session",
+ "action_open_for_current_user",
+ []
+ );
+ await this.action.doAction(action);
+ this.state.pending = 0;
+ }
+}
+
+registry
+ .category("systray")
+ .add(
+ "farm_onboarding.SystrayBell",
+ {Component: FarmOnboardingSystray},
+ {sequence: 100}
+ );
diff --git a/farm_onboarding/static/src/xml/onboarding_systray.xml b/farm_onboarding/static/src/xml/onboarding_systray.xml
new file mode 100644
index 0000000..a8685d7
--- /dev/null
+++ b/farm_onboarding/static/src/xml/onboarding_systray.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/farm_onboarding/tests/test_farm_onboarding_session.py b/farm_onboarding/tests/test_farm_onboarding_session.py
index 863a2b9..9328980 100644
--- a/farm_onboarding/tests/test_farm_onboarding_session.py
+++ b/farm_onboarding/tests/test_farm_onboarding_session.py
@@ -84,3 +84,20 @@ def test_enterprise_selection_persists(self):
veggies = self.env.ref("farm_onboarding.enterprise_vegetables")
sess = self.Session.create({"enterprise_ids": [(6, 0, [eggs.id, veggies.id])]})
self.assertEqual(len(sess.enterprise_ids), 2)
+
+ def test_count_pending_for_current_user(self):
+ # Drives the systray bell — must return 0 when nothing pending,
+ # > 0 when an active session exists. Don't count `done` sessions.
+ # Start from a clean slate: archive any sessions that exist for
+ # the test user from prior tests in this case.
+ self.Session.search([("user_id", "=", self.env.user.id)]).write(
+ {"state": "done", "finished_at": "2026-01-01"}
+ )
+ self.assertEqual(self.Session.count_pending_for_current_user(), 0)
+
+ self.Session.get_or_create_for_current_user()
+ self.assertEqual(self.Session.count_pending_for_current_user(), 1)
+
+ # Skipping completes the session — bell should go away.
+ self.Session.get_or_create_for_current_user().action_skip()
+ self.assertEqual(self.Session.count_pending_for_current_user(), 0)
diff --git a/farm_pack/__init__.py b/farm_pack/__init__.py
index e69de29..88be383 100644
--- a/farm_pack/__init__.py
+++ b/farm_pack/__init__.py
@@ -0,0 +1 @@
+from . import hooks
diff --git a/farm_pack/__manifest__.py b/farm_pack/__manifest__.py
index 96787e5..3b56894 100644
--- a/farm_pack/__manifest__.py
+++ b/farm_pack/__manifest__.py
@@ -1,7 +1,7 @@
{
"name": "Farm Pack",
"summary": "Umbrella: website + ordering + delivery + accounting for small farms",
- "version": "19.0.1.0.0",
+ "version": "19.0.1.1.0",
"license": "AGPL-3",
"author": "Ledo Enterprises, Odoo Community Association (OCA)",
"website": "https://github.com/ledoent/farm-pack",
@@ -38,4 +38,7 @@
"application": True,
"development_status": "Alpha",
"maintainers": ["dkendall"],
+ # Seeds an onboarding session per (user, company) for existing installs
+ # so the systray bell + first-run wizard show up immediately.
+ "post_init_hook": "post_init_hook",
}
diff --git a/farm_pack/hooks.py b/farm_pack/hooks.py
new file mode 100644
index 0000000..aadf4ee
--- /dev/null
+++ b/farm_pack/hooks.py
@@ -0,0 +1,47 @@
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+def post_init_hook(env):
+ """Seed onboarding sessions for existing farm users.
+
+ Without this, an install against a live db leaves every existing
+ user with a hidden menu item and no nudge — the systray bell only
+ pulses when there's an active session. New users created after
+ install pick up a session lazily via
+ `farm.onboarding.session.get_or_create_for_current_user`.
+
+ Scope: internal users (share=False) who belong to the farm-user
+ group. Portal users, the public user, and SaaS-tenant tooling
+ accounts are intentionally excluded.
+ """
+ Session = env["farm.onboarding.session"]
+ farm_user_group = env.ref("farm_base.group_farm_user")
+ farm_users = env["res.users"].search(
+ [
+ ("share", "=", False),
+ ("active", "=", True),
+ ("groups_id", "in", farm_user_group.id),
+ ]
+ )
+ seeded = 0
+ for user in farm_users:
+ for company in user.company_ids:
+ existing = Session.search_count(
+ [
+ ("user_id", "=", user.id),
+ ("company_id", "=", company.id),
+ ]
+ )
+ if existing:
+ continue
+ Session.create(
+ {
+ "user_id": user.id,
+ "company_id": company.id,
+ "farm_name": company.name,
+ }
+ )
+ seeded += 1
+ _logger.info("farm_pack post_init_hook seeded %d onboarding session(s)", seeded)
diff --git a/farm_pack/tests/__init__.py b/farm_pack/tests/__init__.py
new file mode 100644
index 0000000..3b90118
--- /dev/null
+++ b/farm_pack/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_post_init_hook
diff --git a/farm_pack/tests/test_post_init_hook.py b/farm_pack/tests/test_post_init_hook.py
new file mode 100644
index 0000000..1e0390a
--- /dev/null
+++ b/farm_pack/tests/test_post_init_hook.py
@@ -0,0 +1,61 @@
+from odoo.tests.common import TransactionCase
+
+from odoo.addons.farm_pack.hooks import post_init_hook
+
+
+class TestPostInitHook(TransactionCase):
+ """The hook only fires on the actual install transaction; here we
+ re-invoke it inside a TransactionCase against the already-installed
+ db and assert idempotency + the right set of users got a session.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.Session = cls.env["farm.onboarding.session"]
+ cls.farm_user_group = cls.env.ref("farm_base.group_farm_user")
+
+ def test_hook_is_idempotent(self):
+ # Re-running the install hook on a db that already went through
+ # install must not duplicate sessions for existing users.
+ before = self.Session.search_count([])
+ post_init_hook(self.env)
+ after = self.Session.search_count([])
+ self.assertEqual(before, after)
+
+ def test_hook_creates_session_for_new_farm_user(self):
+ # Add a brand-new farm user that has no session yet, then re-run.
+ new_user = self.env["res.users"].create(
+ {
+ "name": "Hook Test User",
+ "login": "hook-test-user@example.com",
+ "groups_id": [(4, self.farm_user_group.id)],
+ }
+ )
+ self.assertEqual(
+ self.Session.search_count([("user_id", "=", new_user.id)]),
+ 0,
+ "fresh user must start with no sessions",
+ )
+ post_init_hook(self.env)
+ self.assertGreater(
+ self.Session.search_count([("user_id", "=", new_user.id)]),
+ 0,
+ "hook must seed a session for a new farm user",
+ )
+
+ def test_hook_skips_portal_users(self):
+ portal_group = self.env.ref("base.group_portal")
+ portal_user = self.env["res.users"].create(
+ {
+ "name": "Portal Test User",
+ "login": "portal-test-user@example.com",
+ "groups_id": [(6, 0, [portal_group.id])],
+ }
+ )
+ post_init_hook(self.env)
+ self.assertEqual(
+ self.Session.search_count([("user_id", "=", portal_user.id)]),
+ 0,
+ "portal users (share=True) must not get an onboarding session",
+ )
From 770073e4767b779eeee23f86cff1ddeb674c678e Mon Sep 17 00:00:00 2001
From: Don Kendall
Date: Tue, 19 May 2026 22:15:12 -0400
Subject: [PATCH 2/4] fix(farm_onboarding): handle systray RPC failure
gracefully
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The systray component is registered globally, so it mounts for every
authenticated user — including users without farm_base.group_farm_user.
For those users, count_pending_for_current_user raises AccessError,
which surfaced as an unhandled promise rejection in the browser
console on every page load.
Wrap the RPC in try/catch with a state.pending=0 fallback. The bell
stays hidden (the `t-if` already handles count=0), and the console
stays clean.
---
.../static/src/js/onboarding_systray.esm.js | 21 +++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/farm_onboarding/static/src/js/onboarding_systray.esm.js b/farm_onboarding/static/src/js/onboarding_systray.esm.js
index d04314f..7c4a99c 100644
--- a/farm_onboarding/static/src/js/onboarding_systray.esm.js
+++ b/farm_onboarding/static/src/js/onboarding_systray.esm.js
@@ -27,12 +27,21 @@ export class FarmOnboardingSystray extends Component {
this.state = useState({pending: 0, loaded: false});
onWillStart(async () => {
- this.state.pending = await this.orm.call(
- "farm.onboarding.session",
- "count_pending_for_current_user",
- []
- );
- this.state.loaded = true;
+ // The systray is registered globally; users without
+ // farm_base.group_farm_user will hit AccessError on the
+ // search_count. Swallow it so we don't spam the console —
+ // a user who can't access the wizard shouldn't see the bell.
+ try {
+ this.state.pending = await this.orm.call(
+ "farm.onboarding.session",
+ "count_pending_for_current_user",
+ []
+ );
+ } catch {
+ this.state.pending = 0;
+ } finally {
+ this.state.loaded = true;
+ }
});
}
From fe2ada2ae08acb0a5fcef3c6c7b442b3f1d7fc83 Mon Sep 17 00:00:00 2001
From: Don Kendall
Date: Tue, 19 May 2026 22:16:38 -0400
Subject: [PATCH 3/4] test(farm_onboarding,farm_pack): cover multi-company +
portal-overlap edges
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three test additions/cleanups from the pre-merge review:
- `test_hook_skips_user_holding_both_portal_and_farm_groups` — a user
in both `base.group_portal` and `farm_base.group_farm_user` has
`share=True` (set by the portal-group implication). The hook's
`share=False` filter must exclude them; pin that behavior so a
future refactor doesn't silently surface the wizard to portal
accounts that can't actually use it.
- `test_hook_seeds_one_session_per_company_for_multi_company_user` —
the hook iterates `user.company_ids` and creates one session per
company. Pin the multi-company shape so a future switch to
`env.company` would fail loudly.
- Replace `finished_at="2026-01-01"` with `fields.Datetime.now()` in
the existing `test_count_pending_for_current_user`. The string
coerces fine through Odoo's ORM but reads as bug-bait.
---
.../tests/test_farm_onboarding_session.py | 3 +-
farm_pack/tests/test_post_init_hook.py | 53 +++++++++++++++++++
2 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/farm_onboarding/tests/test_farm_onboarding_session.py b/farm_onboarding/tests/test_farm_onboarding_session.py
index 9328980..24e7516 100644
--- a/farm_onboarding/tests/test_farm_onboarding_session.py
+++ b/farm_onboarding/tests/test_farm_onboarding_session.py
@@ -1,3 +1,4 @@
+from odoo import fields
from odoo.tests.common import TransactionCase
@@ -91,7 +92,7 @@ def test_count_pending_for_current_user(self):
# Start from a clean slate: archive any sessions that exist for
# the test user from prior tests in this case.
self.Session.search([("user_id", "=", self.env.user.id)]).write(
- {"state": "done", "finished_at": "2026-01-01"}
+ {"state": "done", "finished_at": fields.Datetime.now()}
)
self.assertEqual(self.Session.count_pending_for_current_user(), 0)
diff --git a/farm_pack/tests/test_post_init_hook.py b/farm_pack/tests/test_post_init_hook.py
index 1e0390a..38a5873 100644
--- a/farm_pack/tests/test_post_init_hook.py
+++ b/farm_pack/tests/test_post_init_hook.py
@@ -59,3 +59,56 @@ def test_hook_skips_portal_users(self):
0,
"portal users (share=True) must not get an onboarding session",
)
+
+ def test_hook_skips_user_holding_both_portal_and_farm_groups(self):
+ # Defensive: the share=False filter is what excludes portal users.
+ # A user with both base.group_portal AND farm_base.group_farm_user
+ # has share=True (set by the portal-group implication), so they
+ # must still be excluded — otherwise we'd surface the wizard to
+ # an account that can't actually use it.
+ portal_group = self.env.ref("base.group_portal")
+ hybrid = self.env["res.users"].create(
+ {
+ "name": "Hybrid Portal+Farm User",
+ "login": "hybrid-user@example.com",
+ "groups_id": [(6, 0, [portal_group.id, self.farm_user_group.id])],
+ }
+ )
+ self.assertTrue(hybrid.share, "portal-implied share flag must be set")
+ post_init_hook(self.env)
+ self.assertEqual(
+ self.Session.search_count([("user_id", "=", hybrid.id)]),
+ 0,
+ "share=True users must be excluded regardless of farm-group membership",
+ )
+
+ def test_hook_seeds_one_session_per_company_for_multi_company_user(self):
+ # The hook iterates user.company_ids — a user with access to two
+ # companies should get two sessions (each company is its own
+ # onboarding scope). Future refactors that switch to env.company
+ # would silently regress this; the test pins the behavior.
+ second_company = self.env["res.company"].create({"name": "Multi-Co Test Farm"})
+ multi_co_user = self.env["res.users"].create(
+ {
+ "name": "Multi-Co Farm User",
+ "login": "multi-co-user@example.com",
+ "groups_id": [(4, self.farm_user_group.id)],
+ "company_ids": [
+ (4, self.env.company.id),
+ (4, second_company.id),
+ ],
+ "company_id": self.env.company.id,
+ }
+ )
+ post_init_hook(self.env)
+ sessions = self.Session.search([("user_id", "=", multi_co_user.id)])
+ self.assertEqual(
+ len(sessions),
+ 2,
+ "multi-company farm user must get one session per company",
+ )
+ self.assertEqual(
+ sorted(sessions.mapped("company_id").ids),
+ sorted([self.env.company.id, second_company.id]),
+ "the two sessions must target the two companies in company_ids",
+ )
From a932308ed90c5c0d3bd36474410674e479f3e055 Mon Sep 17 00:00:00 2001
From: Don Kendall
Date: Tue, 19 May 2026 22:19:11 -0400
Subject: [PATCH 4/4] docs(farm_onboarding,farm_pack): describe systray bell +
post_init_hook
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Captures the review-pass docs gaps:
- `farm_onboarding/readme/DESCRIPTION.md` — adds a "the wizard
surfaces itself two ways" section covering the navbar bell + the
menu item, plus a paragraph on the per-user-per-company session
shape and the install-time seeding done by `farm_pack`.
- `farm_onboarding/readme/USAGE.md` — leads with the navbar bell
instead of the menu item, mirrors the wizard's actual discovery
path.
- `farm_pack/readme/DESCRIPTION.md` — documents the new
`post_init_hook` (what it seeds, who's excluded, idempotency).
- `farm_onboarding/readme/newsfragments/+systray_bell.feature.md`
and `farm_pack/readme/newsfragments/+post_init_seed.feature.md`
— first newsfragments in the pack, mirroring OCA changelog
convention.
Plus two small clean-ups flagged in the review:
- Tighten the docstring on `count_pending_for_current_user`: keep
the "why separate from get_or_create" rationale, drop the rest.
- Split the `post_init_hook` log line: INFO when something seeded,
DEBUG on the idempotent quiet path so ops doesn't see
"seeded 0 onboarding session(s)" on every re-run.
README.rst + static/description/index.html regenerated by
oca-gen-addon-readme to reflect the fragment edits.
---
farm_onboarding/README.rst | 36 +++++++++++++------
.../models/farm_onboarding_session.py | 13 +++----
farm_onboarding/readme/DESCRIPTION.md | 26 ++++++++++----
farm_onboarding/readme/USAGE.md | 11 +++---
.../newsfragments/+systray_bell.feature.md | 10 ++++++
farm_onboarding/static/description/index.html | 36 +++++++++++++------
farm_pack/README.rst | 6 ++++
farm_pack/hooks.py | 6 +++-
farm_pack/readme/DESCRIPTION.md | 6 ++++
.../newsfragments/+post_init_seed.feature.md | 9 +++++
farm_pack/static/description/index.html | 5 +++
11 files changed, 122 insertions(+), 42 deletions(-)
create mode 100644 farm_onboarding/readme/newsfragments/+systray_bell.feature.md
create mode 100644 farm_pack/readme/newsfragments/+post_init_seed.feature.md
diff --git a/farm_onboarding/README.rst b/farm_onboarding/README.rst
index 4adfd3f..0080a1e 100644
--- a/farm_onboarding/README.rst
+++ b/farm_onboarding/README.rst
@@ -32,19 +32,31 @@ enterprise types), how you keep books today (QBO / QBD / Excel / paper /
nothing), done — with a state machine, progress bar, save-as-you-go, and
back/next/skip/restart actions.
-This MVP uses a regular form view with state-conditional groups so
-sessions persist across browser closes. The original plan called for an
-OWL client action with full-screen modal; that polish is deferred to v1
-in favor of shipping the data model and flow first.
+The wizard surfaces itself two ways:
+
+- **Navbar bell.** A leaf icon with a red badge dot appears in the
+ systray when the current farm user has an unfinished session. Click it
+ to jump straight into the wizard. The bell is hidden for users outside
+ ``farm_base.group_farm_user`` and disappears once the session is
+ marked done.
+- **Farm → Setup Wizard menu item.** Same destination, accessible at any
+ time for a re-run.
+
+The form itself is a regular Odoo form view with state-conditional
+groups so sessions persist across browser closes. A richer custom-
+chrome design pass lives in a follow-on PR (Step 2 of the UX refinement
+plan).
Adds 12 pre-loaded ``farm.enterprise.type`` records (eggs, vegetables,
fruit, herbs, orchard, cattle, dairy, poultry, hogs, sheep/goats, value-
added, workshops) with emoji icons — these drive product catalog and
report pre-configuration in downstream modules.
-The "Setup Wizard" menu item routes each user to their own open session
-(creates one if none exists), so multiple farm-users on the same company
-each get an independent walkthrough.
+Each user gets their own session per company, so multiple farm-users on
+the same company each get an independent walkthrough. The umbrella
+``farm_pack`` module's ``post_init_hook`` seeds a session for every
+existing internal farm user on install; new users created later pick one
+up lazily on first wizard open.
.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
@@ -59,14 +71,16 @@ each get an independent walkthrough.
Usage
=====
-1. **Farm → Setup Wizard** — opens (or resumes) your active session.
+1. **Look for the leaf icon + red dot in the navbar.** That's the
+ onboarding bell — click it to jump into the wizard. (Or use **Farm →
+ Setup Wizard** for the same destination.)
2. Click **Next** to step through: Welcome → Your Farm → What You Grow →
Books Today → All Set.
3. Each screen saves on Next/Back so closing the browser doesn't lose
progress. Skip jumps straight to Done.
-4. Once done, the Farm menu's other items (Eggs, CSA, Markets, Delivery,
- QuickBooks) are where the actual work happens — the wizard pre-tells
- the pack what to surface for your operation.
+4. Once done, the bell disappears and the Farm menu's other items (Eggs,
+ CSA, Markets, Delivery, QuickBooks) are where the actual work happens
+ — the wizard pre-tells the pack what to surface for your operation.
Bug Tracker
===========
diff --git a/farm_onboarding/models/farm_onboarding_session.py b/farm_onboarding/models/farm_onboarding_session.py
index f58b431..0874b88 100644
--- a/farm_onboarding/models/farm_onboarding_session.py
+++ b/farm_onboarding/models/farm_onboarding_session.py
@@ -133,14 +133,11 @@ def get_or_create_for_current_user(self):
@api.model
def count_pending_for_current_user(self):
- """Lightweight RPC endpoint for the systray bell.
-
- Returns the number of unfinished onboarding sessions for the
- current (user, company). The bell uses this to decide whether
- to render the pulsing dot. Kept separate from
- `get_or_create_for_current_user` so the systray's mount path
- is read-only — we do NOT want every page load creating session
- rows.
+ """Pending-session count for the systray bell.
+
+ Kept separate from `get_or_create_for_current_user` so the
+ bell's mount path is read-only — the systray must not create
+ session rows on every page load.
"""
return self.search_count(
[
diff --git a/farm_onboarding/readme/DESCRIPTION.md b/farm_onboarding/readme/DESCRIPTION.md
index 0b63335..3792c1b 100644
--- a/farm_onboarding/readme/DESCRIPTION.md
+++ b/farm_onboarding/readme/DESCRIPTION.md
@@ -4,16 +4,28 @@ enterprise types), how you keep books today (QBO / QBD / Excel / paper /
nothing), done — with a state machine, progress bar, save-as-you-go, and
back/next/skip/restart actions.
-This MVP uses a regular form view with state-conditional groups so
-sessions persist across browser closes. The original plan called for an
-OWL client action with full-screen modal; that polish is deferred to
-v1 in favor of shipping the data model and flow first.
+The wizard surfaces itself two ways:
+
+- **Navbar bell.** A leaf icon with a red badge dot appears in the
+ systray when the current farm user has an unfinished session. Click
+ it to jump straight into the wizard. The bell is hidden for users
+ outside `farm_base.group_farm_user` and disappears once the session
+ is marked done.
+- **Farm → Setup Wizard menu item.** Same destination, accessible at
+ any time for a re-run.
+
+The form itself is a regular Odoo form view with state-conditional
+groups so sessions persist across browser closes. A richer custom-
+chrome design pass lives in a follow-on PR (Step 2 of the UX
+refinement plan).
Adds 12 pre-loaded `farm.enterprise.type` records (eggs, vegetables,
fruit, herbs, orchard, cattle, dairy, poultry, hogs, sheep/goats, value-
added, workshops) with emoji icons — these drive product catalog and
report pre-configuration in downstream modules.
-The "Setup Wizard" menu item routes each user to their own open session
-(creates one if none exists), so multiple farm-users on the same company
-each get an independent walkthrough.
+Each user gets their own session per company, so multiple farm-users on
+the same company each get an independent walkthrough. The umbrella
+`farm_pack` module's `post_init_hook` seeds a session for every existing
+internal farm user on install; new users created later pick one up
+lazily on first wizard open.
diff --git a/farm_onboarding/readme/USAGE.md b/farm_onboarding/readme/USAGE.md
index 26c04da..e97f303 100644
--- a/farm_onboarding/readme/USAGE.md
+++ b/farm_onboarding/readme/USAGE.md
@@ -1,8 +1,11 @@
-1. **Farm → Setup Wizard** — opens (or resumes) your active session.
+1. **Look for the leaf icon + red dot in the navbar.** That's the
+ onboarding bell — click it to jump into the wizard. (Or use
+ **Farm → Setup Wizard** for the same destination.)
2. Click **Next** to step through: Welcome → Your Farm → What You Grow
→ Books Today → All Set.
3. Each screen saves on Next/Back so closing the browser doesn't lose
progress. Skip jumps straight to Done.
-4. Once done, the Farm menu's other items (Eggs, CSA, Markets, Delivery,
- QuickBooks) are where the actual work happens — the wizard pre-tells
- the pack what to surface for your operation.
+4. Once done, the bell disappears and the Farm menu's other items
+ (Eggs, CSA, Markets, Delivery, QuickBooks) are where the actual work
+ happens — the wizard pre-tells the pack what to surface for your
+ operation.
diff --git a/farm_onboarding/readme/newsfragments/+systray_bell.feature.md b/farm_onboarding/readme/newsfragments/+systray_bell.feature.md
new file mode 100644
index 0000000..3301c3c
--- /dev/null
+++ b/farm_onboarding/readme/newsfragments/+systray_bell.feature.md
@@ -0,0 +1,10 @@
+Adds a navbar systray bell (leaf icon + red badge dot) that surfaces
+the setup wizard whenever the current farm user has an unfinished
+onboarding session. The bell is hidden for users outside the
+`farm_base.group_farm_user` group and disappears once the session
+is marked done.
+
+New `farm.onboarding.session.count_pending_for_current_user` RPC
+endpoint drives the bell's visibility — read-only, scoped to the
+current `(user, company)`, kept separate from
+`get_or_create_for_current_user` so page loads cannot create rows.
diff --git a/farm_onboarding/static/description/index.html b/farm_onboarding/static/description/index.html
index d0dfde5..94cee15 100644
--- a/farm_onboarding/static/description/index.html
+++ b/farm_onboarding/static/description/index.html
@@ -380,17 +380,29 @@
Farm Onboarding
enterprise types), how you keep books today (QBO / QBD / Excel / paper /
nothing), done — with a state machine, progress bar, save-as-you-go, and
back/next/skip/restart actions.
-
This MVP uses a regular form view with state-conditional groups so
-sessions persist across browser closes. The original plan called for an
-OWL client action with full-screen modal; that polish is deferred to v1
-in favor of shipping the data model and flow first.
+
The wizard surfaces itself two ways:
+
+
Navbar bell. A leaf icon with a red badge dot appears in the
+systray when the current farm user has an unfinished session. Click it
+to jump straight into the wizard. The bell is hidden for users outside
+farm_base.group_farm_user and disappears once the session is
+marked done.
+
Farm → Setup Wizard menu item. Same destination, accessible at any
+time for a re-run.
+
+
The form itself is a regular Odoo form view with state-conditional
+groups so sessions persist across browser closes. A richer custom-
+chrome design pass lives in a follow-on PR (Step 2 of the UX refinement
+plan).
Adds 12 pre-loaded farm.enterprise.type records (eggs, vegetables,
fruit, herbs, orchard, cattle, dairy, poultry, hogs, sheep/goats, value-
added, workshops) with emoji icons — these drive product catalog and
report pre-configuration in downstream modules.
-
The “Setup Wizard” menu item routes each user to their own open session
-(creates one if none exists), so multiple farm-users on the same company
-each get an independent walkthrough.
+
Each user gets their own session per company, so multiple farm-users on
+the same company each get an independent walkthrough. The umbrella
+farm_pack module’s post_init_hook seeds a session for every
+existing internal farm user on install; new users created later pick one
+up lazily on first wizard open.
Important
This is an alpha version, the data model and design can change at any time without warning.
@@ -413,14 +425,16 @@
Farm → Setup Wizard — opens (or resumes) your active session.
+
Look for the leaf icon + red dot in the navbar. That’s the
+onboarding bell — click it to jump into the wizard. (Or use Farm →
+Setup Wizard for the same destination.)
Click Next to step through: Welcome → Your Farm → What You Grow →
Books Today → All Set.
Each screen saves on Next/Back so closing the browser doesn’t lose
progress. Skip jumps straight to Done.
-
Once done, the Farm menu’s other items (Eggs, CSA, Markets, Delivery,
-QuickBooks) are where the actual work happens — the wizard pre-tells
-the pack what to surface for your operation.
+
Once done, the bell disappears and the Farm menu’s other items (Eggs,
+CSA, Markets, Delivery, QuickBooks) are where the actual work happens
+— the wizard pre-tells the pack what to surface for your operation.
diff --git a/farm_pack/README.rst b/farm_pack/README.rst
index 3c3a2d3..322188d 100644
--- a/farm_pack/README.rst
+++ b/farm_pack/README.rst
@@ -39,6 +39,12 @@ also install the leaf modules individually (``farm_egg_production``,
Composition rationale and the four-jobs plan: see
`plans/2-farm-pack-composition.md `__.
+On install, a ``post_init_hook`` seeds one ``farm.onboarding.session``
+per (internal farm user, company) pair so the navbar bell from
+``farm_onboarding`` shows up immediately for every existing farm user.
+Portal users and tooling accounts are excluded. The hook is idempotent —
+re-running it (e.g., after an upgrade) does not duplicate sessions.
+
.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
diff --git a/farm_pack/hooks.py b/farm_pack/hooks.py
index aadf4ee..f6d9588 100644
--- a/farm_pack/hooks.py
+++ b/farm_pack/hooks.py
@@ -44,4 +44,8 @@ def post_init_hook(env):
}
)
seeded += 1
- _logger.info("farm_pack post_init_hook seeded %d onboarding session(s)", seeded)
+ if seeded:
+ _logger.info("farm_pack post_init_hook seeded %d onboarding session(s)", seeded)
+ else:
+ # Idempotent re-run / fresh db with no farm users — quiet path.
+ _logger.debug("farm_pack post_init_hook: no new onboarding sessions to seed")
diff --git a/farm_pack/readme/DESCRIPTION.md b/farm_pack/readme/DESCRIPTION.md
index 7b7fd90..56c5fbc 100644
--- a/farm_pack/readme/DESCRIPTION.md
+++ b/farm_pack/readme/DESCRIPTION.md
@@ -10,3 +10,9 @@ install the leaf modules individually (`farm_egg_production`, `farm_csa`,
Composition rationale and the four-jobs plan: see
[plans/2-farm-pack-composition.md](https://github.com/ledoent/farm-pack/blob/main/plans/2-farm-pack-composition.md).
+
+On install, a `post_init_hook` seeds one `farm.onboarding.session` per
+(internal farm user, company) pair so the navbar bell from
+`farm_onboarding` shows up immediately for every existing farm user.
+Portal users and tooling accounts are excluded. The hook is idempotent
+— re-running it (e.g., after an upgrade) does not duplicate sessions.
diff --git a/farm_pack/readme/newsfragments/+post_init_seed.feature.md b/farm_pack/readme/newsfragments/+post_init_seed.feature.md
new file mode 100644
index 0000000..a73876a
--- /dev/null
+++ b/farm_pack/readme/newsfragments/+post_init_seed.feature.md
@@ -0,0 +1,9 @@
+Adds a `post_init_hook` that seeds one `farm.onboarding.session` per
+(internal farm user, company) pair on install. Existing installs
+upgrade and surface the `farm_onboarding` navbar bell immediately
+instead of waiting for users to discover the **Farm → Setup Wizard**
+menu item.
+
+Portal users (`share=True`) and tooling accounts are excluded. The
+hook is idempotent — re-running it after an upgrade does not
+duplicate sessions.
diff --git a/farm_pack/static/description/index.html b/farm_pack/static/description/index.html
index 865eb8c..f41ab5f 100644
--- a/farm_pack/static/description/index.html
+++ b/farm_pack/static/description/index.html
@@ -385,6 +385,11 @@
Farm Pack
farm_csa, farm_delivery_routes) for narrower deployments.
On install, a post_init_hook seeds one farm.onboarding.session
+per (internal farm user, company) pair so the navbar bell from
+farm_onboarding shows up immediately for every existing farm user.
+Portal users and tooling accounts are excluded. The hook is idempotent —
+re-running it (e.g., after an upgrade) does not duplicate sessions.
Important
This is an alpha version, the data model and design can change at any time without warning.