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/__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..0874b88 100644 --- a/farm_onboarding/models/farm_onboarding_session.py +++ b/farm_onboarding/models/farm_onboarding_session.py @@ -131,6 +131,22 @@ def get_or_create_for_current_user(self): ) return session + @api.model + def count_pending_for_current_user(self): + """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( + [ + ("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/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 @@
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:
+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 @@
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. 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..38a5873 --- /dev/null +++ b/farm_pack/tests/test_post_init_hook.py @@ -0,0 +1,114 @@ +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", + ) + + 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", + )