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

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

Add PDFDocumentSlipPage and related components.

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