def get_amount(self, row):
        self.assert_is_row(row)

        if self.invert_amounts:
            return data.Amount(-D(row.amount), self.currency)
        else:
            return data.Amount(D(row.amount), self.currency)
示例#2
0
文件: dk.py 项目: runarp/coolbeans
    def entry_from_gl(self, entries: typing.List[Entry]) -> typing.Iterable:
        """Given a single GL_ID and as a list of Entrys
        """
        first = entries[0]

        postings = []
        all_tags = set()
        all_meta = {'lineno': 0, 'filename': "", 'gl-id': first.gl_id}
        all_links = set()

        for entry in entries:
            self.new_accounts.add(entry.account)

            if first.entry_type == "Balance":
                if entry.amount and entry.date.year == 2016:
                    yield data.Pad(date=entry.date -
                                   datetime.timedelta(days=1),
                                   account=entry.account,
                                   source_account="Equity:OpeningBalances",
                                   meta={
                                       'lineno': 0,
                                       'filename': '',
                                       'note': entry.narration,
                                       'gl-id': entry.gl_id
                                   })
                yield data.Balance(date=entry.date,
                                   amount=data.Amount(entry.amount,
                                                      entry.currency),
                                   account=entry.account,
                                   tolerance=None,
                                   diff_amount=None,
                                   meta={
                                       'lineno': 0,
                                       'filename': '',
                                       'note': entry.narration,
                                       'gl-id': entry.gl_id
                                   })
            else:
                posting = data.Posting(entry.account,
                                       data.Amount(entry.amount,
                                                   entry.currency),
                                       None,
                                       None,
                                       flag='*',
                                       meta=entry.meta)
                all_tags.update(entry.tags)
                all_links.update(entry.links)

                postings.append(posting)
                all_meta.update(posting.meta or {})

        if postings:
            yield data.Transaction(meta=all_meta,
                                   date=entry.date,
                                   flag='*',
                                   payee=entry.payee,
                                   narration=entry.narration,
                                   tags=all_tags,
                                   links=all_links,
                                   postings=postings)
示例#3
0
    def get_amount(self, row):
        self.assert_is_row(row)

        commodity = self.investments.get(row.investment)

        if self.invert_amounts:
            return data.Amount(-D(row.amount), commodity)
        else:
            return data.Amount(D(row.amount), commodity)
示例#4
0
    def convert_transaction(txn, acct_map):
        meta = {}
        date = txn.GetDate().strftime('%Y-%m-%d')
        flag = '*'
        payee = ''
        narration = txn.GetDescription()
        base_currency = txn.GetCurrency().get_mnemonic()

        postings = []
        for split in txn.GetSplitList():
            split_meta = {}
            memo = split.GetMemo()
            if memo:
                split_meta['memo'] = memo
            gnc_acct = split.GetAccount()
            is_currency = gnc_acct.GetCommodity().get_namespace() == 'CURRENCY'
            gnc_amount = split.GetAmount()
            acct = acct_map[gnc_acct.GetGUID().to_string()]
            amount = Converter.normalize_numeric(gnc_amount)
            units = data.Amount(amount, acct.currencies[0])
            split_flag = None
            if acct.currencies[0] != base_currency:
                total_cost = data.Amount(
                    abs(Converter.normalize_numeric(split.GetValue())),
                    base_currency)
                num_units = abs(gnc_amount.to_double())
                if num_units == 0.:
                    cost = price = None
                else:
                    price = data.Amount(
                        decimal.Decimal(
                            float(split.GetValue().to_double()) /
                            float(gnc_amount.to_double())), base_currency)
                    if amount > 0 and not is_currency:
                        cost = data.Cost(price, base_currency,
                                         txn.GetDate().date(), None)
                    else:
                        cost = None
                postings.append(
                    CostBasedPosting(acct.account, units, cost, price,
                                     total_cost, split_flag, split_meta))
            else:
                cost = price = None
                postings.append(
                    data.Posting(acct.account, units, cost, price, split_flag,
                                 split_meta))
        return data.Transaction(meta, date, flag, payee, narration, None, None,
                                postings)
