PageRenderTime 22ms CodeModel.GetById 36ms RepoModel.GetById 0ms app.codeStats 0ms

/reporters/members.py

https://gitlab.com/made-financial/made-financial-scripts
Python | 189 lines | 140 code | 23 blank | 26 comment | 35 complexity | 38505c66ba5e7943eec78025cea14d14 MD5 | raw file
  1. # Reports all membership payments, per month.
  2. # Supported options:
  3. # - ids: output movement ids
  4. # - from=month: display starting from this month
  5. # - to=month: display until this month
  6. # - member=id: display only payments from this member
  7. # Supported formats:
  8. # - csv: for import into a spreadsheet
  9. import sys
  10. from collections import namedtuple
  11. import pandas as pd
  12. from .common import get_refunded
  13. sys.path.append("../data/plugins")
  14. from categorize.entities import Entities
  15. entities = Entities("../data/")
  16. sources = dict(PP="Paypal", BK="Bank", CA="Cash", CB="Bitcoin")
  17. Payment = namedtuple('Payment', 'month payee amount source id')
  18. Summary = namedtuple('Summary', 'payments total by_source by_level')
  19. def members(movements, options, format):
  20. items = []
  21. month_from = options.get("from")
  22. month_to = options.get("to")
  23. refunded = get_refunded(movements)
  24. for m in movements:
  25. # All movmements should have an id, if not discard them for now.
  26. if not 'movement_id' in m.meta:
  27. continue
  28. id = m.meta['movement_id']
  29. # Discard anything that has zero postings in Income:Membership
  30. payments = [p for p in m.postings if p.account == "Income:Membership"]
  31. if len(payments) == 0:
  32. continue
  33. # If a payment has been refunded, exclude it
  34. if id in refunded:
  35. continue
  36. for p in payments:
  37. # We need to find out which month this payment is for.
  38. # There are three possibilities:
  39. # - pay only for current month: no payment_period meta
  40. # - pay for another month: movement has payment_period meta
  41. # - pay for various months: each posting has a payment_period meta
  42. period = p.meta.get('payment_period', None)
  43. if period is None:
  44. period = m.meta.get('payment_period', None)
  45. if period is None:
  46. period = "{}-{:0>2}".format(m.date.year, m.date.month)
  47. if month_from is not None and period < month_from:
  48. continue
  49. if month_to is not None and period > month_to:
  50. continue
  51. # We also want to know who the payment is from.
  52. # In most cases the parent transaction's payee is what we want.
  53. # However in some cases a single transaction pays for various
  54. # members, identified by their entity_id.
  55. payee = "<< Unknown >>"
  56. member_id = p.meta.get('entity_id', None)
  57. if member_id is None:
  58. member_id = m.meta.get('entity_id', None)
  59. if member_id is not None and member_id != 'unknown':
  60. member = entities.members.get(int(member_id), None)
  61. if member is not None:
  62. payee = member['fullname']
  63. else:
  64. if m.payee is not None:
  65. payee = m.payee
  66. # If we set up a filter for a specific member, exclude all
  67. # that don't match
  68. filter_member = options.get('member')
  69. if filter_member:
  70. if payee != filter_member:
  71. continue
  72. elif 'id' in options:
  73. if options['id'] != member_id:
  74. continue
  75. item = dict(id = m.meta['movement_id'],
  76. month = period,
  77. amount = round(p.units.number * -1),
  78. payee = payee,
  79. source = m.meta['movement_id'][0:2])
  80. items.append(item)
  81. if len(items) == 0:
  82. print("No records for this member.")
  83. return
  84. months = pd.DataFrame().from_records(items)
  85. # output the data, depending on the report format we want
  86. if format == "csv":
  87. # output headers first
  88. fields = ["Month", "Member", "Amount", "Payment Method"]
  89. if "ids" in options:
  90. fields.append("Transaction ID")
  91. print(",".join(fields))
  92. # then output the actual data
  93. for row in rows:
  94. data = [row.month, row.payee, str(row.amount), sources[row.source]]
  95. if "ids" in options:
  96. data.append(row.id)
  97. print(",".join(data))
  98. else: # default "screen" format
  99. as_is = lambda v: str(v)
  100. format = lambda f: lambda v: f.format(v)
  101. if not 'member' in options and not 'id' in options:
  102. # regular report for all members, by month
  103. cols = ['payee', 'amount', 'source']
  104. formats = [format('{:<35}'), format('{:>6}'), as_is]
  105. if 'ids' in options:
  106. cols.append('id')
  107. formats.append(as_is)
  108. g = months.groupby('month')
  109. lastmonth = None
  110. for month, items in g:
  111. summary = Summary(
  112. by_source = items.groupby('source')['source'].count().items(),
  113. by_level = items.groupby('amount')['amount'].count().items(),
  114. payments = items['amount'].count(),
  115. total = items['amount'].sum())
  116. print("{}, {} payments, {} EUR total".format(month,
  117. summary.payments,
  118. summary.total))
  119. out = []
  120. for level, value in summary.by_level:
  121. out.append("{} x {} EUR".format(value, level))
  122. print("{:<9}{}".format("", ", ".join(out)))
  123. out = []
  124. for source, value in summary.by_source:
  125. out.append("{} by {}".format(value, sources[source]))
  126. print("{:<9}{}".format("", ", ".join(out)))
  127. thismonth = { m for m in items['payee'] }
  128. if lastmonth is not None:
  129. join = thismonth - lastmonth
  130. left = lastmonth - thismonth
  131. print("Join {}: {}".format(len(join), ", ".join(join)))
  132. print("Left {}: {}".format(len(left), ", ".join(left)))
  133. lastmonth = thismonth
  134. if not options.get('summaries', False):
  135. print(items.sort_values(by='payee').to_string(columns=cols,
  136. formatters=formats,
  137. index=False,
  138. header=False))
  139. print("\n")
  140. else:
  141. if 'id' in options:
  142. member = entities.members.get(int(options['id']), None)
  143. if member is not None:
  144. fullname = member['fullname']
  145. else:
  146. fullname = options['member']
  147. print("{}: {} payments for {} EUR".format(fullname,
  148. months['amount'].count(),
  149. months['amount'].sum()))
  150. cols = ['month', 'amount', 'source']
  151. formats = [as_is, format('{:>6}'), as_is]
  152. if 'ids' in options:
  153. cols.append('id')
  154. formats.append(as_is)
  155. print(months.sort_values(by='month').to_string(columns=cols,
  156. formatters=formats,
  157. index=False,
  158. header=False))
  159. print(options)