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

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

Replace 'not displayable' in docs_as_flowables.

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