diff --git a/pytr/dl.py b/pytr/dl.py index c85e239..6f85714 100644 --- a/pytr/dl.py +++ b/pytr/dl.py @@ -20,6 +20,7 @@ def __init__( history_file='pytr_history', max_workers=8, universal_filepath=False, + sort_export=False ): ''' tr: api object @@ -33,6 +34,7 @@ def __init__( self.filename_fmt = filename_fmt self.since_timestamp = since_timestamp self.universal_filepath = universal_filepath + self.sort_export = sort_export self.session = FuturesSession(max_workers=max_workers, session=self.tr._websession) self.futures = [] diff --git a/pytr/event.py b/pytr/event.py index 882143d..a7b1ffc 100644 --- a/pytr/event.py +++ b/pytr/event.py @@ -1,100 +1,253 @@ +from dataclasses import dataclass from datetime import datetime +from enum import auto, Enum import re - -tr_eventType_to_pp_type = { - 'INCOMING_TRANSFER': 'DEPOSIT', - 'PAYMENT_INBOUND': 'DEPOSIT', - 'PAYMENT_INBOUND_GOOGLE_PAY': 'DEPOSIT', - 'PAYMENT_INBOUND_SEPA_DIRECT_DEBIT': 'DEPOSIT', - 'card_refund': 'DEPOSIT', - - 'CREDIT': 'DIVIDENDS', - 'ssp_corporate_action_invoice_cash': 'DIVIDENDS', - - 'INTEREST_PAYOUT_CREATED': 'INTEREST', - - 'OUTGOING_TRANSFER_DELEGATION': 'REMOVAL', - 'PAYMENT_OUTBOUND': 'REMOVAL', - 'card_failed_transaction': 'REMOVAL', - 'card_order_billed': 'REMOVAL', - 'card_successful_atm_withdrawal': 'REMOVAL', - 'card_successful_transaction': 'REMOVAL', - - 'ORDER_EXECUTED': 'TRADE_INVOICE', - 'SAVINGS_PLAN_EXECUTED': 'TRADE_INVOICE', - 'SAVINGS_PLAN_INVOICE_CREATED': 'TRADE_INVOICE', - 'TRADE_INVOICE': 'TRADE_INVOICE' +from typing import Any, Dict, Optional, Tuple, Union + + +class ConditionalEventType(Enum): + """Events that conditionally map to None or one/multiple PPEventType events""" + + FAILED_CARD_TRANSACTION = auto() + SAVEBACK = auto() + TRADE_INVOICE = auto() + + +class PPEventType(Enum): + """PP Event Types""" + + BUY = "BUY" + DEPOSIT = "DEPOSIT" + DIVIDEND = "DIVIDEND" + FEES = "FEES" # Currently not mapped to + FEES_REFUND = "FEES_REFUND" # Currently not mapped to + INTEREST = "INTEREST" + INTEREST_CHARGE = "INTEREST_CHARGE" # Currently not mapped to + REMOVAL = "REMOVAL" + SELL = "SELL" + TAXES = "TAXES" # Currently not mapped to + TAX_REFUND = "TAX_REFUND" + TRANSFER_IN = "TRANSFER_IN" # Currently not mapped to + TRANSFER_OUT = "TRANSFER_OUT" # Currently not mapped to + + +class EventType(Enum): + PP_EVENT_TYPE = PPEventType + CONDITIONAL_EVENT_TYPE = ConditionalEventType + + +tr_event_type_mapping = { + # Deposits + "INCOMING_TRANSFER": PPEventType.DEPOSIT, + "INCOMING_TRANSFER_DELEGATION": PPEventType.DEPOSIT, + "PAYMENT_INBOUND": PPEventType.DEPOSIT, + "PAYMENT_INBOUND_GOOGLE_PAY": PPEventType.DEPOSIT, + "PAYMENT_INBOUND_SEPA_DIRECT_DEBIT": PPEventType.DEPOSIT, + "card_refund": PPEventType.DEPOSIT, + "card_successful_oct": PPEventType.DEPOSIT, + # Dividends + "CREDIT": PPEventType.DIVIDEND, + "ssp_corporate_action_invoice_cash": PPEventType.DIVIDEND, + # Failed card transactions + "card_failed_transaction": ConditionalEventType.FAILED_CARD_TRANSACTION, + # Interests + "INTEREST_PAYOUT": PPEventType.INTEREST, + "INTEREST_PAYOUT_CREATED": PPEventType.INTEREST, + # Removals + "OUTGOING_TRANSFER_DELEGATION": PPEventType.REMOVAL, + "PAYMENT_OUTBOUND": PPEventType.REMOVAL, + "card_order_billed": PPEventType.REMOVAL, + "card_successful_atm_withdrawal": PPEventType.REMOVAL, + "card_successful_transaction": PPEventType.REMOVAL, + # Saveback + "benefits_saveback_execution": ConditionalEventType.SAVEBACK, + # Tax refunds + "TAX_REFUND": PPEventType.TAX_REFUND, + # Trade invoices + "ORDER_EXECUTED": ConditionalEventType.TRADE_INVOICE, + "SAVINGS_PLAN_EXECUTED": ConditionalEventType.TRADE_INVOICE, + "SAVINGS_PLAN_INVOICE_CREATED": ConditionalEventType.TRADE_INVOICE, + "TRADE_INVOICE": ConditionalEventType.TRADE_INVOICE, } + +@dataclass class Event: - def __init__(self, event_json): - self.event = event_json - self.shares = "" - self.isin = "" - - self.pp_type = tr_eventType_to_pp_type.get(self.event["eventType"], "") - self.body = self.event.get("body", "") - self.process_event() - - @property - def date(self): - dateTime = datetime.fromisoformat(self.event["timestamp"][:19]) - return dateTime.strftime("%Y-%m-%d") - - @property - def is_pp_relevant(self): - if self.event["eventType"] == "card_failed_transaction": - if self.event["status"] == "CANCELED": - return False - return self.pp_type != "" - - @property - def amount(self): - return str(self.event["amount"]["value"]) - - @property - def note(self): - if self.event["eventType"].find("card_") == 0: - return self.event["eventType"] - else: - return "" - - @property - def title(self): - return self.event["title"] - - def determine_pp_type(self): - if self.pp_type == "TRADE_INVOICE": - if self.event["amount"]["value"] < 0: - self.pp_type = "BUY" - else: - self.pp_type = "SELL" - - def determine_shares(self): - if self.pp_type == "TRADE_INVOICE": - sections = self.event.get("details", {}).get("sections", [{}]) - for section in sections: - if section.get("title") == "Transaktion": - amount = section.get("data", [{}])[0]["detail"]["text"] - amount = re.sub("[^\,\d-]", "", amount) - self.shares = amount.replace(",", ".") - - def determine_isin(self): - if self.pp_type in ("DIVIDENDS", "TRADE_INVOICE"): - sections = self.event.get("details", {}).get("sections", [{}]) - self.isin = self.event.get("icon", "") - self.isin = self.isin[self.isin.find("/") + 1 :] - self.isin = self.isin[: self.isin.find("/")] - isin2 = self.isin - for section in sections: - action = section.get("action", None) - if action and action.get("type", {}) == "instrumentDetail": - isin2 = section.get("action", {}).get("payload") - break - if self.isin != isin2: - self.isin = isin2 - - def process_event(self): - self.determine_shares() - self.determine_isin() - self.determine_pp_type() + date: datetime + title: str + event_type: Optional[EventType] + fees: Optional[float] + isin: Optional[str] + note: Optional[str] + shares: Optional[float] + taxes: Optional[float] + value: Optional[float] + + @classmethod + def from_dict(cls, event_dict: Dict[Any, Any]): + """Deserializes the event dictionary into an Event object + + Args: + event_dict (json): _description_ + + Returns: + Event: Event object + """ + date: datetime = datetime.fromisoformat(event_dict["timestamp"][:19]) + event_type: Optional[EventType] = cls._parse_type(event_dict) + title: str = event_dict["title"] + value: Optional[float] = ( + v + if (v := event_dict.get("amount", {}).get("value", None)) is not None + and v != 0.0 + else None + ) + fees, isin, note, shares, taxes = cls._parse_type_dependent_params( + event_type, event_dict + ) + return cls(date, title, event_type, fees, isin, note, shares, taxes, value) + + @staticmethod + def _parse_type(event_dict: Dict[Any, Any]) -> Optional[EventType]: + event_type: Optional[EventType] = tr_event_type_mapping.get( + event_dict.get("eventType", ""), None + ) + if event_type == ConditionalEventType.FAILED_CARD_TRANSACTION: + event_type = ( + PPEventType.REMOVAL + if event_dict.get("status", "").lower() == "executed" + else None + ) + return event_type + + @classmethod + def _parse_type_dependent_params( + cls, event_type: EventType, event_dict: Dict[Any, Any] + ) -> Tuple[Optional[Union[str, float]]]: + """Parses the fees, isin, note, shares and taxes fields + + Args: + event_type (EventType): _description_ + event_dict (Dict[Any, Any]): _description_ + + Returns: + Tuple[Optional[Union[str, float]]]]: fees, isin, note, shares, taxes + """ + isin, shares, taxes, note, fees = (None,) * 5 + + if event_type is PPEventType.DIVIDEND: + isin = cls._parse_isin(event_dict) + taxes = cls._parse_taxes(event_dict) + + elif isinstance(event_type, ConditionalEventType): + isin = cls._parse_isin(event_dict) + shares, fees = cls._parse_shares_and_fees(event_dict) + taxes = cls._parse_taxes(event_dict) + + elif event_type is PPEventType.INTEREST: + taxes = cls._parse_taxes(event_dict) + + elif event_type in [PPEventType.DEPOSIT, PPEventType.REMOVAL]: + note = cls._parse_card_note(event_dict) + + return fees, isin, note, shares, taxes + + @staticmethod + def _parse_isin(event_dict: Dict[Any, Any]) -> str: + """Parses the isin + + Args: + event_dict (Dict[Any, Any]): _description_ + + Returns: + str: isin + """ + sections = event_dict.get("details", {}).get("sections", [{}]) + isin = event_dict.get("icon", "") + isin = isin[isin.find("/") + 1 :] + isin = isin[: isin.find("/")] + isin2 = isin + for section in sections: + action = section.get("action", None) + if action and action.get("type", {}) == "instrumentDetail": + isin2 = section.get("action", {}).get("payload") + break + if isin != isin2: + isin = isin2 + return isin + + @staticmethod + def _parse_shares_and_fees(event_dict: Dict[Any, Any]) -> Tuple[Optional[float]]: + """Parses the amount of shares and the applicable fees + + Args: + event_dict (Dict[Any, Any]): _description_ + + Returns: + Tuple[Optional[float]]: [shares, fees] + """ + return_vals = {} + sections = event_dict.get("details", {}).get("sections", [{}]) + for section in sections: + if section.get("title") == "Transaktion": + data = section["data"] + shares_dicts = list( + filter(lambda x: x["title"] in ["Aktien", "Anteile"], data) + ) + fees_dicts = list(filter(lambda x: x["title"] == "Gebühr", data)) + titles = ["shares"] * len(shares_dicts) + ["fees"] * len(fees_dicts) + for key, elem_dict in zip(titles, shares_dicts + fees_dicts): + elem_unparsed = elem_dict.get("detail", {}).get("text", "") + elem_parsed = re.sub("[^\,\.\d-]", "", elem_unparsed).replace( + ",", "." + ) + return_vals[key] = ( + None + if elem_parsed == "" or float(elem_parsed) == 0.0 + else float(elem_parsed) + ) + return return_vals["shares"], return_vals["fees"] + + @staticmethod + def _parse_taxes(event_dict: Dict[Any, Any]) -> Tuple[Optional[float]]: + """Parses the levied taxes + + Args: + event_dict (Dict[Any, Any]): _description_ + + Returns: + Tuple[Optional[float]]: [taxes] + """ + # taxes keywords + taxes_keys = {"Steuer", "Steuern"} + # Gather all section dicts + sections = event_dict.get("details", {}).get("sections", [{}]) + # Gather all dicts pertaining to transactions + transaction_dicts = filter( + lambda x: x["title"] in {"Transaktion", "Geschäft"}, sections + ) + for transaction_dict in transaction_dicts: + # Filter for taxes dicts + data = transaction_dict.get("data", [{}]) + taxes_dicts = filter(lambda x: x["title"] in taxes_keys, data) + # Iterate over dicts containing tax information and parse each one + for taxes_dict in taxes_dicts: + unparsed_taxes_val = taxes_dict.get("detail", {}).get("text", "") + parsed_taxes_val = re.sub("[^\,\.\d-]", "", unparsed_taxes_val).replace( + ",", "." + ) + if parsed_taxes_val != "" and float(parsed_taxes_val) != 0.0: + return float(parsed_taxes_val) + + @staticmethod + def _parse_card_note(event_dict: Dict[Any, Any]) -> Optional[str]: + """Parses the note associated with card transactions + + Args: + event_dict (Dict[Any, Any]): _description_ + + Returns: + Optional[str]: note + """ + if event_dict.get("eventType", "").startswith("card_"): + return event_dict["eventType"] diff --git a/pytr/event_formatter.py b/pytr/event_formatter.py new file mode 100644 index 0000000..f74a307 --- /dev/null +++ b/pytr/event_formatter.py @@ -0,0 +1,96 @@ +from babel.numbers import format_decimal +from copy import deepcopy + +from .event import Event, PPEventType, ConditionalEventType +from .translation import setup_translation + + +class EventCsvFormatter: + def __init__(self, lang): + self.lang = lang + self.translate = setup_translation(language=self.lang) + self.csv_fmt = "{date};{type};{value};{note};{isin};{shares};{fees};{taxes}\n" + + def format_header(self) -> str: + """Outputs header line + + Returns: + str: header line + """ + return self.csv_fmt.format( + date=self.translate("CSVColumn_Date"), + type=self.translate("CSVColumn_Type"), + value=self.translate("CSVColumn_Value"), + note=self.translate("CSVColumn_Note"), + isin=self.translate("CSVColumn_ISIN"), + shares=self.translate("CSVColumn_Shares"), + fees=self.translate("CSVColumn_Fees"), + taxes=self.translate("CSVColumn_Taxes"), + ) + + def format(self, event: Event) -> str: + """Outputs one or multiple csv lines per event + + Args: + event (Event): _description_ + + Yields: + str: csv line(s) + """ + # Empty csv line for non-transaction events + if event.event_type is None: + return "" + + # Initialize the csv line arguments + kwargs = dict( + zip( + ("date", "type", "value", "note", "isin", "shares", "fees", "taxes"), + ["" for _ in range(8)], + ) + ) + + # Handle TRADE_INVOICE + if event.event_type == ConditionalEventType.TRADE_INVOICE: + event.event_type = PPEventType.BUY if event.value < 0 else PPEventType.SELL + + # Apply special formatting to the attributes + kwargs["date"] = event.date.strftime("%Y-%m-%d") + if isinstance(event.event_type, PPEventType): + kwargs["type"] = self.translate(event.event_type.value) + kwargs["value"] = format_decimal( + event.value, locale=self.lang, decimal_quantization=True + ) + kwargs["note"] = ( + self.translate(event.note) + " - " + event.title + if event.note is not None + else event.title + ) + if event.isin is not None: + kwargs["isin"] = event.isin + if event.shares is not None: + kwargs["shares"] = format_decimal( + event.shares, locale=self.lang, decimal_quantization=False + ) + if event.fees is not None: + kwargs["fees"] = format_decimal( + -event.fees, locale=self.lang, decimal_quantization=True + ) + if event.taxes is not None: + kwargs["taxes"] = format_decimal( + -event.taxes, locale=self.lang, decimal_quantization=True + ) + lines = self.csv_fmt.format(**kwargs) + + # Generate BUY and DEPOSIT events from SAVEBACK event + if event.event_type == ConditionalEventType.SAVEBACK: + kwargs["type"] = self.translate(PPEventType.BUY.value) + lines = self.csv_fmt.format(**kwargs) + kwargs["type"] = self.translate(PPEventType.DEPOSIT.value) + kwargs["value"] = format_decimal( + -event.value, locale=self.lang, decimal_quantization=True + ) + kwargs["isin"] = "" + kwargs["shares"] = "" + lines += self.csv_fmt.format(**kwargs) + + return lines diff --git a/pytr/locale/cs/LC_MESSAGES/messages.po b/pytr/locale/cs/LC_MESSAGES/messages.po index fb28691..b3e6aee 100644 --- a/pytr/locale/cs/LC_MESSAGES/messages.po +++ b/pytr/locale/cs/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Koupit" msgid "DEPOSIT" msgstr "Vklad" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dividendy" msgid "FEES" diff --git a/pytr/locale/da/LC_MESSAGES/messages.po b/pytr/locale/da/LC_MESSAGES/messages.po index 853613b..3ab61d5 100644 --- a/pytr/locale/da/LC_MESSAGES/messages.po +++ b/pytr/locale/da/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Køb" msgid "DEPOSIT" msgstr "Indsæt" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Udbytte" msgid "FEES" diff --git a/pytr/locale/de/LC_MESSAGES/messages.po b/pytr/locale/de/LC_MESSAGES/messages.po index ecd0bbe..530c0f8 100644 --- a/pytr/locale/de/LC_MESSAGES/messages.po +++ b/pytr/locale/de/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Kauf" msgid "DEPOSIT" msgstr "Einlage" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dividende" msgid "FEES" diff --git a/pytr/locale/en/LC_MESSAGES/messages.po b/pytr/locale/en/LC_MESSAGES/messages.po index 9da1304..7ef20e2 100644 --- a/pytr/locale/en/LC_MESSAGES/messages.po +++ b/pytr/locale/en/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Buy" msgid "DEPOSIT" msgstr "Deposit" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dividend" msgid "FEES" diff --git a/pytr/locale/es/LC_MESSAGES/messages.po b/pytr/locale/es/LC_MESSAGES/messages.po index 2b95e21..80a0a4b 100644 --- a/pytr/locale/es/LC_MESSAGES/messages.po +++ b/pytr/locale/es/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Compra" msgid "DEPOSIT" msgstr "Depósito" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dividendo" msgid "FEES" diff --git a/pytr/locale/fr/LC_MESSAGES/messages.po b/pytr/locale/fr/LC_MESSAGES/messages.po index 7ac8b94..f80e19b 100644 --- a/pytr/locale/fr/LC_MESSAGES/messages.po +++ b/pytr/locale/fr/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Achat" msgid "DEPOSIT" msgstr "Dépôt" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dividendes" msgid "FEES" diff --git a/pytr/locale/it/LC_MESSAGES/messages.po b/pytr/locale/it/LC_MESSAGES/messages.po index 9208ba9..1ed24b1 100644 --- a/pytr/locale/it/LC_MESSAGES/messages.po +++ b/pytr/locale/it/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Compra" msgid "DEPOSIT" msgstr "Deposito" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dividendo" msgid "FEES" diff --git a/pytr/locale/nl/LC_MESSAGES/messages.po b/pytr/locale/nl/LC_MESSAGES/messages.po index e93f38c..3951bb3 100644 --- a/pytr/locale/nl/LC_MESSAGES/messages.po +++ b/pytr/locale/nl/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Aankoop" msgid "DEPOSIT" msgstr "Storting" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dividend" msgid "FEES" diff --git a/pytr/locale/pl/LC_MESSAGES/messages.po b/pytr/locale/pl/LC_MESSAGES/messages.po index 7bc784c..0772a0c 100644 --- a/pytr/locale/pl/LC_MESSAGES/messages.po +++ b/pytr/locale/pl/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Kupno" msgid "DEPOSIT" msgstr "Depozyt" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dywidenda" msgid "FEES" diff --git a/pytr/locale/pt/LC_MESSAGES/messages.po b/pytr/locale/pt/LC_MESSAGES/messages.po index ba6df93..6af8542 100644 --- a/pytr/locale/pt/LC_MESSAGES/messages.po +++ b/pytr/locale/pt/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Comprar" msgid "DEPOSIT" msgstr "Depósito" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dividendo" msgid "FEES" diff --git a/pytr/locale/ru/LC_MESSAGES/messages.po b/pytr/locale/ru/LC_MESSAGES/messages.po index e828d51..8e39ec9 100644 --- a/pytr/locale/ru/LC_MESSAGES/messages.po +++ b/pytr/locale/ru/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Покупка" msgid "DEPOSIT" msgstr "Пополнение" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Дивиденд" msgid "FEES" diff --git a/pytr/locale/sk/LC_MESSAGES/messages.po b/pytr/locale/sk/LC_MESSAGES/messages.po index 5985c98..5b2af0d 100644 --- a/pytr/locale/sk/LC_MESSAGES/messages.po +++ b/pytr/locale/sk/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "Kúpiť" msgid "DEPOSIT" msgstr "Vklad" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "Dividenda" msgid "FEES" diff --git a/pytr/locale/zh/LC_MESSAGES/messages.po b/pytr/locale/zh/LC_MESSAGES/messages.po index aa68f3f..5cc4b03 100644 --- a/pytr/locale/zh/LC_MESSAGES/messages.po +++ b/pytr/locale/zh/LC_MESSAGES/messages.po @@ -10,7 +10,7 @@ msgstr "买入" msgid "DEPOSIT" msgstr "存款" -msgid "DIVIDENDS" +msgid "DIVIDEND" msgstr "股息" msgid "FEES" diff --git a/pytr/main.py b/pytr/main.py index b642f22..1389926 100644 --- a/pytr/main.py +++ b/pytr/main.py @@ -52,6 +52,12 @@ def formatter(prog): parser_login_args.add_argument('-n', '--phone_no', help='TradeRepublic phone number (international format)') parser_login_args.add_argument('-p', '--pin', help='TradeRepublic pin') + # sort + parser_sort_export = argparse.ArgumentParser(add_help=False) + parser_sort_export.add_argument( + '-s', '--sort', help='Chronologically sort exported csv transactions', action="store_true" + ) + # login info = ( 'Check if credentials file exists. If not create it and ask for input. Try to login.' @@ -74,7 +80,7 @@ def formatter(prog): parser_dl_docs = parser_cmd.add_parser( 'dl_docs', formatter_class=argparse.ArgumentDefaultsHelpFormatter, - parents=[parser_login_args], + parents=[parser_login_args, parser_sort_export], help=info, description=info, ) @@ -141,6 +147,7 @@ def formatter(prog): parser_export_transactions = parser_cmd.add_parser( 'export_transactions', formatter_class=argparse.ArgumentDefaultsHelpFormatter, + parents=[parser_sort_export], help=info, description=info, ) @@ -207,6 +214,7 @@ def main(): since_timestamp=since_timestamp, max_workers=args.workers, universal_filepath=args.universal, + sort_export=args.sort ) asyncio.get_event_loop().run_until_complete(dl.dl_loop()) elif args.command == 'set_price_alarms': @@ -222,7 +230,7 @@ def main(): if args.output is not None: p.portfolio_to_csv(args.output) elif args.command == 'export_transactions': - export_transactions(args.input, args.output, args.lang) + export_transactions(args.input, args.output, args.lang, args.sort) elif args.version: installed_version = version('pytr') print(installed_version) diff --git a/pytr/timeline.py b/pytr/timeline.py index 2b5865a..d33eac0 100644 --- a/pytr/timeline.py +++ b/pytr/timeline.py @@ -172,6 +172,6 @@ async def process_timelineDetail(self, response, dl): with open(dl.output_path / 'all_events.json', 'w', encoding='utf-8') as f: json.dump(self.events_without_docs + self.events_with_docs, f, ensure_ascii=False, indent=2) - export_transactions(dl.output_path / 'all_events.json', dl.output_path / 'account_transactions.csv') + export_transactions(dl.output_path / 'all_events.json', dl.output_path / 'account_transactions.csv', sort = dl.sort_export) dl.work_responses() diff --git a/pytr/transactions.py b/pytr/transactions.py index eaa99ed..54a9f7c 100644 --- a/pytr/transactions.py +++ b/pytr/transactions.py @@ -1,13 +1,12 @@ -from locale import getdefaultlocale -from babel.numbers import format_decimal import json +from locale import getdefaultlocale from .event import Event +from .event_formatter import EventCsvFormatter from .utils import get_logger -from .translation import setup_translation -def export_transactions(input_path, output_path, lang="auto"): +def export_transactions(input_path, output_path, lang="auto", sort=False): """ Create a CSV with the deposits and removals ready for importing into Portfolio Performance The CSV headers for PP are language dependend @@ -36,43 +35,23 @@ def export_transactions(input_path, output_path, lang="auto"): ]: log.info(f"Language not yet supported {lang}") lang = "en" - _ = setup_translation(language=lang) # Read relevant deposit timeline entries with open(input_path, encoding="utf-8") as f: timeline = json.load(f) log.info("Write deposit entries") - with open(output_path, "w", encoding="utf-8") as f: - csv_fmt = "{date};{type};{value};{note};{isin};{shares}\n" - header = csv_fmt.format( - date=_("CSVColumn_Date"), - type=_("CSVColumn_Type"), - value=_("CSVColumn_Value"), - note=_("CSVColumn_Note"), - isin=_("CSVColumn_ISIN"), - shares=_("CSVColumn_Shares"), - ) - f.write(header) - for event_json in timeline: - event = Event(event_json) - if not event.is_pp_relevant: - continue + formatter = EventCsvFormatter(lang=lang) - amount = format_decimal(event.amount, locale=lang, decimal_quantization=False) if event.amount else "" - note = (_(event.note) + " - " + event.title) if event.note else event.title - shares = format_decimal(event.shares, locale=lang, decimal_quantization=False) if event.shares else "" + events = map(lambda x: Event.from_dict(x), timeline) + if sort: + events = sorted(events, key=lambda x: x.date) + lines = map(lambda x: formatter.format(x), events) + lines = formatter.format_header() + "".join(lines) - f.write( - csv_fmt.format( - date=event.date, - type=_(event.pp_type), - value=amount, - note=note, - isin=event.isin, - shares=shares, - ) - ) + # Write transactions into csv file + with open(output_path, "w", encoding="utf-8") as f: + f.write(lines) log.info("Deposit creation finished!") diff --git a/pytr/translation.py b/pytr/translation.py index 1e44a13..c8d7b6e 100644 --- a/pytr/translation.py +++ b/pytr/translation.py @@ -23,5 +23,4 @@ def setup_translation(language="en"): "messages", localedir=locale_dir, languages=[language], fallback=True ) lang.install() - return lambda x: lang.gettext(x) if len(x) > 0 else ""