source: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/student_payment_statistics.py

Last change on this file was 15244, checked in by Henrik Bettermann, 6 years ago

Catch error and resize table.

  • Property svn:keywords set to Id
File size: 14.3 KB
Line 
1## $Id: student_payment_statistics.py 15244 2018-11-15 10:24:58Z henrik $
2##
3## Copyright (C) 2015 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18import grok
19from zope.i18n import translate
20from zope.catalog.interfaces import ICatalog
21from zope.component import queryUtility, getUtility
22from zope.interface import implementer, Interface, Attribute
23from waeup.kofa.interfaces import (
24    IKofaUtils,
25    academic_sessions_vocab, registration_states_vocab)
26from waeup.kofa.students.vocabularies import StudyLevelSource
27from waeup.kofa.interfaces import MessageFactory as _
28from waeup.kofa.reports import IReport
29
30
31class IStudentPaymentStatisticsReport(IReport):
32
33    session = Attribute('Session to report')
34    mode = Attribute('Study modes group to report')
35    level = Attribute('Level to report')
36    creation_dt_string = Attribute('Human readable report creation datetime')
37
38
39def get_student_payment_stats(session, mode, level, entry_session,
40        p_session, breakdown):
41    """Get payment amounts of students in a certain session, study mode
42    and current level.
43
44    Returns a table ordered by code (faculty or department, one per row) and
45    various payment categories (cols). The result is a 3-tuple representing
46    ((<CODES>), (<PAYMENTCATS>), (<NUM_OF_STUDENTS>)). The
47    (<NUM_OF_STUDENTS>) is an n-tuple with each entry containing the
48    number of students found in that faculty/department and with the respective
49    payment.
50
51    Sample result:
52
53      >>> ((u'FAC1', u'FAC2'),
54      ...  ('clearance', 'gown', 'hostel_maintenance', 'schoolfee'),
55      ...  ((12, 10, 1, 14), (0, 5, 25, 16)))
56
57    This result means: there are 5 students in FAC2 in who have paid
58    gown fee while 12 students in 'FAC1' who have paid for clearance.
59    """
60    site = grok.getSite()
61    payment_cats_dict = getUtility(IKofaUtils).REPORTABLE_PAYMENT_CATEGORIES
62    payment_cats = tuple(sorted(payment_cats_dict.keys())) + ('Total',)
63    if breakdown == 'faccode':
64        paths = tuple(sorted([x for x in site['faculties'].keys()],
65                                 key=lambda x: x.lower()))
66    elif breakdown == 'depcode':
67        faculties = site['faculties']
68        deppaths = []
69        for fac in faculties.values():
70            for dep in fac.values():
71                deppaths.append('%s/%s' % (fac.code, dep.code))
72        paths = tuple(sorted([x for x in deppaths],
73                                 key=lambda x: x.lower()))
74    # XXX: Here we do _one_ query and then examine the single
75    #   students. One could also do multiple queries and just look for
76    #   the result length (not introspecting the delivered students
77    #   further).
78    cat = queryUtility(ICatalog, name="students_catalog")
79    if session == 0:
80        result = cat.searchResults(student_id=(None, None))
81    else:
82        result = cat.searchResults(current_session=(session, session))
83    table = [[0 for x in xrange(2*len(payment_cats))]
84                for y in xrange(len(paths)+1)]
85    mode_groups = getUtility(IKofaUtils).MODE_GROUPS
86    for stud in result:
87        if mode != 'All' and stud.current_mode \
88            and stud.current_mode not in mode_groups[mode]:
89            continue
90        if level != 0 and stud.current_level != level:
91            continue
92        if entry_session != 0 and  stud.entry_session != entry_session:
93            continue
94        if None in  (getattr(stud, 'faccode'), getattr(stud, 'depcode')):
95            continue
96        # Dep codes are not unique.
97        if breakdown == 'depcode':
98            row = paths.index(
99                '%s/%s' % (getattr(stud, 'faccode'), getattr(stud, 'depcode')))
100        else:
101            row = paths.index(getattr(stud, breakdown))
102        for key in stud['payments'].keys():
103            ticket = stud['payments'][key]
104            if ticket.p_category not in payment_cats:
105                continue
106            if ticket.p_state != 'paid':
107                continue
108            if p_session != 0 and ticket.p_session != p_session:
109                continue
110            col = payment_cats.index(ticket.p_category)
111
112            # payments in category and fac/dep
113            table[row][2*col] += 1 # number of payments
114            table[row][2*col+1] += ticket.amount_auth
115
116            # payments in category
117            table[-1][2*col] += 1
118            table[-1][2*col+1] += ticket.amount_auth
119
120            # payments in fac/dep
121            table[row][-2] += 1
122            table[row][-1] += ticket.amount_auth
123
124            # all payments
125            table[-1][-2] += 1
126            table[-1][-1] += ticket.amount_auth
127
128    # Build table header
129    table_header = ['' for x in xrange(len(payment_cats))]
130    for cat in payment_cats:
131        cat_title = payment_cats_dict.get(cat, cat)
132        table_header[payment_cats.index(cat)] = cat_title
133
134    # Turn lists into tuples
135    table = tuple([tuple(row) for row in table])
136
137    paths = paths + (u'Total',)
138    return (paths, table_header, table)
139
140from reportlab.lib import colors
141from reportlab.lib.styles import getSampleStyleSheet
142from reportlab.lib.units import cm
143from reportlab.platypus import Paragraph, Table, Spacer
144from waeup.kofa.reports import IReport, IReportGenerator
145from waeup.kofa.reports import Report
146from waeup.kofa.browser.interfaces import IPDFCreator
147from waeup.kofa.students.reports.level_report import TTR
148
149from waeup.kofa.students.reports.student_statistics import (
150    tbl_data_to_table, STYLE)
151
152def tbl_data_to_table(row_names, col_names, data):
153    result = []
154    new_col_names = []
155    for name in col_names:
156        new_col_names.append(name.replace(' ', '\n'))
157    head = [''] + [col_name for col_name in new_col_names]
158    result = [head]
159    for idx, row_name in enumerate(row_names):
160        row = []
161        i = 0
162        while i < len(data[idx]):
163            if row_name == 'Total':
164                row.append(
165                    "%s\n(%s)" % ("{:,}".format(data[idx][i+1]), data[idx][i]))
166            else:
167                row.append(
168                    "%s (%s)" % ("{:,}".format(data[idx][i+1]), data[idx][i]))
169            i += 2
170        row = [row_name] + row
171        result.append(row)
172    return result
173
174TABLE_STYLE = [
175    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
176    ('FONT', (0,0), (0,-1), 'Helvetica-Bold', 8),
177    ('FONT', (0,0), (-1,0), 'Helvetica-Bold', 8),
178    ('FONT', (0,-1), (-1,-1), 'Helvetica-Bold', 8),
179    ('FONT', (-1,0), (-1,-1), 'Helvetica-Bold', 8),
180    ('ALIGN', (0,0), (-1,-1), 'RIGHT'),
181    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
182    ('LINEBELOW', (0,-1), (-1,-1), 0.25, colors.black),
183    ('LINEAFTER', (-1,0), (-1,-1), 0.25, colors.black),
184    ('LINEBEFORE', (-1,0), (-1,-1), 0.25, colors.black),
185    #('LINEABOVE', (0,-1), (-1,-1), 1.0, colors.black),
186    #('LINEABOVE', (0,0), (-1,0), 0.25, colors.black),
187    ]
188
189@implementer(IStudentPaymentStatisticsReport)
190class StudentPaymentStatisticsReport(Report):
191    data = None
192    session = None
193    mode = None
194    entry_session = None
195    p_session = None
196    pdfcreator = 'landscape'
197    title = translate(_('Student Payment Statistics'))
198
199    @property
200    def title(self):
201        return translate(_('Student Payment Statistics'))
202
203    def __init__(self, session, mode, level, entry_session, p_session,
204                 breakdown, author='System'):
205        super(StudentPaymentStatisticsReport, self).__init__(
206            args=[session, mode, level, entry_session, p_session, breakdown],
207            kwargs={'author':author})
208        self.sessioncode = session
209        self.levelcode = level
210        self.entrysessioncode = entry_session
211        self.psessioncode = p_session
212        self.studylevelsource = StudyLevelSource().factory
213        self.portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
214        if session != 0:
215            self.session = academic_sessions_vocab.getTerm(session).title
216        else:
217            self.session = 'All sessions'
218        if entry_session != 0:
219            self.entry_session = academic_sessions_vocab.getTerm(
220                entry_session).title
221        else:
222            self.entry_session = 'All sessions'
223        if p_session != 0:
224            self.p_session = academic_sessions_vocab.getTerm(
225                p_session).title
226        else:
227            self.p_session = 'All sessions'
228        if mode == 'All':
229            self.mode = 'All study modes'
230        else:
231            self.mode = mode
232        if level == 0:
233            self.level = 'All levels'
234        else:
235            self.level = translate(
236                self.studylevelsource.getTitle(None, int(level)),
237                'waeup.kofa', target_language=self.portal_language)
238        self.breakdown = breakdown
239        self.author = author
240        self.creation_dt_string = self.creation_dt.astimezone(
241            getUtility(IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
242        self.data = get_student_payment_stats(
243            session, mode, level, entry_session, p_session, breakdown)
244
245    def create_pdf(self, job_id):
246        creator = getUtility(IPDFCreator, name=self.pdfcreator)
247        # maxlength = len(str(self.data[2][-1][-1])) \
248        #     + len(str(self.data[2][-1][-2])) \
249        #     + 3
250        # maxlength = max(maxlength, 10)
251        table_data = tbl_data_to_table(*self.data)
252        # col_widths = [None,] + [.17*cm*maxlength] * len(self.data[1]) + [None,]
253        pdf_data = [Paragraph('<b>%s - Report %s</b>'
254                              % (self.creation_dt_string, job_id),
255                              STYLE["Normal"]),
256                    Spacer(1, 12),]
257        pdf_data += [Paragraph(
258                    translate(
259                        'Study Mode: ${a}<br />'
260                        'Academic Session: ${b}<br />Level: ${c}<br />'
261                        'Entry Session: ${d}<br />Payment Session: ${e}',
262                        mapping = {'a':self.mode,
263                                   'b':self.session,
264                                   'c':self.level,
265                                   'd':self.entry_session,
266                                   'e':self.p_session,
267                                   }),
268                    STYLE["Normal"]),
269                    Spacer(1, 12),]
270        pdf_data += [
271            Table(table_data, style=TABLE_STYLE)]
272        right_footer = translate(
273            _('Student Payments - ${a} - ${b} - ${c} - ',
274            mapping = {'a':self.mode, 'b':self.session, 'c':self.level}))
275        pdf = creator.create_pdf(
276            pdf_data, None, self.title, self.author, right_footer
277            )
278        return pdf
279
280@implementer(IReportGenerator)
281class StudentPaymentStatisticsReportGenerator(grok.GlobalUtility):
282
283    title = _('Student Payment Statistics')
284    grok.name('student_payment_stats')
285
286    def generate(
287        self, site, session=None, mode=None,
288        level=None, entry_session=None, p_session=None,
289        breakdown=None, author=None):
290        result = StudentPaymentStatisticsReport(
291            session=session, mode=mode, level=level,
292            entry_session=entry_session, p_session=p_session,
293            breakdown=breakdown, author=author)
294        return result
295
296###############################################################
297## Browser related stuff
298##
299## XXX: move to local browser module
300###############################################################
301from waeup.kofa.browser.layout import KofaPage
302from waeup.kofa.interfaces import academic_sessions_vocab
303from waeup.kofa.reports import get_generators
304from waeup.kofa.browser.breadcrumbs import Breadcrumb
305grok.templatedir('browser_templates')
306
307from waeup.kofa.students.reports.student_statistics import (
308    StudentStatisticsReportGeneratorPage,
309    StudentStatisticsReportPDFView)
310
311class StudentPaymentStatisticsReportGeneratorPage(
312    StudentStatisticsReportGeneratorPage):
313
314    grok.context(StudentPaymentStatisticsReportGenerator)
315    grok.template('studentpaymentstatisticsreportgeneratorpage')
316
317    label = _('Create student payment statistics report')
318
319    def update(
320        self, CREATE=None, session=None, mode=None, level=None,
321        entry_session=None, p_session=None, breakdown=None):
322        self.parent_url = self.url(self.context.__parent__)
323        self._set_session_values()
324        self._set_mode_values()
325        self._set_level_values()
326        self._set_breakdown_values()
327        if CREATE and session:
328            # create a new report job for students by session and level
329            container = self.context.__parent__
330            user_id = self.request.principal.id
331            kw = dict(
332                session=int(session),
333                mode=mode,
334                level=int(level),
335                entry_session=int(entry_session),
336                p_session=int(p_session),
337                breakdown=breakdown)
338            self.flash(_('New report is being created in background'))
339            job_id = container.start_report_job(
340                self.generator_name, user_id, kw=kw)
341            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
342            grok.getSite().logger.info(
343                '%s - report %s created: %s (session=%s, mode=%s, level=%s, '
344                'entry_session=%s, p_session=%s, breakdown=%s)' % (
345                ob_class, job_id, self.context.title,
346                session, mode, level, entry_session, p_session, breakdown))
347            self.redirect(self.parent_url)
348            return
349        return
350
351    def _set_session_values(self):
352        vocab_terms = academic_sessions_vocab.by_value.values()
353        self.sessions = [(x.title, x.token) for x in vocab_terms]
354        self.sessions_plus = [(u'All', 0)] + self.sessions
355        return
356
357class StudentPaymentStatisticsReportPDFView(StudentStatisticsReportPDFView):
358
359    grok.context(IStudentPaymentStatisticsReport)
360
361    def _filename(self):
362        return 'StudentPaymentStatisticsReport_rno%s_%s.pdf' % (
363            self.context.__name__,
364            self.context.creation_dt_string)
Note: See TracBrowser for help on using the repository browser.