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

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

We need the file managing base components also in documents.

  • Property svn:keywords set to Id
File size: 17.4 KB
Line 
1## $Id: utils.py 12223 2014-12-14 08:49:01Z 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.browser.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    TRANSLATED_CUSTOMER_STATES = {
300        CREATED: _('created'),
301        STARTED: _('started'),
302        REQUESTED: _('requested'),
303        APPROVED: _('approved'),
304        }
305
306    TRANSLATED_CONTRACT_STATES = {
307        CREATED: _('created'),
308        SUBMITTED: _('submitted for approval'),
309        APPROVED: _('approved'),
310        REJECTED: _('rejected'),
311        EXPIRED:_('expired')
312        }
313
314    TRANSLATED_DOCUMENT_STATES = {
315        CREATED: _('created'),
316        SUBMITTED: _('submitted for verification'),
317        VERIFIED: _('verified'),
318        REJECTED: _('rejected'),
319        EXPIRED:_('expired')
320        }
321
322    DOCTYPES_DICT = {
323        'CustomerSampleDocument': _('Sample Document'),
324        'CustomerPDFDocument': _('PDF Document'),
325        }
326
327    CONTYPES_DICT = {
328        'SampleContract': _('Sample Contract'),
329        }
330
331    SELECTABLE_DOCTYPES_DICT = DOCTYPES_DICT
332
333    SELECTABLE_CONTYPES_DICT = CONTYPES_DICT
334
335    def getPDFCreator(self, context=None):
336        """Get a pdf creator suitable for `context`.
337
338        The default implementation always returns the default creator.
339        """
340        return getUtility(IPDFCreator)
341
342    def renderPDF(self, view, filename='slip.pdf', customer=None,
343                  customerview=None,
344                  tableheader=[], tabledata=[],
345                  note=None, signatures=None, sigs_in_footer=(),
346                  show_scans=True, show_history=True, topMargin=1.5,
347                  omit_fields=(), mergefiles=None, watermark=None):
348        """Render pdf slips for various pages.
349        """
350        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
351        style = getSampleStyleSheet()
352        creator = self.getPDFCreator()
353        data = []
354        doc_title = view.label
355        author = '%s (%s)' % (view.request.principal.title,
356                              view.request.principal.id)
357        footer_text = view.label.split('\n')
358        if len(footer_text) > 2:
359            # We can add a department in first line
360            footer_text = footer_text[1]
361        else:
362            # Only the first line is used for the footer
363            footer_text = footer_text[0]
364        if getattr(customer, 'customer_id', None) is not None:
365            footer_text = "%s - %s - " % (customer.customer_id, footer_text)
366
367        # Insert customer data table
368        if customer is not None:
369            bd_translation = trans(_('Base Data'), portal_language)
370            data.append(Paragraph(bd_translation, HEADING_STYLE))
371            data.append(render_customer_data(
372                customerview, view.context, omit_fields, lang=portal_language,
373                slipname=filename))
374
375        # Insert widgets
376        if view.form_fields:
377            data.append(Paragraph(view.title, HEADING_STYLE))
378            separators = getattr(self, 'SEPARATORS_DICT', {})
379            table = creator.getWidgetsTable(
380                view.form_fields, view.context, None, lang=portal_language,
381                separators=separators)
382            data.append(table)
383
384        # Insert scanned docs
385        if show_scans:
386            data.extend(docs_as_flowables(view, portal_language))
387
388        # Insert history
389        if show_history:
390            if getattr(view.context, 'history', None):
391                hist_translation = trans(_('${a} Workflow History',
392                                           mapping = {'a':view.context.translated_class_name}),
393                                        portal_language)
394                data.append(Paragraph(hist_translation, HEADING_STYLE))
395                data.extend(creator.fromStringList(view.context.history.messages))
396            else:
397                hist_translation = trans(_('Customer Workflow History'), portal_language)
398                data.append(Paragraph(hist_translation, HEADING_STYLE))
399                data.extend(creator.fromStringList(customer.history.messages))
400
401        # Insert content tables (optionally on second page)
402        if hasattr(view, 'tabletitle'):
403            for i in range(len(view.tabletitle)):
404                if tabledata[i] and tableheader[i]:
405                    #data.append(PageBreak())
406                    #data.append(Spacer(1, 20))
407                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
408                    data.append(Spacer(1, 8))
409                    contenttable = render_table_data(tableheader[i],tabledata[i])
410                    data.append(contenttable)
411
412        # Insert signatures
413        # XXX: We are using only sigs_in_footer in waeup.ikoba, so we
414        # do not have a test for the following lines.
415        if signatures and not sigs_in_footer:
416            data.append(Spacer(1, 20))
417            # Render one signature table per signature to
418            # get date and signature in line.
419            for signature in signatures:
420                signaturetables = get_signature_tables(signature)
421                data.append(signaturetables[0])
422
423        view.response.setHeader(
424            'Content-Type', 'application/pdf')
425        try:
426            pdf_stream = creator.create_pdf(
427                data, None, doc_title, author=author, footer=footer_text,
428                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
429        except IOError:
430            view.flash('Error in image file.')
431            return view.redirect(view.url(view.context))
432
433        if mergefiles:
434            merger = PdfFileMerger()
435            merger.append(StringIO(pdf_stream))
436            if watermark:
437                watermark = PdfFileReader(watermark)
438            for file in mergefiles:
439                if watermark:
440                    # Pass through all pages of each file
441                    # and merge with watermark page.
442                    marked_file = PdfFileWriter()
443                    orig_file = PdfFileReader(file[1])
444                    num_pages = orig_file.getNumPages()
445                    for num in range(num_pages):
446                        page = orig_file.getPage(num)
447                        page.mergePage(watermark.getPage(0))
448                        marked_file.addPage(page)
449                    # Save into a file-like object
450                    tmp1 = StringIO()
451                    marked_file.write(tmp1)
452                    # Append the file-like object
453                    merger.append(tmp1)
454                else:
455                    # Just append the file object
456                    merger.append(file[1])
457            # Save into a file-like object
458            tmp2 = StringIO()
459            merger.write(tmp2)
460            return tmp2.getvalue()
461
462        return pdf_stream
Note: See TracBrowser for help on using the repository browser.