示例#5
0
def read_price_stream(stream: typing.Iterable, price_db: typing.Dict[str,
                                                                     dict],
                      quote_currency: str) -> data.Entries:
    """Reads an iterable of tuples and compares against existing price_db

    Returns:
        list of beancount Entries
    """

    entries = []
    for row in stream:
        currency, _date, _amount = row[0:3]
        if currency not in price_db:
            continue
        dp: datetime.datetime = dateparser.parse(_date)
        assert dp, f"Unable to parse date {_date}"
        date = datetime.date(dp.year, dp.month, dp.day)
        amount = data.Amount(decimal.Decimal(_amount), quote_currency)

        history = price_db[currency]
        if date in history:
            continue

        entry = data.Price(date=date,
                           currency=currency,
                           amount=amount,
                           meta=data.new_metadata('', 0))

        entries.append(entry)

    return entries
示例#6
0
def Price(price):
    meta = {}
    date = price.date
    currency = price.commodity.mnemonic
    amount = data.Amount(price.value, price.currency.mnemonic)

    return data.Price(meta, date, currency, amount)
示例#7
0
def Price(price):
    meta = {}
    date = price.date
    currency = commodity_name(price.currency)
    amount = data.Amount(price.value, currency)
    commodity = commodity_name(price.commodity)
    return data.Price(meta, date, commodity, amount)
示例#8
0
    def add_account(self, name, date=None, currency=None, initial_amount=None):
        date = date or config.DEFAULT_DATE
        name = self._format_account_name(name)
        self.accounts.append(name)

        currencies = currency and [currency]

        self.entries.append(
            bc.Open(self._get_meta(), date, name, currencies, booking=None))

        if initial_amount is not None and initial_amount != 0:
            assert currency is not None

            self.pad_balances.append(
                bc.Pad(self._get_meta(),
                       date=config.DEFAULT_PAD_DATE,
                       account=name,
                       source_account=config.OPENING_BALANCE_ACCOUNT))
            self.pad_balances.append(
                bc.Balance(self._get_meta(),
                           date=config.DEFAULT_BALANCE_DATE,
                           account=name,
                           amount=bc.Amount(initial_amount, currency),
                           tolerance=None,
                           diff_amount=None))

        return len(self.accounts) - 1  # index of the account on self.accounts
示例#9
0
def units_for(split):

    number = data.Decimal(split.quantity).quantize(
        data.Decimal(1.0) / data.Decimal(split.account.commodity.fraction))
    currency = commodity_name(split.account.commodity)

    return data.Amount(number, currency)
示例#10
0
def units_for(split):
    # I was having balance precision problems due to how beancount deal
    # with integer precision. So multiply quantity by 1.0 to force at
    # least 1 decimal place.

    number = split.quantity * data.Decimal('1.0')
    currency = split.account.commodity.mnemonic

    return data.Amount(number, currency)
示例#11
0
 def convert_price(name, currency, gnc_price):
     meta = {}
     v = gnc_price.get_value()
     gv = gnucash.gnucash_business.GncNumeric(instance=v)
     price = Converter.normalize_numeric(gv)
     amount = data.Amount(price, currency)
     date = gnc_price.get_time64().strftime('%Y-%m-%d')
     return data.Price(meta, date, Converter.normalize_commodity(name),
                       amount)
示例#12
0
    def extract(self,
                file: cache._FileMemo,
                existing_entries=None) -> data.Entries:
        """Given a File, let's extract the records"""

        name = file.name
        possible, context = self._read_file(name)
        root_account = self.get_root()
        if not root_account:
            logger.error(f"Unable to find an account {context}")
            return []

        new_entries = []

        for entry in possible:
            target_account = self.accounts['default-transfer']

            new_entries.append(
                data.Transaction(
                    date=entry.date,
                    narration=entry.narration,
                    payee=entry.payee,
                    meta=entry.meta,
                    tags=set(),
                    links=set(),
                    flag="!",
                    postings=[
                        data.Posting(account=root_account,
                                     units=data.Amount(entry.amount,
                                                       entry.currency),
                                     flag="*",
                                     cost=None,
                                     price=None,
                                     meta=None),
                        data.Posting(account=target_account,
                                     units=data.Amount(-entry.amount,
                                                       entry.currency),
                                     flag="!",
                                     cost=None,
                                     price=None,
                                     meta=None)
                    ]))
        return new_entries
示例#13
0
def price_for(split):
    acc_comm = split.account.commodity
    txn_comm = split.transaction.currency

    if acc_comm == txn_comm:
        return None

    number = abs(split.value / split.quantity)
    currency = txn_comm.mnemonic

    return data.Amount(number, currency)
示例#14
0
    def extract(self, file, existing_entries):
        csvfile = open(file=file.name, encoding='windows_1252')
        reader = csv.reader(csvfile, delimiter=';')
        meta = data.new_metadata(file.name, 0)
        entries = []

        for row in reader:

            try:
                book_date, text, credit, debit, val_date, balance = tuple(row)
                book_date = datetime.strptime(book_date, '%Y-%m-%d').date()
                if credit:
                    amount = data.Amount(Decimal(credit), self.currency)
                elif debit:
                    amount = data.Amount(Decimal(debit), self.currency)
                else:
                    amount = None
                if balance:
                    balance = data.Amount(Decimal(balance), self.currency)
                else:
                    balance = None
            except Exception as e:
                logging.debug(e)
            else:
                logging.debug((book_date, text, amount, val_date, balance))
                posting = data.Posting(self.account, amount, None, None, None,
                                       None)
                entry = data.Transaction(meta, book_date, '*', '', text,
                                         data.EMPTY_SET, data.EMPTY_SET,
                                         [posting])
                entries.append(entry)
                # only add balance on SOM
                book_date = book_date + timedelta(days=1)
                if balance and book_date.day == 1:
                    entry = data.Balance(meta, book_date, self.account,
                                         balance, None, None)
                    entries.append(entry)

        csvfile.close()
        entries = data.sorted(entries)
        return entries
示例#15
0
    def extract_prices(self,
                       statement: FlexStatement,
                       existing_entries: list = None):
        """
        IBFlex XML Files can contain an object called 'OpenPositions',
        this is very useful because it lets us create

        - Balance assertions
        - Price entries from the Mark
        """
        result = []
        for position in statement.OpenPositions:
            price = position.markPrice
            safe_symbol = self.clean_symbol(position.symbol)
            # Dates are 12 Midnight, let's make it the next day
            date = statement.toDate + timedelta(days=1)
            result.append(
                # TODO De-Duplicate
                data.Price(currency=safe_symbol,
                           amount=data.Amount(price, "USD"),
                           date=date,
                           meta={
                               'lineno': 0,
                               'filename': '',
                           }))
            account = self.account_for_symbol(statement, position.symbol)
            result.append(
                data.Balance(
                    account=account,
                    amount=data.Amount(position.position * position.multiplier,
                                       safe_symbol),
                    date=statement.toDate + timedelta(days=1),
                    meta={
                        'lineno': 0,
                        'filename': ''
                    },
                    tolerance=0.5,
                    diff_amount=0,
                ))
        return result
示例#16
0
def price_for(split):
    acc_comm = split.account.commodity
    txn_comm = split.transaction.currency

    if acc_comm == txn_comm:
        return None

    try:
        number = abs(split.value / split.quantity) * data.Decimal('1.000000')
    except InvalidOperation:  # Skip "bad" transcations (split.value and quantity are 0)
        return None
    currency = commodity_name(txn_comm)

    return data.Amount(number, currency)
示例#17
0
def price_for(split):
    acc_comm = split.account.commodity
    txn_comm = split.transaction.currency

    if acc_comm == txn_comm:
        return None

    try:
        number = abs(split.value / split.quantity)
    except (decimal.DivisionByZero,
            decimal.InvalidOperation):  # invalid operation occurs when 0/0
        if split.quantity.is_zero():
            number = decimal.Decimal('0')
    currency = txn_comm.mnemonic

    return data.Amount(number, currency)
示例#18
0
 def get_member(self, id, name, date, balance):
     #name = member_account(name)
     name = self.accounts_by_id[id]
     if id in self.accounts_by_id and self.accounts_by_id[id] != name:
         # Generate rename
         txn = bcdata.Transaction(meta={},
                                  date=date,
                                  flag="txn",
                                  payee="",
                                  narration="Rename %(from)s to %(to)s" % {
                                      "from": self.accounts_by_id[id],
                                      "to": name,
                                  },
                                  tags=set("rename"),
                                  links=set(),
                                  postings=[])
         bcdata.create_simple_posting(txn, self.accounts_by_id[id], balance,
                                      "EUR")
         bcdata.create_simple_posting(txn, name, -balance, "EUR")
         self.entries.append(txn)
         if self.accounts_by_id[id] not in DONT_CLOSE:
             self.entries.append(
                 bcdata.Close(
                     meta={},
                     date=date + datetime.timedelta(days=1),
                     account=self.accounts_by_id[id],
                 ))
         # Disable the balance assertion for the next day
         self.last_assertion[name] = date
         self.initial_balances[name] = None
         self.accounts_by_id[id] = name
     elif name not in self.initial_balances:
         self.initial_balances[name] = balance
         self.last_assertion[name] = date
         self.accounts_by_id[id] = name
     elif self.last_assertion.get(
             name, None) != date and name != "Assets:Cash:Bar":
         self.entries.append(
             bcdata.Balance({"iline": str(self.line)}, date, name,
                            bcdata.Amount(-balance, "EUR"), None, None))
         self.last_assertion[name] = date
     return name
示例#19
0
    def add_transaction(self, date, payee, narration, main_account,
                        other_account, amount, currency, tags):
        posting_common_args = dict(cost=None, price=None, flag=None, meta=None)

        if config.PREFER_POSITIVE_AMOUNTS and amount < 0:
            main_account, other_account = other_account, main_account
            amount = -amount

        postings = [
            bc.Posting(main_account, bc.Amount(amount, currency),
                       **posting_common_args),
            bc.Posting(other_account, None, **posting_common_args),
        ]

        self.transactions.append(
            bc.Transaction(self._get_meta(),
                           date,
                           flag=config.DEFAULT_FLAG,
                           payee=payee,
                           narration=narration,
                           tags=tags,
                           links=None,
                           postings=postings))
示例#20
0
    def get_transaction(
            self,
            row: Row,
            file_name,
            row_number,
            existing_entries=None):

        flag = flags.FLAG_WARNING

        row = row._replace(**{
            'visit_date': parse_date_liberally(row.visit_date),
            'amount_billed': data.Amount(
                D(row.amount_billed.replace("$", "")), self.currency),
            'amount_deductible': data.Amount(
                D(row.amount_deductible.replace("$", "")), self.currency),
            'amount_plan_paid': data.Amount(
                D(row.amount_plan_paid.replace("$", "")), self.currency),
            'amount_plan_discount': data.Amount(
                D(row.amount_plan_discount.replace("$", "")), self.currency),
            'amount_responsible': data.Amount(
                D(row.amount_responsible.replace("$", "")), self.currency),
            'amount_paid_at_visit': data.Amount(
                D(row.amount_paid_at_visit.replace("$", "")), self.currency),
            'amount_owed': data.Amount(
                D(row.amount_owed.replace("$", "")), self.currency),
        })

        metadata = {
            'claim-number': row.claim_number,
            'claim-type': row.claim_type,
            'patient': row.patient,
            'provider': row.provider,
            'visit-date': row.visit_date,
        }

        postings = []
        if row.amount_plan_paid:
            postings.append(data.Posting(
                account=self.reimbursement_account,
                units=row.amount_plan_paid,
                cost=None,
                price=None,
                flag=None,
                meta={}))

        if row.amount_plan_discount:
            postings.append(data.Posting(
                account=self.discount_account,
                units=row.amount_plan_discount,
                cost=None,
                price=None,
                flag=None,
                meta={}))

        if not postings:
            return None

        return data.Transaction(  # pylint: disable=not-callable
            data.new_metadata(file_name,
                                  row_number,
                                  metadata),
            parse_date_liberally(row.date),
            flag,
            "United Healthcare",
            self.narration_format.format(**row._asdict()),
            self.tags,
            set([self.link_format.format(**row._asdict())]),
            postings)
示例#21
0
 def get_amount(self, row: Row):
     total = row.item_total.strip(''.join(self.currency_symbols))
     return data.Amount(number.D(total), row.currency)
示例#22
0
    def extract_trades(self,
                       statement: FlexStatement,
                       existing_entries: list = None):
        """
        Version one does not attempt to group by order ID.  This allows for perfect lot pricing and simpler
        implementation.  The downside is it's very verbose.

            txn = data.Transaction(meta, date, self.FLAG, payee, narration,
                                   tags, data.EMPTY_SET, [])

        * https://www.interactivebrokers.com/en/software/reportguide/reportguide/tradesfq.htm

        TODO: Handle Duplicates
        """
        fees_account = self.accounts['fees']

        match_key = 'id'

        existing_by_key = self.find_existing(existing_entries, match_key)

        by_order: typing.Dict[str, CombinedTrades] = {}

        for trade in statement.Trades:
            key = CombinedTrades.trade_key(trade)
            assert key.strip(), f"Invalid Key {len(key)}"
            if not trade.openCloseIndicator:
                continue
            if key in by_order:
                combined = by_order[key]
                combined.add_trade(trade.quantity, trade.tradePrice,
                                   trade.ibCommission)
            else:
                combined = CombinedTrades(
                    ibOrderID=trade.ibOrderID,
                    tradeDate=trade.tradeDate,
                    tradePrice=trade.tradePrice,
                    exchange=trade.exchange,
                    multiplier=trade.multiplier,
                    ibCommission=trade.ibCommission,
                    symbol=trade.symbol,
                    currency=trade.currency,
                    safe_symbol=self.clean_symbol(trade.symbol),
                    buySell=trade.buySell,
                    quantity=trade.quantity,
                    securityID=trade.securityID,
                    ibCommissionCurrency=trade.ibCommissionCurrency,
                    openCloseIndicator=trade.openCloseIndicator.name)
                by_order[key] = combined

        result = []
        for trade in by_order.values():

            key = CombinedTrades.trade_key(trade)
            if key in existing_by_key:
                continue

            meta = {
                'lineno': 0,
                'filename': '',
                match_key: key,
                'order_id': trade.ibOrderID,
                'exchange': trade.exchange,
                #               'multiplier': str(trade.multiplier),
                #               'commission': trade.ibCommission,
            }

            # TODO Make this a parameter
            payee = "IB"

            if trade.securityID is None and "." in trade.symbol:
                # FOREX Trade, not really a valid Symbol at all
                # TODO: Better check than blank securityID
                # Usually [currency].[commodity].  For example GBP.JPY
                # In that case trade.currency is JPY, so we just need to parse out the GBP part
                safe_symbol, _ = trade.symbol.split('.')
            else:
                safe_symbol = self.clean_symbol(trade.symbol)

            narration = (
                f"{trade.buySell.name} {trade.quantity} {safe_symbol} @ "
                f"{trade.tradePrice} {trade.currency} on {trade.exchange}")

            tags = data.EMPTY_SET

            # cost = data.Amount(trade.cost, trade.currency)

            cost_account = self.account_for_symbol(statement, trade.currency)
            fees_cost_account = self.account_for_symbol(
                statement, trade.ibCommissionCurrency)
            comm_account = self.account_for_symbol(statement, safe_symbol)

            # This is how much USD it cost us
            # cost_amount = data.Cost(number=trade.netCash, currency=trade.currency, date=trade.tradeDate, label=f"xxx")
            cash_amount = amount.Amount(
                -trade.quantity * trade.multiplier * trade.tradePrice,
                trade.currency)
            unit_amount = amount.Amount(trade.quantity * trade.multiplier,
                                        safe_symbol)

            # post_price = data.Price(currency=trade.currency, amount=trade.tradePrice, meta={}, date=trade.tradeDate)
            post_price = data.Amount(trade.tradePrice, trade.currency)
            txn = data.Transaction(
                meta,
                trade.tradeDate,
                self.FLAG,
                "",  # payee,
                narration,
                tags,
                data.EMPTY_SET,  # {link} -- what is this?
                [
                    data.Posting(
                        account=comm_account,
                        units=unit_amount,
                        cost=
                        EMPTY_COST_SPEC,  #cost_amount, # cost=(Cost, CostSpec, None),
                        price=post_price,
                        flag=self.FLAG,
                        meta={}),
                    # How does this affect our Cash?
                    data.Posting(
                        account=cost_account,
                        units=cash_amount,
                        cost=None,  # cost=(Cost, CostSpec, None),
                        price=None,
                        flag=self.FLAG,
                        meta={}),
                    # Total Fees
                    data.Posting(account=fees_cost_account,
                                 units=amount.Amount(
                                     trade.ibCommission,
                                     trade.ibCommissionCurrency),
                                 cost=None,
                                 price=None,
                                 flag=self.FLAG,
                                 meta={}),
                    data.Posting(account=fees_account,
                                 units=amount.Amount(
                                     -trade.ibCommission,
                                     trade.ibCommissionCurrency),
                                 cost=None,
                                 price=None,
                                 flag=self.FLAG,
                                 meta={})
                ])
            if trade.openCloseIndicator == "CLOSE":
                # This is a reduction in position, add the Income Account:
                data.create_simple_posting(entry=txn,
                                           account=self.accounts.get(
                                               'income', 'Income:FIXME'),
                                           number=None,
                                           currency=None)
            result.append(txn)

        return result
示例#23
0
    def get_price(self, row):
        self.assert_is_row(row)
        price_per_share = row.price_per_share.strip(
            ''.join(self.currency_symbols) + "()")

        return data.Amount(D(price_per_share), self.currency)
示例#24
0
def load_remote_account(connection: gspread.Client, errors: list, account: str,
                        options: typing.Dict[str, str]):
    """Try to Load Entries from URL into Account.

    options include:
        - document_name -- the Actual Google Doc name
        - document_tab -- the Tab name on the Doc
        - default_currency - the entry currency if None is provided
        - reverse_amount - if true, assume positive entries are credits

    """
    entries = []

    document_name = options['document_name']
    document_tab = options.get('document_tab', 0) or 0
    default_currency = options['default_currency']
    reverse_amount = options.get('reverse_amount', False)

    if not document_name:
        return

    m = -1 if reverse_amount else 1
    logger.info(
        f"Attempting to download entries for {account} from {document_name}.{document_tab}"
    )
    workbook = connection.open(document_name)
    sheet = None
    try:
        document_tab = int(document_tab)
        sheet = workbook.get_worksheet(document_tab)
    except ValueError:
        pass
    if sheet is None:
        sheet = workbook.worksheet(document_tab)

    records = sheet.get_all_records()
    import re
    row = 0
    # logger.info(f"Found {len(records)} entries.")
    for record in records:
        row += 1
        record = clean_record(record)
        if 'date' not in record or not record['date']:
            continue
        if 'amount' not in record or not record['amount']:
            continue
    #if 'account' not in record or not record['account'].strip():
    #    continue

        narration = record.pop('narration', None)

        payee = record.pop('payee', None)

        tagstr = record.pop('tags', '')
        tags = set(re.split(r'\W+', tagstr)) if tagstr else set()

        date = dateparser.parse(record.pop('date'))
        if date:
            date = datetime.date(year=date.year,
                                 month=date.month,
                                 day=date.day)

        linkstr = record.pop('links', '')
        links = set(re.split(r'\W+', linkstr)) if linkstr else set()

        meta = {
            'filename': str(options['entry_file']),
            'lineno': 0,
            'document-sheet-row': f"{document_name}/{document_tab}/{row+1}"
        }
        amount = decimal.Decimal(record.pop('amount')) * m
        currency = record.pop('currency', default_currency)
        entry_account = record.pop('account')
        for k, v in record.items():
            if v:
                meta[k] = v

        try:
            if not entry_account:
                errors.append(
                    f"Skipping Record with Blank Account: {meta['document-sheet-row']}"
                )
                logger.warning(
                    f"Skipping Record with Blank Account: {meta['document-sheet-row']}"
                )
                continue
            entry = data.Transaction(
                date=date,
                narration=narration,
                payee=payee,
                tags=tags,
                meta=meta,
                links=links,
                flag='*',
                postings=[
                    data.Posting(account=account,
                                 units=data.Amount(amount, currency),
                                 cost=None,
                                 price=None,
                                 flag='*',
                                 meta={}),
                    data.Posting(account=entry_account,
                                 units=data.Amount(-amount, currency),
                                 cost=None,
                                 price=None,
                                 flag='*',
                                 meta={})
                ])
            entries.append(entry)
        except Exception as exc:
            logger.error(f"Error while parsing {record}", exc_info=exc)
            errors.append(str(exc))

    logger.info(
        f"Loaded {len(entries)} entries for {account} from {document_name}.{document_tab}"
    )
    return entries
示例#25
0
def parse_amount(d: dict) -> bean.Amount:
    if d is None:
        return d
    return bean.Amount(number=d["number"], currency=d["currency"])
示例#26
0
def main():
    position_account = "Liabilities:Crypto:Bitfinex:Positions"
    margin_account = "Liabilities:Crypto:Bitfinex:Positions"
    income_account = "Income:Crypto:Bitfinex:Realized"

    fee_account = "Expenses:Trading:Bitfinex:Fees:Trading"

    positions: typing.Dict[str, Decimal] = defaultdict(Decimal)
    balances: typing.Dict[str, datetime.date] = defaultdict(
        lambda: datetime.date(2000, 1, 1))

    records = list(records_from_string(csvdata))
    records.sort(key=lambda r: (r.date, r.commodity, -r.quantity))

    entries = []
    for record in records:

        # Now, we need to Handle the Incoming Account, Only if our Absolute
        # Position is decreasing:
        current_position = positions[record.commodity]
        new_position = current_position + record.quantity
        positions[record.commodity] = new_position

        # This is the account who's position we need to track for our lots
        commodity_account = account.join(position_account, record.commodity)

        if balances[commodity_account] != record.date:
            entries.append(
                data.Balance(date=record.date,
                             account=commodity_account,
                             amount=Amount(current_position, record.commodity),
                             tolerance=Q,
                             diff_amount=None,
                             meta=data.new_metadata("", 0)))
            balances[commodity_account] = record.date

        entry = data.Transaction(date=record.date,
                                 narration=str(record),
                                 payee="",
                                 tags={"margin"},
                                 links=set(),
                                 flag='*',
                                 meta=data.new_metadata(
                                     "",
                                     0,
                                     kvlist=dict(position=str(new_position),
                                                 order_id=record.order_id)),
                                 postings=[])
        # The Commodity Adjustment

        # Assets:Crypto:Bitfinex:Positions:BTC 200 BTC {} @ 6950 USD
        posting = data.Posting(account=commodity_account,
                               units=data.Amount(record.quantity,
                                                 record.commodity),
                               cost=empty_cost_spec,
                               price=Amount(record.price, record.currency),
                               flag=None,
                               meta=data.new_metadata("", 0))
        entry.postings.append(posting)

        # The Currency Account Adjustment (based on Cost?)
        # ; Liabilities:Crypto:Bitfinex:Borrowed:USD     -1,390,102.19 USD
        data.create_simple_posting(entry=entry,
                                   account=account.join(
                                       margin_account, record.currency),
                                   number=-record.amount,
                                   currency=record.currency)
        if record.fee:
            # We add Two fee Records
            # ; Liabilities:Crypto:Bitfinex:Borrowed:USD               -839.747894 USD
            data.create_simple_posting(entry=entry,
                                       account=account.join(
                                           margin_account, record.currency),
                                       number=record.fee,
                                       currency=record.currency)
            data.create_simple_posting(entry=entry,
                                       account=fee_account,
                                       number=-record.fee,
                                       currency=record.currency)

        if abs(new_position) < abs(current_position):
            # Add an Income Account Entry
            data.create_simple_posting(entry=entry,
                                       account=income_account,
                                       number=None,
                                       currency=None)
        entries.append(entry)

    printer.print_entries(entries)
示例#27
0
def parse_amount(s):
    s = s.replace(',', '.')
    return data.Amount(D(s), 'EUR')
示例#28
0
    def extract(self, filepath, existing):
        """Implements :py:meth:`beangulp.Importer.extract()`.

        This methods costructs a transaction for each data row using
        the date, narration, and amount required fields and the flag,
        payee, account, currency, tag, link, balance optional fields.

        Transaction metadata is constructed with :py:meth:`metadata()`
        and :py:meth:`finalize()` is called on each transaction. These
        can be redefine in subclasses. For customization that cannot
        be implemented with these two extension points, consider
        basing the importer on :py:class:`CSVReader` and implementing
        the :py:class:`~beangulp.Importer` interface with tailored
        processing of the data rows.

        """

        entries = []
        balances = defaultdict(list)
        default_account = self.account(filepath)

        # Compute the line number of the first data line.
        offset = int(self.skiplines) + bool(self.names) + 1

        for lineno, row in enumerate(self.read(filepath), offset):
            # Skip empty lines.
            if not row:
                continue

            tag = getattr(row, 'tag', None)
            tags = {tag} if tag else EMPTY

            link = getattr(row, 'link', None)
            links = {link} if link else EMPTY

            # This looks like an exercise in defensive programming
            # gone too far, but we do not want to depend on any field
            # being defined other than the essential ones.
            flag = getattr(row, 'flag', self.flag)
            payee = getattr(row, 'payee', None)
            account = getattr(row, 'account', default_account)
            currency = getattr(row, 'currency', self.currency)
            units = data.Amount(row.amount, currency)

            # Create a transaction.
            txn = data.Transaction(self.metadata(filepath, lineno, row),
                                   row.date, flag, payee, row.narration, tags, links, [
                                       data.Posting(account, units, None, None, None, None),
                                   ])

            # Apply user processing to the transaction.
            txn = self.finalize(txn, row)

            # Add the transaction to the output list.
            entries.append(txn)

            # Add balance to balances list.
            balance = getattr(row, 'balance', None)
            if balance is not None:
                date = row.date + datetime.timedelta(days=1)
                units = data.Amount(balance, currency)
                meta = data.new_metadata(filepath, lineno)
                balances[currency].append(data.Balance(meta, date, account, units, None, None))

        if not entries:
            return []

        # Reverse the list if the file is in descending order.
        if not entries[0].date <= entries[-1].date:
            entries.reverse()

        # Append balances.
        for currency, balances in balances.items():
            entries.append(max(balances, key=lambda x: x.date))

        return entries
