reports: Balance.format() accepts zero argument.
This change has the same motivation as the recent change to BaseODS.balance_cell(): try to preserve currency information when it's available.
This commit is contained in:
parent
ae974b8e50
commit
7f3a26b555
3 changed files with 47 additions and 27 deletions
|
@ -463,10 +463,11 @@ class BalanceReport(BaseReport):
|
||||||
def _report(self, posts: AccrualPostings, index: int) -> Iterable[str]:
|
def _report(self, posts: AccrualPostings, index: int) -> Iterable[str]:
|
||||||
posts = posts.since_last_nonzero()
|
posts = posts.since_last_nonzero()
|
||||||
date_s = posts[0].meta.date.strftime('%Y-%m-%d')
|
date_s = posts[0].meta.date.strftime('%Y-%m-%d')
|
||||||
|
balance_s = posts.balance_at_cost().format(zero="Zero balance")
|
||||||
if index:
|
if index:
|
||||||
yield ""
|
yield ""
|
||||||
yield f"{posts.invoice}:"
|
yield f"{posts.invoice}:"
|
||||||
yield f" {posts.balance_at_cost()} outstanding since {date_s}"
|
yield f" {balance_s} outstanding since {date_s}"
|
||||||
|
|
||||||
|
|
||||||
class OutgoingReport(BaseReport):
|
class OutgoingReport(BaseReport):
|
||||||
|
|
|
@ -202,28 +202,35 @@ class Balance(Mapping[str, data.Amount]):
|
||||||
return self._all_amounts(op_func, self.tolerance)
|
return self._all_amounts(op_func, self.tolerance)
|
||||||
|
|
||||||
def format(self,
|
def format(self,
|
||||||
fmt: Optional[str]='#,#00.00 ¤¤',
|
fmt: Optional[str]='#,##0.00 ¤¤',
|
||||||
sep: str=', ',
|
sep: str=', ',
|
||||||
empty: str="Zero balance",
|
empty: str="Zero balance",
|
||||||
|
zero: Optional[str]=None,
|
||||||
tolerance: Optional[Decimal]=None,
|
tolerance: Optional[Decimal]=None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Formats the balance as a string with the given parameters
|
"""Formats the balance as a string with the given parameters
|
||||||
|
|
||||||
If the balance is zero (within tolerance), returns ``empty``.
|
If the balance is completely empty, return ``empty``.
|
||||||
Otherwise, returns a string with each amount in the balance formatted
|
If the balance is zero (within tolerance) and ``zero`` is specified,
|
||||||
|
return ``zero``.
|
||||||
|
Otherwise, return a string with each amount in the balance formatted
|
||||||
as ``fmt``, separated by ``sep``.
|
as ``fmt``, separated by ``sep``.
|
||||||
|
|
||||||
If you set ``fmt`` to None, amounts will be formatted according to the
|
If you set ``fmt`` to None, amounts will be formatted according to the
|
||||||
user's locale. The default format is Beancount's input format.
|
user's locale. The default format is Beancount's input format.
|
||||||
"""
|
"""
|
||||||
amounts = list(self.clean_copy(tolerance).values())
|
balance = self.clean_copy(tolerance) or self.copy(tolerance)
|
||||||
if not amounts:
|
if not balance:
|
||||||
return empty
|
return empty
|
||||||
amounts.sort(key=lambda amt: abs(amt.number), reverse=True)
|
elif zero is not None and balance.is_zero():
|
||||||
return sep.join(
|
return zero
|
||||||
babel.numbers.format_currency(amt.number, amt.currency, fmt)
|
else:
|
||||||
for amt in amounts
|
amounts = list(balance.values())
|
||||||
)
|
amounts.sort(key=lambda amt: (-abs(amt.number), amt.currency))
|
||||||
|
return sep.join(
|
||||||
|
babel.numbers.format_currency(amt.number, amt.currency, fmt)
|
||||||
|
for amt in amounts
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MutableBalance(Balance):
|
class MutableBalance(Balance):
|
||||||
|
|
|
@ -28,12 +28,12 @@ from conservancy_beancount.reports import core
|
||||||
|
|
||||||
DEFAULT_STRINGS = [
|
DEFAULT_STRINGS = [
|
||||||
({}, "Zero balance"),
|
({}, "Zero balance"),
|
||||||
({'JPY': 0, 'BRL': 0}, "Zero balance"),
|
({'JPY': 0, 'BRL': 0}, "0.00 BRL, 0 JPY"),
|
||||||
({'USD': '20.00'}, "20.00 USD"),
|
({'USD': '20.00'}, "20.00 USD"),
|
||||||
({'EUR': '50.00', 'GBP': '80.00'}, "80.00 GBP, 50.00 EUR"),
|
({'EUR': '50.00', 'GBP': '80.00'}, "80.00 GBP, 50.00 EUR"),
|
||||||
({'JPY': '-5500.00', 'BRL': '-8500.00'}, "-8,500.00 BRL, -5,500 JPY"),
|
({'JPY': '-5500.00', 'BRL': '-8500.00'}, "-8,500.00 BRL, -5,500 JPY"),
|
||||||
({'USD': 10, 'EUR': '.00015'}, "10.00 USD"),
|
({'USD': 10, 'EUR': '.00015'}, "10.00 USD"),
|
||||||
({'JPY': '-.00015'}, "Zero balance"),
|
({'JPY': '-.00015'}, "-0 JPY"),
|
||||||
]
|
]
|
||||||
|
|
||||||
TOLERANCES = [Decimal(n) for n in ['.1', '.01', '.001', 0]]
|
TOLERANCES = [Decimal(n) for n in ['.1', '.01', '.001', 0]]
|
||||||
|
@ -385,9 +385,9 @@ def test_format_defaults(mapping, expected):
|
||||||
|
|
||||||
@pytest.mark.parametrize('fmt,expected', [
|
@pytest.mark.parametrize('fmt,expected', [
|
||||||
('¤##0.0', '¥5000, -€1500.00'),
|
('¤##0.0', '¥5000, -€1500.00'),
|
||||||
('#,#00.0¤¤', '5,000JPY, -1,500.00EUR'),
|
('#,##0.0¤¤', '5,000JPY, -1,500.00EUR'),
|
||||||
('¤+##0.0;¤-##0.0', '¥+5000, €-1500.00'),
|
('¤+##0.0;¤-##0.0', '¥+5000, €-1500.00'),
|
||||||
('#,#00.0 ¤¤;(#,#00.0 ¤¤)', '5,000 JPY, (1,500.00 EUR)'),
|
('#,##0.0 ¤¤;(#,##0.0 ¤¤)', '5,000 JPY, (1,500.00 EUR)'),
|
||||||
])
|
])
|
||||||
def test_format_fmt(fmt, expected):
|
def test_format_fmt(fmt, expected):
|
||||||
amounts = [testutil.Amount(5000, 'JPY'), testutil.Amount(-1500, 'EUR')]
|
amounts = [testutil.Amount(5000, 'JPY'), testutil.Amount(-1500, 'EUR')]
|
||||||
|
@ -420,20 +420,32 @@ def test_format_empty(empty):
|
||||||
balance = core.Balance()
|
balance = core.Balance()
|
||||||
assert balance.format(empty=empty) == empty
|
assert balance.format(empty=empty) == empty
|
||||||
|
|
||||||
@pytest.mark.parametrize('tolerance', TOLERANCES)
|
@pytest.mark.parametrize('currency,fmt', itertools.product(
|
||||||
def test_str_tolerance(tolerance):
|
['USD', 'JPY', 'BRL'],
|
||||||
chf = testutil.Amount('.005', 'CHF')
|
[None, '¤#,##0.00', '###0.00 ¤¤'],
|
||||||
actual = str(core.Balance([chf], tolerance))
|
))
|
||||||
if tolerance > chf.number:
|
def test_format_zero_balance_fmt(currency, fmt):
|
||||||
assert actual == "Zero balance"
|
zero_amt = testutil.Amount(0, currency)
|
||||||
else:
|
nonzero_amt = testutil.Amount(9, currency)
|
||||||
assert actual == "00.00 CHF"
|
zero_bal = core.Balance([zero_amt])
|
||||||
|
nonzero_bal = core.Balance([nonzero_amt])
|
||||||
|
expected = nonzero_bal.format(fmt).replace('9', '0')
|
||||||
|
assert zero_bal.format(fmt) == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('currency,fmt', testutil.combine_values(
|
||||||
|
['USD', 'JPY', 'BRL'],
|
||||||
|
["N/A", "Zero", "ø"],
|
||||||
|
))
|
||||||
|
def test_format_zero_balance_zero_str(currency, fmt):
|
||||||
|
zero_amt = testutil.Amount(0, currency)
|
||||||
|
zero_bal = core.Balance([zero_amt])
|
||||||
|
assert zero_bal.format(zero=fmt) == fmt
|
||||||
|
|
||||||
@pytest.mark.parametrize('tolerance', TOLERANCES)
|
@pytest.mark.parametrize('tolerance', TOLERANCES)
|
||||||
def test_format_tolerance(tolerance):
|
def test_format_zero_balance_with_tolerance(tolerance):
|
||||||
chf = testutil.Amount('.005', 'CHF')
|
chf = testutil.Amount('.005', 'CHF')
|
||||||
actual = core.Balance([chf]).format(tolerance=tolerance)
|
actual = core.Balance([chf]).format(zero="ø", tolerance=tolerance)
|
||||||
if tolerance > chf.number:
|
if tolerance > chf.number:
|
||||||
assert actual == "Zero balance"
|
assert actual == "ø"
|
||||||
else:
|
else:
|
||||||
assert actual == "00.00 CHF"
|
assert actual == "0.00 CHF"
|
||||||
|
|
Loading…
Reference in a new issue