From 9cfc1737eb928a5cb35e269ac76a35d601219844 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 4 Jun 2026 11:53:49 +0530 Subject: [PATCH] fix: resilient default-company resolution on dashboard open (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger Lab opened to a warning + no data when the resolved default company didn't exist — e.g. a stale user default (a leftover "LL E2E Probe" pointing at a deleted company) took precedence over the valid Global Default and made get_balances throw "Company X does not exist." Key decisions: - Backend `_resolve_company` no longer throws on a missing/stale default; it falls back to the first company the user can read (the Company picker only ever submits real companies, so this never masks a genuine bad selection). Only a company-less site still errors. - Frontend syncs the picker to the server-resolved company via `sync_company_field()`, so a stale client-side default is corrected to the company actually being shown (guarded against a reload loop). Files: ledger_lab/api/dashboard.py, ledger_lab/.../page/ledger_lab/ledger_lab.js Verified (agent-browser, ledger.localhost): page loads with picker=BWH, all boxes populated, equation "✓ Balanced", feed rows present, no warning. Notes for next iteration: issues #5 (transaction filters) and #3 (equation card UI) remain open. Co-Authored-By: Claude Opus 4.8 (1M context) --- ledger_lab/api/dashboard.py | 18 ++++++++++++++---- .../ledger_lab/page/ledger_lab/ledger_lab.js | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/ledger_lab/api/dashboard.py b/ledger_lab/api/dashboard.py index 8867731..f9678c1 100644 --- a/ledger_lab/api/dashboard.py +++ b/ledger_lab/api/dashboard.py @@ -11,17 +11,27 @@ VALID_SCOPES = frozenset({"fy", "all"}) +def _first_accessible_company() -> str | None: + """Oldest company the current user can read, or None if there are none.""" + companies = frappe.get_list("Company", pluck="name", order_by="creation asc", limit=1) + return companies[0] if companies else None + + def _resolve_company(company: str | None) -> str: company = company.strip() if company else None if not company: company = frappe.defaults.get_user_default("Company") or frappe.db.get_single_value( "Global Defaults", "default_company" ) + # A missing or stale default (e.g. a deleted/renamed company, or a leftover + # user default pointing at a company that no longer exists) shouldn't blank + # out the dashboard. Fall back to the first company the user can access so + # the page always opens with data. The Company picker only ever submits real + # companies, so this lenient fallback never masks a genuine bad selection. + if not company or not frappe.db.exists("Company", company): + company = _first_accessible_company() if not company: - frappe.throw(_("No company found. Please set a default company.")) - - if not frappe.db.exists("Company", company): - frappe.throw(_("Company {0} does not exist.").format(company)) + frappe.throw(_("No company found. Please create a company first.")) frappe.has_permission("Company", ptype="read", doc=company, throw=True) return company diff --git a/ledger_lab/ledger_lab/page/ledger_lab/ledger_lab.js b/ledger_lab/ledger_lab/page/ledger_lab/ledger_lab.js index de55b9c..e7e9031 100644 --- a/ledger_lab/ledger_lab/page/ledger_lab/ledger_lab.js +++ b/ledger_lab/ledger_lab/page/ledger_lab/ledger_lab.js @@ -451,6 +451,8 @@ class LedgerLab { if (this.company) this.company_field.set_value(this.company); // Scope tabs: This Fiscal Year / All Time. + // (sync_company_field below keeps the picker honest once the server + // echoes the authoritatively-resolved company.) this.page.main.find(".ll-scope-tab").on("click", (e) => { const scope = e.currentTarget.getAttribute("data-scope"); if (scope === this.scope) return; @@ -558,6 +560,16 @@ class LedgerLab { this.load_feed(); } + // Point the picker at the server-resolved company without re-triggering a + // reload (the change handler no-ops when the value already matches + // this.company, which refresh() sets just before calling this). + sync_company_field() { + if (!this.company_field || !this.company) return; + if (this.company_field.get_value() !== this.company) { + this.company_field.set_value(this.company); + } + } + bind_realtime() { frappe.realtime.off("ledger_lab_gl_posted"); frappe.realtime.on("ledger_lab_gl_posted", (data) => { @@ -592,6 +604,11 @@ class LedgerLab { callback: (r) => { if (!r.message) return; this.company = r.message.company; + // Reflect the server-resolved company in the picker. Handles the + // case where the client-side default was empty or stale (pointing + // at a company that no longer exists): the box loads data for the + // real fallback company, so the picker should show it too. + this.sync_company_field(); this.currency = r.message.currency; this.date_range = r.message.date_range || { start: null, end: null }; const b = r.message.boxes || {};