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

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

Renaming batch 3

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