Changeset - 2c44cc8f5027
[Not reviewed]
0 4 0
Brett Smith - 4 years ago 2020-05-28 13:03:19
brettcsmith@brettcsmith.org
reports: Add Balance.format() method.
4 files changed with 73 insertions and 14 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/core.py
Show inline comments
...
 
@@ -16,16 +16,19 @@
 

	
 
import collections
 
import operator
 

	
 
from decimal import Decimal
 

	
 
import babel.numbers  # type:ignore[import]
 

	
 
from .. import data
 

	
 
from typing import (
 
    overload,
 
    Any,
 
    Callable,
 
    DefaultDict,
 
    Dict,
 
    Iterable,
 
    Iterator,
 
    List,
...
 
@@ -62,17 +65,13 @@ class Balance(Mapping[str, data.Amount]):
 
        }
 

	
 
    def __repr__(self) -> str:
 
        return f"{type(self).__name__}({self._currency_map!r})"
 

	
 
    def __str__(self) -> str:
 
        amounts = [amount for amount in self.values() if amount.number]
 
        if not amounts:
 
            return "Zero balance"
 
        amounts.sort(key=lambda amt: abs(amt.number), reverse=True)
 
        return ', '.join(str(amount) for amount in amounts)
 
        return self.format()
 

	
 
    def __eq__(self, other: Any) -> bool:
 
        if (self.is_zero()
 
            and isinstance(other, Balance)
 
            and other.is_zero()):
 
            return True
...
 
@@ -110,12 +109,32 @@ class Balance(Mapping[str, data.Amount]):
 
        return self._all_amounts(operator.ge, 0)
 

	
 
    def le_zero(self) -> bool:
 
        """Returns true if all amounts in the balance <= 0."""
 
        return self._all_amounts(operator.le, 0)
 

	
 
    def format(self,
 
               fmt: str='#,#00.00 ¤¤',
 
               sep: str=', ',
 
               empty: str="Zero balance",
 
    ) -> str:
 
        """Formats the balance as a string with the given parameters
 

	
 
        If the balance is zero, returns ``empty``. Otherwise, returns a string
 
        with each amount in the balance formatted as ``fmt``, separated by
 
        ``sep``.
 
        """
 
        amounts = [amount for amount in self.values() if amount.number]
 
        if not amounts:
 
            return empty
 
        amounts.sort(key=lambda amt: abs(amt.number), reverse=True)
 
        return sep.join(
 
            babel.numbers.format_currency(amt.number, amt.currency, fmt)
 
            for amt in amounts
 
        )
 

	
 

	
 
class MutableBalance(Balance):
 
    __slots__ = ()
 

	
 
    def add_amount(self, amount: data.Amount) -> None:
 
        try:
setup.py
Show inline comments
...
 
@@ -8,12 +8,13 @@ setup(
 
    version='1.0.6',
 
    author='Software Freedom Conservancy',
 
    author_email='info@sfconservancy.org',
 
    license='GNU AGPLv3+',
 

	
 
    install_requires=[
 
        'babel>=2.6',  # Debian:python3-babel
 
        'beancount>=2.2',  # Debian:beancount
 
        'PyYAML>=3.0',  # Debian:python3-yaml
 
        'regex',  # Debian:python3-regex
 
        'rt>=2.0',
 
    ],
 
    setup_requires=[
tests/test_reports_accrual.py
Show inline comments
...
 
@@ -246,13 +246,13 @@ def check_output(output, expect_patterns):
 
    testutil.check_lines_match(iter(output), expect_patterns)
 

	
 
@pytest.mark.parametrize('invoice,expected', [
 
    ('rt:505/5050', "Zero balance outstanding since 2020-05-05"),
 
    ('rt:510/5100', "Zero balance outstanding since 2020-05-10"),
 
    ('rt:510/6100', "-280.00 USD outstanding since 2020-06-10"),
 
    ('rt://ticket/515/attachments/5150', "1500.00 USD outstanding since 2020-05-15",),
 
    ('rt://ticket/515/attachments/5150', "1,500.00 USD outstanding since 2020-05-15",),
 
])
 
def test_balance_report(accrual_postings, invoice, expected):
 
    related = core.RelatedPostings(
 
        post for post in accrual_postings
 
        if post.meta.get('invoice') == invoice
 
        and post.account.is_under('Assets:Receivable', 'Liabilities:Payable')
...
 
@@ -389,13 +389,13 @@ def test_main_outgoing_report(arglist):
 
def test_main_balance_report(arglist):
 
    retcode, output, errors = run_main(arglist)
 
    assert not errors.getvalue()
 
    assert retcode == 0
 
    check_output(output, [
 
        r'\brt://ticket/515/attachments/5150:$',
 
        r'^\s+1500\.00 USD outstanding since 2020-05-15$',
 
        r'^\s+1,500\.00 USD outstanding since 2020-05-15$',
 
    ])
 

	
 
def test_main_no_books():
 
    check_main_fails([], testutil.TestConfig(), 1 | 8, [
 
        r':1: +no books to load in configuration\b',
 
    ])
tests/test_reports_balance.py
Show inline comments
...
 
@@ -21,12 +21,20 @@ from decimal import Decimal
 
import pytest
 

	
 
from . import testutil
 

	
 
from conservancy_beancount.reports import core
 

	
 
DEFAULT_STRINGS = [
 
    ({}, "Zero balance"),
 
    ({'JPY': 0, 'BRL': 0}, "Zero balance"),
 
    ({'USD': '20.00'}, "20.00 USD"),
 
    ({'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"),
 
]
 

	
 
def test_empty_balance():
 
    balance = core.Balance()
 
    assert not balance
 
    assert len(balance) == 0
 
    assert balance.is_zero()
 
    with pytest.raises(KeyError):
...
 
@@ -147,16 +155,47 @@ def test_neg(balance_map_kwargs):
 
def test_eq(kwargs1, kwargs2, expected):
 
    bal1 = core.Balance(testutil.balance_map(**kwargs1))
 
    bal2 = core.Balance(testutil.balance_map(**kwargs2))
 
    actual = bal1 == bal2
 
    assert actual == expected
 

	
 
@pytest.mark.parametrize('balance_map_kwargs,expected', [
 
    ({}, "Zero balance"),
 
    ({'JPY': 0, 'BRL': 0}, "Zero balance"),
 
    ({'USD': '20.00'}, "20.00 USD"),
 
    ({'EUR': '50.00', 'GBP': '80.00'}, "80.00 GBP, 50.00 EUR"),
 
    ({'JPY': '-55.00', 'BRL': '-85.00'}, "-85.00 BRL, -55.00 JPY"),
 
])
 
@pytest.mark.parametrize('balance_map_kwargs,expected', DEFAULT_STRINGS)
 
def test_str(balance_map_kwargs, expected):
 
    amounts = testutil.balance_map(**balance_map_kwargs)
 
    assert str(core.Balance(amounts.items())) == expected
 

	
 
@pytest.mark.parametrize('bal_kwargs,expected', DEFAULT_STRINGS)
 
def test_format_defaults(bal_kwargs, expected):
 
    amounts = testutil.balance_map(**bal_kwargs)
 
    assert core.Balance(amounts).format() == expected
 

	
 
@pytest.mark.parametrize('fmt,expected', [
 
    ('¤##0.0', '¥5000, -€1500.00'),
 
    ('#,#00.0¤¤', '5,000JPY, -1,500.00EUR'),
 
    ('¤+##0.0;¤-##0.0', '¥+5000, €-1500.00'),
 
    ('#,#00.0 ¤¤;(#,#00.0 ¤¤)', '5,000 JPY, (1,500.00 EUR)'),
 
])
 
def test_format_fmt(fmt, expected):
 
    amounts = testutil.balance_map(JPY=5000, EUR=-1500)
 
    balance = core.Balance(amounts)
 
    assert balance.format(fmt) == expected
 

	
 
@pytest.mark.parametrize('sep', [
 
    '; ',
 
    '—',
 
    '\0',
 
])
 
def test_format_sep(sep):
 
    bal_kwargs, expected = DEFAULT_STRINGS[-1]
 
    expected = expected.replace(', ', sep)
 
    amounts = testutil.balance_map(**bal_kwargs)
 
    balance = core.Balance(amounts)
 
    assert balance.format(sep=sep) == expected
 

	
 
@pytest.mark.parametrize('empty', [
 
    "N/A",
 
    "Zero",
 
    "ø",
 
])
 
def test_format_empty(empty):
 
    balance = core.Balance()
 
    assert balance.format(empty=empty) == empty
0 comments (0 inline, 0 general)