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

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

Move and rename some dicts.

  • Property svn:keywords set to Id
File size: 15.1 KB
Line 
1## $Id: utils.py 12088 2014-11-28 16:37:38Z 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_CUSTOMER_STATES = (STARTED,)
278
279    DOCMANAGE_CUSTOMER_STATES = (APPROVED,)
280
281    DOCMANAGE_DOCUMENT_STATES = (CREATED,)
282
283    APPMANAGE_CUSTOMER_STATES = DOCMANAGE_CUSTOMER_STATES
284
285    APPMANAGE_APPLICATION_STATES = (CREATED,)
286
287    SKIP_UPLOAD_VIEWLETS = ()
288
289    TRANSLATED_CUSTOMER_STATES = {
290        CREATED: _('created'),
291        STARTED: _('started'),
292        REQUESTED: _('requested'),
293        APPROVED: _('approved'),
294        }
295
296    DOCTYPES_DICT = {
297        'CustomerSampleDocument': 'Sample Document',
298        'CustomerPDFDocument': 'PDF Document',
299        }
300
301    SELECTABLE_DOCTYPES_DICT = DOCTYPES_DICT
302
303    def getPDFCreator(self, context=None):
304        """Get a pdf creator suitable for `context`.
305
306        The default implementation always returns the default creator.
307        """
308        return getUtility(IPDFCreator)
309
310    def renderPDF(self, view, filename='slip.pdf', customer=None,
311                  customerview=None,
312                  tableheader=[], tabledata=[],
313                  note=None, signatures=None, sigs_in_footer=(),
314                  show_scans=True, show_history=True, topMargin=1.5,
315                  omit_fields=()):
316        """Render pdf slips for various pages.
317        """
318        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
319        style = getSampleStyleSheet()
320        creator = self.getPDFCreator()
321        data = []
322        doc_title = view.label
323        author = '%s (%s)' % (view.request.principal.title,
324                              view.request.principal.id)
325        footer_text = view.label.split('\n')
326        if len(footer_text) > 2:
327            # We can add a department in first line
328            footer_text = footer_text[1]
329        else:
330            # Only the first line is used for the footer
331            footer_text = footer_text[0]
332        if getattr(customer, 'customer_id', None) is not None:
333            footer_text = "%s - %s - " % (customer.customer_id, footer_text)
334
335        # Insert customer data table
336        if customer is not None:
337            bd_translation = trans(_('Base Data'), portal_language)
338            data.append(Paragraph(bd_translation, HEADING_STYLE))
339            data.append(render_customer_data(
340                customerview, view.context, omit_fields, lang=portal_language,
341                slipname=filename))
342
343        # Insert widgets
344        if view.form_fields:
345            data.append(Paragraph(view.title, HEADING_STYLE))
346            separators = getattr(self, 'SEPARATORS_DICT', {})
347            table = creator.getWidgetsTable(
348                view.form_fields, view.context, None, lang=portal_language,
349                separators=separators)
350            data.append(table)
351
352        # Insert scanned docs
353        if show_scans:
354            data.extend(docs_as_flowables(view, portal_language))
355
356        # Insert history
357        if show_history:
358            if getattr(view.context, 'history', None):
359                hist_translation = trans(_('Document Workflow History'), portal_language)
360                data.append(Paragraph(hist_translation, HEADING_STYLE))
361                data.extend(creator.fromStringList(view.context.history.messages))
362            else:
363                hist_translation = trans(_('Customer Workflow History'), portal_language)
364                data.append(Paragraph(hist_translation, HEADING_STYLE))
365                data.extend(creator.fromStringList(customer.history.messages))
366
367        # Insert content tables (optionally on second page)
368        if hasattr(view, 'tabletitle'):
369            for i in range(len(view.tabletitle)):
370                if tabledata[i] and tableheader[i]:
371                    #data.append(PageBreak())
372                    #data.append(Spacer(1, 20))
373                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
374                    data.append(Spacer(1, 8))
375                    contenttable = render_table_data(tableheader[i],tabledata[i])
376                    data.append(contenttable)
377
378        # Insert signatures
379        # XXX: We are using only sigs_in_footer in waeup.ikoba, so we
380        # do not have a test for the following lines.
381        if signatures and not sigs_in_footer:
382            data.append(Spacer(1, 20))
383            # Render one signature table per signature to
384            # get date and signature in line.
385            for signature in signatures:
386                signaturetables = get_signature_tables(signature)
387                data.append(signaturetables[0])
388
389        view.response.setHeader(
390            'Content-Type', 'application/pdf')
391        try:
392            pdf_stream = creator.create_pdf(
393                data, None, doc_title, author=author, footer=footer_text,
394                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
395        except IOError:
396            view.flash('Error in image file.')
397            return view.redirect(view.url(view.context))
398        return pdf_stream
Note: See TracBrowser for help on using the repository browser.