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 || {};