/reporters/members.py
Python | 189 lines | 140 code | 23 blank | 26 comment | 35 complexity | 38505c66ba5e7943eec78025cea14d14 MD5 | raw file
- # Reports all membership payments, per month.
- # Supported options:
- # - ids: output movement ids
- # - from=month: display starting from this month
- # - to=month: display until this month
- # - member=id: display only payments from this member
- # Supported formats:
- # - csv: for import into a spreadsheet
- import sys
- from collections import namedtuple
- import pandas as pd
- from .common import get_refunded
- sys.path.append("../data/plugins")
- from categorize.entities import Entities
- entities = Entities("../data/")
- sources = dict(PP="Paypal", BK="Bank", CA="Cash", CB="Bitcoin")
- Payment = namedtuple('Payment', 'month payee amount source id')
- Summary = namedtuple('Summary', 'payments total by_source by_level')
- def members(movements, options, format):
- items = []
- month_from = options.get("from")
- month_to = options.get("to")
- refunded = get_refunded(movements)
- for m in movements:
- # All movmements should have an id, if not discard them for now.
- if not 'movement_id' in m.meta:
- continue
- id = m.meta['movement_id']
- # Discard anything that has zero postings in Income:Membership
- payments = [p for p in m.postings if p.account == "Income:Membership"]
- if len(payments) == 0:
- continue
- # If a payment has been refunded, exclude it
- if id in refunded:
- continue
- for p in payments:
- # We need to find out which month this payment is for.
- # There are three possibilities:
- # - pay only for current month: no payment_period meta
- # - pay for another month: movement has payment_period meta
- # - pay for various months: each posting has a payment_period meta
- period = p.meta.get('payment_period', None)
- if period is None:
- period = m.meta.get('payment_period', None)
- if period is None:
- period = "{}-{:0>2}".format(m.date.year, m.date.month)
- if month_from is not None and period < month_from:
- continue
- if month_to is not None and period > month_to:
- continue
- # We also want to know who the payment is from.
- # In most cases the parent transaction's payee is what we want.
- # However in some cases a single transaction pays for various
- # members, identified by their entity_id.
- payee = "<< Unknown >>"
- member_id = p.meta.get('entity_id', None)
- if member_id is None:
- member_id = m.meta.get('entity_id', None)
- if member_id is not None and member_id != 'unknown':
- member = entities.members.get(int(member_id), None)
- if member is not None:
- payee = member['fullname']
- else:
- if m.payee is not None:
- payee = m.payee
- # If we set up a filter for a specific member, exclude all
- # that don't match
- filter_member = options.get('member')
- if filter_member:
- if payee != filter_member:
- continue
- elif 'id' in options:
- if options['id'] != member_id:
- continue
- item = dict(id = m.meta['movement_id'],
- month = period,
- amount = round(p.units.number * -1),
- payee = payee,
- source = m.meta['movement_id'][0:2])
- items.append(item)
- if len(items) == 0:
- print("No records for this member.")
- return
- months = pd.DataFrame().from_records(items)
- # output the data, depending on the report format we want
- if format == "csv":
- # output headers first
- fields = ["Month", "Member", "Amount", "Payment Method"]
- if "ids" in options:
- fields.append("Transaction ID")
- print(",".join(fields))
- # then output the actual data
- for row in rows:
- data = [row.month, row.payee, str(row.amount), sources[row.source]]
- if "ids" in options:
- data.append(row.id)
- print(",".join(data))
- else: # default "screen" format
- as_is = lambda v: str(v)
- format = lambda f: lambda v: f.format(v)
- if not 'member' in options and not 'id' in options:
- # regular report for all members, by month
- cols = ['payee', 'amount', 'source']
- formats = [format('{:<35}'), format('{:>6}'), as_is]
- if 'ids' in options:
- cols.append('id')
- formats.append(as_is)
- g = months.groupby('month')
- lastmonth = None
- for month, items in g:
- summary = Summary(
- by_source = items.groupby('source')['source'].count().items(),
- by_level = items.groupby('amount')['amount'].count().items(),
- payments = items['amount'].count(),
- total = items['amount'].sum())
- print("{}, {} payments, {} EUR total".format(month,
- summary.payments,
- summary.total))
- out = []
- for level, value in summary.by_level:
- out.append("{} x {} EUR".format(value, level))
- print("{:<9}{}".format("", ", ".join(out)))
- out = []
- for source, value in summary.by_source:
- out.append("{} by {}".format(value, sources[source]))
- print("{:<9}{}".format("", ", ".join(out)))
- thismonth = { m for m in items['payee'] }
- if lastmonth is not None:
- join = thismonth - lastmonth
- left = lastmonth - thismonth
- print("Join {}: {}".format(len(join), ", ".join(join)))
- print("Left {}: {}".format(len(left), ", ".join(left)))
- lastmonth = thismonth
- if not options.get('summaries', False):
- print(items.sort_values(by='payee').to_string(columns=cols,
- formatters=formats,
- index=False,
- header=False))
- print("\n")
- else:
- if 'id' in options:
- member = entities.members.get(int(options['id']), None)
- if member is not None:
- fullname = member['fullname']
- else:
- fullname = options['member']
- print("{}: {} payments for {} EUR".format(fullname,
- months['amount'].count(),
- months['amount'].sum()))
- cols = ['month', 'amount', 'source']
- formats = [as_is, format('{:>6}'), as_is]
- if 'ids' in options:
- cols.append('id')
- formats.append(as_is)
- print(months.sort_values(by='month').to_string(columns=cols,
- formatters=formats,
- index=False,
- header=False))
- print(options)