-
Notifications
You must be signed in to change notification settings - Fork 0
/
subs_report.py
324 lines (249 loc) · 10.7 KB
/
subs_report.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# coding: utf-8
"""Online Scout Manager Interface - generate report from subs.
Usage:
subs_report.py [-d] [-u] [--term=<term>] [--email=<address>]
<apiid> <token> <outdir>
subs_report.py (-h | --help)
subs_report.py --version
Options:
<outdir> Output directory for vcard files.
-d,--debug Turn on debug output.
-u, --upload Upload to Drive.
--email=<email> Send to only this email address.
--term=<term> Which OSM term to use [default: current].
-h,--help Show this screen.
--version Show version.
"""
# Setup the OSM access
# In[1]:
import os.path
import osm
from group import Group
import update
import json
import traceback
import logging
import itertools
import smtplib
from docopt import docopt
from datetime import date
from datetime import datetime
from dateutil.relativedelta import relativedelta
from dateutil.tz import tzutc
from pandas.io.json import json_normalize
import pandas as pd
from email.encoders import encode_base64
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
log = logging.getLogger(__name__)
try:
from gdrive_upload import upload
except:
log.warning("Failed to import gdrive_upload.")
DEF_CACHE = "osm.cache"
DEF_CREDS = "osm.creds"
FROM = "Richard Taylor <r.taylor@bcs.org.uk>"
MONTH_MAP = {1: "10", 2: "11", 3: "12", 4: "01", 5: "02", 6: "03", 7: "04", 8: "05", 9: "06", 10: "07", 11: "08",
12: "09"}
def send(to, subject, report_path, fro=FROM):
for dest in to:
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = fro
msg['To'] = dest
fp = open(report_path, 'rb')
file1 = MIMEBase('application', 'vnd.ms-excel')
file1.set_payload(fp.read())
fp.close()
encode_base64(file1)
file1.add_header('Content-Disposition',
'attachment;filename=output.xlsx')
msg.attach(file1)
hostname = 'localhost'
s = smtplib.SMTP(hostname)
try:
s.sendmail(fro, dest, msg.as_string())
except:
log.error(msg.as_string(),
exc_info=True)
s.quit()
def get_status(d):
if not d:
return "Payment Required?"
detail = [_ for _ in d if _['latest'] == '1']
return detail[0]['status']
def fetch_scheme(group, acc, section, scheme, term):
def set_subs_type(d, group=group):
try:
members = group.find_by_scoutid(d['scoutid'])
if len(members) == 0:
print(f"Can't find {d['scoutid']} {d} in OSM")
return members[0]['customisable_data.cf_subs_type_n_g_d_']
except:
if len(members) > 0:
print("failed to find sub type for: {} {} {}".format(
d['scoutid'],
repr(member),
traceback.format_exc()))
else:
print("Failed to find scoutid: {} {} (member has probably left the section)".format(
d,
traceback.format_exc()))
return "Unknown"
schedules = acc("ext/finances/onlinepayments/?action=getPaymentSchedule"
"§ionid={}&schemeid={}&termid={}".format(
section['id'], scheme['schemeid'], term))
status = acc("ext/finances/onlinepayments/?action="
"getPaymentStatus§ionid={}&schemeid={}&termid={}".format(
section['id'], scheme['schemeid'], term))
# Fix up wrongly named payment schedule in the Group Subs
if (scheme['name'] == 'Discounted Subscriptions for 7th Lichfield Scout Group' and
section['name'] == 'Subs'):
for payment in schedules['payments']:
if payment['date'] == '2017-02-20':
payment['name'] = '2017 - Spring Term - Part 2'
schedules = [_ for _ in schedules['payments'] if _['archived'] == '0']
try:
data = json_normalize(status['items'])
except:
return pd.DataFrame()
for schedule in schedules:
data[schedule['paymentid']] = data[schedule['paymentid']].apply(
lambda x: get_status(json.loads(x)['status']))
data['subs_type'] = data.apply(set_subs_type, axis=1)
data['section'] = section['name']
data['scheme'] = (
"General Subscriptions"
if scheme['name'].startswith("General Subscriptions")
else "Discounted Subscriptions")
for schedule in schedules:
data.rename(columns={schedule['paymentid']: schedule['name']},
inplace=True)
return data
# In[3]:
def fetch_section(group, acc, section, term):
schemes = acc(
"ext/finances/onlinepayments/?action=getSchemes§ionid={}".format(
section['id']))
# filter only General and Discounted Subscriptions
schemes = [_ for _ in schemes['items'] if (
_['name'].startswith("General Subscriptions") or
_['name'].startswith("Discounted Subscriptions"))]
# Check that we only have two subscriptions remaining. If there is
# more, the rest of the report is going to barf.
if len(schemes) > 2:
log.error("Found more than 2 matching schemes in {}."
"Matching schemes were: {}".format(section['name'],
",".join(schemes)))
c = pd.concat([fetch_scheme(group, acc, section, scheme, term)
for scheme in schemes
if scheme['name'] != 'Camps and Events'],
ignore_index=True)
return c
def _main(osm, auth, outdir, email, term, do_upload):
assert os.path.exists(outdir) and os.path.isdir(outdir)
group = Group(osm, auth, update.MAPPING.keys(), term)
# Nasty hack to pick up the current term if the user did not
# pass in a specific term.
actual_term = list(group._sections.sections.values())[0].term['termid']
acc = group._sections._accessor
sections = [
{'name': 'Paget', 'id': '9960'},
{'name': 'Swinfen', 'id': '17326'},
{'name': 'Maclean', 'id': '14324'},
{'name': 'Rowallan', 'id': '12700'},
{'name': 'Johnson', 'id': '5882'},
{'name': 'Garrick', 'id': '20711'},
{'name': 'Erasmus', 'id': '20707'},
{'name': 'Somers', 'id': '20706'},
{'name': 'Boswell', 'id': '10363'},
{'name': 'Subs', 'id': '33593'}
]
subs_names = ['General Subscriptions', 'Discounted Subscriptions']
subs_types = ['G', 'D']
subs_names_and_types = list(zip(subs_names, subs_types))
all_types = subs_types + ['N', ]
al = pd.concat([fetch_section(group, acc, section, actual_term)
for section in sections], ignore_index=True)
# al[(al['scheme'] == 'Discounted Subscriptions') & (
# al['subs_type'] == 'D')].dropna(axis=1, how='all')
# find all members that do not have at least one subscription to either
# 'Discounted Subscriptions' or 'General Subscriptions'
# filtered by those that have a 'N' in their subscription type.
#
# all_yp_members = group.all_yp_members_without_leaders()
all = [[[_['member_id'],
_['first_name'],
_['last_name'],
_['customisable_data.cf_subs_type_n_g_d_'],
section] for _ in
group.section_yp_members_without_leaders(section)]
for section in group.YP_SECTIONS]
all_in_one = list(itertools.chain.from_iterable(all))
all_members_df = pd.DataFrame(all_in_one, columns=(
'scoutid', 'firstname', 'lastname', 'subs_type', 'section'))
al_only_subs = al[al['scheme'].isin(subs_names)]
# only those that are paying more than one subscription.
members_paying_multiple_subs = al_only_subs[
al_only_subs.duplicated('scoutid', take_last=True) |
al_only_subs.duplicated('scoutid')]
# Calculate file name
frm = datetime((date.today() - relativedelta(months=+1)).year,
(date.today() - relativedelta(months=+1)).month,
4, 0, 0, 0, tzinfo=tzutc())
to = frm + relativedelta(months=+1)
filename = os.path.join(outdir, "{} {} {} {} Subs Report.xls".format(MONTH_MAP[to.month],
to.day - 1,
to.strftime("%b"),
to.year))
with pd.ExcelWriter(filename,
engine='xlsxwriter') as writer:
# Status of all subs.
for scheme in subs_names:
al[al['scheme'] == scheme].dropna(
axis=1, how='all').to_excel(writer, scheme)
# All subs with the correct subs_type
for scheme in subs_names_and_types:
al[(al['scheme'] == scheme[0]) &
(al['subs_type'] == scheme[1])].dropna(
axis=1, how='all').to_excel(writer, scheme[0] + "_OK")
# All subs with the wrong subs type
for scheme in subs_names_and_types:
al[(al['scheme'] == scheme[0]) &
(al['subs_type'] != scheme[1])].dropna(
axis=1, how='all').to_excel(writer, scheme[0] + "_BAD")
# Members not in the subs that their sub_type says they should be.
for scheme in subs_names_and_types:
gen = al[al['scheme'] == scheme[0]].dropna(axis=1, how='all')
all_gen_members = all_members_df[
all_members_df['subs_type'] == scheme[1]]
all_gen_members['scoutid'] = all_gen_members['scoutid'].astype(str)
all_gen_members[~all_gen_members['scoutid'].isin(
gen['scoutid'].values)].to_excel(writer, "Not in " + scheme[0])
# All YP members without their subs_type set to anything.
all_members_df[~all_members_df['subs_type'].isin(
all_types)].to_excel(writer, "Unknown Subs Type")
# Members paying multiple subs
members_paying_multiple_subs.dropna(
axis=1, how='all').to_excel(writer, "Multiple payers")
if email:
send([email, ], "OSM Subs Report", filename)
if do_upload:
from gc_accounts import SECTION_MAP, DRIVE_FOLDERS
if filename is not None:
upload(filename, DRIVE_FOLDERS['Group'],
filename=os.path.splitext(os.path.split(filename)[1])[0])
if __name__ == '__main__':
args = docopt(__doc__, version='OSM 2.0')
if args['--debug']:
level = logging.DEBUG
else:
level = logging.WARN
if args['--term'] in [None, 'current']:
args['--term'] = None
logging.basicConfig(level=level)
log.debug("Debug On\n")
auth = osm.Authorisor(args['<apiid>'], args['<token>'])
auth.load_from_file(open(DEF_CREDS, 'r'))
_main(osm, auth,
args['<outdir>'], args['--email'], args['--term'], args['--upload'])