diff --git a/src/beanify/base/partial.py b/src/beanify/base/partial.py index d2f6b15..5ea24e7 100644 --- a/src/beanify/base/partial.py +++ b/src/beanify/base/partial.py @@ -65,10 +65,7 @@ class PartialTXN: tags: str = "" links: str = "" source_posting: PartialPosting - source_fee_asset_posting: Optional[PartialPosting] = None - source_fee_expense_posting: Optional[PartialPosting] = None target_posting: PartialPosting - target_fee_expense_posting: Optional[PartialPosting] = None # Exposing set of fields which may be updated (e.g. from rules) # Map field name -> display name @@ -81,17 +78,11 @@ class PartialTXN: "tags": "Tags", "links": "Links", "source_account": "Source Account", - "source_fee_asset_account": "Source Fee Asset Account", - "source_fee_expense_account": "Source Fee Expense Account", "target_account": "Target Account", - "target_fee_expense_account": "Target Fee Expense Account", } posting_fields = { "source_posting": "source_account", - "source_fee_asset_posting": "source_fee_asset_account", - "source_fee_expense_posting": "source_fee_expense_account", "target_posting": "target_account", - "target_fee_expense_posting": "target_fee_expense_account", } @property @@ -110,48 +101,6 @@ class PartialTXN: def target_account(self, value: str): self.target_posting.account = value - @property - def source_fee_asset_account(self): - if (posting := self.source_fee_asset_posting) is not None: - return posting.account - - @source_fee_asset_account.setter - def source_fee_asset_account(self, value: str): - if (posting := self.source_fee_asset_posting) is not None: - posting.account = value - else: - raise ValueError( - "This TXN does not have a source fee asset posting to set." - ) - - @property - def source_fee_expense_account(self): - if (posting := self.source_fee_expense_posting) is not None: - return posting.account - - @source_fee_expense_account.setter - def source_fee_expense_account(self, value: str): - if (posting := self.source_fee_expense_posting) is not None: - posting.account = value - else: - raise ValueError( - "This TXN does not have a source fee expense posting to set." - ) - - @property - def target_fee_expense_account(self): - if (posting := self.target_fee_expense_posting) is not None: - return posting.account - - @target_fee_expense_account.setter - def target_fee_expense_account(self, value: str): - if (posting := self.target_fee_expense_posting) is not None: - posting.account = value - else: - raise ValueError( - "This TXN does not have a target fee expense posting to set." - ) - @property def postings(self): postings = {} @@ -265,12 +214,6 @@ class PartialTXN: updater[name] = userstr or None case "source_account" | "target_account": updater[name] = userstr - case "source_fee_asset_account": - updater[name] = userstr - case "source_fee_expense_account": - updater[name] = userstr - case "target_fee_expense_account": - updater[name] = userstr case _: raise ValueError(f"Unknown field {name} passed to TXN parser.") return updater diff --git a/src/beanify/converters/wise_converter.py b/src/beanify/converters/wise_converter.py index 3585e04..933ddce 100644 --- a/src/beanify/converters/wise_converter.py +++ b/src/beanify/converters/wise_converter.py @@ -1,14 +1,16 @@ -import csv +from typing import Optional from dataclasses import dataclass from enum import Enum import datetime as dt from datetime import datetime import logging +import csv from ..base import Converter, PartialTXN, PartialPosting, Record, Amount from ..base.converter import ConverterConfig from ..base.rules import RuleSet from ..base.transaction import TXNFlag +from ..base.partial import UserInputError from . import available_converter @@ -212,11 +214,107 @@ class WiseConfig(ConverterConfig): return cls(data["asset-account"], data["fee-account"]) +@dataclass(kw_only=True) +class WisePartialTXN(PartialTXN): + source_fee_asset_posting: Optional[PartialPosting] = None + source_fee_expense_posting: Optional[PartialPosting] = None + target_fee_expense_posting: Optional[PartialPosting] = None + + fields = PartialTXN.fields | { + "source_fee_asset_account": "Source Fee Asset Account", + "source_fee_expense_account": "Source Fee Expense Account", + "target_fee_expense_account": "Target Fee Expense Account", + } + + posting_fields = PartialTXN.posting_fields | { + "source_fee_asset_posting": "source_fee_asset_account", + "source_fee_expense_posting": "source_fee_expense_account", + "target_fee_expense_posting": "target_fee_expense_account", + } + + @property + def source_fee_asset_account(self): + if (posting := self.source_fee_asset_posting) is not None: + return posting.account + + @source_fee_asset_account.setter + def source_fee_asset_account(self, value: str): + if (posting := self.source_fee_asset_posting) is not None: + posting.account = value + else: + raise ValueError( + "This TXN does not have a source fee asset posting to set." + ) + + @property + def source_fee_expense_account(self): + if (posting := self.source_fee_expense_posting) is not None: + return posting.account + + @source_fee_expense_account.setter + def source_fee_expense_account(self, value: str): + if (posting := self.source_fee_expense_posting) is not None: + posting.account = value + else: + raise ValueError( + "This TXN does not have a source fee expense posting to set." + ) + + @property + def target_fee_expense_account(self): + if (posting := self.target_fee_expense_posting) is not None: + return posting.account + + @target_fee_expense_account.setter + def target_fee_expense_account(self, value: str): + if (posting := self.target_fee_expense_posting) is not None: + posting.account = value + else: + raise ValueError( + "This TXN does not have a target fee expense posting to set." + ) + + def parse_input(self, entries: dict[str, str]): + """ + Parse a map of field name -> user entered strings + into a dictionary which may be used in update() + """ + updater = {} + for name, userstr in entries.items(): + userstr = userstr.strip() + # TODO: Each of these cases needs custom validation + match name: + case "flag": + if userstr == "!": + updater["flag"] = TXNFlag.INCOMPLETE.value + elif userstr == "*": + updater["flag"] = TXNFlag.COMPLETE.value + else: + raise UserInputError( + "Transaction flag must be either '*' or '!'" + ) + case "payee" | "narration" | "tags" | "links": + updater[name] = userstr + case "comment" | "document": + updater[name] = userstr or None + case "source_account" | "target_account": + updater[name] = userstr + case "source_fee_asset_account": + updater[name] = userstr + case "source_fee_expense_account": + updater[name] = userstr + case "target_fee_expense_account": + updater[name] = userstr + case _: + raise ValueError(f"Unknown field {name} passed to TXN parser.") + return updater + + @available_converter -class WiseConverter(Converter[WiseRecord, PartialTXN, WiseConfig]): +class WiseConverter(Converter[WiseRecord, WisePartialTXN, WiseConfig]): record_type = WiseRecord config_type = WiseConfig - txn_type = PartialTXN + txn_type = WisePartialTXN converter_name = "wise" version = "0" display_name = "Wise Record Converter v0" @@ -225,9 +323,9 @@ class WiseConverter(Converter[WiseRecord, PartialTXN, WiseConfig]): def __init__(self, config: WiseConfig, **kwargs): self.config = config - def annotation(self, record: WiseRecord, partial: PartialTXN): ... + def annotation(self, record: WiseRecord, partial: WisePartialTXN): ... - def convert(self, record: WiseRecord, ruleset: RuleSet) -> PartialTXN: + def convert(self, record: WiseRecord, ruleset: RuleSet) -> WisePartialTXN: fields = {} match record.direction: @@ -302,7 +400,7 @@ class WiseConverter(Converter[WiseRecord, PartialTXN, WiseConfig]): amount=record.target_fee, ) - txn = PartialTXN(**args) + txn = WisePartialTXN(**args) logger.debug(f"Converted Wise Record {record!r} to Partial Transaction {txn!r}") return txn diff --git a/src/beanify/gui/mainwindow.py b/src/beanify/gui/mainwindow.py index 149b7d5..008a164 100644 --- a/src/beanify/gui/mainwindow.py +++ b/src/beanify/gui/mainwindow.py @@ -74,8 +74,12 @@ class MainWindow(ThemedTk): menu_file.add_command(label="Save Rules", command=self.do_save_rules) menu_file.add_command(label="Reload Rules", command=self.do_reload_rules) menu_file.add_separator() - menu_file.add_command(label="Save Partial Transactions", command=self.do_save_partial) - menu_file.add_command(label="Load Partial Transactions", command=self.do_load_partial) + menu_file.add_command( + label="Save Partial Transactions", command=self.do_save_partial + ) + menu_file.add_command( + label="Load Partial Transactions", command=self.do_load_partial + ) menu_file.add_separator() menu_file.add_command(label="Exit", command=lambda: self.destroy()) self.menubar.add_cascade(menu=menu_file, label="File") @@ -100,9 +104,7 @@ class MainWindow(ThemedTk): self.bind_all("", lambda event: self.rowtree.focus_prev()) self.bind_all("", lambda event: self.rowtree.focus_next()) self.bind_all("", lambda event: self.rowtree.focus_prev_partial()) - self.bind_all( - "", lambda event: self.rowtree.focus_next_partial() - ) + self.bind_all("", lambda event: self.rowtree.focus_next_partial()) self.bind_all( "", lambda event: self.editor.focus_record_frame() @@ -138,7 +140,7 @@ class MainWindow(ThemedTk): self.content.add(self.rowtree, weight=1) self.editor = RowEditor( - self, acmpl_cache=self.account_cache, padding=(3, 3, 12, 12) + self, self.converter, acmpl_cache=self.account_cache, padding=(3, 3, 12, 12) ) self.content.add(self.editor, weight=1) @@ -335,7 +337,7 @@ class MainWindow(ThemedTk): # Merge into a map, and update the cached map with it. # Build the list of account names we want to acmpl - field_names = list(PartialTXN.posting_fields.values()) + field_names = list(self.converter.txn_type.posting_fields.values()) cache = {name: set() for name in field_names} # Read the ruleset rules for options @@ -400,7 +402,7 @@ class MainWindow(ThemedTk): if result["converter"] != self.converter.qual_name(): messagebox.showerror( "Load Error", - f"Cannot load partial data for converter '{result['converter']}'" + f"Cannot load partial data for converter '{result['converter']}'", ) return self.rows |= result["rows"] diff --git a/src/beanify/gui/roweditor.py b/src/beanify/gui/roweditor.py index df7c397..87a6c3a 100644 --- a/src/beanify/gui/roweditor.py +++ b/src/beanify/gui/roweditor.py @@ -1,6 +1,8 @@ from tkinter import BooleanVar, StringVar, ttk +from typing import Type +from ..base.converter import Converter from ..base.partial import PartialTXN from ..base.record import Record @@ -8,9 +10,11 @@ from . import logger class RowEditor(ttk.Frame): - def __init__(self, master, acmpl_cache={}, **kwargs): + def __init__(self, master, converter: Converter, acmpl_cache={}, **kwargs): super().__init__(master, **kwargs) + self.converter = converter + # Data state self.rows: list[tuple[Record, PartialTXN]] = [] self.acmpl_cache: dict[str, list[str]] = acmpl_cache @@ -432,7 +436,7 @@ class RowEditor(ttk.Frame): ) # The txn display fields will always be a subset of this - fieldnames = PartialTXN.fields + fieldnames = self.converter.txn_type.fields # field -> stringvar txnvars = {name: StringVar() for name in fieldnames} diff --git a/src/beanify/gui/rowtree.py b/src/beanify/gui/rowtree.py index 9ff135c..1037f59 100644 --- a/src/beanify/gui/rowtree.py +++ b/src/beanify/gui/rowtree.py @@ -175,10 +175,10 @@ class RowTree(ttk.Frame): "txn.tags": ("Bean Tags", str), "txn.links": ("Bean Links", str), "txn.source_account": ("Bean Source", str), - "txn.source_fee_asset_account": ("Bean Source Fee Asset Acc", str), - "txn.source_fee_expense_account": ("Bean Source Fee Expense Acc", str), + # "txn.source_fee_asset_account": ("Bean Source Fee Asset Acc", str), + # "txn.source_fee_expense_account": ("Bean Source Fee Expense Acc", str), "txn.target_account": ("Bean Target", str), - "txn.target_fee_expense_account": ("Bean Target Fee Acc", str), + # "txn.target_fee_expense_account": ("Bean Target Fee Acc", str), } return columns