Remember the last-used GnuCash account per file

AppConfig now keeps a gnucash_last_accounts map (file path -> account
guid). When the import dialog opens a file it already knows, it
auto-selects whichever account was picked last time for that specific
file instead of always defaulting to the first one alphabetically;
picking a different account updates and persists the mapping right away.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-08-14 23:02:37 +02:00
co-authored by Claude Sonnet 5
parent 745e634a8b
commit 42cde2a0c8
3 changed files with 60 additions and 2 deletions
+10 -1
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import json import json
import math import math
import os import os
from dataclasses import dataclass from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
class AppConfig: class AppConfig:
store_path: str = "" store_path: str = ""
gnucash_path: str = "" gnucash_path: str = ""
gnucash_last_accounts: dict[str, str] = field(default_factory=dict)
theme_mode: str = "dark" theme_mode: str = "dark"
run_housekeeper_on_startup: bool = True run_housekeeper_on_startup: bool = True
splash_minimum_seconds: float = 5.0 splash_minimum_seconds: float = 5.0
@@ -43,6 +44,7 @@ class AppConfig:
"schema_version": 1, "schema_version": 1,
"store_path": self.store_path, "store_path": self.store_path,
"gnucash_path": self.gnucash_path, "gnucash_path": self.gnucash_path,
"gnucash_last_accounts": self.gnucash_last_accounts,
"theme_mode": self.theme_mode, "theme_mode": self.theme_mode,
"run_housekeeper_on_startup": self.run_housekeeper_on_startup, "run_housekeeper_on_startup": self.run_housekeeper_on_startup,
"splash_minimum_seconds": _non_negative_float(self.splash_minimum_seconds, 5.0), "splash_minimum_seconds": _non_negative_float(self.splash_minimum_seconds, 5.0),
@@ -97,9 +99,16 @@ def load_config() -> AppConfig:
monitor_bounds = None monitor_bounds = None
if isinstance(monitor_raw, list) and len(monitor_raw) == 4: if isinstance(monitor_raw, list) and len(monitor_raw) == 4:
monitor_bounds = tuple(int(value) for value in monitor_raw) monitor_bounds = tuple(int(value) for value in monitor_raw)
last_accounts_raw = data.get("gnucash_last_accounts")
gnucash_last_accounts = (
{str(key): str(value) for key, value in last_accounts_raw.items()}
if isinstance(last_accounts_raw, dict)
else {}
)
return AppConfig( return AppConfig(
store_path=store_override or str(data.get("store_path", "")), store_path=store_override or str(data.get("store_path", "")),
gnucash_path=str(data.get("gnucash_path", "")), gnucash_path=str(data.get("gnucash_path", "")),
gnucash_last_accounts=gnucash_last_accounts,
theme_mode=str(data.get("theme_mode", "dark")), theme_mode=str(data.get("theme_mode", "dark")),
run_housekeeper_on_startup=bool(data.get("run_housekeeper_on_startup", True)), run_housekeeper_on_startup=bool(data.get("run_housekeeper_on_startup", True)),
splash_minimum_seconds=_non_negative_float(data.get("splash_minimum_seconds", 5.0), 5.0), splash_minimum_seconds=_non_negative_float(data.get("splash_minimum_seconds", 5.0), 5.0),
+26 -1
View File
@@ -210,7 +210,7 @@ class GnuCashImportDialog(tk.Toplevel):
} }
self.account_combo.configure(values=list(self.account_by_label)) self.account_combo.configure(values=list(self.account_by_label))
if self.account_by_label and self.account_var.get() not in self.account_by_label: if self.account_by_label and self.account_var.get() not in self.account_by_label:
self.account_var.set(next(iter(self.account_by_label))) self.account_var.set(self._preferred_account_label() or next(iter(self.account_by_label)))
self.selected_guids.clear() self.selected_guids.clear()
if self.account_var.get(): if self.account_var.get():
self._account_selected() self._account_selected()
@@ -221,6 +221,27 @@ class GnuCashImportDialog(tk.Toplevel):
self.config.gnucash_path = path_text self.config.gnucash_path = path_text
self.config.save() self.config.save()
def _current_file_key(self) -> str | None:
path_text = self.file_var.get().strip()
if not path_text:
return None
path = Path(path_text).expanduser()
if not path.is_file():
return None
return str(path.resolve())
def _preferred_account_label(self) -> str | None:
file_key = self._current_file_key()
if file_key is None:
return None
last_guid = self.config.gnucash_last_accounts.get(file_key)
if not last_guid:
return None
return next(
(label for label, account in self.account_by_label.items() if account.guid == last_guid),
None,
)
def _account_selected(self) -> None: def _account_selected(self) -> None:
account = self.account_by_label.get(self.account_var.get()) account = self.account_by_label.get(self.account_var.get())
if account is None: if account is None:
@@ -236,6 +257,10 @@ class GnuCashImportDialog(tk.Toplevel):
self.transactions = [item for item in all_transactions if item.amount > 0] self.transactions = [item for item in all_transactions if item.amount > 0]
self.selected_guids.clear() self.selected_guids.clear()
self._render_transactions() self._render_transactions()
file_key = self._current_file_key()
if file_key is not None and self.config.gnucash_last_accounts.get(file_key) != account.guid:
self.config.gnucash_last_accounts[file_key] = account.guid
self.config.save()
def _filtered_transactions(self) -> list[GnuCashTransaction]: def _filtered_transactions(self) -> list[GnuCashTransaction]:
needle = self.description_filter_var.get().strip().casefold() needle = self.description_filter_var.get().strip().casefold()
+24
View File
@@ -48,3 +48,27 @@ def test_store_path_can_be_overridden_from_ccma_environment(tmp_path, monkeypatc
monkeypatch.setenv("CCMA_STORE", str(tmp_path / "store")) monkeypatch.setenv("CCMA_STORE", str(tmp_path / "store"))
assert load_config().store_path == str(tmp_path / "store") assert load_config().store_path == str(tmp_path / "store")
def test_gnucash_last_account_per_file_round_trips(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("CCMA_CONFIG_DIR", str(tmp_path / "config"))
first_file = str(tmp_path / "verein.gnucash")
second_file = str(tmp_path / "spenden.gnucash")
config = AppConfig(
gnucash_last_accounts={first_file: "bank-guid", second_file: "cash-guid"}
)
config.save()
loaded = load_config()
assert loaded.gnucash_last_accounts == {first_file: "bank-guid", second_file: "cash-guid"}
def test_gnucash_last_accounts_defaults_to_empty_dict_for_malformed_value(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("CCMA_CONFIG_DIR", str(tmp_path / "config"))
config_path = tmp_path / "config" / "config.json"
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(
json.dumps({"schema_version": 1, "gnucash_last_accounts": "not-a-dict"}), encoding="utf-8"
)
assert load_config().gnucash_last_accounts == {}