Changeset - f66460f34304
[Not reviewed]
0 4 0
Brett Smith - 4 years ago 2020-05-28 13:05:18
brettcsmith@brettcsmith.org
accrual: Outgoing report includes total at cost.
4 files changed with 39 insertions and 4 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/accrual.py
Show inline comments
...
 
@@ -287,38 +287,46 @@ def outgoing_report(groups: PostGroups,
 
            rt_requestor = None
 
        if rt_requestor is None:
 
            requestor = ''
 
            requestor_name = ''
 
        else:
 
            requestor_name = (
 
                rt_requestor.get('RealName')
 
                or ticket.get('CF.{payment-to}')
 
                or ''
 
            )
 
            requestor = f'{requestor_name} <{rt_requestor["EmailAddress"]}>'.strip()
 

	
 
        raw_balance = -related.balance()
 
        cost_balance = -related.balance_at_cost()
 
        cost_balance_s = cost_balance.format(None)
 
        if raw_balance == cost_balance:
 
            balance_s = cost_balance_s
 
        else:
 
            balance_s = f'{raw_balance} ({cost_balance_s})'
 

	
 
        contract_links = related.all_meta_links('contract')
 
        if contract_links:
 
            contract_s = ' , '.join(rt_wrapper.iter_urls(
 
                contract_links, missing_fmt='<BROKEN RT LINK: {}>',
 
            ))
 
        else:
 
            contract_s = "NO CONTRACT GOVERNS THIS TRANSACTION"
 
        projects = [v for v in related.meta_values('project')
 
                    if isinstance(v, str)]
 

	
 
        print(
 
            "PAYMENT FOR APPROVAL:",
 
            f"REQUESTOR: {requestor}",
 
            f"TOTAL TO PAY: {-related.balance()}",
 
            f"TOTAL TO PAY: {balance_s}",
 
            f"AGREEMENT: {contract_s}",
 
            f"PAYMENT TO: {ticket.get('CF.{payment-to}') or requestor_name}",
 
            f"PAYMENT METHOD: {ticket.get('CF.{payment-method}', '')}",
 
            f"PROJECT: {', '.join(projects)}",
 
            "\nBEANCOUNT ENTRIES:\n",
 
            sep='\n', file=out_file,
 
        )
 

	
 
        last_txn: Optional[Transaction] = None
 
        for post in related:
 
            txn = post.meta.txn
 
            if txn is not last_txn:
setup.py
Show inline comments
 
#!/usr/bin/env python3
 

	
 
from setuptools import setup
 

	
 
setup(
 
    name='conservancy_beancount',
 
    description="Plugin, library, and reports for reading Conservancy's books",
 
    version='1.0.6',
 
    version='1.0.7',
 
    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/books/accruals.beancount
Show inline comments
...
 
@@ -64,12 +64,19 @@
 
  rt-id: "rt:510"
 
  invoice: "rt:510/6100"
 
  contract: "rt:510/4000"
 
  Expenses:Services:Legal  220.00 USD
 
  Liabilities:Payable:Accounts  -220.00 USD
 

	
 
2020-06-12 * "Lawyer" "Additional legal fees for May"
 
  rt-id: "rt:510"
 
  invoice: "rt:510/6100"
 
  contract: "rt:510/4000"
 
  Expenses:FilingFees  60.00 USD
 
  Liabilities:Payable:Accounts  -60.00 USD
 

	
 
2020-06-18 * "EuroGov" "European legal fees"
 
  rt-id: "rt:520"
 
  invoice: "rt:520/5200"
 
  contract: "rt:520/5220"
 
  Liabilities:Payable:Accounts  -1,000 EUR {1.100 USD}
 
  Expenses:FilingFees  1,000 EUR {1.100 USD}
tests/test_reports_accrual.py
Show inline comments
...
 
@@ -59,24 +59,25 @@ class RTClient(testutil.RTClient):
 
        '44': [
 
            ('440', 'invoice feb.csv', 'text/csv', '40.4k'),
 
        ],
 
        '490': [],
 
        '505': [],
 
        '510': [
 
            ('4000', 'contract.pdf', 'application/pdf', '1.4m'),
 
            ('5100', 'invoice april.pdf', 'application/pdf', '1.5m'),
 
            ('5105', 'payment.png', 'image/png', '51.5k'),
 
            ('6100', 'invoice may.pdf', 'application/pdf', '1.6m'),
 
        ],
 
        '515': [],
 
        '520': [],
 
    }
 

	
 

	
 
@pytest.fixture
 
def accrual_entries():
 
    return copy.deepcopy(_accruals_load[0])
 

	
 
@pytest.fixture
 
def accrual_postings():
 
    entries = copy.deepcopy(_accruals_load[0])
 
    return data.Posting.from_entries(entries)
 

	
...
 
@@ -272,25 +273,25 @@ def test_outgoing_report(accrual_postings):
 
    errors = io.StringIO()
 
    rt_client = RTClient()
 
    rt_cache = rtutil.RT(rt_client)
 
    accrual.outgoing_report({invoice: related}, output, errors, rt_client, rt_cache)
 
    assert not errors.getvalue()
 
    rt_url = rt_client.DEFAULT_URL[:-9]
 
    rt_id_url = rf'\b{re.escape(f"{rt_url}Ticket/Display.html?id=510")}\b'
 
    contract_url = rf'\b{re.escape(f"{rt_url}Ticket/Attachment/4000/4000/contract.pdf")}\b'
 
    print(output.getvalue())
 
    check_output(output, [
 
        r'^PAYMENT FOR APPROVAL:$',
 
        r'^REQUESTOR: Mx\. 510 <mx510@example\.org>$',
 
        r'^TOTAL TO PAY: 280\.00 USD$',
 
        r'^TOTAL TO PAY: \$280\.00$',
 
        fr'^AGREEMENT: {contract_url}',
 
        r'^PAYMENT TO: Hon\. Mx\. 510$',
 
        r'^PAYMENT METHOD: payment method 510$',
 
        r'^BEANCOUNT ENTRIES:$',
 
        # For each transaction, check for the date line, a metadata, and the
 
        # Expenses posting.
 
        r'^\s*2020-06-10\s',
 
        fr'^\s+rt-id: "{rt_id_url}"$',
 
        r'^\s+Expenses:Services:Legal\s+220\.00 USD$',
 
        r'^\s*2020-06-12\s',
 
        fr'^\s+contract: "{contract_url}"$',
 
        r'^\s+Expenses:FilingFees\s+60\.00 USD$',
...
 
@@ -307,24 +308,43 @@ def test_outgoing_report_custom_field_fallbacks(accrual_postings):
 
    errors = io.StringIO()
 
    rt_client = RTClient(want_cfs=False)
 
    rt_cache = rtutil.RT(rt_client)
 
    accrual.outgoing_report({invoice: related}, output, errors, rt_client, rt_cache)
 
    assert not errors.getvalue()
 
    check_output(output, [
 
        r'^PAYMENT FOR APPROVAL:$',
 
        r'^REQUESTOR: <mx510@example\.org>$',
 
        r'^PAYMENT TO:\s*$',
 
        r'^PAYMENT METHOD:\s*$',
 
    ])
 

	
 
def test_outgoing_report_fx_amounts(accrual_postings):
 
    invoice = 'rt:520/5200'
 
    related = core.RelatedPostings(
 
        post for post in accrual_postings
 
        if post.meta.get('invoice') == invoice
 
        and post.account.is_under('Assets:Receivable', 'Liabilities:Payable')
 
    )
 
    output = io.StringIO()
 
    errors = io.StringIO()
 
    rt_client = RTClient()
 
    rt_cache = rtutil.RT(rt_client)
 
    accrual.outgoing_report({invoice: related}, output, errors, rt_client, rt_cache)
 
    assert not errors.getvalue()
 
    check_output(output, [
 
        r'^PAYMENT FOR APPROVAL:$',
 
        r'^REQUESTOR: Mx\. 520 <mx520@example\.org>$',
 
        r'^TOTAL TO PAY: 1,000\.00 EUR \(\$1,100.00\)$',
 
    ])
 

	
 
def run_main(arglist, config=None):
 
    if config is None:
 
        config = testutil.TestConfig(
 
            books_path=testutil.test_path('books/accruals.beancount'),
 
            rt_client=RTClient(),
 
        )
 
    output = io.StringIO()
 
    errors = io.StringIO()
 
    retcode = accrual.main(arglist, output, errors, config)
 
    return retcode, output, errors
 

	
 
def check_main_fails(arglist, config, error_flags, error_patterns):
...
 
@@ -366,25 +386,25 @@ def test_output_payments_when_only_match(arglist, expect_invoice):
 
    ['510/6100'],
 
    ['entity=Lawyer'],
 
])
 
def test_main_outgoing_report(arglist):
 
    retcode, output, errors = run_main(arglist)
 
    assert not errors.getvalue()
 
    assert retcode == 0
 
    rt_url = RTClient.DEFAULT_URL[:-9]
 
    rt_id_url = re.escape(f'<{rt_url}Ticket/Display.html?id=510>')
 
    contract_url = re.escape(f'<{rt_url}Ticket/Attachment/4000/4000/contract.pdf>')
 
    check_output(output, [
 
        r'^REQUESTOR: Mx\. 510 <mx510@example\.org>$',
 
        r'^TOTAL TO PAY: 280\.00 USD$',
 
        r'^TOTAL TO PAY: \$280\.00$',
 
        r'^\s*2020-06-12\s',
 
        r'^\s+Expenses:FilingFees\s+60\.00 USD$',
 
    ])
 

	
 
@pytest.mark.parametrize('arglist', [
 
    ['-t', 'balance'],
 
    ['515'],
 
    ['515/5150'],
 
    ['entity=DonorB'],
 
])
 
def test_main_balance_report(arglist):
 
    retcode, output, errors = run_main(arglist)
0 comments (0 inline, 0 general)