Changeset - 948d3a2d14d1
[Not reviewed]
0 4 0
Brett Smith - 4 years ago 2020-06-09 19:59:09
brettcsmith@brettcsmith.org
accrual: Add columns to the aging report. RT#11439.

This adds almost all the metadata that's relevant to accruals.
I considered adding statement, but that cuased rows to get spaced out a lot,
and statement's kind of a low-value column, so I decided against it.

Ultimately I would like to make this configurable but that's for the
future.
4 files changed with 38 insertions and 6 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/accrual.py
Show inline comments
...
 
@@ -331,14 +331,18 @@ class BaseReport:
 
class AgingODS(core.BaseODS[AccrualPostings, Optional[data.Account]]):
 
    COLUMNS = [
 
        'Date',
 
        'Entity',
 
        'Invoice Amount',
 
        'Booked Amount',
 
        'Project',
 
        'Ticket',
 
        'Invoice',
 
        'Approval',
 
        'Contract',
 
        'Purchase Order',
 
    ]
 
    COL_COUNT = len(COLUMNS)
 

	
 
    def __init__(self,
 
                 rt_client: rt.Rt,
 
                 date: datetime.date,
...
 
@@ -399,13 +403,13 @@ class AgingODS(core.BaseODS[AccrualPostings, Optional[data.Account]]):
 

	
 
    def end_section(self, key: Optional[data.Account]) -> None:
 
        if key is None:
 
            return
 
        total_balance = core.MutableBalance()
 
        text_style = self.merge_styles(self.style_bold, self.style_endtext)
 
        text_span = self.COL_COUNT - 1
 
        text_span = 4
 
        last_age_text: Optional[str] = None
 
        self.add_row()
 
        for threshold, balance in zip(self.age_thresholds, self.age_balances):
 
            years, days = divmod(threshold, 365)
 
            years_text = f"{years} {'Year' if years == 1 else 'Years'}"
 
            days_text = f"{days} Days"
...
 
@@ -471,19 +475,25 @@ class AgingODS(core.BaseODS[AccrualPostings, Optional[data.Account]]):
 
        if row.accrual_type is not None:
 
            raw_balance = row.accrual_type.normalize_amount(raw_balance)
 
        if raw_balance == row.end_balance:
 
            amount_cell = odf.table.TableCell()
 
        else:
 
            amount_cell = self.balance_cell(raw_balance)
 
        projects = {post.meta.get('project') or None for post in row}
 
        projects.discard(None)
 
        self.add_row(
 
            self.date_cell(row[0].meta.date),
 
            self.multiline_cell(row.entities()),
 
            amount_cell,
 
            self.balance_cell(row.end_balance),
 
            self.multiline_cell(sorted(projects)),
 
            self.multilink_cell(self._link_seq(row, 'rt-id')),
 
            self.multilink_cell(self._link_seq(row, 'invoice')),
 
            self.multilink_cell(self._link_seq(row, 'approval')),
 
            self.multilink_cell(self._link_seq(row, 'contract')),
 
            self.multilink_cell(self._link_seq(row, 'purchase-order')),
 
        )
 

	
 

	
 
class AgingReport(BaseReport):
 
    def __init__(self,
 
                 rt_client: rt.Rt,
setup.py
Show inline comments
...
 
@@ -2,13 +2,13 @@
 

	
 
from setuptools import setup
 

	
 
setup(
 
    name='conservancy_beancount',
 
    description="Plugin, library, and reports for reading Conservancy's books",
 
    version='1.1.5',
 
    version='1.1.6',
 
    author='Software Freedom Conservancy',
 
    author_email='info@sfconservancy.org',
 
    license='GNU AGPLv3+',
 

	
 
    install_requires=[
 
        'babel>=2.6',  # Debian:python3-babel
tests/books/accruals.beancount
Show inline comments
...
 
@@ -12,124 +12,142 @@
 
  Assets:Receivable:Accounts  6000 USD
 
  Liabilities:Payable:Accounts  -5000 USD
 

	
 
2010-03-05 * "EarlyBird" "Payment for receivable from previous FY"
 
  rt-id: "rt:40"
 
  invoice: "rt:40/400"
 
  project: "Conservancy"
 
  Assets:Receivable:Accounts  -500 USD
 
  Assets:Checking  500 USD
 

	
 
2010-03-06 * "EarlyBird" "Payment for payment from previous FY"
 
  rt-id: "rt:44"
 
  invoice: "rt:44/440"
 
  project: "Conservancy"
 
  Liabilities:Payable:Accounts  125 USD
 
  Assets:Checking  -125 USD
 

	
 
2010-03-15 * "GrantCo" "2010Q1 grant"
 
  rt-id: "rt:470"
 
  invoice: "rt:470/4700"
 
  project: "Development Grant"
 
  Assets:Receivable:Accounts  5000 USD
 
  Income:Donations           -5000 USD
 

	
 
2010-03-25 * "GrantCo" "2010Q1 grant ACH payment"
 
  rt-id: "rt:470"
 
  invoice: "rt:470/4700"
 
  project: "Development Grant"
 
  Assets:Receivable:Accounts  -5000 USD
 
  Assets:Checking              5000 USD
 

	
 
2010-03-30 * "EarlyBird" "Travel reimbursement"
 
  rt-id: "rt:490"
 
  invoice: "rt:490/4900"
 
  project: "Conservancy"
 
  Liabilities:Payable:Accounts  -75 USD
 
  Expenses:Travel  75 USD
 

	
 
2010-04-15 * "Multiparty invoice"
 
  rt-id: "rt:480"
 
  invoice: "rt:480/4800"
 
  project: "Conservancy"
 
  Expenses:Travel  250 USD
 
  Liabilities:Payable:Accounts  -125 USD
 
  entity: "MultiPartyA"
 
  Liabilities:Payable:Accounts  -125 USD
 
  entity: "MultiPartyB"
 

	
 
2010-04-18 * "MultiPartyA" "Payment for 480"
 
  rt-id: "rt:480"
 
  invoice: "rt:480/4800"
 
  project: "Conservancy"
 
  Liabilities:Payable:Accounts  125 USD
 
  Assets:Checking  -125 USD
 

	
 
2010-04-20 * "MultiPartyB" "Payment for 480"
 
  rt-id: "rt:480"
 
  invoice: "rt:480/4800"
 
  project: "Conservancy"
 
  Liabilities:Payable:Accounts  125 USD
 
  Assets:Checking  -125 USD
 

	
 
2010-04-30 ! "Vendor" "Travel reimbursement"
 
  rt-id: "rt:310"
 
  contract: "rt:310/3100"
 
  invoice: "FIXME"  ; still waiting on them to send it
 
  project: "Conservancy"
 
  Liabilities:Payable:Accounts  -200 USD
 
  Expenses:Travel  200 USD
 

	
 
2010-05-05 * "DonorA" "Donation pledge"
 
  rt-id: "rt:505"
 
  invoice: "rt:505/5050"
 
  approval: "rt:505/5040"
 
  project: "Conservancy"
 
  Income:Donations  -2,500 EUR {1.100 USD}
 
  Assets:Receivable:Accounts  2,500 EUR {1.100 USD}
 

	
 
2010-05-10 * "Lawyer" "April legal services"
 
  rt-id: "rt:510"
 
  invoice: "rt:510/5100"
 
  contract: "rt:510/4000"
 
  project: "Conservancy"
 
  Expenses:Services:Legal  200.00 USD
 
  Liabilities:Payable:Accounts  -200.00 USD
 

	
 
2010-05-15 * "MatchingProgram" "May matched donations"
 
  invoice: "rt://ticket/515/attachments/5150"
 
  approval: "rt://ticket/515/attachments/5140"
 
  project: "Conservancy"
 
  Income:Donations  -1500.00 USD
 
  Assets:Receivable:Accounts  1500.00 USD
 

	
 
2010-05-20 * "DonorA" "Donation made"
 
  rt-id: "rt:505"
 
  invoice: "rt:505/5050"
 
  project: "Conservancy"
 
  Assets:Receivable:Accounts  -2,750.00 USD
 
  Assets:Checking  2,750.00 USD
 
  receipt: "DonorAWire.pdf"
 

	
 
2010-05-25 * "Lawyer" "May payment"
 
  rt-id: "rt:510"
 
  invoice: "rt:510/5100"
 
  project: "Conservancy"
 
  Liabilities:Payable:Accounts  200.00 USD
 
  contract: "rt:510/4000"
 
  Assets:Checking  -200.00 USD
 
  receipt: "rt:510/5105"
 

	
 
2010-06-10 * "Lawyer" "May legal services"
 
  rt-id: "rt:510"
 
  invoice: "rt:510/6100"
 
  contract: "rt:510/4000"
 
  project: "Conservancy"
 
  Expenses:Services:Legal  220.00 USD
 
  Liabilities:Payable:Accounts  -220.00 USD
 

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

	
 
2010-06-18 * "EuroGov" "European legal fees"
 
  ; Multiple rt-ids are used to test proper handling for both
 
  ; searching and generating the outgoing report.
 
  rt-id: "rt:520 rt:525"
 
  invoice: "rt:520/5200"
 
  contract: "rt:520/5220"
 
  project: "Conservancy"
 
  Liabilities:Payable:Accounts  -1,000 EUR {1.100 USD}
 
  Expenses:FilingFees  1,000 EUR {1.100 USD}
 

	
 
2010-06-15 * "GrantCo" "2010Q2 grant"
 
  rt-id: "rt:470"
 
  invoice: "rt:470/4700"
 
  project: "Development Grant"
 
  Assets:Receivable:Accounts  5500 USD
 
  Income:Donations           -5500 USD
tests/test_reports_accrual.py
Show inline comments
...
 
@@ -72,36 +72,39 @@ class AgingRow(NamedTuple):
 
    date: datetime.date
 
    entity: Sequence[str]
 
    amount: Optional[Sequence[bc_data.Amount]]
 
    at_cost: bc_data.Amount
 
    rt_id: Sequence[str]
 
    invoice: Sequence[str]
 
    project: Sequence[str]
 

	
 
    @classmethod
 
    def make_simple(cls, date, entity, at_cost, invoice, rt_id=None, orig_amount=None):
 
    def make_simple(cls, date, entity, at_cost, invoice,
 
                    rt_id=None, orig_amount=None, project='Conservancy'):
 
        if isinstance(date, str):
 
            date = datetime.datetime.strptime(date, '%Y-%m-%d').date()
 
        if not isinstance(at_cost, tuple):
 
            at_cost = testutil.Amount(at_cost)
 
        if rt_id is None:
 
            rt_id, _, _ = invoice.partition('/')
 
        return cls(date, [entity], orig_amount, at_cost, [rt_id], [invoice])
 
        return cls(date, [entity], orig_amount, at_cost, [rt_id], [invoice], [project])
 

	
 
    def check_row_match(self, sheet_row):
 
        cells = testutil.ODSCell.from_row(sheet_row)
 
        assert len(cells) == len(self)
 
        assert len(cells) >= len(self)
 
        cells = iter(cells)
 
        assert next(cells).value == self.date
 
        assert next(cells).text == '\0'.join(self.entity)
 
        assert next(cells).text == '\0'.join(
 
            babel.numbers.format_currency(number, currency, format_type='accounting')
 
            for number, currency in self.amount or ()
 
        )
 
        usd_cell = next(cells)
 
        assert usd_cell.value_type == 'currency'
 
        assert usd_cell.value == self.at_cost.number
 
        assert next(cells).text == '\0'.join(self.project)
 
        for index, cell in enumerate(cells):
 
            links = cell.getElementsByType(odf.text.A)
 
            assert len(links) == len(cell.childNodes)
 
        assert index >= 1
 

	
 

	
...
 
@@ -115,13 +118,14 @@ AGING_AP = [
 
]
 

	
 
AGING_AR = [
 
    AgingRow.make_simple('2010-03-05', 'EarlyBird', -500, 'rt:40/400'),
 
    AgingRow.make_simple('2010-05-15', 'MatchingProgram', 1500,
 
                         'rt://ticket/515/attachments/5150'),
 
    AgingRow.make_simple('2010-06-15', 'GrantCo', 5500, 'rt:470/4700'),
 
    AgingRow.make_simple('2010-06-15', 'GrantCo', 5500, 'rt:470/4700',
 
                         project='Development Grant'),
 
]
 

	
 
class RTClient(testutil.RTClient):
 
    TICKET_DATA = {
 
        '40': [
 
            ('400', 'invoice feb.csv', 'text/csv', '40.4k'),
0 comments (0 inline, 0 general)