source: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/utils.py @ 12793

Last change on this file since 12793 was 12793, checked in by Henrik Bettermann, 10 years ago

Render all payment data (depending on interface) on PDFContractReceiptPage.

  • Property svn:keywords set to Id
File size: 19.6 KB
Line 
1## $Id: utils.py 12793 2015-03-19 11:27:36Z henrik $
2##
3## Copyright (C) 2014 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##
18"""General helper functions and utilities for the customer section.
19"""
20import re
21import os
22import grok
23from copy import deepcopy
24from StringIO import StringIO
25from reportlab.lib import colors
26from reportlab.lib.units import cm
27from reportlab.lib.pagesizes import A4
28from reportlab.lib.styles import getSampleStyleSheet
29from reportlab.platypus import Paragraph, Image, Table, Spacer
30from reportlab.platypus.flowables import PageBreak
31from PyPDF2 import PdfFileMerger, PdfFileReader, PdfFileWriter
32from zope.event import notify
33from zope.schema.interfaces import ConstraintNotSatisfied
34from zope.component import getUtility, createObject
35from zope.formlib.form import setUpEditWidgets
36from zope.i18n import translate
37from waeup.ikoba.interfaces import MessageFactory as _
38from waeup.ikoba.interfaces import (
39    STARTED, CREATED, REQUESTED, APPROVED, SUBMITTED, VERIFIED, REJECTED,
40    EXPIRED, PROVISIONALLY, AWAITING,
41    IIkobaUtils, IExtFileStore)
42from waeup.ikoba.customers.interfaces import ICustomersUtils
43from waeup.ikoba.browser.interfaces import IPDFCreator
44from waeup.ikoba.browser.pdf import (
45    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE, HEADLINE1_STYLE,
46    get_signature_tables, get_qrcode)
47from waeup.ikoba.payments.interfaces import STATE_PAID
48
49RE_CUSTID_NON_NUM = re.compile('[^\d]+')
50
51def generate_customer_id():
52    customers = grok.getSite()['customers']
53    new_id = customers.unique_customer_id
54    return new_id
55
56def path_from_custid(customer_id):
57    """Convert a customer_id into a predictable relative folder path.
58
59    Used for storing files.
60
61    Returns the name of folder in which files for a particular customer
62    should be stored. This is a relative path, relative to any general
63    customers folder with 5 zero-padded digits (except when customer_id
64    is overlong).
65
66    We normally map 1,000 different customer ids into one single
67    path. For instance ``K1000000`` will give ``01000/K1000000``,
68    ``K1234567`` will give ``0123/K1234567`` and ``K12345678`` will
69    result in ``1234/K12345678``.
70
71    For lower numbers < 10**6 we return the same path for up to 10,000
72    customer_ids. So for instance ``KM123456`` will result in
73    ``00120/KM123456`` (there will be no path starting with
74    ``00123``).
75
76    Works also with overlong number: here the leading zeros will be
77    missing but ``K123456789`` will give reliably
78    ``12345/K123456789`` as expected.
79    """
80    # remove all non numeric characters and turn this into an int.
81    num = int(RE_CUSTID_NON_NUM.sub('', customer_id))
82    if num < 10**6:
83        # store max. of 10000 custs per folder and correct num for 5 digits
84        num = num / 10000 * 10
85    else:
86        # store max. of 1000 custs per folder
87        num = num / 1000
88    # format folder name to have 5 zero-padded digits
89    folder_name = u'%05d' % num
90    folder_name = os.path.join(folder_name, customer_id)
91    return folder_name
92
93SLIP_STYLE = [
94    ('VALIGN',(0,0),(-1,-1),'TOP'),
95    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
96    ]
97
98CONTENT_STYLE = [
99    ('VALIGN',(0,0),(-1,-1),'TOP'),
100    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
101    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
102    #('BACKGROUND',(0,0),(-1,0),colors.black),
103    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
104    ('BOX', (0,0), (-1,-1), 1, colors.black),
105
106    ]
107
108FONT_SIZE = 10
109FONT_COLOR = 'black'
110
111def trans(text, lang):
112    # shortcut
113    return translate(text, 'waeup.ikoba', target_language=lang)
114
115
116def formatted_text(text, color=FONT_COLOR, lang='en'):
117    """Turn `text`, `color` and `size` into an HTML snippet.
118
119    The snippet is suitable for use with reportlab and generating PDFs.
120    Wraps the `text` into a ``<font>`` tag with passed attributes.
121
122    Also non-strings are converted. Raw strings are expected to be
123    utf-8 encoded (usually the case for widgets etc.).
124
125    Finally, a br tag is added if widgets contain div tags
126    which are not supported by reportlab.
127
128    The returned snippet is unicode type.
129    """
130    if not isinstance(text, unicode):
131        if isinstance(text, basestring):
132            text = text.decode('utf-8')
133        else:
134            text = unicode(text)
135    if text == 'None':
136        text = ''
137    # Mainly for boolean values we need our customized
138    # localisation of the zope domain
139    text = translate(text, 'zope', target_language=lang)
140    text = text.replace('</div>', '<br /></div>')
141    tag1 = u'<font color="%s">' % (color)
142    return tag1 + u'%s</font>' % text
143
144def render_customer_data(customerview, context, omit_fields=(),
145                        lang='en', slipname=None):
146    """Render customer table for an existing frame.
147    """
148    width, height = A4
149    set_up_widgets(customerview, ignore_request=True)
150    data_left = []
151    data_middle = []
152    style = getSampleStyleSheet()
153    img = getUtility(IExtFileStore).getFileByContext(
154        customerview.context, attr='passport.jpg')
155    if img is None:
156        from waeup.ikoba.browser import DEFAULT_PASSPORT_IMAGE_PATH
157        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
158    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
159    data_left.append([doc_img])
160    #data.append([Spacer(1, 12)])
161
162    f_label = trans(_('Name:'), lang)
163    f_label = Paragraph(f_label, ENTRY1_STYLE)
164    f_text = formatted_text(customerview.context.display_fullname)
165    f_text = Paragraph(f_text, ENTRY1_STYLE)
166    data_middle.append([f_label,f_text])
167
168    for widget in customerview.widgets:
169        if 'name' in widget.name:
170            continue
171        f_label = translate(
172            widget.label.strip(), 'waeup.ikoba',
173            target_language=lang)
174        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
175        f_text = formatted_text(widget(), lang=lang)
176        f_text = Paragraph(f_text, ENTRY1_STYLE)
177        data_middle.append([f_label,f_text])
178
179    if getattr(customerview.context, 'certcode', None):
180        if not 'date_of_birth' in omit_fields:
181            f_label = trans(_('Date of Birth:'), lang)
182            f_label = Paragraph(f_label, ENTRY1_STYLE)
183            date_of_birth = customerview.context.date_of_birth
184            tz = getUtility(IIkobaUtils).tzinfo
185            date_of_birth = to_timezone(date_of_birth, tz)
186            if date_of_birth is not None:
187                date_of_birth = date_of_birth.strftime("%d/%m/%Y")
188            f_text = formatted_text(date_of_birth)
189            f_text = Paragraph(f_text, ENTRY1_STYLE)
190            data_middle.append([f_label,f_text])
191
192    # append QR code to the right
193    if slipname:
194        url = customerview.url(context, slipname)
195        data_right = [[get_qrcode(url, width=70.0)]]
196        table_right = Table(data_right,style=SLIP_STYLE)
197    else:
198        table_right = None
199
200    table_left = Table(data_left,style=SLIP_STYLE)
201    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
202    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
203    return table
204
205def render_table_data(tableheader, tabledata, lang='en'):
206    """Render children table for an existing frame.
207    """
208    data = []
209    #data.append([Spacer(1, 12)])
210    line = []
211    style = getSampleStyleSheet()
212    for element in tableheader:
213        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
214        field = Paragraph(field, style["Normal"])
215        line.append(field)
216    data.append(line)
217    for ticket in tabledata:
218        line = []
219        for element in tableheader:
220              field = formatted_text(getattr(ticket,element[1],u' '))
221              field = Paragraph(field, style["Normal"])
222              line.append(field)
223        data.append(line)
224    table = Table(data,colWidths=[
225        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
226    return table
227
228def docs_as_flowables(view, lang='en'):
229    """Create reportlab flowables out of scanned docs.
230    """
231    # XXX: fix circular import problem
232    from waeup.ikoba.browser.fileviewlets import FileManager
233    from waeup.ikoba.browser import DEFAULT_IMAGE_PATH
234    style = getSampleStyleSheet()
235    data = []
236
237    # Collect viewlets
238    fm = FileManager(view.context, view.request, view)
239    fm.update()
240    if fm.viewlets:
241        sc_translation = trans(_('Connected Files'), lang)
242        data.append(Paragraph(sc_translation, HEADING_STYLE))
243        # Insert list of scanned documents
244        table_data = []
245        for viewlet in fm.viewlets:
246            if viewlet.file_exists:
247                # Show viewlet only if file exists
248                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
249                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
250                    view.context, attr=viewlet.download_name), 'name', None)
251                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
252                if img_path is None:
253                    pass
254                elif img_path[-4:] == '.pdf':
255                    f_text = Paragraph('%s (see file attached)' % (
256                        viewlet.title,), ENTRY1_STYLE)
257                elif not img_path[-4:] in ('.jpg', '.JPG'):
258                    # reportlab requires jpg images, I think.
259                    f_text = Paragraph('%s (no preview available)' % (
260                        viewlet.title,), ENTRY1_STYLE)
261                else:
262                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
263                table_data.append([f_label, f_text])
264        if table_data:
265            # safety belt; empty tables lead to problems.
266            data.append(Table(table_data, style=SLIP_STYLE))
267    return data
268
269
270def set_up_widgets(view, ignore_request=False):
271    view.adapters = {}
272    view.widgets = setUpEditWidgets(
273        view.form_fields, view.prefix, view.context, view.request,
274        adapters=view.adapters, for_display=True,
275        ignore_request=ignore_request
276        )
277
278def generate_contract_id():
279    new_id = grok.getSite().unique_contract_id
280    return new_id
281
282class CustomersUtils(grok.GlobalUtility):
283    """A collection of methods subject to customization.
284    """
285    grok.implements(ICustomersUtils)
286
287    #: A prefix used when generating new customer ids. Each customer id will
288    #: start with this string. The default is 'K'.
289    CUSTOMER_ID_PREFIX = u'K'
290
291    CUSTMANAGE_CUSTOMER_STATES = (STARTED,)
292
293    DOCMANAGE_CUSTOMER_STATES = (REQUESTED, APPROVED, PROVISIONALLY)
294
295    DOCMANAGE_DOCUMENT_STATES = (CREATED,)
296
297    CONMANAGE_CUSTOMER_STATES = deepcopy(DOCMANAGE_CUSTOMER_STATES)
298
299    CONMANAGE_CONTRACT_STATES = (CREATED,)
300
301    TRANSLATED_CUSTOMER_STATES = {
302        CREATED: _('created'),
303        STARTED: _('started'),
304        REQUESTED: _('requested'),
305        APPROVED: _('approved'),
306        PROVISIONALLY: _('provisionally approved'),
307        }
308
309    TRANSLATED_CONTRACT_STATES = {
310        CREATED: _('created'),
311        AWAITING: _('awaiting payment'),
312        SUBMITTED: _('submitted for approval'),
313        APPROVED: _('approved'),
314        REJECTED: _('rejected'),
315        EXPIRED:_('expired')
316        }
317
318    TRANSLATED_DOCUMENT_STATES = {
319        CREATED: _('created'),
320        SUBMITTED: _('submitted for verification'),
321        VERIFIED: _('verified'),
322        REJECTED: _('rejected'),
323        EXPIRED:_('expired')
324        }
325
326    DOCTYPES_DICT = {
327        'CustomerSampleDocument': _('Sample Document'),
328        'CustomerPDFDocument': _('PDF Document'),
329        }
330
331    CONTYPES_DICT = {
332        'SampleContract': _('Sample Contract'),
333        }
334
335    SELECTABLE_DOCTYPES_DICT = deepcopy(DOCTYPES_DICT)
336
337    SELECTABLE_CONTYPES_DICT = deepcopy(CONTYPES_DICT)
338
339    EXPORTER_NAMES = ('customers', 'customersampledocuments', 'samplecontracts')
340
341    def getPDFCreator(self, context=None):
342        """Get a pdf creator suitable for `context`.
343
344        The default implementation always returns the default creator.
345        """
346        return getUtility(IPDFCreator)
347
348    def renderPDF(self, view, filename='slip.pdf', customer=None,
349                  customerview=None,
350                  tableheader=[], tabledata=[],
351                  note=None, signatures=None, sigs_in_footer=(),
352                  show_scans=True, show_history=True, topMargin=1.5,
353                  omit_fields=(), mergefiles=None, watermark=None):
354        """Render pdf slips for various pages.
355        """
356        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
357        style = getSampleStyleSheet()
358        creator = self.getPDFCreator()
359        data = []
360        doc_title = view.label
361        author = '%s (%s)' % (view.request.principal.title,
362                              view.request.principal.id)
363        footer_text = view.label.split('\n')
364        if len(footer_text) > 2:
365            # We can add a department in first line
366            footer_text = footer_text[1]
367        else:
368            # Only the first line is used for the footer
369            footer_text = footer_text[0]
370        if getattr(customer, 'customer_id', None) is not None:
371            footer_text = "%s - %s - " % (customer.customer_id, footer_text)
372
373        # Insert customer data table
374        if customer is not None:
375            if view.__name__ == 'contract_slip.pdf':
376                bd_translation = trans(_('Contractor'), portal_language)
377                data.append(Paragraph(bd_translation, HEADING_STYLE))
378            if view.__name__ == 'document_slip.pdf':
379                bd_translation = trans(_('Document Owner'), portal_language)
380                data.append(Paragraph(bd_translation, HEADING_STYLE))
381            data.append(render_customer_data(
382                customerview, view.context, omit_fields, lang=portal_language,
383                slipname=filename))
384
385        # Insert widgets
386        if view.form_fields:
387            if view.__name__.startswith('contract'):
388                data_header = trans(_('Contract Data'), portal_language)
389                data.append(Paragraph(data_header, HEADING_STYLE))
390            separators = getattr(self, 'SEPARATORS_DICT', {})
391            table = creator.getWidgetsTable(
392                view.form_fields, view.context, None, lang=portal_language,
393                separators=separators)
394            data.append(table)
395
396        # Insert payment data
397        if getattr(view, 'payment_tuples', None) is not None:
398            data_header = trans(_('Payments'), portal_language)
399            data.append(Paragraph(data_header, HEADING_STYLE))
400            payment_number = 0
401            for payment_tuple in getattr(view, 'payment_tuples', None):
402                payment_number += 1
403                payment = payment_tuple[0]
404                payment_form_fields = payment_tuple[1]
405                if payment.state != STATE_PAID:
406                    continue
407                data.append(Paragraph("Payment %s" % payment_number, HEADLINE1_STYLE))
408                separators = getattr(self, 'SEPARATORS_DICT', {})
409                table = creator.getWidgetsTable(
410                    payment_form_fields, payment, None, lang=portal_language,
411                    separators=separators)
412                data.append(table)
413
414        # Insert scanned docs
415        if show_scans:
416            data.extend(docs_as_flowables(view, portal_language))
417
418        # Insert history
419        if show_history:
420            if getattr(view.context, 'history', None):
421                hist_translation = trans(_('Transaction History'),
422                    portal_language)
423                data.append(Paragraph(hist_translation, HEADING_STYLE))
424                data.extend(
425                    creator.fromStringList(view.context.history.messages))
426            else:
427                hist_translation = trans(_('Registration History'),
428                    portal_language)
429                data.append(Paragraph(hist_translation, HEADING_STYLE))
430                data.extend(creator.fromStringList(customer.history.messages))
431
432        # Insert validity period
433        if getattr(view, 'validity_period', None):
434            vp_translation = trans(_('Validity Period'), portal_language)
435            data.append(Paragraph(vp_translation, HEADING_STYLE))
436            vp = view.validity_period
437            data.append(Paragraph(vp, NOTE_STYLE))
438
439        # Insert content tables (optionally on second page)
440        if getattr(view, 'tabletitle', None):
441            for i in range(len(view.tabletitle)):
442                if tabledata[i] and tableheader[i]:
443                    #data.append(PageBreak())
444                    #data.append(Spacer(1, 20))
445                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
446                    data.append(Spacer(1, 8))
447                    contenttable = render_table_data(tableheader[i],tabledata[i])
448                    data.append(contenttable)
449
450        # Insert terms and conditions
451        if getattr(view, 'terms_and_conditions', None):
452            tc_translation = trans(_('Terms and Conditions'), portal_language)
453            data.append(Paragraph(tc_translation, HEADING_STYLE))
454            tc = format_html(view.terms_and_conditions)
455            data.append(Paragraph(tc, NOTE_STYLE))
456
457        # Insert signatures
458        # XXX: We are using only sigs_in_footer in waeup.ikoba, so we
459        # do not have a test for the following lines.
460        if signatures and not sigs_in_footer:
461            data.append(Spacer(1, 20))
462            # Render one signature table per signature to
463            # get date and signature in line.
464            for signature in signatures:
465                signaturetables = get_signature_tables(signature)
466                data.append(signaturetables[0])
467
468        view.response.setHeader(
469            'Content-Type', 'application/pdf')
470        try:
471            pdf_stream = creator.create_pdf(
472                data, None, doc_title, author=author, footer=footer_text,
473                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
474        except IOError:
475            view.flash('Error in image file.')
476            return view.redirect(view.url(view.context))
477
478        if mergefiles:
479            merger = PdfFileMerger()
480            merger.append(StringIO(pdf_stream))
481            if watermark:
482                watermark = PdfFileReader(watermark)
483            for file in mergefiles:
484                if watermark:
485                    # Pass through all pages of each file
486                    # and merge with watermark page.
487                    marked_file = PdfFileWriter()
488                    orig_file = PdfFileReader(file[1])
489                    num_pages = orig_file.getNumPages()
490                    for num in range(num_pages):
491                        page = orig_file.getPage(num)
492                        page.mergePage(watermark.getPage(0))
493                        marked_file.addPage(page)
494                    # Save into a file-like object
495                    tmp1 = StringIO()
496                    marked_file.write(tmp1)
497                    # Append the file-like object
498                    merger.append(tmp1)
499                else:
500                    # Just append the file object
501                    merger.append(file[1])
502            # Save into a file-like object
503            tmp2 = StringIO()
504            merger.write(tmp2)
505            return tmp2.getvalue()
506
507        return pdf_stream
Note: See TracBrowser for help on using the repository browser.