From 42cde2a0c882f7569a047c77b9b14e010608684b Mon Sep 17 00:00:00 2001 From: Marcel Peterkau Date: Fri, 14 Aug 2026 22:33:57 +0200 Subject: [PATCH] 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 --- src/ccma/config.py | 11 ++++++++++- src/ccma/ui/gnucash_import_dialog.py | 27 ++++++++++++++++++++++++++- tests/test_config.py | 24 ++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/ccma/config.py b/src/ccma/config.py index 82abf55..ad61a57 100644 --- a/src/ccma/config.py +++ b/src/ccma/config.py @@ -3,7 +3,7 @@ from __future__ import annotations import json import math import os -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING @@ -18,6 +18,7 @@ if TYPE_CHECKING: class AppConfig: store_path: str = "" gnucash_path: str = "" + gnucash_last_accounts: dict[str, str] = field(default_factory=dict) theme_mode: str = "dark" run_housekeeper_on_startup: bool = True splash_minimum_seconds: float = 5.0 @@ -43,6 +44,7 @@ class AppConfig: "schema_version": 1, "store_path": self.store_path, "gnucash_path": self.gnucash_path, + "gnucash_last_accounts": self.gnucash_last_accounts, "theme_mode": self.theme_mode, "run_housekeeper_on_startup": self.run_housekeeper_on_startup, "splash_minimum_seconds": _non_negative_float(self.splash_minimum_seconds, 5.0), @@ -97,9 +99,16 @@ def load_config() -> AppConfig: monitor_bounds = None if isinstance(monitor_raw, list) and len(monitor_raw) == 4: 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( store_path=store_override or str(data.get("store_path", "")), gnucash_path=str(data.get("gnucash_path", "")), + gnucash_last_accounts=gnucash_last_accounts, theme_mode=str(data.get("theme_mode", "dark")), 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), diff --git a/src/ccma/ui/gnucash_import_dialog.py b/src/ccma/ui/gnucash_import_dialog.py index 74754fd..0fd75d4 100644 --- a/src/ccma/ui/gnucash_import_dialog.py +++ b/src/ccma/ui/gnucash_import_dialog.py @@ -210,7 +210,7 @@ class GnuCashImportDialog(tk.Toplevel): } 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: - 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() if self.account_var.get(): self._account_selected() @@ -221,6 +221,27 @@ class GnuCashImportDialog(tk.Toplevel): self.config.gnucash_path = path_text 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: account = self.account_by_label.get(self.account_var.get()) 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.selected_guids.clear() 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]: needle = self.description_filter_var.get().strip().casefold() diff --git a/tests/test_config.py b/tests/test_config.py index f18c591..ff5e554 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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")) 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 == {}