def _(entry: Balance) -> Any: """Serialise an entry.""" ret = entry._asdict() ret["type"] = "Balance" amt = ret["amount"] ret["amount"] = {"number": str(amt.number), "currency": amt.currency} return ret
def balance(self, filename, lineno, date, account, amount, tolerance, kvlist): """Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to balance. amount: The expected amount, to be checked. tolerance: The tolerance number. kvlist: a list of KeyValue instances. Returns: A new Balance object. """ diff_amount = None meta = new_metadata(filename, lineno, kvlist) # Only support explicit tolerance syntax if the experiment is enabled. if (tolerance is not None and not self.options["experiment_explicit_tolerances"]): self.errors.append( ParserError(meta, "Tolerance syntax is not supported", None)) tolerance = '__tolerance_syntax_not_supported__' return Balance(meta, date, account, amount, tolerance, diff_amount)
def test_serialise_balance() -> None: bal = Balance( {}, datetime.date(2019, 9, 17), "Assets:ETrade:Cash", A("0.1234567891011121314151617 CHF"), None, None, ) json = { "date": "2019-09-17", "amount": { "currency": "CHF", "number": "0.1234567891011121314151617" }, "diff_amount": None, "meta": {}, "tolerance": None, "account": "Assets:ETrade:Cash", "type": "Balance", } serialised = loads(dumps(serialise(bal))) assert serialised == json
def get_out_balance(self): outBalance = Balance(self.meta, self.date_out, 'Assets:VB:Johannes:Giro', Amount(self.balance_out, 'EUR'), None, None) return outBalance
def get_directive(self) -> Balance: return Balance( meta=None, date=self.date, account=self.account, amount=self.amount, tolerance=None, diff_amount=None, )
def get_in_balance(self): inBalance = Balance(self.meta, self.date_in, 'Assets:VB:Johannes:Giro', Amount(self.balance_in, 'EUR'), None, None) return inBalance
def get_balance_at_step(self, index): meta = dict(self.meta, **{data.LINENO_KEY: str(index)}) date = self.raw_data['Datum'][index] date = datetime.date(*[int(d) for d in date.split('.')[::-1]]) date += datetime.timedelta(1) # add one day balance = D(german2usNumber(self.raw_data['Guthaben'][index])) balance_dir = Balance(meta, date, 'Assets:PayPal', Amount(balance, 'EUR'), None, None) return balance_dir
def test_deserialise_balance(): json_bal = { 'type': 'Balance', 'date': '2017-12-12', 'account': 'Assets:ETrade:Cash', 'number': '100', 'currency': 'USD', 'meta': {}, } bal = Balance({}, datetime.date(2017, 12, 12), 'Assets:ETrade:Cash', A('100 USD'), None, None) assert deserialise(json_bal) == bal
def _transform_balance(self, truelayer_balance, account, time_txns, pending_time_txns): """Transforms TrueLayer Balance to beancount Balance. Balance from TrueLayer can be effective at the middle of a day with pending transactions ignored, while beancount balance assertions must be applied at the beginning of a day and pending transactions are taken into account. It is not always possible to get pending transactions. If that is not available balance assertions may have to be corrected retrospectively. """ balance_time = dateutil.parser.parse( truelayer_balance['update_timestamp']).astimezone() assertion_time = datetime.datetime.combine(balance_time, datetime.time.min, balance_time.tzinfo) txns_to_remove = [ txn for t, txn in time_txns if assertion_time <= t < balance_time ] inventory_to_remove = inventory.Inventory() for txn in txns_to_remove: for posting in txn.postings: inventory_to_remove.add_position(posting) amount_to_remove = inventory_to_remove.get_currency_units( truelayer_balance['currency']) txns_to_add = [ txn for t, txn in pending_time_txns if t < assertion_time ] inventory_to_add = inventory.Inventory() for txn in txns_to_add: for posting in txn.postings: inventory_to_add.add_position(posting) amount_to_add = inventory_to_add.get_currency_units( truelayer_balance['currency']) number = currency_to_decimal(truelayer_balance['current']) if account['liability']: number = -number number += amount_to_add.number number -= amount_to_remove.number return Balance( meta=new_metadata('', 0), date=assertion_time.date(), account=account['beancount_account'], amount=Amount(number, truelayer_balance['currency']), tolerance=None, diff_amount=None, )
def test_deserialise_balance() -> None: json_bal = { "type": "Balance", "date": "2017-12-12", "account": "Assets:ETrade:Cash", "amount": {"number": "100", "currency": "USD"}, "meta": {}, } bal = Balance( {}, datetime.date(2017, 12, 12), "Assets:ETrade:Cash", A("100 USD"), None, None, ) assert deserialise(json_bal) == bal
def close_zero(entries, options_map): default_currencies = options_map['operating_currency'] errors = [] currencies = {} new_entries = [] for entry in entries: if isinstance(entry, Open): currencies[entry.account] = entry.currencies elif isinstance(entry, Close): for currency in currencies.get(entry.account, default_currencies): new_entry = Balance(new_metadata('<close_zero>', 0), entry.date + datetime.timedelta(days=1), entry.account, Amount(ZERO, currency), None, None) new_entries.append(new_entry) new_entries.append(entry) return new_entries, errors
def prepare(self, journal: JournalEditor, results: SourceResults) -> None: account_to_mint_id, mint_id_to_account = description_based_source.get_account_mapping( journal.accounts, 'mint_id') missing_accounts = set() # type: Set[str] def get_converted_mint_entries(entries): for raw_mint_entry in entries: account = mint_id_to_account.get(raw_mint_entry.account) if not account: missing_accounts.add(raw_mint_entry.account) continue match_entry = raw_mint_entry._replace(account=account) yield match_entry description_based_source.get_pending_and_invalid_entries( raw_entries=get_converted_mint_entries(self.mint_entries), journal_entries=journal.all_entries, account_set=account_to_mint_id.keys(), get_key_from_posting=_get_key_from_posting, get_key_from_raw_entry=_get_key_from_csv_entry, make_import_result=_make_import_result, results=results) for mint_account in missing_accounts: results.add_warning( 'No Beancount account associated with Mint account %r.' % (mint_account, )) for raw_balance in get_converted_mint_entries(self.balances): date = raw_balance.date + datetime.timedelta(days=1) results.add_pending_entry( ImportResult( date=date, info=get_info(raw_balance), entries=[ Balance( account=raw_balance.account, date=date, meta=None, amount=raw_balance.amount, tolerance=None, diff_amount=None) ]))
def balance(self, filename, lineno, date, account, amount, tolerance, kvlist): """Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to balance. amount: The expected amount, to be checked. tolerance: The tolerance number. kvlist: a list of KeyValue instances. Returns: A new Balance object. """ diff_amount = None meta = new_metadata(filename, lineno, kvlist) return Balance(meta, date, account, amount, tolerance, diff_amount)
def deserialise(json_entry: Any) -> Directive: """Parse JSON to a Beancount entry. Args: json_entry: The entry. Raises: KeyError: if one of the required entry fields is missing. FavaAPIException: if the type of the given entry is not supported. """ date = parse_date(json_entry.get("date", ""))[0] if not isinstance(date, datetime.date): raise FavaAPIException("Invalid entry date.") if json_entry["type"] == "Transaction": narration, tags, links = extract_tags_links(json_entry["narration"]) postings = [deserialise_posting(pos) for pos in json_entry["postings"]] return Transaction( json_entry["meta"], date, json_entry.get("flag", ""), json_entry.get("payee", ""), narration or "", tags, links, postings, ) if json_entry["type"] == "Balance": raw_amount = json_entry["amount"] amount = Amount(D(str(raw_amount["number"])), raw_amount["currency"]) return Balance(json_entry["meta"], date, json_entry["account"], amount, None, None) if json_entry["type"] == "Note": comment = json_entry["comment"].replace('"', "") return Note(json_entry["meta"], date, json_entry["account"], comment) raise FavaAPIException("Unsupported entry type.")
def get_out_balance(self): outBalance = Balance(self.meta_out, self.date_out, 'Assets:PayPal', Amount(self.balance_out, 'EUR'), None, None) return outBalance
def parse(self): d = self.soup transactions = [] last_account = '' date_string = d.text.split('出单日:')[1].split('日期范围')[0].strip() balance_date = date(int(date_string[0:4]), int( date_string[5:7]), int(date_string[8:10])) balances = d.select('[style="busi-cunkuan1.tab3.display"] .table1 tr') for balance in balances: tds = balance.select('td.dspts') if len(tds) == 0 or len(tds) < 3: continue account = tds[0].text.strip() account = last_account if account == '' else account last_account = account balance_account = get_account_by_name('ICBC_' + account) currency = self.change_currency(tds[3].text.strip()) price = str(tds[5].text.strip().replace(',', '')) entry = Balance( account=balance_account, amount=Amount(Decimal(price), currency), meta={}, tolerance='', diff_amount=Amount(Decimal('0'), currency), date=balance_date ) transactions.append(entry) bands = d.select('[style="busi-other_detail.tab3.display"] .table1 tr') for band in bands: tds = band.select('td.dspts') if len(tds) == 0: continue trade_date = tds[10].text.strip() if trade_date == '': continue time = date(int(trade_date[0:4]), int( trade_date[4:6]), int(trade_date[6:8])) description = tds[6].text.strip() trade_currency = self.change_currency(tds[3].text.strip()) trade_price = tds[7].text.strip() account = tds[0].text.strip() account = last_account if account == '' else account last_account = account print("Importing {} at {}".format(description, time)) trade_account = get_account_by_name('ICBC_' + account) flag = "*" amount = float(trade_price.replace(',', '')) if account == "Unknown": flag = "!" meta = {} meta = data.new_metadata( 'beancount/core/testing.beancount', 12345, meta ) entry = Transaction( meta, time, flag, description, None, data.EMPTY_SET, data.EMPTY_SET, [] ) data.create_simple_posting( entry, trade_account, trade_price, trade_currency) data.create_simple_posting(entry, AccountUnknown, None, None) if not self.deduplicate.find_duplicate(entry, -amount, None, account): transactions.append(entry) self.deduplicate.apply_beans() return transactions
def prepare(self, journal: JournalEditor, results: SourceResults): matched_transfer_postings = dict( ) # type: Dict[str, List[Tuple[Transaction,Posting]]] matched_payment_postings = dict( ) # type: Dict[str, List[Tuple[Transaction,Posting]]] for entry in journal.all_entries: if not isinstance(entry, Transaction): continue for posting in entry.postings: if posting.meta is None: continue if posting.account != self.assets_account: continue venmo_transfer_id = posting.meta.get(VENMO_TRANSFER_KEY) venmo_payment_id = posting.meta.get(VENMO_PAYMENT_KEY) if venmo_transfer_id is not None: matched_transfer_postings.setdefault( venmo_transfer_id, []).append((entry, posting)) if venmo_payment_id is not None: matched_payment_postings.setdefault(venmo_payment_id, []).append( (entry, posting)) valid_ids = set() for raw_txn in self.raw_transactions: venmo_id = raw_txn[CSV_ID_KEY] t = raw_txn[CSV_TYPE_KEY] valid_ids.add(venmo_id) has_transfer = False has_payment = False if t == 'Standard Transfer': has_transfer = True has_payment = False elif t == 'Charge' or t == 'Payment': has_transfer = raw_txn[ CSV_FUNDING_SOURCE_KEY] != 'Venmo balance' and raw_txn[ CSV_DESTINATION_KEY] != 'Venmo balance' has_payment = True else: raise RuntimeError('Unknown transaction type: %r' % (t, )) for has, matched_postings, make in ( (has_transfer, matched_transfer_postings, self.make_transfer_transaction), (has_payment, matched_payment_postings, self.make_payment_transaction)): existing = matched_postings.get(venmo_id) if existing is not None: num_needed = 1 if has else 0 if len(existing) > num_needed: results.add_invalid_reference( InvalidSourceReference( len(existing) - num_needed, existing)) elif has: txn = make(raw_txn, has_transfer and has_payment) results.add_pending_entry( ImportResult(date=txn.date, entries=[txn], info=get_info(raw_txn))) for raw_balance in self.raw_balances: start_amount_text = raw_balance[BALANCE_START_BALANCE_KEY] if start_amount_text != 'unknown': start_amount = amount_parsing.parse_amount(start_amount_text) start_date = parse_balance_date( raw_balance[BALANCE_START_DATE_KEY]) results.add_pending_entry( ImportResult( date=start_date, entries=[ Balance( date=start_date, meta=None, account=self.assets_account, amount=start_amount, tolerance=None, diff_amount=None, ) ], info=get_info(raw_balance), )) end_amount_text = raw_balance[BALANCE_END_BALANCE_KEY] if end_amount_text != 'unknown': end_amount = amount_parsing.parse_amount(end_amount_text) end_date = parse_balance_date( raw_balance[BALANCE_END_DATE_KEY]) + datetime.timedelta( days=1) results.add_pending_entry( ImportResult( date=end_date, entries=[ Balance( date=end_date, meta=None, account=self.assets_account, amount=end_amount, tolerance=None, diff_amount=None, ) ], info=get_info(raw_balance), )) results.add_account(self.assets_account)
def _make_import_result(fifththird_entry: FifthThirdEntry) -> ImportResult: meta = collections.OrderedDict() for field, value in fifththird_entry._asdict().items(): if field == "filename": continue if field == "line": continue if value is not None: meta["fifththird_%s" % field] = value transaction_ids = ['fifththird.%s' % fifththird_entry.transaction_id] # balance assertion if fifththird_entry.transaction_code == "9999": # set the date one day forward to account for the fact beancount # orders by date, only, so these balance assertions can happen before bal_date = (fifththird_entry.posting_date + datetime.timedelta(days=1)) entries = [] if fifththird_entry.principal_amount: entries.append( Balance(account='Liabilities:Mortgage:FifthThird', date=bal_date, amount=Amount( -1 * fifththird_entry.principal_amount.number, fifththird_entry.principal_amount.currency), meta=meta, tolerance=None, diff_amount=None)) if fifththird_entry.escrow_amount: entries.append( Balance(account='Assets:FifthThird:Escrow', date=bal_date, amount=fifththird_entry.escrow_amount, meta=meta, tolerance=None, diff_amount=None)) return ImportResult( date=bal_date, info=get_info(fifththird_entry), entries=entries, ) # escrow withdraw, payment to third party elif fifththird_entry.transaction_code == "5850": src_posting = Posting( account='Assets:FifthThird:Escrow', units=-fifththird_entry.amount, cost=None, price=None, flag=None, meta=meta, ) payee = 'Escrow Payment - ' account = 'Expenses:FIXME' if 'HAZ INS' in fifththird_entry.description: payee = payee + "Homeowner's Insurance" account = 'Expenses:House:Insurance' elif 'TAXES' in fifththird_entry.description: payee = payee + "Taxes" account = 'Expenses:House:Taxes' else: payee = payee + fifththird_entry.description dst_posting = Posting(account=account, units=fifththird_entry.escrow_amount, price=None, cost=None, flag=None, meta=None) transaction = Transaction(meta=None, date=fifththird_entry.posting_date, flag=FLAG_OKAY, payee=payee, narration=fifththird_entry.description, tags=EMPTY_SET, links=EMPTY_SET, postings=[src_posting, dst_posting]) return ImportResult(date=fifththird_entry.posting_date, info=get_info(fifththird_entry), entries=[transaction]) else: src_posting = Posting( account='Assets:FifthThird:Payment', units=-fifththird_entry.amount, cost=None, price=None, flag=None, meta=meta, ) payee = 'Fifth Third Mortgage' dst_postings = [] if fifththird_entry.principal_amount: dst_postings.append( Posting(account='Liabilities:Mortgage:FifthThird', units=fifththird_entry.principal_amount, price=None, cost=None, flag=None, meta=None)) if fifththird_entry.interest_amount: dst_postings.append( Posting(account='Expenses:House:Mortgage:Interest', units=fifththird_entry.interest_amount, price=None, cost=None, flag=None, meta=None)) if fifththird_entry.escrow_amount: dst_postings.append( Posting(account='Assets:FifthThird:Escrow', units=fifththird_entry.escrow_amount, price=None, cost=None, flag=None, meta=None)) if fifththird_entry.other_amount: dst_postings.append( Posting(account='Expenses:House:Mortgage:Other', units=fifththird_entry.other_amount, price=None, cost=None, flag=None, meta=None)) transaction = Transaction(meta=None, date=fifththird_entry.posting_date, flag=FLAG_OKAY, payee=payee, narration=fifththird_entry.description, tags=EMPTY_SET, links=EMPTY_SET, postings=[src_posting] + dst_postings) return ImportResult(date=fifththird_entry.posting_date, info=get_info(fifththird_entry), entries=[transaction])
def parse(self): d = self.soup transactions = [] # balance = d.select('#fixBand16')[0].text.replace('RMB', '').strip() date_range = d.select('#fixBand6 div font')[0].text.strip() transaction_date = dateparser.parse( date_range.split('-')[1].split('(')[0]) transaction_date = date(transaction_date.year, transaction_date.month, transaction_date.day) self.date = transaction_date balance = '-' + \ d.select('#fixBand7 div font')[0].text.replace( '¥', '').replace(',', '').strip() entry = Balance(account=Account招商, amount=Amount(Decimal(balance), 'CNY'), meta={}, tolerance='', diff_amount=Amount(Decimal('0'), 'CNY'), date=self.date) transactions.append(entry) # bands = d.select('#fixBand29 #loopBand2>table>tbody>tr') bands = d.select("#fixBand29 #loopBand2 > table >tr") for band in bands: tds = band.select('td #fixBand15 table table td') if len(tds) == 0: continue # trade_date = tds[1].text.strip() # if trade_date == '': trade_date = tds[2].text.strip() time = self.get_date(trade_date) if '支付宝' in tds[3].text.strip(): full_descriptions = tds[3].text.strip().split('-') payee = full_descriptions[0] description = '-'.join(full_descriptions[1:]) elif '还款' in tds[3].text.strip(): payee = "还款" description = tds[3].text.strip() else: full_descriptions = tds[3].text.strip().split() payee = full_descriptions[0] description = '-'.join(full_descriptions[1:]) trade_currency = self.change_currency(tds[6].text.strip()) trade_price = tds[7].text.replace('\xa0', '').strip() real_currency = 'CNY' real_price = tds[4].text.replace('¥', '').replace('\xa0', '').strip() print("Importing {} at {}".format(description, time)) account = get_account_by_guess(payee, description, time) flag = "*" amount = float(real_price.replace(',', '')) if account == "Unknown": flag = "!" meta = {} meta = data.new_metadata('beancount/core/testing.beancount', 12345, meta) entry = Transaction(meta, time, flag, payee, description, data.EMPTY_SET, data.EMPTY_SET, []) if real_currency == trade_currency: data.create_simple_posting(entry, account, trade_price, trade_currency) else: trade_amount = Amount(Decimal(trade_price), trade_currency) real_amount = Amount( Decimal(abs(round(float(real_price), 2))) / Decimal(abs(round(float(trade_price), 2))), real_currency) posting = Posting(account, trade_amount, None, real_amount, None, None) entry.postings.append(posting) data.create_simple_posting(entry, Account招商, None, None) if not self.deduplicate.find_duplicate(entry, -amount, None, Account招商): transactions.append(entry) self.deduplicate.apply_beans() return transactions
def prepare(self, journal: JournalEditor, results: SourceResults): account_to_id, id_to_account = description_based_source.get_account_mapping( journal.accounts, 'healthequity_account_id') def convert_account(entry: RawEntry): account_id = entry.account if isinstance(entry, CashTransaction): suffix = 'Cash' else: suffix = entry.units.currency full_account = id_to_account[account_id] + ':' + suffix account_to_id[full_account] = account_id return entry._replace(account=full_account) balances = [convert_account(entry) for entry in self.raw_balances] transactions = [ convert_account(entry) for entry in self.raw_transactions ] description_based_source.get_pending_and_invalid_entries( raw_entries=transactions, journal_entries=journal.all_entries, account_set=account_to_id.keys(), get_key_from_posting=get_key_from_posting, get_key_from_raw_entry=get_key_from_raw_entry, make_import_result=lambda x: make_import_result( x, accounts=journal.accounts, account_to_id=account_to_id, id_to_account=id_to_account), results=results) balance_entries = collections.OrderedDict( ) # type: Dict[Tuple[datetime.date, str, str], ImportResult] for entry in transactions: date = entry.date + datetime.timedelta(days=1) balance_entries[(date, entry.account, entry.balance.currency)] = ImportResult( date=date, entries=[ Balance(date=date, meta=None, account=entry.account, amount=entry.balance, tolerance=None, diff_amount=None) ], info=get_info(entry)) for entry in balance_entries.values(): results.add_pending_entry(entry) for balance in balances: # Skip outputting recent balances --- just output prices. # All transactions provide a balance. # output.append( # ImportResult( # date=balance.date, # entries=[Balance( # date=balance.date, # meta=None, # account=balance.account, # amount=balance.units, # tolerance=None, # diff_amount=None)])) results.add_pending_entry( ImportResult(date=balance.date, info=get_info(balance), entries=[ Price(date=balance.date, meta=None, currency=balance.units.currency, amount=balance.price) ]))
def fetch_transactions(self, name, item, access_token): # Pull transactions for the last 30 days start_date = "{:%Y-%m-%d}".format(datetime.now() + timedelta(days=-self.args.days)) end_date = "{:%Y-%m-%d}".format(datetime.now()) # the transactions in the response are paginated, so make multiple calls while increasing the offset to # retrieve all transactions transactions = [] total_transactions = 1 first_response = None while len(transactions) < total_transactions: try: response = self.client.Transactions.get( access_token, start_date, end_date, offset=len(transactions)) except plaid.errors.PlaidError as e: logging.warning("Plaid error: %s", e.message) return transactions.extend(response["transactions"]) if first_response is None: first_response = response total_transactions = response["total_transactions"] if self.args.debug: pretty_print_stderr(response) if "accounts" not in first_response: logging.warning("No accounts, aborting") return assert "accounts" in item for account in item["accounts"]: if account["sync"] != "transactions": continue currency = account["currency"] # checking for every configured account in the response t_account = next( filter( lambda tacc: account["id"] == tacc["account_id"], first_response["accounts"], ), None, ) if t_account is None: logging.warning("Not present in response: %s", account["name"]) continue ledger = [] for transaction in transactions: if account["id"] != transaction["account_id"]: continue assert currency == transaction["iso_currency_code"] if transaction["pending"]: # we want to wait for the transaction to be posted continue amount = D(transaction["amount"]) # sadly, plaid-python parses as `float` https://github.com/plaid/plaid-python/issues/136 amount = round(amount, 2) posting = Posting(account["name"], Amount(-amount, currency), None, None, None, None) ref = data.new_metadata("foo", 0) entry = Transaction( # pylint: disable=not-callable meta=ref, date=date.fromisoformat(transaction["date"]), flag=flags.FLAG_OKAY, payee=transaction["name"], narration="", tags=data.EMPTY_SET, links=data.EMPTY_SET, postings=[posting], ) ledger.append(entry) ledger.reverse( ) # API returns transactions in reverse chronological order if self.output_mode == "text": # print entries to stdout print("; = {}, {} =".format(account["name"], currency)) print("; {} transactions\n".format(len(ledger))) # flag the duplicates self.annotate_duplicate_entries(ledger) # add the balance directive if "current" in t_account["balances"]: bal = D(t_account["balances"]["current"]) # sadly, plaid-python parses as `float` https://github.com/plaid/plaid-python/issues/136 bal = round(bal, 2) if t_account["type"] in {"credit", "loan"}: # the balance is a liability in the case of credit cards, and loans # https://plaid.com/docs/#account-types bal = -bal if t_account["balances"]["current"] != None: meta = data.new_metadata("foo", 0) entry = Balance( # pylint: disable=not-callable meta=meta, date=date.today(), account=account["name"], amount=Amount(bal, currency), tolerance=None, diff_amount=None, ) ledger.append(entry) if self.output_mode == "db": # write the account's ledger to intermediate output, pickled file self.output[account["name"]] = ledger else: assert self.output_mode == "text" # print out all the entries for entry in ledger: out = printer.format_entry(entry) if DUPLICATE_META in entry.meta: out = textwrap.indent(out, "; ") print(out) logging.info("Done %s", name) if self.output_mode == "text": print() # newline
def fetch_balance(self, name, item, access_token): try: response = self.client.Accounts.get(access_token) except plaid.errors.PlaidError as e: logging.warning("Plaid error: %s", e.message) return if self.args.debug: pretty_print_stderr(response) if "accounts" not in response: logging.warning("No accounts, aborting") return assert "accounts" in item for account_def in item["accounts"]: if account_def["sync"] != "balance": continue # checking for every configured account in the response account_res = next( filter( lambda tacc: account_def["id"] == tacc["account_id"], response["accounts"], ), None, ) if account_res is None: logging.warning("Not present in response: %s", account_def["name"]) continue assert "balances" in account_res assert (account_def["currency"] == account_res["balances"] ["iso_currency_code"]) if ("current" not in account_res["balances"] or account_res["balances"]["current"] is None): logging.warning("No 'current' account balance, aborting") continue bal = D(account_res["balances"]["current"]) # sadly, plaid-python parses as `float` https://github.com/plaid/plaid-python/issues/136 bal = round(bal, 2) if account_res["type"] in {"credit", "loan"}: # the balance is a liability in the case of credit cards, and loans # https://plaid.com/docs/#account-types bal = -bal meta = data.new_metadata("foo", 0) balance_entry = Balance( # pylint: disable=not-callable meta=meta, date=date.today(), account=account_def["name"], amount=Amount(bal, account_def["currency"]), tolerance=None, diff_amount=None, ) ledger = [] ledger.append(self.pad(meta, account_def["name"])) ledger.append(balance_entry) if self.output_mode == "text": print( f"; = {account_def['name']}, {account_def['currency']} =") for entry in ledger: out = printer.format_entry(entry) print(out) else: assert self.output_mode == "db" self.output[account_def["name"]] = ledger logging.info("Done %s", name) if self.output_mode == "text": print() # newline
def _make_import_result(entry) -> ImportResult: tags = EMPTY_SET date = datetime.date.fromisoformat(entry['date']) if 'transaction_id' in entry: meta = collections.OrderedDict( date=date, plaid_transaction_id=entry['transaction_id'], ) if entry['account_owner']: meta["account_owner"] = entry['account_owner'] if entry['category']: meta["category"] = ", ".join(entry['category']) meta["source_desc"] = entry['name'] sign = -1 # json parsed the number as a float, need to make it fixed point. amount = Amount(number=sign * D(str(entry['amount'])), currency=entry['iso_currency_code']) if 'category' in meta and meta['category'] == "Payment, Credit Card": counter_account = "Assets:Transfer:ACH" else: counter_account = FIXME_ACCOUNT journal_entry = Transaction(meta=None, date=date, flag=FLAG_OKAY, payee=entry['merchant_name'], narration=entry['name'], tags=tags, links=EMPTY_SET, postings=[ Posting( account=entry['account'], units=amount, cost=None, price=None, flag=None, meta=meta, ), Posting( account=FIXME_ACCOUNT, units=-amount, cost=None, price=None, flag=None, meta=None, ), ]) else: balance = entry['balances'] sign = 1 if entry['account'].startswith('Liabilities'): sign = -1 journal_entry = Balance( date=date, meta=None, account=entry['account'], amount=Amount( # json parsed the number as a float, need to make it fixed point. number=sign * D(str(balance['current'])), currency=balance['iso_currency_code'], ), tolerance=None, diff_amount=None, ) return ImportResult( date=date, info=dict( type='text/plain', filename=entry['file'], ), entries=[journal_entry], )
def parse_cmb(filename): account = "Liabilities:CreditCard:CMB" transactions = [] with open(filename, "rb") as f: file_bytes = f.read() parsed_eml = eml_parser.eml_parser.decode_email_b(file_bytes, include_raw_body=True) # print(parsed_eml) content = parsed_eml["body"][0]["content"] soup = BeautifulSoup(content, "html.parser") print(soup) # balance according to bill amount date_range = soup.select("#fixBand38 div font")[0].text.strip() transaction_date = dateparser.parse( date_range.split('-')[1].split('(')[0]) transaction_date = date(transaction_date.year, transaction_date.month, transaction_date.day) start_date = dateparser.parse(date_range.split('-')[0]) start_date = date(start_date.year, start_date.month, start_date.day) balance = '-' + \ soup.select('#fixBand40 div font')[0].text.replace( '¥', '').replace(',', '').strip() entry = Balance( account=account, amount=Amount(Decimal(balance), 'CNY'), meta={}, tolerance='', diff_amount=Amount(Decimal('0'), 'CNY'), date=transaction_date ) transactions.append(entry) # bands = soup.select('#fixBand29 #loopBand2>table>tbody>tr') bands = soup.select("#fixBand29 #loopBand2>table>tr") for band in bands: tds = band.select('td #fixBand15 table table td') if len(tds) == 0: continue trade_date = tds[1].text.strip() if trade_date == '': trade_date = tds[2].text.strip() time = get_date(start_date, trade_date) full_descriptions = tds[3].text.strip().split('-') payee = full_descriptions[0] description = '-'.join(full_descriptions[1:]) trade_currency = get_currency(tds[6].text.strip()) trade_price = tds[7].text.replace('\xa0', '').strip() real_currency = 'CNY' real_price = tds[4].text.replace( '¥', '').replace('\xa0', '').strip() print("Importing {} - {} at {}".format(payee, description, time)) category = get_category(description, payee) if (payee == "自动还款" or payee == "掌上生活还款"): description = payee category = "Assets:DepositCard:CMB9843" flag = "*" amount = float(real_price.replace(',', '')) meta = {} entry = Transaction(meta, time, flag, payee, description, data.EMPTY_SET, data.EMPTY_SET, []) if real_currency == trade_currency: data.create_simple_posting( entry, category, trade_price, trade_currency) else: trade_amount = Amount(Decimal(trade_price), trade_currency) real_amount = Amount(Decimal(abs(round(float( real_price), 2))) / Decimal(abs(round(float(trade_price), 2))), real_currency) posting = Posting(category, trade_amount, None, real_amount, None, None) entry.postings.append(posting) data.create_simple_posting(entry, account, None, None) transactions.append(entry) return transactions
def get_in_balance(self): inBalance = Balance(self.meta_in, self.date_in, 'Assets:PayPal', Amount(self.balance_in, 'EUR'), None, None) return inBalance