示例#29
0
    def extract(self,
                file: cache._FileMemo,
                existing_entries=None) -> data.Entries:
        """
        """

        entries = []
        errors = []
        content = self._read_file(file.name)
        records = content.pop('records')
        currencies = content['currencies']
        account = content['account']

        if currencies:
            default_currency = currencies[0]
        else:
            default_currency = DEFAULT_CURRENCY

        row = 0
        for record in records:
            row += 1
            record = clean_record(record)
            if 'date' not in record or not record['date']:
                continue
            if 'amount' not in record or not record['amount']:
                continue

            narration = record.pop('narration', '')

            payee = record.pop('payee', '')

            tagstr = record.pop('tags', '')
            tags = set(re.split(r'\W+', tagstr)) if tagstr else set()

            # Date handling through dateparser
            date = dateparser.parse(record.pop('date'))
            if date:
                date = datetime.date(year=date.year,
                                     month=date.month,
                                     day=date.day)

            # Links
            linkstr = record.pop('links', '')
            links = set(re.split(r'\W+', linkstr)) if linkstr else set()

            meta = {
                'filename':
                '',
                'lineno':
                0,
                'document-sheet-row':
                f"{content['document']}/{content['tab']}/{row+1}"
            }

            # Need more protections
            amount = decimal.Decimal(record.pop('amount'))
            currency = record.pop('currency', default_currency)
            entry_account = record.pop('account')

            meta_target = {}
            meta_source = {}

            for k, v in record.items():
                if not v or not k:
                    continue
                clean_key = k.lower().replace('-', '').replace('_', '')
                if clean_key in ('transferaccount', 'targetaccount'):
                    meta_target['account'] = record[k]
                else:
                    meta_source[k] = v

            try:
                if not entry_account:
                    errors.append(
                        f"Skipping Record with Blank Account: {meta['document-sheet-row']}"
                    )
                    logger.warning(
                        f"Skipping Record with Blank Account: {meta['document-sheet-row']}"
                    )
                    continue

                entry = data.Transaction(
                    date=date,
                    narration=narration,
                    payee=payee,
                    tags=tags,
                    meta=meta,
                    links=links,
                    flag='*',
                    postings=[
                        data.Posting(account=account,
                                     units=data.Amount(amount, currency),
                                     cost=None,
                                     price=None,
                                     flag=None,
                                     meta=meta_source),
                        data.Posting(account=entry_account,
                                     units=data.Amount(-amount, currency),
                                     cost=None,
                                     price=None,
                                     flag=None,
                                     meta=meta_target)
                    ])
                entries.append(entry)
            except Exception as exc:
                logger.error(f"Error while parsing {record}", exc_info=exc)
                errors.append(str(exc))

        return entries
示例#30
0
    def _extract_stock_operations(self, filename, rd):
        rd = csv.reader(rd, dialect="fortuneo")

        entries = []
        header = True
        line_index = 0

        for row in rd:
            # Check header
            if header:
                if set(row) != set(STOCK_FIELDS):
                    raise InvalidFormatError()
                header = False

                line_index += 1

                continue

            if len(row) != len(STOCK_FIELDS):
                continue

            # Extract data

            row_date = datetime.strptime(row[3], "%d/%m/%Y")
            label = row[0].strip() + " - " + row[1]
            currency = row[9]

            stock_amount = data.Amount(D(row[4]), 'STK')
            stock_cost = position.Cost(
                number=D(row[5]),
                currency=currency,
                date=row_date.date(),
                label=None,
            )

            # Prepare the transaction

            meta = data.new_metadata(filename, line_index)

            txn = data.Transaction(
                meta=meta,
                date=row_date.date(),
                flag=flags.FLAG_OKAY,
                payee="",
                narration=label,
                tags=set(),
                links=set(),
                postings=[],
            )

            # Create the postings.

            txn.postings.append(
                data.Posting(account="Assets:Stock:STK",
                             units=stock_amount,
                             cost=stock_cost,
                             price=None,
                             flag=None,
                             meta=None))
            txn.postings.append(
                make_posting(self.broker_fees_account, -parse_amount(row[7])))
            txn.postings.append(
                make_posting(self.stock_account, parse_amount(row[8])))

            # Done

            entries.append(txn)

            line_index += 1

        return entries