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

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

Use proper data headers.

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