From d02e8768087a049224c5423e2b522120cd79fe4f Mon Sep 17 00:00:00 2001 From: Martin Michlmayr Date: Fri, 13 Nov 2020 14:36:06 +0800 Subject: [PATCH] Add price to cost when needed by ledger Ledger doesn't use the cost to determine an exchange (see https://github.com/ledger/ledger/issues/630), which means we have to add the price explicitly if ledger can't do an implicit conversion. This improves the logic that was added in commit e915d2b7 ("Support entries mixed with held-at-cost and price-conversion postings") to deal with one test case. Fixes #22 --- beancount2ledger/common.py | 31 +++++++++++++++++++++++++++++++ beancount2ledger/ledger.py | 25 +++++++++++++------------ docs/changelog.md | 1 + tests/hledger_test.py | 23 +++++++++++++++++++++++ tests/ledger_test.py | 23 +++++++++++++++++++++++ 5 files changed, 91 insertions(+), 12 deletions(-) diff --git a/beancount2ledger/common.py b/beancount2ledger/common.py index cceedc8..85cc1c1 100644 --- a/beancount2ledger/common.py +++ b/beancount2ledger/common.py @@ -179,3 +179,34 @@ def get_lineno(posting): meta = posting.meta or {} return meta.get("lineno", sys.maxsize) + + +def is_automatic_posting(posting): + """ + Is posting an automatic posting added by beancount? + """ + + if not posting.meta: + return False + if '__automatic__' in posting.meta and not '__residual__' in posting.meta: + return True + return False + + +def filter_display_postings(entry, dformat): + """ + Return entry without postings that wouldn't be displayed because + the display precision rounds them to 0.00. + """ + + postings = list(entry.postings) + new_postings = [] + for posting in postings: + pos_str = posting.units.to_string(dformat) + # Don't create a posting if the amount (rounded to the display + # precision) is 0.00. + amt = amount.from_string(pos_str) + if amt: + new_postings.append(posting) + entry = entry._replace(postings=new_postings) + return entry diff --git a/beancount2ledger/ledger.py b/beancount2ledger/ledger.py index ffe6c26..0360384 100644 --- a/beancount2ledger/ledger.py +++ b/beancount2ledger/ledger.py @@ -22,7 +22,7 @@ from .common import ROUNDING_ACCOUNT from .common import ledger_flag, ledger_str, quote_currency, postings_by_type, user_meta -from .common import set_default, get_lineno +from .common import set_default, get_lineno, is_automatic_posting, filter_display_postings class LedgerPrinter: @@ -82,6 +82,9 @@ def Transaction(self, entry): # a bug, so instead, we simply insert a rounding account to absorb the # residual and precisely balance the transaction. entry = interpolate.fill_residual_posting(entry, ROUNDING_ACCOUNT) + # Remove postings which wouldn't be displayed (due to precision + # rounding amounts to 0.00) + entry = filter_display_postings(entry, self.dformat) # Compute the string for the payee and narration line. strings = [] @@ -150,14 +153,9 @@ def Posting(self, posting, entry): # We don't use position.to_string() because that uses the same # dformat for amount and cost, but we want dformat from our # dcontext to format amounts to the right precision while - # retaining the full rpecision for costs. + # retaining the full precision for costs. if isinstance(posting.units, Amount): pos_str = posting.units.to_string(self.dformat) - # Don't create a posting if the amount (rounded to the display - # precision) is 0.00. - amt = amount.from_string(pos_str) - if not amt: - return # We can't use default=True, even though we're interested in the # cost details, but we have to add them ourselves in the format # expected by ledger. @@ -176,17 +174,20 @@ def Posting(self, posting, entry): else: # Figure out if we need to insert a price on a posting held at cost. # See https://groups.google.com/d/msg/ledger-cli/35hA0Dvhom0/WX8gY_5kHy0J - (postings_simple, postings_at_price, - postings_at_cost) = postings_by_type(entry) - + # and https://github.com/ledger/ledger/issues/630 + (postings_simple, _, __) = postings_by_type(entry) + postings_no_amount = [ + posting for posting in postings_simple + if not posting.units or is_automatic_posting(posting) + ] cost = posting.cost - if postings_at_price and postings_at_cost and cost: + if cost and not postings_no_amount and len(entry.postings) > 2: price_str = '@ {}'.format( amount.Amount(cost.number, cost.currency).to_string()) else: price_str = '' - if posting.meta and '__automatic__' in posting.meta and not '__residual__' in posting.meta: + if is_automatic_posting(posting): posting_str = f'{flag_posting}' else: # Width we have available for the amount: take width of diff --git a/docs/changelog.md b/docs/changelog.md index 92f3a9f..b06b6c2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,7 @@ * Add rounding postings only when required ([issue #9](https://github.com/beancount/beancount2ledger/issues/9)) * Avoid printing too much precision for a currency ([issue #21](https://github.com/beancount/beancount2ledger/issues/21)) * Avoid creating two or more postings with null amount ([issue #23](https://github.com/beancount/beancount2ledger/issues/23)) +* Add price to cost when needed by ledger ([issue #22](https://github.com/beancount/beancount2ledger/issues/22)) * Add config option `indent` * Show metadata with hledger output * Support setting auxiliary dates and posting dates from metadata ([issue #14](https://github.com/beancount/beancount2ledger/issues/14)) diff --git a/tests/hledger_test.py b/tests/hledger_test.py index 8d1fe0c..ae96d2b 100644 --- a/tests/hledger_test.py +++ b/tests/hledger_test.py @@ -534,6 +534,29 @@ def test_avoid_multiple_null_postings(self, entries, _, ___): Equity:Opening-balance """, result) + @loader.load_doc() + def test_add_price_when_needed(self, entries, _, ___): + """ + 2020-01-01 open Assets:Property + 2020-01-01 open Equity:Opening-Balance + + 2020-11-13 * "Cost without price" + Assets:Property 0.1 FOO {300.00 EUR} + Assets:Property 0.2 BAR {200.00 EUR} + Equity:Opening-Balance -70.00 EUR + """ + result = beancount2ledger.convert(entries, "hledger") + self.assertLines(r""" + account Assets:Property + + account Equity:Opening-Balance + + 2020-11-13 * Cost without price + Assets:Property 0.1 FOO @ 300.00 EUR + Assets:Property 0.2 BAR @ 200.00 EUR + Equity:Opening-Balance -70.00 EUR + """, result) + def test_example(self): """ Test converted example with hledger diff --git a/tests/ledger_test.py b/tests/ledger_test.py index 3345d3a..f1b3528 100644 --- a/tests/ledger_test.py +++ b/tests/ledger_test.py @@ -840,6 +840,29 @@ def test_avoid_multiple_null_postings(self, entries, _, ___): Equity:Opening-balance """, result) + @loader.load_doc() + def test_add_price_when_needed(self, entries, _, ___): + """ + 2020-01-01 open Assets:Property + 2020-01-01 open Equity:Opening-Balance + + 2020-11-13 * "We need to add @ price due to ledger bug #630" + Assets:Property 0.1 FOO {300.00 EUR} + Assets:Property 0.2 BAR {200.00 EUR} + Equity:Opening-Balance -70.00 EUR + """ + result = beancount2ledger.convert(entries) + self.assertLines(r""" + account Assets:Property + + account Equity:Opening-Balance + + 2020-11-13 * We need to add @ price due to ledger bug #630 + Assets:Property 0.1 FOO {300.00 EUR} @ 300.00 EUR + Assets:Property 0.2 BAR {200.00 EUR} @ 200.00 EUR + Equity:Opening-Balance -70.00 EUR + """, result) + def test_example(self): with tempfile.NamedTemporaryFile('w', suffix='.beancount',