#!/usr/bin/env python """ Invoice generator using ReportLab. """ import warnings warnings.filterwarnings("ignore", category=Warning) ## doesn't work damnit import cgi from time import * from cStringIO import StringIO from reportlab.platypus import BaseDocTemplate, SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Frame from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.pagesizes import letter from reportlab.lib.units import inch from reportlab.lib import colors ## from basicproperty import propertied, common, basic ## from sipfe import billutilities BASE_HEADER_STYLE = [ ('FONT',(0,0),(-1,-1),'Times-Roman', 12), ('FONT',(2,0),(2,0),'Times-Roman', 12), ('FONT',(1,0),(1,0),'Times-Roman', 18), ('ALIGN', (1,0), (1,-1), 'CENTER'), ('ALIGN', (2,0), (2,-1), 'RIGHT'), ('VALIGN', (0,0), (-1,-1), 'TOP'), ] HEADER_STYLE = BASE_HEADER_STYLE +[ ('BACKGROUND',(0,0),(-1,2), colors.steelblue), ('TEXTCOLOR',(0,0),(-1,2), colors.white), ('BACKGROUND',(0,2),(-1,-1), colors.azure), ('TEXTCOLOR',(0,2),(-1,-1), colors.black), ] REST_HEADER_STYLE = BASE_HEADER_STYLE + [ ('BACKGROUND',(0,0),(-1,-1), colors.steelblue), ('TEXTCOLOR',(0,0),(-1,-1), colors.white), ] SUMMARY_STYLE = TableStyle([ ('BACKGROUND',(0,0),(-1,-1), colors.whitesmoke), ('TEXTCOLOR',(0,0),(-1,-1), colors.black), ('FONT',(0,0),(-1,-1),'Times-Roman', 12), ('FONT',(1,0),(1,0),'Times-Roman', 18), ('ALIGN', (1,0), (1,-1), 'CENTER'), ('ALIGN', (2,0), (2,-1), 'RIGHT'), ('ALIGN', (1,1), (1,-1), 'RIGHT'), ('LEFTPADDING', (2,0), (2,-1), 8), ('VALIGN', (0,0), (-1,-1), 'TOP'), ('BACKGROUND', (0,-2),(-1,-1), colors.oldlace), ('FONT',(0,-2),(-1,-1),'Times-Bold', 14), ('BOX', (0,0), (-1,-1), 0.50, colors.black), ]) STREAM_STYLE = [ ('BACKGROUND',(0,0),(-1,-1), colors.whitesmoke), ('TEXTCOLOR',(0,0),(-1,-1), colors.black), ('FONT',(0,0),(-1,-1),'Times-Roman', 12), ('FONT',(1,0),(1,0),'Times-Roman', 18), ('ALIGN', (1,0), (1,-1), 'CENTER'), ('ALIGN', (2,0), (2,-1), 'RIGHT'), ('ALIGN', (1,1), (1,-1), 'RIGHT'), ('LEFTPADDING', (2,0), (2,-1), 8), ('VALIGN', (0,0), (-1,-1), 'TOP'), ('BACKGROUND', (0,-2),(-1,-1), colors.oldlace), ('FONT',(0,-2),(-1,-1),'Times-Bold', 14), ] LINEITEMS_STYLE = [ ('SPAN',(0,0),(-1,0)), ('BACKGROUND',(0,0),(-1,0), colors.steelblue), ('TEXTCOLOR',(0,0),(-1,0), colors.white), ('BACKGROUND',(0,1),(-1,1), colors.azure), ('TEXTCOLOR',(0,1),(-1,1), colors.black), ('FONT',(0,0),(-1,-1), 'Times-Roman', 12), ('FONT',(0,1),(-1,1), 'Times-Bold'), ('ALIGN', (2,0), (2,-1), 'RIGHT'), ('VALIGN', (0,0), (-1,-1), 'TOP'), ] CALLITEMS_STYLE = [ ('SPAN',(0,0),(-1,0)), ('BACKGROUND',(0,0),(-1,0), colors.steelblue), ('TEXTCOLOR',(0,0),(-1,0), colors.white), ('FONT',(0,0),(-1,-1), 'Times-Roman', 12), ('FONT',(0,1),(-1,1), 'Times-Bold'), ('ALIGN', (2,0), (2,-1), 'RIGHT'), ('SPAN',(0,0),(-1,0)), ('ALIGN', (-1,0), (-1,-1), 'RIGHT'), ('VALIGN', (0,0), (-1,-1), 'TOP'), ] PAGE_HEIGHT = letter[1] PAGE_WIDTH = letter[0] styles = getSampleStyleSheet() class PDFInvoice(object): """PDF Generator for an Invoice object""" invoice = None """Invoice for which we are generating the printable document""", DATE_FORMAT = '%Y-%m-%d' FULL_DATE_FORMAT = DATE_FORMAT CALL_TIME_FORMAT = '%Y-%m-%d %H:%M' detailStyle = getSampleStyleSheet()['Normal'] def generatePDF(self): """Produce the PDF code for creating the document""" buffer = StringIO() doc = SimpleDocTemplate( buffer, pagesize = letter, leftMargin = 0.5*inch, rightMargin = 0.5*inch, bottomMargin = 1*inch, topMargin = 1*inch, ) Story = [ Spacer(1, 2.0*inch) ] t = self.summaryTable() Story.append(t) ## for stream in self.invoice.streams: ## if len(stream): ## if stream.streamId != billutilities.STREAM_ID_CALLS: ## t = self.streamTable(stream) ## else: ## t = self.callsTable(stream) ## Story.append(Spacer(1, 0.15*inch)) ## Story.append(t) doc.build( Story, ## onFirstPage=self.firstPage, ## onLaterPages=self.restPage, ) pdf = buffer.getvalue() buffer.close() return pdf def summaryTable(self): """Produce the account-summary table for the first page""" data = [('','Account Summary', ''),] if self.invoice.outstandingInvoices: data.append(('','Outstanding Invoices','')) for (date, id, remaining) in self.invoice.outstandingInvoices: data.append( (date.Format(self.FULL_DATE_FORMAT), 'Invoice #%05d'%(id,), str(remaining)) ) data.append(('','Previous Balance', str(self.invoice.outstandingTotal))) data.append(('','This Invoice Total', str(self.invoice.total))) if self.invoice.paymentType in ('INVOICE',None): description = 'Please Pay' elif self.invoice.paymentType == 'PAW': description = 'Your bank account will be automatically debited' elif self.invoice.paymentType == 'PACC': description = 'Your credit card will be automatically charged' else: raise ValueError("""Unknown payment type: %r specified"""%( self.invoice.paymentType, )) data.append(( self.invoice.client.inv_date.Format(self.FULL_DATE_FORMAT), description, str(self.invoice.totalDue) )) colwidths = (1*inch,None, None) t = Table(data, colwidths) t.setStyle(SUMMARY_STYLE) return t def streamTable(self, stream): """Produce a Table describing charges for a particular stream""" style = LINEITEMS_STYLE[:] data = [ (stream.streamName, '',''), ('Date','Detail','Cost'), ] records = list(stream) stopPosition = len(data)+len(records) -1 style.append( ('ROWBACKGROUNDS',(0,len(data)),(-1,stopPosition), [ '#f7f7f7','#f0f0f0', ]) ) for record in records: data.append( ( record.trdate.Format(self.DATE_FORMAT), self.detailAsParagraphs(record.detail), str(record.tramount), ) ) style.extend([ ('BACKGROUND',(0,len(data)),(-1,len(data)+3-1), colors.azure), ('ALIGN',(1,len(data)),(-1,len(data)+3-1), 'RIGHT'), ]) data.append( ('','%s Subtotal'%(stream.streamName,), str(stream.subtotal)) ) data.append( ('','GST', str(stream.gst)) ) data.append( ('','PST', str(stream.pst)) ) if stream.totalDiscount(): style.extend([ ('BACKGROUND',(0,len(data)),(-1,len(data)), colors.azure), ('ALIGN',(1,len(data)),(-1,len(data)), 'RIGHT'), ]) data.append(('','Pre-Discount Total', str(stream.preDiscountTotal))) records = list(stream.iterDiscount()) style.append( ('ROWBACKGROUNDS',(0,len(data)),(-1,len(data)+len(records)-1), [ '#f7f7f7','#f0f0f0', ]) ) for record in records: data.append( ( record.trdate.Format(self.DATE_FORMAT), self.detailAsParagraphs(record.detail), str(record.tramount), ) ) style.extend([ ('BACKGROUND',(0,len(data)),(-1,len(data)), colors.azure), ('ALIGN',(1,len(data)),(-1,len(data)), 'RIGHT'), ]) data.append( ('', '%s Total'%(stream.streamName,), str(stream.total)) ) style = TableStyle(style) colwidths = (1*inch,PAGE_WIDTH-(2*inch+.5*inch), 1*inch) t = Table(data, colwidths) t.setStyle(style) return t def callsTable(self, stream): """Produce a Table describing calls from period for a particular stream""" style = CALLITEMS_STYLE[:] data = [ (stream.streamName, '','','','','',''), ] for account in stream.iterAccounts(): style.extend([ ('SPAN',(0,len(data)),(-1,len(data))), ('BACKGROUND',(0,len(data)),(-1,len(data)), '#f0f0ff'), ('TEXTCOLOR',(0,len(data)),(-1,len(data)), colors.black), ]) data.append( ( 'Account: %s %s (%s@%s)'%( account.firstName,account.lastName, account.sipId['username'], account.sipId['domain'], ), '','','','','','', ) ) style.extend([ ('BACKGROUND',(0,len(data)),(-1,len(data)), colors.azure), ('TEXTCOLOR',(0,len(data)),(-1,len(data)), colors.black), ('SPAN',(1,len(data)),(2,len(data))), ('SPAN',(3,len(data)),(4,len(data))), ]) data.append( ('Date','Destination','','Call Type','','Duration','Cost'), ) calls = list(stream.iterAccount(account)) stopPosition = len(data)+len(calls) -1 style.append( ('ROWBACKGROUNDS',(0,len(data)),(-1,stopPosition), [ '#f7f7f7','#f0f0f0', ]) ) for call in calls: data.append( ( call['call_date'].Format(self.CALL_TIME_FORMAT), stream.formatDestination(call), stream.formatDestinationPlace(call), stream.formatMethod(call), call['call_method_ext'], stream.formatDuration(call), stream.formatCost(call), ) ) style.extend([ ('BACKGROUND',(0,len(data)),(-1,len(data)+3-1), colors.azure), ('ALIGN',(1,len(data)),(1,len(data)+3-1), 'RIGHT'), ] + [ ('SPAN',(1,i),(5,i)) for i in range(len(data), len(data)+3) ]) data.append( ('','%s Subtotal'%(stream.streamName,), '','','','', str(stream.subtotal)) ) data.append( ('','GST', '','','','', str(stream.gst)) ) data.append( ('','PST', '','','','', str(stream.pst)) ) if stream.totalDiscount(): style.extend([ ('SPAN',(1,len(data)),(5,len(data))), ]) data.append(('','Pre-Discount Total', '','','','', str(stream.preDiscountTotal))) discounts = list(stream.iterDiscount()) style.extend([ ('ROWBACKGROUNDS',(0,len(data)),(-1,len(data)+len(discounts)-1), [ '#f7f7f7','#f0f0f0', ]), ] + [ ('SPAN',(1,i),(5,i)) for i in range(len(data), len(data)+len(discounts)-1) ]) for record in discounts: data.append( ( record.trdate.Format(self.DATE_FORMAT), self.detailAsParagraphs(record.detail), '','','','', str(record.tramount), ) ) style.extend([ ('BACKGROUND',(0,len(data)),(-1,len(data)), colors.azure), ('ALIGN',(1,len(data)),(-1,len(data)), 'RIGHT'), ('SPAN',(1,len(data)),(5,len(data))), ]) data.append( ('', '%s Total'%(stream.streamName,), '','','','', str(stream.total)) ) style = TableStyle(style) colwidths = (1.5*inch,1*inch,1.75*inch,1*inch,1*inch,1*inch,.75*inch) t = Table(data, colwidths) t.setStyle(style) return t def firstPage(self, canvas, document): """Produce the first page of the invoice""" canvas.saveState() try: self.drawHeader(canvas, document) self.drawFooter(canvas, document) finally: canvas.restoreState() def restPage(self, canvas, document): """Produce the first page of the invoice""" canvas.saveState() try: self.drawHeader(canvas, document, subHeader=False) self.drawFooter(canvas, document) finally: canvas.restoreState() def drawHeader(self, canvas, document, subHeader=True): """Draw the page-header""" f = Frame( .25*inch, PAGE_HEIGHT-2.75*inch, PAGE_WIDTH-(.5*inch), 2.5*inch, showBoundary=False, ) data = [ ( 'Logo Here', 'Some Cable', 'Page %s'%(canvas.getPageNumber()) ), ] if subHeader: # get the lines for the client's address... clientAddress = [] client = self.invoice.client clientAddress.append('%s %s'%(getattr(client,'fname',''),client.lname)) clientAddress.append(getattr(client, 'address', '') or '') clientAddress.append('%s %s %s'%(client.city,client.prov,client.postal)) for name,property in [ ('Phone','phone1'), ('Alternate Phone','phone2'), ('Mobile','cel'), ('Fax','fax'), ]: if getattr(client, property, None): clientAddress.append('%s: %s'%(name, getattr(client,property))) # now the lines for the invoice meta-information... t2 = Table([ ('Invoice #', '%05d'%(self.invoice.invoice_id,)), ('Terms', client.terms or ''), ('Salesperson', client.rep_id), ('Account #', client.client_id), ], (None,None)) data.extend([ ( '', '''P.O. Box 888 Somewhere, Ontario M5P 3K8 (416) 555-0000''', '' ), ( 'Invoice Date\n%s'%( self.invoice.client.inv_date.Format(self.FULL_DATE_FORMAT), ), "\n".join(clientAddress), t2, ), ]) t = Table(data, (1.5*inch, PAGE_WIDTH-(3*inch + .5*inch), 1.5*inch),) if subHeader: t.setStyle(TableStyle(HEADER_STYLE)) else: t.setStyle(TableStyle(REST_HEADER_STYLE)) f.addFromList([t], canvas) def drawFooter(self, canvas, document): """Draw the page footer""" canvas.setFont('Times-Roman', 9) canvas.drawString(0.5*inch, 0.75*inch, 'Terms and Conditions down here with some nice background GST Number %s'%( self.invoice.gstNumber, )) def detailAsParagraphs(self, detail): """Turn plain-text detail into a Paragraph flowable""" lines = [line.strip() for line in detail.split('\n') if line.strip()] return [ Paragraph( cgi.escape(line), self.detailStyle, ) for line in lines ] invoice = PDFInvoice() f = open('invoice.pdf', 'w') f.write(invoice.generatePDF())