feat: Parametrise converter by txn type
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("<Alt-k>", lambda event: self.rowtree.focus_prev())
|
||||
self.bind_all("<Alt-j>", lambda event: self.rowtree.focus_next())
|
||||
self.bind_all("<Alt-Shift-K>", lambda event: self.rowtree.focus_prev_partial())
|
||||
self.bind_all(
|
||||
"<Alt-Shift-J>", lambda event: self.rowtree.focus_next_partial()
|
||||
)
|
||||
self.bind_all("<Alt-Shift-J>", lambda event: self.rowtree.focus_next_partial())
|
||||
|
||||
self.bind_all(
|
||||
"<Alt-KeyPress-2>", 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"]
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user