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,7 +270,8 @@ 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()
|
||||||
retcode = ledger.main(arglist, output, errors, config)
|
with clean_account_meta():
|
||||||
|
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,9 +44,11 @@ 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():
|
||||||
yield
|
try:
|
||||||
data.Account.load_options_map(bc_options.OPTIONS_DEFAULTS)
|
yield
|
||||||
data.Account._meta_map.clear()
|
finally:
|
||||||
|
data.Account.load_options_map(bc_options.OPTIONS_DEFAULTS)
|
||||||
|
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'
|
||||||
|
|
Loading…
Reference in a new issue