data: Add AccountMeta class.
This commit is contained in:
		
							parent
							
								
									a19b3fb6c4
								
							
						
					
					
						commit
						6a7815090c
					
				
					 5 changed files with 301 additions and 2 deletions
				
			
		| 
						 | 
					@ -38,22 +38,36 @@ if TYPE_CHECKING:
 | 
				
			||||||
    from . import errors as errormod
 | 
					    from . import errors as errormod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Account = bc_data.Account
 | 
					Account = bc_data.Account
 | 
				
			||||||
 | 
					Currency = bc_data.Currency
 | 
				
			||||||
 | 
					Meta = bc_data.Meta
 | 
				
			||||||
MetaKey = str
 | 
					MetaKey = str
 | 
				
			||||||
MetaValue = Any
 | 
					MetaValue = Any
 | 
				
			||||||
MetaValueEnum = str
 | 
					MetaValueEnum = str
 | 
				
			||||||
Posting = bc_data.Posting
 | 
					Posting = bc_data.Posting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Directive(NamedTuple):
 | 
					class Directive(NamedTuple):
 | 
				
			||||||
    meta: bc_data.Meta
 | 
					    meta: Meta
 | 
				
			||||||
    date: datetime.date
 | 
					    date: datetime.date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Close(Directive):
 | 
				
			||||||
 | 
					    account: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Error(NamedTuple):
 | 
					class Error(NamedTuple):
 | 
				
			||||||
    source: Mapping[MetaKey, MetaValue]
 | 
					    source: Mapping[MetaKey, MetaValue]
 | 
				
			||||||
    message: str
 | 
					    message: str
 | 
				
			||||||
    entry: Optional[Directive]
 | 
					    entry: Optional[Directive]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Open(Directive):
 | 
				
			||||||
 | 
					    account: str
 | 
				
			||||||
 | 
					    # Beancount's own type declarations say these aren't Optional, but in
 | 
				
			||||||
 | 
					    # practice these fields are both None if not defined in the directive.
 | 
				
			||||||
 | 
					    currencies: Optional[List[Currency]]
 | 
				
			||||||
 | 
					    booking: Optional[bc_data.Booking]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Transaction(Directive):
 | 
					class Transaction(Directive):
 | 
				
			||||||
    flag: bc_data.Flag
 | 
					    flag: bc_data.Flag
 | 
				
			||||||
    payee: Optional[str]
 | 
					    payee: Optional[str]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ import re
 | 
				
			||||||
from beancount.core import account as bc_account
 | 
					from beancount.core import account as bc_account
 | 
				
			||||||
from beancount.core import amount as bc_amount
 | 
					from beancount.core import amount as bc_amount
 | 
				
			||||||
from beancount.core import convert as bc_convert
 | 
					from beancount.core import convert as bc_convert
 | 
				
			||||||
 | 
					from beancount.core import data as bc_data
 | 
				
			||||||
