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

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

Extend contract workflow to integrate payment.

Prepare (empty) page to select payment method and finally create a payment object.

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