Beispiel #1
0
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
Beispiel #2
0
    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)
Beispiel #3
0
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
Beispiel #5
0
 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
Beispiel #8
0
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
Beispiel #9
0
    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,
        )
Beispiel #10
0
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
Beispiel #11
0
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
Beispiel #12
0
    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)
                    ]))
Beispiel #13
0
    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)
Beispiel #14
0
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
Beispiel #17
0
    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)
Beispiel #18
0
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])
Beispiel #19
0
    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
Beispiel #20
0
    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)
                             ]))
Beispiel #21
0
    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
Beispiel #22
0
    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
Beispiel #23
0
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