from beancount.core import position as bc_position
 | 
					from beancount.core import position as bc_position
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from typing import (
 | 
					from typing import (
 | 
				
			||||||
| 
						 | 
					@ -45,9 +46,13 @@ from typing import (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .beancount_types import (
 | 
					from .beancount_types import (
 | 
				
			||||||
 | 
					    Close,
 | 
				
			||||||
 | 
					    Currency,
 | 
				
			||||||
    Directive,
 | 
					    Directive,
 | 
				
			||||||
 | 
					    Meta,
 | 
				
			||||||
    MetaKey,
 | 
					    MetaKey,
 | 
				
			||||||
    MetaValue,
 | 
					    MetaValue,
 | 
				
			||||||
 | 
					    Open,
 | 
				
			||||||
    Posting as BasePosting,
 | 
					    Posting as BasePosting,
 | 
				
			||||||
    Transaction,
 | 
					    Transaction,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -67,6 +72,78 @@ LINK_METADATA = frozenset([
 | 
				
			||||||
    'tax-statement',
 | 
					    'tax-statement',
 | 
				
			||||||
])
 | 
					])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountMeta(MutableMapping[MetaKey, MetaValue]):
 | 
				
			||||||
 | 
					    """Access account metadata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This class provides a consistent interface to all the metadata provided by
 | 
				
			||||||
 | 
					    Beancount's ``open`` and ``close`` directives: open and close dates,
 | 
				
			||||||
 | 
					    used currencies, booking method, and the metadata associated with each.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    For convenience, you can use this class as a Mapping to access the ``open``
 | 
				
			||||||
 | 
					    directive's metadata directly.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('_opening', '_closing')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, opening: Open, closing: Optional[Close]=None) -> None:
 | 
				
			||||||
 | 
					        self._opening = opening
 | 
				
			||||||
 | 
					        self._closing: Optional[Close] = None
 | 
				
			||||||
 | 
					        if closing is not None:
 | 
				
			||||||
 | 
					            self.add_closing(closing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_closing(self, closing: Close) -> None:
 | 
				
			||||||
 | 
					        if self._closing is not None and self._closing is not closing:
 | 
				
			||||||
 | 
					            raise ValueError(f"{self.account} already closed by {self._closing!r}")
 | 
				
			||||||
 | 
					        elif closing.account != self.account:
 | 
				
			||||||
 | 
					            raise ValueError(f"cannot close {self.account} with {closing.account}")
 | 
				
			||||||
 | 
					        elif closing.date < self.open_date:
 | 
				
			||||||
 | 
					            raise ValueError(f"close date {closing.date} predates open date {self.open_date}")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self._closing = closing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __iter__(self) -> Iterator[MetaKey]:
 | 
				
			||||||
 | 
					        return iter(self._opening.meta)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __len__(self) -> int:
 | 
				
			||||||
 | 
					        return len(self._opening.meta)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __getitem__(self, key: MetaKey) -> MetaValue:
 | 
				
			||||||
 | 
					        return self._opening.meta[key]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __setitem__(self, key: MetaKey, value: MetaValue) -> None:
 | 
				
			||||||
 | 
					        self._opening.meta[key] = value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __delitem__(self, key: MetaKey) -> None:
 | 
				
			||||||
 | 
					        del self._opening.meta[key]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account(self) -> 'Account':
 | 
				
			||||||
 | 
					        return Account(self._opening.account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def booking(self) -> Optional[bc_data.Booking]:
 | 
				
			||||||
 | 
					        return self._opening.booking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def close_date(self) -> Optional[datetime.date]:
 | 
				
			||||||
 | 
					        return None if self._closing is None else self._closing.date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def close_meta(self) -> Optional[Meta]:
 | 
				
			||||||
 | 
					        return None if self._closing is None else self._closing.meta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def currencies(self) -> Optional[Sequence[Currency]]:
 | 
				
			||||||
 | 
					        return self._opening.currencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def open_date(self) -> datetime.date:
 | 
				
			||||||
 | 
					        return self._opening.date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def open_meta(self) -> Meta:
 | 
				
			||||||
 | 
					        return self._opening.meta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Account(str):
 | 
					class Account(str):
 | 
				
			||||||
    """Account name string
 | 
					    """Account name string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -77,6 +154,33 @@ class Account(str):
 | 
				
			||||||
    __slots__ = ()
 | 
					    __slots__ = ()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    SEP = bc_account.sep
 | 
					    SEP = bc_account.sep
 | 
				
			||||||
 | 
					    _meta_map: MutableMapping[str, AccountMeta] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def load_opening(cls, opening: Open) -> None:
 | 
				
			||||||
 | 
					        cls._meta_map[opening.account] = AccountMeta(opening)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def load_closing(cls, closing: Close) -> None:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            cls._meta_map[closing.account].add_closing(closing)
 | 
				
			||||||
 | 
					        except KeyError:
 | 
				
			||||||
 | 
					            raise ValueError(
 | 
				
			||||||
 | 
					                f"tried to load {closing.account} close directive before open",
 | 
				
			||||||
 | 
					            ) from None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def load_openings_and_closings(cls, entries: Iterable[Directive]) -> None:
 | 
				
			||||||
 | 
					        for entry in entries:
 | 
				
			||||||
 | 
					            # type ignores because Beancount's directives aren't type-checkable.
 | 
				
			||||||
 | 
					            if isinstance(entry, bc_data.Open):
 | 
				
			||||||
 | 
					                cls.load_opening(entry)  # type:ignore[arg-type]
 | 
				
			||||||
 | 
					            elif isinstance(entry, bc_data.Close):
 | 
				
			||||||
 | 
					                cls.load_closing(entry)  # type:ignore[arg-type]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def meta(self) -> AccountMeta:
 | 
				
			||||||
 | 
					        return self._meta_map[self]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_cash_equivalent(self) -> bool:
 | 
					    def is_cash_equivalent(self) -> bool:
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,8 +16,32 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import testutil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from datetime import date as Date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from beancount.core.data import Open, Close, Booking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from conservancy_beancount import data
 | 
					from conservancy_beancount import data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					clean_account_meta = pytest.fixture()(testutil.clean_account_meta)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def check_account_meta(acct_meta, opening, closing=None):
 | 
				
			||||||
 | 
					    if isinstance(acct_meta, str):
 | 
				
			||||||
 | 
					        acct_meta = data.Account(acct_meta).meta
 | 
				
			||||||
 | 
					    assert acct_meta == opening.meta
 | 
				
			||||||
 | 
					    assert acct_meta.account == opening.account
 | 
				
			||||||
 | 
					    assert acct_meta.booking == opening.booking
 | 
				
			||||||
 | 
					    assert acct_meta.currencies == opening.currencies
 | 
				
			||||||
 | 
					    assert acct_meta.open_date == opening.date
 | 
				
			||||||
 | 
					    assert acct_meta.open_meta == opening.meta
 | 
				
			||||||
 | 
					    if closing is None:
 | 
				
			||||||
 | 
					        assert acct_meta.close_date is None
 | 
				
			||||||
 | 
					        assert acct_meta.close_meta is None
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        assert acct_meta.close_date == closing.date
 | 
				
			||||||
 | 
					        assert acct_meta.close_meta == closing.meta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize('acct_name,under_arg,expected', [
 | 
					@pytest.mark.parametrize('acct_name,under_arg,expected', [
 | 
				
			||||||
    ('Expenses:Tax:Sales', 'Expenses:Tax:Sales:', False),
 | 
					    ('Expenses:Tax:Sales', 'Expenses:Tax:Sales:', False),
 | 
				
			||||||
    ('Expenses:Tax:Sales', 'Expenses:Tax:Sales', True),
 | 
					    ('Expenses:Tax:Sales', 'Expenses:Tax:Sales', True),
 | 
				
			||||||
| 
						 | 
					@ -205,3 +229,41 @@ def test_root_part(acct_name):
 | 
				
			||||||
    assert account.root_part() == parts[0]
 | 
					    assert account.root_part() == parts[0]
 | 
				
			||||||
    assert account.root_part(1) == parts[0]
 | 
					    assert account.root_part(1) == parts[0]
 | 
				
			||||||
    assert account.root_part(2) == ':'.join(parts[:2])
 | 
					    assert account.root_part(2) == ':'.join(parts[:2])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_load_opening(clean_account_meta):
 | 
				
			||||||
 | 
					    opening = Open({'lineno': 210}, Date(2010, 2, 1), 'Assets:Cash', None, None)
 | 
				
			||||||
 | 
					    data.Account.load_opening(opening)
 | 
				
			||||||
 | 
					    check_account_meta('Assets:Cash', opening)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_load_closing(clean_account_meta):
 | 
				
			||||||
 | 
					    name = 'Assets:Checking'
 | 
				
			||||||
 | 
					    opening = Open({'lineno': 230}, Date(2010, 10, 1), name, None, None)
 | 
				
			||||||
 | 
					    closing = Close({'lineno': 235}, Date(2010, 11, 1), name)
 | 
				
			||||||
 | 
					    data.Account.load_opening(opening)
 | 
				
			||||||
 | 
					    data.Account.load_closing(closing)
 | 
				
			||||||
 | 
					    check_account_meta(name, opening, closing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_load_closing_without_opening(clean_account_meta):
 | 
				
			||||||
 | 
					    closing = Close({'lineno': 245}, Date(2010, 3, 1), 'Assets:Cash')
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        data.Account.load_closing(closing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_load_openings_and_closings(clean_account_meta):
 | 
				
			||||||
 | 
					    entries = [
 | 
				
			||||||
 | 
					        Open({'lineno': 1, 'income-type': 'Donations'},
 | 
				
			||||||
 | 
					             Date(2000, 3, 1), 'Income:Donations', None, None),
 | 
				
			||||||
 | 
					        Open({'lineno': 2},
 | 
				
			||||||
 | 
					             Date(2000, 3, 1), 'Income:Other', None, None),
 | 
				
			||||||
 | 
					        Open({'lineno': 3, 'asset-type': 'Cash equivalent'},
 | 
				
			||||||
 | 
					             Date(2000, 4, 1), 'Assets:Checking', ['USD', 'EUR'], Booking.STRICT),
 | 
				
			||||||
 | 
					        testutil.Transaction(date=Date(2000, 4, 10), postings=[
 | 
				
			||||||
 | 
					            ('Income:Donations', -10),
 | 
				
			||||||
 | 
					            ('Assets:Checking', 10),
 | 
				
			||||||
 | 
					        ]),
 | 
				
			||||||
 | 
					        Close({'lineno': 30, 'why': 'Changed banks'},
 | 
				
			||||||
 | 
					              Date(2000, 5, 1), 'Assets:Checking')
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    data.Account.load_openings_and_closings(iter(entries))
 | 
				
			||||||
 | 
					    check_account_meta('Income:Donations', entries[0])
 | 
				
			||||||
 | 
					    check_account_meta('Income:Other', entries[1])
 | 
				
			||||||
 | 
					    check_account_meta('Assets:Checking', entries[2], entries[-1])
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										113
									
								
								tests/test_data_account_meta.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								tests/test_data_account_meta.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					"""Test AccountMeta class"""
 | 
				
			||||||
 | 
					# Copyright © 2020  Brett Smith
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# This program is free software: you can redistribute it and/or modify
 | 
				
			||||||
 | 
					# it under the terms of the GNU Affero General Public License as published by
 | 
				
			||||||
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
 | 
					# (at your option) any later version.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# This program is distributed in the hope that it will be useful,
 | 
				
			||||||
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
 | 
					# GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# You should have received a copy of the GNU Affero General Public License
 | 
				
			||||||
 | 
					# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import itertools
 | 
				
			||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from datetime import date as Date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from beancount.core.data import Open, Close, Booking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from conservancy_beancount import data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_attributes():
 | 
				
			||||||
 | 
					    open_date = Date(2019, 6, 1)
 | 
				
			||||||
 | 
					    name = 'Assets:Bank:Checking'
 | 
				
			||||||
 | 
					    currencies = ['USD', 'EUR']
 | 
				
			||||||
 | 
					    booking = Booking.STRICT
 | 
				
			||||||
 | 
					    actual = data.AccountMeta(Open({}, open_date, name, list(currencies), booking))
 | 
				
			||||||
 | 
					    assert actual.open_date == open_date
 | 
				
			||||||
 | 
					    assert actual.account == name
 | 
				
			||||||
 | 
					    assert isinstance(actual.account, data.Account)
 | 
				
			||||||
 | 
					    assert actual.currencies == currencies
 | 
				
			||||||
 | 
					    assert actual.booking == booking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_mapping():
 | 
				
			||||||
 | 
					    src_meta = {'filename': 'maptest', 'lineno': 10}
 | 
				
			||||||
 | 
					    actual = data.AccountMeta(Open(
 | 
				
			||||||
 | 
					        src_meta.copy(), Date(2019, 6, 1), 'Income:Donations', None, None,
 | 
				
			||||||
 | 
					    ))
 | 
				
			||||||
 | 
					    assert len(actual) == 2
 | 
				
			||||||
 | 
					    assert set(actual) == set(src_meta)  # Test __iter__
 | 
				
			||||||
 | 
					    for key, expected in src_meta.items():
 | 
				
			||||||
 | 
					        assert actual[key] == expected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_close_attributes_without_closing():
 | 
				
			||||||
 | 
					    actual = data.AccountMeta(Open(
 | 
				
			||||||
 | 
					        {}, Date(2019, 6, 1), 'Assets:Cash', None, None,
 | 
				
			||||||
 | 
					    ))
 | 
				
			||||||
 | 
					    assert actual.close_date is None
 | 
				
			||||||
 | 
					    assert actual.close_meta is None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_close_at_init():
 | 
				
			||||||
 | 
					    src_meta = {'filename': 'initclose', 'lineno': 50}
 | 
				
			||||||
 | 
					    close_date = Date(2020, 6, 1)
 | 
				
			||||||
 | 
					    name = 'Assets:Bank:MoneyMarket'
 | 
				
			||||||
 | 
					    actual = data.AccountMeta(
 | 
				
			||||||
 | 
					        Open({}, Date(2019, 6, 1), name, None, None),
 | 
				
			||||||
 | 
					        Close(src_meta.copy(), close_date, name),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert actual.close_date == close_date
 | 
				
			||||||
 | 
					    assert actual.close_meta == src_meta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_add_closing():
 | 
				
			||||||
 | 
					    src_meta = {'filename': 'laterclose', 'lineno': 65}
 | 
				
			||||||
 | 
					    close_date = Date(2020, 1, 1)
 | 
				
			||||||
 | 
					    name = 'Assets:Bank:EUR'
 | 
				
			||||||
 | 
					    actual = data.AccountMeta(Open({}, Date(2019, 6, 1), name, None, None))
 | 
				
			||||||
 | 
					    assert actual.close_date is None
 | 
				
			||||||
 | 
					    assert actual.close_meta is None
 | 
				
			||||||
 | 
					    actual.add_closing(Close(src_meta.copy(), close_date, name))
 | 
				
			||||||
 | 
					    assert actual.close_date == close_date
 | 
				
			||||||
 | 
					    assert actual.close_meta == src_meta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_add_closing_already_inited():
 | 
				
			||||||
 | 
					    name = 'Assets:Bank:Savings'
 | 
				
			||||||
 | 
					    actual = data.AccountMeta(
 | 
				
			||||||
 | 
					        Open({}, Date(2019, 6, 1), name, None, None),
 | 
				
			||||||
 | 
					        Close({}, Date(2019, 7, 1), name),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        actual.add_closing(Close({}, Date(2019, 8, 1), name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_add_closing_called_twice():
 | 
				
			||||||
 | 
					    name = 'Assets:Bank:FX'
 | 
				
			||||||
 | 
					    actual = data.AccountMeta(Open({}, Date(2019, 6, 1), name, None, None))
 | 
				
			||||||
 | 
					    actual.add_closing(Close({}, Date(2019, 7, 1), name))
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        actual.add_closing(Close({}, Date(2019, 8, 1), name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize('close_date,close_name', [
 | 
				
			||||||
 | 
					    (Date(2020, 6, 1), 'Income:Grants'),  # Account name doesn't match
 | 
				
			||||||
 | 
					    (Date(2010, 6, 1), 'Income:Donations'),  # Close predates Open
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					def test_bad_closing_at_init(close_date, close_name):
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        data.AccountMeta(
 | 
				
			||||||
 | 
					            Open({}, Date(2019, 6, 1), 'Income:Donations', None, None),
 | 
				
			||||||
 | 
					            Close({}, close_date, close_name),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize('close_date,close_name', [
 | 
				
			||||||
 | 
					    (Date(2020, 6, 1), 'Income:Grants'),  # Account name doesn't match
 | 
				
			||||||
 | 
					    (Date(2010, 6, 1), 'Income:Donations'),  # Close predates Open
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					def test_add_closing_wrong_account(close_date, close_name):
 | 
				
			||||||
 | 
					    actual = data.AccountMeta(
 | 
				
			||||||
 | 
					        Open({}, Date(2019, 6, 1), 'Income:Donations', None, None),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        actual.add_closing(Close({}, close_date, close_name))
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,7 @@ from decimal import Decimal
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
from typing import Any, Optional, NamedTuple
 | 
					from typing import Any, Optional, NamedTuple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from conservancy_beancount import books, rtutil
 | 
					from conservancy_beancount import books, data, rtutil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
EXTREME_FUTURE_DATE = datetime.date(datetime.MAXYEAR, 12, 30)
 | 
					EXTREME_FUTURE_DATE = datetime.date(datetime.MAXYEAR, 12, 30)
 | 
				
			||||||
FUTURE_DATE = datetime.date.today() + datetime.timedelta(days=365 * 99)
 | 
					FUTURE_DATE = datetime.date.today() + datetime.timedelta(days=365 * 99)
 | 
				
			||||||
| 
						 | 
					@ -39,6 +39,12 @@ FY_MID_DATE = datetime.date(2020, 9, 1)
 | 
				
			||||||
PAST_DATE = datetime.date(2000, 1, 1)
 | 
					PAST_DATE = datetime.date(2000, 1, 1)
 | 
				
			||||||
TESTS_DIR = Path(__file__).parent
 | 
					TESTS_DIR = Path(__file__).parent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This function is primarily used as a fixture, but different test files use
 | 
				
			||||||
 | 
					# it with different scopes. Typical usage looks like:
 | 
				
			||||||
 | 
					#   clean_account_meta = pytest.fixture([options])(testutil.clean_account_meta)
 | 
				
			||||||
 | 
					def clean_account_meta():
 | 
				
			||||||
 | 
					    data.Account._meta_map.clear()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _ods_cell_value_type(cell):
 | 
					def _ods_cell_value_type(cell):
 | 
				
			||||||
    assert cell.tagName == 'table:table-cell'
 | 
					    assert cell.tagName == 'table:table-cell'
 | 
				
			||||||
    return cell.getAttribute('valuetype')
 | 
					    return cell.getAttribute('valuetype')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue