Compare commits

...

7 Commits

11 changed files with 584 additions and 82 deletions
+4 -3
View File
@@ -44,7 +44,7 @@ class ConverterConfig:
raise NotImplementedError
class Converter[RecordT: Record, ConfigT: ConverterConfig]:
class Converter[RecordT: Record, TxnT: PartialTXN, ConfigT: ConverterConfig]:
"""
ABC for Record -> Transaction conversion interface.
@@ -60,6 +60,7 @@ class Converter[RecordT: Record, ConfigT: ConverterConfig]:
record_type: ClassVar[Type[RecordT]]
config_type: ClassVar[Type[ConfigT]]
txn_type: ClassVar[Type[TxnT]]
def __init__(self, config: ConfigT, **kwargs):
self.config = config
@@ -68,7 +69,7 @@ class Converter[RecordT: Record, ConfigT: ConverterConfig]:
def qual_name(cls):
return f"{cls.converter_name}_v{cls.version}"
def annotation(self, record: RecordT, partial: PartialTXN) -> Optional[str]:
def annotation(self, record: RecordT, partial: TxnT) -> Optional[str]:
"""
Optional user-readable note/warning to attach to a mapped record.
@@ -76,7 +77,7 @@ class Converter[RecordT: Record, ConfigT: ConverterConfig]:
"""
raise NotImplementedError
def convert(self, record: RecordT, ruleset: RuleSet) -> PartialTXN | _SKIPT:
def convert(self, record: RecordT, ruleset: RuleSet) -> TxnT | _SKIPT:
"""
The meat of the conversion process.
Take a raw Record from the data and convert it into a partial transaction,
-57
View File
@@ -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
View File
@@ -34,3 +34,4 @@ def available_converter(converter_cls):
from .wise_converter import *
from .cba_converter import *
from .cbastatement_converter import *
from .paypal_converter import *
+3 -2
View File
@@ -97,9 +97,10 @@ class CBAConfig(ConverterConfig):
@available_converter
class CBAConverter(Converter[CBARecord, CBAConfig]):
class CBAConverter(Converter[CBARecord, PartialTXN, CBAConfig]):
record_type = CBARecord
config_type = CBAConfig
txn_type = PartialTXN
converter_name = "cbacsv"
version = "0"
display_name = "CBACSV converter v0"
@@ -205,7 +206,7 @@ class CBAConverter(Converter[CBARecord, CBAConfig]):
records = []
for row in reader:
if row[0].lower().strip() == "date":
if "date" in row[0].lower().strip():
# Skip header row
continue
record = self._make_record(CBACSVRow(*row))
@@ -94,9 +94,10 @@ class CBASTConfig(ConverterConfig):
@available_converter
class CBASTConverter(Converter[CBASTRecord, CBASTConfig]):
class CBASTConverter(Converter[CBASTRecord, PartialTXN, CBASTConfig]):
record_type = CBASTRecord
config_type = CBASTConfig
txn_type = PartialTXN
converter_name = "cbast"
version = "0"
display_name = "CBAST converter v0"
+398
View File
@@ -0,0 +1,398 @@
"""
Beanify Converter implementation for the hot mess which is paypal csv records.
"""
from typing import NamedTuple, Optional
from dataclasses import dataclass, field
from enum import Enum
import datetime as dt
from datetime import date, datetime, time
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, TXNPosting, Transaction
from ..base.partial import UserInputError
from . import available_converter
logger = logging.getLogger(__name__)
__all__ = [
"PaypalRecord",
"PaypalRecordDirection",
"PaypalCSVRow",
"PaypalPartialTXN",
"PaypalConverter",
]
class PaypalCSVRow(NamedTuple):
date: str
time: str
timezone: str
name: str
txntype: str
status: str
currency: str
amount: str
fees: str
total: str
ex_rate: str
receipt_id: str
balance: str
txnid: str
item_title: str
class PaypalRecordDirection(Enum):
IGNORE = -1
TRANSFERIN = 0
IN = 1
CONVERT = 2
OUT = 3
TRANSFEROUT = 0
def get_direction(typestr):
match typestr:
case "Reversal of General Account Hold":
return PaypalRecordDirection.IGNORE
case "General Currency Conversion":
return PaypalRecordDirection.CONVERT
case "Subscription Payment":
return PaypalRecordDirection.OUT
case "Express Checkout Payment":
return PaypalRecordDirection.OUT
case "Pre-approved Payment Bill User Payment":
return PaypalRecordDirection.OUT
case "Mass payment":
return PaypalRecordDirection.IN
case "Transfer to PayPal account":
return PaypalRecordDirection.TRANSFERIN
case "Account Hold for Open Authorisation":
return PaypalRecordDirection.IGNORE
case "User Initiated Withdrawal":
# TODO: Probably don't actually want to ignore this
# return PaypalRecordDirection.TRANSFEROUT
return PaypalRecordDirection.IGNORE
case "Reversal of ACH Withdrawal Transaction":
return PaypalRecordDirection.IGNORE
case "BillPay transaction":
return PaypalRecordDirection.OUT
case "Payment Refund":
return PaypalRecordDirection.IN
case "Reversal of ACH Deposit":
return PaypalRecordDirection.IGNORE
case "Mobile Payment":
return PaypalRecordDirection.IN
case _:
raise ValueError(f"Unknown Paypal typestr {typestr}")
@dataclass(kw_only=True, frozen=True, eq=False)
class PaypalRecord(Record):
# With time zone, for a change
created_on: datetime
txnid: str
name: str
txntype: str
direction: PaypalRecordDirection
balance_effect: Amount
ex_target_amount: Amount | None = None
# Each source amount is (Amount, exchange rate)
ex_source_amounts: list[tuple[Amount, float]] = field(default_factory=list)
transferred_in: Amount | None = None
_display_fields = [
("txnid", "ID"),
("txntype", "Type"),
("direction", "Direction"),
("created_on", "Created"),
("name", "Name"),
("source_account", "Source"),
("target_account", "Target"),
("balance_effect", "Balance Change"),
("transferred_in", "Transferred In"),
]
_match_fields = [
"txnid",
"txntype",
"direction",
"balance_effect",
"name",
]
@classmethod
def sample_record(cls):
self = cls(
date=dt.date.today(),
source_account="John Doe",
target_account="Jane Austen",
from_source=Amount(314, "CROC"),
to_target=Amount(314, "CROC"),
created_on=datetime.now(),
txnid="0",
name="Sample",
txntype="Payment Refund",
direction=PaypalRecordDirection.IN,
balance_effect=Amount(314, "CROC"),
transferred_in=Amount(314, "CROC"),
ex_target_amount=None,
ex_source_amounts=[],
)
return self
@dataclass
class PaypalConfig(ConverterConfig):
asset_account: str
required = {
"asset-account",
}
@classmethod
def from_dict(cls, data: dict) -> "PaypalConfig":
if (f := next((f for f in cls.required if f not in data), None)) is not None:
raise ValueError(f"Paypal Configuration missing required field: {f}")
return cls(data["asset-account"])
@dataclass(kw_only=True)
class PaypalPartialTXN(PartialTXN):
# Hard-coded postings that will be added to the final transaction
# Very much a hack, will fix later
extra_postings: list[TXNPosting] | None = None
def upgrade(self, defaults=...) -> Transaction:
txn = super().upgrade(defaults)
if self.extra_postings:
txn.postings.extend(self.extra_postings)
return txn
@available_converter
class PaypalConverter(Converter[PaypalRecord, PaypalPartialTXN, PaypalConfig]):
record_type = PaypalRecord
config_type = PaypalConfig
txn_type = PaypalPartialTXN
converter_name = "paypal"
version = "0"
display_name = "Paypal CSV converter v0"
config_field = "PAYPAL"
def __init__(self, config: PaypalConfig, *kwargs):
self.config = config
def convert(self, record: PaypalRecord, ruleset: RuleSet) -> PaypalPartialTXN:
fields = {}
match record.direction:
case PaypalRecordDirection.TRANSFERIN:
fields["target_account"] = self.config.asset_account
case PaypalRecordDirection.IN:
fields["target_account"] = self.config.asset_account
case PaypalRecordDirection.OUT:
fields["source_account"] = self.config.asset_account
case PaypalRecordDirection.TRANSFEROUT:
fields["source_account"] = self.config.asset_account
fields |= ruleset.apply(record.match_fields())
args = {}
args["date"] = record.date
for name in {
"payee",
"narration",
"comment",
"document",
"tags",
"links",
"flag",
}:
if name in fields:
args[name] = fields[name]
# source posting
# target posting
args["source_posting"] = PartialPosting(
account=fields.get("source_account", None),
amount=record.from_source,
total_cost=abs(record.to_target)
if abs(record.from_source) != abs(record.to_target)
else None,
)
args["target_posting"] = PartialPosting(
account=fields.get("target_account", None),
amount=record.to_target,
)
# comment
args.setdefault(
"comment",
f"{record.txntype} | {record.name} | {record.txnid} | Transferred {record.transferred_in}",
)
currency_postings = []
if record.ex_target_amount is not None:
for source_amount, exrate in record.ex_source_amounts:
source_posting = TXNPosting(
account=self.config.asset_account,
amount=source_amount,
price=Amount(exrate, record.ex_target_amount.currency),
)
currency_postings.append(source_posting)
target_posting = TXNPosting(
account=self.config.asset_account, amount=record.ex_target_amount
)
currency_postings.append(target_posting)
txn = PaypalPartialTXN(extra_postings=currency_postings, **args)
logger.debug(f"Converted Paypal CSV Record {record!r} to PaypalTXN {txn!r}")
return txn
def _make_record(self, *rows: PaypalCSVRow) -> PaypalRecord | None:
# Find main, which is an in or out
main_txns = []
convert_rows = []
transfer_rows = []
for row in rows:
dir = get_direction(row.txntype)
if dir is PaypalRecordDirection.IGNORE:
continue
elif dir in (PaypalRecordDirection.IN, PaypalRecordDirection.OUT):
main_txns.append(row)
elif dir is PaypalRecordDirection.TRANSFERIN:
transfer_rows.append(row)
elif dir is PaypalRecordDirection.CONVERT:
convert_rows.append(row)
# Some sanity checks
if not main_txns:
# No main transaction
if not convert_rows:
return None
raise ValueError(
f"Paypal Row block with convert by no main transactions: {rows}"
)
if len(main_txns) > 1:
raise ValueError(f"Paypal Row block with multiple main txns: {rows}")
if len(convert_rows) == 1:
raise ValueError(f"Paypal Row block with orphan convert: {rows}")
if convert_rows and not any(not row.ex_rate for row in rows):
raise ValueError(f"Paypal Row block without main convert: {rows}")
if any(float(row.fees) for row in rows):
raise ValueError(f"Paypal Row block with fees: {rows}")
# We guarantee that we are somewhat sane
mainrow = main_txns[0]
direction = get_direction(mainrow.txntype)
dt_format = "%d/%m/%Y %H:%M:%S"
created_on = datetime.strptime(f"{mainrow.date} {mainrow.time}", dt_format)
balance_effect = Amount(float(mainrow.amount), mainrow.currency)
# transfer rows
if transfer_rows:
assert len({row.currency for row in transfer_rows}) == 1
transferred_in = Amount(
sum(float(row.amount) for row in transfer_rows),
transfer_rows[0].currency,
)
else:
transferred_in = None
# Currency conversion
source_amounts = []
target_amount = None
for row in convert_rows:
if row.ex_rate:
# If the row has an exchange rate, it's a source row
# Note the amount will be negative
source_details = (Amount(row.amount, row.currency), float(row.ex_rate))
source_amounts.append(source_details)
else:
# Otherwise, main row
target_amount = Amount(row.amount, row.currency)
if direction is PaypalRecordDirection.IN:
record = PaypalRecord(
date=created_on.date(),
source_account=mainrow.name,
target_account="ACCOUNT",
from_source=-balance_effect,
to_target=balance_effect,
created_on=created_on,
txnid=mainrow.txnid,
name=mainrow.name,
txntype=mainrow.txntype,
direction=direction,
balance_effect=balance_effect,
transferred_in=transferred_in,
ex_target_amount=target_amount,
ex_source_amounts=source_amounts,
)
else:
record = PaypalRecord(
date=created_on.date(),
target_account=mainrow.name,
source_account="ACCOUNT",
from_source=balance_effect,
to_target=-balance_effect,
created_on=created_on,
txnid=mainrow.txnid,
name=mainrow.name,
txntype=mainrow.txntype,
direction=direction,
balance_effect=balance_effect,
transferred_in=transferred_in,
ex_target_amount=target_amount,
ex_source_amounts=source_amounts,
)
return record
def ingest_string(self, data: str) -> list[PaypalRecord]:
"""
Parse a string of Paypal CSV input into a list of records
"""
reader = csv.reader(data.splitlines())
records = []
thisblock = []
thisblock_key = None
for i, row in enumerate(reader):
if not i:
# Skip header row
continue
row = PaypalCSVRow(*row)
key = (row.date, row.time)
if key != thisblock_key:
if thisblock:
# Flush
record = self._make_record(*thisblock)
if record:
records.append(record)
thisblock.clear()
thisblock_key = key
thisblock.append(row)
if thisblock:
# Flush
record = self._make_record(*thisblock)
if record:
records.append(record)
thisblock.clear()
return records
def ingest_file(self, path) -> list[PaypalRecord]:
with open(path) as f:
f.readline()
return self.ingest_string(f.read())
+2 -1
View File
@@ -65,9 +65,10 @@ class SampleConfig(ConverterConfig):
@available_converter
class SampleConverter(Converter[SampleRecord, SampleConfig]):
class SampleConverter(Converter[SampleRecord, PartialTXN, SampleConfig]):
record_type = SampleRecord
config_type = SampleConfig
txn_type = PartialTXN
converter_name = "sample"
version = "0"
display_name = "Sample converter v0"
+105 -5
View File
@@ -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
@@ -27,6 +29,7 @@ logger = logging.getLogger(__name__)
class WiseRecordStatus(Enum):
COMPLETED = "COMPLETED"
CANCELLED = "CANCELLED"
REFUNDED = "REFUNDED"
class WiseRecordDirection(Enum):
@@ -212,10 +215,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, WiseConfig]):
class WiseConverter(Converter[WiseRecord, WisePartialTXN, WiseConfig]):
record_type = WiseRecord
config_type = WiseConfig
txn_type = WisePartialTXN
converter_name = "wise"
version = "0"
display_name = "Wise Record Converter v0"
@@ -224,9 +324,9 @@ class WiseConverter(Converter[WiseRecord, 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:
@@ -301,7 +401,7 @@ class WiseConverter(Converter[WiseRecord, 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
+60 -8
View File
@@ -1,3 +1,4 @@
import pickle
import tkinter as tk
from tkinter import StringVar, filedialog, messagebox, ttk
@@ -73,6 +74,13 @@ 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_separator()
menu_file.add_command(label="Exit", command=lambda: self.destroy())
self.menubar.add_cascade(menu=menu_file, label="File")
@@ -93,12 +101,10 @@ class MainWindow(ThemedTk):
# Alt+up/down for next entry in that direction
# Alt+shift+up/down for next incomplete entry in that direction
self.bind_all("<Alt-KeyPress-1>", lambda event: self.rowtree.grab_focus())
self.bind_all("<Alt-Up>", lambda event: self.rowtree.focus_prev())
self.bind_all("<Alt-Down>", lambda event: self.rowtree.focus_next())
self.bind_all("<Alt-Shift-Up>", lambda event: self.rowtree.focus_prev_partial())
self.bind_all(
"<Alt-Shift-Down>", lambda event: self.rowtree.focus_next_partial()
)
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-KeyPress-2>", lambda event: self.editor.focus_record_frame()
@@ -134,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)
@@ -331,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
@@ -359,3 +365,49 @@ class MainWindow(ThemedTk):
"""
self.ruleset.reload_rules()
self.regenerate_rows()
def do_save_partial(self):
"""
Save all partial entries to be loaded later.
"""
flattened = {
"converter": self.converter.qual_name(),
"rows": self.rows,
"custom": self.custom,
}
result = pickle.dumps(flattened)
path = filedialog.asksaveasfilename(
defaultextension=".pickle",
filetypes=[
("All Files", "*.*"),
],
)
if path:
with open(path, "wb") as f:
f.write(result)
message = f"Saved progress to {path}"
else:
message = "Save cancelled"
self.update_status(message)
def do_load_partial(self):
# Prompt for file to ingest
path = filedialog.askopenfilename(
defaultextension=".pickle",
filetypes=[("Pickle Files", ".pickle"), ("All Files", "*.*")],
)
with open(path, "rb") as f:
result = pickle.load(f)
if result["converter"] != self.converter.qual_name():
messagebox.showerror(
"Load Error",
f"Cannot load partial data for converter '{result['converter']}'",
)
return
self.rows |= result["rows"]
self.custom |= result["custom"]
self.rowtree.update_rows(self.rows)
self.show_ingest_summary([path], self.rows)
self.rebuild_account_cache()
+6 -2
View File
@@ -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}
+3 -3
View File
@@ -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