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

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

Add pdf utility methods first pdf slip components.

  • Property svn:keywords set to Id
File size: 14.4 KB
RevLine 
[12018]1## $Id: utils.py 12051 2014-11-24 11:27:53Z henrik $
[11956]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"""
[12035]20import re
21import os
[11956]22import grok
[12051]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
[12032]33from waeup.ikoba.interfaces import MessageFactory as _
[12051]34from waeup.ikoba.interfaces import (
35    CREATED, STARTED, REQUESTED, APPROVED, IIkobaUtils, IExtFileStore)
[11956]36from waeup.ikoba.customers.interfaces import ICustomersUtils
[12051]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)
[11956]41
[12035]42RE_CUSTID_NON_NUM = re.compile('[^\d]+')
[11985]43
[11956]44def generate_customer_id():
45    customers = grok.getSite()['customers']
46    new_id = customers.unique_customer_id
47    return new_id
48
[12035]49def path_from_custid(customer_id):
50    """Convert a customer_id into a predictable relative folder path.
[11985]51
[12035]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
[12051]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(_('Scanned Documents'), 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
[11956]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'
[11971]276
[12018]277    CUSTMANAGE_STATES = (STARTED,)
278
279    DOCMANAGE_STATES = (APPROVED,)
[12032]280
[12035]281    SKIP_UPLOAD_VIEWLETS = ()
282
[12032]283    TRANSLATED_STATES = {
284        CREATED: _('created'),
285        STARTED: _('started'),
286        REQUESTED: _('requested'),
287        APPROVED: _('approved'),
288        }
[12051]289
290    def getPDFCreator(self, context=None):
291        """Get a pdf creator suitable for `context`.
292
293        The default implementation always returns the default creator.
294        """
295        return getUtility(IPDFCreator)
296
297    def renderPDF(self, view, filename='slip.pdf', customer=None,
298                  customerview=None,
299                  tableheader=[], tabledata=[],
300                  note=None, signatures=None, sigs_in_footer=(),
301                  show_scans=True, show_history=True, topMargin=1.5,
302                  omit_fields=()):
303        """Render pdf slips for various pages.
304        """
305        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
306        style = getSampleStyleSheet()
307        creator = self.getPDFCreator()
308        data = []
309        doc_title = view.label
310        author = '%s (%s)' % (view.request.principal.title,
311                              view.request.principal.id)
312        footer_text = view.label.split('\n')
313        if len(footer_text) > 2:
314            # We can add a department in first line
315            footer_text = footer_text[1]
316        else:
317            # Only the first line is used for the footer
318            footer_text = footer_text[0]
319        if getattr(customer, 'customer_id', None) is not None:
320            footer_text = "%s - %s - " % (customer.customer_id, footer_text)
321
322        # Insert customer data table
323        if customer is not None:
324            bd_translation = trans(_('Base Data'), portal_language)
325            data.append(Paragraph(bd_translation, HEADING_STYLE))
326            data.append(render_customer_data(
327                customerview, view.context, omit_fields, lang=portal_language,
328                slipname=filename))
329
330        # Insert widgets
331        if view.form_fields:
332            data.append(Paragraph(view.title, HEADING_STYLE))
333            separators = getattr(self, 'SEPARATORS_DICT', {})
334            table = creator.getWidgetsTable(
335                view.form_fields, view.context, None, lang=portal_language,
336                separators=separators)
337            data.append(table)
338
339        # Insert scanned docs
340        if show_scans:
341            data.extend(docs_as_flowables(view, portal_language))
342
343        # Insert history
344        if show_history:
345            hist_translation = trans(_('Workflow History'), portal_language)
346            data.append(Paragraph(hist_translation, HEADING_STYLE))
347            data.extend(creator.fromStringList(customer.history.messages))
348
349        # Insert content tables (optionally on second page)
350        if hasattr(view, 'tabletitle'):
351            for i in range(len(view.tabletitle)):
352                if tabledata[i] and tableheader[i]:
353                    #data.append(PageBreak())
354                    #data.append(Spacer(1, 20))
355                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
356                    data.append(Spacer(1, 8))
357                    contenttable = render_table_data(tableheader[i],tabledata[i])
358                    data.append(contenttable)
359
360        # Insert signatures
361        # XXX: We are using only sigs_in_footer in waeup.ikoba, so we
362        # do not have a test for the following lines.
363        if signatures and not sigs_in_footer:
364            data.append(Spacer(1, 20))
365            # Render one signature table per signature to
366            # get date and signature in line.
367            for signature in signatures:
368                signaturetables = get_signature_tables(signature)
369                data.append(signaturetables[0])
370
371        view.response.setHeader(
372            'Content-Type', 'application/pdf')
373        try:
374            pdf_stream = creator.create_pdf(
375                data, None, doc_title, author=author, footer=footer_text,
376                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
377        except IOError:
378            view.flash('Error in image file.')
379            return view.redirect(view.url(view.context))
380        return pdf_stream
Note: See TracBrowser for help on using the repository browser.