Skip to content

Incorrect importing of balance assertion directives with tolerance specified #192

@NReilingh

Description

@NReilingh

By default, balance assertions have a tolerance of 1 unit of the least significant digit of the balance assertion, unless the unit is a whole number in which case the tolerance will be 0.

To avoid this confusing (to me) behavior, I want to specify all of my imported balance assertions with an explicit 0 tolerance, e.g.

2021-01-01 balance Assets:Cash 42.99 ~ 0 USD

I built the following beangulp v0.2.0 importer and have noticed certain behavior when I pass non-None values to data.Balance(tolerance=

from os import path
import decimal
import datetime
from beancount.core import ( data, amount )
from beangulp import ( mimetypes, Importer )
from beangulp.importers.csvbase import (
    Amount,
    Column,
    CSVReader,
    Date
)

class BalanceAssertionImporter(Importer):
    def account(self, filepath):
        return "Equity:OpeningBalances"

    def identify(self, filepath):
        mimetype, encoding = mimetypes.guess_type(filepath)
        if mimetype != "text/csv":
            return False
        with open(filepath, mode='r', encoding='utf-8-sig') as fd:
            head = fd.read(1024)

        return head.startswith(
            'Filename,account,assertdate,amount,curr,usdprice,pricedate'
        )

    def extract(self, filepath, existing):
        def build_directives(rows):
            count = 0
            for lineno, row in rows:
                directive = data.Balance(
                    meta=data.new_metadata(filepath,lineno,
                        {'document': row.attachment}),
                    date=row.date,
                    account=row.account,
                    amount=amount.Amount(row.amount, row.currency),
                    diff_amount=None,
                    tolerance=decimal.Decimal(0), ###################### <-- Problems #####################
                )
                yield directive
                count += 1
                if row.price and row.price_date:
                    pricedir = data.Price(
                        meta=data.new_metadata(filepath,lineno,
                            {'document': row.attachment}),
                        date=datetime.datetime.strptime(row.price_date, "%Y-%m-%d").date(),
                        currency=row.currency,
                        amount=amount.Amount(decimal.Decimal(row.price), "USD"),
                    )
                    yield pricedir
                    count += 1
            print(f"Importing {count} directives from {path.basename(filepath)}")

        class Reader(CSVReader):
            attachment = Column("Filename")
            account = Column("account")
            date = Date("assertdate", "%Y-%m-%d")
            amount = Amount("amount")
            currency = Column("curr")
            price = Column("usdprice")
            price_date = Column("pricedate")
        reader = Reader()
        return list(build_directives(enumerate(reader.read(filepath))))

Passing decimal.Decimal(0) results in no explicit tolerance being written to the output directives. I have not yet found where this is implemented, but I suspect there may be behavior that treats decimal zero the same as None. e.g.

Expected:

2021-01-01 balance Assets:Cash 42.99 ~ 0 USD

Actual:

2021-01-01 balance Assets:Cash 42.99 USD

Out of curiosity, I also tested passing decimal.Decimal(0.01) and discovered this very unexpected behavior:

Expected:

2021-01-01 balance Assets:Cash 42.99 ~ 0.01 USD

Actual:

2021-01-01 balance Assets:Cash 42.99 ~ 0.01000000000000000020816681711721685132943093776702880859375 USD

I believe this suggests a conversion at some point to float before being cast to string.

A semi-workaround is possible in that balance assertions can be overspecified with additional digits past the decimal point and this will change their tolerance. So in my CSV import example, instead of importing 42.99 I could import 42.990 and this will cause a tolerance of .001 to be used per the documented behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions