## $Id: utils.py 12339 2014-12-30 09:01:23Z henrik $ ## ## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## """General helper functions and utilities for the customer section. """ import re import os import grok from copy import deepcopy from StringIO import StringIO from reportlab.lib import colors from reportlab.lib.units import cm from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet from reportlab.platypus import Paragraph, Image, Table, Spacer from reportlab.platypus.flowables import PageBreak from PyPDF2 import PdfFileMerger, PdfFileReader, PdfFileWriter from zope.event import notify from zope.schema.interfaces import ConstraintNotSatisfied from zope.component import getUtility, createObject from zope.formlib.form import setUpEditWidgets from zope.i18n import translate from waeup.ikoba.interfaces import MessageFactory as _ from waeup.ikoba.interfaces import ( STARTED, CREATED, REQUESTED, APPROVED, SUBMITTED, VERIFIED, REJECTED, EXPIRED, IIkobaUtils, IExtFileStore) from waeup.ikoba.customers.interfaces import ICustomersUtils from waeup.ikoba.browser.interfaces import IPDFCreator from waeup.ikoba.browser.pdf import ( ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE, get_signature_tables, get_qrcode) RE_CUSTID_NON_NUM = re.compile('[^\d]+') def generate_customer_id(): customers = grok.getSite()['customers'] new_id = customers.unique_customer_id return new_id def path_from_custid(customer_id): """Convert a customer_id into a predictable relative folder path. Used for storing files. Returns the name of folder in which files for a particular customer should be stored. This is a relative path, relative to any general customers folder with 5 zero-padded digits (except when customer_id is overlong). We normally map 1,000 different customer ids into one single path. For instance ``K1000000`` will give ``01000/K1000000``, ``K1234567`` will give ``0123/K1234567`` and ``K12345678`` will result in ``1234/K12345678``. For lower numbers < 10**6 we return the same path for up to 10,000 customer_ids. So for instance ``KM123456`` will result in ``00120/KM123456`` (there will be no path starting with ``00123``). Works also with overlong number: here the leading zeros will be missing but ``K123456789`` will give reliably ``12345/K123456789`` as expected. """ # remove all non numeric characters and turn this into an int. num = int(RE_CUSTID_NON_NUM.sub('', customer_id)) if num < 10**6: # store max. of 10000 custs per folder and correct num for 5 digits num = num / 10000 * 10 else: # store max. of 1000 custs per folder num = num / 1000 # format folder name to have 5 zero-padded digits folder_name = u'%05d' % num folder_name = os.path.join(folder_name, customer_id) return folder_name SLIP_STYLE = [ ('VALIGN',(0,0),(-1,-1),'TOP'), #('FONT', (0,0), (-1,-1), 'Helvetica', 11), ] CONTENT_STYLE = [ ('VALIGN',(0,0),(-1,-1),'TOP'), #('FONT', (0,0), (-1,-1), 'Helvetica', 8), #('TEXTCOLOR',(0,0),(-1,0),colors.white), #('BACKGROUND',(0,0),(-1,0),colors.black), ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), ('BOX', (0,0), (-1,-1), 1, colors.black), ] FONT_SIZE = 10 FONT_COLOR = 'black' def trans(text, lang): # shortcut return translate(text, 'waeup.kofa', target_language=lang) def formatted_text(text, color=FONT_COLOR, lang='en'): """Turn `text`, `color` and `size` into an HTML snippet. The snippet is suitable for use with reportlab and generating PDFs. Wraps the `text` into a ```` tag with passed attributes. Also non-strings are converted. Raw strings are expected to be utf-8 encoded (usually the case for widgets etc.). Finally, a br tag is added if widgets contain div tags which are not supported by reportlab. The returned snippet is unicode type. """ if not isinstance(text, unicode): if isinstance(text, basestring): text = text.decode('utf-8') else: text = unicode(text) if text == 'None': text = '' # Mainly for boolean values we need our customized # localisation of the zope domain text = translate(text, 'zope', target_language=lang) text = text.replace('', '
') tag1 = u'' % (color) return tag1 + u'%s' % text def render_customer_data(customerview, context, omit_fields=(), lang='en', slipname=None): """Render customer table for an existing frame. """ width, height = A4 set_up_widgets(customerview, ignore_request=True) data_left = [] data_middle = [] style = getSampleStyleSheet() img = getUtility(IExtFileStore).getFileByContext( customerview.context, attr='passport.jpg') if img is None: from waeup.ikoba.browser import DEFAULT_PASSPORT_IMAGE_PATH img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb') doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound') data_left.append([doc_img]) #data.append([Spacer(1, 12)]) f_label = trans(_('Name:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text(customerview.context.display_fullname) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) for widget in customerview.widgets: if 'name' in widget.name: continue f_label = translate( widget.label.strip(), 'waeup.ikoba', target_language=lang) f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE) f_text = formatted_text(widget(), lang=lang) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) if getattr(customerview.context, 'certcode', None): if not 'date_of_birth' in omit_fields: f_label = trans(_('Date of Birth:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) date_of_birth = customerview.context.date_of_birth tz = getUtility(IIkobaUtils).tzinfo date_of_birth = to_timezone(date_of_birth, tz) if date_of_birth is not None: date_of_birth = date_of_birth.strftime("%d/%m/%Y") f_text = formatted_text(date_of_birth) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) # append QR code to the right if slipname: url = customerview.url(context, slipname) data_right = [[get_qrcode(url, width=70.0)]] table_right = Table(data_right,style=SLIP_STYLE) else: table_right = None table_left = Table(data_left,style=SLIP_STYLE) table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm]) table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE) return table def render_table_data(tableheader, tabledata, lang='en'): """Render children table for an existing frame. """ data = [] #data.append([Spacer(1, 12)]) line = [] style = getSampleStyleSheet() for element in tableheader: field = '%s' % formatted_text(element[0], lang=lang) field = Paragraph(field, style["Normal"]) line.append(field) data.append(line) for ticket in tabledata: line = [] for element in tableheader: field = formatted_text(getattr(ticket,element[1],u' ')) field = Paragraph(field, style["Normal"]) line.append(field) data.append(line) table = Table(data,colWidths=[ element[2]*cm for element in tableheader], style=CONTENT_STYLE) return table def docs_as_flowables(view, lang='en'): """Create reportlab flowables out of scanned docs. """ # XXX: fix circular import problem from waeup.ikoba.browser.fileviewlets import FileManager from waeup.ikoba.browser import DEFAULT_IMAGE_PATH style = getSampleStyleSheet() data = [] # Collect viewlets fm = FileManager(view.context, view.request, view) fm.update() if fm.viewlets: sc_translation = trans(_('Connected Files'), lang) data.append(Paragraph(sc_translation, HEADING_STYLE)) # Insert list of scanned documents table_data = [] for viewlet in fm.viewlets: if viewlet.file_exists: # Show viewlet only if file exists f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE) img_path = getattr(getUtility(IExtFileStore).getFileByContext( view.context, attr=viewlet.download_name), 'name', None) #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE) if img_path is None: pass elif img_path[-4:] == '.pdf': f_text = Paragraph('%s (see file attached)' % ( viewlet.title,), ENTRY1_STYLE) elif not img_path[-4:] in ('.jpg', '.JPG'): # reportlab requires jpg images, I think. f_text = Paragraph('%s (no preview available)' % ( viewlet.title,), ENTRY1_STYLE) else: f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound') table_data.append([f_label, f_text]) if table_data: # safety belt; empty tables lead to problems. data.append(Table(table_data, style=SLIP_STYLE)) return data def set_up_widgets(view, ignore_request=False): view.adapters = {} view.widgets = setUpEditWidgets( view.form_fields, view.prefix, view.context, view.request, adapters=view.adapters, for_display=True, ignore_request=ignore_request ) def generate_contract_id(): new_id = grok.getSite().unique_contract_id return new_id class CustomersUtils(grok.GlobalUtility): """A collection of methods subject to customization. """ grok.implements(ICustomersUtils) #: A prefix used when generating new customer ids. Each customer id will #: start with this string. The default is 'K'. CUSTOMER_ID_PREFIX = u'K' CUSTMANAGE_CUSTOMER_STATES = (STARTED,) DOCMANAGE_CUSTOMER_STATES = (APPROVED,) DOCMANAGE_DOCUMENT_STATES = (CREATED,) CONMANAGE_CUSTOMER_STATES = deepcopy(DOCMANAGE_CUSTOMER_STATES) CONMANAGE_CONTRACT_STATES = (CREATED,) TRANSLATED_CUSTOMER_STATES = { CREATED: _('created'), STARTED: _('started'), REQUESTED: _('requested'), APPROVED: _('approved'), } TRANSLATED_CONTRACT_STATES = { CREATED: _('created'), SUBMITTED: _('submitted for approval'), APPROVED: _('approved'), REJECTED: _('rejected'), EXPIRED:_('expired') } TRANSLATED_DOCUMENT_STATES = { CREATED: _('created'), SUBMITTED: _('submitted for verification'), VERIFIED: _('verified'), REJECTED: _('rejected'), EXPIRED:_('expired') } DOCTYPES_DICT = { 'CustomerSampleDocument': _('Sample Document'), 'CustomerPDFDocument': _('PDF Document'), } CONTYPES_DICT = { 'SampleContract': _('Sample Contract'), } SELECTABLE_DOCTYPES_DICT = deepcopy(DOCTYPES_DICT) SELECTABLE_CONTYPES_DICT = deepcopy(CONTYPES_DICT) EXPORTER_NAMES = ('customers', 'customersampledocuments', 'samplecontracts') def getPDFCreator(self, context=None): """Get a pdf creator suitable for `context`. The default implementation always returns the default creator. """ return getUtility(IPDFCreator) def renderPDF(self, view, filename='slip.pdf', customer=None, customerview=None, tableheader=[], tabledata=[], note=None, signatures=None, sigs_in_footer=(), show_scans=True, show_history=True, topMargin=1.5, omit_fields=(), mergefiles=None, watermark=None): """Render pdf slips for various pages. """ portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE style = getSampleStyleSheet() creator = self.getPDFCreator() data = [] doc_title = view.label author = '%s (%s)' % (view.request.principal.title, view.request.principal.id) footer_text = view.label.split('\n') if len(footer_text) > 2: # We can add a department in first line footer_text = footer_text[1] else: # Only the first line is used for the footer footer_text = footer_text[0] if getattr(customer, 'customer_id', None) is not None: footer_text = "%s - %s - " % (customer.customer_id, footer_text) # Insert customer data table if customer is not None: bd_translation = trans(_('Base Data'), portal_language) data.append(Paragraph(bd_translation, HEADING_STYLE)) data.append(render_customer_data( customerview, view.context, omit_fields, lang=portal_language, slipname=filename)) # Insert widgets if view.form_fields: data.append(Paragraph(view.title, HEADING_STYLE)) separators = getattr(self, 'SEPARATORS_DICT', {}) table = creator.getWidgetsTable( view.form_fields, view.context, None, lang=portal_language, separators=separators) data.append(table) # Insert scanned docs if show_scans: data.extend(docs_as_flowables(view, portal_language)) # Insert history if show_history: if getattr(view.context, 'history', None): hist_translation = trans(_('${a} Workflow History', mapping = {'a':view.context.translated_class_name}), portal_language) data.append(Paragraph(hist_translation, HEADING_STYLE)) data.extend(creator.fromStringList(view.context.history.messages)) else: hist_translation = trans(_('Customer Workflow History'), portal_language) data.append(Paragraph(hist_translation, HEADING_STYLE)) data.extend(creator.fromStringList(customer.history.messages)) # Insert content tables (optionally on second page) if hasattr(view, 'tabletitle'): for i in range(len(view.tabletitle)): if tabledata[i] and tableheader[i]: #data.append(PageBreak()) #data.append(Spacer(1, 20)) data.append(Paragraph(view.tabletitle[i], HEADING_STYLE)) data.append(Spacer(1, 8)) contenttable = render_table_data(tableheader[i],tabledata[i]) data.append(contenttable) # Insert signatures # XXX: We are using only sigs_in_footer in waeup.ikoba, so we # do not have a test for the following lines. if signatures and not sigs_in_footer: data.append(Spacer(1, 20)) # Render one signature table per signature to # get date and signature in line. for signature in signatures: signaturetables = get_signature_tables(signature) data.append(signaturetables[0]) view.response.setHeader( 'Content-Type', 'application/pdf') try: pdf_stream = creator.create_pdf( data, None, doc_title, author=author, footer=footer_text, note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin) except IOError: view.flash('Error in image file.') return view.redirect(view.url(view.context)) if mergefiles: merger = PdfFileMerger() merger.append(StringIO(pdf_stream)) if watermark: watermark = PdfFileReader(watermark) for file in mergefiles: if watermark: # Pass through all pages of each file # and merge with watermark page. marked_file = PdfFileWriter() orig_file = PdfFileReader(file[1]) num_pages = orig_file.getNumPages() for num in range(num_pages): page = orig_file.getPage(num) page.mergePage(watermark.getPage(0)) marked_file.addPage(page) # Save into a file-like object tmp1 = StringIO() marked_file.write(tmp1) # Append the file-like object merger.append(tmp1) else: # Just append the file object merger.append(file[1]) # Save into a file-like object tmp2 = StringIO() merger.write(tmp2) return tmp2.getvalue() return pdf_stream