feat: Parametrise converter by txn type
This commit is contained in:
@@ -65,10 +65,7 @@ class PartialTXN:
|
|||||||
tags: str = ""
|
tags: str = ""
|
||||||
links: str = ""
|
links: str = ""
|
||||||
source_posting: PartialPosting
|
source_posting: PartialPosting
|
||||||
source_fee_asset_posting: Optional[PartialPosting] = None
|
|
||||||
source_fee_expense_posting: Optional[PartialPosting] = None
|
|
||||||
target_posting: PartialPosting
|
target_posting: PartialPosting
|
||||||
target_fee_expense_posting: Optional[PartialPosting] = None
|
|
||||||
|
|
||||||
# Exposing set of fields which may be updated (e.g. from rules)
|
# Exposing set of fields which may be updated (e.g. from rules)
|
||||||
# Map field name -> display name
|
# Map field name -> display name
|
||||||
@@ -81,17 +78,11 @@ class PartialTXN:
|
|||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"links": "Links",
|
"links": "Links",
|
||||||
"source_account": "Source Account",
|
"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_account": "Target Account",
|
||||||
"target_fee_expense_account": "Target Fee Expense Account",
|
|
||||||
}
|
}
|
||||||
posting_fields = {
|
posting_fields = {
|
||||||
"source_posting": "source_account",
|
"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_posting": "target_account",
|
||||||
"target_fee_expense_posting": "target_fee_expense_account",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -110,48 +101,6 @@ class PartialTXN:
|
|||||||
def target_account(self, value: str):
|
def target_account(self, value: str):
|
||||||
self.target_posting.account = value
|
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
|
@property
|
||||||
def postings(self):
|
def postings(self):
|
||||||
postings = {}
|
postings = {}
|
||||||
@@ -265,12 +214,6 @@ class PartialTXN:
|
|||||||
updater[name] = userstr or None
|
updater[name] = userstr or None
|
||||||
case "source_account" | "target_account":
|
case "source_account" | "target_account":
|
||||||
updater[name] = userstr
|
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 _:
|
case _:
|
||||||
raise ValueError(f"Unknown field {name} passed to TXN parser.")
|
raise ValueError(f"Unknown field {name} passed to TXN parser.")
|
||||||
return updater
|
return updater
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import csv
|
from typing import Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import csv
|
||||||
|
|
||||||
from ..base import Converter, PartialTXN, PartialPosting, Record, Amount
|
from ..base import Converter, PartialTXN, PartialPosting, Record, Amount
|
||||||
from ..base.converter import ConverterConfig
|
from ..base.converter import ConverterConfig
|
||||||
from ..base.rules import RuleSet
|
from ..base.rules import RuleSet
|
||||||
from ..base.transaction import TXNFlag
|
from ..base.transaction import TXNFlag
|
||||||
|
from ..base.partial import UserInputError
|
||||||
|
|
||||||
from . import available_converter
|
from . import available_converter
|
||||||
|
|
||||||
@@ -212,11 +214,107 @@ class WiseConfig(ConverterConfig):
|
|||||||
return cls(data["asset-account"], data["fee-account"])
|
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
|
@available_converter
|
||||||
class WiseConverter(Converter[WiseRecord, PartialTXN, WiseConfig]):
|
class WiseConverter(Converter[WiseRecord, WisePartialTXN, WiseConfig]):
|
||||||
record_type = WiseRecord
|
record_type = WiseRecord
|
||||||
config_type = WiseConfig
|
config_type = WiseConfig
|
||||||
txn_type = PartialTXN
|
txn_type = WisePartialTXN
|
||||||
converter_name = "wise"
|
converter_name = "wise"
|
||||||
version = "0"
|
version = "0"
|
||||||
display_name = "Wise Record Converter v0"
|
display_name = "Wise Record Converter v0"
|
||||||
@@ -225,9 +323,9 @@ class WiseConverter(Converter[WiseRecord, PartialTXN, WiseConfig]):
|
|||||||
def __init__(self, config: WiseConfig, **kwargs):
|
def __init__(self, config: WiseConfig, **kwargs):
|
||||||
self.config = config
|
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 = {}
|
fields = {}
|
||||||
|
|
||||||
match record.direction:
|
match record.direction:
|
||||||
@@ -302,7 +400,7 @@ class WiseConverter(Converter[WiseRecord, PartialTXN, WiseConfig]):
|
|||||||
amount=record.target_fee,
|
amount=record.target_fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
txn = PartialTXN(**args)
|
txn = WisePartialTXN(**args)
|
||||||
|
|
||||||
logger.debug(f"Converted Wise Record {record!r} to Partial Transaction {txn!r}")
|
logger.debug(f"Converted Wise Record {record!r} to Partial Transaction {txn!r}")
|
||||||
return txn
|
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="Save Rules", command=self.do_save_rules)
|
||||||
menu_file.add_command(label="Reload Rules", command=self.do_reload_rules)
|
menu_file.add_command(label="Reload Rules", command=self.do_reload_rules)
|
||||||
menu_file.add_separator()
|
menu_file.add_separator()
|
||||||
menu_file.add_command(label="Save Partial Transactions", command=self.do_save_partial)
|
menu_file.add_command(
|
||||||
menu_file.add_command(label="Load Partial Transactions", command=self.do_load_partial)
|
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_separator()
|
||||||
menu_file.add_command(label="Exit", command=lambda: self.destroy())
|
menu_file.add_command(label="Exit", command=lambda: self.destroy())
|
||||||
self.menubar.add_cascade(menu=menu_file, label="File")
|
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-k>", lambda event: self.rowtree.focus_prev())
|
||||||
self.bind_all("<Alt-j>", lambda event: self.rowtree.focus_next())
|
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-K>", lambda event: self.rowtree.focus_prev_partial())
|
||||||
self.bind_all(
|
self.bind_all("<Alt-Shift-J>", lambda event: self.rowtree.focus_next_partial())
|
||||||
"<Alt-Shift-J>", lambda event: self.rowtree.focus_next_partial()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.bind_all(
|
self.bind_all(
|
||||||
"<Alt-KeyPress-2>", lambda event: self.editor.focus_record_frame()
|
"<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.content.add(self.rowtree, weight=1)
|
||||||
|
|
||||||
self.editor = RowEditor(
|
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)
|
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.
|
# Merge into a map, and update the cached map with it.
|
||||||
|
|
||||||
# Build the list of account names we want to acmpl
|
# 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}
|
cache = {name: set() for name in field_names}
|
||||||
|
|
||||||
# Read the ruleset rules for options
|
# Read the ruleset rules for options
|
||||||
@@ -400,7 +402,7 @@ class MainWindow(ThemedTk):
|
|||||||
if result["converter"] != self.converter.qual_name():
|
if result["converter"] != self.converter.qual_name():
|
||||||
messagebox.showerror(
|
messagebox.showerror(
|
||||||
"Load Error",
|
"Load Error",
|
||||||
f"Cannot load partial data for converter '{result['converter']}'"
|
f"Cannot load partial data for converter '{result['converter']}'",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
self.rows |= result["rows"]
|
self.rows |= result["rows"]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from tkinter import BooleanVar, StringVar, ttk
|
from tkinter import BooleanVar, StringVar, ttk
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
|
||||||
|
from ..base.converter import Converter
|
||||||
from ..base.partial import PartialTXN
|
from ..base.partial import PartialTXN
|
||||||
from ..base.record import Record
|
from ..base.record import Record
|
||||||
|
|
||||||
@@ -8,9 +10,11 @@ from . import logger
|
|||||||
|
|
||||||
|
|
||||||
class RowEditor(ttk.Frame):
|
class RowEditor(ttk.Frame):
|
||||||
def __init__(self, master, acmpl_cache={}, **kwargs):
|
def __init__(self, master, converter: Converter, acmpl_cache={}, **kwargs):
|
||||||
super().__init__(master, **kwargs)
|
super().__init__(master, **kwargs)
|
||||||
|
|
||||||
|
self.converter = converter
|
||||||
|
|
||||||
# Data state
|
# Data state
|
||||||
self.rows: list[tuple[Record, PartialTXN]] = []
|
self.rows: list[tuple[Record, PartialTXN]] = []
|
||||||
self.acmpl_cache: dict[str, list[str]] = acmpl_cache
|
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
|
# The txn display fields will always be a subset of this
|
||||||
fieldnames = PartialTXN.fields
|
fieldnames = self.converter.txn_type.fields
|
||||||
|
|
||||||
# field -> stringvar
|
# field -> stringvar
|
||||||
txnvars = {name: StringVar() for name in fieldnames}
|
txnvars = {name: StringVar() for name in fieldnames}
|
||||||
|
|||||||
@@ -175,10 +175,10 @@ class RowTree(ttk.Frame):
|
|||||||
"txn.tags": ("Bean Tags", str),
|
"txn.tags": ("Bean Tags", str),
|
||||||
"txn.links": ("Bean Links", str),
|
"txn.links": ("Bean Links", str),
|
||||||
"txn.source_account": ("Bean Source", str),
|
"txn.source_account": ("Bean Source", str),
|
||||||
"txn.source_fee_asset_account": ("Bean Source Fee Asset 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.source_fee_expense_account": ("Bean Source Fee Expense Acc", str),
|
||||||
"txn.target_account": ("Bean Target", 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
|
return columns
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user