reports: Balance classes support addition.
This commit is contained in:
		
							parent
							
								
									cc0656dde9
								
							
						
					
					
						commit
						069939b2d3
					
				
					 2 changed files with 110 additions and 8 deletions
				
			
		| 
						 | 
					@ -49,6 +49,7 @@ from ..beancount_types import (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DecimalCompat = data.DecimalCompat
 | 
					DecimalCompat = data.DecimalCompat
 | 
				
			||||||
 | 
					BalanceType = TypeVar('BalanceType', bound='Balance')
 | 
				
			||||||
RelatedType = TypeVar('RelatedType', bound='RelatedPostings')
 | 
					RelatedType = TypeVar('RelatedType', bound='RelatedPostings')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Balance(Mapping[str, data.Amount]):
 | 
					class Balance(Mapping[str, data.Amount]):
 | 
				
			||||||
| 
						 | 
					@ -69,6 +70,25 @@ class Balance(Mapping[str, data.Amount]):
 | 
				
			||||||
            currency: amount.number for currency, amount in source
 | 
					            currency: amount.number for currency, amount in source
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _add_amount(self,
 | 
				
			||||||
 | 
					                    currency_map: MutableMapping[str, Decimal],
 | 
				
			||||||
 | 
					                    amount: data.Amount,
 | 
				
			||||||
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            currency_map[amount.currency] += amount.number
 | 
				
			||||||
 | 
					        except KeyError:
 | 
				
			||||||
 | 
					            currency_map[amount.currency] = amount.number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _add_other(self,
 | 
				
			||||||
 | 
					                   currency_map: MutableMapping[str, Decimal],
 | 
				
			||||||
 | 
					                   other: Union[data.Amount, 'Balance'],
 | 
				
			||||||
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					        if isinstance(other, Balance):
 | 
				
			||||||
 | 
					            for amount in other.values():
 | 
				
			||||||
 | 
					                self._add_amount(currency_map, amount)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self._add_amount(currency_map, other)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __repr__(self) -> str:
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
        return f"{type(self).__name__}({self._currency_map!r})"
 | 
					        return f"{type(self).__name__}({self._currency_map!r})"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -80,6 +100,12 @@ class Balance(Mapping[str, data.Amount]):
 | 
				
			||||||
            (key, bc_amount.abs(amt)) for key, amt in self.items()
 | 
					            (key, bc_amount.abs(amt)) for key, amt in self.items()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __add__(self: BalanceType, other: Union[data.Amount, 'Balance']) -> BalanceType:
 | 
				
			||||||
 | 
					        retval_map = self._currency_map.copy()
 | 
				
			||||||
 | 
					        self._add_other(retval_map, other)
 | 
				
			||||||
 | 
					        return type(self)((code, data.Amount(number, code))
 | 
				
			||||||
 | 
					                          for code, number in retval_map.items())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __eq__(self, other: Any) -> bool:
 | 
					    def __eq__(self, other: Any) -> bool:
 | 
				
			||||||
        if (self.is_zero()
 | 
					        if (self.is_zero()
 | 
				
			||||||
            and isinstance(other, Balance)
 | 
					            and isinstance(other, Balance)
 | 
				
			||||||
| 
						 | 
					@ -149,11 +175,9 @@ class Balance(Mapping[str, data.Amount]):
 | 
				
			||||||
class MutableBalance(Balance):
 | 
					class MutableBalance(Balance):
 | 
				
			||||||
    __slots__ = ()
 | 
					    __slots__ = ()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_amount(self, amount: data.Amount) -> None:
 | 
					    def __iadd__(self: BalanceType, other: Union[data.Amount, Balance]) -> BalanceType:
 | 
				
			||||||
        try:
 | 
					        self._add_other(self._currency_map, other)
 | 
				
			||||||
            self._currency_map[amount.currency] += amount.number
 | 
					        return self
 | 
				
			||||||
        except KeyError:
 | 
					 | 
				
			||||||
            self._currency_map[amount.currency] = amount.number
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RelatedPostings(Sequence[data.Posting]):
 | 
					class RelatedPostings(Sequence[data.Posting]):
 | 
				
			||||||
| 
						 | 
					@ -234,7 +258,7 @@ class RelatedPostings(Sequence[data.Posting]):
 | 
				
			||||||
    def iter_with_balance(self) -> Iterator[Tuple[data.Posting, Balance]]:
 | 
					    def iter_with_balance(self) -> Iterator[Tuple[data.Posting, Balance]]:
 | 
				
			||||||
        balance = MutableBalance()
 | 
					        balance = MutableBalance()
 | 
				
			||||||
        for post in self:
 | 
					        for post in self:
 | 
				
			||||||
            balance.add_amount(post.units)
 | 
					            balance += post.units
 | 
				
			||||||
            yield post, balance
 | 
					            yield post, balance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def balance(self) -> Balance:
 | 
					    def balance(self) -> Balance:
 | 
				
			||||||
| 
						 | 
					@ -249,10 +273,10 @@ class RelatedPostings(Sequence[data.Posting]):
 | 
				
			||||||
        balance = MutableBalance()
 | 
					        balance = MutableBalance()
 | 
				
			||||||
        for post in self:
 | 
					        for post in self:
 | 
				
			||||||
            if post.cost is None:
 | 
					            if post.cost is None:
 | 
				
			||||||
                balance.add_amount(post.units)
 | 
					                balance += post.units
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                number = post.units.number * post.cost.number
 | 
					                number = post.units.number * post.cost.number
 | 
				
			||||||
                balance.add_amount(data.Amount(number, post.cost.currency))
 | 
					                balance += data.Amount(number, post.cost.currency)
 | 
				
			||||||
        return balance
 | 
					        return balance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def meta_values(self,
 | 
					    def meta_values(self,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -179,6 +179,84 @@ def test_eq(kwargs1, kwargs2, expected):
 | 
				
			||||||
    actual = bal1 == bal2
 | 
					    actual = bal1 == bal2
 | 
				
			||||||
    assert actual == expected
 | 
					    assert actual == expected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize('number,currency', {
 | 
				
			||||||
 | 
					    (50, 'USD'),
 | 
				
			||||||
 | 
					    (-50, 'USD'),
 | 
				
			||||||
 | 
					    (50000, 'BRL'),
 | 
				
			||||||
 | 
					    (-4000, 'BRL'),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					def test_add_amount(number, currency):
 | 
				
			||||||
 | 
					    start_amounts = testutil.balance_map(USD=500)
 | 
				
			||||||
 | 
					    start_bal = core.Balance(start_amounts)
 | 
				
			||||||
 | 
					    add_amount = testutil.Amount(number, currency)
 | 
				
			||||||
 | 
					    actual = start_bal + add_amount
 | 
				
			||||||
 | 
					    if currency == 'USD':
 | 
				
			||||||
 | 
					        assert len(actual) == 1
 | 
				
			||||||
 | 
					        assert actual['USD'] == testutil.Amount(500 + number)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        assert len(actual) == 2
 | 
				
			||||||
 | 
					        assert actual['USD'] == testutil.Amount(500)
 | 
				
			||||||
 | 
					        assert actual[currency] == add_amount
 | 
				
			||||||
 | 
					    assert start_bal == start_amounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize('number,currency', {
 | 
				
			||||||
 | 
					    (50, 'USD'),
 | 
				
			||||||
 | 
					    (-50, 'USD'),
 | 
				
			||||||
 | 
					    (50000, 'BRL'),
 | 
				
			||||||
 | 
					    (-4000, 'BRL'),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					def test_iadd_amount(number, currency):
 | 
				
			||||||
 | 
					    start_amounts = testutil.balance_map(USD=500)
 | 
				
			||||||
 | 
					    balance = core.MutableBalance(start_amounts)
 | 
				
			||||||
 | 
					    add_amount = testutil.Amount(number, currency)
 | 
				
			||||||
 | 
					    balance += add_amount
 | 
				
			||||||
 | 
					    if currency == 'USD':
 | 
				
			||||||
 | 
					        assert len(balance) == 1
 | 
				
			||||||
 | 
					        assert balance['USD'] == testutil.Amount(500 + number)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        assert len(balance) == 2
 | 
				
			||||||
 | 
					        assert balance['USD'] == testutil.Amount(500)
 | 
				
			||||||
 | 
					        assert balance[currency] == add_amount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize('balance_map_kwargs', [
 | 
				
			||||||
 | 
					    {},
 | 
				
			||||||
 | 
					    {'USD': 0},
 | 
				
			||||||
 | 
					    {'EUR': 10},
 | 
				
			||||||
 | 
					    {'JPY': 20, 'BRL': 30},
 | 
				
			||||||
 | 
					    {'EUR': -15},
 | 
				
			||||||
 | 
					    {'JPY': -25, 'BRL': -35},
 | 
				
			||||||
 | 
					    {'JPY': 40, 'USD': 0, 'EUR': -50},
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					def test_add_balance(balance_map_kwargs):
 | 
				
			||||||
 | 
					    start_numbers = {'USD': 500, 'BRL': 40000}
 | 
				
			||||||
 | 
					    start_bal = core.Balance(testutil.balance_map(**start_numbers))
 | 
				
			||||||
 | 
					    expect_numbers = start_numbers.copy()
 | 
				
			||||||
 | 
					    for code, number in balance_map_kwargs.items():
 | 
				
			||||||
 | 
					        expect_numbers[code] = expect_numbers.get(code, 0) + number
 | 
				
			||||||
 | 
					    add_bal = core.Balance(testutil.balance_map(**balance_map_kwargs))
 | 
				
			||||||
 | 
					    actual = start_bal + add_bal
 | 
				
			||||||
 | 
					    expected = core.Balance(testutil.balance_map(**expect_numbers))
 | 
				
			||||||
 | 
					    assert actual == expected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize('balance_map_kwargs', [
 | 
				
			||||||
 | 
					    {},
 | 
				
			||||||
 | 
					    {'USD': 0},
 | 
				
			||||||
 | 
					    {'EUR': 10},
 | 
				
			||||||
 | 
					    {'JPY': 20, 'BRL': 30},
 | 
				
			||||||
 | 
					    {'EUR': -15},
 | 
				
			||||||
 | 
					    {'JPY': -25, 'BRL': -35},
 | 
				
			||||||
 | 
					    {'JPY': 40, 'USD': 0, 'EUR': -50},
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					def test_iadd_balance(balance_map_kwargs):
 | 
				
			||||||
 | 
					    start_numbers = {'USD': 500, 'BRL': 40000}
 | 
				
			||||||
 | 
					    balance = core.MutableBalance(testutil.balance_map(**start_numbers))
 | 
				
			||||||
 | 
					    expect_numbers = start_numbers.copy()
 | 
				
			||||||
 | 
					    for code, number in balance_map_kwargs.items():
 | 
				
			||||||
 | 
					        expect_numbers[code] = expect_numbers.get(code, 0) + number
 | 
				
			||||||
 | 
					    balance += core.Balance(testutil.balance_map(**balance_map_kwargs))
 | 
				
			||||||
 | 
					    expected = core.Balance(testutil.balance_map(**expect_numbers))
 | 
				
			||||||
 | 
					    assert balance == expected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize('balance_map_kwargs,expected', DEFAULT_STRINGS)
 | 
					@pytest.mark.parametrize('balance_map_kwargs,expected', DEFAULT_STRINGS)
 | 
				
			||||||
def test_str(balance_map_kwargs, expected):
 | 
					def test_str(balance_map_kwargs, expected):
 | 
				
			||||||
    amounts = testutil.balance_map(**balance_map_kwargs)
 | 
					    amounts = testutil.balance_map(**balance_map_kwargs)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue