ledger: --account accepts a classification.
This makes it easier for users to specify a group of accounts.
This commit is contained in:
		
							parent
							
								
									fd3bd68326
								
							
						
					
					
						commit
						3f0b201d16
					
				
					 4 changed files with 67 additions and 25 deletions
				
			
		| 
						 | 
					@ -60,6 +60,7 @@ from typing import (
 | 
				
			||||||
    Mapping,
 | 
					    Mapping,
 | 
				
			||||||
    Optional,
 | 
					    Optional,
 | 
				
			||||||
    Sequence,
 | 
					    Sequence,
 | 
				
			||||||
 | 
					    Set,
 | 
				
			||||||
    TextIO,
 | 
					    TextIO,
 | 
				
			||||||
    Tuple,
 | 
					    Tuple,
 | 
				
			||||||
    Union,
 | 
					    Union,
 | 
				
			||||||
| 
						 | 
					@ -401,12 +402,13 @@ date was also not specified.
 | 
				
			||||||
""")
 | 
					""")
 | 
				
			||||||
    parser.add_argument(
 | 
					    parser.add_argument(
 | 
				
			||||||
        '--account', '-a',
 | 
					        '--account', '-a',
 | 
				
			||||||
        dest='sheet_names',
 | 
					        dest='accounts',
 | 
				
			||||||
        metavar='ACCOUNT',
 | 
					        metavar='ACCOUNT',
 | 
				
			||||||
        action='append',
 | 
					        action='append',
 | 
				
			||||||
        help="""Show this account in the report. You can specify this option
 | 
					        help="""Show this account in the report. You can specify this option
 | 
				
			||||||
multiple times. If not specified, the default set adapts to your search
 | 
					multiple times. You can specify a part of the account hierarchy, or an account
 | 
				
			||||||
criteria.
 | 
					classification from metadata. If not specified, the default set adapts to your
 | 
				
			||||||
 | 
					search criteria.
 | 
				
			||||||
""")
 | 
					""")
 | 
				
			||||||
    parser.add_argument(
 | 
					    parser.add_argument(
 | 
				
			||||||
        '--sheet-size', '--size',
 | 
					        '--sheet-size', '--size',
 | 
				
			||||||
| 
						 | 
					@ -436,18 +438,18 @@ metadata to match. A single ticket number is a shortcut for
 | 
				
			||||||
`rt-id=rt:NUMBER`. Any other word is a shortcut for `project=TERM`.
 | 
					`rt-id=rt:NUMBER`. Any other word is a shortcut for `project=TERM`.
 | 
				
			||||||
""")
 | 
					""")
 | 
				
			||||||
    args = parser.parse_args(arglist)
 | 
					    args = parser.parse_args(arglist)
 | 
				
			||||||
    if args.sheet_names is None:
 | 
					    if args.accounts is None:
 | 
				
			||||||
        if any(term.meta_key == 'project' for term in args.search_terms):
 | 
					        if any(term.meta_key == 'project' for term in args.search_terms):
 | 
				
			||||||
            args.sheet_names = [
 | 
					            args.accounts = [
 | 
				
			||||||
                'Income',
 | 
					                'Income',
 | 
				
			||||||
                'Expenses',
 | 
					                'Expenses',
 | 
				
			||||||
                'Assets:Receivable',
 | 
					                'Assets:Receivable',
 | 
				
			||||||
 | 
					                'Liabilities:Payable',
 | 
				
			||||||
                'Assets:Prepaid',
 | 
					                'Assets:Prepaid',
 | 
				
			||||||
                'Liabilities:UnearnedIncome',
 | 
					                'Liabilities:UnearnedIncome',
 | 
				
			||||||
                'Liabilities:Payable',
 | 
					 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            args.sheet_names = list(LedgerODS.ACCOUNT_COLUMNS)
 | 
					            args.accounts = list(LedgerODS.ACCOUNT_COLUMNS)
 | 
				
			||||||
    return args
 | 
					    return args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def diff_year(date: datetime.date, diff: int) -> datetime.date:
 | 
					def diff_year(date: datetime.date, diff: int) -> datetime.date:
 | 
				
			||||||
| 
						 | 
					@ -483,14 +485,26 @@ def main(arglist: Optional[Sequence[str]]=None,
 | 
				
			||||||
    returncode = 0
 | 
					    returncode = 0
 | 
				
			||||||
    books_loader = config.books_loader()
 | 
					    books_loader = config.books_loader()
 | 
				
			||||||
    if books_loader is None:
 | 
					    if books_loader is None:
 | 
				
			||||||
        entries, load_errors, _ = books.Loader.load_none(config.config_file_path())
 | 
					        entries, load_errors, options = books.Loader.load_none(config.config_file_path())
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        entries, load_errors, _ = books_loader.load_fy_range(args.start_date, args.stop_date)
 | 
					        entries, load_errors, options = books_loader.load_fy_range(args.start_date, args.stop_date)
 | 
				
			||||||
    for error in load_errors:
 | 
					    for error in load_errors:
 | 
				
			||||||
        bc_printer.print_error(error, file=stderr)
 | 
					        bc_printer.print_error(error, file=stderr)
 | 
				
			||||||
        returncode |= ReturnFlag.LOAD_ERRORS
 | 
					        returncode |= ReturnFlag.LOAD_ERRORS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    postings = data.Posting.from_entries(entries)
 | 
					    data.Account.load_from_books(entries, options)
 | 
				
			||||||
 | 
					    accounts: Set[data.Account] = set()
 | 
				
			||||||
 | 
					    sheet_names: Dict[str, None] = collections.OrderedDict()
 | 
				
			||||||
 | 
					    for acct_arg in args.accounts:
 | 
				
			||||||
 | 
					        for account in data.Account.iter_accounts(acct_arg):
 | 
				
			||||||
 | 
					            accounts.add(account)
 | 
				
			||||||
 | 
					            if not account.is_under(*sheet_names):
 | 
				
			||||||
 | 
					                new_sheet = account.is_under(*LedgerODS.ACCOUNT_COLUMNS)
 | 
				
			||||||
 | 
					                assert new_sheet is not None
 | 
				
			||||||
 | 
					                sheet_names[new_sheet] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    postings = (post for post in data.Posting.from_entries(entries)
 | 
				
			||||||
 | 
					                if post.account in accounts)
 | 
				
			||||||
    for search_term in args.search_terms:
 | 
					    for search_term in args.search_terms:
 | 
				
			||||||
        postings = search_term.filter_postings(postings)
 | 
					        postings = search_term.filter_postings(postings)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -500,7 +514,7 @@ def main(arglist: Optional[Sequence[str]]=None,
 | 
				
			||||||
    report = LedgerODS(
 | 
					    report = LedgerODS(
 | 
				
			||||||
        args.start_date,
 | 
					        args.start_date,
 | 
				
			||||||
        args.stop_date,
 | 
					        args.stop_date,
 | 
				
			||||||
        args.sheet_names,
 | 
					        list(sheet_names),
 | 
				
			||||||
        rt_wrapper,
 | 
					        rt_wrapper,
 | 
				
			||||||
        args.sheet_size,
 | 
					        args.sheet_size,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,16 @@
 | 
				
			||||||
2018-01-01 open Equity:OpeningBalance
 | 
					2018-01-01 open Equity:OpeningBalance
 | 
				
			||||||
2018-01-01 open Assets:Checking
 | 
					2018-01-01 open Assets:Checking
 | 
				
			||||||
 | 
					  classification: "Cash"
 | 
				
			||||||
2018-01-01 open Assets:Receivable:Accounts
 | 
					2018-01-01 open Assets:Receivable:Accounts
 | 
				
			||||||
 | 
					  classification: "Accounts receivable"
 | 
				
			||||||
2018-01-01 open Expenses:Other
 | 
					2018-01-01 open Expenses:Other
 | 
				
			||||||
 | 
					  classification: "Other expenses"
 | 
				
			||||||
2018-01-01 open Income:Other
 | 
					2018-01-01 open Income:Other
 | 
				
			||||||
 | 
					  classification: "Other income"
 | 
				
			||||||
2018-01-01 open Liabilities:CreditCard
 | 
					2018-01-01 open Liabilities:CreditCard
 | 
				
			||||||
 | 
					  classification: "Accounts payable"
 | 
				
			||||||
2018-01-01 open Liabilities:Payable:Accounts
 | 
					2018-01-01 open Liabilities:Payable:Accounts
 | 
				
			||||||
 | 
					  classification: "Accounts payable"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2018-02-28 * "Opening balance"
 | 
					2018-02-28 * "Opening balance"
 | 
				
			||||||
  Equity:OpeningBalance  -10,000 USD
 | 
					  Equity:OpeningBalance  -10,000 USD
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@
 | 
				
			||||||
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
					# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import collections
 | 
					import collections
 | 
				
			||||||
 | 
					import contextlib
 | 
				
			||||||
import copy
 | 
					import copy
 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
import io
 | 
					import io
 | 
				
			||||||
| 
						 | 
					@ -33,6 +34,8 @@ from conservancy_beancount import data
 | 
				
			||||||
from conservancy_beancount.reports import core
 | 
					from conservancy_beancount.reports import core
 | 
				
			||||||
from conservancy_beancount.reports import ledger
 | 
					from conservancy_beancount.reports import ledger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					clean_account_meta = contextlib.contextmanager(testutil.clean_account_meta)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Acct = data.Account
 | 
					Acct = data.Account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_ledger_load = bc_loader.load_file(testutil.test_path('books/ledger.beancount'))
 | 
					_ledger_load = bc_loader.load_file(testutil.test_path('books/ledger.beancount'))
 | 
				
			||||||
| 
						 | 
					@ -43,19 +46,11 @@ DEFAULT_REPORT_SHEETS = [
 | 
				
			||||||
    'Equity',
 | 
					    'Equity',
 | 
				
			||||||
    'Assets:Receivable',
 | 
					    'Assets:Receivable',
 | 
				
			||||||
    'Liabilities:Payable',
 | 
					    'Liabilities:Payable',
 | 
				
			||||||
    'Assets:PayPal',
 | 
					 | 
				
			||||||
    'Assets',
 | 
					    'Assets',
 | 
				
			||||||
    'Liabilities',
 | 
					    'Liabilities',
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
PROJECT_REPORT_SHEETS = [
 | 
					PROJECT_REPORT_SHEETS = DEFAULT_REPORT_SHEETS[:6]
 | 
				
			||||||
    'Balance',
 | 
					del PROJECT_REPORT_SHEETS[3]
 | 
				
			||||||
    'Income',
 | 
					 | 
				
			||||||
    'Expenses',
 | 
					 | 
				
			||||||
    'Assets:Receivable',
 | 
					 | 
				
			||||||
    'Assets:Prepaid',
 | 
					 | 
				
			||||||
    'Liabilities:UnearnedIncome',
 | 
					 | 
				
			||||||
    'Liabilities:Payable',
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
OVERSIZE_RE = re.compile(
 | 
					OVERSIZE_RE = re.compile(
 | 
				
			||||||
    r'^([A-Za-z0-9:]+) has ([0-9,]+) rows, over size ([0-9,]+)$'
 | 
					    r'^([A-Za-z0-9:]+) has ([0-9,]+) rows, over size ([0-9,]+)$'
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -275,6 +270,7 @@ def run_main(arglist, config=None):
 | 
				
			||||||
    arglist.insert(0, '--output-file=-')
 | 
					    arglist.insert(0, '--output-file=-')
 | 
				
			||||||
    output = io.BytesIO()
 | 
					    output = io.BytesIO()
 | 
				
			||||||
    errors = io.StringIO()
 | 
					    errors = io.StringIO()
 | 
				
			||||||
 | 
					    with clean_account_meta():
 | 
				
			||||||
        retcode = ledger.main(arglist, output, errors, config)
 | 
					        retcode = ledger.main(arglist, output, errors, config)
 | 
				
			||||||
    output.seek(0)
 | 
					    output.seek(0)
 | 
				
			||||||
    return retcode, output, errors
 | 
					    return retcode, output, errors
 | 
				
			||||||
| 
						 | 
					@ -292,6 +288,30 @@ def test_main(ledger_entries):
 | 
				
			||||||
    for _, expected in ExpectedPostings.group_by_account(postings):
 | 
					    for _, expected in ExpectedPostings.group_by_account(postings):
 | 
				
			||||||
        expected.check_report(ods, START_DATE, STOP_DATE)
 | 
					        expected.check_report(ods, START_DATE, STOP_DATE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize('acct_arg', [
 | 
				
			||||||
 | 
					    'Liabilities',
 | 
				
			||||||
 | 
					    'Accounts payable',
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					def test_main_account_limit(ledger_entries, acct_arg):
 | 
				
			||||||
 | 
					    retcode, output, errors = run_main([
 | 
				
			||||||
 | 
					        '-a', acct_arg,
 | 
				
			||||||
 | 
					        '-b', START_DATE.isoformat(),
 | 
				
			||||||
 | 
					        '-e', STOP_DATE.isoformat(),
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					    assert not errors.getvalue()
 | 
				
			||||||
 | 
					    assert retcode == 0
 | 
				
			||||||
 | 
					    ods = odf.opendocument.load(output)
 | 
				
			||||||
 | 
					    assert get_sheet_names(ods) == ['Balance', 'Liabilities']
 | 
				
			||||||
 | 
					    postings = data.Posting.from_entries(ledger_entries)
 | 
				
			||||||
 | 
					    for account, expected in ExpectedPostings.group_by_account(postings):
 | 
				
			||||||
 | 
					        should_find = account.startswith('Liabilities')
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            expected.check_report(ods, START_DATE, STOP_DATE)
 | 
				
			||||||
 | 
					        except NotFound:
 | 
				
			||||||
 | 
					            assert not should_find
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            assert should_find
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize('project,start_date,stop_date', [
 | 
					@pytest.mark.parametrize('project,start_date,stop_date', [
 | 
				
			||||||
    ('eighteen', START_DATE, MID_DATE.replace(day=30)),
 | 
					    ('eighteen', START_DATE, MID_DATE.replace(day=30)),
 | 
				
			||||||
    ('nineteen', MID_DATE, STOP_DATE),
 | 
					    ('nineteen', MID_DATE, STOP_DATE),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,9 @@ TESTS_DIR = Path(__file__).parent
 | 
				
			||||||
# it with different scopes. Typical usage looks like:
 | 
					# it with different scopes. Typical usage looks like:
 | 
				
			||||||
#   clean_account_meta = pytest.fixture([options])(testutil.clean_account_meta)
 | 
					#   clean_account_meta = pytest.fixture([options])(testutil.clean_account_meta)
 | 
				
			||||||
def clean_account_meta():
 | 
					def clean_account_meta():
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
        yield
 | 
					        yield
 | 
				
			||||||
 | 
					    finally:
 | 
				
			||||||
        data.Account.load_options_map(bc_options.OPTIONS_DEFAULTS)
 | 
					        data.Account.load_options_map(bc_options.OPTIONS_DEFAULTS)
 | 
				
			||||||
        data.Account._meta_map.clear()
 | 
					        data.Account._meta_map.clear()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue