-
Notifications
You must be signed in to change notification settings - Fork 15
/
lagerbidrag.py
395 lines (324 loc) · 13.6 KB
/
lagerbidrag.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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# -*- coding: utf-8 -*-
"""
Calculate data needed and fill in forms for lägerbidragsansökan (hike grants).
The limits and data needed depends on region.
Göteborg has 2-14 nights, age 7-25, and includes som older people.
Stocholm has 2-7 days, age 7-20, and does not include older people.
Stockholm also needs postal address for each scout.
"""
from collections import namedtuple
from data import Meeting, Troop
from datetime import datetime, date
from flask import render_template, make_response
from mailmerge import MailMerge
import io
import copy
import math
import urllib
from functools import cmp_to_key
RegionLimits = namedtuple('RegionLimits', ['min_days', 'max_days', 'min_age', 'max_age', 'count_over_max_age'])
LIMITS = {'gbg' : RegionLimits(2, 15, 7, 25, False), # min 2 days, max 14 days, 7-25 yers + some older
'sthlm' : RegionLimits(2, 7, 7, 20, False)} # min 2 days, max 7 days, 7-20 years
DATE_FORMAT = '%Y-%m-%d'
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M'
def render_lagerbidrag(request, scoutgroup, context, sgroup_key=None, user=None, trooppersons=None, troop_key=None):
if context == "group":
assert sgroup_key is not None
assert user is not None
troops = Troop.getTroopsForUser(sgroup_key, user)
elif context == "troop":
assert trooppersons is not None
assert troop_key is not None
else:
raise ValueError("Context %s unknown" % context)
region = request.args.get('region')
limits = LIMITS[region]
bidrag = LagerBidrag(scoutgroup.getname())
bidrag.foreningsID = scoutgroup.foreningsID
bidrag.firmatecknare = scoutgroup.firmatecknare
bidrag.firmatecknartelefon = scoutgroup.firmatecknartelefon
bidrag.firmatecknaremail = scoutgroup.firmatecknaremail
bidrag.organisationsnummer = scoutgroup.organisationsnummer
bidrag.email = scoutgroup.epost
bidrag.phone = scoutgroup.telefon
bidrag.address = scoutgroup.adress
bidrag.zipCode = scoutgroup.postadress
bidrag.account = scoutgroup.bankkonto
bidrag.contact = request.form['contactperson']
bidrag.contactemail = request.form.get('contactemail', '')
bidrag.contactphone = request.form.get('contactphone', '')
bidrag.site = request.form['site']
bidrag.dateFrom = request.form['fromDate']
bidrag.dateTo = request.form['toDate']
bidrag.hikeduringbreak = request.form.get('hikeduringbreak')
try:
if context == "group":
bidragcontainer = createLagerbidragGroup(limits, scoutgroup, troops, bidrag)
else:
bidragcontainer = createLagerbidrag(limits, scoutgroup, trooppersons, troop_key, bidrag)
if region == 'gbg':
return response_gbg(bidragcontainer)
elif region == 'sthlm':
return response_sthlm(bidragcontainer)
else:
raise ValueError("Unknown region %s" % region)
except ValueError as e:
return render_template('error.html', error=str(e))
def response_gbg(bidragcontainer):
result = render_template(
'lagerbidrag_gbg.html',
bidrag=bidragcontainer.bidrag,
persons=bidragcontainer.persons,
numbers=bidragcontainer.numbers)
response = make_response(result)
return response
def response_sthlm(bidragcontainer):
per_diem = 35 # Amount per scout per day 2021
h1_end = datetime(2021, 5, 31, 23, 59)
bidrag = bidragcontainer.bidrag
persons = bidragcontainer.persons
nr_persons = bidragcontainer.nr_persons_total
persons = persons[:nr_persons]
start = datetime.strptime(bidrag.dateFrom, DATE_FORMAT)
end = datetime.strptime(bidrag.dateTo, DATE_FORMAT)
nr_days = (end - start).days + 1
if bidrag.zipCode != "":
zcParts = bidrag.zipCode.split()
postnr = "".join(zcParts[:-1]) # remove possible space in zip code
postort = zcParts[-1]
today = date.today()
data = {
'organisationsnummer': bidrag.organisationsnummer,
'kontonummer': bidrag.kontonummer,
'kundnummer': bidrag.foreningsID,
'foreningsnamn': bidrag.kar,
'epost': bidrag.email,
'ledare': bidrag.contact,
'ledartelefon': bidrag.contactphone,
'ledaremail': bidrag.contactemail,
'lov': u'X' if bidrag.hikeduringbreak else u'\u2610',
'helg': u'\u2610' if bidrag.hikeduringbreak else u'X',
'lagerplats': bidrag.site,
'startdatum': bidrag.dateFrom,
'slutdatum': bidrag.dateTo,
'datum': today.strftime(DATE_FORMAT),
'dag': str(today.day),
'manad': str(today.month),
'ordforande': bidrag.firmatecknare,
'ordforandetelefon': bidrag.firmatecknartelefon,
'ordforandeemail': bidrag.firmatecknaremail,
'adress': bidrag.address,
'postnr': postnr,
'postort': postort,
'bankkonto': bidrag.account,
'antalmedlemmar': str(bidragcontainer.nr_young_persons),
'antaldagar': str(nr_days), # Should be total number of days
'summa': str(bidrag.days * per_diem) + ",00",
'H1': u'\u2610' if h1_end <= end else u'X',
'H2': u'\u2610' if h1_end > end else u'X'
}
period_data = generate_sthlm_period_data(today.year, today.month, today.day)
data.update(period_data)
persons_per_page = 25
nr_pages, rest = divmod(nr_persons, persons_per_page)
if rest > 0:
nr_pages += 1
pages = []
for page_nr in range(nr_pages):
page_data = copy.deepcopy(data)
persons_in_page = persons[page_nr * persons_per_page:(page_nr + 1) * persons_per_page]
for i, person in enumerate(persons_in_page):
nr = i + 1
page_data['namn%d' % nr] = person.name
page_data['pa%d' % nr] = person.postal_address
page_data['ar%d' % nr] = str(person.year)
if person.days != nr_days:
page_data['ad%d' % nr] = str(person.days)
pages.append(page_data)
document = MailMerge('templates/lagerbidragsmall_sthlm_2021.docx')
document.merge_pages(pages)
bytesIO = io.BytesIO()
document.write(bytesIO)
resultbytes = bytesIO.getvalue()
doc_name = "Lagerbidrag_%s_%s_%s.docx" % (bidrag.site, bidrag.dateFrom, bidrag.dateTo)
ascii_doc_name = doc_name.encode('ascii', 'ignore')
response = make_response(resultbytes)
response.headers['Content-Type'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
response.headers['Content-Disposition'] = 'attachment; filename=' + urllib.parse.quote(ascii_doc_name)
return response
def generate_sthlm_period_data(year, month, day):
"""Generate dictionary with period data for right period.
Dec 16 - Jun 30, the period is Dec 1 to May 31.
Jul 1 - Dec 15, the period is Jun 1 to Nov. 30
"""
if month <= 6 or (month == 12 and day > 15): # Period 1
if month <= 6:
y = year
else:
y = year + 1
pd = {
'ansokningsar': str(y),
'sistaansokan': '30 juni %d' % y,
'periodstart': '1 december %d' % (y - 1),
'periodslut': '31 maj %d' % y
}
else: # Period 2
pd = {
'ansokningsar': str(year),
'sistaansokan': '15 december %d' % year,
'periodstart': '1 juni %d' % year,
'periodslut': '30 november %d' % year
}
return pd
def person_sort(a, b):
if a.year == b.year:
if a.name > b.name:
return 1
else:
return -1
elif a.year > b.year:
return 1
else:
return -1
class LagerBidragContainer:
bidrag = ""
nr_young_persons = 0
nr_older_persons = 0
nr_under_min_age = 0
nr_persons_total = 0
def __init__(self):
self.persons = []
self.numbers = []
class LagerPerson():
def __init__(self, person="", name="", year="", age=0, postal_address=""):
self.person = person
self.name = name
self.year = year
self.age = age
self.postal_address = postal_address
self.days = 0
class LagerBidrag():
contact = ""
contactemail = ""
contactphone = ""
kar = ""
foreningsID = ""
account = ""
firmatecknare = ""
firmatecknartelefon = ""
firmatecknaremail = ""
organisationsnummer = ""
kontonummer = ""
dateFrom = ""
dateTo = ""
hikeduringbreak = None
site = ""
address = ""
zipCode = ""
phone = ""
email = ""
uptoMaxAge = 0
overMaxAge = 0
nights = 0
nightsUpToMaxAge = 0
nightsOverMaxAge = 0
days = 0
divider = 25
def __init__(self, kar):
self.kar = kar
def _add_person(person, persons, year, days=None):
"Create LagerPerson from person and add to persons lists."
person_ob = person.get()
postal_address = "%s %s" % (person_ob.zip_code, person_ob.zip_name)
lager_person = LagerPerson(person, person_ob.getname(), person_ob.birthdate.year, person_ob.getyearsoldthisyear(year), postal_address)
if days is not None:
lager_person.days = days
persons.append(lager_person)
def createLagerbidragGroup(limits, scoutgroup, troops, bidrag):
from_date_time = datetime.strptime(bidrag.dateFrom + " 00:00", DATE_TIME_FORMAT)
to_date_time = datetime.strptime(bidrag.dateTo + " 23:59", DATE_TIME_FORMAT)
year = to_date_time.year
persons = []
person_days = {}
validateLagetbidragInput(from_date_time, to_date_time, limits)
for troop in troops:
# Count number of days participating
for meeting in Meeting.query(Meeting.datetime >= from_date_time, Meeting.datetime <= to_date_time, Meeting.troop == troop.key).fetch():
for person in meeting.attendingPersons:
if person not in person_days:
person_days[person] = 1
else:
person_days[person] += 1
# Collect number of persons
for person, days in person_days.items():
_add_person(person, persons, year, days)
return createLagerbidragReport(limits, scoutgroup, persons, bidrag)
def createLagerbidrag(limits, scoutgroup, trooppersons, troopkey_key, bidrag):
from_date_time = datetime.strptime(bidrag.dateFrom + " 00:00", DATE_TIME_FORMAT)
to_date_time = datetime.strptime(bidrag.dateTo + " 23:59", DATE_TIME_FORMAT)
year = to_date_time.year
persons = []
validateLagetbidragInput(from_date_time, to_date_time, limits)
# Collect number of persons
for troopperson in trooppersons:
_add_person(troopperson.person, persons, year)
# Count number of days participating
for meeting in Meeting.query(Meeting.datetime >= from_date_time, Meeting.datetime <= to_date_time, Meeting.troop == troopkey_key).fetch():
for person in persons:
is_attending = person.person in meeting.attendingPersons
if is_attending:
person.days += 1
return createLagerbidragReport(limits, scoutgroup, persons, bidrag)
def validateLagetbidragInput(from_date_time, to_date_time, limits):
"Check the number of days compared to limits."
delta = to_date_time.date() - from_date_time.date()
nr_days = delta.days + 1 # The 00:00 - 23:59 does not give any extra day
if nr_days > limits.max_days:
raise ValueError('Lägret får max vara %d dagar' % limits.max_days)
if nr_days < limits.min_days:
raise ValueError('Lägret måste vara minst %d dagar' % limits.min_days)
def createLagerbidragReport(limits, scoutgroup, persons, bidrag):
"Create a container with lagerbidrag data. The input bidrag instance will be enhanced."
container = LagerBidragContainer()
container.persons = persons
# Filter out persons participation at least limits.min_days days
container.persons = [p for p in container.persons if p.days >= limits.min_days]
# Sort by number of days to get the persons over limits.max_age with most days first
container.persons.sort(key=lambda x: x.days, reverse=True)
# sum number of persons and days for person under max_age
for person in container.persons:
if person.age > limits.max_age:
bidrag.overMaxAge = bidrag.overMaxAge + 1
container.nr_older_persons += 1
bidrag.nightsOverMaxAge += person.days - 1
elif person.age >= limits.min_age:
bidrag.uptoMaxAge = bidrag.uptoMaxAge + 1
bidrag.nightsUpToMaxAge += person.days - 1
bidrag.nights += person.days - 1
bidrag.days += person.days
container.nr_young_persons += 1
else:
container.nr_under_min_age += 1
if limits.count_over_max_age:
# Count number of days for person over max_age
allowed_over_max_age = math.floor(bidrag.uptoMaxAge / 3)
count_over_max_age = 0
for person in container.persons:
if count_over_max_age == allowed_over_max_age:
break
if person.age > limits.max_age:
count_over_max_age += 1
bidrag.nights += person.days - 1
bidrag.days += person.days
container.persons.sort(key=cmp_to_key(person_sort))
number_of_persons = container.nr_young_persons + container.nr_older_persons + container.nr_under_min_age
assert number_of_persons == len(container.persons)
container.nr_persons_total = number_of_persons
# Set up divider to get two columns of data (used by Gbg template)
tmp_divider = int(math.ceil(number_of_persons / 2.0))
bidrag.divider = 25 if tmp_divider < 25 else tmp_divider
# Add empty persons
container.persons.extend([LagerPerson() for i in range(0, bidrag.divider * 2 - len(container.persons))])
container.numbers = [x for x in range(0, bidrag.divider)]
container.bidrag = bidrag
return container